mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
Refactor room data blob handling and permissions
- Updated the RoomsController to utilize synthetic CDN keys for data blobs. - Introduced methods for determining current or synthetic data blob names. - Adjusted room role permissions to align with new game role definitions. - Enhanced the SanitizeController with a new endpoint for purity checks. - Modified Match and PlayerPresence services to handle deferred presence commits. - Improved RoomDataBlobService to support new game role and tool tag settings. - Added support for boot dorm grace period in PlayerPresenceService. - Updated protobuf definitions to reflect new room role and game role structures. You are now able to go into dorm and the rec center!
This commit is contained in:
+321
-13
@@ -49,6 +49,13 @@ public class Mod : MelonMod
|
|||||||
private readonly HashSet<string> _diagnosticPatchLabels = new();
|
private readonly HashSet<string> _diagnosticPatchLabels = new();
|
||||||
private bool _networkPatchesRegistered;
|
private bool _networkPatchesRegistered;
|
||||||
private bool _joinTracePatchesRegistered;
|
private bool _joinTracePatchesRegistered;
|
||||||
|
private bool _quitTraceCoreRegistered;
|
||||||
|
private bool _quitTraceGameComplete;
|
||||||
|
private int _quitTraceRetryFrame;
|
||||||
|
private bool _antiTamperCallbackPatchesComplete;
|
||||||
|
private int _antiTamperCallbackRetryFrame;
|
||||||
|
private bool _toxModPatchesComplete;
|
||||||
|
private int _toxModRetryFrame;
|
||||||
private bool _diagnosticCoreRegistered;
|
private bool _diagnosticCoreRegistered;
|
||||||
private bool _diagnosticGameComplete;
|
private bool _diagnosticGameComplete;
|
||||||
private bool _firstUpdateLogged;
|
private bool _firstUpdateLogged;
|
||||||
@@ -178,6 +185,9 @@ public class Mod : MelonMod
|
|||||||
"NotifyServerCertificate",
|
"NotifyServerCertificate",
|
||||||
prefix: nameof(TlsPatches.NotifyServerCertificate_Prefix));
|
prefix: nameof(TlsPatches.NotifyServerCertificate_Prefix));
|
||||||
}
|
}
|
||||||
|
RegisterQuitTracePatches(logMisses: true);
|
||||||
|
RegisterAntiTamperCallbackPatches(logMisses: true);
|
||||||
|
RegisterToxModPatches(logMisses: true);
|
||||||
PatchFileHashCheckerCallback();
|
PatchFileHashCheckerCallback();
|
||||||
// Post-save blob-name refresh. The 2020 watch's
|
// Post-save blob-name refresh. The 2020 watch's
|
||||||
// RoomPersistenceManager.RoomDataBlobName is only ever set by
|
// RoomPersistenceManager.RoomDataBlobName is only ever set by
|
||||||
@@ -480,6 +490,93 @@ public class Mod : MelonMod
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterAntiTamperCallbackPatches(bool logMisses)
|
||||||
|
{
|
||||||
|
var complete = true;
|
||||||
|
complete &= PatchCallbackMethod(
|
||||||
|
"hile warning callback",
|
||||||
|
type => TypeNameContains(type, "FPIBGPIAOBI") && TypeNameContains(type, "KHIIHOIFDMP"),
|
||||||
|
method => MethodNameContains(method, "CreateHileWarning") && MethodNameContains(method, "b__0"),
|
||||||
|
nameof(AntiTamperPatches.CreateHileWarningCallback_Prefix),
|
||||||
|
logMisses);
|
||||||
|
complete &= PatchCallbackMethod(
|
||||||
|
"unknown DLL callback",
|
||||||
|
type => TypeNameContains(type, "CheatManager") && TypeNameContains(type, "PNHDGEKCJCH"),
|
||||||
|
method => MethodNameContains(method, "OnUnknownDLLDetected") && MethodNameContains(method, "b__0"),
|
||||||
|
nameof(AntiTamperPatches.UnknownDllDetectedCallback_Prefix),
|
||||||
|
logMisses);
|
||||||
|
_antiTamperCallbackPatchesComplete = complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterToxModPatches(bool logMisses)
|
||||||
|
{
|
||||||
|
var complete = true;
|
||||||
|
complete &= TryPatchDiagnosticByName("ToxMod.ToxModVoiceComponent",
|
||||||
|
"CanInitializeToxMod",
|
||||||
|
args: Type.EmptyTypes,
|
||||||
|
prefix: nameof(ToxModPatches.CanInitializeToxMod_Prefix),
|
||||||
|
logMiss: logMisses);
|
||||||
|
complete &= TryPatchDiagnosticByName("ToxMod.ToxModVoiceComponent",
|
||||||
|
"PhotonVoiceCreated",
|
||||||
|
prefix: nameof(ToxModPatches.PhotonVoiceCreated_Prefix),
|
||||||
|
logMiss: logMisses);
|
||||||
|
_toxModPatchesComplete = complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PatchCallbackMethod(string label,
|
||||||
|
Func<Type, bool> typePredicate,
|
||||||
|
Func<MethodInfo, bool> methodPredicate,
|
||||||
|
string prefix,
|
||||||
|
bool logMiss)
|
||||||
|
{
|
||||||
|
if (_diagnosticPatchLabels.Contains(label))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var prefixMethod = GetPatchMethod(prefix);
|
||||||
|
var patched = 0;
|
||||||
|
|
||||||
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
foreach (var type in GetLoadableTypes(asm))
|
||||||
|
{
|
||||||
|
if (!typePredicate(type)) continue;
|
||||||
|
foreach (var method in AccessTools.GetDeclaredMethods(type))
|
||||||
|
{
|
||||||
|
if (method.IsAbstract || method.ContainsGenericParameters) continue;
|
||||||
|
if (!methodPredicate(method)) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HarmonyInstance.Patch(method, prefix: new HarmonyMethod(prefixMethod));
|
||||||
|
patched++;
|
||||||
|
Log.Msg($"[patch-ok] {label} {type.FullName}.{method.Name}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning($"[patch-fail] {label} {type.FullName}.{method.Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patched == 0)
|
||||||
|
{
|
||||||
|
if (logMiss) Log.Warning($"[patch-miss] {label}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_diagnosticPatchLabels.Add(label);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TypeNameContains(Type type, string value)
|
||||||
|
{
|
||||||
|
var fullName = type.FullName ?? type.Name;
|
||||||
|
return fullName.Contains(value, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MethodNameContains(MethodInfo method, string value)
|
||||||
|
=> method.Name.Contains(value, StringComparison.Ordinal);
|
||||||
|
|
||||||
private static bool MatchesFileHashCheckerCallbackType(Type type)
|
private static bool MatchesFileHashCheckerCallbackType(Type type)
|
||||||
{
|
{
|
||||||
var fullName = type.FullName ?? type.Name;
|
var fullName = type.FullName ?? type.Name;
|
||||||
@@ -616,6 +713,24 @@ public class Mod : MelonMod
|
|||||||
Log.Msg("[lifecycle] first OnUpdate tick (Unity frame loop is live)");
|
Log.Msg("[lifecycle] first OnUpdate tick (Unity frame loop is live)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_quitTraceGameComplete && ++_quitTraceRetryFrame >= 300)
|
||||||
|
{
|
||||||
|
_quitTraceRetryFrame = 0;
|
||||||
|
RegisterQuitTracePatches(logMisses: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_antiTamperCallbackPatchesComplete && ++_antiTamperCallbackRetryFrame >= 300)
|
||||||
|
{
|
||||||
|
_antiTamperCallbackRetryFrame = 0;
|
||||||
|
RegisterAntiTamperCallbackPatches(logMisses: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_toxModPatchesComplete && ++_toxModRetryFrame >= 300)
|
||||||
|
{
|
||||||
|
_toxModRetryFrame = 0;
|
||||||
|
RegisterToxModPatches(logMisses: false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnApplicationQuit()
|
public override void OnApplicationQuit()
|
||||||
@@ -921,7 +1036,7 @@ public class Mod : MelonMod
|
|||||||
{
|
{
|
||||||
// Look in all three patch holder classes — small enough that a
|
// Look in all three patch holder classes — small enough that a
|
||||||
// linear scan is cheaper than per-class lookups.
|
// linear scan is cheaper than per-class lookups.
|
||||||
foreach (var holder in new[] { typeof(UriPatches), typeof(HttpTracePatches), typeof(AuthPatches), typeof(AntiCheatPatches), typeof(FileHashCheckerPatches), typeof(TlsPatches), 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(DebugConsolePatches), typeof(ScreenSharePatches), typeof(MakerPenGiftPreviewPatches), typeof(QuestTeamSize) })
|
||||||
{
|
{
|
||||||
var m = holder.GetMethod(name, BindingFlags.Public | BindingFlags.Static);
|
var m = holder.GetMethod(name, BindingFlags.Public | BindingFlags.Static);
|
||||||
if (m is not null) return m;
|
if (m is not null) return m;
|
||||||
@@ -929,6 +1044,41 @@ public class Mod : MelonMod
|
|||||||
throw new MissingMethodException($"patch method '{name}' not found on any holder");
|
throw new MissingMethodException($"patch method '{name}' not found on any holder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterQuitTracePatches(bool logMisses)
|
||||||
|
{
|
||||||
|
if (!_quitTraceCoreRegistered)
|
||||||
|
{
|
||||||
|
TryPatchByName("UnityEngine.Application", "Quit", Type.EmptyTypes,
|
||||||
|
prefix: nameof(QuitTracePatches.ApplicationQuit_Prefix),
|
||||||
|
logMiss: logMisses);
|
||||||
|
TryPatchByName("UnityEngine.Application", "Quit", new[] { typeof(int) },
|
||||||
|
prefix: nameof(QuitTracePatches.ApplicationQuitInt_Prefix),
|
||||||
|
logMiss: logMisses);
|
||||||
|
_quitTraceCoreRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
complete &= TryPatchDiagnosticByName("OPEOLPDLEBO", "DGHJGMBNHCL",
|
||||||
|
prefix: nameof(QuitTracePatches.CreateExitTask_Prefix),
|
||||||
|
logMiss: logMisses);
|
||||||
|
|
||||||
|
_quitTraceGameComplete = complete;
|
||||||
|
}
|
||||||
|
|
||||||
private void RegisterDiagnostics()
|
private void RegisterDiagnostics()
|
||||||
{
|
{
|
||||||
Log.Msg("=== Registering diagnostics ===");
|
Log.Msg("=== Registering diagnostics ===");
|
||||||
@@ -1326,7 +1476,10 @@ internal static class RoomLoadDiagnostics
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.Add($"{field.Name}={DescribeValue(value, 400)}");
|
var described = DescribeValue(value, 400);
|
||||||
|
if (ShouldExpandField(field, value))
|
||||||
|
described += " " + DumpNestedFields(value, 16);
|
||||||
|
parts.Add($"{field.Name}={described}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $"{type.FullName} {{{string.Join("; ", parts)}}}";
|
return $"{type.FullName} {{{string.Join("; ", parts)}}}";
|
||||||
@@ -1337,6 +1490,58 @@ internal static class RoomLoadDiagnostics
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldExpandField(FieldInfo field, object? value)
|
||||||
|
{
|
||||||
|
if (value is null) return false;
|
||||||
|
if (value is string) return false;
|
||||||
|
|
||||||
|
var valueType = value.GetType();
|
||||||
|
if (valueType.IsPrimitive || valueType.IsEnum || value is decimal)
|
||||||
|
return false;
|
||||||
|
if (value is IEnumerable)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var fieldName = field.Name ?? string.Empty;
|
||||||
|
var typeName = valueType.FullName ?? valueType.Name;
|
||||||
|
return fieldName.IndexOf("this", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||||
|
typeName.IndexOf("OMJKHJLFOCO", StringComparison.Ordinal) >= 0 ||
|
||||||
|
typeName.IndexOf("RoomPermissions", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DumpNestedFields(object value, int maxFields)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var type = value.GetType();
|
||||||
|
var parts = new List<string>();
|
||||||
|
const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||||
|
foreach (var field in type.GetFields(flags))
|
||||||
|
{
|
||||||
|
if (parts.Count >= maxFields)
|
||||||
|
{
|
||||||
|
parts.Add("...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
object? nested;
|
||||||
|
try { nested = field.GetValue(value); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
parts.Add($"{field.Name}=<read failed {ex.GetType().Name}>");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.Add($"{field.Name}={DescribeValue(nested, 220)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{nested " + string.Join("; ", parts) + "}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"{{nested dump failed {ex.GetType().Name}: {Sanitize(ex.Message, 220)}}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string DescribeValue(object? value, int limit)
|
private static string DescribeValue(object? value, int limit)
|
||||||
{
|
{
|
||||||
if (value is null) return "null";
|
if (value is null) return "null";
|
||||||
@@ -3287,6 +3492,21 @@ internal static class AntiCheatPatches
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class AntiTamperPatches
|
||||||
|
{
|
||||||
|
public static bool CreateHileWarningCallback_Prefix(bool shouldQuit)
|
||||||
|
{
|
||||||
|
Mod.Log.Msg($"[hile] suppressed warning callback shouldQuit={shouldQuit}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool UnknownDllDetectedCallback_Prefix()
|
||||||
|
{
|
||||||
|
Mod.Log.Msg("[cheatmanager] suppressed unknown DLL fatal callback");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static class FileHashCheckerPatches
|
internal static class FileHashCheckerPatches
|
||||||
{
|
{
|
||||||
public static bool InitializeCallback_Prefix(object? result)
|
public static bool InitializeCallback_Prefix(object? result)
|
||||||
@@ -3296,6 +3516,22 @@ internal static class FileHashCheckerPatches
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class ToxModPatches
|
||||||
|
{
|
||||||
|
public static bool CanInitializeToxMod_Prefix(ref bool __result)
|
||||||
|
{
|
||||||
|
__result = false;
|
||||||
|
Mod.Log.Msg("[toxmod] disabled local ToxMod init");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool PhotonVoiceCreated_Prefix()
|
||||||
|
{
|
||||||
|
Mod.Log.Msg("[toxmod] skipped PhotonVoiceCreated");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static class TlsPatches
|
internal static class TlsPatches
|
||||||
{
|
{
|
||||||
// Both BouncyCastle TLS authenticators have the same `void
|
// Both BouncyCastle TLS authenticators have the same `void
|
||||||
@@ -3309,6 +3545,83 @@ internal static class TlsPatches
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class QuitTracePatches
|
||||||
|
{
|
||||||
|
public static bool ApplicationQuit_Prefix()
|
||||||
|
{
|
||||||
|
Write("[quit-trace] UnityEngine.Application.Quit()");
|
||||||
|
WriteStack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ApplicationQuitInt_Prefix(int exitCode)
|
||||||
|
{
|
||||||
|
Write($"[quit-trace] UnityEngine.Application.Quit({exitCode})");
|
||||||
|
WriteStack();
|
||||||
|
if (exitCode == 1550502249 || exitCode == 533223478)
|
||||||
|
{
|
||||||
|
Write($"[quit-blocked] UnityEngine.Application.Quit fatal client exit {exitCode}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SessionManagerTryApplicationQuit_Prefix(MethodBase __originalMethod)
|
||||||
|
{
|
||||||
|
Write($"[quit-trace] {FormatOriginal(__originalMethod)}");
|
||||||
|
WriteStack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SessionManagerTryApplicationQuitInt_Prefix(int DKOGJICFAEP, MethodBase __originalMethod)
|
||||||
|
{
|
||||||
|
Write($"[quit-trace] {FormatOriginal(__originalMethod)}({DKOGJICFAEP})");
|
||||||
|
WriteStack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SessionManagerFatalApplicationQuit_Prefix(int DKOGJICFAEP, string EPAIMCEIMPA, MethodBase __originalMethod)
|
||||||
|
{
|
||||||
|
Write($"[quit-trace] {FormatOriginal(__originalMethod)}({DKOGJICFAEP}, {EPAIMCEIMPA})");
|
||||||
|
WriteStack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApplicationWantsToQuit_Prefix(MethodBase __originalMethod)
|
||||||
|
{
|
||||||
|
Write($"[quit-trace] {FormatOriginal(__originalMethod)} wants-to-quit handler entered");
|
||||||
|
WriteStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateExitTask_Prefix(MethodBase __originalMethod)
|
||||||
|
{
|
||||||
|
Write($"[quit-trace] {FormatOriginal(__originalMethod)} creating exit task");
|
||||||
|
WriteStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatOriginal(MethodBase method)
|
||||||
|
{
|
||||||
|
return $"{method.DeclaringType?.FullName}.{method.Name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Write(string message)
|
||||||
|
{
|
||||||
|
try { Mod.Log.Msg(message); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteStack()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Write("[quit-trace-stack] " + new StackTrace(skipFrames: 2, fNeedFileInfo: false));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Write("[quit-trace-stack] failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static class DiagnosticPatches
|
internal static class DiagnosticPatches
|
||||||
{
|
{
|
||||||
public static void Write(string message)
|
public static void Write(string message)
|
||||||
@@ -3334,11 +3647,6 @@ internal static class DiagnosticPatches
|
|||||||
{
|
{
|
||||||
Write($"[quit] UnityEngine.Application.Quit({exitCode})");
|
Write($"[quit] UnityEngine.Application.Quit({exitCode})");
|
||||||
WriteStack();
|
WriteStack();
|
||||||
if (exitCode == 1550502249 || exitCode == 533223478)
|
|
||||||
{
|
|
||||||
Write($"[quit-blocked] UnityEngine.Application.Quit fatal client exit {exitCode}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3356,23 +3664,23 @@ internal static class DiagnosticPatches
|
|||||||
|
|
||||||
public static bool SessionManagerTryApplicationQuit_Prefix(MethodBase __originalMethod)
|
public static bool SessionManagerTryApplicationQuit_Prefix(MethodBase __originalMethod)
|
||||||
{
|
{
|
||||||
Write($"[quit-blocked] {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name}");
|
Write($"[quit-trace] {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name}");
|
||||||
WriteStack();
|
WriteStack();
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool SessionManagerTryApplicationQuitInt_Prefix(int DKOGJICFAEP, MethodBase __originalMethod)
|
public static bool SessionManagerTryApplicationQuitInt_Prefix(int DKOGJICFAEP, MethodBase __originalMethod)
|
||||||
{
|
{
|
||||||
Write($"[quit-blocked] {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name}({DKOGJICFAEP})");
|
Write($"[quit-trace] {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name}({DKOGJICFAEP})");
|
||||||
WriteStack();
|
WriteStack();
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool SessionManagerFatalApplicationQuit_Prefix(int DKOGJICFAEP, string EPAIMCEIMPA, MethodBase __originalMethod)
|
public static bool SessionManagerFatalApplicationQuit_Prefix(int DKOGJICFAEP, string EPAIMCEIMPA, MethodBase __originalMethod)
|
||||||
{
|
{
|
||||||
Write($"[quit-blocked] {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name}({DKOGJICFAEP}, {EPAIMCEIMPA})");
|
Write($"[quit-trace] {__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name}({DKOGJICFAEP}, {EPAIMCEIMPA})");
|
||||||
WriteStack();
|
WriteStack();
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteStack()
|
private static void WriteStack()
|
||||||
|
|||||||
@@ -1185,15 +1185,13 @@ public class RoomsController(
|
|||||||
// proceeds with default-zero permissions.
|
// proceeds with default-zero permissions.
|
||||||
//
|
//
|
||||||
// If a room has a saved blob, use it. Otherwise point at
|
// If a room has a saved blob, use it. Otherwise point at
|
||||||
// room_<id>_v1.dat, which misses S3 and is served by
|
// a synthetic CDN key, which misses S3 and is served by
|
||||||
// RoomDataBlobService.GetDefaultBlob. That default is a captured
|
// RoomDataBlobService.GetDefaultBlob. That default is a captured
|
||||||
// PersistedRoomData (v38); the older synthetic role-only blob and
|
// PersistedRoomData (v38); the older synthetic role-only blob and
|
||||||
// empty DataBlobName path both leave the 2023 room-permissions
|
// empty DataBlobName path both leave the 2023 room-permissions
|
||||||
// runtime without data it dereferences during spawn.
|
// runtime without data it dereferences during spawn.
|
||||||
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
||||||
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
var dataBlobName = CurrentOrSyntheticDataBlobName(room);
|
||||||
? room.CurrentDataBlobName
|
|
||||||
: $"room_{room.Id}_v1.dat";
|
|
||||||
|
|
||||||
// Multi-scene rooms (imported via tools/import-room.ps1) carry one
|
// Multi-scene rooms (imported via tools/import-room.ps1) carry one
|
||||||
// RoomSceneEntity row per sub-room. Emit the full Scenes[] array
|
// RoomSceneEntity row per sub-room. Emit the full Scenes[] array
|
||||||
@@ -1215,9 +1213,7 @@ public class RoomsController(
|
|||||||
DataBlobName = !string.IsNullOrWhiteSpace(overrideDataBlobName) &&
|
DataBlobName = !string.IsNullOrWhiteSpace(overrideDataBlobName) &&
|
||||||
(overrideSceneId == s.OrderIndex || sceneRows.Count == 1)
|
(overrideSceneId == s.OrderIndex || sceneRows.Count == 1)
|
||||||
? overrideDataBlobName
|
? overrideDataBlobName
|
||||||
: !string.IsNullOrWhiteSpace(s.DataBlobName)
|
: SceneOrSyntheticDataBlobName(room, s.DataBlobName, dataBlobName),
|
||||||
? s.DataBlobName
|
|
||||||
: dataBlobName,
|
|
||||||
MaxPlayers = s.MaxPlayers,
|
MaxPlayers = s.MaxPlayers,
|
||||||
CanMatchmakeInto = s.CanMatchmakeInto,
|
CanMatchmakeInto = s.CanMatchmakeInto,
|
||||||
DataModifiedAt = (s.DataModifiedAt == default ? DateTime.UtcNow : s.DataModifiedAt)
|
DataModifiedAt = (s.DataModifiedAt == default ? DateTime.UtcNow : s.DataModifiedAt)
|
||||||
@@ -1309,10 +1305,8 @@ public class RoomsController(
|
|||||||
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
||||||
// 2023's room-permissions runtime needs a real PersistedRoomData
|
// 2023's room-permissions runtime needs a real PersistedRoomData
|
||||||
// source during spawn. If no saved blob exists, use the captured
|
// source during spawn. If no saved blob exists, use the captured
|
||||||
// default via the synthetic room_<id>_v1.dat CDN path.
|
// default via the synthetic CDN path.
|
||||||
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
var dataBlobName = CurrentOrSyntheticDataBlobName(room);
|
||||||
? room.CurrentDataBlobName
|
|
||||||
: $"room_{room.Id}_v1.dat";
|
|
||||||
var updatedAt = (room.UpdatedAt == default ? DateTime.UtcNow : room.UpdatedAt)
|
var updatedAt = (room.UpdatedAt == default ? DateTime.UtcNow : room.UpdatedAt)
|
||||||
.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||||
|
|
||||||
@@ -1324,9 +1318,7 @@ public class RoomsController(
|
|||||||
Name = s.Name,
|
Name = s.Name,
|
||||||
DataBlob = !string.IsNullOrWhiteSpace(overrideDataBlobName) && sceneRows.Count == 1
|
DataBlob = !string.IsNullOrWhiteSpace(overrideDataBlobName) && sceneRows.Count == 1
|
||||||
? overrideDataBlobName
|
? overrideDataBlobName
|
||||||
: !string.IsNullOrWhiteSpace(s.DataBlobName)
|
: SceneOrSyntheticDataBlobName(room, s.DataBlobName, dataBlobName),
|
||||||
? s.DataBlobName
|
|
||||||
: dataBlobName,
|
|
||||||
DataSavedAt = (s.DataModifiedAt == default ? DateTime.UtcNow : s.DataModifiedAt)
|
DataSavedAt = (s.DataModifiedAt == default ? DateTime.UtcNow : s.DataModifiedAt)
|
||||||
.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
||||||
IsSandbox = s.IsSandbox,
|
IsSandbox = s.IsSandbox,
|
||||||
@@ -1352,7 +1344,7 @@ public class RoomsController(
|
|||||||
|
|
||||||
var wireRoles = new List<object>
|
var wireRoles = new List<object>
|
||||||
{
|
{
|
||||||
BuildRoomAccountRoleWire(room.CreatorPlayerId, 255),
|
BuildRoomAccountRoleWire(room.CreatorPlayerId, 30),
|
||||||
};
|
};
|
||||||
wireRoles.AddRange(roleList.Select(BuildRoomRoleGrantWire));
|
wireRoles.AddRange(roleList.Select(BuildRoomRoleGrantWire));
|
||||||
|
|
||||||
@@ -1522,6 +1514,7 @@ public class RoomsController(
|
|||||||
[HttpGet("api/rooms/v2/myrecent")]
|
[HttpGet("api/rooms/v2/myrecent")]
|
||||||
public IActionResult MyOtherTabs() => Ok(Array.Empty<object>());
|
public IActionResult MyOtherTabs() => Ok(Array.Empty<object>());
|
||||||
|
|
||||||
|
[HttpGet("player_room_data/{roomId:long}")]
|
||||||
[HttpGet("rooms/{roomId:long}/playerdata/me")]
|
[HttpGet("rooms/{roomId:long}/playerdata/me")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> PlayerDataForMe(long roomId)
|
public async Task<IActionResult> PlayerDataForMe(long roomId)
|
||||||
@@ -1545,6 +1538,7 @@ public class RoomsController(
|
|||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
|
Data = "CAE=",
|
||||||
RoomId = room?.Id ?? roomId,
|
RoomId = room?.Id ?? roomId,
|
||||||
PlayerId = pid.Value,
|
PlayerId = pid.Value,
|
||||||
Favorite = favorited,
|
Favorite = favorited,
|
||||||
@@ -2690,19 +2684,38 @@ public class RoomsController(
|
|||||||
var rows = await db.RoomRoles.Where(r => r.RoomId == roomId).ToListAsync();
|
var rows = await db.RoomRoles.Where(r => r.RoomId == roomId).ToListAsync();
|
||||||
var list = new List<object>(rows.Count + 1)
|
var list = new List<object>(rows.Count + 1)
|
||||||
{
|
{
|
||||||
BuildRoomAccountRoleWire(room.CreatorPlayerId, 255),
|
BuildRoomAccountRoleWire(room.CreatorPlayerId, 30),
|
||||||
};
|
};
|
||||||
list.AddRange(rows.Select(BuildRoomRoleGrantWire));
|
list.AddRange(rows.Select(BuildRoomRoleGrantWire));
|
||||||
return Ok(list);
|
return Ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string CurrentOrSyntheticDataBlobName(RoomEntity room)
|
||||||
|
{
|
||||||
|
if (room.IsDormRoom)
|
||||||
|
return RoomService.ResolveWireRoomDataBlobName(room.Id, room.CurrentDataBlobName);
|
||||||
|
|
||||||
|
return !string.IsNullOrWhiteSpace(room.CurrentDataBlobName)
|
||||||
|
? room.CurrentDataBlobName
|
||||||
|
: RoomService.SyntheticDefaultRoomDataBlobName(room.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SceneOrSyntheticDataBlobName(RoomEntity room, string? sceneBlobName, string fallbackBlobName)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(sceneBlobName) &&
|
||||||
|
!(room.IsDormRoom && RoomService.IsLegacySyntheticDefaultRoomDataBlobName(room.Id, sceneBlobName)))
|
||||||
|
return sceneBlobName;
|
||||||
|
|
||||||
|
return fallbackBlobName;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("rooms/{roomId:long}/roles/{playerId:long}")]
|
[HttpGet("rooms/{roomId:long}/roles/{playerId:long}")]
|
||||||
public async Task<IActionResult> RoleForPlayer(long roomId, long playerId)
|
public async Task<IActionResult> RoleForPlayer(long roomId, long playerId)
|
||||||
{
|
{
|
||||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||||
if (room is null) return NotFound();
|
if (room is null) return NotFound();
|
||||||
if (room.CreatorPlayerId == playerId)
|
if (room.CreatorPlayerId == playerId)
|
||||||
return Ok(BuildRoomAccountRoleWire(playerId, 255));
|
return Ok(BuildRoomAccountRoleWire(playerId, 30));
|
||||||
var row = await db.RoomRoles
|
var row = await db.RoomRoles
|
||||||
.FirstOrDefaultAsync(r => r.RoomId == roomId && r.PlayerId == playerId);
|
.FirstOrDefaultAsync(r => r.RoomId == roomId && r.PlayerId == playerId);
|
||||||
if (row is null) return NotFound();
|
if (row is null) return NotFound();
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
|
|||||||
public string? Text { get; set; }
|
public string? Text { get; set; }
|
||||||
public string? Input { get; set; }
|
public string? Input { get; set; }
|
||||||
public string? Value { get; set; }
|
public string? Value { get; set; }
|
||||||
|
public string? Context { get; set; }
|
||||||
|
public int? Intent { get; set; }
|
||||||
|
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("ruleset")]
|
||||||
|
public int? Ruleset { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>POST <c>api/sanitize/v1/purifyString</c> — returns
|
/// <summary>POST <c>api/sanitize/v1/purifyString</c> — returns
|
||||||
@@ -116,6 +121,32 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
|
|||||||
return Ok(ProfanityFilter.IsClean(input));
|
return Ok(ProfanityFilter.IsClean(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>POST <c>api/sanitize/v1/isPure</c> — 2023 route used
|
||||||
|
/// by CV2 maker pen text such as port names. It imports an object
|
||||||
|
/// field named <c>IsPure</c>.</summary>
|
||||||
|
[HttpPost("api/sanitize/v1/isPure")]
|
||||||
|
public IActionResult IsPureV1(
|
||||||
|
[FromBody] SanitizeBody? body,
|
||||||
|
[FromForm(Name = "Text")] string? textForm,
|
||||||
|
[FromForm(Name = "Input")] string? inputForm,
|
||||||
|
[FromForm(Name = "Value")] string? valueForm,
|
||||||
|
[FromQuery(Name = "text")] string? textQuery)
|
||||||
|
{
|
||||||
|
var input = body?.Text ?? body?.Input ?? body?.Value
|
||||||
|
?? textForm ?? inputForm ?? valueForm ?? textQuery ?? string.Empty;
|
||||||
|
var isPure = ProfanityFilter.IsClean(input);
|
||||||
|
logger.LogInformation(
|
||||||
|
"[sanitize] isPure context={Context} intent={Intent} ruleset={Ruleset} valueLength={ValueLength} isPure={IsPure}",
|
||||||
|
body?.Context ?? string.Empty,
|
||||||
|
body?.Intent,
|
||||||
|
body?.Ruleset,
|
||||||
|
input.Length,
|
||||||
|
isPure);
|
||||||
|
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(new { IsPure = isPure });
|
||||||
|
return Content(json, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>POST <c>api/sanitize/isPure</c> — December 2020 route
|
/// <summary>POST <c>api/sanitize/isPure</c> — December 2020 route
|
||||||
/// used by room/settings forms. Its importer reads an <c>IsPure</c>
|
/// used by room/settings forms. Its importer reads an <c>IsPure</c>
|
||||||
/// object field, unlike the newer bare-bool route above.</summary>
|
/// object field, unlike the newer bare-bool route above.</summary>
|
||||||
|
|||||||
@@ -1289,7 +1289,7 @@ public class GoToController(
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
||||||
|
|
||||||
return $"room_{room.Id}_v1.dat";
|
return RoomService.SyntheticDefaultRoomDataBlobName(room.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ public class MatchController(
|
|||||||
/// we mostly use this for visibility into Photon-side issues —
|
/// we mostly use this for visibility into Photon-side issues —
|
||||||
/// without it, all we see in the catch-all is an opaque "200, empty
|
/// without it, all we see in the catch-all is an opaque "200, empty
|
||||||
/// reply" and no clue why a player crashed back to dorm. Failure
|
/// reply" and no clue why a player crashed back to dorm. Failure
|
||||||
/// reports also clear the pending /goto room and push a ModerationKick
|
/// reports also clear the pending /goto room so the next matchmaking
|
||||||
/// so the watch leaves the "Joining room" overlay instead of softlocking.</summary>
|
/// attempt starts from a clean presence state.</summary>
|
||||||
[HttpPost("/roominstance/{instanceId:long}/reportjoinresult")]
|
[HttpPost("/roominstance/{instanceId:long}/reportjoinresult")]
|
||||||
public async Task<IActionResult> ReportJoinResult(long instanceId, [FromForm(Name = "result")] int? result)
|
public async Task<IActionResult> ReportJoinResult(long instanceId, [FromForm(Name = "result")] int? result)
|
||||||
{
|
{
|
||||||
@@ -59,6 +59,7 @@ public class MatchController(
|
|||||||
{
|
{
|
||||||
presence.SetRoom(pid, completed.Value.TargetRoom);
|
presence.SetRoom(pid, completed.Value.TargetRoom);
|
||||||
presence.MarkActive(pid);
|
presence.MarkActive(pid);
|
||||||
|
presence.EndBootDormGrace(pid);
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[joinresult] committed presence player={Player} room={RoomId}/{RoomName} instance={Instance} deferred={Deferred}",
|
"[joinresult] committed presence player={Player} room={RoomId}/{RoomName} instance={Instance} deferred={Deferred}",
|
||||||
pid,
|
pid,
|
||||||
@@ -71,6 +72,7 @@ public class MatchController(
|
|||||||
{
|
{
|
||||||
presence.MarkActive(pid);
|
presence.MarkActive(pid);
|
||||||
presence.TouchRoom(pid);
|
presence.TouchRoom(pid);
|
||||||
|
presence.EndBootDormGrace(pid);
|
||||||
}
|
}
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[joinresult] player={Player} instance={Instance} OK ({Label})",
|
"[joinresult] player={Player} instance={Instance} OK ({Label})",
|
||||||
@@ -80,7 +82,7 @@ public class MatchController(
|
|||||||
{
|
{
|
||||||
await joinTimeouts.MarkFailedAsync(pid, instanceId, label);
|
await joinTimeouts.MarkFailedAsync(pid, instanceId, label);
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
"[joinresult] player={Player} instance={Instance} FAILED code={Code} ({Label}) — Photon-side error, check Photon AppId/region/auth",
|
"[joinresult] player={Player} instance={Instance} FAILED code={Code} ({Label})",
|
||||||
pid, instanceId, code, label);
|
pid, instanceId, code, label);
|
||||||
}
|
}
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -92,6 +94,22 @@ public class MatchController(
|
|||||||
var pid = this.CurrentPlayerId() ?? 0;
|
var pid = this.CurrentPlayerId() ?? 0;
|
||||||
if (pid == 0) return Unauthorized();
|
if (pid == 0) return Unauthorized();
|
||||||
|
|
||||||
|
var pending = joinTimeouts.GetPending(pid);
|
||||||
|
if (pending is { DeferPresenceCommit: true })
|
||||||
|
{
|
||||||
|
presence.MarkActive(pid);
|
||||||
|
logger.LogInformation(
|
||||||
|
"[matchmake-none] player={Player} has deferred pending room={Room} instance={Instance}; not committing presence",
|
||||||
|
pid,
|
||||||
|
pending.Value.TargetRoom.RoomId,
|
||||||
|
pending.Value.TargetRoom.RoomInstanceId);
|
||||||
|
return Ok(new MatchmakingResponseDto
|
||||||
|
{
|
||||||
|
ErrorCode = 0,
|
||||||
|
RoomInstance = pending.Value.TargetRoom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var current = presence.GetRoom(pid) ?? await BuildDormRoomInstanceAsync(pid);
|
var current = presence.GetRoom(pid) ?? await BuildDormRoomInstanceAsync(pid);
|
||||||
presence.SetRoom(pid, current);
|
presence.SetRoom(pid, current);
|
||||||
presence.MarkActive(pid);
|
presence.MarkActive(pid);
|
||||||
@@ -110,8 +128,22 @@ public class MatchController(
|
|||||||
var appId = config["Photon:AppId"] ?? string.Empty;
|
var appId = config["Photon:AppId"] ?? string.Empty;
|
||||||
var voiceAppId = config["Photon:VoiceAppId"] ?? string.Empty;
|
var voiceAppId = config["Photon:VoiceAppId"] ?? string.Empty;
|
||||||
var region = (config["Photon:CloudRegion"] ?? "us").ToLowerInvariant();
|
var region = (config["Photon:CloudRegion"] ?? "us").ToLowerInvariant();
|
||||||
|
var currentRoom = presence.GetRoom(pid);
|
||||||
|
var permissions = BuildPhotonPermissionSeed();
|
||||||
|
logger.LogInformation(
|
||||||
|
"[photon-token] player={PlayerId} roomInstance={RoomInstanceId} permissions={PermissionCount}",
|
||||||
|
pid, currentRoom?.RoomInstanceId ?? 0L, permissions.Length);
|
||||||
return Ok(new Dictionary<string, object>
|
return Ok(new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
|
// 2023 deserializes this endpoint as ALIIIAGAPND before room
|
||||||
|
// permissions initialization. If Permissions is missing/empty,
|
||||||
|
// OMJKHJLFOCO falls into the persisted-role migration path that
|
||||||
|
// currently NREs during first spawn.
|
||||||
|
["RoomInstanceId"] = currentRoom?.RoomInstanceId ?? 0L,
|
||||||
|
["PhotonAccessToken"] = string.Empty,
|
||||||
|
["Permissions"] = permissions,
|
||||||
|
|
||||||
|
// Existing custom-auth/app-id shape kept for Photon auth callers.
|
||||||
["ResultCode"] = 1,
|
["ResultCode"] = 1,
|
||||||
["Message"] = string.Empty,
|
["Message"] = string.Empty,
|
||||||
["UserId"] = pid.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
["UserId"] = pid.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
@@ -125,6 +157,30 @@ public class MatchController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object[] BuildPhotonPermissionSeed()
|
||||||
|
=>
|
||||||
|
[
|
||||||
|
BuildPhotonPermission(13, 30), // Can Use Maker Pen
|
||||||
|
BuildPhotonPermission(20, 30), // Can Spawn Inventions
|
||||||
|
BuildPhotonPermission(21, 30), // Can Spawn Consumables
|
||||||
|
BuildPhotonPermission(22, 30), // Can Use Room Reset Button
|
||||||
|
BuildPhotonPermission(23, 30), // Can Use Play Gizmos Toggle
|
||||||
|
BuildPhotonPermission(13, 255), // Creator alias
|
||||||
|
BuildPhotonPermission(20, 255),
|
||||||
|
BuildPhotonPermission(21, 255),
|
||||||
|
BuildPhotonPermission(22, 255),
|
||||||
|
BuildPhotonPermission(23, 255),
|
||||||
|
];
|
||||||
|
|
||||||
|
private static object BuildPhotonPermission(int permission, int role) => new
|
||||||
|
{
|
||||||
|
Permission = permission.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
Role = role,
|
||||||
|
Override = true,
|
||||||
|
Type = 0,
|
||||||
|
Value = "true",
|
||||||
|
};
|
||||||
|
|
||||||
[HttpPost("/matchmake/dorm")]
|
[HttpPost("/matchmake/dorm")]
|
||||||
public async Task<IActionResult> MatchmakeDorm()
|
public async Task<IActionResult> MatchmakeDorm()
|
||||||
{
|
{
|
||||||
@@ -132,11 +188,11 @@ public class MatchController(
|
|||||||
if (pid == 0) return Unauthorized();
|
if (pid == 0) return Unauthorized();
|
||||||
|
|
||||||
var room = await BuildDormRoomInstanceAsync(pid);
|
var room = await BuildDormRoomInstanceAsync(pid);
|
||||||
presence.SetRoom(pid, room);
|
joinTimeouts.MarkPending(pid, room, deferPresenceCommit: true);
|
||||||
presence.MarkActive(pid);
|
presence.MarkActive(pid);
|
||||||
await RecordVisitAsync(pid, room.RoomId);
|
await RecordVisitAsync(pid, room.RoomId);
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[matchmake-dorm] player={Player} room={Room} instance={Instance} photon={Photon}",
|
"[matchmake-dorm] player={Player} room={Room} instance={Instance} photon={Photon} deferredPresence=True",
|
||||||
pid, room.RoomId, room.RoomInstanceId, room.PhotonRoomId);
|
pid, room.RoomId, room.RoomInstanceId, room.PhotonRoomId);
|
||||||
return Ok(new MatchmakingResponseDto
|
return Ok(new MatchmakingResponseDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public class MatchPlayerController(
|
|||||||
NotificationService notifications,
|
NotificationService notifications,
|
||||||
RoomService rooms,
|
RoomService rooms,
|
||||||
PrivateInstanceService privateInstances,
|
PrivateInstanceService privateInstances,
|
||||||
|
JoinTimeoutService joinTimeouts,
|
||||||
IConfiguration config,
|
IConfiguration config,
|
||||||
DorkNetDbContext db,
|
DorkNetDbContext db,
|
||||||
ILogger<MatchPlayerController> log) : ControllerBase
|
ILogger<MatchPlayerController> log) : ControllerBase
|
||||||
@@ -61,6 +62,8 @@ public class MatchPlayerController(
|
|||||||
var playerId = TryGetCurrentPlayerId();
|
var playerId = TryGetCurrentPlayerId();
|
||||||
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
||||||
{
|
{
|
||||||
|
presence.Clear(playerId);
|
||||||
|
presence.BeginBootDormGrace(playerId);
|
||||||
var replaced = presence.SetLock(playerId, loginLock);
|
var replaced = presence.SetLock(playerId, loginLock);
|
||||||
if (replaced)
|
if (replaced)
|
||||||
{
|
{
|
||||||
@@ -276,8 +279,27 @@ public class MatchPlayerController(
|
|||||||
if (playerId == 0) return null;
|
if (playerId == 0) return null;
|
||||||
|
|
||||||
var room = presence.GetRoom(playerId);
|
var room = presence.GetRoom(playerId);
|
||||||
|
var pending = joinTimeouts.GetPending(playerId);
|
||||||
|
if (pending is { DeferPresenceCommit: true })
|
||||||
|
{
|
||||||
|
log.LogInformation(
|
||||||
|
"[player-heartbeat] player={PlayerId} pending deferred room={RoomId} instance={InstanceId}; returning null until join result",
|
||||||
|
playerId,
|
||||||
|
pending.Value.TargetRoom.RoomId,
|
||||||
|
pending.Value.TargetRoom.RoomInstanceId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (room is null)
|
if (room is null)
|
||||||
{
|
{
|
||||||
|
if (presence.IsBootDormGraceActive(playerId))
|
||||||
|
{
|
||||||
|
log.LogInformation(
|
||||||
|
"[player-heartbeat] player={PlayerId} in boot dorm grace; returning null roomInstance",
|
||||||
|
playerId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Boot-time race: the watch hits /player/heartbeat before
|
// Boot-time race: the watch hits /player/heartbeat before
|
||||||
// /goto/room/DormRoom has finished recording presence. The
|
// /goto/room/DormRoom has finished recording presence. The
|
||||||
// watch's OnPresenceHeartbeatResponse compares
|
// watch's OnPresenceHeartbeatResponse compares
|
||||||
@@ -434,7 +456,7 @@ public class MatchPlayerController(
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
if (!string.IsNullOrWhiteSpace(room.CurrentDataBlobName)) return room.CurrentDataBlobName;
|
||||||
|
|
||||||
return $"room_{room.Id}_v1.dat";
|
return RoomService.SyntheticDefaultRoomDataBlobName(room.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,25 @@ message OverridableListStringData {
|
|||||||
repeated string inner_value = 2;
|
repeated string inner_value = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Game role collection (rec_room/player_roles/game_role_collection_data.proto)
|
||||||
|
|
||||||
|
message PlayerGameRoleData {
|
||||||
|
}
|
||||||
|
|
||||||
|
message GameRoleCollectionData {
|
||||||
|
repeated PlayerGameRoleData DEPRECATED_game_roles = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tool tag settings (rec_room/tools/tool_tag_settings_data.proto)
|
||||||
|
|
||||||
|
message TagData {
|
||||||
|
repeated string tags = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ToolTagSettingsData {
|
||||||
|
TagData runtime_tag_data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Per-role permission grants (rec_room/player_roles/player_room_role_data.proto)
|
// ── Per-role permission grants (rec_room/player_roles/player_room_role_data.proto)
|
||||||
//
|
//
|
||||||
// Field numbers verified against the 2023 WriteTo disassembly in
|
// Field numbers verified against the 2023 WriteTo disassembly in
|
||||||
@@ -70,31 +89,100 @@ message PlayerRoomRoleData {
|
|||||||
OverridableBoolData disable_mic_auto_mute = 25;
|
OverridableBoolData disable_mic_auto_mute = 25;
|
||||||
OverridableBoolData can_use_share_cam = 26;
|
OverridableBoolData can_use_share_cam = 26;
|
||||||
string role_guid = 27;
|
string role_guid = 27;
|
||||||
OverridableBoolData unknown_permission_20 = 28;
|
OverridableBoolData can_spawn_inventions = 28;
|
||||||
OverridableBoolData unknown_permission_21 = 29;
|
OverridableBoolData can_spawn_consumables = 29;
|
||||||
OverridableBoolData unknown_permission_22 = 30;
|
OverridableBoolData can_use_room_reset_button = 30;
|
||||||
OverridableBoolData unknown_permission_23 = 31;
|
OverridableBoolData can_use_play_gizmos_toggle = 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Role collection (rec_room/player_roles/room_role_collection_data.proto)
|
// ── Role collection (rec_room/player_roles/room_role_collection_data.proto)
|
||||||
|
|
||||||
|
enum MigratedToRecNetVersion {
|
||||||
|
NotYetMigrated = 0;
|
||||||
|
MigratedToRecNet = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message RoomRoleCollectionData {
|
message RoomRoleCollectionData {
|
||||||
repeated PlayerRoomRoleData room_roles = 1;
|
repeated PlayerRoomRoleData room_roles = 1;
|
||||||
int32 rec_net_migration_version = 2;
|
MigratedToRecNetVersion rec_net_migration_version = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PersistedRoomData top-level (rec_room/persistence/persisted_room_data.proto)
|
// ── PersistedRoomData top-level (rec_room/persistence/persisted_room_data.proto)
|
||||||
//
|
//
|
||||||
// The full schema has many more fields — we only set the fields the
|
enum DEPRECATED_RoomPersistenceVersion {
|
||||||
// December 2020 client needs for an editable minimal blob. Its
|
option allow_alias = true;
|
||||||
// RoomPersistenceManager checks field 1 against a minimum editable
|
V0NewRoomPersistenceVersion = 0;
|
||||||
// persistence version of 20, so generated blobs must carry a current
|
V1Initial = 1;
|
||||||
// version or room editors see the native "save data is out-of-date" prompt.
|
V2ExtrusionsCenterOfMass = 2;
|
||||||
|
V3PersistentHostSettings = 3;
|
||||||
|
V4RadioPersistence = 4;
|
||||||
|
V5ExtrusionsPhysicsMode = 5;
|
||||||
|
V6CustomGameSettings = 6;
|
||||||
|
V7NewSandboxButton = 7;
|
||||||
|
V8CircuitChipOrientation = 8;
|
||||||
|
V9NewTriggerVolume = 9;
|
||||||
|
V10RoomRoles = 10;
|
||||||
|
V11SubextrusionsPersistedIds = 11;
|
||||||
|
V12EverythingARoom = 12;
|
||||||
|
V13CreationObjectAdded = 13;
|
||||||
|
V14InventionPieceAdded = 14;
|
||||||
|
V15GameConfigurationAdded = 15;
|
||||||
|
V16EraserMarkersFixed = 16;
|
||||||
|
V17GrabbableAdded = 17;
|
||||||
|
V18ManualSpawnPointsAdded = 18;
|
||||||
|
V19StuntRunnerButtonNodeChange = 19;
|
||||||
|
V20GameRolesInChips = 20;
|
||||||
|
V21PlayerAttractorConfigurable = 21;
|
||||||
|
V22AutosaveValidation = 22;
|
||||||
|
V23TriggerVolumeNoShapeContainers = 23;
|
||||||
|
V24GadgetVisibilityMovedToSubroomSetting = 24;
|
||||||
|
V25RoomKeysBeta = 25;
|
||||||
|
V26DirectionalVoiceChat = 26;
|
||||||
|
V27JuniorEnabledObjectFlag = 27;
|
||||||
|
V28SfxPlayForAll = 28;
|
||||||
|
V29UVManipulationAdded = 29;
|
||||||
|
V30HomeValuesAdded = 30;
|
||||||
|
V31SeatCollisionToggleAdded = 31;
|
||||||
|
V32StoringRotationAsVector4 = 32;
|
||||||
|
V33LightValuesFloat = 33;
|
||||||
|
V34OptimizeCollidersToggle = 34;
|
||||||
|
V35AudioPlayerProperties = 35;
|
||||||
|
V36BackpackToolsForbiddenInSpawner = 36;
|
||||||
|
V37ToolHandednessAdded = 37;
|
||||||
|
V38ProjectileSpreadFloat = 38;
|
||||||
|
LatestRoomPersistenceVersion = 38;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PersistedRoomVersion {
|
||||||
|
option allow_alias = true;
|
||||||
|
V0New = 0;
|
||||||
|
V1SerializeDefaultTags = 1;
|
||||||
|
V2ProgressionEventPopupShop = 2;
|
||||||
|
V3SandboxGunHandleCanReload = 3;
|
||||||
|
V4StudioAssetBundleChanges = 4;
|
||||||
|
V5ParticleEmitterSettings = 5;
|
||||||
|
V6HolotarProjectorCV2 = 6;
|
||||||
|
V7LaserPointer = 7;
|
||||||
|
V8TextScreenRelease = 8;
|
||||||
|
V9AudioPlayerRolloff = 9;
|
||||||
|
V7SunDirection = 10;
|
||||||
|
V11StudioSingleAudioMixer = 11;
|
||||||
|
V12StudioExposedProperties = 12;
|
||||||
|
V13AuthorizeRestrictedTools = 13;
|
||||||
|
V14LaserDirection = 14;
|
||||||
|
V15StudioAudioClipRegistry = 15;
|
||||||
|
V16NewUGCShapes = 16;
|
||||||
|
V17TriggerHandleControlLabel = 17;
|
||||||
|
V18StudioObjectCanvasInteraction = 18;
|
||||||
|
V19February23BetaRelease = 19;
|
||||||
|
LatestVersion = 19;
|
||||||
|
}
|
||||||
|
|
||||||
message PersistedRoomData {
|
message PersistedRoomData {
|
||||||
int32 DEPRECATED_version = 1;
|
DEPRECATED_RoomPersistenceVersion DEPRECATED_version = 1;
|
||||||
// field 2 (persistence_views) — repeated, default empty list.
|
ToolTagSettingsData tool_tag_settings_data = 20;
|
||||||
// fields 3..22 — left at defaults.
|
GameRoleCollectionData game_role_data = 22;
|
||||||
RoomRoleCollectionData room_role_data = 23;
|
RoomRoleCollectionData room_role_data = 23;
|
||||||
// field 24+ — left at defaults.
|
PersistedRoomVersion version = 30;
|
||||||
|
bool creative_roles_enabled = 31;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public sealed class JoinTimeoutService(
|
|||||||
{
|
{
|
||||||
if (room is null) return;
|
if (room is null) return;
|
||||||
|
|
||||||
if (IsDorm(room))
|
if (IsDorm(room) && !deferPresenceCommit)
|
||||||
{
|
{
|
||||||
ClearForPlayer(playerId, "dorm");
|
ClearForPlayer(playerId, "dorm");
|
||||||
return;
|
return;
|
||||||
@@ -40,6 +40,12 @@ public sealed class JoinTimeoutService(
|
|||||||
_ = WatchAsync(playerId, pending);
|
_ = WatchAsync(playerId, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (RoomInstanceDto TargetRoom, bool DeferPresenceCommit)? GetPending(long playerId)
|
||||||
|
{
|
||||||
|
if (!_pending.TryGetValue(playerId, out var pending)) return null;
|
||||||
|
return (pending.TargetRoom, pending.DeferPresenceCommit);
|
||||||
|
}
|
||||||
|
|
||||||
public (RoomInstanceDto TargetRoom, bool DeferPresenceCommit)? MarkCompleted(long playerId, long instanceId, string outcome)
|
public (RoomInstanceDto TargetRoom, bool DeferPresenceCommit)? MarkCompleted(long playerId, long instanceId, string outcome)
|
||||||
{
|
{
|
||||||
if (!_pending.TryGetValue(playerId, out var pending) || pending.InstanceId != instanceId)
|
if (!_pending.TryGetValue(playerId, out var pending) || pending.InstanceId != instanceId)
|
||||||
@@ -55,7 +61,7 @@ public sealed class JoinTimeoutService(
|
|||||||
return (pending.TargetRoom, pending.DeferPresenceCommit);
|
return (pending.TargetRoom, pending.DeferPresenceCommit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MarkFailedAsync(long playerId, long instanceId, string outcome)
|
public Task MarkFailedAsync(long playerId, long instanceId, string outcome)
|
||||||
{
|
{
|
||||||
var completed = MarkCompleted(playerId, instanceId, outcome);
|
var completed = MarkCompleted(playerId, instanceId, outcome);
|
||||||
|
|
||||||
@@ -66,14 +72,14 @@ public sealed class JoinTimeoutService(
|
|||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[join-watchdog] failure ignored player={PlayerId} instance={InstanceId} current={CurrentInstance}",
|
"[join-watchdog] failure ignored player={PlayerId} instance={InstanceId} current={CurrentInstance}",
|
||||||
playerId, instanceId, current?.RoomInstanceId);
|
playerId, instanceId, current?.RoomInstanceId);
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
presence.Clear(playerId);
|
presence.Clear(playerId);
|
||||||
await notifications.KickPlayerAsync(playerId, "Room join failed. Returning to dorm.");
|
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
"[join-watchdog] kicked failed join player={PlayerId} room={RoomId}/{RoomName} instance={InstanceId} outcome={Outcome}",
|
"[join-watchdog] cleared failed join player={PlayerId} room={RoomId}/{RoomName} instance={InstanceId} outcome={Outcome}",
|
||||||
playerId, failedRoom.RoomId, failedRoom.Name, instanceId, outcome);
|
playerId, failedRoom.RoomId, failedRoom.Name, instanceId, outcome);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WatchAsync(long playerId, PendingJoin pending)
|
private async Task WatchAsync(long playerId, PendingJoin pending)
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class PlayerPresenceService
|
|||||||
private readonly ConcurrentDictionary<long, RoomInstanceDto> _local = new();
|
private readonly ConcurrentDictionary<long, RoomInstanceDto> _local = new();
|
||||||
private readonly ConcurrentDictionary<long, DateTimeOffset> _localRoomExpires = new();
|
private readonly ConcurrentDictionary<long, DateTimeOffset> _localRoomExpires = new();
|
||||||
private readonly ConcurrentDictionary<long, DateTimeOffset> _localActiveExpires = new();
|
private readonly ConcurrentDictionary<long, DateTimeOffset> _localActiveExpires = new();
|
||||||
|
private readonly ConcurrentDictionary<long, DateTimeOffset> _localBootDormGraceExpires = new();
|
||||||
private readonly IConnectionMultiplexer? _redis;
|
private readonly IConnectionMultiplexer? _redis;
|
||||||
private readonly ILogger<PlayerPresenceService>? _log;
|
private readonly ILogger<PlayerPresenceService>? _log;
|
||||||
|
|
||||||
@@ -68,6 +69,15 @@ public class PlayerPresenceService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static TimeSpan ActivityTtl { get; } = TimeSpan.FromSeconds(90);
|
public static TimeSpan ActivityTtl { get; } = TimeSpan.FromSeconds(90);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Right after login the 2023 watch may boot into its cached offline
|
||||||
|
/// DormRoom before the live Photon join finishes. During that window
|
||||||
|
/// a null roomInstance is safer than returning stale/live presence:
|
||||||
|
/// the client treats the offline placeholder as invalid and accepts
|
||||||
|
/// null, but a live private dorm forces a resync.
|
||||||
|
/// </summary>
|
||||||
|
public static TimeSpan BootDormGraceTtl { get; } = TimeSpan.FromSeconds(45);
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||||
{
|
{
|
||||||
// Match the wire format used by the matchmaking response so
|
// Match the wire format used by the matchmaking response so
|
||||||
@@ -150,6 +160,63 @@ public class PlayerPresenceService
|
|||||||
_localActiveExpires[playerId] = DateTimeOffset.UtcNow.Add(ActivityTtl);
|
_localActiveExpires[playerId] = DateTimeOffset.UtcNow.Add(ActivityTtl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BeginBootDormGrace(long playerId)
|
||||||
|
{
|
||||||
|
if (_redis is { } mux)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mux.GetDatabase().StringSet(BootDormGraceKey(playerId), "1", BootDormGraceTtl);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.LogWarning(ex, "[presence] redis boot grace SET failed for player={PlayerId}", playerId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_localBootDormGraceExpires[playerId] = DateTimeOffset.UtcNow.Add(BootDormGraceTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsBootDormGraceActive(long playerId)
|
||||||
|
{
|
||||||
|
if (_redis is { } mux)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return mux.GetDatabase().KeyExists(BootDormGraceKey(playerId));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.LogWarning(ex, "[presence] redis boot grace GET failed for player={PlayerId}", playerId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_localBootDormGraceExpires.TryGetValue(playerId, out var expires)) return false;
|
||||||
|
if (expires > DateTimeOffset.UtcNow) return true;
|
||||||
|
_localBootDormGraceExpires.TryRemove(playerId, out _);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndBootDormGrace(long playerId)
|
||||||
|
{
|
||||||
|
if (_redis is { } mux)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mux.GetDatabase().KeyDelete(BootDormGraceKey(playerId));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.LogWarning(ex, "[presence] redis boot grace DEL failed for player={PlayerId}", playerId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_localBootDormGraceExpires.TryRemove(playerId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
public IReadOnlyCollection<long> RecentlyActivePlayerIds()
|
public IReadOnlyCollection<long> RecentlyActivePlayerIds()
|
||||||
{
|
{
|
||||||
if (_redis is { } mux)
|
if (_redis is { } mux)
|
||||||
@@ -265,6 +332,7 @@ public class PlayerPresenceService
|
|||||||
var db = mux.GetDatabase();
|
var db = mux.GetDatabase();
|
||||||
db.KeyDelete(Key(playerId));
|
db.KeyDelete(Key(playerId));
|
||||||
db.KeyDelete(ActiveKey(playerId));
|
db.KeyDelete(ActiveKey(playerId));
|
||||||
|
db.KeyDelete(BootDormGraceKey(playerId));
|
||||||
db.SetRemove(ActiveSetKey, playerId);
|
db.SetRemove(ActiveSetKey, playerId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -272,6 +340,7 @@ public class PlayerPresenceService
|
|||||||
_local.TryRemove(playerId, out _);
|
_local.TryRemove(playerId, out _);
|
||||||
_localRoomExpires.TryRemove(playerId, out _);
|
_localRoomExpires.TryRemove(playerId, out _);
|
||||||
_localActiveExpires.TryRemove(playerId, out _);
|
_localActiveExpires.TryRemove(playerId, out _);
|
||||||
|
_localBootDormGraceExpires.TryRemove(playerId, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +385,7 @@ public class PlayerPresenceService
|
|||||||
|
|
||||||
private static string Key(long playerId) => $"presence:{playerId}";
|
private static string Key(long playerId) => $"presence:{playerId}";
|
||||||
private static string ActiveKey(long playerId) => $"presence-active:{playerId}";
|
private static string ActiveKey(long playerId) => $"presence-active:{playerId}";
|
||||||
|
private static string BootDormGraceKey(long playerId) => $"presence-boot-dorm-grace:{playerId}";
|
||||||
private const string ActiveSetKey = "presence-active";
|
private const string ActiveSetKey = "presence-active";
|
||||||
|
|
||||||
// ── LoginLock single-session enforcement ─────────────────────────
|
// ── LoginLock single-session enforcement ─────────────────────────
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ namespace DorkNet.Server.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RoomDataBlobService
|
public class RoomDataBlobService
|
||||||
{
|
{
|
||||||
private const int December2020PersistenceVersion = 26;
|
private const DEPRECATED_RoomPersistenceVersion LatestDeprecatedPersistenceVersion =
|
||||||
|
DEPRECATED_RoomPersistenceVersion.LatestRoomPersistenceVersion;
|
||||||
|
|
||||||
|
private const PersistedRoomVersion LatestPersistenceVersion =
|
||||||
|
PersistedRoomVersion.LatestVersion;
|
||||||
|
|
||||||
private readonly RoomRoleCollectionData _allPermsRoleData = BuildAllPermsRoleData();
|
private readonly RoomRoleCollectionData _allPermsRoleData = BuildAllPermsRoleData();
|
||||||
private readonly byte[] _rroEditableBlob = BuildRroEditableBlob();
|
private readonly byte[] _rroEditableBlob = BuildRroEditableBlob();
|
||||||
@@ -46,8 +50,10 @@ public class RoomDataBlobService
|
|||||||
if (bytes.Length > 0)
|
if (bytes.Length > 0)
|
||||||
{
|
{
|
||||||
var msg = PersistedRoomData.Parser.ParseFrom(StripTopLevelField(bytes, 2));
|
var msg = PersistedRoomData.Parser.ParseFrom(StripTopLevelField(bytes, 2));
|
||||||
EnsureEditablePersistenceVersion(msg);
|
Ensure2023PersistenceHeader(msg);
|
||||||
msg.RoomRoleData = BuildDefaultRoomRoleData();
|
msg.RoomRoleData = BuildDefaultRoomRoleData();
|
||||||
|
msg.GameRoleData ??= BuildDefaultGameRoleData();
|
||||||
|
msg.ToolTagSettingsData ??= BuildDefaultToolTagSettingsData();
|
||||||
return msg.ToByteArray();
|
return msg.ToByteArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,16 +124,20 @@ public class RoomDataBlobService
|
|||||||
public byte[] OverlayAllPermsRoleData(byte[] existingBlob)
|
public byte[] OverlayAllPermsRoleData(byte[] existingBlob)
|
||||||
{
|
{
|
||||||
var msg = PersistedRoomData.Parser.ParseFrom(existingBlob);
|
var msg = PersistedRoomData.Parser.ParseFrom(existingBlob);
|
||||||
EnsureEditablePersistenceVersion(msg);
|
Ensure2023PersistenceHeader(msg);
|
||||||
msg.RoomRoleData = _allPermsRoleData.Clone();
|
msg.RoomRoleData = _allPermsRoleData.Clone();
|
||||||
|
msg.GameRoleData ??= BuildDefaultGameRoleData();
|
||||||
|
msg.ToolTagSettingsData ??= BuildDefaultToolTagSettingsData();
|
||||||
return msg.ToByteArray();
|
return msg.ToByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] OverlayRroEditableRoleData(byte[] existingBlob)
|
public byte[] OverlayRroEditableRoleData(byte[] existingBlob)
|
||||||
{
|
{
|
||||||
var msg = PersistedRoomData.Parser.ParseFrom(existingBlob);
|
var msg = PersistedRoomData.Parser.ParseFrom(existingBlob);
|
||||||
EnsureEditablePersistenceVersion(msg);
|
Ensure2023PersistenceHeader(msg);
|
||||||
msg.RoomRoleData = _rroEditableRoleData.Clone();
|
msg.RoomRoleData = _rroEditableRoleData.Clone();
|
||||||
|
msg.GameRoleData ??= BuildDefaultGameRoleData();
|
||||||
|
msg.ToolTagSettingsData ??= BuildDefaultToolTagSettingsData();
|
||||||
return msg.ToByteArray();
|
return msg.ToByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +145,11 @@ public class RoomDataBlobService
|
|||||||
{
|
{
|
||||||
var msg = new PersistedRoomData
|
var msg = new PersistedRoomData
|
||||||
{
|
{
|
||||||
DEPRECATEDVersion = December2020PersistenceVersion,
|
DEPRECATEDVersion = LatestDeprecatedPersistenceVersion,
|
||||||
|
Version = LatestPersistenceVersion,
|
||||||
|
CreativeRolesEnabled = true,
|
||||||
|
ToolTagSettingsData = BuildDefaultToolTagSettingsData(),
|
||||||
|
GameRoleData = BuildDefaultGameRoleData(),
|
||||||
RoomRoleData = BuildAllPermsRoleData(),
|
RoomRoleData = BuildAllPermsRoleData(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,7 +160,11 @@ public class RoomDataBlobService
|
|||||||
{
|
{
|
||||||
var msg = new PersistedRoomData
|
var msg = new PersistedRoomData
|
||||||
{
|
{
|
||||||
DEPRECATEDVersion = December2020PersistenceVersion,
|
DEPRECATEDVersion = LatestDeprecatedPersistenceVersion,
|
||||||
|
Version = LatestPersistenceVersion,
|
||||||
|
CreativeRolesEnabled = true,
|
||||||
|
ToolTagSettingsData = BuildDefaultToolTagSettingsData(),
|
||||||
|
GameRoleData = BuildDefaultGameRoleData(),
|
||||||
RoomRoleData = BuildDefaultRoomRoleData(),
|
RoomRoleData = BuildDefaultRoomRoleData(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -157,7 +175,7 @@ public class RoomDataBlobService
|
|||||||
{
|
{
|
||||||
var collection = new RoomRoleCollectionData
|
var collection = new RoomRoleCollectionData
|
||||||
{
|
{
|
||||||
RecNetMigrationVersion = 1,
|
RecNetMigrationVersion = MigratedToRecNetVersion.MigratedToRecNet,
|
||||||
};
|
};
|
||||||
collection.RoomRoles.Add(BuildPermissiveRole(0, 100, "Creator"));
|
collection.RoomRoles.Add(BuildPermissiveRole(0, 100, "Creator"));
|
||||||
|
|
||||||
@@ -166,20 +184,18 @@ public class RoomDataBlobService
|
|||||||
|
|
||||||
private static RoomRoleCollectionData BuildDefaultRoomRoleData()
|
private static RoomRoleCollectionData BuildDefaultRoomRoleData()
|
||||||
{
|
{
|
||||||
var collection = new RoomRoleCollectionData
|
return BuildRroEditableRoleData();
|
||||||
{
|
|
||||||
RecNetMigrationVersion = 1,
|
|
||||||
};
|
|
||||||
collection.RoomRoles.Add(BuildPermissiveRole(2_097_152, 100, "Creator")); // AG_CREATOR
|
|
||||||
|
|
||||||
return collection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] BuildRroEditableBlob()
|
private static byte[] BuildRroEditableBlob()
|
||||||
{
|
{
|
||||||
var msg = new PersistedRoomData
|
var msg = new PersistedRoomData
|
||||||
{
|
{
|
||||||
DEPRECATEDVersion = December2020PersistenceVersion,
|
DEPRECATEDVersion = LatestDeprecatedPersistenceVersion,
|
||||||
|
Version = LatestPersistenceVersion,
|
||||||
|
CreativeRolesEnabled = true,
|
||||||
|
ToolTagSettingsData = BuildDefaultToolTagSettingsData(),
|
||||||
|
GameRoleData = BuildDefaultGameRoleData(),
|
||||||
RoomRoleData = BuildRroEditableRoleData(),
|
RoomRoleData = BuildRroEditableRoleData(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -190,8 +206,18 @@ public class RoomDataBlobService
|
|||||||
{
|
{
|
||||||
var collection = new RoomRoleCollectionData
|
var collection = new RoomRoleCollectionData
|
||||||
{
|
{
|
||||||
RecNetMigrationVersion = 1,
|
RecNetMigrationVersion = MigratedToRecNetVersion.MigratedToRecNet,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The 2023 room-permissions runtime looks up roles by RecNet account-role
|
||||||
|
// values during load. Keep the AG role bitmasks below, but also provide
|
||||||
|
// compatibility aliases for the keys OMJKHJLFOCO.ONECLLALJEO can return.
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(0, 10, "Default"));
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(10, 60, "Player"));
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(20, 80, "Host"));
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(30, 90, "Co-owner"));
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(255, 100, "Creator"));
|
||||||
|
|
||||||
collection.RoomRoles.Add(BuildPermissiveRole(2_097_152, 100, "Creator")); // AG_CREATOR
|
collection.RoomRoles.Add(BuildPermissiveRole(2_097_152, 100, "Creator")); // AG_CREATOR
|
||||||
collection.RoomRoles.Add(BuildPermissiveRole(4_194_304, 90, "Co-owner")); // AG_COOWNER
|
collection.RoomRoles.Add(BuildPermissiveRole(4_194_304, 90, "Co-owner")); // AG_COOWNER
|
||||||
collection.RoomRoles.Add(BuildPermissiveRole(8_388_608, 80, "Host")); // AG_HOST
|
collection.RoomRoles.Add(BuildPermissiveRole(8_388_608, 80, "Host")); // AG_HOST
|
||||||
@@ -250,10 +276,10 @@ public class RoomDataBlobService
|
|||||||
CanSaveInventions = OverrideTrue(),
|
CanSaveInventions = OverrideTrue(),
|
||||||
DisableMicAutoMute = OverrideTrue(),
|
DisableMicAutoMute = OverrideTrue(),
|
||||||
CanUseShareCam = OverrideTrue(),
|
CanUseShareCam = OverrideTrue(),
|
||||||
UnknownPermission20 = OverrideTrue(),
|
CanSpawnInventions = OverrideTrue(),
|
||||||
UnknownPermission21 = OverrideTrue(),
|
CanSpawnConsumables = OverrideTrue(),
|
||||||
UnknownPermission22 = OverrideTrue(),
|
CanUseRoomResetButton = OverrideTrue(),
|
||||||
UnknownPermission23 = OverrideTrue(),
|
CanUsePlayGizmosToggle = OverrideTrue(),
|
||||||
|
|
||||||
// Vote-kick permission — int field, set to a permissive value.
|
// Vote-kick permission — int field, set to a permissive value.
|
||||||
// 0 = AnyoneCanVoteKickAnyone in the 2020 enum (verified via
|
// 0 = AnyoneCanVoteKickAnyone in the 2020 enum (verified via
|
||||||
@@ -268,12 +294,29 @@ public class RoomDataBlobService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureEditablePersistenceVersion(PersistedRoomData msg)
|
private static GameRoleCollectionData BuildDefaultGameRoleData()
|
||||||
{
|
{
|
||||||
if (msg.DEPRECATEDVersion < 20)
|
return new GameRoleCollectionData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ToolTagSettingsData BuildDefaultToolTagSettingsData()
|
||||||
|
{
|
||||||
|
return new ToolTagSettingsData
|
||||||
{
|
{
|
||||||
msg.DEPRECATEDVersion = December2020PersistenceVersion;
|
RuntimeTagData = new TagData(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Ensure2023PersistenceHeader(PersistedRoomData msg)
|
||||||
|
{
|
||||||
|
if ((int)msg.DEPRECATEDVersion < 20)
|
||||||
|
{
|
||||||
|
msg.DEPRECATEDVersion = LatestDeprecatedPersistenceVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msg.Version = LatestPersistenceVersion;
|
||||||
|
msg.CreativeRolesEnabled = true;
|
||||||
|
msg.ToolTagSettingsData ??= BuildDefaultToolTagSettingsData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string StableRoleGuid(int roleId) =>
|
private static string StableRoleGuid(int roleId) =>
|
||||||
@@ -284,6 +327,10 @@ public class RoomDataBlobService
|
|||||||
8_388_608 => "BD0F2F3A-F931-419E-B50C-ECDFF3F56B52",
|
8_388_608 => "BD0F2F3A-F931-419E-B50C-ECDFF3F56B52",
|
||||||
16_777_216 => "32300035-3BEA-457E-95C0-1630AFDFA6BD",
|
16_777_216 => "32300035-3BEA-457E-95C0-1630AFDFA6BD",
|
||||||
0 => Guid.Empty.ToString(),
|
0 => Guid.Empty.ToString(),
|
||||||
|
10 => "3C66F53A-6B76-4DB1-A93F-76F1F59E03B8",
|
||||||
|
20 => "1E8890ED-D729-446B-835B-3C96D8C7D939",
|
||||||
|
30 => "FA1825F9-8F41-54E8-8DB4-530C6B24B3E5",
|
||||||
|
255 => "6F31F2B8-AC6E-549F-8F8C-F8344C9BAE1E",
|
||||||
_ => GuidUtility.Create(GuidUtility.UrlNamespace, $"dorknet-room-role:{roleId}").ToString(),
|
_ => GuidUtility.Create(GuidUtility.UrlNamespace, $"dorknet-room-role:{roleId}").ToString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ public class RoomService(DorkNetDbContext db)
|
|||||||
/// Resolve the saved room-data blob for a personal dorm and repair stale
|
/// Resolve the saved room-data blob for a personal dorm and repair stale
|
||||||
/// pointers left by older builds. Some existing dorm rows only have the
|
/// pointers left by older builds. Some existing dorm rows only have the
|
||||||
/// real save in RoomDataBlobs or RoomScenes while DormStates/Rooms still
|
/// real save in RoomDataBlobs or RoomScenes while DormStates/Rooms still
|
||||||
/// point at the synthetic room_{id}_v1.dat fallback.
|
/// point at an old synthetic fallback blob.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<string> ResolveDormDataBlobNameAsync(long playerId, long dormRoomId)
|
public async Task<string> ResolveDormDataBlobNameAsync(long playerId, long dormRoomId)
|
||||||
{
|
{
|
||||||
@@ -627,9 +627,20 @@ public class RoomService(DorkNetDbContext db)
|
|||||||
return latestDormBlob;
|
return latestDormBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return SyntheticDefaultRoomDataBlobName(dormRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string SyntheticDefaultRoomDataBlobName(long roomId) =>
|
||||||
|
$"room_{roomId}_dorknet_v8.dat";
|
||||||
|
|
||||||
|
public static bool IsLegacySyntheticDefaultRoomDataBlobName(long roomId, string? blobName) =>
|
||||||
|
string.Equals(blobName, $"room_{roomId}_v1.dat", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static string ResolveWireRoomDataBlobName(long roomId, string? blobName) =>
|
||||||
|
!string.IsNullOrWhiteSpace(blobName) && !IsLegacySyntheticDefaultRoomDataBlobName(roomId, blobName)
|
||||||
|
? blobName
|
||||||
|
: SyntheticDefaultRoomDataBlobName(roomId);
|
||||||
|
|
||||||
private async Task<bool> IsUsableDormBlobNameAsync(string? blobName, long dormRoomId)
|
private async Task<bool> IsUsableDormBlobNameAsync(string? blobName, long dormRoomId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(blobName)) return false;
|
if (string.IsNullOrWhiteSpace(blobName)) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user