RUMBLE does not support other mod managers. If you want to use a manager, you must use the RUMBLE Mod Manager, a manager specifically designed for this game.
Decompiled source of RumbleParkPen v1.1.0
Mods\RumbleParkPen.dll
Decompiled a month ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using HarmonyLib; using Il2CppExitGames.Client.Photon; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppPhoton.Pun; using Il2CppPhoton.Realtime; using Il2CppRUMBLE.Players; using Il2CppRUMBLE.Players.Subsystems; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using MelonLoader; using MelonLoader.Preferences; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using RumbleModdingAPI.RMAPI; using RumbleParkPen; using RumbleParkPen.Patches; using RumbleParkPen.Services.Drawing; using RumbleParkPen.Services.Input; using RumbleParkPen.Services.Persistence; using RumbleParkPen.Services.Sync; using RumbleParkPen.Unity; using RumbleParkPen.Unity.Input; using RumbleParkPen.Unity.Rendering; using UIFramework; using UIFramework.UiExtensions; using UnityEngine; using UnityEngine.Rendering; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(PenMod), "RumbleParkPen", "1.1.0", "ACutiePi", null)] [assembly: MelonGame("Buckethead Entertainment", "RUMBLE")] [assembly: MelonAdditionalDependencies(new string[] { "UIFramework", "RumbleModdingAPI" })] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("RumbleParkPen")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0")] [assembly: AssemblyInformationalVersion("1.1.0+f645f5656586cd08423a550c31b778619ffff62b")] [assembly: AssemblyProduct("RumbleParkPen")] [assembly: AssemblyTitle("RumbleParkPen")] [assembly: AssemblyVersion("1.1.0.0")] [module: RefSafetyRules(11)] [CompilerGenerated] internal sealed class <>z__ReadOnlyArray<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T> { int ICollection.Count => _items.Length; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object? IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection<T>.Count => _items.Length; T IReadOnlyList<T>.this[int index] => _items[index]; int ICollection<T>.Count => _items.Length; bool ICollection<T>.IsReadOnly => true; T IList<T>.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } public <>z__ReadOnlyArray(T[] items) { _items = items; } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.CopyTo(Array array, int index) { ((ICollection)_items).CopyTo(array, index); } int IList.Add(object? value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object? value) { return ((IList)_items).Contains(value); } int IList.IndexOf(object? value) { return ((IList)_items).IndexOf(value); } void IList.Insert(int index, object? value) { throw new NotSupportedException(); } void IList.Remove(object? value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return ((IEnumerable<T>)_items).GetEnumerator(); } void ICollection<T>.Add(T item) { throw new NotSupportedException(); } void ICollection<T>.Clear() { throw new NotSupportedException(); } bool ICollection<T>.Contains(T item) { return ((ICollection<T>)_items).Contains(item); } void ICollection<T>.CopyTo(T[] array, int arrayIndex) { ((ICollection<T>)_items).CopyTo(array, arrayIndex); } bool ICollection<T>.Remove(T item) { throw new NotSupportedException(); } int IList<T>.IndexOf(T item) { return ((IList<T>)_items).IndexOf(item); } void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); } void IList<T>.RemoveAt(int index) { throw new NotSupportedException(); } } [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T> { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator<T> { object IEnumerator.Current => _item; T IEnumerator<T>.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object? IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection<T>.Count => 1; T IReadOnlyList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection<T>.Count => 1; bool ICollection<T>.IsReadOnly => true; T IList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object? value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object? value) { return EqualityComparer<T>.Default.Equals(_item, (T)value); } int IList.IndexOf(object? value) { if (!EqualityComparer<T>.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object? value) { throw new NotSupportedException(); } void IList.Remove(object? value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return new Enumerator(_item); } void ICollection<T>.Add(T item) { throw new NotSupportedException(); } void ICollection<T>.Clear() { throw new NotSupportedException(); } bool ICollection<T>.Contains(T item) { return EqualityComparer<T>.Default.Equals(_item, item); } void ICollection<T>.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection<T>.Remove(T item) { throw new NotSupportedException(); } int IList<T>.IndexOf(T item) { if (!EqualityComparer<T>.Default.Equals(_item, item)) { return -1; } return 0; } void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); } void IList<T>.RemoveAt(int index) { throw new NotSupportedException(); } } namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RumbleParkPen { public sealed class PenMod : RumbleMod { private PenSession _session; private InputService _inputs; public static Instance Logger { get; private set; } public override void OnInitializeMelon() { Logger = ((MelonBase)this).LoggerInstance; _inputs = new InputService((MelonBase)(object)this, "RumbleParkPen_Controls", "Controls", Settings.ConfigPath); _session = new PenSession(this, _inputs); Settings.Initialize((MelonBase)(object)this, new Settings.Actions { TogglePen = delegate { _session.TogglePen(); }, Save = delegate { _session.TrySave(); }, Load = delegate { _session.TryLoad(); }, Clear = delegate { _session.ClearDrawing(); }, DeleteSave = delegate { _session.DeleteSave(); } }, _inputs.Category); try { RmapiLogFilter.Apply(((MelonBase)this).HarmonyInstance); } catch (Exception ex) { Logger.Warning("Could not quieten RMAPI network logging: " + ex.Message); } Logger.Msg("RumbleParkPen loaded."); } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { try { _session.OnSceneChanged(sceneName); } catch (Exception value) { Logger.Error($"Scene change to '{sceneName}' failed: {value}"); } } public override void OnUpdate() { try { _inputs.RefreshVisibility(); _session.Tick(Time.deltaTime); } catch (Exception value) { Logger.Error($"Update failed: {value}"); } } public override void OnEvent(List<Object> data) { try { PenMessage penMessage = PenNetwork.Decode(data); if (penMessage.Kind != PenMessageKind.Unknown) { _session.OnNetworkMessage(penMessage); } } catch (Exception value) { Logger.Error($"Handling a network message failed: {value}"); } } public override void OnApplicationQuit() { try { if (Settings.AutoSave.Value) { _session.TrySave(); } } catch (Exception value) { Logger.Error($"Save on quit failed: {value}"); } } } public enum PenHand { Right, Left } public static class Settings { public sealed class Actions { public Action TogglePen { get; init; } = delegate { }; public Action Save { get; init; } = delegate { }; public Action Load { get; init; } = delegate { }; public Action Clear { get; init; } = delegate { }; public Action DeleteSave { get; init; } = delegate { }; } private const string ConfigFolder = "UserData/RumbleParkPen"; private const string ConfigFile = "config.cfg"; public static string ConfigPath { get; } = Path.Combine("UserData/RumbleParkPen", "config.cfg"); public static MelonPreferences_Entry<PenHand> Hand { get; private set; } = null; public static MelonPreferences_Entry<Color> InkColor { get; private set; } = null; public static MelonPreferences_Entry<float> LineWidth { get; private set; } = null; public static MelonPreferences_Entry<bool> PressureSensitivity { get; private set; } = null; public static MelonPreferences_Entry<float> PointSpacing { get; private set; } = null; public static MelonPreferences_Entry<float> EraserRadius { get; private set; } = null; public static MelonPreferences_Entry<float> PenScale { get; private set; } = null; public static MelonPreferences_Entry<float> PenPitch { get; private set; } = null; public static MelonPreferences_Entry<float> PenYaw { get; private set; } = null; public static MelonPreferences_Entry<float> PenOffsetForward { get; private set; } = null; public static MelonPreferences_Entry<float> PenOffsetUp { get; private set; } = null; public static MelonPreferences_Entry<float> PenOffsetSide { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoSave { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoLoad { get; private set; } = null; public static MelonPreferences_Entry<bool> ShareDrawings { get; private set; } = null; public static MelonPreferences_Entry<bool> ShowOthersDrawings { get; private set; } = null; public static MelonPreferences_Entry<bool> SyncOnJoin { get; private set; } = null; public static MelonPreferences_Entry<bool> QuietNetworkLog { get; private set; } = null; public static void Initialize(MelonBase melon, Actions actions, MelonPreferences_Category controls) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Expected O, but got Unknown //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected O, but got Unknown //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Expected O, but got Unknown //IL_019e: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Expected O, but got Unknown //IL_01e2: Unknown result type (might be due to invalid IL or missing references) //IL_01e7: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Unknown result type (might be due to invalid IL or missing references) //IL_020a: Expected O, but got Unknown //IL_0226: Unknown result type (might be due to invalid IL or missing references) //IL_022b: Unknown result type (might be due to invalid IL or missing references) //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_0241: Unknown result type (might be due to invalid IL or missing references) //IL_024e: Expected O, but got Unknown //IL_026a: Unknown result type (might be due to invalid IL or missing references) //IL_026f: Unknown result type (might be due to invalid IL or missing references) //IL_027a: Unknown result type (might be due to invalid IL or missing references) //IL_0285: Unknown result type (might be due to invalid IL or missing references) //IL_0292: Expected O, but got Unknown //IL_02ae: Unknown result type (might be due to invalid IL or missing references) //IL_02b3: Unknown result type (might be due to invalid IL or missing references) //IL_02be: Unknown result type (might be due to invalid IL or missing references) //IL_02c9: Unknown result type (might be due to invalid IL or missing references) //IL_02d6: Expected O, but got Unknown //IL_02f2: Unknown result type (might be due to invalid IL or missing references) //IL_02f7: Unknown result type (might be due to invalid IL or missing references) //IL_0302: Unknown result type (might be due to invalid IL or missing references) //IL_030d: Unknown result type (might be due to invalid IL or missing references) //IL_031a: Expected O, but got Unknown Directory.CreateDirectory("UserData/RumbleParkPen"); string configPath = ConfigPath; MelonPreferences_Category val = MelonPreferences.CreateCategory("RumbleParkPen_Pen", "Pen"); val.SetFilePath(configPath); UI.CreateButtonEntry(val, "Spawn / Put Away", "Pen", "Spawns the pen in your hand", actions.TogglePen); Hand = val.CreateEntry<PenHand>("Hand", PenHand.Right, "Pen Hand", "Which hand holds the pen", false, false, (ValueValidator)null, (string)null); InkColor = val.CreateEntry<Color>("InkColor", Color.white, "Ink Colour", "Colour of new strokes, cycle with primary button on pen hand", false, false, (ValueValidator)null, (string)null); LineWidth = val.CreateEntry<float>("LineWidth", 0.01f, "Line Width", "Stroke thickness in (metres)", false, false, (ValueValidator)new SliderDescriptor { Min = 0.002f, Max = 0.05f, DecimalPlaces = 3 }, (string)null); PressureSensitivity = val.CreateEntry<bool>("PressureSensitivity", true, "Pressure Sensitivity", "Squeeze the trigger harder for a thicker line", false, false, (ValueValidator)null, (string)null); PointSpacing = val.CreateEntry<float>("PointSpacing", 0.006f, "Detail", "How far each saved point of a line is (metres)", false, false, (ValueValidator)new SliderDescriptor { Min = 0.002f, Max = 0.05f, DecimalPlaces = 3 }, (string)null); EraserRadius = val.CreateEntry<float>("EraserRadius", 0.1f, "Eraser Radius", "How close the nib must get to a stroke to rub it out (metres)", false, false, (ValueValidator)new SliderDescriptor { Min = 0.02f, Max = 0.5f, DecimalPlaces = 3 }, (string)null); MelonPreferences_Category val2 = MelonPreferences.CreateCategory("RumbleParkPen_Fit", "Pen Fit"); val2.SetFilePath(configPath); PenScale = val2.CreateEntry<float>("PenScale", 1f, "Pen Size", "Pen model scale", false, false, (ValueValidator)new SliderDescriptor { Min = 0.5f, Max = 2f, DecimalPlaces = 2 }, (string)null); PenPitch = val2.CreateEntry<float>("PenPitch", 40f, "Pen Pitch", "Tilts pen up/down", false, false, (ValueValidator)new SliderDescriptor { Min = -90f, Max = 90f, DecimalPlaces = 0 }, (string)null); PenYaw = val2.CreateEntry<float>("PenYaw", 0f, "Pen Yaw", "Tilts pen lef/right", false, false, (ValueValidator)new SliderDescriptor { Min = -90f, Max = 90f, DecimalPlaces = 0 }, (string)null); PenOffsetForward = val2.CreateEntry<float>("PenOffsetForward", 0.032f, "Forward Offset", "Pen position forward/back", false, false, (ValueValidator)new SliderDescriptor { Min = -0.15f, Max = 0.15f, DecimalPlaces = 3 }, (string)null); PenOffsetUp = val2.CreateEntry<float>("PenOffsetUp", 0.1f, "Vertical Offset", "Pen position up/down", false, false, (ValueValidator)new SliderDescriptor { Min = -0.15f, Max = 0.15f, DecimalPlaces = 3 }, (string)null); PenOffsetSide = val2.CreateEntry<float>("PenOffsetSide", 0f, "Sideways Offset", "Pen posision left/right", false, false, (ValueValidator)new SliderDescriptor { Min = -0.15f, Max = 0.15f, DecimalPlaces = 3 }, (string)null); MelonPreferences_Category val3 = MelonPreferences.CreateCategory("RumbleParkPen_Drawing", "Drawing"); val3.SetFilePath(configPath); UI.CreateButtonEntry(val3, "Save", "Save Drawing", "Save current drawing to a file", actions.Save); UI.CreateButtonEntry(val3, "Load", "Load Drawing", "Replace the current drawn scene with a saved one", actions.Load); UI.CreateButtonEntry(val3, "Clear", "Clear Drawing", "Clear all drawings in the scene", actions.Clear); UI.CreateButtonEntry(val3, "Delete", "Delete Save File", "Deletes this scene's save file", actions.DeleteSave); AutoLoad = val3.CreateEntry<bool>("AutoLoad", true, "Load On Arrival", "Restores scenes' saved drawings automatically when you enter Park or Gym", false, false, (ValueValidator)null, (string)null); AutoSave = val3.CreateEntry<bool>("AutoSave", true, "Save On Leaving", "Saves automatically when you leave a Park or Gym", false, false, (ValueValidator)null, (string)null); MelonPreferences_Category val4 = MelonPreferences.CreateCategory("RumbleParkPen_Multiplayer", "Multiplayer"); val4.SetFilePath(configPath); ShareDrawings = val4.CreateEntry<bool>("ShareDrawings", true, "Share Your Drawings", "Sends your strokes to other players (with this mod) in the room", false, false, (ValueValidator)null, (string)null); ShowOthersDrawings = val4.CreateEntry<bool>("ShowOthersDrawings", true, "Show Others' Drawings", "Shows drawings from other players in the room", false, false, (ValueValidator)null, (string)null); SyncOnJoin = val4.CreateEntry<bool>("SyncOnJoin", true, "Sync On Join", "Sync drawings when others arrive to a room that already has drawings, or you arrive to other rooms with drawings", false, false, (ValueValidator)null, (string)null); QuietNetworkLog = val4.CreateEntry<bool>("QuietNetworkLog", true, "Quiet Network Logging", "Hides the two lines RUMBLE Modding API writes for every stroke message received (reduces network log spam)", false, false, (ValueValidator)null, (string)null); UI.RegisterMelon(melon, (MelonPreferences_Category[])(object)new MelonPreferences_Category[5] { val, val2, val3, val4, controls }); val.SaveToFile(false); val2.SaveToFile(false); val3.SaveToFile(false); val4.SaveToFile(false); controls.SaveToFile(false); } } } namespace RumbleParkPen.Unity { public sealed class PenNetwork(PenMod mod) { private readonly struct PendingSend { public string Payload { get; init; } public int TargetActor { get; init; } } private const int ReplayMessagesPerTick = 3; private readonly PenMod _mod = mod; private readonly Queue<PendingSend> _replayQueue = new Queue<PendingSend>(); private readonly HashSet<int> _peers = new HashSet<int>(); private readonly HashSet<int> _present = new HashSet<int>(); private readonly HashSet<int> _warnedMismatch = new HashSet<int>(); private readonly HashSet<int> _liveActors = new HashSet<int>(); private static readonly Object PresenceKey = (Object)(object)String.op_Implicit("RumbleParkPen.protocol"); private static readonly string ProtocolVersionText = 1.ToString(CultureInfo.InvariantCulture); private int _strokeSequence; public static bool CanTransmit { get { try { return PhotonNetwork.InRoom && PhotonNetwork.PlayerList != null && ((Il2CppArrayBase<Player>)(object)PhotonNetwork.PlayerList).Length > 1; } catch (Exception) { return false; } } } public static bool NetworkingEnabled { get { if (!Settings.ShareDrawings.Value) { return Settings.ShowOthersDrawings.Value; } return true; } } public int PeerCount => _peers.Count; public IReadOnlyCollection<int> PresentActors => _present; public static int LocalActorNumber { get { try { Player localPlayer = PhotonNetwork.LocalPlayer; return (localPlayer != null) ? localPlayer.ActorNumber : 0; } catch (Exception) { return 0; } } } public static void PublishPresence() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown if (!NetworkingEnabled) { return; } try { Player localPlayer = PhotonNetwork.LocalPlayer; if (localPlayer != null) { Hashtable val = new Hashtable(); val[PresenceKey] = (Object)(object)String.op_Implicit(ProtocolVersionText); localPlayer.SetCustomProperties(val, (Hashtable)null, (WebFlags)null); } } catch (Exception ex) { PenMod.Logger.Warning("Could not publish pen presence: " + ex.Message); } } public List<int> RefreshPeers() { List<int> list = new List<int>(); _present.Clear(); try { if (!PhotonNetwork.InRoom || PhotonNetwork.PlayerList == null) { _peers.Clear(); return list; } int localActorNumber = LocalActorNumber; foreach (Player item in (Il2CppArrayBase<Player>)(object)PhotonNetwork.PlayerList) { int actorNumber = item.ActorNumber; _present.Add(actorNumber); if (actorNumber == localActorNumber) { continue; } string text = ReadPresence(item); if (text == null) { _peers.Remove(actorNumber); } else if (string.Equals(text, ProtocolVersionText, StringComparison.Ordinal)) { if (_peers.Add(actorNumber)) { list.Add(actorNumber); } } else if (_warnedMismatch.Add(actorNumber)) { _peers.Remove(actorNumber); PenMod.Logger.Warning($"'{item.NickName}' is running a different RumbleParkPen protocol (theirs: {text}, ours: {ProtocolVersionText}); drawings will not be shared " + "with them. One of you needs to update."); } } _peers.IntersectWith(_present); _warnedMismatch.IntersectWith(_present); } catch (Exception) { } return list; } private static string? ReadPresence(Player player) { try { Hashtable customProperties = player.CustomProperties; if (customProperties == null || !((Dictionary<Object, Object>)(object)customProperties).ContainsKey(PresenceKey)) { return null; } Object obj = customProperties[PresenceKey]; return (obj != null) ? obj.ToString() : null; } catch (Exception) { return null; } } public void AddPeer(int actor) { if (actor > 0 && actor != LocalActorNumber) { _peers.Add(actor); } } public void ForgetPeers() { _peers.Clear(); _present.Clear(); _warnedMismatch.Clear(); } public string NextStrokeId() { return StrokeId.Create(LocalActorNumber, ++_strokeSequence); } public void EnsureRegistered() { RmapiRegistration.Ensure(mod); } public void SendStrokeStart(string strokeId, float r, float g, float b) { Send(PenSyncFormat.StrokeStart(strokeId, r, g, b)); } public void SendStrokePoints(string strokeId, IReadOnlyList<PenPoint> points) { if (points.Count != 0) { Send(PenSyncFormat.StrokePoints(strokeId, points)); } } public void SendStrokeEnd(string strokeId) { Send(PenSyncFormat.StrokeEnd(strokeId)); } public void SendErase(IReadOnlyList<string> strokeIds) { if (strokeIds.Count != 0) { Send(PenSyncFormat.Erase(strokeIds)); } } public void SendClear() { Send(PenSyncFormat.Clear(LocalActorNumber)); } public void SendSyncRequest(int targetActor, bool wantsReplay) { if (CanTransmit && NetworkingEnabled) { Transmit(PenSyncFormat.SyncRequest(LocalActorNumber, wantsReplay), targetActor); } } public void QueueReplay(IEnumerable<StrokeData> strokes, int targetActor, int pointsPerMessage = 60) { foreach (StrokeData stroke in strokes) { if (stroke.IsRenderable && !string.IsNullOrEmpty(stroke.Id)) { Enqueue(PenSyncFormat.StrokeStart(stroke.Id, stroke.ColorR, stroke.ColorG, stroke.ColorB), targetActor); for (int i = 0; i < stroke.Points.Count; i += pointsPerMessage) { int count = Math.Min(pointsPerMessage, stroke.Points.Count - i); Enqueue(PenSyncFormat.StrokePoints(stroke.Id, stroke.Points.GetRange(i, count)), targetActor); } Enqueue(PenSyncFormat.StrokeEnd(stroke.Id), targetActor); } } } public void PumpReplayQueue() { for (int i = 0; i < 3; i++) { if (_replayQueue.Count <= 0) { break; } PendingSend pendingSend = _replayQueue.Dequeue(); Transmit(pendingSend.Payload, pendingSend.TargetActor); } } public void DiscardQueuedReplays() { _replayQueue.Clear(); } private void Enqueue(string payload, int targetActor) { _replayQueue.Enqueue(new PendingSend { Payload = payload, TargetActor = targetActor }); } private void Send(string payload) { if (CanTransmit && Settings.ShareDrawings.Value) { int[] array = LivePeers(); if (array.Length != 0) { Transmit(payload, array); } } } private int[] LivePeers() { if (_peers.Count == 0) { return Array.Empty<int>(); } try { _liveActors.Clear(); foreach (Player item in (Il2CppArrayBase<Player>)(object)PhotonNetwork.PlayerList) { _liveActors.Add(item.ActorNumber); } _peers.IntersectWith(_liveActors); } catch (Exception) { return Array.Empty<int>(); } if (_peers.Count == 0) { return Array.Empty<int>(); } int[] array = new int[_peers.Count]; _peers.CopyTo(array); return array; } private void Transmit(string payload, int targetActor) { if (targetActor > 0) { Transmit(payload, new int[1] { targetActor }); } } private void Transmit(string payload, int[] targetActors) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_0056: Unknown result type (might be due to invalid IL or missing references) if (!CanTransmit || targetActors.Length == 0) { return; } try { List<Object> list = new List<Object> { Object.op_Implicit(payload) }; Il2CppStructArray<int> val = new Il2CppStructArray<int>((long)targetActors.Length); for (int i = 0; i < targetActors.Length; i++) { ((Il2CppArrayBase<int>)(object)val)[i] = targetActors[i]; } RaiseEventOptions val2 = new RaiseEventOptions { TargetActors = val }; ((RumbleMod)mod).RaiseEvent(list, val2, SendOptions.SendReliable); } catch (Exception ex) { PenMod.Logger.Warning("Pen network send failed: " + ex.Message); } } public static PenMessage Decode(List<Object> data) { try { if (data == null || data.Count == 0) { return new PenMessage { Kind = PenMessageKind.Unknown }; } Object val = data[0]; return (val == null) ? new PenMessage { Kind = PenMessageKind.Unknown } : PenSyncFormat.Decode(val.ToString()); } catch (Exception) { return new PenMessage { Kind = PenMessageKind.Unknown }; } } } public readonly record struct PenFit(Vector3 Offset, Vector3 Rotation, float Scale) { public static PenFit FromSettings() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) return new PenFit(new Vector3(Settings.PenOffsetSide.Value, Settings.PenOffsetUp.Value, Settings.PenOffsetForward.Value), new Vector3(Settings.PenPitch.Value, Settings.PenYaw.Value, 0f), Settings.PenScale.Value); } } public sealed class PenRig { private const float BodyRadius = 0.008f; private const float BodyLength = 0.11f; private const float TaperRadius = 0.005f; private const float TaperLength = 0.03f; private const float NibDiameter = 0.007f; private const float TipDistance = 0.15f; private GameObject? _root; private Renderer? _taperRenderer; private Renderer? _nibRenderer; private PenFit _fit; private bool _fitApplied; public Transform? Tip { get; private set; } public bool IsAlive { get { if ((Object)(object)_root != (Object)null) { return (Object)(object)Tip != (Object)null; } return false; } } public Vector3? TipPosition { get { //IL_0014: Unknown result type (might be due to invalid IL or missing references) Transform? tip = Tip; if (tip == null) { return null; } return tip.position; } } public bool Build(Transform anchor, in PenFit fit) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Expected O, but got Unknown //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)anchor == (Object)null) { return false; } Destroy(); _root = new GameObject("RumbleParkPen.Pen"); _root.transform.SetParent(anchor, false); ApplyFit(in fit); CreatePart((PrimitiveType)2, "Body", new Vector3(0f, 0f, 0.055f), new Vector3(0.016f, 0.055f, 0.016f), 0.12f, 0.12f, 0.13f, out Renderer _); CreatePart((PrimitiveType)2, "Taper", new Vector3(0f, 0f, 0.125f), new Vector3(0.01f, 0.015f, 0.01f), 1f, 1f, 1f, out _taperRenderer); CreatePart((PrimitiveType)0, "Nib", new Vector3(0f, 0f, 0.1465f), new Vector3(0.007f, 0.007f, 0.007f), 1f, 1f, 1f, out _nibRenderer); GameObject val = new GameObject("Tip"); val.transform.SetParent(_root.transform, false); val.transform.localPosition = new Vector3(0f, 0f, 0.15f); val.transform.localRotation = Quaternion.identity; Tip = val.transform; return true; } private void CreatePart(PrimitiveType type, string name, Vector3 localPosition, Vector3 localScale, float r, float g, float b, out Renderer? renderer) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Invalid comparison between Unknown and I4 //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) GameObject val = GameObject.CreatePrimitive(type); ((Object)val).name = name; val.transform.SetParent(_root.transform, false); val.transform.localPosition = localPosition; val.transform.localRotation = (((int)type == 2) ? Quaternion.Euler(90f, 0f, 0f) : Quaternion.identity); val.transform.localScale = localScale; Collider component = val.GetComponent<Collider>(); if ((Object)(object)component != (Object)null) { Object.Destroy((Object)(object)component); } renderer = val.GetComponent<Renderer>(); Renderer? obj = renderer; if (obj != null) { obj.sharedMaterial = PenMaterials.GetLineMaterial(r, g, b); } } public void ApplyFit(in PenFit fit) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_root == (Object)null) && (!_fitApplied || !(_fit == fit))) { _fit = fit; _fitApplied = true; Transform transform = _root.transform; transform.localPosition = fit.Offset; transform.localRotation = Quaternion.Euler(fit.Rotation); transform.localScale = Vector3.one * fit.Scale; } } public void SetInkColor(float r, float g, float b) { Material lineMaterial = PenMaterials.GetLineMaterial(r, g, b); Renderer? taperRenderer = _taperRenderer; if (taperRenderer != null) { taperRenderer.sharedMaterial = lineMaterial; } Renderer? nibRenderer = _nibRenderer; if (nibRenderer != null) { nibRenderer.sharedMaterial = lineMaterial; } } public void Destroy() { if ((Object)(object)_root != (Object)null) { Object.Destroy((Object)(object)_root); } _root = null; Tip = null; _taperRenderer = null; _nibRenderer = null; _fitApplied = false; } public void ForgetSceneObjects() { _root = null; Tip = null; _taperRenderer = null; _nibRenderer = null; _fitApplied = false; } } public sealed class PenSession { private const float PressThreshold = 0.5f; private const float MinPressureScale = 0.3f; private readonly StrokeStore _store; private readonly StrokeRenderer _renderer = new StrokeRenderer(); private readonly PenRig _rig = new PenRig(); private readonly PenPalette _palette = new PenPalette(); private readonly DrawingRepository _repository; private readonly PenNetwork _network; private readonly RemoteStrokes _remote = new RemoteStrokes(); private readonly PointBatcher _outgoing = new PointBatcher(); private StrokeData? _sharedStroke; private const float PresenceIntervalSeconds = 2f; private bool _presencePublished; private float _presenceTimer; private readonly InputService _inputs; private Vector3? _tip; private readonly List<StrokeData> _erased = new List<StrokeData>(); private bool _penRequested; private Color _lastInkColor = Color.white; private bool _warnedNoAnchor; private bool PenReady { get { if (_penRequested) { Vector3? tip = _tip; return tip.HasValue; } return false; } } public bool IsPenOut => _rig.IsAlive; public int StrokeCount => _store.Count; public string CurrentScene { get; private set; } = string.Empty; public PenSession(PenMod mod, InputService inputs, DrawingRepository? repository = null) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) _repository = repository ?? new DrawingRepository(); _network = new PenNetwork(mod); _inputs = inputs; _store = new StrokeStore(_network.NextStrokeId); inputs.Register("Draw", "Draw", "Lay down ink while these are held.", BindingPreset.DominantTrigger, ActivationMode.WhileHeld, activationIsFixed: true).OnState = HandleDrawing; inputs.Register("Erase", "Erase", "Rub out whole strokes the nib touches while these are held.", BindingPreset.DominantGrip, ActivationMode.WhileHeld, activationIsFixed: true).OnState = HandleErasing; inputs.Register("CycleColour", "Cycle Colour", "Step the ink through the palette.", BindingPreset.DominantPrimary, ActivationMode.Tap).OnFired = CycleColour; inputs.Register("Undo", "Undo", "Remove your most recent stroke.", BindingPreset.DominantSecondary, ActivationMode.Tap).OnFired = Undo; inputs.Register("Clear", "Clear", "Wipe your own drawing. Other players' strokes are left alone.", BindingPreset.DominantSecondary, ActivationMode.Hold).OnFired = delegate { if (PenReady) { ClearDrawing(); } }; inputs.Register("TogglePen", "Spawn / Put Away", "Spawn/remove the pen from your hand.", BindingPreset.NonDominantPrimaryAndSecondary, ActivationMode.Hold, activationIsFixed: false, 0.35f).OnFired = TogglePen; inputs.DeleteSettings("HoldToClearSeconds", "ToggleCombo", "ToggleHoldSeconds", "DesktopHotkey"); } public static bool IsDrawableScene(string scene) { if (scene == "Park" || scene == "Gym") { return true; } return false; } public void OnSceneChanged(string newScene) { if (IsDrawableScene(CurrentScene) && Settings.AutoSave.Value && _store.Count > 0) { TrySave(); } _renderer.ForgetSceneObjects(); _rig.ForgetSceneObjects(); PenMaterials.ResetCache(); _store.Clear(); _remote.ClearAll(); _outgoing.Discard(); _network.DiscardQueuedReplays(); _network.ForgetPeers(); _sharedStroke = null; _presencePublished = false; _presenceTimer = 0f; _inputs.Reset(); _tip = null; _warnedNoAnchor = false; CurrentScene = newScene; if (!IsDrawableScene(newScene)) { _penRequested = false; } else if (Settings.AutoLoad.Value && _repository.HasSave(newScene)) { TryLoad(); } } public void TogglePen() { if (!IsDrawableScene(CurrentScene)) { PenMod.Logger.Msg("The pen only works in the Park and the Gym (currently in '" + CurrentScene + "')."); return; } if (_penRequested) { PutPenAway(); return; } _penRequested = true; _warnedNoAnchor = false; TryBuildRig(); } private void PutPenAway() { _penRequested = false; StrokeData activeStroke = _store.ActiveStroke; if (activeStroke != null) { StrokeData strokeData = _store.EndStroke(); if (strokeData != null) { _renderer.Sync(strokeData); } else { _renderer.Remove(activeStroke); } } _rig.Destroy(); } public void Tick(float deltaTime) { if (IsDrawableScene(CurrentScene)) { PumpNetwork(deltaTime); SyncInkColorFromSettings(); PositionPen(); _inputs.Update(deltaTime, (Settings.Hand.Value == PenHand.Left) ? InputHand.Left : InputHand.Right); } } private void PositionPen() { _tip = null; if (_penRequested && (_rig.IsAlive || TryBuildRig())) { _rig.ApplyFit(PenFit.FromSettings()); _tip = _rig.TipPosition; } } private void HandleDrawing(InputAction action) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) if (!PenReady) { return; } Vector3 value = _tip.Value; if (action.IsHeld) { if (!_store.IsDrawing) { Color value2 = Settings.InkColor.Value; _store.BeginStroke(value2.r, value2.g, value2.b); } float width = WidthFor(action.Analog); PenPoint point = new PenPoint(value.x, value.y, value.z, width); if (!_store.TryAddPoint(in point, Settings.PointSpacing.Value)) { return; } StrokeData activeStroke = _store.ActiveStroke; if (activeStroke != null) { _renderer.Sync(activeStroke); if (activeStroke != _sharedStroke) { BeginSharedStroke(activeStroke); } _outgoing.Add(in point); } } else if (_store.IsDrawing) { StrokeData activeStroke2 = _store.ActiveStroke; StrokeData strokeData = _store.EndStroke(); if (strokeData != null) { _renderer.Sync(strokeData); } else if (activeStroke2 != null) { _renderer.Remove(activeStroke2); } EndSharedStroke(); } } private void PumpNetwork(float deltaTime) { _network.EnsureRegistered(); _network.PumpReplayQueue(); RefreshPresence(deltaTime); _outgoing.Advance(deltaTime); if (_sharedStroke != null && _outgoing.TryFlush(out List<PenPoint> batch)) { _network.SendStrokePoints(_sharedStroke.Id, batch); } } private void RefreshPresence(float deltaTime) { if (!PenNetwork.CanTransmit || !PenNetwork.NetworkingEnabled) { return; } _presenceTimer -= deltaTime; if (_presenceTimer > 0f) { return; } _presenceTimer = 2f; if (!_presencePublished) { PenNetwork.PublishPresence(); _presencePublished = true; } foreach (int item in _network.RefreshPeers()) { _network.SendSyncRequest(item, Settings.SyncOnJoin.Value); } DropDepartedPlayersStrokes(); } private void DropDepartedPlayersStrokes() { IReadOnlyCollection<int> presentActors = _network.PresentActors; if (presentActors.Count == 0) { return; } List<string> list = new List<string>(); foreach (StrokeData item in _remote.All) { if (StrokeId.TryGetOwner(item.Id, out var ownerActorNumber) && !presentActors.Contains(ownerActorNumber)) { list.Add(item.Id); } } if (list.Count > 0) { _renderer.RemoveAll(_remote.Erase(list)); } } private void BeginSharedStroke(StrokeData stroke) { EndSharedStroke(); _sharedStroke = stroke; _network.SendStrokeStart(stroke.Id, stroke.ColorR, stroke.ColorG, stroke.ColorB); } private void EndSharedStroke() { if (_sharedStroke != null) { List<PenPoint> list = _outgoing.FlushRemaining(); if (list.Count > 0) { _network.SendStrokePoints(_sharedStroke.Id, list); } _network.SendStrokeEnd(_sharedStroke.Id); _sharedStroke = null; } } public void OnNetworkMessage(PenMessage message) { if (!IsDrawableScene(CurrentScene)) { return; } switch (message.Kind) { case PenMessageKind.StrokeStart: if (Settings.ShowOthersDrawings.Value) { _remote.Start(message.StrokeId, message.ColorR, message.ColorG, message.ColorB); } break; case PenMessageKind.StrokePoints: if (Settings.ShowOthersDrawings.Value) { StrokeData strokeData = _remote.Append(message.StrokeId, message.Points); if (strokeData != null) { _renderer.Sync(strokeData); } } break; case PenMessageKind.Unknown: case PenMessageKind.StrokeEnd: break; case PenMessageKind.Erase: ApplyRemoteErase(message.StrokeIds); break; case PenMessageKind.Clear: _renderer.RemoveAll(_remote.ClearOwner(message.Actor)); break; case PenMessageKind.SyncRequest: _network.AddPeer(message.Actor); if (message.WantsReplay && Settings.SyncOnJoin.Value && Settings.ShareDrawings.Value) { _network.QueueReplay(_store.Strokes, message.Actor); } break; case PenMessageKind.Hello: _network.AddPeer(message.Actor); break; } } private void ApplyRemoteErase(IReadOnlyList<string> strokeIds) { _renderer.RemoveAll(_remote.Erase(strokeIds)); foreach (string strokeId in strokeIds) { StrokeData strokeData = _store.RemoveById(strokeId); if (strokeData != null) { _renderer.Remove(strokeData); } } } private static float WidthFor(float trigger) { float value = Settings.LineWidth.Value; if (!Settings.PressureSensitivity.Value) { return value; } float num = Mathf.InverseLerp(0.5f, 1f, trigger); return Mathf.Lerp(value * 0.3f, value, num); } private void HandleErasing(InputAction action) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) if (PenReady && action.IsHeld) { Vector3 value = _tip.Value; PenPoint probe = new PenPoint(value.x, value.y, value.z, 0f); float value2 = Settings.EraserRadius.Value; _erased.Clear(); _store.EraseNear(in probe, value2, _erased); if (Settings.ShowOthersDrawings.Value) { _remote.EraseNear(in probe, value2, _erased); } if (_erased.Count != 0) { _renderer.RemoveAll(_erased); BroadcastErased(_erased); } } } private void BroadcastErased(IEnumerable<StrokeData> strokes) { List<string> list = new List<string>(); foreach (StrokeData stroke in strokes) { if (!string.IsNullOrEmpty(stroke.Id)) { list.Add(stroke.Id); } } _network.SendErase(list); } private void CycleColour() { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) if (PenReady) { PaletteColor paletteColor = _palette.Advance(); Color val = default(Color); ((Color)(ref val))..ctor(paletteColor.R, paletteColor.G, paletteColor.B, 1f); Settings.InkColor.Value = val; _lastInkColor = val; _rig.SetInkColor(paletteColor.R, paletteColor.G, paletteColor.B); } } private void Undo() { if (PenReady) { StrokeData strokeData = _store.Undo(); if (strokeData != null) { _renderer.Remove(strokeData); BroadcastErased(new <>z__ReadOnlySingleElementList<StrokeData>(strokeData)); } } } private void SyncInkColorFromSettings() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) Color value = Settings.InkColor.Value; if (!Mathf.Approximately(value.r, _lastInkColor.r) || !Mathf.Approximately(value.g, _lastInkColor.g) || !Mathf.Approximately(value.b, _lastInkColor.b)) { _lastInkColor = value; _palette.SelectNearest(value.r, value.g, value.b); _rig.SetInkColor(value.r, value.g, value.b); } } private bool TryBuildRig() { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) Transform val = ResolveControllerTransform(); if ((Object)(object)val == (Object)null) { if (!_warnedNoAnchor) { _warnedNoAnchor = true; PenMod.Logger.Warning("Waiting for the VR rig before the pen can be spawned."); } return false; } _warnedNoAnchor = false; if (!_rig.Build(val, PenFit.FromSettings())) { return false; } Color value = Settings.InkColor.Value; _rig.SetInkColor(value.r, value.g, value.b); return true; } private static Transform? ResolveControllerTransform() { try { PlayerController localPlayerController = Players.GetLocalPlayerController(); if ((Object)(object)localPlayerController == (Object)null) { return null; } PlayerVR playerVR = localPlayerController.PlayerVR; if ((Object)(object)playerVR == (Object)null) { return null; } VRTransform obj = ((Settings.Hand.Value == PenHand.Left) ? playerVR.LeftController : playerVR.RightController); return (obj != null) ? obj.Transform : null; } catch (Exception) { return null; } } public void TrySave() { if (!IsDrawableScene(CurrentScene)) { PenMod.Logger.Msg("Nothing to save here - the pen only works in the Park and the Gym."); return; } try { _repository.SaveScene(_store.Strokes, CurrentScene); } catch (Exception ex) { PenMod.Logger.Error("Could not save the drawing: " + ex.Message); } } public void TryLoad() { if (!IsDrawableScene(CurrentScene)) { PenMod.Logger.Msg("Nothing to load here - the pen only works in the Park and the Gym."); return; } try { List<StrokeData> strokes = _repository.LoadScene(CurrentScene); _store.Replace(strokes); _renderer.RebuildAll(_store.Strokes); } catch (Exception ex) { PenMod.Logger.Error("Could not load the drawing: " + ex.Message); } } public void ClearDrawing() { _renderer.RemoveAll(_store.Clear()); _outgoing.Discard(); _sharedStroke = null; _network.SendClear(); } public void DeleteSave() { try { if (!_repository.DeleteScene(CurrentScene)) { PenMod.Logger.Msg("There is no saved drawing for " + CurrentScene + "."); } } catch (Exception ex) { PenMod.Logger.Error("Could not delete the save file: " + ex.Message); } } } internal static class RmapiRegistration { private static FieldInfo? _instanceField; private static FieldInfo? _registeredModsField; private static bool _resolved; private static bool _warned; public static bool Ensure(PenMod mod) { try { if (!Resolve()) { return false; } object value = _instanceField.GetValue(null); if (value == null) { return false; } if (!(_registeredModsField.GetValue(value) is Dictionary<string, RumbleMod> dictionary)) { return false; } string key = ((MelonBase)mod).Info.Name + "|" + ((MelonBase)mod).Info.Author; if (!dictionary.ContainsKey(key)) { dictionary[key] = (RumbleMod)(object)mod; } ((RumbleMod)mod).EventsRegisted = true; return true; } catch (Exception ex) { if (!_warned) { _warned = true; PenMod.Logger.Warning("Could not verify RMAPI event registration: " + ex.Message); } return false; } } private static bool Resolve() { if (_resolved) { if ((object)_instanceField != null) { return (object)_registeredModsField != null; } return false; } _resolved = true; Type nestedType = typeof(Utilities).GetNestedType("RaiseEventManager", BindingFlags.NonPublic); if ((object)nestedType == null) { return false; } _instanceField = nestedType.GetField("instance", BindingFlags.Static | BindingFlags.NonPublic); _registeredModsField = nestedType.GetField("RegisteredMods", BindingFlags.Instance | BindingFlags.NonPublic); if ((object)_instanceField != null) { return (object)_registeredModsField != null; } return false; } } } namespace RumbleParkPen.Unity.Rendering { public static class PenMaterials { private static readonly string[] ShaderCandidates = new string[4] { "Universal Render Pipeline/Particles/Unlit", "Universal Render Pipeline/Unlit", "Sprites/Default", "Unlit/Color" }; private static readonly Dictionary<int, Material> MaterialCache = new Dictionary<int, Material>(); private static bool _resolved; [CompilerGenerated] private static Shader? <LineShader>k__BackingField; public static Shader? LineShader { get { if (_resolved) { return <LineShader>k__BackingField; } _resolved = true; string[] shaderCandidates = ShaderCandidates; for (int i = 0; i < shaderCandidates.Length; i++) { Shader val = Shader.Find(shaderCandidates[i]); if (!((Object)(object)val == (Object)null)) { <LineShader>k__BackingField = val; return <LineShader>k__BackingField; } } PenMod.Logger.Error("No usable ink shader found. Strokes will render with Unity's default material. Tried: " + string.Join(", ", ShaderCandidates)); return null; } } public static Material GetLineMaterial(float r, float g, float b) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Expected O, but got Unknown //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) int key = QuantiseKey(r, g, b); if (MaterialCache.TryGetValue(key, out Material value) && (Object)(object)value != (Object)null) { return value; } Shader lineShader = LineShader; Material val = (((Object)(object)lineShader != (Object)null) ? new Material(lineShader) : new Material(Shader.Find("Diffuse"))); Color val2 = default(Color); ((Color)(ref val2))..ctor(r, g, b, 1f); if (val.HasProperty("_BaseColor")) { val.SetColor("_BaseColor", val2); } if (val.HasProperty("_Color")) { val.SetColor("_Color", val2); } if (val.HasProperty("_EmissionColor")) { val.SetColor("_EmissionColor", val2); } MaterialCache[key] = val; return val; } private static int QuantiseKey(float r, float g, float b) { int num = Mathf.Clamp(Mathf.RoundToInt(r * 31f), 0, 31); int num2 = Mathf.Clamp(Mathf.RoundToInt(g * 31f), 0, 31); int num3 = Mathf.Clamp(Mathf.RoundToInt(b * 31f), 0, 31); return (num << 10) | (num2 << 5) | num3; } public static void ResetCache() { MaterialCache.Clear(); } } public sealed class StrokeRenderer { private sealed class StrokeView { public GameObject Object; public LineRenderer Line; public int RenderedPoints; public bool VariableWidth; public int WidthCurvePoints; public float WidthMultiplier = -1f; } private const string RootName = "RumbleParkPen.Strokes"; private const int RoundingVertices = 4; private const int MaxWidthCurveKeys = 12; private const int WidthCurveRefreshPoints = 10; private readonly Dictionary<StrokeData, StrokeView> _views = new Dictionary<StrokeData, StrokeView>(); private GameObject? _root; public int ViewCount => _views.Count; private GameObject Root { get { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown if ((Object)(object)_root != (Object)null) { return _root; } _root = new GameObject("RumbleParkPen.Strokes"); return _root; } } public void Sync(StrokeData stroke) { //IL_00a2: Unknown result type (might be due to invalid IL or missing references) if (!stroke.IsRenderable) { Remove(stroke); return; } if (!_views.TryGetValue(stroke, out StrokeView value) || (Object)(object)value.Object == (Object)null) { value = CreateView(stroke); _views[stroke] = value; } int count = stroke.Points.Count; if (value.RenderedPoints != count) { value.Line.positionCount = count; for (int i = ((count > value.RenderedPoints) ? value.RenderedPoints : 0); i < count; i++) { PenPoint penPoint = stroke.Points[i]; value.Line.SetPosition(i, new Vector3(penPoint.X, penPoint.Y, penPoint.Z)); } value.RenderedPoints = count; ApplyWidth(stroke, value); } } private StrokeView CreateView(StrokeData stroke) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Stroke"); val.transform.SetParent(Root.transform, true); LineRenderer val2 = val.AddComponent<LineRenderer>(); val2.useWorldSpace = true; val2.numCapVertices = 4; val2.numCornerVertices = 4; val2.alignment = (LineAlignment)0; val2.textureMode = (LineTextureMode)0; ((Renderer)val2).shadowCastingMode = (ShadowCastingMode)0; ((Renderer)val2).receiveShadows = false; Material lineMaterial = PenMaterials.GetLineMaterial(stroke.ColorR, stroke.ColorG, stroke.ColorB); ((Renderer)val2).sharedMaterial = lineMaterial; Color val3 = default(Color); ((Color)(ref val3))..ctor(stroke.ColorR, stroke.ColorG, stroke.ColorB, 1f); val2.startColor = val3; val2.endColor = val3; return new StrokeView { Object = val, Line = val2 }; } private static void ApplyWidth(StrokeData stroke, StrokeView view) { //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Expected O, but got Unknown float num = stroke.MaxWidth; if (num <= 0f) { num = 0.01f; } if (view.WidthMultiplier != num) { view.Line.widthMultiplier = num; view.WidthMultiplier = num; } if (!stroke.HasVariableWidth) { if (view.VariableWidth) { view.Line.widthCurve = AnimationCurve.Constant(0f, 1f, 1f); view.VariableWidth = false; } return; } int count = stroke.Points.Count; if (!view.VariableWidth || count <= 12 || count - view.WidthCurvePoints >= 10) { int num2 = Mathf.Min(count, 12); AnimationCurve val = new AnimationCurve(); for (int i = 0; i < num2; i++) { float num3 = ((num2 == 1) ? 0f : ((float)i / (float)(num2 - 1))); int index = Mathf.RoundToInt(num3 * (float)(count - 1)); val.AddKey(num3, stroke.Points[index].Width / num); } view.Line.widthCurve = val; view.VariableWidth = true; view.WidthCurvePoints = count; } } public void Remove(StrokeData stroke) { if (_views.TryGetValue(stroke, out StrokeView value)) { _views.Remove(stroke); if ((Object)(object)value.Object != (Object)null) { Object.Destroy((Object)(object)value.Object); } } } public void RemoveAll(IEnumerable<StrokeData> strokes) { foreach (StrokeData stroke in strokes) { Remove(stroke); } } public void Clear() { foreach (StrokeView value in _views.Values) { if ((Object)(object)value.Object != (Object)null) { Object.Destroy((Object)(object)value.Object); } } _views.Clear(); if ((Object)(object)_root != (Object)null) { Object.Destroy((Object)(object)_root); _root = null; } } public void ForgetSceneObjects() { _views.Clear(); _root = null; } public void RebuildAll(IEnumerable<StrokeData> strokes) { Clear(); foreach (StrokeData stroke in strokes) { Sync(stroke); } } } } namespace RumbleParkPen.Unity.Input { public sealed class BindingSettingsRow { private readonly bool _modeIsFixed; public InputAction Action { get; } public ActivationMode FixedMode { get; } public MelonPreferences_Entry<BindingPreset> Preset { get; private set; } public MelonPreferences_Entry<string> Custom { get; private set; } public MelonPreferences_Entry<ActivationMode>? Mode { get; private set; } public MelonPreferences_Entry<float>? HoldSeconds { get; private set; } public ActivationMode CurrentMode { get { if (!_modeIsFixed) { return Mode.Value; } return FixedMode; } } public float CurrentHoldSeconds => HoldSeconds?.Value ?? 1f; public InputBinding CurrentBinding => Preset.Value.Resolve(Custom.Value); private ActivationMode EditedMode { get { if (!_modeIsFixed) { return Mode.EditedValue; } return FixedMode; } } private BindingSettingsRow(InputAction action, bool modeIsFixed, ActivationMode fixedMode) { Action = action; _modeIsFixed = modeIsFixed; FixedMode = fixedMode; } internal static BindingSettingsRow Create(InputService owner, MelonPreferences_Category category, InputAction action, string description, BindingPreset defaultPreset, ActivationMode defaultMode, bool modeIsFixed, float defaultHoldSeconds) { //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Expected O, but got Unknown BindingSettingsRow bindingSettingsRow = new BindingSettingsRow(action, modeIsFixed, defaultMode); string id = action.Id; string label = action.Label; bindingSettingsRow.Preset = category.CreateEntry<BindingPreset>(id + "Binding", defaultPreset, label, description, false, false, (ValueValidator)null, (string)null); bindingSettingsRow.Custom = category.CreateEntry<string>(id + "Custom", defaultPreset.Resolve(null).ToString(), label + " (Custom)", "Buttons joined with +, all held together. For example NonDominantPrimary+NonDominantSecondary, DominantTrigger, LeftStickClick, Key:F7, or None.\n", false, false, (ValueValidator)null, (string)null); if (!modeIsFixed) { bindingSettingsRow.Mode = category.CreateEntry<ActivationMode>(id + "Mode", defaultMode, label + " Activation", "Button activiation mode.", false, false, (ValueValidator)null, (string)null); bindingSettingsRow.HoldSeconds = category.CreateEntry<float>(id + "HoldSeconds", defaultHoldSeconds, label + " Hold Time", "How long the buttons must be held.", false, false, (ValueValidator)new SliderDescriptor { Min = 0.1f, Max = 3f, DecimalPlaces = 2 }, (string)null); } ((MelonEventBase<LemonAction<BindingPreset, BindingPreset>>)(object)bindingSettingsRow.Preset.OnEntryValueChanged).Subscribe((LemonAction<BindingPreset, BindingPreset>)delegate { owner.HasChanged(); }, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)bindingSettingsRow.Custom.OnEntryValueChanged).Subscribe((LemonAction<string, string>)delegate { owner.HasChanged(); }, 0, false); ((MelonEventBase<LemonAction<ActivationMode, ActivationMode>>)(object)bindingSettingsRow.Mode?.OnEntryValueChanged).Subscribe((LemonAction<ActivationMode, ActivationMode>)delegate { owner.HasChanged(); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)bindingSettingsRow.HoldSeconds?.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate { owner.HasChanged(); }, 0, false); return bindingSettingsRow; } public bool ApplyVisibility() { bool flag = Preset.EditedValue != BindingPreset.Custom; bool flag2 = EditedMode != ActivationMode.Hold; bool result = ((MelonPreferences_Entry)Custom).IsHidden != flag || (HoldSeconds != null && ((MelonPreferences_Entry)HoldSeconds).IsHidden != flag2); ((MelonPreferences_Entry)Custom).IsHidden = flag; MelonPreferences_Entry<float>? holdSeconds = HoldSeconds; if (holdSeconds != null) { ((MelonPreferences_Entry)holdSeconds).IsHidden = flag2; return result; } return result; } public string? BindingError() { if (Preset.Value != BindingPreset.Custom) { return null; } if (!InputBinding.TryParse(Custom.Value, out InputBinding _, out string error)) { return error; } return null; } } public sealed class ControllerReader(Instance logger) { private readonly InputsSnapshot _snapshot = new InputsSnapshot(); private readonly Dictionary<string, KeyCode> _keyCodes = new Dictionary<string, KeyCode>(StringComparer.OrdinalIgnoreCase); private List<string> _boundKeys = new List<string>(); private bool _warnedUnknownKey; public void SetBoundKeys(IEnumerable<string> keys) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) _boundKeys = keys.ToList(); foreach (string boundKey in _boundKeys) { if (!_keyCodes.ContainsKey(boundKey)) { if (Enum.TryParse<KeyCode>(boundKey, ignoreCase: true, out KeyCode result)) { _keyCodes[boundKey] = result; } else if (!_warnedUnknownKey) { _warnedUnknownKey = true; logger.Warning("'" + boundKey + "' is not a key name, so that binding will never fire. Use a Unity KeyCode such as F7, LeftShift or Space."); } } } } public InputsSnapshot? TryRead(InputHand penHand) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) try { _snapshot.BeginFrame(penHand); Fill(InputHand.Left); Fill(InputHand.Right); foreach (string boundKey in _boundKeys) { if (_keyCodes.TryGetValue(boundKey, out var value) && Input.GetKey(value)) { _snapshot.SetKeyHeld(boundKey); } } return _snapshot; } catch (Exception) { return null; } } private void Fill(InputHand hand) { if (hand == InputHand.Left) { _snapshot.Set(hand, InputButton.Trigger, LeftController.GetTrigger()); _snapshot.Set(hand, InputButton.Grip, LeftController.GetGrip()); _snapshot.Set(hand, InputButton.Primary, LeftController.GetPrimary()); _snapshot.Set(hand, InputButton.Secondary, LeftController.GetSecondary()); _snapshot.Set(hand, InputButton.StickClick, LeftController.GetJoystickClick()); } else { _snapshot.Set(hand, InputButton.Trigger, RightController.GetTrigger()); _snapshot.Set(hand, InputButton.Grip, RightController.GetGrip()); _snapshot.Set(hand, InputButton.Primary, RightController.GetPrimary()); _snapshot.Set(hand, InputButton.Secondary, RightController.GetSecondary()); _snapshot.Set(hand, InputButton.StickClick, RightController.GetJoystickClick()); } } } public sealed class InputService { private readonly MelonBase _melon; private readonly ControllerReader _reader; private readonly InputMap _map = new InputMap(); private readonly List<BindingSettingsRow> _settings = new List<BindingSettingsRow>(); private int _revision = 1; private int _applied; public MelonPreferences_Category Category { get; } public IReadOnlyList<InputAction> Actions => _map.Actions; public InputService(MelonBase melon, string categoryId, string categoryLabel, string configPath) { _melon = melon; _reader = new ControllerReader(melon.LoggerInstance); Category = MelonPreferences.CreateCategory(categoryId, categoryLabel); Category.SetFilePath(configPath); } public InputAction Register(string id, string label, string description, BindingPreset defaultBinding, ActivationMode defaultActivation, bool activationIsFixed = false, float defaultHoldSeconds = 1f) { InputAction inputAction = _map.Register(id, label); _settings.Add(BindingSettingsRow.Create(this, Category, inputAction, description, defaultBinding, defaultActivation, activationIsFixed, defaultHoldSeconds)); return inputAction; } public void DeleteSettings(params string[] retiredEntries) { foreach (BindingSettingsRow setting in _settings) { setting.ApplyVisibility(); } foreach (string text in retiredEntries) { try { Category.DeleteEntry(text); } catch (Exception) { } } } public bool Update(float deltaTime, InputHand primaryHand) { ApplySettingsIfChanged(); InputsSnapshot inputsSnapshot = _reader.TryRead(primaryHand); if (inputsSnapshot == null) { return false; } _map.Update(inputsSnapshot, deltaTime); return true; } public void Reset() { _map.Reset(); } internal void HasChanged() { _revision++; } public void RefreshVisibility() { bool flag = false; foreach (BindingSettingsRow setting in _settings) { flag |= setting.ApplyVisibility(); } if (flag) { UI.RequestRefresh(_melon); } } private void ApplySettingsIfChanged() { if (_applied == _revision) { return; } _applied = _revision; foreach (BindingSettingsRow setting in _settings) { _map.Configure(setting.Action, setting.CurrentBinding, setting.CurrentMode, setting.CurrentHoldSeconds); string text = setting.BindingError(); if (text != null) { _melon.LoggerInstance.Warning(setting.Action.Label + " binding: " + text); } } _reader.SetBoundKeys(_map.BoundKeys()); } } } namespace RumbleParkPen.Services.Sync { public enum PenMessageKind { Unknown, StrokeStart, StrokePoints, StrokeEnd, Erase, Clear, SyncRequest, Hello } public sealed class PenMessage { public PenMessageKind Kind { get; init; } public string StrokeId { get; init; } = string.Empty; public int Actor { get; init; } public float ColorR { get; init; } public float ColorG { get; init; } public float ColorB { get; init; } public IReadOnlyList<PenPoint> Points { get; init; } = Array.Empty<PenPoint>(); public IReadOnlyList<string> StrokeIds { get; init; } = Array.Empty<string>(); public bool WantsReplay { get; init; } } public static class PenSyncFormat { public const int ProtocolVersion = 1; private const char FieldSeparator = ';'; private const char ValueSeparator = ','; private const char StartTag = 'S'; private const char PointsTag = 'P'; private const char EndTag = 'E'; private const char EraseTag = 'X'; private const char ClearTag = 'C'; private const char SyncTag = 'Q'; private const char HelloTag = 'H'; private const string PositionFormat = "0.###"; private const string WidthFormat = "0.####"; private const string ColorFormat = "0.###"; private static readonly CultureInfo Invariant = CultureInfo.InvariantCulture; public static string StrokeStart(string strokeId, float r, float g, float b) { return string.Join(';', 1.ToString(Invariant), 'S', strokeId, r.ToString("0.###", Invariant), g.ToString("0.###", Invariant), b.ToString("0.###", Invariant)); } public static string StrokePoints(string strokeId, IReadOnlyList<PenPoint> points) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(1.ToString(Invariant)).Append(';').Append('P') .Append(';') .Append(strokeId) .Append(';'); for (int i = 0; i < points.Count; i++) { if (i > 0) { stringBuilder.Append(','); } PenPoint penPoint = points[i]; stringBuilder.Append(penPoint.X.ToString("0.###", Invariant)).Append(',').Append(penPoint.Y.ToString("0.###", Invariant)) .Append(',') .Append(penPoint.Z.ToString("0.###", Invariant)) .Append(',') .Append(penPoint.Width.ToString("0.####", Invariant)); } return stringBuilder.ToString(); } public static string StrokeEnd(string strokeId) { return string.Join(';', 1.ToString(Invariant), 'E', strokeId); } public static string Erase(IReadOnlyList<string> strokeIds) { return string.Join(';', 1.ToString(Invariant), 'X', string.Join(',', strokeIds)); } public static string Clear(int actor) { return string.Join(';', 1.ToString(Invariant), 'C', actor.ToString(Invariant)); } public static string SyncRequest(int actor, bool wantsReplay) { return string.Join(';', 1.ToString(Invariant), 'Q', actor.ToString(Invariant), wantsReplay ? "1" : "0"); } public static string Hello(int actor) { return string.Join(';', 1.ToString(Invariant), 'H', actor.ToString(Invariant)); } public static PenMessage Decode(string? payload) { if (string.IsNullOrWhiteSpace(payload)) { return Unknown(); } string[] array = payload.Split(';'); if (array.Length < 2) { return Unknown(); } if (!int.TryParse(array[0], NumberStyles.Integer, Invariant, out var result) || result != 1) { return Unknown(); } if (array[1].Length != 1) { return Unknown(); } return array[1][0] switch { 'S' => DecodeStart(array), 'P' => DecodePoints(array), 'E' => (array.Length >= 3) ? new PenMessage { Kind = PenMessageKind.StrokeEnd, StrokeId = array[2] } : Unknown(), 'X' => DecodeErase(array), 'C' => DecodeActorOnly(array, PenMessageKind.Clear), 'Q' => DecodeSyncRequest(array), 'H' => DecodeActorOnly(array, PenMessageKind.Hello), _ => Unknown(), }; } private static PenMessage DecodeStart(string[] fields) { if (fields.Length < 6 || string.IsNullOrEmpty(fields[2])) { return Unknown(); } return new PenMessage { Kind = PenMessageKind.StrokeStart, StrokeId = fields[2], ColorR = ParseFloat(fields[3]), ColorG = ParseFloat(fields[4]), ColorB = ParseFloat(fields[5]) }; } private static PenMessage DecodePoints(string[] fields) { if (fields.Length < 4 || string.IsNullOrEmpty(fields[2])) { return Unknown(); } string[] array = fields[3].Split(','); List<PenPoint> list = new List<PenPoint>(array.Length / 4); for (int i = 0; i + 3 < array.Length; i += 4) { list.Add(new PenPoint(ParseFloat(array[i]), ParseFloat(array[i + 1]), ParseFloat(array[i + 2]), ParseFloat(array[i + 3]))); } if (list.Count == 0) { return Unknown(); } return new PenMessage { Kind = PenMessageKind.StrokePoints, StrokeId = fields[2], Points = list }; } private static PenMessage DecodeErase(string[] fields) { if (fields.Length < 3) { return Unknown(); } string[] array = fields[2].Split(','); List<string> list = new List<string>(array.Length); string[] array2 = array; foreach (string text in array2) { if (!string.IsNullOrWhiteSpace(text)) { list.Add(text); } } if (list.Count == 0) { return Unknown(); } return new PenMessage { Kind = PenMessageKind.Erase, StrokeIds = list }; } private static PenMessage DecodeSyncRequest(string[] fields) { PenMessage penMessage = DecodeActorOnly(fields, PenMessageKind.SyncRequest); if (penMessage.Kind == PenMessageKind.Unknown) { return penMessage; } bool wantsReplay = fields.Length < 4 || fields[3] != "0"; return new PenMessage { Kind = PenMessageKind.SyncRequest, Actor = penMessage.Actor, WantsReplay = wantsReplay }; } private static PenMessage DecodeActorOnly(string[] fields, PenMessageKind kind) { if (fields.Length < 3 || !int.TryParse(fields[2], NumberStyles.Integer, Invariant, out var result)) { return Unknown(); } return new PenMessage { Kind = kind, Actor = result }; } private static float ParseFloat(string value) { if (!float.TryParse(value, NumberStyles.Float, Invariant, out var result)) { return 0f; } return result; } private static PenMessage Unknown() { return new PenMessage { Kind = PenMessageKind.Unknown }; } } public sealed class PointBatcher { private readonly List<PenPoint> _pending = new List<PenPoint>(); private float _waited; public int MaxBatchSize { get; } = Math.Max(1, maxBatchSize); public float MaxDelaySeconds { get; } = Math.Max(0f, maxDelaySeconds); public int PendingCount => _pending.Count; public bool HasPending => _pending.Count > 0; public PointBatcher(int maxBatchSize = 12, float maxDelaySeconds = 0.1f) { } public void Add(in PenPoint point) { _pending.Add(point); } public void Advance(float deltaTime) { if (_pending.Count > 0) { _waited += deltaTime; } } public bool TryFlush([NotNullWhen(true)] out List<PenPoint>? batch) { if (_pending.Count == 0 || (_pending.Count < MaxBatchSize && _waited < MaxDelaySeconds)) { batch = null; return false; } batch = TakeAll(); return true; } public List<PenPoint> FlushRemaining() { return TakeAll(); } public void Discard() { _pending.Clear(); _waited = 0f; } private List<PenPoint> TakeAll() { List<PenPoint> result = new List<PenPoint>(_pending); _pending.Clear(); _waited = 0f; return result; } } public sealed class RemoteStrokes { private readonly Dictionary<string, StrokeData> _byId = new Dictionary<string, StrokeData>(); public int Count => _byId.Count; public IEnumerable<StrokeData> All => _byId.Values; public StrokeData? Find(string strokeId) { if (!_byId.TryGetValue(strokeId, out StrokeData value)) { return null; } return value; } public StrokeData Start(string strokeId, float r, float g, float b) { StrokeData strokeData = new StrokeData(r, g, b) { Id = strokeId }; _byId[strokeId] = strokeData; return strokeData; } public StrokeData? Append(string strokeId, IReadOnlyList<PenPoint> points) { if (!_byId.TryGetValue(strokeId, out StrokeData value)) { return null; } foreach (PenPoint point2 in points) { PenPoint point = point2; value.Add(in point); } return value; } public void EraseNear(in PenPoint probe, float radius, List<StrokeData> removed) { List<string> list = null; foreach (StrokeData value in _byId.Values) { if (value.IsWithin(in probe, radius)) { (list ?? (list = new List<string>())).Add(value.Id); removed.Add(value); } } if (list == null) { return; } foreach (string item in list) { _byId.Remove(item); } } public List<StrokeData> Erase(IReadOnlyList<string> strokeIds) { List<StrokeData> list = new List<StrokeData>(); foreach (string strokeId in strokeIds) { if (_byId.TryGetValue(strokeId, out StrokeData value)) { _byId.Remove(strokeId); list.Add(value); } } return list; } public List<StrokeData> ClearOwner(int actorNumber) { List<StrokeData> list = new List<StrokeData>(); foreach (StrokeData value in _byId.Values) { if (value.IsOwnedBy(actorNumber)) { list.Add(value); } } foreach (StrokeData item in list) { _byId.Remove(item.Id); } return list; } public List<StrokeData> ClearAll() { List<StrokeData> result = new List<StrokeData>(_byId.Values); _byId.Clear(); return result; } } } namespace RumbleParkPen.Services.Persistence { public sealed class DrawingRepository { public const string DefaultRootFolder = "UserData/RumbleParkPen"; public string RootFolder { get; } = rootFolder ?? "UserData/RumbleParkPen"; public DrawingRepository(string? rootFolder = null) { } public string PathForScene(string scene) { return Path.Combine(RootFolder, SanitiseSceneFile(scene) + ".json"); } public bool HasSave(string scene) { return File.Exists(PathForScene(scene)); } public string SaveScene(IEnumerable<StrokeData> strokes, string scene) { Directory.CreateDirectory(RootFolder); string text = PathForScene(scene); string contents = DrawingSerializer.Serialize(strokes, scene); string text2 = text + ".tmp"; File.WriteAllText(text2, contents); if (File.Exists(text)) { File.Delete(text); } File.Move(text2, text); return text; } public List<StrokeData> LoadScene(string scene) { string path = PathForScene(scene); if (!File.Exists(path)) { return new List<StrokeData>(); } return DrawingSerializer.Deserialize(File.ReadAllText(path)); } public bool DeleteScene(string scene) { string path = PathForScene(scene); if (!File.Exists(path)) { return false; } File.Delete(path); return true; } private static string SanitiseSceneFile(string scene) { if (string.IsNullOrWhiteSpace(scene)) { return "Unknown"; } char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); StringBuilder stringBuilder = new StringBuilder(scene.Length); foreach (char c in scene) { stringBuilder.Append((Array.IndexOf(invalidFileNameChars, c) >= 0) ? '_' : c); } return stringBuilder.ToString(); } } public static class DrawingSerializer { private sealed class DrawingDto { [JsonProperty("v")] public int Version { get; set; } = 1; [JsonProperty("scene")] public string Scene { get; set; } = string.Empty; [JsonProperty("strokes")] public List<StrokeDto> Strokes { get; set; } = new List<StrokeDto>(); } private sealed class StrokeDto { [JsonProperty("c")] public float[]? Color { get; set; } [JsonProperty("p")] public float[] Points { get; set; } = Array.Empty<float>(); } public const int CurrentVersion = 1; private const int FloatsPerPoint = 4; public static string Serialize(IEnumerable<StrokeData> strokes, string scene) { DrawingDto drawingDto = new DrawingDto { Version = 1, Scene = scene }; foreach (StrokeData stroke in strokes) { if (stroke.IsRenderable) { float[] array = new float[stroke.Points.Count * 4]; for (int i = 0; i < stroke.Points.Count; i++) { PenPoint penPoint = stroke.Points[i]; int num = i * 4; array[num] = penPoint.X; array[num + 1] = penPoint.Y; array[num + 2] = penPoint.Z; array[num + 3] = penPoint.Width; } drawingDto.Strokes.Add(new StrokeDto { Color = new float[3] { stroke.ColorR, stroke.ColorG, stroke.ColorB }, Points = array }); } } return JsonConvert.SerializeObject((object)drawingDto, (Formatting)0); } public static List<StrokeData> Deserialize(string json) { List<StrokeData> list = new List<StrokeData>(); if (string.IsNullOrWhiteSpace(json)) { return list; } DrawingDto drawingDto = JsonConvert.DeserializeObject<DrawingDto>(json); if (drawingDto?.Strokes == null) { return list; } foreach (StrokeDto stroke in drawingDto.Strokes) { if (stroke?.Points != null) { float[] array = stroke.Color ?? Array.Empty<float>(); StrokeData strokeData = new StrokeData((array.Length != 0) ? array[0] : 1f, (array.Length > 1) ? array[1] : 1f, (array.Length > 2) ? array[2] : 1f); int num = stroke.Points.Length / 4; for (int i = 0; i < num; i++) { int num2 = i * 4; strokeData.Add(new PenPoint(stroke.Points[num2], stroke.Points[num2 + 1], stroke.Points[num2 + 2], stroke.Points[num2 + 3])); } if (strokeData.IsRenderable) { list.Add(strokeData); } } } return list; } } } namespace RumbleParkPen.Services.Input { public enum BindingPreset { None, DominantTrigger, DominantGrip, DominantPrimary, DominantSecondary, DominantStickClick, NonDominantTrigger, NonDominantGrip, NonDominantPrimary, NonDominantSecondary, NonDominantStickClick, DominantPrimaryAndSecondary, NonDominantPrimaryAndSecondary, NonDominantGripAndPrimary, BothTriggers, BothGrips, BothPrimaries, BothSecondaries, Custom } public static class BindingPresetExtensions { private static readonly Dictionary<BindingPreset, string> Combos = new Dictionary<BindingPreset, string> { [BindingPreset.DominantPrimaryAndSecondary] = "DominantPrimary+DominantSecondary", [BindingPreset.NonDominantPrimaryAndSecondary] = "NonDominantPrimary+NonDominantSecondary", [BindingPreset.NonDominantGripAndPrimary] = "NonDominantGrip+NonDominantPrimary", [BindingPreset.BothTriggers] = "LeftTrigger+RightTrigger", [BindingPreset.BothGrips] = "LeftGrip+RightGrip", [BindingPreset.BothPrimaries] = "LeftPrimary+RightPrimary", [BindingPreset.BothSecondaries] = "LeftSecondary+RightSecondary" }; public static string? BindingText(this BindingPreset preset) { switch (preset) { case BindingPreset.Custom: return null; case BindingPreset.None: return "None"; default: { if (!Combos.TryGetValue(preset, out string value)) { return preset.ToString(); } return value; } } } public static InputBinding Resolve(this BindingPreset preset, string? customText) { return InputBinding.Parse(preset.BindingText() ?? customText); } } public sealed class ButtonLatch { private bool _holdFired; public bool Tapped { get; private set; } public bool HoldTriggered { get; private set; } public bool WasPressed { get; private set; } public float HeldTime { get; private set; } public void Update(bool pressed, float deltaTime, float holdSeconds) { Tapped = false; HoldTriggered = false; if (pressed) { HeldTime += deltaTime; if (!_holdFired && HeldTime >= holdSeconds) { HoldTriggered = true; _holdFired = true; } } else if (WasPressed) { if (!_holdFired) { Tapped = true; } HeldTime = 0f; _holdFired = false; } WasPressed = pressed; } public void Reset() { _holdFired = false; WasPressed = false; HeldTime = 0f; Tapped = false; HoldTriggered = false; } } public sealed class InputAction(string id, string label) { public string Id { get; } = id; public string Label { get; } = label; public InputBinding Binding { get; private set; } = InputBinding.None; public ActivationMode Mode { get; private set; } = ActivationMode.Tap; public float HoldSeconds { get; private set; } = 1f; public bool IsHeld { get; internal set; } public bool Fired { get; internal set; } public float Analog { get; internal set; } public Action<InputAction>? OnState { get; set; } public Action? OnFired { get; set; } internal ButtonLatch Latch { get; } = new ButtonLatch(); internal float LatchHoldSeconds { get; set; } = float.MaxValue; internal bool ShadowedThisPress { get; set; } internal InputAction[] ShadowedBy { get; set; } = Array.Empty<InputAction>(); public void Configure(InputBinding binding, ActivationMode mode, float holdSeconds) { Binding = binding; Mode = mode; HoldSeconds = holdSeconds; } internal void Evaluate(InputsSnapshot snapshot, float deltaTime) { bool flag = Binding.IsSatisfiedBy(snapshot); if (!flag) { ShadowedThisPress = false; } else if (IsShadowed(snapshot)) { ShadowedThisPress = true; } if (flag && ShadowedThisPress) { Latch.Reset(); ClearState(); return; } IsHeld = flag && Mode == ActivationMode.WhileHeld; Analog = (flag ? Binding.AnalogIn(snapshot) : 0f); Latch.Update(flag, deltaTime, LatchHoldSeconds); Fired = Mode switch { ActivationMode.Tap => Latch.Tapped, ActivationMode.Hold => Latch.HoldTriggered, ActivationMode.WhileHeld => false, _ => false, }; } private bool IsShadowed(InputsSnapshot snapshot) { InputAction[] shadowedBy = ShadowedBy; for (int i = 0; i < shadowedBy.Length; i++) { if (shadowedBy[i].Binding.IsSatisfiedBy(snapshot)) { return true; } } return false; } internal void Dispatch() { OnState?.Invoke(this); if (Fired) { OnFired?.Invoke(); } } internal void ClearState() { IsHeld = false; Fired = false; Analog = 0f; } public override string ToString() { return $"{Id} ({Binding}, {Mode})"; } } public sealed class InputMap { private readonly List<InputAction> _actions = new List<InputAction>(); public IReadOnlyList<InputAction> Actions => _actions; public InputAction Register(string id, string label) { InputAction inputAction = new InputAction(id, label); _actions.Add(inputAction); return inputAction; } public InputAction? Find(string id) { return _actions.Find((InputAction a) => string.Equals(a.Id, id, StringComparison.Ordinal)); } public void Configure(InputAction action, InputBinding binding, ActivationMode mode, float holdSeconds) { action.Configure(binding, mode, holdSeconds); RecomputeRelationships(); } private void RecomputeRelationships() { foreach (InputAction action in _actions) { action.ShadowedBy = _actions.Where((InputAction other) => other != action && action.Binding.IsSubsetOf(other.Binding)).ToArray(); } foreach (InputAction action2 in _actions) { if (action2.Mode == ActivationMode.Hold) { action2.LatchHoldSeconds = action2.HoldSeconds; continue; } if (action2.Binding.IsUnbound) { action2.LatchHoldSeconds = float.MaxValue; continue; } float num = float.MaxValue; foreach (InputAction action3 in _actions) { if (action3 != action2 && action3.Mode == ActivationMode.Hold && action2.Binding.HasSameInputsAs(action3.Binding)) { num = Math.Min(num, action3.HoldSeconds); } } action2.LatchHoldSeconds = num; } } public void Update(InputsSnapshot snapshot, float deltaTime) { foreach (InputAction action in _actions) { action.Evaluate(snapshot, deltaTime); } foreach (InputAction action2 in _actions) { action2.Dispatch(); } } public void Reset() { foreach (InputAction action in _actions) { action.Latch.Reset(); action.ClearState(); action.ShadowedThisPress = false; } } public List<string> BoundKeys() { List<string> list = new List<string>(); foreach (InputAction action in _actions) { foreach (string key in action.Binding.Keys) { if (!list.Contains<string>(key, StringComparer.OrdinalIgnoreCase)) { list.Add(key); } } } return list; } } public enum InputHand { Dominant, NonDominant, Left, Right } public enum InputButton { Trigger, Grip, Primary, Secondary, StickClick } public enum ActivationMode { WhileHeld, Tap, Hold } public readonly record struct ButtonLocation(InputHand Hand, InputButton Button); public sealed class InputBinding(IReadOnlyList<ButtonLocation> controller, IReadOnlyList<string> keys) { public static readonly InputBinding None = new InputBinding(Array.Empty<ButtonLocation>(), Array.Empty<string>()); public const string UnboundText = "None"; private const char TermSeparator = '+'; private const string KeyPrefix = "Key:"; private static readonly InputButton[] ButtonsByNameLength = (from b in Enum.GetValues<InputButton>() orderby b.ToString().Length descending select b).ToArray(); public IReadOnlyList<ButtonLocation> Controller { get; } = controller; public IReadOnlyList<string> Keys { get; } = keys; public bool IsUnbound { get { if (Controller.Count == 0) { return Keys.Count == 0; } return false; } } public int TotalInputs => Controller.Count + Keys.Count; public bool IsSatisfiedBy(InputsSnapshot snapshot) { if (IsUnbound) { return false; } foreach (ButtonLocation item in Controller) { if (!snapshot.IsPressed(item)) { return false; } } foreach (string key in Keys) { if (!snapshot.IsKeyHeld(key)) { return false; } } return true; } public bool IsSubsetOf(InputBinding other) { if (IsUnbound || other.IsUnbound) { return false; } if (TotalInputs >= other.TotalInputs) { return false; } foreach (ButtonLocation item in Controller) { if (!other.Controller.Contains(item)) { return false; } } foreach (string key in Keys) { if (!other.Keys.Contains<string>(key, StringComparer.OrdinalIgnoreCase)) { return false; } } return true; } public bool HasSameInputsAs(InputBinding other) { if (TotalInputs != other.TotalInputs) { return false; } if (IsUnbound == other.IsUnbound && Controller.All(((IEnumerable<ButtonLocation>)other.Controller).Contains<ButtonLocation>)) { return Keys.All((string k) => other.Keys.Contains<string>(k, StringComparer.OrdinalIgnoreCase)); } return false; } public float AnalogIn(InputsSnapshot snapshot) { if (Controller.Count == 0) { return 1f; } float num = 1f; foreach (ButtonLocation item in Controller) { num = Math.Min(num, snapshot.Value(item.Hand, item.Button)); } return num; } public static bool TryParse(string? text, out InputBinding binding, out string? error) { binding = None; error = null; if (string.IsNullOrWhiteSpace(text)) { return true; } if (string.Equals(text.Trim(), "None", StringComparison.OrdinalIgnoreCase)) { return true; } List<ButtonLocation> list = new List<ButtonLocation>(); List<string> list2 = new List<string>(); string[] array = text.Split('+'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0) { continue; } if (text2.StartsWith("Key:", StringComparison.OrdinalIgnoreCase)) { string text3 = text2.Substring("Key:".Length).Trim(); if (text3.Length == 0) { error = "'" + text2 + "' is missing a key name, e.g. Key:F7"; return false; } if (!list2.Contains<string>(text3, StringComparer.OrdinalIgnoreCase)) { list2.Add(text3); } } else { if (!TryParseButtonLocation(text2, out var parsed)) { error = "'" + text2 + "' is not a button. Expected something like DominantTrigger, NonDominantPrimary, LeftGrip, RightStickClick or Key:F7"; return false; } if (!list.Contains(parsed)) { list.Add(parsed); } } } binding = ((list.Count == 0 && list2.Count == 0) ? None : new InputBinding(list, list2)); return true; } public static InputBinding Parse(string? text) { if (!TryParse(text, out InputBinding binding, out string _)) { return None; } return binding; } public override string ToString() { if (IsUnbound) { return "None"; } StringBuilder stringBuilder = new StringBuilder(); foreach (ButtonLocation item in Controller) { if (stringBuilder.Length > 0) { stringBuilder.Append('+'); } stringBuilder.Append(item.Hand).Append(item.Button); } foreach (string key in Keys) { if (stringBuilder.Length > 0) { stringBuilder.Append('+'); } stringBuilder.Append("Key:").Append(key); } return stringBuilder.ToString(); } private static bool TryParseButtonLocation(string term, out ButtonLocation parsed) { parsed = default(ButtonLocation); InputButton[] buttonsByNameLength = ButtonsByNameLength; for (int i = 0; i < buttonsByNameLength.Length; i++) { InputButton button = buttonsByNameLength[i]; string text = button.ToString(); if (term.Length > text.Length && term.EndsWith(text, StringComparison.OrdinalIgnoreCase)) { int length = text.Length; if (Enum.TryParse<InputHand>(term.Substring(0, term.Length - length), ignoreCase: true, out var result)) { parsed = new ButtonLocation(result, button); return true; } } } return false; } } public sealed class InputsSnapshot { public const float PressThreshold = 0.5f; private const int Hands = 2; private const int Buttons = 5; private readonly float[,] _values = new float[2, 5]; private readonly HashSet<string> _heldKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase); public InputHand DominantHand { get; private set; } = InputHand.Right; public void BeginFrame(InputHand dominantHand) { DominantHand = ((dominantHand == InputHand.Left) ? InputHand.Left : InputHand.Right); Array.Clear(_values, 0, _values.Length); _heldKeys.Clear(); } public void Set(InputHand hand, InputButton button, float value) { _values[HandIndex(Resolve(hand)), (int)button] = value; } public void SetKeys(IEnumerable<string> heldKeys) { _heldKeys.Clear(); foreach (string heldKey in heldKeys) { _heldKeys.Add(heldKey); } } public void SetKeyHeld(string key) { _heldKeys.Add(key); } public float Value(InputHand hand, InputButton button) { return _values[HandIndex(Resolve(hand)), (int)button]; } public bool IsPressed(ButtonLocation term) { return Value(term.Hand, term.Button) > 0.5f; } public bool IsKeyHeld(string key) { return _heldKeys.Contains(key); } public InputHand Resolve(InputHand hand) { return hand switch { InputHand.Dominant => DominantHand, InputHand.NonDominant => (DominantHand == InputHand.Left) ? InputHand.Right : InputHand.Left, InputHand.Left => InputHand.Left, InputHand.Right => InputHand.Right, _ => hand, }; } private static int HandIndex(InputHand physical) { return (physical != InputHand.Left) ? 1 : 0; } } } namespace RumbleParkPen.Services.Drawing { public readonly struct PaletteColor { public string Name { get; } public float R { get; } public float G { get; } public float B { get; } public PaletteColor(string name, float r, float g, float b) { Name = name; R = r; G = g; B = b; } public float SquaredDistanceTo(float r, float g, float b) { float num = R - r; float num2 = G - g; float num3 = B - b; return num * num + num2 * num2 + num3 * num3; } } public sealed class PenPalette { public static readonly IReadOnlyList<PaletteColor> Default = new <>z__ReadOnlyArray<PaletteColor>(new PaletteColor[10] { new PaletteColor("White", 1f, 1f, 1f), new PaletteColor("Red", 0.95f, 0.15f, 0.15f), new PaletteColor("Orange", 1f, 0.5f, 0.05f), new PaletteColor("Yellow", 1f, 0.92f, 0.1f), new PaletteColor("Green", 0.2f, 0.85f, 0.25f), new PaletteColor("Cyan