From 5ed8fe5992b690c4f9d4c1a5ceebb72f5675ae2f Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:53:57 +0200 Subject: [PATCH 1/6] docs + minor improvements --- SecretAPI/Extensions/MirrorExtensions.cs | 15 ++++++++-- SecretAPI/Features/Effects/BlastResistance.cs | 4 +-- SecretAPI/Features/PrefabStore.cs | 12 +++----- .../UserSettings/CustomPlainTextSetting.cs | 4 ++- .../Features/UserSettings/CustomSetting.cs | 2 +- .../UserSettings/SettingResponseType.cs | 2 +- .../Features/ReportDefinedSettingPatch.cs | 19 ------------ .../{ => RoundIgnore}/RoundEndIgnorePatch.cs | 2 +- .../RoundIgnoreCountPatch.cs | 2 +- .../Settings/ReportDefinedSettingPatch.cs | 29 +++++++++++++++++++ .../{ => Settings}/SendSettingsPlayerSync.cs | 2 +- .../{ => Settings}/SendSettingsServerSync.cs | 2 +- .../SettingsOriginalDefinitionFix.cs | 2 +- .../{ => Settings}/SettingsSyncValidateFix.cs | 2 +- 14 files changed, 59 insertions(+), 40 deletions(-) delete mode 100644 SecretAPI/Patches/Features/ReportDefinedSettingPatch.cs rename SecretAPI/Patches/Features/{ => RoundIgnore}/RoundEndIgnorePatch.cs (97%) rename SecretAPI/Patches/Features/{ => RoundIgnore}/RoundIgnoreCountPatch.cs (97%) create mode 100644 SecretAPI/Patches/Features/Settings/ReportDefinedSettingPatch.cs rename SecretAPI/Patches/Features/{ => Settings}/SendSettingsPlayerSync.cs (93%) rename SecretAPI/Patches/Features/{ => Settings}/SendSettingsServerSync.cs (91%) rename SecretAPI/Patches/Features/{ => Settings}/SettingsOriginalDefinitionFix.cs (94%) rename SecretAPI/Patches/Features/{ => Settings}/SettingsSyncValidateFix.cs (94%) diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs index 5b4058d..8421a8f 100644 --- a/SecretAPI/Extensions/MirrorExtensions.cs +++ b/SecretAPI/Extensions/MirrorExtensions.cs @@ -16,6 +16,7 @@ /// public static class MirrorExtensions { + private static readonly Dictionary TypeToWriterCache = new(); private static readonly Dictionary SubWriteClassToMinULong = new() { [typeof(AdminToyBase)] = 32, @@ -188,6 +189,15 @@ public static void SendFakeState(this Player target, NetworkBehaviour behaviour, /// The object to write. public static void ProperWrite(this NetworkWriter writer, object obj) { + Type type = obj.GetType(); + + // caching to avoid unnecessary reflection + if (TypeToWriterCache.TryGetValue(type, out Delegate del)) + { + del.DynamicInvoke(writer, obj); + return; + } + Type genericType = typeof(Writer<>).MakeGenericType(obj.GetType()); FieldInfo? writeField = genericType.GetField("write", BindingFlags.Static | BindingFlags.Public); if (writeField == null) @@ -197,13 +207,14 @@ public static void ProperWrite(this NetworkWriter writer, object obj) } object? writeDelegate = writeField.GetValue(null); - if (writeDelegate is not Delegate del) + if (writeDelegate is not Delegate dele) { Logger.Warn($"Writer<{obj.GetType()}>.write is not a delegate!"); return; } - del.DynamicInvoke(writer, obj); + TypeToWriterCache.Add(type, dele); + dele.DynamicInvoke(writer, obj); } private static ulong GetSubclassMinDirtyBit(Type type) diff --git a/SecretAPI/Features/Effects/BlastResistance.cs b/SecretAPI/Features/Effects/BlastResistance.cs index 178b148..a99e34d 100644 --- a/SecretAPI/Features/Effects/BlastResistance.cs +++ b/SecretAPI/Features/Effects/BlastResistance.cs @@ -10,10 +10,10 @@ public class BlastResistance : CustomPlayerEffect, IDamageModifierEffect { /// - public bool DamageModifierActive => IsEnabled; + public override byte MaxIntensity => 200; /// - public override byte MaxIntensity => 200; + public bool DamageModifierActive => IsEnabled; /// public float GetDamageModifier(float baseDamage, DamageHandlerBase handler, HitboxType hitboxType) diff --git a/SecretAPI/Features/PrefabStore.cs b/SecretAPI/Features/PrefabStore.cs index b60c89e..b8a767f 100644 --- a/SecretAPI/Features/PrefabStore.cs +++ b/SecretAPI/Features/PrefabStore.cs @@ -2,16 +2,16 @@ using System.Collections.Generic; using System.Linq; +using AdminToys; using Interactables.Interobjects; using Mirror; -using NorthwoodLib.Pools; using UnityEngine; /// /// Handles the storing of a prefab. /// /// The prefab to use. -/// For Doors use . +/// For or use . public static class PrefabStore where TPrefab : NetworkBehaviour { @@ -43,18 +43,14 @@ public static TPrefab[] AllComponentPrefabs if (field != null) return field; - List allPrefabs = ListPool.Shared.Rent(); - + List allPrefabs = new(); foreach (GameObject gameObject in NetworkClient.prefabs.Values) { if (gameObject.TryGetComponent(out TPrefab prefab)) allPrefabs.Add(prefab); } - field = allPrefabs.ToArray(); - ListPool.Shared.Return(allPrefabs); - - return field; + return field = allPrefabs.ToArray(); } } } \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomPlainTextSetting.cs b/SecretAPI/Features/UserSettings/CustomPlainTextSetting.cs index a06a1d9..a1016a2 100644 --- a/SecretAPI/Features/UserSettings/CustomPlainTextSetting.cs +++ b/SecretAPI/Features/UserSettings/CustomPlainTextSetting.cs @@ -7,6 +7,7 @@ /// /// Wrapper for . /// +// TODO: Make LastInputText nullable for 4.0 public abstract class CustomPlainTextSetting : CustomSetting, ISetting { /// @@ -108,7 +109,8 @@ public int CharacterLimit protected override void HandleBeforeSettingUpdate() { base.HandleBeforeSettingUpdate(); - LastInputText = InputText; + if (LastUpdateType != SettingResponseType.Initial) + LastInputText = InputText; } /// diff --git a/SecretAPI/Features/UserSettings/CustomSetting.cs b/SecretAPI/Features/UserSettings/CustomSetting.cs index f4434c2..011a44c 100644 --- a/SecretAPI/Features/UserSettings/CustomSetting.cs +++ b/SecretAPI/Features/UserSettings/CustomSetting.cs @@ -403,7 +403,7 @@ protected void ResyncToOwner() /// Checks if a player is able to view a setting. /// /// The player to check. - /// A value indicating whether a player is able to view the setting. + /// A value indicating whether a player is able to view the setting. This is true by default. protected virtual bool CanView(Player player) => true; /// diff --git a/SecretAPI/Features/UserSettings/SettingResponseType.cs b/SecretAPI/Features/UserSettings/SettingResponseType.cs index 9403cd9..eca2a34 100644 --- a/SecretAPI/Features/UserSettings/SettingResponseType.cs +++ b/SecretAPI/Features/UserSettings/SettingResponseType.cs @@ -16,7 +16,7 @@ public enum SettingResponseType Initial, /// - /// Indicates that this is an update, changing the value. + /// Indicates that this is an update, changing the value after initial response. /// Update, } \ No newline at end of file diff --git a/SecretAPI/Patches/Features/ReportDefinedSettingPatch.cs b/SecretAPI/Patches/Features/ReportDefinedSettingPatch.cs deleted file mode 100644 index 4993765..0000000 --- a/SecretAPI/Patches/Features/ReportDefinedSettingPatch.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SecretAPI.Patches.Features; - -using HarmonyLib; -using SecretAPI.Attributes; -using SecretAPI.Features.UserSettings; -using UserSettings.ServerSpecific; - -/// -/// Logs issues with . -/// -[HarmonyPatchCategory(nameof(CustomSetting))] -[HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.DefinedSettings), MethodType.Setter)] -internal static class ReportDefinedSettingPatch -{ - private static void Postfix(ref ServerSpecificSettingBase[] value) - { - value.ForEach(setting => CustomSetting.ValidateSettingInternal(null, setting)); - } -} \ No newline at end of file diff --git a/SecretAPI/Patches/Features/RoundEndIgnorePatch.cs b/SecretAPI/Patches/Features/RoundIgnore/RoundEndIgnorePatch.cs similarity index 97% rename from SecretAPI/Patches/Features/RoundEndIgnorePatch.cs rename to SecretAPI/Patches/Features/RoundIgnore/RoundEndIgnorePatch.cs index 0a6dc7a..750bb29 100644 --- a/SecretAPI/Patches/Features/RoundEndIgnorePatch.cs +++ b/SecretAPI/Patches/Features/RoundIgnore/RoundEndIgnorePatch.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Patches.Features; +namespace SecretAPI.Patches.Features.RoundIgnore; using System; using System.Collections.Generic; diff --git a/SecretAPI/Patches/Features/RoundIgnoreCountPatch.cs b/SecretAPI/Patches/Features/RoundIgnore/RoundIgnoreCountPatch.cs similarity index 97% rename from SecretAPI/Patches/Features/RoundIgnoreCountPatch.cs rename to SecretAPI/Patches/Features/RoundIgnore/RoundIgnoreCountPatch.cs index 51b79a1..0dbbb96 100644 --- a/SecretAPI/Patches/Features/RoundIgnoreCountPatch.cs +++ b/SecretAPI/Patches/Features/RoundIgnore/RoundIgnoreCountPatch.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Patches.Features; +namespace SecretAPI.Patches.Features.RoundIgnore; using System; using System.Collections.Generic; diff --git a/SecretAPI/Patches/Features/Settings/ReportDefinedSettingPatch.cs b/SecretAPI/Patches/Features/Settings/ReportDefinedSettingPatch.cs new file mode 100644 index 0000000..30ec0c5 --- /dev/null +++ b/SecretAPI/Patches/Features/Settings/ReportDefinedSettingPatch.cs @@ -0,0 +1,29 @@ +namespace SecretAPI.Patches.Features.Settings; + +using System.Collections.Generic; +using HarmonyLib; +using NorthwoodLib.Pools; +using SecretAPI.Attributes; +using SecretAPI.Features.UserSettings; +using UserSettings.ServerSpecific; + +/// +/// Logs issues with . +/// +[HarmonyPatchCategory(nameof(CustomSetting))] +[HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.DefinedSettings), MethodType.Setter)] +internal static class ReportDefinedSettingPatch +{ + private static void Postfix(ref ServerSpecificSettingBase[] value) + { + // OLD : value.ForEach(setting => CustomSetting.ValidateSettingInternal(null, setting)); + List settings = ListPool.Shared.Rent(); + foreach (ServerSpecificSettingBase setting in value) + { + if (CustomSetting.ValidateSettingInternal(null, setting)) + settings.Add(setting); + } + + value = settings.ToArray(); + } +} \ No newline at end of file diff --git a/SecretAPI/Patches/Features/SendSettingsPlayerSync.cs b/SecretAPI/Patches/Features/Settings/SendSettingsPlayerSync.cs similarity index 93% rename from SecretAPI/Patches/Features/SendSettingsPlayerSync.cs rename to SecretAPI/Patches/Features/Settings/SendSettingsPlayerSync.cs index 835231b..95ed920 100644 --- a/SecretAPI/Patches/Features/SendSettingsPlayerSync.cs +++ b/SecretAPI/Patches/Features/Settings/SendSettingsPlayerSync.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Patches.Features; +namespace SecretAPI.Patches.Features.Settings; using HarmonyLib; using LabApi.Features.Wrappers; diff --git a/SecretAPI/Patches/Features/SendSettingsServerSync.cs b/SecretAPI/Patches/Features/Settings/SendSettingsServerSync.cs similarity index 91% rename from SecretAPI/Patches/Features/SendSettingsServerSync.cs rename to SecretAPI/Patches/Features/Settings/SendSettingsServerSync.cs index 4f68402..1714753 100644 --- a/SecretAPI/Patches/Features/SendSettingsServerSync.cs +++ b/SecretAPI/Patches/Features/Settings/SendSettingsServerSync.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Patches.Features; +namespace SecretAPI.Patches.Features.Settings; using HarmonyLib; using SecretAPI.Attributes; diff --git a/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs b/SecretAPI/Patches/Features/Settings/SettingsOriginalDefinitionFix.cs similarity index 94% rename from SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs rename to SecretAPI/Patches/Features/Settings/SettingsOriginalDefinitionFix.cs index 87a96ec..c3f0a7f 100644 --- a/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs +++ b/SecretAPI/Patches/Features/Settings/SettingsOriginalDefinitionFix.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Patches.Features; +namespace SecretAPI.Patches.Features.Settings; using HarmonyLib; using SecretAPI.Attributes; diff --git a/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs b/SecretAPI/Patches/Features/Settings/SettingsSyncValidateFix.cs similarity index 94% rename from SecretAPI/Patches/Features/SettingsSyncValidateFix.cs rename to SecretAPI/Patches/Features/Settings/SettingsSyncValidateFix.cs index f1b1ca9..31cb578 100644 --- a/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs +++ b/SecretAPI/Patches/Features/Settings/SettingsSyncValidateFix.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Patches.Features; +namespace SecretAPI.Patches.Features.Settings; using HarmonyLib; using SecretAPI.Attributes; From c087cc23f6f371f2f25580930247f06f08a589e1 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:11:18 +0200 Subject: [PATCH 2/6] fix: Ensure future proofing --- .../Settings/ExampleFakeSyncButton.cs | 9 ++++ SecretAPI/Extensions/MirrorExtensions.cs | 51 ++++++++++++------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs index c923ec9..44d2da8 100644 --- a/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs +++ b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs @@ -5,6 +5,7 @@ using Mirror; using SecretAPI.Extensions; using SecretAPI.Features.UserSettings; +using UnityEngine; /// /// Example version for fake syncing on a . @@ -38,8 +39,16 @@ protected override void HandleSettingUpdate() textToy.TextFormat = "{0}"; textToy.Arguments.Add("Default Text!"); + CapybaraToy capybaraToy = CapybaraToy.Create(KnownOwner.Position + new Vector3(0, 1, 0), KnownOwner.Rotation); + capybaraToy.Base.NetworkCollisionsEnabled = true; + Timing.CallDelayed(5, () => { + // sync var example + KnownOwner.SendFakeSyncVar(capybaraToy.Base, 1L, KnownOwner.Position + new Vector3(0, 1, 0)); // position + KnownOwner.SendFakeSyncVar(capybaraToy.Base, 32L, false); // collisions + + // sync list example MirrorExtensions.SyncListChange change = new() { Operation = SyncList.Operation.OP_SET, diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs index 8421a8f..ab10269 100644 --- a/SecretAPI/Extensions/MirrorExtensions.cs +++ b/SecretAPI/Extensions/MirrorExtensions.cs @@ -17,9 +17,9 @@ public static class MirrorExtensions { private static readonly Dictionary TypeToWriterCache = new(); - private static readonly Dictionary SubWriteClassToMinULong = new() + private static readonly ExtraWriteInfo[] SubWriteInfos = new[] { - [typeof(AdminToyBase)] = 32, + new ExtraWriteInfo(typeof(AdminToyBase), 16UL /*Max written in AdminToyBase is 16*/, 1), }; /// @@ -106,19 +106,21 @@ public static void SendFakeSyncVar(this Player target, NetworkBehaviour behav // Always write the dirty bit writer.WriteULong(dirtyBit); - ulong minDirtyBit = GetSubclassMinDirtyBit(behaviour.GetType()); + ExtraWriteInfo info = GetExtraWriteInfo(behaviour.GetType()); bool isWritten = false; - if (dirtyBit >= minDirtyBit) + // the current bit being written is higher than the max of the base, so we need to write extras before the value + if (dirtyBit > info.MaxDirtyBit) { - writer.WriteULong(dirtyBit); + WriteExtraDirtyBit(info, writer, dirtyBit); isWritten = true; } writer.Write(value); + // bit is not higher than the max, and we must write more after the value is written if (!isWritten) - writer.WriteULong(dirtyBit); + WriteExtraDirtyBit(info, writer, dirtyBit); }); } @@ -137,7 +139,7 @@ public static void SendFakeState(this Player target, NetworkBehaviour behaviour, using NetworkWriterPooled pooledWriter = NetworkWriterPool.Get(); - // write the compressed bitmask + // write the compressed bitmask of the behaviour index int index = behaviour.netIdentity.NetworkBehaviours.IndexOf(behaviour); ulong mask = (ulong)(1 << index); Compression.CompressVarUInt(pooledWriter, mask); @@ -161,10 +163,7 @@ public static void SendFakeState(this Player target, NetworkBehaviour behaviour, { // 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); + WriteExtraDirtyBit(GetExtraWriteInfo(behaviour.GetType()), pooledWriter); } // fill in length hash as the last byte of the 4 byte length @@ -217,16 +216,24 @@ public static void ProperWrite(this NetworkWriter writer, object obj) dele.DynamicInvoke(writer, obj); } - private static ulong GetSubclassMinDirtyBit(Type type) + private static void WriteExtraDirtyBit(ExtraWriteInfo writeInfo, NetworkWriter writer, ulong dirtyBit = 0) { - // full credit to https://github.com/KadavasKingdom/LabApiExtensions/blob/main/LabApiExtensions/FakeExtension/FakeSyncVarExtension.cs#L17 for this - foreach (KeyValuePair kvp in SubWriteClassToMinULong) + if (!writeInfo.IsSet) + return; + + for (int index = 0; index < writeInfo.ExtraWriteCount; ++index) + writer.WriteULong(dirtyBit); + } + + private static ExtraWriteInfo GetExtraWriteInfo(Type type) + { + foreach (ExtraWriteInfo subWriteInfo in SubWriteInfos) { - if (type.IsSubclassOf(kvp.Key)) - return kvp.Value; + if (type.IsSubclassOf(subWriteInfo.ClassType)) + return subWriteInfo; } - return ulong.MaxValue; + return ExtraWriteInfo.None; } /// @@ -251,4 +258,14 @@ public struct SyncListChange /// public T Item; } + + private readonly struct ExtraWriteInfo(Type classType, ulong dirtyBit, int writeCount) + { + public static readonly ExtraWriteInfo None = new(null!, 0, 0); + + public readonly Type ClassType = classType; + public readonly ulong MaxDirtyBit = dirtyBit; + public readonly int ExtraWriteCount = writeCount; + public readonly bool IsSet = classType != null; + } } \ No newline at end of file From 3c20ccec5cdf686c87d6f34aa8432a0e82845df5 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:28:10 +0200 Subject: [PATCH 3/6] use default keyword --- SecretAPI/Extensions/MirrorExtensions.cs | 4 +--- SecretAPI/SecretApi.cs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs index ab10269..0e1fcdc 100644 --- a/SecretAPI/Extensions/MirrorExtensions.cs +++ b/SecretAPI/Extensions/MirrorExtensions.cs @@ -233,7 +233,7 @@ private static ExtraWriteInfo GetExtraWriteInfo(Type type) return subWriteInfo; } - return ExtraWriteInfo.None; + return default; } /// @@ -261,8 +261,6 @@ public struct SyncListChange private readonly struct ExtraWriteInfo(Type classType, ulong dirtyBit, int writeCount) { - public static readonly ExtraWriteInfo None = new(null!, 0, 0); - public readonly Type ClassType = classType; public readonly ulong MaxDirtyBit = dirtyBit; public readonly int ExtraWriteCount = writeCount; diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 3042b91..2268034 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 => PathManager.Configs.CreateSubdirectory("SecretAPI"); + internal static DirectoryInfo ConfigDirectory => field ??= PathManager.Configs.CreateSubdirectory("SecretAPI"); /// public override void Enable() From 9260713e35924d3aad55527224174da6ec5845b0 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:11:38 +0200 Subject: [PATCH 4/6] Minor --- SecretAPI/Debugging/PrefabDebugger.cs | 1 - SecretAPI/Extensions/MirrorExtensions.cs | 3 ++ SecretAPI/Features/DecalHelpers.cs | 28 ++++++------- .../Features/Effects/CustomPlayerEffect.cs | 40 +++++++++++++++++-- .../Effects/CustomTickingPlayerEffect.cs | 2 +- .../Features/UserSettings/CustomSetting.cs | 26 ++++++------ 6 files changed, 68 insertions(+), 32 deletions(-) diff --git a/SecretAPI/Debugging/PrefabDebugger.cs b/SecretAPI/Debugging/PrefabDebugger.cs index e7b28c6..98b5f3f 100644 --- a/SecretAPI/Debugging/PrefabDebugger.cs +++ b/SecretAPI/Debugging/PrefabDebugger.cs @@ -18,7 +18,6 @@ namespace SecretAPI.Debugging; /// /// Debugs base-game prefabs by logging information about them. /// -// ? Possibly write to file instead of server console? internal static class PrefabDebugger { /// diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs index 0e1fcdc..55a1078 100644 --- a/SecretAPI/Extensions/MirrorExtensions.cs +++ b/SecretAPI/Extensions/MirrorExtensions.cs @@ -64,7 +64,10 @@ public static void SendFakeRpcMessage(this Player target, NetworkBehaviour behav 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()}"); + return; // avoid client kick + } SendFakeState(target, behaviour, writer => { diff --git a/SecretAPI/Features/DecalHelpers.cs b/SecretAPI/Features/DecalHelpers.cs index 2283287..ce5f76e 100644 --- a/SecretAPI/Features/DecalHelpers.cs +++ b/SecretAPI/Features/DecalHelpers.cs @@ -15,9 +15,7 @@ namespace SecretAPI.Features; /// public static class DecalHelpers { - private static bool hasData = false; - private static ItemType itemType = ItemType.None; - private static byte subcomponentIndex = 0; + private static AutoSyncData? autoSyncData; /// /// Creates an for spawning a decal that can be sent to players. @@ -31,16 +29,16 @@ public static AutosyncMessage GetDecalMessage(Vector3 position, Vector3 startPos RelativePosition hitPoint = new(position); RelativePosition startRaycastPoint = new(startPosition); - (ItemType itemTypeId, byte moduleIndex) = GetItemData(); + AutoSyncData data = GetAutoSyncData(); - using NetworkWriterPooled? writer = NetworkWriterPool.Get(); - writer.WriteByte(moduleIndex); + using NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.WriteByte(data.SubcomponentIndex); writer.WriteSubheader(ImpactEffectsModule.RpcType.ImpactDecal); writer.WriteByte((byte)type); writer.WriteRelativePosition(hitPoint); writer.WriteRelativePosition(startRaycastPoint); - return new AutosyncMessage(writer, new ItemIdentifier(itemTypeId, 0)); + return new AutosyncMessage(writer, new ItemIdentifier(data.ItemType, 0)); } /// @@ -88,24 +86,24 @@ public static void SpawnDecalFromDirection(Vector3 position, Vector3 direction, public static void SpawnDecalFromDirection(Vector3 position, Quaternion direction, DecalPoolType type = DecalPoolType.Blood) => GetDecalMessage(position, position - (direction * Vector3.forward), type).SendToAuthenticated(); - private static (ItemType ItemType, byte SubcomponentIndex) GetItemData() + private static AutoSyncData GetAutoSyncData() { - if (hasData) - return (itemType, subcomponentIndex); + if (autoSyncData != null) + return autoSyncData; - foreach (ModularAutosyncItem? autoItem in ModularAutosyncItem.AllTemplates) + foreach (ModularAutosyncItem autoItem in ModularAutosyncItem.AllTemplates) { for (byte b = 0; b < autoItem.AllSubcomponents.Length; b++) { if (autoItem.AllSubcomponents[b] is not ImpactEffectsModule) continue; - subcomponentIndex = b; - itemType = autoItem.ItemTypeId; - hasData = true; - return (itemType, subcomponentIndex); + + return autoSyncData = new AutoSyncData(autoItem.ItemTypeId, b); } } throw new InvalidOperationException("Couldn't find the `InventorySystem.Items.Firearms.Modules.ImpactEffectsModule` in the any ModularAutosyncItem!"); } + + private sealed record AutoSyncData(ItemType ItemType, byte SubcomponentIndex); } \ No newline at end of file diff --git a/SecretAPI/Features/Effects/CustomPlayerEffect.cs b/SecretAPI/Features/Effects/CustomPlayerEffect.cs index dc7d58c..13699b4 100644 --- a/SecretAPI/Features/Effects/CustomPlayerEffect.cs +++ b/SecretAPI/Features/Effects/CustomPlayerEffect.cs @@ -10,23 +10,54 @@ using Logger = LabApi.Features.Console.Logger; +// ! PLEASE NW MAKE CUSTOM EFFECTS GOOD I'M GONNA CRY + /// /// Handles custom player effects. -/// Must register to to work. +/// Must register with in order to work. /// public abstract class CustomPlayerEffect : StatusEffectBase { /// /// Gets a list of types to register (Must inherit ). - /// Must be , can be gotten through typeof(Scp207) /// - public static List EffectsToRegister { get; } = []; + [Obsolete("This is becoming readonly in 4.0 - Use Register()")] + public static List EffectsToRegister { get; } = []; // TODO 4.0: private static HashSet RegisteredEffects /// /// Gets the with this effect. /// public Player Owner => field ??= Player.Get(Hub); + /// + /// Registers a custom effect type. + /// + /// The effect type to register. + public static void Register() + where T : StatusEffectBase => Register(typeof(T)); + + /// + /// Registers a custom effect type. + /// + /// The to register. May not be abstract and must inherit . + public static void Register(Type type) + { + if (!type.IsAssignableFrom(typeof(StatusEffectBase))) + { + Logger.Error($"[CustomPlayerEffect.Register] {type.FullName} does not inherit StatusEffectBase!"); + return; + } + + if (type.IsAbstract) + { + Logger.Error($"[CustomPlayerEffect.Register] {type.FullName} is abstract!"); + return; + } + +#pragma warning disable CS0618 // Type or member is obsolete + EffectsToRegister.Add(type); + } + /// public override string ToString() => $"{GetType().Name}: Owner ({Owner}) - Intensity ({Intensity}) - Duration {Duration}"; @@ -41,6 +72,9 @@ internal static void Initialize() EffectsToRegister.Add(typeof(Depleted)); EffectsToRegister.Add(typeof(BlastResistance)); + // it would probably be better to patch PlayerEffectsController::Awake instead of using sceneLoaded + // this would also allow registering after players join + // although the current way might be the fastest since its just base unity handling it on the prefab SceneManager.sceneLoaded += OnSceneLoaded; } diff --git a/SecretAPI/Features/Effects/CustomTickingPlayerEffect.cs b/SecretAPI/Features/Effects/CustomTickingPlayerEffect.cs index b948220..3355e49 100644 --- a/SecretAPI/Features/Effects/CustomTickingPlayerEffect.cs +++ b/SecretAPI/Features/Effects/CustomTickingPlayerEffect.cs @@ -4,7 +4,7 @@ using UnityEngine; /// -/// Custom Effect for . +/// Custom version of for . /// public abstract class CustomTickingPlayerEffect : CustomPlayerEffect { diff --git a/SecretAPI/Features/UserSettings/CustomSetting.cs b/SecretAPI/Features/UserSettings/CustomSetting.cs index 011a44c..5715e44 100644 --- a/SecretAPI/Features/UserSettings/CustomSetting.cs +++ b/SecretAPI/Features/UserSettings/CustomSetting.cs @@ -58,9 +58,8 @@ protected CustomSetting(ServerSpecificSettingBase setting) public ServerSpecificSettingBase Base { get; } /// - /// Gets the known owner. + /// Gets the known owner or on the initial object. /// - /// This is null on the original object. public Player? KnownOwner { get; private set; } /// @@ -75,7 +74,7 @@ protected CustomSetting(ServerSpecificSettingBase setting) public SettingResponseType LastUpdateType { get; private set; } = SettingResponseType.None; /// - /// Gets a value indicating whether the current value received is different to that prior to the most recent call. + /// Gets a value indicating whether the current value received is different to that prior to the most recent call. /// public virtual bool HasValueChanged { get; } = false; @@ -154,10 +153,7 @@ public bool IsShared set => CollectionId = value ? byte.MinValue : byte.MaxValue; } - /// - /// Registers a collection of settings. - /// - /// The settings to register. + /// public static void Register(params CustomSetting[] settings) => Register((IEnumerable)settings); /// @@ -173,11 +169,8 @@ public static void Register(IEnumerable settings) } } - /// - /// Unregisters collection of settings. - /// - /// The settings to unregister. - public static void UnRegister(params CustomSetting[] settings) => CustomSettings.RemoveAll(settings.Contains); + /// + public static void UnRegister(params CustomSetting[] settings) => UnRegister((IEnumerable)settings); /// /// Unregisters a collection of settings. @@ -185,6 +178,15 @@ public static void Register(IEnumerable settings) /// The settings to unregister. public static void UnRegister(IEnumerable settings) => CustomSettings.RemoveAll(settings.Contains); + /// + public static void UnRegister(params Type[] types) => UnRegister((IEnumerable)types); + + /// + /// Unregisters a collection of setting based on their . + /// + /// A collection of setting types to unregister. This should be the exact type, not the base. + public static void UnRegister(IEnumerable types) => CustomSettings.RemoveAll(s => types.Any(t => s.GetType() == t)); + /// /// Tries to get player specific setting. /// From 3c67ec2c79723360fdfa3ec3f78b20ceea4a86c7 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:30:30 +0200 Subject: [PATCH 5/6] Example for fake syncing ammo --- SecretAPI.Examples/ExampleEntry.cs | 5 +++ .../Patches/ExampleAmmoPatch.cs | 34 +++++++++++++++++++ SecretAPI.Examples/Patches/ExamplePatch.cs | 23 ------------- .../Settings/ExampleFakeSyncButton.cs | 25 ++++++++++++++ 4 files changed, 64 insertions(+), 23 deletions(-) create mode 100644 SecretAPI.Examples/Patches/ExampleAmmoPatch.cs delete mode 100644 SecretAPI.Examples/Patches/ExamplePatch.cs diff --git a/SecretAPI.Examples/ExampleEntry.cs b/SecretAPI.Examples/ExampleEntry.cs index bf6b9d9..1839237 100644 --- a/SecretAPI.Examples/ExampleEntry.cs +++ b/SecretAPI.Examples/ExampleEntry.cs @@ -1,8 +1,10 @@ namespace SecretAPI.Examples; using System; +using HarmonyLib; using LabApi.Loader.Features.Plugins; using SecretAPI.Examples.Settings; +using SecretAPI.Extensions; using SecretAPI.Features.UserSettings; /// @@ -10,6 +12,8 @@ /// public class ExampleEntry : Plugin { + private Harmony harmony = new("SecretAPI.Examples"); + /// public override string Name => "SecretAPI.Examples"; @@ -29,6 +33,7 @@ public class ExampleEntry : Plugin public override void Enable() { CustomSetting.Register(new ExampleKeybindSetting(), new ExampleDropdownSetting(), new ExampleButtonSetting(), new ExampleFakeSyncButton()); + harmony.PatchCategory(nameof(ExampleFakeSyncButton)); } /// diff --git a/SecretAPI.Examples/Patches/ExampleAmmoPatch.cs b/SecretAPI.Examples/Patches/ExampleAmmoPatch.cs new file mode 100644 index 0000000..46467c6 --- /dev/null +++ b/SecretAPI.Examples/Patches/ExampleAmmoPatch.cs @@ -0,0 +1,34 @@ +namespace SecretAPI.Examples.Patches; + +using HarmonyLib; +using InventorySystem.Configs; +using SecretAPI.Attributes; +using SecretAPI.Examples.Settings; + +/// +/// An example harmony patch. +/// +[HarmonyPatchCategory(nameof(ExampleFakeSyncButton))] +[HarmonyPatch(typeof(InventoryLimits), nameof(InventoryLimits.GetAmmoLimit), [typeof(ItemType), typeof(ReferenceHub)])] +public static class ExampleAmmoPatch +{ + // gets called before the original method is called + private static void Prefix() + { + // we return void so original method is always run + } + + // gets called after the original method is called + // We grab the method params of ammoType and player, the names must be correct + // ref __result will become a reference to the return value +#pragma warning disable SA1313 + private static void Postfix(ItemType ammoType, ReferenceHub player, ref ushort __result) +#pragma warning restore SA1313 + { + // make sure we don't modify the max ammo if its not the correct ammo type or the player hasn't been fake synced + if (ammoType != ExampleFakeSyncButton.AmmoFakeSync || !ExampleFakeSyncButton.FakeSyncs.TryGetValue(player, out ushort sync)) + return; + + __result = sync; + } +} \ No newline at end of file diff --git a/SecretAPI.Examples/Patches/ExamplePatch.cs b/SecretAPI.Examples/Patches/ExamplePatch.cs deleted file mode 100644 index 29cd386..0000000 --- a/SecretAPI.Examples/Patches/ExamplePatch.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace SecretAPI.Examples.Patches; - -using SecretAPI.Attributes; - -/// -/// An example harmony patch. -/// -[HarmonyPatchCategory(nameof(ExampleEntry))] -/*[HarmonyPatch]*/ -public static class ExamplePatch -{ - // gets called before the original method is called - private static bool Prefix() - { - // prevent original method from running - return false; - } - - // gets called after the original method is called - private static void Postfix() - { - } -} \ No newline at end of file diff --git a/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs index 44d2da8..54d16d9 100644 --- a/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs +++ b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs @@ -1,5 +1,6 @@ namespace SecretAPI.Examples.Settings; +using System.Collections.Generic; using LabApi.Features.Wrappers; using MEC; using Mirror; @@ -20,6 +21,16 @@ public ExampleFakeSyncButton() { } + /// + /// Gets a dictionary of to theirfake synced ammos. + /// + public static Dictionary FakeSyncs { get; } = new(); + + /// + /// Gets the who's ammo is being fake synced. + /// + public static ItemType AmmoFakeSync => ItemType.Ammo9x19; + /// public override CustomHeader Header => CustomHeader.Examples; @@ -35,6 +46,20 @@ protected override void HandleSettingUpdate() if (KnownOwner == null) return; + // fake syncing ammo limits + // you must patch the values on server-side too, refer to ExampleAmmoPatch.cs + FakeSyncs[KnownOwner.ReferenceHub] = ushort.MinValue; + KnownOwner.SendFakeSyncListData(ServerConfigSynchronizer.Singleton, 2, new() + { + Operation = SyncList.Operation.OP_SET, + Index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(limit => limit.AmmoType == AmmoFakeSync), + Item = new ServerConfigSynchronizer.AmmoLimit() + { + AmmoType = AmmoFakeSync, + Limit = ushort.MinValue, + }, + }); + TextToy textToy = TextToy.Create(KnownOwner.Position, KnownOwner.Rotation); textToy.TextFormat = "{0}"; textToy.Arguments.Add("Default Text!"); From ae81c685b23522ea7fd6b6284e0b25c6c609dc78 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:32:55 +0200 Subject: [PATCH 6/6] Version bump | 3.4.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 3791e55..24401c1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@  enable - 3.3.0 + 3.4.0