From fb189d98c4ffca1657c057fd9cf1f10fe11833cd Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Thu, 3 Sep 2026 21:46:03 +0200
Subject: [PATCH 1/9] Add some improvements, docs and debug
---
SecretAPI.Examples/ExampleEntry.cs | 2 +-
.../Settings/ExampleButtonSetting.cs | 37 +++++++++++++++++++
.../Settings/ExampleDropdownSetting.cs | 2 +-
.../Settings/ExampleKeybindSetting.cs | 2 +-
SecretAPI/Debugging/PatchDebugger.cs | 4 +-
SecretAPI/Debugging/PrefabDebugger.cs | 14 +++++--
SecretAPI/Extensions/CodeMatcherExtensions.cs | 7 +---
.../Features/Effects/CustomPlayerEffect.cs | 5 ++-
SecretAPI/Features/IRegister.cs | 1 +
.../UserSettings/CustomButtonSetting.cs | 13 ++++++-
SecretAPI/SecretApi.cs | 8 +++-
11 files changed, 77 insertions(+), 18 deletions(-)
create mode 100644 SecretAPI.Examples/Settings/ExampleButtonSetting.cs
diff --git a/SecretAPI.Examples/ExampleEntry.cs b/SecretAPI.Examples/ExampleEntry.cs
index c0784a0..3cd317b 100644
--- a/SecretAPI.Examples/ExampleEntry.cs
+++ b/SecretAPI.Examples/ExampleEntry.cs
@@ -28,7 +28,7 @@ public class ExampleEntry : Plugin
///
public override void Enable()
{
- CustomSetting.Register(new ExampleKeybindSetting(), new ExampleDropdownSetting());
+ CustomSetting.Register(new ExampleKeybindSetting(), new ExampleDropdownSetting(), new ExampleButtonSetting());
}
///
diff --git a/SecretAPI.Examples/Settings/ExampleButtonSetting.cs b/SecretAPI.Examples/Settings/ExampleButtonSetting.cs
new file mode 100644
index 0000000..9dc0ee8
--- /dev/null
+++ b/SecretAPI.Examples/Settings/ExampleButtonSetting.cs
@@ -0,0 +1,37 @@
+namespace SecretAPI.Examples.Settings;
+
+using LabApi.Features.Wrappers;
+using SecretAPI.Features.Effects;
+using SecretAPI.Features.UserSettings;
+
+///
+/// Example version of .
+///
+public class ExampleButtonSetting : CustomButtonSetting
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ExampleButtonSetting()
+ : base(typeof(ExampleButtonSetting).FullName?.GetHashCode(), "Example Button", "Press!!")
+ {
+ }
+
+ ///
+ public override CustomHeader Header => CustomHeader.Examples;
+
+ ///
+ protected override bool CanView(Player player) => player.RemoteAdminAccess;
+
+ ///
+ protected override CustomSetting CreateDuplicate() => new ExampleButtonSetting();
+
+ ///
+ protected override void HandleSettingUpdate()
+ {
+ if (KnownOwner == null)
+ return;
+
+ KnownOwner.EnableEffect();
+ }
+}
\ No newline at end of file
diff --git a/SecretAPI.Examples/Settings/ExampleDropdownSetting.cs b/SecretAPI.Examples/Settings/ExampleDropdownSetting.cs
index f3a17a0..e99ad5a 100644
--- a/SecretAPI.Examples/Settings/ExampleDropdownSetting.cs
+++ b/SecretAPI.Examples/Settings/ExampleDropdownSetting.cs
@@ -16,7 +16,7 @@ public class ExampleDropdownSetting : CustomDropdownSetting
/// Initializes a new instance of the class.
///
public ExampleDropdownSetting()
- : base(901, "Example dropdown", ExampleOptions)
+ : base(typeof(ExampleDropdownSetting).FullName?.GetHashCode(), "Example dropdown", ExampleOptions)
{
}
diff --git a/SecretAPI.Examples/Settings/ExampleKeybindSetting.cs b/SecretAPI.Examples/Settings/ExampleKeybindSetting.cs
index df726cb..fc5c455 100644
--- a/SecretAPI.Examples/Settings/ExampleKeybindSetting.cs
+++ b/SecretAPI.Examples/Settings/ExampleKeybindSetting.cs
@@ -13,7 +13,7 @@ public class ExampleKeybindSetting : CustomKeybindSetting
/// Initializes a new instance of the class.
///
public ExampleKeybindSetting()
- : base(900, "Example Kill Button", KeyCode.G, allowSpectatorTrigger: false)
+ : base(typeof(ExampleKeybindSetting).FullName?.GetHashCode(), "Example Kill Button", KeyCode.G, allowSpectatorTrigger: false)
{
}
diff --git a/SecretAPI/Debugging/PatchDebugger.cs b/SecretAPI/Debugging/PatchDebugger.cs
index 652c502..2e38b72 100644
--- a/SecretAPI/Debugging/PatchDebugger.cs
+++ b/SecretAPI/Debugging/PatchDebugger.cs
@@ -5,12 +5,12 @@ namespace SecretAPI.Debugging;
using SecretAPI.Attributes;
///
-/// Patch debugger.
+/// Ensures all patches are loaded for debug purposes.
///
internal static class PatchDebugger
{
///
- /// Loads the debugs for patches and ensures all patches have loaded.
+ /// Loads the debug.
///
[CallOnLoad]
internal static void Load()
diff --git a/SecretAPI/Debugging/PrefabDebugger.cs b/SecretAPI/Debugging/PrefabDebugger.cs
index fee96df..03a015c 100644
--- a/SecretAPI/Debugging/PrefabDebugger.cs
+++ b/SecretAPI/Debugging/PrefabDebugger.cs
@@ -4,8 +4,11 @@ namespace SecretAPI.Debugging;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Reflection;
+using HarmonyLib;
using LabApi.Events.Handlers;
+using LabApi.Loader.Features.Paths;
using Mirror;
using SecretAPI.Attributes;
using SecretAPI.Features;
@@ -13,9 +16,9 @@ namespace SecretAPI.Debugging;
using Logger = LabApi.Features.Console.Logger;
///
-/// Debugs basegame prefabs by logging information about them.
+/// Debugs base-game prefabs by logging information about them.
///
-//? Possibly write to file instead of server console?
+// ? Possibly write to file instead of server console?
internal static class PrefabDebugger
{
///
@@ -29,6 +32,7 @@ internal static void Load()
private static void OnWaiting()
{
+ List logTexts = new();
foreach (PropertyInfo properties in typeof(PrefabManager).GetProperties())
{
try
@@ -44,8 +48,12 @@ private static void OnWaiting()
foreach (KeyValuePair pair in NetworkClient.prefabs)
{
- Logger.Debug($"[PrefabDebugging] Key ({pair.Key}) - Value ({pair.Value.name})");
+ // Logger.Debug(txt);
+ string txt = $"Prefab Key ({pair.Key}) - Value ({pair.Value.name})";
+ logTexts.Add(txt);
}
+
+ File.WriteAllLines(Path.Combine(SecretApi.ConfigDirectory.FullName, "debug_prefabs.txt") + "/debug_prefabs.txt", logTexts);
}
}
diff --git a/SecretAPI/Extensions/CodeMatcherExtensions.cs b/SecretAPI/Extensions/CodeMatcherExtensions.cs
index 12c726b..6bfd06a 100644
--- a/SecretAPI/Extensions/CodeMatcherExtensions.cs
+++ b/SecretAPI/Extensions/CodeMatcherExtensions.cs
@@ -17,12 +17,7 @@ public static class CodeMatcherExtensions
///
/// The amount to reverse. Must be positive number.
/// The current .
- public CodeMatcher EndAndBacktrack(int backtrackCount)
- {
- matcher.End();
- matcher.Advance(-backtrackCount);
- return matcher;
- }
+ public CodeMatcher EndAndBacktrack(int backtrackCount) => matcher.End().Advance(-backtrackCount);
///
/// Declares a local to be used of a certain .
diff --git a/SecretAPI/Features/Effects/CustomPlayerEffect.cs b/SecretAPI/Features/Effects/CustomPlayerEffect.cs
index 5b77bec..6e0862d 100644
--- a/SecretAPI/Features/Effects/CustomPlayerEffect.cs
+++ b/SecretAPI/Features/Effects/CustomPlayerEffect.cs
@@ -33,6 +33,7 @@ public abstract class CustomPlayerEffect : StatusEffectBase
///
/// Initializes the to implement .
///
+ /// The effects must be loaded on before is created.
[CallOnLoad]
internal static void Initialize()
{
@@ -46,7 +47,7 @@ private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
SceneManager.sceneLoaded -= OnSceneLoaded;
- Transform playerEffects = PrefabStore.Prefab.playerEffectsController.effectsGameObject.transform;
+ GameObject playerEffects = PrefabStore.Prefab.playerEffectsController.effectsGameObject;
foreach (Type type in EffectsToRegister)
{
if (!typeof(StatusEffectBase).IsAssignableFrom(type))
@@ -56,7 +57,7 @@ private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
}
// register effect into prefab
- new GameObject(type.Name, type).transform.parent = playerEffects;
+ playerEffects.AddComponent(type);
}
}
}
\ No newline at end of file
diff --git a/SecretAPI/Features/IRegister.cs b/SecretAPI/Features/IRegister.cs
index 11d04b4..91bb494 100644
--- a/SecretAPI/Features/IRegister.cs
+++ b/SecretAPI/Features/IRegister.cs
@@ -7,6 +7,7 @@
///
/// Interface used to define a type that should auto register.
///
+// TODO: Source generate this
public interface IRegister
{
///
diff --git a/SecretAPI/Features/UserSettings/CustomButtonSetting.cs b/SecretAPI/Features/UserSettings/CustomButtonSetting.cs
index d20f3e9..d131111 100644
--- a/SecretAPI/Features/UserSettings/CustomButtonSetting.cs
+++ b/SecretAPI/Features/UserSettings/CustomButtonSetting.cs
@@ -1,6 +1,7 @@
namespace SecretAPI.Features.UserSettings;
using System;
+using System.Diagnostics;
using global::UserSettings.ServerSpecific;
///
@@ -34,10 +35,20 @@ protected CustomButtonSetting(int? id, string label, string buttonText, float? h
///
public new SSButton Base { get; }
+ ///
+ /// Gets the controlling the last press.
+ ///
+ public Stopwatch LastPressWatch => Base.SyncLastPress;
+
///
/// Gets the of the last press.
///
- public TimeSpan LastPress => Base.SyncLastPress.Elapsed;
+ public TimeSpan LastPress => LastPressWatch.Elapsed;
+
+ ///
+ /// Gets a value indicating whether the button has ever been pressed.
+ ///
+ public bool HasPressedEver => LastPressWatch.IsRunning;
///
/// Gets or sets the text of the button.
diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs
index 2a288bd..d7cdb38 100644
--- a/SecretAPI/SecretApi.cs
+++ b/SecretAPI/SecretApi.cs
@@ -1,12 +1,13 @@
namespace SecretAPI;
using System;
+using System.IO;
using System.Reflection;
using HarmonyLib;
using LabApi.Features;
+using LabApi.Loader.Features.Paths;
using LabApi.Loader.Features.Plugins;
using LabApi.Loader.Features.Plugins.Enums;
-using SecretAPI.Attributes;
///
/// Main class handling loading API.
@@ -45,6 +46,11 @@ public class SecretApi : Plugin
///
internal static Assembly Assembly { get; } = typeof(SecretApi).Assembly;
+ ///
+ /// Gets the config directory within "SCP Secret Laboratory/LabAPI/configs/SecretAPI" for internal use.
+ ///
+ internal static DirectoryInfo ConfigDirectory { get; } = PathManager.Configs.CreateSubdirectory("SecretAPI");
+
///
public override void Enable()
{
From 67894b7107545e9825de200b1de752e9eac31b7d Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Fri, 4 Sep 2026 14:04:09 +0200
Subject: [PATCH 2/9] Rename
---
SecretAPI/Features/UserSettings/CustomButtonSetting.cs | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/SecretAPI/Features/UserSettings/CustomButtonSetting.cs b/SecretAPI/Features/UserSettings/CustomButtonSetting.cs
index d131111..df65faa 100644
--- a/SecretAPI/Features/UserSettings/CustomButtonSetting.cs
+++ b/SecretAPI/Features/UserSettings/CustomButtonSetting.cs
@@ -43,12 +43,18 @@ protected CustomButtonSetting(int? id, string label, string buttonText, float? h
///
/// Gets the of the last press.
///
- public TimeSpan LastPress => LastPressWatch.Elapsed;
+ [Obsolete("Use TimeSinceLastPress instead - This will be removed in 4.0")]
+ public TimeSpan LastPress => TimeSinceLastPress;
+
+ ///
+ /// Gets the of the last press.
+ ///
+ public TimeSpan TimeSinceLastPress => LastPressWatch.Elapsed;
///
/// Gets a value indicating whether the button has ever been pressed.
///
- public bool HasPressedEver => LastPressWatch.IsRunning;
+ public bool EverPressed => LastPressWatch.IsRunning;
///
/// Gets or sets the text of the button.
From d258a316aa2386a2e57174d52f7eeadd1b95b21b Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Fri, 4 Sep 2026 14:52:35 +0200
Subject: [PATCH 3/9] Fix: Directory is created even when unused
---
SecretAPI/SecretApi.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs
index d7cdb38..3042b91 100644
--- a/SecretAPI/SecretApi.cs
+++ b/SecretAPI/SecretApi.cs
@@ -49,7 +49,7 @@ public class SecretApi : Plugin
///
/// Gets the config directory within "SCP Secret Laboratory/LabAPI/configs/SecretAPI" for internal use.
///
- internal static DirectoryInfo ConfigDirectory { get; } = PathManager.Configs.CreateSubdirectory("SecretAPI");
+ internal static DirectoryInfo ConfigDirectory => PathManager.Configs.CreateSubdirectory("SecretAPI");
///
public override void Enable()
From c1683db9dc33092f1ee3d004a217f9ceb0f044b9 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Fri, 4 Sep 2026 14:53:02 +0200
Subject: [PATCH 4/9] Fix: Effect Desync + Shortened the code
---
SecretAPI/Features/Effects/CustomPlayerEffect.cs | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/SecretAPI/Features/Effects/CustomPlayerEffect.cs b/SecretAPI/Features/Effects/CustomPlayerEffect.cs
index 6e0862d..171dfff 100644
--- a/SecretAPI/Features/Effects/CustomPlayerEffect.cs
+++ b/SecretAPI/Features/Effects/CustomPlayerEffect.cs
@@ -48,16 +48,6 @@ private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
SceneManager.sceneLoaded -= OnSceneLoaded;
GameObject playerEffects = PrefabStore.Prefab.playerEffectsController.effectsGameObject;
- foreach (Type type in EffectsToRegister)
- {
- if (!typeof(StatusEffectBase).IsAssignableFrom(type))
- {
- Logger.Error($"[CustomPlayerEffect.Initialize] {type.FullName} is not a valid StatusEffectBase and thus could not be registered!");
- continue;
- }
-
- // register effect into prefab
- playerEffects.AddComponent(type);
- }
+ _ = new GameObject("SecretAPI-CustomPlayerEffect", EffectsToRegister.ToArray()).transform.parent = playerEffects.transform;
}
}
\ No newline at end of file
From c44e802059211dd580912b8a2af9ba0b9ff028f0 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Fri, 4 Sep 2026 22:22:18 +0200
Subject: [PATCH 5/9] feature: Effects.BlastResistance
---
SecretAPI/Debugging/PrefabDebugger.cs | 2 +-
SecretAPI/Features/Effects/BlastResistance.cs | 27 +++++++++++++++++++
.../Features/Effects/CustomPlayerEffect.cs | 17 +++++++++---
3 files changed, 42 insertions(+), 4 deletions(-)
create mode 100644 SecretAPI/Features/Effects/BlastResistance.cs
diff --git a/SecretAPI/Debugging/PrefabDebugger.cs b/SecretAPI/Debugging/PrefabDebugger.cs
index 03a015c..e7b28c6 100644
--- a/SecretAPI/Debugging/PrefabDebugger.cs
+++ b/SecretAPI/Debugging/PrefabDebugger.cs
@@ -53,7 +53,7 @@ private static void OnWaiting()
logTexts.Add(txt);
}
- File.WriteAllLines(Path.Combine(SecretApi.ConfigDirectory.FullName, "debug_prefabs.txt") + "/debug_prefabs.txt", logTexts);
+ File.WriteAllLines(Path.Combine(SecretApi.ConfigDirectory.FullName, "debug_prefabs.txt"), logTexts);
}
}
diff --git a/SecretAPI/Features/Effects/BlastResistance.cs b/SecretAPI/Features/Effects/BlastResistance.cs
new file mode 100644
index 0000000..178b148
--- /dev/null
+++ b/SecretAPI/Features/Effects/BlastResistance.cs
@@ -0,0 +1,27 @@
+namespace SecretAPI.Features.Effects;
+
+using CustomPlayerEffects;
+using PlayerStatsSystem;
+
+///
+/// Grants the player a percentage immunity towards explosion damage.
+///
+/// This is 0.5% resistance per intensity level and is capped at 200.
+public class BlastResistance : CustomPlayerEffect, IDamageModifierEffect
+{
+ ///
+ public bool DamageModifierActive => IsEnabled;
+
+ ///
+ public override byte MaxIntensity => 200;
+
+ ///
+ public float GetDamageModifier(float baseDamage, DamageHandlerBase handler, HitboxType hitboxType)
+ {
+ if (handler is not ExplosionDamageHandler)
+ return 1;
+
+ float modifier = 1 - ((float)Intensity / 200);
+ return modifier;
+ }
+}
\ No newline at end of file
diff --git a/SecretAPI/Features/Effects/CustomPlayerEffect.cs b/SecretAPI/Features/Effects/CustomPlayerEffect.cs
index 171dfff..dc7d58c 100644
--- a/SecretAPI/Features/Effects/CustomPlayerEffect.cs
+++ b/SecretAPI/Features/Effects/CustomPlayerEffect.cs
@@ -5,9 +5,9 @@
using CustomPlayerEffects;
using LabApi.Features.Wrappers;
using SecretAPI.Attributes;
-using SecretAPI.Extensions;
using UnityEngine;
using UnityEngine.SceneManagement;
+
using Logger = LabApi.Features.Console.Logger;
///
@@ -39,6 +39,7 @@ internal static void Initialize()
{
EffectsToRegister.Add(typeof(Energized));
EffectsToRegister.Add(typeof(Depleted));
+ EffectsToRegister.Add(typeof(BlastResistance));
SceneManager.sceneLoaded += OnSceneLoaded;
}
@@ -47,7 +48,17 @@ private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
SceneManager.sceneLoaded -= OnSceneLoaded;
- GameObject playerEffects = PrefabStore.Prefab.playerEffectsController.effectsGameObject;
- _ = new GameObject("SecretAPI-CustomPlayerEffect", EffectsToRegister.ToArray()).transform.parent = playerEffects.transform;
+ Transform playerEffects = PrefabStore.Prefab.playerEffectsController.effectsGameObject.transform;
+ foreach (Type type in EffectsToRegister)
+ {
+ if (!typeof(StatusEffectBase).IsAssignableFrom(type))
+ {
+ Logger.Error($"[CustomPlayerEffect.OnSceneLoaded] {type.FullName} is not a valid StatusEffectBase and thus could not be registered!");
+ continue;
+ }
+
+ // register effect into prefab
+ new GameObject(type.Name, type).transform.parent = playerEffects;
+ }
}
}
\ No newline at end of file
From 48716487fa0869f0ec11282e8383e4e2e79c9d27 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Sat, 5 Sep 2026 18:39:20 +0200
Subject: [PATCH 6/9] feature: More fake syncing
---
SecretAPI.Examples/ExampleEntry.cs | 2 +-
.../Settings/ExampleButtonSetting.cs | 3 +-
.../Settings/ExampleFakeSyncButton.cs | 53 ++++++
SecretAPI/Extensions/MirrorExtensions.cs | 175 +++++++++++++++++-
4 files changed, 230 insertions(+), 3 deletions(-)
create mode 100644 SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs
diff --git a/SecretAPI.Examples/ExampleEntry.cs b/SecretAPI.Examples/ExampleEntry.cs
index 3cd317b..bf6b9d9 100644
--- a/SecretAPI.Examples/ExampleEntry.cs
+++ b/SecretAPI.Examples/ExampleEntry.cs
@@ -28,7 +28,7 @@ public class ExampleEntry : Plugin
///
public override void Enable()
{
- CustomSetting.Register(new ExampleKeybindSetting(), new ExampleDropdownSetting(), new ExampleButtonSetting());
+ CustomSetting.Register(new ExampleKeybindSetting(), new ExampleDropdownSetting(), new ExampleButtonSetting(), new ExampleFakeSyncButton());
}
///
diff --git a/SecretAPI.Examples/Settings/ExampleButtonSetting.cs b/SecretAPI.Examples/Settings/ExampleButtonSetting.cs
index 9dc0ee8..0d46347 100644
--- a/SecretAPI.Examples/Settings/ExampleButtonSetting.cs
+++ b/SecretAPI.Examples/Settings/ExampleButtonSetting.cs
@@ -32,6 +32,7 @@ protected override void HandleSettingUpdate()
if (KnownOwner == null)
return;
- KnownOwner.EnableEffect();
+ KnownOwner.EnableEffect(duration: 30);
+ KnownOwner.EnableEffect(200, 30);
}
}
\ No newline at end of file
diff --git a/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs
new file mode 100644
index 0000000..060d900
--- /dev/null
+++ b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs
@@ -0,0 +1,53 @@
+namespace SecretAPI.Examples.Settings;
+
+using LabApi.Features.Wrappers;
+using MEC;
+using Mirror;
+using SecretAPI.Extensions;
+using SecretAPI.Features.UserSettings;
+
+///
+/// Example version for fake syncing on a .
+///
+public class ExampleFakeSyncButton : CustomButtonSetting
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ExampleFakeSyncButton()
+ : base(typeof(ExampleFakeSyncButton).FullName?.GetHashCode(), "Example Fake Sync Button", "Fake!")
+ {
+ }
+
+ ///
+ public override CustomHeader Header => CustomHeader.Examples;
+
+ ///
+ protected override bool CanView(Player player) => player.RemoteAdminAccess;
+
+ ///
+ protected override CustomSetting CreateDuplicate() => new ExampleFakeSyncButton();
+
+ ///
+ protected override void HandleSettingUpdate()
+ {
+ if (KnownOwner == null)
+ return;
+
+ TextToy textToy = TextToy.Create(KnownOwner.Position, KnownOwner.Rotation);
+ textToy.TextFormat = "{0}";
+ textToy.Arguments.Add("Default Text!");
+
+ Timing.CallDelayed(5, () =>
+ {
+ MirrorExtensions.SyncListChange change = new()
+ {
+ Index = 0,
+ Item = "Fake synced text!",
+ Operation = SyncList.Operation.OP_SET,
+ };
+
+ KnownOwner.SendFakeSyncListData(textToy.Base, 1L, change);
+ });
+ }
+}
\ No newline at end of file
diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs
index 4b2dc81..0e32ab6 100644
--- a/SecretAPI/Extensions/MirrorExtensions.cs
+++ b/SecretAPI/Extensions/MirrorExtensions.cs
@@ -1,16 +1,25 @@
namespace SecretAPI.Extensions;
using System;
+using System.Collections.Generic;
using System.Reflection;
-using LabApi.Features.Console;
+using global::AdminToys;
using LabApi.Features.Wrappers;
using Mirror;
+using Logger = LabApi.Features.Console.Logger;
+
+#pragma warning disable SA1117 // The parameters should all be placed on the same line or each parameter should be placed on its own line
///
/// Extensions related to Mirror.
///
public static class MirrorExtensions
{
+ private static readonly Dictionary SubWriteClassToMinULong = new()
+ {
+ [typeof(AdminToyBase)] = 32,
+ };
+
///
/// Send a fake rpc message to a player.
///
@@ -21,6 +30,10 @@ public static class MirrorExtensions
/// The values to write to the writer.
public static void SendFakeRpcMessage(this Player target, NetworkBehaviour behaviour, Type type, string rpcName, params object[] values)
{
+ // validate the target is connected still
+ if (!target.GameObject)
+ return;
+
NetworkWriterPooled pooledWriter = NetworkWriterPool.Get();
foreach (object obj in values)
@@ -38,6 +51,132 @@ public static void SendFakeRpcMessage(this Player target, NetworkBehaviour behav
NetworkWriterPool.Return(pooledWriter);
}
+ ///
+ /// Sends fake data of a based on parameters.
+ ///
+ /// The target to send the fake data to.
+ /// The containing the .
+ /// The index of the on the .
+ /// The type of change to fake sync.
+ /// The type contained by the .
+ public static void SendFakeSyncListData(this Player target, NetworkBehaviour behaviour, ulong listIndex, SyncListChange change)
+ {
+ SendFakeState(target, behaviour, writer =>
+ {
+ // write the index of the list
+ writer.WriteULong(listIndex);
+
+ // copied from SyncList.OnSerializeDelta
+ writer.WriteUInt(1); // only 1 change
+ writer.WriteByte((byte)change.Operation);
+ switch (change.Operation)
+ {
+ case SyncList.Operation.OP_ADD:
+ writer.Write(change.Item);
+ break;
+ case SyncList.Operation.OP_INSERT:
+ case SyncList.Operation.OP_SET:
+ writer.WriteUInt((uint)change.Index);
+ writer.Write(change.Item);
+ break;
+ case SyncList.Operation.OP_REMOVEAT:
+ writer.WriteUInt((uint)change.Index);
+ break;
+ }
+ }, null);
+ }
+
+ ///
+ /// Sends a fake state of a on a to a target.
+ ///
+ /// The player to send the fake state to.
+ /// The behaviour to send fake data of.
+ /// The dirty bit of the syncvar to fake.
+ /// The value to fake the syncvar as.
+ /// The type of the sync var.
+ public static void SendFakeSyncVar(this Player target, NetworkBehaviour behaviour, ulong dirtyBit, T value)
+ {
+ SendFakeState(target, behaviour, null, writer =>
+ {
+ // Always write the dirty bit
+ writer.WriteULong(dirtyBit);
+
+ ulong minDirtyBit = GetSubclassMinDirtyBit(behaviour.GetType());
+ bool isWritten = false;
+
+ if (dirtyBit >= minDirtyBit)
+ {
+ writer.WriteULong(dirtyBit);
+ isWritten = true;
+ }
+
+ writer.Write(value);
+
+ if (!isWritten)
+ writer.WriteULong(dirtyBit);
+ });
+ }
+
+ ///
+ /// Sends a fake entity state based on parameters to a target player.
+ ///
+ /// The player to send the fake state to.
+ /// The to send the fake state of.
+ /// An Action indicating how to handle writing data related to s.
+ /// An Action indicating how to handle writing data related to s.
+ public static void SendFakeState(this Player target, NetworkBehaviour behaviour, Action? syncObjectWriter, Action? syncVarWriter)
+ {
+ // validate the target is connected still
+ if (!target.GameObject)
+ return;
+
+ using NetworkWriterPooled pooledWriter = NetworkWriterPool.Get();
+
+ // write the compressed bitmask
+ int index = behaviour.netIdentity.NetworkBehaviours.IndexOf(behaviour);
+ ulong mask = (ulong)(1 << index);
+ Compression.CompressVarUInt(pooledWriter, mask);
+
+ // from NetworkBehaviour.Serialize
+ // start serializing
+ int headerPosition = pooledWriter.Position;
+ pooledWriter.WriteByte(0);
+ int contentPosition = pooledWriter.Position;
+
+ if (syncObjectWriter != null)
+ syncObjectWriter.Invoke(pooledWriter);
+ else
+ pooledWriter.WriteULong(0); // only needs to be written once for sync object
+
+ if (syncVarWriter != null)
+ {
+ syncVarWriter.Invoke(pooledWriter);
+ }
+ else
+ {
+ // dirty bit is always 0 in this case
+ pooledWriter.WriteULong(0);
+
+ // write it again for subclass
+ if (GetSubclassMinDirtyBit(behaviour.GetType()) != ulong.MaxValue)
+ pooledWriter.WriteULong(0);
+ }
+
+ // fill in length hash as the last byte of the 4 byte length
+ int endPosition = pooledWriter.Position;
+ pooledWriter.Position = headerPosition;
+ int size = endPosition - contentPosition;
+ byte safety = (byte)(size & 0xFF); // https://github.com/MirrorNetworking/Mirror/blob/master/Assets/Mirror/Core/NetworkBehaviour.cs#L1383
+ pooledWriter.WriteByte(safety);
+ pooledWriter.Position = endPosition;
+
+ target.Connection.Send(new EntityStateMessage()
+ {
+ netId = behaviour.netId,
+ payload = pooledWriter.ToArraySegment(),
+ });
+ }
+
///
/// Handles writing into a .
///
@@ -62,4 +201,38 @@ public static void ProperWrite(this NetworkWriter writer, object obj)
del.DynamicInvoke(writer, obj);
}
+
+ private static ulong GetSubclassMinDirtyBit(Type type)
+ {
+ // full credit to https://github.com/KadavasKingdom/LabApiExtensions/blob/main/LabApiExtensions/FakeExtension/FakeSyncVarExtension.cs#L17 for this
+ foreach (KeyValuePair kvp in SubWriteClassToMinULong)
+ {
+ if (type.IsSubclassOf(kvp.Key))
+ return kvp.Value;
+ }
+
+ return ulong.MaxValue;
+ }
+
+ ///
+ /// Public version of .
+ ///
+ /// The item being contained by the .
+ public struct SyncListChange
+ {
+ ///
+ /// The operation to do on the .
+ ///
+ public SyncList.Operation Operation;
+
+ ///
+ /// The index on which the Operation should be performed.
+ ///
+ public int Index;
+
+ ///
+ /// The value/item to use. Ignored when Operation is .
+ ///
+ public T Item;
+ }
}
\ No newline at end of file
From 936610c5b31efdfe4b5820c06f061ed60f9cd937 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Sat, 5 Sep 2026 18:51:00 +0200
Subject: [PATCH 7/9] Add validation for SendFakeSyncListData listIndex being
lower than 1
---
SecretAPI/Extensions/MirrorExtensions.cs | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs
index 0e32ab6..2a4eff8 100644
--- a/SecretAPI/Extensions/MirrorExtensions.cs
+++ b/SecretAPI/Extensions/MirrorExtensions.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Reflection;
using global::AdminToys;
using LabApi.Features.Wrappers;
@@ -56,11 +57,14 @@ public static void SendFakeRpcMessage(this Player target, NetworkBehaviour behav
///
/// The target to send the fake data to.
/// The containing the .
- /// The index of the on the .
+ /// The index of the on the . The index starts at 1.
/// The type of change to fake sync.
/// The type contained by the .
public static void SendFakeSyncListData(this Player target, NetworkBehaviour behaviour, ulong listIndex, SyncListChange change)
{
+ if (listIndex <= 0)
+ Logger.Warn($"[MirrorExtensions.SendFakeSyncListData] Index is {listIndex} - Expected 1 or higher!: + {new StackTrace()}");
+
SendFakeState(target, behaviour, writer =>
{
// write the index of the list
From f0a668695a2f9a9594e87b6dd422440fdc21e620 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Sat, 5 Sep 2026 18:55:16 +0200
Subject: [PATCH 8/9] fix: docs
---
SecretAPI/Extensions/MirrorExtensions.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs
index 2a4eff8..54eaee3 100644
--- a/SecretAPI/Extensions/MirrorExtensions.cs
+++ b/SecretAPI/Extensions/MirrorExtensions.cs
@@ -219,7 +219,7 @@ private static ulong GetSubclassMinDirtyBit(Type type)
}
///
- /// Public version of .
+ /// Public representation of the internal Change class.
///
/// The item being contained by the .
public struct SyncListChange
From b7882f9535fd2916682432cc327854aafd966a4a Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Sat, 5 Sep 2026 19:04:55 +0200
Subject: [PATCH 9/9] Improve docs and remove requirement for
SyncListChange.Index
---
SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs | 2 +-
SecretAPI/Extensions/MirrorExtensions.cs | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs
index 060d900..c923ec9 100644
--- a/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs
+++ b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs
@@ -42,9 +42,9 @@ protected override void HandleSettingUpdate()
{
MirrorExtensions.SyncListChange change = new()
{
+ Operation = SyncList.Operation.OP_SET,
Index = 0,
Item = "Fake synced text!",
- Operation = SyncList.Operation.OP_SET,
};
KnownOwner.SendFakeSyncListData(textToy.Base, 1L, change);
diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs
index 54eaee3..5b4058d 100644
--- a/SecretAPI/Extensions/MirrorExtensions.cs
+++ b/SecretAPI/Extensions/MirrorExtensions.cs
@@ -227,15 +227,16 @@ public struct SyncListChange
///
/// The operation to do on the .
///
- public SyncList.Operation Operation;
+ public required SyncList.Operation Operation;
///
/// The index on which the Operation should be performed.
///
+ /// This is ignored when Operation is or .
public int Index;
///
- /// The value/item to use. Ignored when Operation is .
+ /// The value/item to use. Ignored when Operation is or .
///
public T Item;
}