Initial PlayerList mod, hold TAB to show connected players
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
dist/
|
||||||
|
*.zip
|
||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using RedLoader;
|
||||||
|
using RedLoader.Utils;
|
||||||
|
using SonsSdk;
|
||||||
|
using SonsSdk.Attributes;
|
||||||
|
using TheForest.Utils;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PlayerList;
|
||||||
|
|
||||||
|
public class PlayerList : SonsMod
|
||||||
|
{
|
||||||
|
internal static KeyCode ListKey = KeyCode.Tab;
|
||||||
|
|
||||||
|
private static string _configPath;
|
||||||
|
private static readonly List<string> Names = new();
|
||||||
|
private static bool _showing;
|
||||||
|
private static float _nextRefresh;
|
||||||
|
private static GUIStyle _titleStyle;
|
||||||
|
private static GUIStyle _rowStyle;
|
||||||
|
private static GUIStyle _boxStyle;
|
||||||
|
private static Texture2D _bg;
|
||||||
|
|
||||||
|
public PlayerList()
|
||||||
|
{
|
||||||
|
OnUpdateCallback = OnUpdate;
|
||||||
|
OnGUICallback = OnGui;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSdkInitialized()
|
||||||
|
{
|
||||||
|
_configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "PlayerList.txt");
|
||||||
|
Load();
|
||||||
|
Save();
|
||||||
|
RLog.Msg($"PlayerList 1.0.1 loaded. Hold {ListKey} to show connected players. Console: playerlist");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InMultiplayer => BoltNetwork.isRunning && LocalPlayer.Entity != null;
|
||||||
|
|
||||||
|
private void OnUpdate()
|
||||||
|
{
|
||||||
|
_showing = InMultiplayer && Input.GetKey(ListKey);
|
||||||
|
if (!_showing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Time.unscaledTime >= _nextRefresh)
|
||||||
|
{
|
||||||
|
_nextRefresh = Time.unscaledTime + 0.5f;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Refresh()
|
||||||
|
{
|
||||||
|
Names.Clear();
|
||||||
|
var seen = new HashSet<IntPtr>();
|
||||||
|
|
||||||
|
var local = LocalPlayer.Entity;
|
||||||
|
if (local != null)
|
||||||
|
{
|
||||||
|
seen.Add(local.Pointer);
|
||||||
|
Names.Add((GetName(local) ?? "You") + " (you)");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracker = PlayerTracker.Instance;
|
||||||
|
if (tracker == null || tracker.AllPlayerEntities == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var others = new List<string>();
|
||||||
|
foreach (var entity in tracker.AllPlayerEntities)
|
||||||
|
{
|
||||||
|
if (entity == null || !seen.Add(entity.Pointer))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var name = GetName(entity);
|
||||||
|
if (name != null)
|
||||||
|
others.Add(name);
|
||||||
|
}
|
||||||
|
others.Sort(StringComparer.OrdinalIgnoreCase);
|
||||||
|
Names.AddRange(others);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetName(BoltEntity entity)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (entity.TryFindState<IPlayerState>(out var state) && !string.IsNullOrWhiteSpace(state.name) && !state.name.Equals("Unknown", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return state.name;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"PlayerList: could not read player name: {e.Message}");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureStyles()
|
||||||
|
{
|
||||||
|
if (_boxStyle != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_bg = new Texture2D(1, 1);
|
||||||
|
_bg.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.75f));
|
||||||
|
_bg.Apply();
|
||||||
|
_bg.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
|
||||||
|
_boxStyle = new GUIStyle(GUI.skin.box);
|
||||||
|
_boxStyle.normal.background = _bg;
|
||||||
|
|
||||||
|
_titleStyle = new GUIStyle(GUI.skin.label)
|
||||||
|
{
|
||||||
|
fontSize = 22,
|
||||||
|
fontStyle = FontStyle.Bold,
|
||||||
|
alignment = TextAnchor.MiddleCenter
|
||||||
|
};
|
||||||
|
_titleStyle.normal.textColor = Color.white;
|
||||||
|
|
||||||
|
_rowStyle = new GUIStyle(GUI.skin.label)
|
||||||
|
{
|
||||||
|
fontSize = 18,
|
||||||
|
alignment = TextAnchor.MiddleLeft
|
||||||
|
};
|
||||||
|
_rowStyle.normal.textColor = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGui()
|
||||||
|
{
|
||||||
|
if (!_showing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
EnsureStyles();
|
||||||
|
|
||||||
|
const float width = 420f;
|
||||||
|
const float rowHeight = 28f;
|
||||||
|
const float titleHeight = 40f;
|
||||||
|
const float pad = 12f;
|
||||||
|
|
||||||
|
var height = titleHeight + Names.Count * rowHeight + pad * 2;
|
||||||
|
var x = (Screen.width - width) / 2f;
|
||||||
|
var y = Screen.height * 0.15f;
|
||||||
|
|
||||||
|
GUI.Box(new Rect(x, y, width, height), GUIContent.none, _boxStyle);
|
||||||
|
GUI.Label(new Rect(x, y + pad, width, titleHeight), $"Players ({Names.Count})", _titleStyle);
|
||||||
|
|
||||||
|
for (var i = 0; i < Names.Count; i++)
|
||||||
|
{
|
||||||
|
var rowY = y + pad + titleHeight + i * rowHeight;
|
||||||
|
GUI.Label(new Rect(x + pad * 2, rowY, width - pad * 4, rowHeight), Names[i], _rowStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebugCommand("playerlist")]
|
||||||
|
private void PlayerListCommand(string args)
|
||||||
|
{
|
||||||
|
var parts = (args ?? "").Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (parts.Length == 2 && parts[0].Equals("key", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (Enum.TryParse(parts[1], true, out KeyCode key))
|
||||||
|
{
|
||||||
|
ListKey = key;
|
||||||
|
Save();
|
||||||
|
SonsTools.ShowMessage($"PlayerList key: {ListKey}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SonsTools.ShowMessage($"Unknown key: {parts[1]}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!InMultiplayer)
|
||||||
|
{
|
||||||
|
SonsTools.ShowMessage("Not in a multiplayer game");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Refresh();
|
||||||
|
DumpEntities();
|
||||||
|
RLog.Msg($"PlayerList: {Names.Count} player(s)");
|
||||||
|
foreach (var name in Names)
|
||||||
|
RLog.Msg($" {name}");
|
||||||
|
SonsTools.ShowMessage($"Players ({Names.Count}): {string.Join(", ", Names)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpEntities()
|
||||||
|
{
|
||||||
|
var tracker = PlayerTracker.Instance;
|
||||||
|
if (tracker == null || tracker.AllPlayerEntities == null)
|
||||||
|
{
|
||||||
|
RLog.Msg("PlayerList: no PlayerTracker");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RLog.Msg($"PlayerList: {tracker.AllPlayerEntities.Count} tracked entities, local {LocalPlayer.Entity?.Pointer}");
|
||||||
|
foreach (var entity in tracker.AllPlayerEntities)
|
||||||
|
{
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
RLog.Msg(" null entity");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw = entity.TryFindState<IPlayerState>(out var state) ? state.name : "(no state)";
|
||||||
|
RLog.Msg($" ptr={entity.Pointer} go={entity.gameObject.name} name='{raw}' owner={entity.isOwner} attached={entity.isAttached} source={(entity.source == null ? "none" : entity.source.ToString())}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Load()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_configPath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var line in File.ReadAllLines(_configPath))
|
||||||
|
{
|
||||||
|
var kv = line.Split('=', 2);
|
||||||
|
if (kv.Length != 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var k = kv[0].Trim().ToLowerInvariant();
|
||||||
|
var v = kv[1].Trim();
|
||||||
|
|
||||||
|
if (k == "key" && Enum.TryParse(v, true, out KeyCode key))
|
||||||
|
ListKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(_configPath, $"key={ListKey}\n");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"PlayerList: could not save config: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<AssemblyName>PlayerList</AssemblyName>
|
||||||
|
<RootNamespace>PlayerList</RootNamespace>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(GameDir)' == ''">
|
||||||
|
<GameDir>$(SOTF_PATH)</GameDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CheckGameDir" BeforeTargets="BeforeBuild">
|
||||||
|
<Error Condition="'$(GameDir)' == ''"
|
||||||
|
Text="GameDir is not set. Run build.ps1, pass -p:GameDir="path", or set SOTF_PATH." />
|
||||||
|
<Error Condition="!Exists('$(GameDir)\_RedLoader\Game\Sons.dll')"
|
||||||
|
Text="No RedLoader interop assemblies at $(GameDir)\_RedLoader\Game. Install RedLoader and launch the game once." />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="$(GameDir)\_RedLoader\Game\*.dll" Private="false" />
|
||||||
|
<Reference Include="$(GameDir)\_RedLoader\net6\RedLoader.dll" Private="false" />
|
||||||
|
<Reference Include="$(GameDir)\_RedLoader\net6\SonsSdk.dll" Private="false" />
|
||||||
|
<Reference Include="$(GameDir)\_RedLoader\net6\Il2CppInterop.Runtime.dll" Private="false" />
|
||||||
|
<Reference Include="$(GameDir)\_RedLoader\net6\Il2CppInterop.Common.dll" Private="false" />
|
||||||
|
<Reference Include="$(GameDir)\_RedLoader\net6\0Harmony.dll" Private="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="manifest.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$GameDir,
|
||||||
|
[switch]$Install,
|
||||||
|
[switch]$Package
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Test-Dotnet {
|
||||||
|
$cmd = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||||
|
if (-not $cmd) { return $false }
|
||||||
|
$sdks = & dotnet --list-sdks 2>$null
|
||||||
|
return ($LASTEXITCODE -eq 0 -and $sdks)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-Path {
|
||||||
|
$env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' +
|
||||||
|
[System.Environment]::GetEnvironmentVariable('Path','User')
|
||||||
|
$dotnetRoot = Join-Path $env:ProgramFiles 'dotnet'
|
||||||
|
if ((Test-Path $dotnetRoot) -and ($env:Path -notlike "*$dotnetRoot*")) {
|
||||||
|
$env:Path += ";$dotnetRoot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-Dotnet {
|
||||||
|
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
||||||
|
Write-Host 'Installing the .NET 8 SDK with winget' -ForegroundColor Cyan
|
||||||
|
winget install --id Microsoft.DotNet.SDK.8 --accept-source-agreements --accept-package-agreements --silent
|
||||||
|
Update-Path
|
||||||
|
if (Test-Dotnet) { return }
|
||||||
|
Write-Host ' winget install did not make dotnet available, falling back' -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Downloading the official dotnet-install script' -ForegroundColor Cyan
|
||||||
|
$script = Join-Path $env:TEMP 'dotnet-install.ps1'
|
||||||
|
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $script -UseBasicParsing
|
||||||
|
|
||||||
|
Write-Host 'Installing the .NET 8 SDK to your user profile' -ForegroundColor Cyan
|
||||||
|
& $script -Channel 8.0 -InstallDir "$env:LOCALAPPDATA\Microsoft\dotnet"
|
||||||
|
$userDotnet = "$env:LOCALAPPDATA\Microsoft\dotnet"
|
||||||
|
$env:Path = "$userDotnet;$env:Path"
|
||||||
|
[Environment]::SetEnvironmentVariable(
|
||||||
|
'Path',
|
||||||
|
"$userDotnet;" + [Environment]::GetEnvironmentVariable('Path','User'),
|
||||||
|
'User')
|
||||||
|
Remove-Item $script -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-RedLoader {
|
||||||
|
param([string]$Target)
|
||||||
|
|
||||||
|
Write-Host 'Looking up the latest RedLoader release' -ForegroundColor Cyan
|
||||||
|
$release = Invoke-RestMethod 'https://api.github.com/repos/ToniMacaroni/RedLoader/releases/latest' -Headers @{ 'User-Agent' = 'ps' }
|
||||||
|
$asset = $release.assets | Where-Object { $_.name -match '^RedLoader.*\.zip$' } | Select-Object -First 1
|
||||||
|
if (-not $asset) { throw 'Could not find RedLoader.zip in the latest release' }
|
||||||
|
Write-Host " $($release.tag_name) / $($asset.name)"
|
||||||
|
|
||||||
|
$zip = Join-Path $env:TEMP $asset.name
|
||||||
|
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zip -UseBasicParsing
|
||||||
|
|
||||||
|
$stage = Join-Path $env:TEMP 'redloader-extract'
|
||||||
|
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
|
||||||
|
Expand-Archive -Path $zip -DestinationPath $stage -Force
|
||||||
|
Copy-Item -Path (Join-Path $stage '*') -Destination $Target -Recurse -Force
|
||||||
|
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Write-Host ' RedLoader installed' -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-GameDir {
|
||||||
|
if ($env:SOTF_PATH) { return $env:SOTF_PATH }
|
||||||
|
$steam = (Get-ItemProperty 'HKCU:\Software\Valve\Steam' -ErrorAction SilentlyContinue).SteamPath
|
||||||
|
if (-not $steam) { return $null }
|
||||||
|
$libs = @($steam)
|
||||||
|
$vdf = Join-Path $steam 'steamapps\libraryfolders.vdf'
|
||||||
|
if (Test-Path $vdf) {
|
||||||
|
Select-String -Path $vdf -Pattern '"path"\s+"(.+?)"' -AllMatches |
|
||||||
|
ForEach-Object { $_.Matches } |
|
||||||
|
ForEach-Object { $libs += $_.Groups[1].Value.Replace('\\','\') }
|
||||||
|
}
|
||||||
|
foreach ($lib in $libs) {
|
||||||
|
$candidate = Join-Path $lib 'steamapps\common\Sons Of The Forest'
|
||||||
|
if (Test-Path (Join-Path $candidate 'SonsOfTheForest.exe')) { return $candidate }
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Dotnet)) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'The .NET 8 SDK is required to build this mod and was not found.' -ForegroundColor Yellow
|
||||||
|
$answer = Read-Host 'Download and install it now? (y/N)'
|
||||||
|
if ($answer -ne 'y') {
|
||||||
|
throw 'Cannot build without the .NET SDK. Get it at https://dotnet.microsoft.com/download'
|
||||||
|
}
|
||||||
|
Install-Dotnet
|
||||||
|
if (-not (Test-Dotnet)) {
|
||||||
|
throw 'The SDK was installed but dotnet is still not on PATH. Reopen PowerShell and rerun this script.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ("dotnet SDK: " + (dotnet --version)) -ForegroundColor Green
|
||||||
|
|
||||||
|
if (-not $GameDir) { $GameDir = Find-GameDir }
|
||||||
|
if (-not $GameDir) {
|
||||||
|
throw 'Could not find Sons Of The Forest. Pass -GameDir "path" or set SOTF_PATH.'
|
||||||
|
}
|
||||||
|
Write-Host "Game directory: $GameDir" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $GameDir '_RedLoader\net6\SonsSdk.dll'))) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'RedLoader is not installed in your game folder.' -ForegroundColor Yellow
|
||||||
|
$answer = Read-Host 'Download and install the latest RedLoader release now? (y/N)'
|
||||||
|
if ($answer -ne 'y') {
|
||||||
|
throw 'RedLoader is required. Get it at https://github.com/ToniMacaroni/RedLoader/releases'
|
||||||
|
}
|
||||||
|
Install-RedLoader -Target $GameDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $GameDir '_RedLoader\Game\Sons.dll'))) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'RedLoader is installed but has not generated its game assemblies yet.' -ForegroundColor Yellow
|
||||||
|
Write-Host 'Launch Sons of the Forest once and wait for the main menu. The first'
|
||||||
|
Write-Host 'launch takes a few minutes while it processes the game files.'
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Then quit the game and run this script again.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Write-Host 'RedLoader ready' -ForegroundColor Green
|
||||||
|
|
||||||
|
dotnet build -c Release --nologo -p:GameDir="$GameDir"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Build failed' }
|
||||||
|
|
||||||
|
$dll = Join-Path $PSScriptRoot 'bin\Release\net6\PlayerList.dll'
|
||||||
|
|
||||||
|
if ($Install) {
|
||||||
|
$modsDir = Join-Path $GameDir 'Mods'
|
||||||
|
$modDataDir = Join-Path $modsDir 'PlayerList'
|
||||||
|
New-Item -ItemType Directory -Force -Path $modDataDir | Out-Null
|
||||||
|
Copy-Item $dll -Destination $modsDir -Force
|
||||||
|
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination $modDataDir -Force
|
||||||
|
Write-Host "Installed to $modsDir" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Package) {
|
||||||
|
$stage = Join-Path $PSScriptRoot 'dist'
|
||||||
|
$zip = Join-Path $PSScriptRoot 'PlayerList.zip'
|
||||||
|
|
||||||
|
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'PlayerList') | Out-Null
|
||||||
|
Copy-Item $dll -Destination $stage -Force
|
||||||
|
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination (Join-Path $stage 'PlayerList') -Force
|
||||||
|
|
||||||
|
if (Test-Path $zip) { Remove-Item $zip -Force }
|
||||||
|
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zip
|
||||||
|
Remove-Item $stage -Recurse -Force
|
||||||
|
|
||||||
|
Write-Host "Packaged $zip" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/ToniMacaroni/RedLoader/main/MetadataSchema.json",
|
||||||
|
"id": "PlayerList",
|
||||||
|
"name": "PlayerList",
|
||||||
|
"author": "JustinOros",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Hold TAB to see the players connected to the server",
|
||||||
|
"gameVersion": "1.0.0",
|
||||||
|
"platform": "Client",
|
||||||
|
"type": "Mod"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user