diff --git a/ChatBoxHud/ChatBoxHud.csproj b/ChatBoxHud/ChatBoxHud.csproj new file mode 100644 index 0000000..1b27897 --- /dev/null +++ b/ChatBoxHud/ChatBoxHud.csproj @@ -0,0 +1,6 @@ + + + + ChatBoxHud + + diff --git a/ChatBoxHud/Main.cs b/ChatBoxHud/Main.cs new file mode 100644 index 0000000..107e8e6 --- /dev/null +++ b/ChatBoxHud/Main.cs @@ -0,0 +1,438 @@ +using System.Text; +using ABI_RC.Core.Player; +using ABI_RC.Core.Networking.IO.Social; +using ABI_RC.Core.UI; +using ABI_RC.Core.UI.Hud; +using ABI_RC.Systems.ChatBox; +using ABI_RC.Systems.Communications.Settings; +using ABI_RC.Systems.GameEventSystem; +using ABI_RC.Systems.PlayerColors; +using MelonLoader; +using UnityEngine; + +namespace NAK.ChatBoxHud; + +public class ChatBoxHudMod : MelonMod +{ + public enum HudTarget { Disabled, LookAt, Nearest, LookAtThenNearest } + public enum SourceFilter { None, FriendsOnly, Everyone } + public enum MaxUsersOnHud { One, Two, Three } + + private const int MaxLines = 4; + private const int BaseCPL = 62; + private const int BaseSize = 80; + private const int MinSize = 50; + private const int ShrinkAt = 3; + private const float MinLife = 5f; + private const float MaxLife = 20f; + private const float WPM = 250f; + private const float CPW = 5f; + private const float LookAngle = 20f; + private const float FadePct = 0.2f; + private const int MsgCap = 1000; + + private static readonly MelonPreferences_Category Cat = + MelonPreferences.CreateCategory(nameof(ChatBoxHud)); + + internal static readonly MelonPreferences_Entry EntryHudMode = + Cat.CreateEntry("hud_mode", HudTarget.LookAtThenNearest, "HUD Target Mode"); + + internal static readonly MelonPreferences_Entry EntryHudFilter = + Cat.CreateEntry("hud_filter", SourceFilter.Everyone, "Chat Filter", + description: "Whose chat messages appear on the HUD."); + + internal static readonly MelonPreferences_Entry EntryMaxUsersOnHud = + Cat.CreateEntry("max_users_on_hud", MaxUsersOnHud.Three, "Max Users on HUD", + description: "How many users can be stacked on the HUD when multiple users chat at once."); + + internal static readonly MelonPreferences_Entry EntryUsePlayerColors = + Cat.CreateEntry("player_colors", true, "Use Player Colors", + description: "Use player-chosen colors for names instead of friend colors."); + + private static readonly Dictionary Buffers = new(); + private static readonly StringBuilder SB = new(); + private static readonly int[] VisSlots = new int[MaxLines * 3 + 1]; + + private static PortalHudIndicator _hud; + + #region Slot System + + // Stable slot list: once a user occupies an index they keep it until + // all their messages expire or they leave range. New users append to + // the first open position. This prevents blocks from jumping around + // when someone else sends a new message. + private static readonly List _slotIds = new(); + private static bool _hudActive; + + private struct Candidate + { + public string Id, Name; + public Vector3 Pos; + public float Score; // lower = higher priority + } + + private static readonly List _candidates = new(); + private static readonly Dictionary _candidateLookup = new(); + + #endregion + + private struct Incoming + { + public string Id, Name, Text; + public ChatBoxAPI.MessageSource Source; + } + + private class Msg + { + public string[] Lines; + public int Size; + public float Stamp, Life; + public ChatBoxAPI.MessageSource Source; + } + + private class ChatBuf + { + public string Id; + public readonly List Msgs = new(); + } + + public override void OnInitializeMelon() + { + ChatBoxAPI.OnMessageReceived += OnReceive; + CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(OnPlayerSetupStart); + } + + private static void OnPlayerSetupStart() + { + Transform src = HudController.Instance.PortalHudIndicator.transform; + GameObject go = UnityEngine.Object.Instantiate(src.gameObject, src.parent); + go.transform.SetLocalPositionAndRotation(src.localPosition, src.localRotation); + go.transform.localScale = src.localScale; + _hud = go.GetComponent(); + } + + public override void OnUpdate() + { + Tick(); + } + + private static void Tick() + { + if (_hud == null || EntryHudMode.Value == HudTarget.Disabled) { Hide(); return; } + Prune(); + + int maxSlots = (int)EntryMaxUsersOnHud.Value + 1; + GatherCandidates(); + + // Remove slots whose user has no messages left or is no longer + // a valid candidate (walked out of range, left the instance, etc.) + for (int i = _slotIds.Count - 1; i >= 0; i--) + { + string sid = _slotIds[i]; + bool dead = !_candidateLookup.ContainsKey(sid) + || !Buffers.TryGetValue(sid, out ChatBuf b) + || b.Msgs.Count == 0; + if (dead) _slotIds.RemoveAt(i); + } + + // If the setting was lowered at runtime, trim from the end + while (_slotIds.Count > maxSlots) + _slotIds.RemoveAt(_slotIds.Count - 1); + + // Fill empty slots from best-scored candidates not already slotted + if (_slotIds.Count < maxSlots && _candidates.Count > 0) + { + _candidates.Sort((a, b) => a.Score.CompareTo(b.Score)); + foreach (Candidate c in _candidates) + { + if (_slotIds.Count >= maxSlots) break; + if (_slotIds.Contains(c.Id)) continue; + _slotIds.Add(c.Id); + } + } + + if (_slotIds.Count == 0) { Hide(); return; } + + // Render every active slot into one combined string + SB.Clear(); + bool first = true; + foreach (string slotId in _slotIds) + { + if (!_candidateLookup.TryGetValue(slotId, out Candidate cand)) continue; + if (!Buffers.TryGetValue(slotId, out ChatBuf buf) || buf.Msgs.Count == 0) continue; + + if (!first) SB.Append('\n'); + RenderBlock(SB, cand.Name, cand.Id, buf, cand.Pos); + first = false; + } + + _hud.SetText(SB.ToString()); + if (!_hudActive) { _hudActive = true; _hud.Activate(); } + } + + private static void Hide() + { + if (!_hudActive) return; + _hud?.Deactivate(); + _hudActive = false; + _slotIds.Clear(); + } + + // Scores every player who has buffered messages, is in comms range, + // and passes the current HudTarget mode filter. + private static void GatherCandidates() + { + _candidates.Clear(); + _candidateLookup.Clear(); + + HudTarget mode = EntryHudMode.Value; + Camera cam = Camera.main; + if (cam == null) return; + + Vector3 lp = PlayerSetup.Instance.GetPlayerPosition(); + Vector3 cf = cam.transform.forward; + Vector3 cp = cam.transform.position; + float commsRange = Comms_SettingsHandler.FalloffDistance + 1; + + foreach (KeyValuePair kvp in Buffers) + { + if (kvp.Value.Msgs.Count == 0) continue; + if (!CVRPlayerManager.Instance.TryGetPlayerBase(kvp.Key, out PlayerBase pb)) continue; + + Vector3 pp = pb.GetPlayerWorldRootPosition(); + float d = Vector3.Distance(lp, pp); + if (d > commsRange) continue; + + string uid = kvp.Key; + string n = pb.PlayerUsername ?? "???"; + float angle = Vector3.Angle(cf, (pp - cp).normalized); + bool inLook = angle < LookAngle; + + float score; + switch (mode) + { + case HudTarget.LookAt: + if (!inLook) continue; // hard filter + score = angle; + break; + case HudTarget.Nearest: + score = d; + break; + case HudTarget.LookAtThenNearest: + // look-at targets always rank above distance-only + score = inLook ? angle : 1000f + d; + break; + default: + continue; + } + + Candidate c = new Candidate { Id = uid, Name = n, Pos = pp, Score = score }; + _candidates.Add(c); + _candidateLookup[uid] = c; + } + } + + private static void RenderBlock(StringBuilder sb, string playerName, string playerId, + ChatBuf buf, Vector3 tgtPos) + { + Camera cam = PlayerSetup.Instance.activeCam; + float now = Time.time; + + // direction arrows from camera angle to target + float la = 0f, ra = 0f; + { + Vector3 toT = tgtPos - cam.transform.position; + Vector3 fwd = cam.transform.forward; + toT.y = 0f; + fwd.y = 0f; + if (toT.sqrMagnitude > 0.001f && fwd.sqrMagnitude > 0.001f) + { + float sa = Vector3.SignedAngle(fwd.normalized, toT.normalized, Vector3.up); + float abs = Mathf.Abs(sa); + if (abs > LookAngle) + { + float t = Mathf.InverseLerp(LookAngle, 90f, abs); + float v = Mathf.Clamp01(t); + la = sa < 0f ? v : 0f; + ra = sa > 0f ? v : 0f; + } + } + } + + // name color: player colors or friend-based fallback + string hex; + if (EntryUsePlayerColors.Value) + { + PlayerColors pc = PlayerColorsManager.GetPlayerColors(playerId); + hex = "#" + ColorUtility.ToHtmlStringRGB(pc.PrimaryColor); + } + else hex = Friends.FriendsWith(playerId) ? "#4FC3F7" : "#E0E0E0"; + + // header line + AppendAlpha(sb, la); sb.Append("\u25C4 "); + sb.Append("') + .Append(Esc(playerName)).Append(""); + AppendAlpha(sb, ra); sb.Append(" \u25BA\n"); + + // allocate visible line count per message, newest gets priority + int count = 0, budget = MaxLines; + for (int i = buf.Msgs.Count - 1; i >= 0 && budget > 0; i--) + { + int v = Mathf.Min(buf.Msgs[i].Lines.Length, budget); + VisSlots[count++] = v; + budget -= v; + } + + // render oldest to newest (slots were stored newest-first, so reverse) + int startIdx = buf.Msgs.Count - count; + for (int i = 0; i < count; i++) + { + int mi = startIdx + i; + int vis = VisSlots[count - 1 - i]; + Msg m = buf.Msgs[mi]; + float age = now - m.Stamp; + + float fadeAt = m.Life * (1f - FadePct); + float alpha = age < fadeAt ? 1f + : Mathf.Clamp01(1f - (age - fadeAt) / (m.Life * FadePct)); + byte a = (byte)(alpha * 255); + + // smooth line-by-line scroll for overflowing messages + int off = 0; + if (m.Lines.Length > vis) + { + float usable = m.Life * (1f - FadePct); + float perLine = usable / m.Lines.Length; + float initHold = perLine * vis; + off = Mathf.Min( + (int)(Mathf.Max(0f, age - initHold) / perLine), + m.Lines.Length - vis); + } + + for (int j = 0; j < vis; j++) + { + sb.Append("') + .Append("") + .Append(m.Lines[off + j]) + .Append("\n"); + } + } + + // trim trailing newline from this block + if (sb.Length > 0 && sb[sb.Length - 1] == '\n') sb.Length--; + } + + private static void AppendAlpha(StringBuilder sb, float a01) + { + sb.Append("'); + } + + private static void OnReceive(ChatBoxAPI.ChatBoxMessage msg) + { + if (msg.Source != ChatBoxAPI.MessageSource.Internal) return; + if (string.IsNullOrWhiteSpace(msg.Message)) return; + + string sid = msg.SenderGuid; + if (!CVRPlayerManager.Instance.TryGetPlayerBase(sid, out PlayerBase pb)) return; + + SourceFilter filter = EntryHudFilter.Value; + if (filter == SourceFilter.None) return; + if (filter == SourceFilter.FriendsOnly && !Friends.FriendsWith(sid)) return; + + Store(new Incoming + { + Id = sid, + Name = pb.PlayerUsername ?? "???", + Text = msg.Message, + Source = msg.Source + }); + } + + private static void Store(in Incoming inc) + { + if (!Buffers.TryGetValue(inc.Id, out ChatBuf buf)) + { + buf = new ChatBuf { Id = inc.Id }; + Buffers[inc.Id] = buf; + } + + string text = inc.Text; + if (text.Length > MsgCap) text = text.Substring(0, MsgCap); + text = text.Replace("\r", ""); + + Fit(text, out string[] lines, out int size); + for (int i = 0; i < lines.Length; i++) lines[i] = Esc(lines[i]); + + buf.Msgs.Add(new Msg + { + Lines = lines, + Size = size, + Stamp = Time.time, + Life = Life(text.Length), + Source = inc.Source + }); + + while (buf.Msgs.Count > MaxLines * 3) + buf.Msgs.RemoveAt(0); + } + + private static void Prune() + { + float now = Time.time; + foreach (ChatBuf buf in Buffers.Values) + buf.Msgs.RemoveAll(m => now - m.Stamp > m.Life); + } + + // shrink text size until line count is manageable + private static void Fit(string text, out string[] lines, out int size) + { + lines = Wrap(text, BaseCPL); + size = BaseSize; + if (lines.Length <= ShrinkAt) return; + + for (int s = BaseSize - 5; s >= MinSize; s -= 5) + { + int cpl = (int)(BaseCPL * BaseSize / (float)s); + string[] attempt = Wrap(text, cpl); + lines = attempt; size = s; + if (attempt.Length <= ShrinkAt) return; + } + } + + // word wrap respecting embedded newlines + private static string[] Wrap(string text, int max) + { + if (string.IsNullOrEmpty(text)) return new[] { string.Empty }; + + List result = new List(4); + foreach (string seg in text.Split('\n')) + { + if (seg.Length <= max) { result.Add(seg); continue; } + + StringBuilder cur = new StringBuilder(max); + foreach (string w in seg.Split(' ')) + { + if (w.Length == 0) continue; + if (w.Length > max) + { + if (cur.Length > 0) { result.Add(cur.ToString()); cur.Clear(); } + for (int c = 0; c < w.Length; c += max) + result.Add(w.Substring(c, Math.Min(max, w.Length - c))); + continue; + } + if (cur.Length == 0) cur.Append(w); + else if (cur.Length + 1 + w.Length <= max) cur.Append(' ').Append(w); + else { result.Add(cur.ToString()); cur.Clear(); cur.Append(w); } + } + if (cur.Length > 0) result.Add(cur.ToString()); + } + return result.ToArray(); + } + + private static float Life(int chars) + { + return Mathf.Clamp(MinLife + chars * (60f / (WPM * CPW)), MinLife, MaxLife); + } + + private static string Esc(string s) => s.Replace("<", "<\u200B"); +} \ No newline at end of file diff --git a/ChatBoxHud/Properties/AssemblyInfo.cs b/ChatBoxHud/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a6cd27b --- /dev/null +++ b/ChatBoxHud/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using MelonLoader; +using NAK.ChatBoxHud.Properties; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.ChatBoxHud))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.ChatBoxHud))] + +[assembly: MelonInfo( + typeof(NAK.ChatBoxHud.ChatBoxHudMod), + nameof(NAK.ChatBoxHud), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ChatBoxHud" +)] + +[assembly: MelonGame("ChilloutVR", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonColor(255, 246, 25, 99)] // red-pink +[assembly: MelonAuthorColor(255, 158, 21, 32)] // red +[assembly: HarmonyDontPatchAll] + +namespace NAK.ChatBoxHud.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "1.0.0"; + public const string Author = "NotAKidoS"; +} \ No newline at end of file diff --git a/ChatBoxHud/README.md b/ChatBoxHud/README.md new file mode 100644 index 0000000..126be0a --- /dev/null +++ b/ChatBoxHud/README.md @@ -0,0 +1,14 @@ +# ChatBoxHud + +Shows nearby ChatBox messages directly on your HUD so you can read them comfortably in VR without breaking your neck. + +--- + +Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI. +https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games + +> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive. + +> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use. + +> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive. diff --git a/ChatBoxHud/format.json b/ChatBoxHud/format.json new file mode 100644 index 0000000..cd46a8b --- /dev/null +++ b/ChatBoxHud/format.json @@ -0,0 +1,23 @@ +{ + "_id": -1, + "name": "ChatBoxHud", + "modversion": "1.0.0", + "gameversion": "2026r181", + "loaderversion": "0.7.2", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Shows nearby ChatBox messages directly on your HUD so you can read them comfortably in VR without breaking your neck.", + "searchtags": [ + "chat", + "box", + "hud", + "indicator", + "captions" + ], + "requirements": [ + ], + "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r49/ChatBoxHud.dll", + "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ChatBoxHud/", + "changelog": "- Initial release", + "embedcolor": "#f61963" +} \ No newline at end of file diff --git a/README.md b/README.md index 79bc57d..9b18896 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,26 @@ | Name | Description | Download | |------|-------------|----------| -| [ASTExtension](ASTExtension/README.md) | Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool): | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ASTExtension.dll) | +| [ASTExtension](ASTExtension/README.md) | Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool): | No Download | +| [ChatBoxHud](ChatBoxHud/README.md) | Shows nearby ChatBox messages directly on your HUD so you can read them comfortably in VR without breaking your neck. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r49/ChatBoxHud.dll) | | [ConfigureCalibrationPose](ConfigureCalibrationPose/README.md) | Select FBT calibration pose. | No Download | -| [CustomSpawnPoint](CustomSpawnPoint/README.md) | Replaces the unused Images button in the World Details page with a button to set a custom spawn point. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/CustomSpawnPoint.dll) | -| [DesktopInteractions](DesktopInteractions/README.md) | Adds IK-driven hand gestures to your avatar in Desktop: earpiece grab (GMOD-style) when typing in ChatBox, and binocular cupping when zooming. Both gestures are toggleable in settings. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/DesktopInteractions.dll) | -| [DoubleTapJumpToExitSeat](DoubleTapJumpToExitSeat/README.md) | Replaces seat exit controls with a double-tap of the jump button, avoiding accidental exits from joystick drift or opening the menu. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/DoubleTapJumpToExitSeat.dll) | -| [ESCBothMenus](ESCBothMenus/README.md) | Makes the Quick Menu appear when pressing ESC in Desktop. Pressing twice will open straight to Main Menu. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ESCBothMenus.dll) | -| [FuckToes](FuckToes/README.md) | Prevents VRIK from autodetecting toes in Halfbody or Fullbody. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/FuckToes.dll) | -| [PlapPlapForAll](PlapPlapForAll/README.md) | Penetrator SFX mod which adds Noach's PlapPlap prefab to any detected DPS setup on avatars that do not already have it. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PlapPlapForAll.dll) | -| [PropLoadingHexagon](PropLoadingHexagon/README.md) | https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/a892c765-71c1-47f3-a781-bdb9b60ba117 | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PropLoadingHexagon.dll) | -| [PropsButBetter](PropsButBetter/README.md) | Prop quality-of-life suite. Adds a few new ways to interact with and manage Props. | No Download | -| [RCCVirtualSteeringWheel](RCCVirtualSteeringWheel/README.md) | Allows you to physically grab rigged RCC steering wheels in VR to provide steering input. No explicit setup required other than defining the Steering Wheel transform within the RCC component. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RCCVirtualSteeringWheel.dll) | -| [RelativeSyncJitterFix](RelativeSyncJitterFix/README.md) | Relative sync jitter fix is the single harmony patch that could not make it into the native release of RelativeSync. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RelativeSyncJitterFix.dll) | -| [ShareBubbles](ShareBubbles/README.md) | Share Bubbles! Allows you to drop down bubbles containing Avatars & Props. Requires both users to have the mod installed. Synced over Mod Network. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ShareBubbles.dll) | -| [ShowPlayerInSelfMirror](ShowPlayerInSelfMirror/README.md) | Adds an option in the Quick Menu selected player page to show the target player's avatar in your self mirror. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ShowPlayerInSelfMirror.dll) | -| [SmootherRay](SmootherRay/README.md) | Smoothes your controller while the raycast lines are visible. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/SmootherRay.dll) | +| [CustomSpawnPoint](CustomSpawnPoint/README.md) | Replaces the unused Images button in the World Details page with a button to set a custom spawn point. | No Download | +| [DesktopInteractions](DesktopInteractions/README.md) | Adds IK-driven hand gestures to your avatar in Desktop: earpiece grab (GMOD-style) when typing in ChatBox, and binocular cupping when zooming. Both gestures are toggleable in settings. | No Download | +| [DoubleTapJumpToExitSeat](DoubleTapJumpToExitSeat/README.md) | Replaces seat exit controls with a double-tap of the jump button, avoiding accidental exits from joystick drift or opening the menu. | No Download | +| [ESCBothMenus](ESCBothMenus/README.md) | Makes the Quick Menu appear when pressing ESC in Desktop. Pressing twice will open straight to Main Menu. | No Download | +| [FuckToes](FuckToes/README.md) | Prevents VRIK from autodetecting toes in Halfbody or Fullbody. | No Download | +| [PlapPlapForAll](PlapPlapForAll/README.md) | Penetrator SFX mod which adds Noach's PlapPlap prefab to any detected DPS setup on avatars that do not already have it. | No Download | +| [PropLoadingHexagon](PropLoadingHexagon/README.md) | https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/a892c765-71c1-47f3-a781-bdb9b60ba117 | No Download | +| [PropsButBetter](PropsButBetter/README.md) | Prop quality-of-life suite. Adds a few new ways to interact with and manage Props. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r49/PropsButBetter.dll) | +| [RCCVirtualSteeringWheel](RCCVirtualSteeringWheel/README.md) | Allows you to physically grab rigged RCC steering wheels in VR to provide steering input. No explicit setup required other than defining the Steering Wheel transform within the RCC component. | No Download | +| [RelativeSyncJitterFix](RelativeSyncJitterFix/README.md) | Relative sync jitter fix is the single harmony patch that could not make it into the native release of RelativeSync. | No Download | +| [ShareBubbles](ShareBubbles/README.md) | Share Bubbles! Allows you to drop down bubbles containing Avatars & Props. Requires both users to have the mod installed. Synced over Mod Network. | No Download | +| [ShowPlayerInSelfMirror](ShowPlayerInSelfMirror/README.md) | Adds an option in the Quick Menu selected player page to show the target player's avatar in your self mirror. | No Download | +| [SmootherRay](SmootherRay/README.md) | Smoothes your controller while the raycast lines are visible. | No Download | | [Stickers](Stickers/README.md) | Stickers! Allows you to place small images on any surface. Requires both users to have the mod installed. Synced over Mod Network. | No Download | -| [ThirdPerson](ThirdPerson/README.md) | Original repo: https://github.com/oestradiol/CVR-Mods | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ThirdPerson.dll) | -| [Tinyboard](Tinyboard/README.md) | Makes the keyboard small and smart. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/Tinyboard.dll) | -| [YouAreMyPropNowWeAreHavingSoftTacosLater](YouAreMyPropNowWeAreHavingSoftTacosLater/README.md) | Lets you bring held, attached, and occupied props through world loads. This is configurable in the mod settings. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/YouAreMyPropNowWeAreHavingSoftTacosLater.dll) | +| [ThirdPerson](ThirdPerson/README.md) | Original repo: https://github.com/oestradiol/CVR-Mods | No Download | +| [Tinyboard](Tinyboard/README.md) | Makes the keyboard small and smart. | No Download | +| [YouAreMyPropNowWeAreHavingSoftTacosLater](YouAreMyPropNowWeAreHavingSoftTacosLater/README.md) | Lets you bring held, attached, and occupied props through world loads. This is configurable in the mod settings. | No Download | ### Experimental Mods @@ -33,9 +34,9 @@ | [ByeByePerformanceThankYouAMD](.Experimental/ByeByePerformanceThankYouAMD/README.md) | Fixes search terms that use spaces. | No Download | | [CVRLuaToolsExtension](.Experimental/CVRLuaToolsExtension/README.md) | Extension mod for [CVRLuaTools](https://github.com/NotAKidoS/CVRLuaTools) Hot Reload functionality. | No Download | | [CustomRichPresence](.Experimental/CustomRichPresence/README.md) | Lets you customize the Steam & Discord rich presence messages & values. | No Download | -| [LuaNetworkVariables](.Experimental/LuaNetworkVariables/README.md) | Adds a simple module for creating network variables & events *kinda* similar to Garry's Mod. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/LuaNetworkVariables.dll) | +| [LuaNetworkVariables](.Experimental/LuaNetworkVariables/README.md) | Adds a simple module for creating network variables & events *kinda* similar to Garry's Mod. | No Download | | [LuaTTS](.Experimental/LuaTTS/README.md) | Provides access to the built-in text-to-speech (TTS) functionality to lua scripts. Allows you to make the local player speak. | No Download | -| [OriginShift](.Experimental/OriginShift/README.md) | Experimental mod that allows world origin to be shifted to prevent floating point precision issues. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/OriginShift.dll) | +| [OriginShift](.Experimental/OriginShift/README.md) | Experimental mod that allows world origin to be shifted to prevent floating point precision issues. | No Download | | [ScriptingSpoofer](.Experimental/ScriptingSpoofer/README.md) | Prevents **local** scripts from accessing your Username or UserID by spoofing them with random values each session. | No Download |