581 lines
18 KiB
C#
581 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using BepInEx;
|
|
using BepInEx.Configuration;
|
|
using BepInEx.Logging;
|
|
using BepInEx.Unity.IL2CPP;
|
|
using FishNet;
|
|
using FishNet.Connection;
|
|
using FishNet.Managing.Client;
|
|
using FishNet.Managing.Server;
|
|
using FishNet.Object;
|
|
using FishNet.Serializing;
|
|
using FishNet.Transporting;
|
|
using HarmonyLib;
|
|
using Il2CppInterop.Runtime;
|
|
using Il2CppInterop.Runtime.Injection;
|
|
using Skopje.Modding;
|
|
using UnityEngine;
|
|
|
|
namespace PeePeeMod
|
|
{
|
|
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
|
public class PeePeePlugin : BasePlugin
|
|
{
|
|
public const string PluginGuid = "com.skopje.peepeemod";
|
|
public const string PluginName = "PeePeeMod";
|
|
public const string PluginVersion = "1.0.0";
|
|
|
|
internal static ManualLogSource LogSource;
|
|
internal static ConfigEntry<bool> Enabled;
|
|
internal static ConfigEntry<KeyCode> Key;
|
|
internal static ConfigEntry<bool> Toggle;
|
|
|
|
internal static GameObject Host;
|
|
|
|
static Harmony _harmony;
|
|
static SkopjePlayer _local;
|
|
static bool _pissing;
|
|
static bool _latched;
|
|
static float _nextSend;
|
|
static int _errors;
|
|
static bool _loggedInput;
|
|
|
|
static Il2CppSystem.Action _onClientStarted;
|
|
static Il2CppSystem.Action _onClientStopped;
|
|
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStarted;
|
|
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStopped;
|
|
static Il2CppSystem.Action _onGameOver;
|
|
|
|
public override void Load()
|
|
{
|
|
LogSource = Log;
|
|
Enabled = Config.Bind("Pee", "Enabled", true, "Hold the key to piss. Everyone in the session sees the stream.");
|
|
Key = Config.Bind("Pee", "Key", KeyCode.P, "Button that starts pissing.");
|
|
Toggle = Config.Bind("Pee", "Toggle", false, "Press once to start and again to stop. Off means hold the key, like Postal.");
|
|
|
|
ClassInjector.RegisterTypeInIl2Cpp<PeeDriver>();
|
|
|
|
_onClientStarted = DelegateSupport.ConvertDelegate<Il2CppSystem.Action>(new Action(OnClientStarted));
|
|
_onClientStopped = DelegateSupport.ConvertDelegate<Il2CppSystem.Action>(new Action(OnClientStopped));
|
|
_onPlayerStarted = DelegateSupport.ConvertDelegate<Il2CppSystem.Action<SkopjePlayer>>(new Action<SkopjePlayer>(OnPlayerStarted));
|
|
_onPlayerStopped = DelegateSupport.ConvertDelegate<Il2CppSystem.Action<SkopjePlayer>>(new Action<SkopjePlayer>(OnPlayerStopped));
|
|
_onGameOver = DelegateSupport.ConvertDelegate<Il2CppSystem.Action>(new Action(OnGameOver));
|
|
|
|
ModAPI.add_OnClientStarted(_onClientStarted);
|
|
ModAPI.add_OnClientStopped(_onClientStopped);
|
|
ModAPI.add_OnPlayerStartedClient(_onPlayerStarted);
|
|
ModAPI.add_OnPlayerStoppedClient(_onPlayerStopped);
|
|
ModAPI.add_OnGameOver(_onGameOver);
|
|
|
|
_harmony = new Harmony(PluginGuid);
|
|
Patch(AccessTools.Method(typeof(ServerManager), "ParseBroadcast"), nameof(PeeNet.BeforeServerBroadcast));
|
|
Patch(AccessTools.Method(typeof(ClientManager), "ParseBroadcast"), nameof(PeeNet.BeforeClientBroadcast));
|
|
|
|
Host = new GameObject("PeePeeMod");
|
|
UnityEngine.Object.DontDestroyOnLoad(Host);
|
|
Host.hideFlags = HideFlags.HideAndDontSave;
|
|
Host.AddComponent<PeeDriver>();
|
|
|
|
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded. Hold {Key.Value} to piss. The stream is a server RPC relayed to every client.");
|
|
}
|
|
|
|
public override bool Unload()
|
|
{
|
|
_pissing = false;
|
|
_latched = false;
|
|
_local = null;
|
|
if (_harmony != null)
|
|
_harmony.UnpatchSelf();
|
|
if (_onClientStarted != null)
|
|
ModAPI.remove_OnClientStarted(_onClientStarted);
|
|
if (_onClientStopped != null)
|
|
ModAPI.remove_OnClientStopped(_onClientStopped);
|
|
if (_onPlayerStarted != null)
|
|
ModAPI.remove_OnPlayerStartedClient(_onPlayerStarted);
|
|
if (_onPlayerStopped != null)
|
|
ModAPI.remove_OnPlayerStoppedClient(_onPlayerStopped);
|
|
if (_onGameOver != null)
|
|
ModAPI.remove_OnGameOver(_onGameOver);
|
|
PeeVisual.Clear();
|
|
PeePlayers.Clear();
|
|
if (Host != null)
|
|
{
|
|
UnityEngine.Object.Destroy(Host);
|
|
Host = null;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void Patch(System.Reflection.MethodInfo method, string prefix)
|
|
{
|
|
if (method == null)
|
|
{
|
|
LogSource.LogWarning("Pee RPC hook was not found: " + prefix);
|
|
return;
|
|
}
|
|
|
|
_harmony.Patch(method, prefix: new HarmonyMethod(typeof(PeeNet), prefix));
|
|
}
|
|
|
|
internal static void Fail(Exception ex)
|
|
{
|
|
if (_errors >= 8 || LogSource == null)
|
|
return;
|
|
_errors++;
|
|
LogSource.LogError("PeePeeMod failed: " + ex.Message);
|
|
}
|
|
|
|
internal static void Tick()
|
|
{
|
|
SkopjePlayer player = FindLocal();
|
|
bool want = WantPee(player);
|
|
Vector3 aim = player != null ? PeeVisual.AimOf(player) : Vector3.forward;
|
|
|
|
if (want != _pissing)
|
|
{
|
|
_pissing = want;
|
|
_nextSend = Time.unscaledTime + 0.12f;
|
|
if (want && player != null)
|
|
PeeVisual.ShowLocal(player, aim);
|
|
else if (player != null)
|
|
PeeVisual.Hide(player.ObjectId);
|
|
if (player != null)
|
|
PeeNet.ServerRpc(player, want, aim, true);
|
|
}
|
|
else if (want && player != null)
|
|
{
|
|
PeeVisual.ShowLocal(player, aim);
|
|
if (Time.unscaledTime >= _nextSend)
|
|
{
|
|
_nextSend = Time.unscaledTime + 0.12f;
|
|
PeeNet.ServerRpc(player, true, aim, false);
|
|
}
|
|
}
|
|
|
|
PeeVisual.Tick();
|
|
}
|
|
|
|
static bool WantPee(SkopjePlayer player)
|
|
{
|
|
if (Enabled == null || !Enabled.Value || player == null || !CanPee(player))
|
|
{
|
|
_latched = false;
|
|
return false;
|
|
}
|
|
|
|
KeyCode key = Key != null ? Key.Value : KeyCode.None;
|
|
if (Toggle != null && Toggle.Value)
|
|
{
|
|
if (Pressed(key))
|
|
_latched = !_latched;
|
|
return _latched;
|
|
}
|
|
|
|
return Held(key);
|
|
}
|
|
|
|
static bool CanPee(SkopjePlayer player)
|
|
{
|
|
if (player.IsDead || player.IsFallen || player.IsSpectating || player.IsPaused || player.InInventory)
|
|
return false;
|
|
if (player.InsideVehicle != null)
|
|
return false;
|
|
return player.NetworkObject != null && player.NetworkObject.IsSpawned;
|
|
}
|
|
|
|
static SkopjePlayer FindLocal()
|
|
{
|
|
return _local;
|
|
}
|
|
|
|
static void OnClientStarted()
|
|
{
|
|
_pissing = false;
|
|
_latched = false;
|
|
}
|
|
|
|
static void OnClientStopped()
|
|
{
|
|
_pissing = false;
|
|
_latched = false;
|
|
_local = null;
|
|
PeeVisual.Clear();
|
|
PeePlayers.Clear();
|
|
}
|
|
|
|
static void OnPlayerStarted(SkopjePlayer player)
|
|
{
|
|
if (player != null && player.IsOwner)
|
|
_local = player;
|
|
PeePlayers.Remember(player);
|
|
}
|
|
|
|
static void OnPlayerStopped(SkopjePlayer player)
|
|
{
|
|
if (player != null)
|
|
{
|
|
PeeVisual.Hide(player.ObjectId);
|
|
PeeVisual.Forget(player.ObjectId);
|
|
}
|
|
PeePlayers.Forget(player);
|
|
if (_local == player)
|
|
{
|
|
_local = null;
|
|
_pissing = false;
|
|
_latched = false;
|
|
}
|
|
}
|
|
|
|
static void OnGameOver()
|
|
{
|
|
_pissing = false;
|
|
_latched = false;
|
|
PeeVisual.Clear();
|
|
}
|
|
|
|
static bool Pressed(KeyCode key)
|
|
{
|
|
if (key == KeyCode.None)
|
|
return false;
|
|
try
|
|
{
|
|
return Input.GetKeyDown(key);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_loggedInput)
|
|
{
|
|
_loggedInput = true;
|
|
LogSource.LogWarning("Keyboard input is unavailable: " + ex.Message);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool Held(KeyCode key)
|
|
{
|
|
if (key == KeyCode.None)
|
|
return false;
|
|
try
|
|
{
|
|
return Input.GetKey(key);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_loggedInput)
|
|
{
|
|
_loggedInput = true;
|
|
LogSource.LogWarning("Keyboard input is unavailable: " + ex.Message);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class PeeDriver : MonoBehaviour
|
|
{
|
|
void Update()
|
|
{
|
|
try
|
|
{
|
|
PeePeePlugin.Tick();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PeePeePlugin.Fail(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
static class PeePlayers
|
|
{
|
|
static readonly Dictionary<int, SkopjePlayer> _players = new Dictionary<int, SkopjePlayer>();
|
|
|
|
public static void Remember(SkopjePlayer player)
|
|
{
|
|
if (player == null)
|
|
return;
|
|
try
|
|
{
|
|
if (player.NetworkObject == null)
|
|
return;
|
|
_players[player.ObjectId] = player;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
public static void Forget(SkopjePlayer player)
|
|
{
|
|
if (player == null)
|
|
return;
|
|
_players.Remove(player.ObjectId);
|
|
}
|
|
|
|
public static void Clear()
|
|
{
|
|
_players.Clear();
|
|
}
|
|
|
|
public static SkopjePlayer Find(int objectId)
|
|
{
|
|
SkopjePlayer player;
|
|
if (_players.TryGetValue(objectId, out player))
|
|
return player;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static class PeeNet
|
|
{
|
|
const byte Magic = 0xA7;
|
|
static readonly ushort Key = Hash("Skopje.PeePeeMod.PeeBroadcast");
|
|
|
|
static ushort _seq = 1;
|
|
static readonly Dictionary<int, float> _relayAt = new Dictionary<int, float>();
|
|
static bool _loggedRelay;
|
|
static bool _loggedSeen;
|
|
|
|
public static void ServerRpc(SkopjePlayer player, bool active, Vector3 aim, bool reliable)
|
|
{
|
|
if (player == null || player.NetworkObject == null || !player.NetworkObject.IsSpawned)
|
|
return;
|
|
if (!InstanceFinder.IsClientStarted)
|
|
return;
|
|
|
|
if (aim.sqrMagnitude < 0.0001f)
|
|
aim = Vector3.forward;
|
|
else
|
|
aim.Normalize();
|
|
|
|
ushort seq = _seq++;
|
|
if (_seq == 0)
|
|
_seq = 1;
|
|
|
|
Send(true, reliable ? Channel.Reliable : Channel.Unreliable, player.ObjectId, active, aim, seq);
|
|
}
|
|
|
|
public static bool BeforeServerBroadcast(PooledReader reader, NetworkConnection conn, Channel channel)
|
|
{
|
|
int start = reader != null ? reader.Position : 0;
|
|
int end;
|
|
int objectId;
|
|
bool active;
|
|
Vector3 aim;
|
|
ushort seq;
|
|
if (!TryRead(reader, start, out end, out objectId, out active, out aim, out seq))
|
|
return true;
|
|
|
|
try
|
|
{
|
|
Relay(conn, channel, objectId, active, aim, seq);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PeePeePlugin.Fail(ex);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool BeforeClientBroadcast(PooledReader reader, Channel channel)
|
|
{
|
|
int start = reader != null ? reader.Position : 0;
|
|
int end;
|
|
int objectId;
|
|
bool active;
|
|
Vector3 aim;
|
|
ushort seq;
|
|
if (!TryRead(reader, start, out end, out objectId, out active, out aim, out seq))
|
|
return true;
|
|
|
|
try
|
|
{
|
|
ObserversRpc(objectId, active, aim, seq);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PeePeePlugin.Fail(ex);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void Relay(NetworkConnection conn, Channel channel, int objectId, bool active, Vector3 aim, ushort seq)
|
|
{
|
|
if (conn == null || !conn.IsAuthenticated || !InstanceFinder.IsServerStarted)
|
|
return;
|
|
|
|
SkopjePlayer player = PeePlayers.Find(objectId);
|
|
if (player == null || !Owns(player, conn))
|
|
return;
|
|
if (active && !Allow(conn.ClientId))
|
|
return;
|
|
|
|
if (!_loggedRelay && PeePeePlugin.LogSource != null)
|
|
{
|
|
_loggedRelay = true;
|
|
PeePeePlugin.LogSource.LogInfo("Pee server RPC accepted. Relaying to observers.");
|
|
}
|
|
|
|
Send(false, channel, objectId, active, aim, seq);
|
|
}
|
|
|
|
static void ObserversRpc(int objectId, bool active, Vector3 aim, ushort seq)
|
|
{
|
|
SkopjePlayer player = PeePlayers.Find(objectId);
|
|
if (player == null || player.IsOwner)
|
|
return;
|
|
if (!PeeVisual.Accept(objectId, seq))
|
|
return;
|
|
|
|
if (!_loggedSeen && PeePeePlugin.LogSource != null)
|
|
{
|
|
_loggedSeen = true;
|
|
PeePeePlugin.LogSource.LogInfo("Pee observers RPC received.");
|
|
}
|
|
|
|
if (active)
|
|
PeeVisual.ShowRemote(player, aim);
|
|
else
|
|
PeeVisual.Hide(objectId);
|
|
}
|
|
|
|
static bool Owns(SkopjePlayer player, NetworkConnection conn)
|
|
{
|
|
if (player.Owner == conn)
|
|
return true;
|
|
return player.OwnerId == conn.ClientId;
|
|
}
|
|
|
|
static bool Allow(int clientId)
|
|
{
|
|
float now = Time.unscaledTime;
|
|
float last;
|
|
if (_relayAt.TryGetValue(clientId, out last) && now - last < 0.05f)
|
|
return false;
|
|
_relayAt[clientId] = now;
|
|
return true;
|
|
}
|
|
|
|
static void Send(bool toServer, Channel channel, int objectId, bool active, Vector3 aim, ushort seq)
|
|
{
|
|
var manager = InstanceFinder.NetworkManager;
|
|
if (manager == null || manager.TransportManager == null)
|
|
return;
|
|
if (toServer && !InstanceFinder.IsClientStarted)
|
|
return;
|
|
if (!toServer && !InstanceFinder.IsServerStarted)
|
|
return;
|
|
|
|
PooledWriter data = WriterPool.Retrieve();
|
|
PooledWriter writer = WriterPool.Retrieve();
|
|
try
|
|
{
|
|
data.WriteByte(Magic);
|
|
data.WriteInt32(objectId);
|
|
data.WriteBoolean(active);
|
|
data.WriteUInt16(seq);
|
|
data.WriteSingle(aim.x);
|
|
data.WriteSingle(aim.y);
|
|
data.WriteSingle(aim.z);
|
|
|
|
writer.WriteUInt16((ushort)PacketId.Broadcast);
|
|
writer.WriteUInt16(Key);
|
|
writer.WriteInt32(data.Length);
|
|
writer.WriteArraySegment(data.GetArraySegment());
|
|
|
|
var segment = writer.GetArraySegment();
|
|
if (toServer)
|
|
manager.TransportManager.SendToServer((byte)channel, segment, true, DataOrderType.Default);
|
|
else
|
|
manager.TransportManager.SendToClients((byte)channel, segment, true);
|
|
}
|
|
finally
|
|
{
|
|
WriterPool.Store(data);
|
|
WriterPool.Store(writer);
|
|
}
|
|
}
|
|
|
|
static bool TryRead(PooledReader reader, int start, out int end, out int objectId, out bool active, out Vector3 aim, out ushort seq)
|
|
{
|
|
end = start;
|
|
objectId = 0;
|
|
active = false;
|
|
aim = Vector3.forward;
|
|
seq = 0;
|
|
if (reader == null)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
if (reader.Remaining < 2)
|
|
return false;
|
|
|
|
ushort key = reader.ReadUInt16();
|
|
if (key != Key)
|
|
{
|
|
reader.Position = start;
|
|
return false;
|
|
}
|
|
|
|
int length = reader.ReadInt32();
|
|
int payload = reader.Position;
|
|
if (length < 1 || length > 128 || reader.Remaining < length)
|
|
{
|
|
reader.Position = start;
|
|
return false;
|
|
}
|
|
|
|
end = payload + length;
|
|
if (reader.ReadByte() != Magic)
|
|
{
|
|
reader.Position = start;
|
|
return false;
|
|
}
|
|
|
|
objectId = reader.ReadInt32();
|
|
active = reader.ReadBoolean();
|
|
seq = reader.ReadUInt16();
|
|
aim = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
|
|
reader.Position = end;
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PeePeePlugin.Fail(ex);
|
|
try
|
|
{
|
|
reader.Position = start;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static ushort Hash(string text)
|
|
{
|
|
unchecked
|
|
{
|
|
uint hash = 2166136261;
|
|
for (int i = 0; i < text.Length; i++)
|
|
{
|
|
hash *= 16777619;
|
|
hash ^= text[i];
|
|
}
|
|
|
|
return (ushort)((hash >> 16) ^ hash);
|
|
}
|
|
}
|
|
}
|
|
}
|