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; }