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()
{