Compare commits

...

2 commits

Author SHA1 Message Date
github-actions[bot]
2b8f17b287 [NAK_CVR_Mods] Update mod list in README
Some checks are pending
Update Mod List / update-modlist (push) Waiting to run
2026-04-15 23:45:24 +00:00
NotAKid
c13dc8375a [ChatBoxHud] Initial release 2026-04-15 18:44:53 -05:00
6 changed files with 533 additions and 19 deletions

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>ChatBoxHud</RootNamespace>
</PropertyGroup>
</Project>

438
ChatBoxHud/Main.cs Normal file
View file

@ -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<HudTarget> EntryHudMode =
Cat.CreateEntry("hud_mode", HudTarget.LookAtThenNearest, "HUD Target Mode");
internal static readonly MelonPreferences_Entry<SourceFilter> EntryHudFilter =
Cat.CreateEntry("hud_filter", SourceFilter.Everyone, "Chat Filter",
description: "Whose chat messages appear on the HUD.");
internal static readonly MelonPreferences_Entry<MaxUsersOnHud> 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<bool> EntryUsePlayerColors =
Cat.CreateEntry("player_colors", true, "Use Player Colors",
description: "Use player-chosen colors for names instead of friend colors.");
private static readonly Dictionary<string, ChatBuf> 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<string> _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<Candidate> _candidates = new();
private static readonly Dictionary<string, Candidate> _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<Msg> 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<PortalHudIndicator>();
}
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<string, ChatBuf> 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("<b>\u25C4 </b>");
sb.Append("<alpha=#FF><b><color=").Append(hex).Append('>')
.Append(Esc(playerName)).Append("</color></b>");
AppendAlpha(sb, ra); sb.Append("<b> \u25BA</b><alpha=#FF>\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("<alpha=#").Append(a.ToString("X2")).Append('>')
.Append("<size=").Append(m.Size).Append("%>")
.Append(m.Lines[off + j])
.Append("</size>\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("<alpha=#").Append(((byte)(a01 * 255)).ToString("X2")).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<string> result = new List<string>(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");
}

View file

@ -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";
}

14
ChatBoxHud/README.md Normal file
View file

@ -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.

23
ChatBoxHud/format.json Normal file
View file

@ -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"
}

View file

@ -6,25 +6,26 @@
| Name | Description | Download | | 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 | | [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) | | [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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/DesktopInteractions.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. | 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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/DoubleTapJumpToExitSeat.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. | No Download |
| [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) | | [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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/FuckToes.dll) | | [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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PlapPlapForAll.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. | No Download |
| [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) | | [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. | 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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RCCVirtualSteeringWheel.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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RelativeSyncJitterFix.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. | 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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ShareBubbles.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. | 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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ShowPlayerInSelfMirror.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. | No Download |
| [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) | | [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 | | [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) | | [ThirdPerson](ThirdPerson/README.md) | Original repo: https://github.com/oestradiol/CVR-Mods | No Download |
| [Tinyboard](Tinyboard/README.md) | Makes the keyboard small and smart. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/Tinyboard.dll) | | [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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/YouAreMyPropNowWeAreHavingSoftTacosLater.dll) | | [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 ### Experimental Mods
@ -33,9 +34,9 @@
| [ByeByePerformanceThankYouAMD](.Experimental/ByeByePerformanceThankYouAMD/README.md) | Fixes search terms that use spaces. | No Download | | [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 | | [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 | | [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 | | [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 | | [ScriptingSpoofer](.Experimental/ScriptingSpoofer/README.md) | Prevents **local** scripts from accessing your Username or UserID by spoofing them with random values each session. | No Download |
<!-- END MOD LIST --> <!-- END MOD LIST -->