Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<Version>3.3.0</Version>
<Version>3.4.0</Version>
</PropertyGroup>

<PropertyGroup Label="Nuget">
Expand Down
5 changes: 5 additions & 0 deletions SecretAPI.Examples/ExampleEntry.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
namespace SecretAPI.Examples;

using System;
using HarmonyLib;
using LabApi.Loader.Features.Plugins;
using SecretAPI.Examples.Settings;
using SecretAPI.Extensions;
using SecretAPI.Features.UserSettings;

/// <summary>
/// Defines the entry for the plugin.
/// </summary>
public class ExampleEntry : Plugin
{
private Harmony harmony = new("SecretAPI.Examples");

/// <inheritdoc/>
public override string Name => "SecretAPI.Examples";

Expand All @@ -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));
}

/// <inheritdoc/>
Expand Down
34 changes: 34 additions & 0 deletions SecretAPI.Examples/Patches/ExampleAmmoPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace SecretAPI.Examples.Patches;

using HarmonyLib;
using InventorySystem.Configs;
using SecretAPI.Attributes;
using SecretAPI.Examples.Settings;

/// <summary>
/// An example harmony patch.
/// </summary>
[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;
}
}
23 changes: 0 additions & 23 deletions SecretAPI.Examples/Patches/ExamplePatch.cs

This file was deleted.

34 changes: 34 additions & 0 deletions SecretAPI.Examples/Settings/ExampleFakeSyncButton.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
namespace SecretAPI.Examples.Settings;

using System.Collections.Generic;
using LabApi.Features.Wrappers;
using MEC;
using Mirror;
using SecretAPI.Extensions;
using SecretAPI.Features.UserSettings;
using UnityEngine;

/// <summary>
/// Example version for fake syncing on a <see cref="CustomButtonSetting"/>.
Expand All @@ -19,6 +21,16 @@ public ExampleFakeSyncButton()
{
}

/// <summary>
/// Gets a dictionary of <see cref="ReferenceHub"/> to theirfake synced ammos.
/// </summary>
public static Dictionary<ReferenceHub, ushort> FakeSyncs { get; } = new();

/// <summary>
/// Gets the <see cref="ItemType"/> who's ammo is being fake synced.
/// </summary>
public static ItemType AmmoFakeSync => ItemType.Ammo9x19;

/// <inheritdoc />
public override CustomHeader Header => CustomHeader.Examples;

Expand All @@ -34,12 +46,34 @@ 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.AmmoLimit>(ServerConfigSynchronizer.Singleton, 2, new()
{
Operation = SyncList<ServerConfigSynchronizer.AmmoLimit>.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!");

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<string> change = new()
{
Operation = SyncList<string>.Operation.OP_SET,
Expand Down
1 change: 0 additions & 1 deletion SecretAPI/Debugging/PrefabDebugger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ namespace SecretAPI.Debugging;
/// <summary>
/// Debugs base-game prefabs by logging information about them.
/// </summary>
// ? Possibly write to file instead of server console?
internal static class PrefabDebugger
{
/// <summary>
Expand Down
67 changes: 48 additions & 19 deletions SecretAPI/Extensions/MirrorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
/// </summary>
public static class MirrorExtensions
{
private static readonly Dictionary<Type, ulong> SubWriteClassToMinULong = new()
private static readonly Dictionary<Type, Delegate> TypeToWriterCache = new();
private static readonly ExtraWriteInfo[] SubWriteInfos = new[]
{
[typeof(AdminToyBase)] = 32,
new ExtraWriteInfo(typeof(AdminToyBase), 16UL /*Max written in AdminToyBase is 16*/, 1),
};

/// <summary>
Expand Down Expand Up @@ -63,7 +64,10 @@ public static void SendFakeRpcMessage(this Player target, NetworkBehaviour behav
public static void SendFakeSyncListData<T>(this Player target, NetworkBehaviour behaviour, ulong listIndex, SyncListChange<T> 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 =>
{
Expand Down Expand Up @@ -105,19 +109,21 @@ public static void SendFakeSyncVar<T>(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);
});
}

Expand All @@ -136,7 +142,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);
Expand All @@ -160,10 +166,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
Expand All @@ -188,6 +191,15 @@ public static void SendFakeState(this Player target, NetworkBehaviour behaviour,
/// <param name="obj">The object to write.</param>
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)
Expand All @@ -197,25 +209,34 @@ 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)
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<Type, ulong> 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 default;
}

/// <summary>
Expand All @@ -240,4 +261,12 @@ public struct SyncListChange<T>
/// </summary>
public T Item;
}

private readonly struct ExtraWriteInfo(Type classType, ulong dirtyBit, int writeCount)
{
public readonly Type ClassType = classType;
public readonly ulong MaxDirtyBit = dirtyBit;
public readonly int ExtraWriteCount = writeCount;
public readonly bool IsSet = classType != null;
}
}
28 changes: 13 additions & 15 deletions SecretAPI/Features/DecalHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ namespace SecretAPI.Features;
/// </summary>
public static class DecalHelpers
{
private static bool hasData = false;
private static ItemType itemType = ItemType.None;
private static byte subcomponentIndex = 0;
private static AutoSyncData? autoSyncData;

/// <summary>
/// Creates an <see cref="AutosyncMessage"/> for spawning a decal that can be sent to players.
Expand All @@ -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));
}

/// <summary>
Expand Down Expand Up @@ -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);
}
4 changes: 2 additions & 2 deletions SecretAPI/Features/Effects/BlastResistance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
public class BlastResistance : CustomPlayerEffect, IDamageModifierEffect
{
/// <inheritdoc />
public bool DamageModifierActive => IsEnabled;
public override byte MaxIntensity => 200;

/// <inheritdoc />
public override byte MaxIntensity => 200;
public bool DamageModifierActive => IsEnabled;

/// <inheritdoc />
public float GetDamageModifier(float baseDamage, DamageHandlerBase handler, HitboxType hitboxType)
Expand Down
Loading
Loading