diff --git a/SecretAPI.Examples/ExampleEntry.cs b/SecretAPI.Examples/ExampleEntry.cs index c0784a0..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()); + CustomSetting.Register(new ExampleKeybindSetting(), new ExampleDropdownSetting(), new ExampleButtonSetting(), new ExampleFakeSyncButton()); } /// diff --git a/SecretAPI.Examples/Settings/ExampleButtonSetting.cs b/SecretAPI.Examples/Settings/ExampleButtonSetting.cs new file mode 100644 index 0000000..0d46347 --- /dev/null +++ b/SecretAPI.Examples/Settings/ExampleButtonSetting.cs @@ -0,0 +1,38 @@ +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(duration: 30); + KnownOwner.EnableEffect(200, 30); + } +} \ 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/ExampleFakeSyncButton.cs b/SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs new file mode 100644 index 0000000..c923ec9 --- /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() + { + Operation = SyncList.Operation.OP_SET, + Index = 0, + Item = "Fake synced text!", + }; + + KnownOwner.SendFakeSyncListData(textToy.Base, 1L, change); + }); + } +} \ No newline at end of file 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..e7b28c6 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"), 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/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs index 4b2dc81..5b4058d 100644 --- a/SecretAPI/Extensions/MirrorExtensions.cs +++ b/SecretAPI/Extensions/MirrorExtensions.cs @@ -1,16 +1,26 @@ namespace SecretAPI.Extensions; using System; +using System.Collections.Generic; +using System.Diagnostics; 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 +31,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 +52,135 @@ 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 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 + 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 +205,39 @@ 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 representation of the internal Change class. + /// + /// The item being contained by the . + public struct SyncListChange + { + /// + /// The operation to do on the . + /// + 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 or . + /// + public T Item; + } } \ No newline at end of file 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 5b77bec..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; /// @@ -33,11 +33,13 @@ public abstract class CustomPlayerEffect : StatusEffectBase /// /// Initializes the to implement . /// + /// The effects must be loaded on before is created. [CallOnLoad] internal static void Initialize() { EffectsToRegister.Add(typeof(Energized)); EffectsToRegister.Add(typeof(Depleted)); + EffectsToRegister.Add(typeof(BlastResistance)); SceneManager.sceneLoaded += OnSceneLoaded; } @@ -51,7 +53,7 @@ private static void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (!typeof(StatusEffectBase).IsAssignableFrom(type)) { - Logger.Error($"[CustomPlayerEffect.Initialize] {type.FullName} is not a valid StatusEffectBase and thus could not be registered!"); + Logger.Error($"[CustomPlayerEffect.OnSceneLoaded] {type.FullName} is not a valid StatusEffectBase and thus could not be registered!"); continue; } 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..df65faa 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,26 @@ 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. + /// + [Obsolete("Use TimeSinceLastPress instead - This will be removed in 4.0")] + public TimeSpan LastPress => TimeSinceLastPress; + /// /// Gets the of the last press. /// - public TimeSpan LastPress => Base.SyncLastPress.Elapsed; + public TimeSpan TimeSinceLastPress => LastPressWatch.Elapsed; + + /// + /// Gets a value indicating whether the button has ever been pressed. + /// + public bool EverPressed => LastPressWatch.IsRunning; /// /// Gets or sets the text of the button. diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 2a288bd..3042b91 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 => PathManager.Configs.CreateSubdirectory("SecretAPI"); + /// public override void Enable() {