Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3e7146c
No longer keeping hard references to Assembly's instead just purely t…
Odjit Jul 18, 2025
6e3fe65
As we now support command overloading and choosing which command to e…
Odjit Jul 19, 2025
0272bb2
Adding the ability to handle the remainder of a command input with a …
Odjit Jul 19, 2025
987255e
Command history won't store anymore duplicate sets of command/args
Odjit Sep 14, 2025
650805f
Fixing a few compile warnings
Odjit Sep 14, 2025
7a0ee8d
Command History persists across server restarts. Loaded at first com…
Odjit Sep 14, 2025
487e87f
Moved Command History code into its own static class
Odjit Sep 14, 2025
c4bbc1e
Changed to storing history based on the context's name instead of pla…
Odjit Sep 15, 2025
0939845
Realized the context wasn't being updated on the history commands whe…
Odjit Sep 15, 2025
0feff69
Start of version checking installed plugins
Odjit Sep 23, 2025
f0e425b
When admins auth do a version check and let them know what needs upda…
Odjit Oct 20, 2025
10691f0
Fixing line endings
Odjit Oct 20, 2025
93ca6a5
Adds .version command to VCF for allowing anyone to see what plugins …
Odjit Oct 21, 2025
144f8ea
* Version command admin only now
Odjit Nov 6, 2025
83970b0
Should only be checking the name not dependencies for the package for…
Odjit Jan 13, 2026
623419f
Deciding to remove Thunderstore Version checking of Plugins and only …
Odjit Mar 25, 2026
628c858
Fixing _remainder to work properly with command groups and commands t…
Odjit Mar 25, 2026
c603235
SImplifying VersionChecker removing unnecessary code that we don't ha…
Odjit Mar 25, 2026
26e008b
Fixing line endings
Odjit Mar 25, 2026
ccd6b0c
Remove Thunderstore version checking documentation and fix .version d…
Odjit Mar 25, 2026
414c020
Fix command history replay using stale context instead of current con…
Odjit Mar 25, 2026
001a98a
Remove UnityMainThreadDispatcher as it is no longer needed
Odjit Mar 25, 2026
36d0bcf
Input parsing now uses CommandRegistry.ParseInput and fixed _remainde…
Odjit Apr 11, 2026
c73976c
Moving saving command history to be async to not halt the chat proces…
Odjit Apr 12, 2026
abffa43
Changing _remainder to use a Remainder parameter which if used has to…
Odjit Apr 12, 2026
289b266
Chat messages had a potential to be processed on the client out of or…
Odjit Apr 12, 2026
f4f427c
Updated README.md with remainder parameter information
Odjit Apr 13, 2026
fca8433
💚 Unit test fix for non-determism in parallel and across platform
decaprime Apr 13, 2026
52fedb7
Command history wasn't marked as loaded if the history file was missi…
Odjit Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,14 @@ When your input could match multiple command variations, you'll see a list of op

## Universal Configuration Management
Built-in commands for managing BepInEx configurations across all plugins:
- `.config dump <plugin>` - View plugin configuration
- `.config set <plugin> <section> <key> <value>` - Modify settings
- `.config dump <plugin>` - View plugin configuration (admin only)
- `.config set <plugin> <section> <key> <value>` - Modify settings (admin only)

## Plugin Version Management
VCF includes tools to help you track and manage plugin versions on your server.

### Commands:
- `.version` - Lists all installed plugins and their current versions (admin only)


## Help
Expand Down
2 changes: 1 addition & 1 deletion VCF.Core/Basics/BepInExConfigCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public void DumpConfig(ICommandContext ctx, string pluginGuid, string section, s

ctx.SysReply($"Set {def.Key} = {convertedValue}");
}
catch (Exception e)
catch (Exception)
{
throw ctx.Error($"Can not convert {value} to {entry.SettingType}");
}
Expand Down
8 changes: 4 additions & 4 deletions VCF.Core/Basics/HelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static void HelpCommand(ICommandContext ctx, string search = null, string
// If search is specified first look for matching assembly, then matching command
if (!string.IsNullOrEmpty(search))
{
var foundAssembly = CommandRegistry.AssemblyCommandMap.FirstOrDefault(x => x.Key.GetName().Name.StartsWith(search, StringComparison.OrdinalIgnoreCase));
var foundAssembly = CommandRegistry.AssemblyCommandMap.FirstOrDefault(x => x.Key.StartsWith(search, StringComparison.OrdinalIgnoreCase));
if (foundAssembly.Value != null)
{
StringBuilder sb = new();
Expand Down Expand Up @@ -67,7 +67,7 @@ public static void HelpCommand(ICommandContext ctx, string search = null, string
sb.AppendLine($"Use {B(".help <plugin>").Color(Color.Gold)} for commands in that plugin");
// List all plugins they have a command they can execute for
foreach (var assemblyName in CommandRegistry.AssemblyCommandMap.Where(x => x.Value.Keys.Any(c => CommandRegistry.CanCommandExecute(ctx, c)))
.Select(x => x.Key.GetName().Name)
.Select(x => x.Key)
.OrderBy(x => x))
{
sb.AppendLine($"{assemblyName.Color(Color.Lilac)}");
Expand Down Expand Up @@ -125,9 +125,9 @@ public static void HelpAllCommand(ICommandContext ctx, string filter = null)
ctx.SysPaginatedReply(sb);
}

static void PrintAssemblyHelp(ICommandContext ctx, KeyValuePair<Assembly, Dictionary<CommandMetadata, List<string>>> assembly, StringBuilder sb, string filter = null)
static void PrintAssemblyHelp(ICommandContext ctx, KeyValuePair<string, Dictionary<CommandMetadata, List<string>>> assembly, StringBuilder sb, string filter = null)
{
var name = assembly.Key.GetName().Name;
var name = assembly.Key;
name = _trailingLongDashRegex.Replace(name, "");

sb.AppendLine($"Commands from {name.Medium().Color(Color.Primary)}:".Underline());
Expand Down
16 changes: 16 additions & 0 deletions VCF.Core/Basics/VersionCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Unity.Entities;
using VampireCommandFramework.Common;

namespace VampireCommandFramework.Basics;

internal static class VersionCommands
{
[Command("version", description: "Lists all installed plugins and their versions", adminOnly: true)]
public static void VersionCommand(ICommandContext ctx)
{
// Get the user entity if this is a ChatCommandContext
var userEntity = ctx is ChatCommandContext chatCtx ? chatCtx.Event.SenderUserEntity : default;

VersionChecker.ListAllPluginVersions(userEntity);
}
}
75 changes: 36 additions & 39 deletions VCF.Core/Breadstone/ChatHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,52 @@ public static class ChatMessageSystem_Patch
{
public static void Prefix(ChatMessageSystem __instance)
{
if (__instance.__query_661171423_0 != null)
NativeArray<Entity> entities = __instance.__query_661171423_0.ToEntityArray(Allocator.Temp);
foreach (var entity in entities)
{
NativeArray<Entity> entities = __instance.__query_661171423_0.ToEntityArray(Allocator.Temp);
foreach (var entity in entities)
{
var fromData = __instance.EntityManager.GetComponentData<FromCharacter>(entity);
var userData = __instance.EntityManager.GetComponentData<User>(fromData.User);
var chatEventData = __instance.EntityManager.GetComponentData<ChatMessageEvent>(entity);
var fromData = __instance.EntityManager.GetComponentData<FromCharacter>(entity);
var userData = __instance.EntityManager.GetComponentData<User>(fromData.User);
var chatEventData = __instance.EntityManager.GetComponentData<ChatMessageEvent>(entity);

var messageText = chatEventData.MessageText.ToString();
var messageText = chatEventData.MessageText.ToString();

if (!messageText.StartsWith(".") || messageText.StartsWith("..")) continue;
if (!messageText.StartsWith(".") || messageText.StartsWith("..")) continue;

VChatEvent ev = new VChatEvent(fromData.User, fromData.Character, messageText, chatEventData.MessageType, userData);
var ctx = new ChatCommandContext(ev);
VChatEvent ev = new VChatEvent(fromData.User, fromData.Character, messageText, chatEventData.MessageType, userData);
var ctx = new ChatCommandContext(ev);

CommandResult result;
try
{
result = CommandRegistry.Handle(ctx, messageText);
}
catch (Exception e)
{
Log.Error($"Error while handling chat message {e}");
continue;
}
CommandResult result;
try
{
result = CommandRegistry.Handle(ctx, messageText);
}
catch (Exception e)
{
Log.Error($"Error while handling chat message {e}");
continue;
}

// Legacy .help pass through support
if (result == CommandResult.Success && messageText.StartsWith(".help-legacy", System.StringComparison.InvariantCulture))
{
chatEventData.MessageText = messageText.Replace("-legacy", string.Empty);
__instance.EntityManager.SetComponentData(entity, chatEventData);
continue;
}
else if (result == CommandResult.Unmatched)
{
var sb = new StringBuilder();
// Legacy .help pass through support
if (result == CommandResult.Success && messageText.StartsWith(".help-legacy", System.StringComparison.InvariantCulture))
{
chatEventData.MessageText = messageText.Replace("-legacy", string.Empty);
__instance.EntityManager.SetComponentData(entity, chatEventData);
continue;
}
else if (result == CommandResult.Unmatched)
{
var sb = new StringBuilder();

sb.AppendLine($"Command not found: {messageText.Color(Color.Command)}");
sb.AppendLine($"Command not found: {messageText.Color(Color.Command)}");

var closeMatches = CommandRegistry.FindCloseMatches(ctx, messageText).ToArray();
if (closeMatches.Length > 0)
{
sb.AppendLine($"Did you mean: {string.Join(", ", closeMatches.Select(c => c.Color(Color.Command)))}");
}
ctx.SysReply(sb.ToString());
var closeMatches = CommandRegistry.FindCloseMatches(ctx, messageText).ToArray();
if (closeMatches.Length > 0)
{
sb.AppendLine($"Did you mean: {string.Join(", ", closeMatches.Select(c => c.Color(Color.Command)))}");
}
VWorld.Server.EntityManager.DestroyEntity(entity);
ctx.SysReply(sb.ToString());
}
VWorld.Server.EntityManager.DestroyEntity(entity);
}
}
}
105 changes: 105 additions & 0 deletions VCF.Core/Common/VersionChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using BepInEx.Unity.IL2CPP;
using ProjectM;
using ProjectM.Network;
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using Unity.Entities;
using VampireCommandFramework.Breadstone;

namespace VampireCommandFramework.Common;


internal static class VersionChecker
{
public static void ListAllPluginVersions(Entity userEntity = default)
{
try
{
// Get all loaded plugins
var installedPlugins = GetInstalledPlugins();

if (installedPlugins.Count == 0)
{
LogInfoAndSendMessageToClient(userEntity, "No plugins found.");
return;
}

LogInfoAndSendMessageToClient(userEntity, $"Installed Plugins ({installedPlugins.Count}):");

// Sort plugins by name for easier reading
foreach (var plugin in installedPlugins.OrderBy(p => p.Name))
{
var pluginMessage = $"{plugin.Name.Color(Color.Command)}: {plugin.Version.Color(Color.Green)}";
var formattedMessage = $"[vcf] ".Color(Color.Primary) + pluginMessage;
SendMessageToClient(userEntity, formattedMessage);
}
}
catch (Exception ex)
{
Log.Error($"Error listing plugin versions: {ex.Message}");
}
}

static void SendMessageToClient(Entity userEntity, string message)
{
if (userEntity == default) return;

try
{
if (VWorld.Server?.EntityManager == null) return;
if (!VWorld.Server.EntityManager.Exists(userEntity)) return;
if (!VWorld.Server.EntityManager.HasComponent<User>(userEntity)) return;

var user = VWorld.Server.EntityManager.GetComponentData<User>(userEntity);
if (!user.IsConnected) return;

var msg = new FixedString512Bytes(message);
ServerChatUtils.SendSystemMessageToClient(VWorld.Server.EntityManager, user, ref msg);
}
catch (Exception ex)
{
Log.Debug($"Could not send message to client (user may have disconnected): {ex.Message}");
}
}

static void LogInfoAndSendMessageToClient(Entity userEntity, string message)
{
Log.Info(message);
SendMessageToClient(userEntity, message);
}

/// Gets information about all installed BepInEx plugins
private static List<InstalledPluginInfo> GetInstalledPlugins()
{
var plugins = new List<InstalledPluginInfo>();

foreach (var pluginKvp in IL2CPPChainloader.Instance.Plugins)
Comment thread
decaprime marked this conversation as resolved.
{
var pluginInfo = pluginKvp.Value;
if (pluginInfo?.Metadata != null)
{
plugins.Add(new InstalledPluginInfo
{
GUID = pluginInfo.Metadata.GUID,
Name = pluginInfo.Metadata.Name,
Version = pluginInfo.Metadata.Version.ToString()
});
}
}

return plugins;
}


/// Information about an installed plugin
private class InstalledPluginInfo
{
public string GUID { get; set; }
public string Name { get; set; }
public string Version { get; set; }
}


}
3 changes: 2 additions & 1 deletion VCF.Core/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace VampireCommandFramework;
internal class Plugin : BasePlugin
{
private Harmony _harmony;

public override void Load()
{
Common.Log.Instance = Log;
Expand All @@ -26,6 +26,7 @@ public override void Load()
CommandRegistry.RegisterCommandType(typeof(Basics.HelpCommands));
CommandRegistry.RegisterCommandType(typeof(Basics.BepInExConfigCommands));
CommandRegistry.RegisterCommandType(typeof(Basics.RepeatCommands));
CommandRegistry.RegisterCommandType(typeof(Basics.VersionCommands));


IL2CPPChainloader.Instance.Plugins.TryGetValue(PluginInfo.PLUGIN_GUID, out var info);
Expand Down
11 changes: 4 additions & 7 deletions VCF.Core/Registry/CacheResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,23 @@ namespace VampireCommandFramework.Registry;

internal record CacheResult
{
internal IEnumerable<CommandMetadata> Commands { get; }
internal string[] Args { get; }
internal IEnumerable<(CommandMetadata Command, string[] Args)> Commands { get; }
internal IEnumerable<CommandMetadata> PartialMatches { get; }

internal bool IsMatched => Commands != null && Commands.Any();
internal bool HasPartial => PartialMatches?.Any() ?? false;

// Constructor for multiple commands
public CacheResult(IEnumerable<CommandMetadata> commands, string[] args, IEnumerable<CommandMetadata> partialMatches)
public CacheResult(IEnumerable<(CommandMetadata Command, string[] Args)> commands, IEnumerable<CommandMetadata> partialMatches)
{
Commands = commands;
Args = args ?? Array.Empty<string>(); // Ensure Args is never null
PartialMatches = partialMatches;
}

// Constructor for single command or null
public CacheResult(CommandMetadata command, string[] args, IEnumerable<CommandMetadata> partialMatches)
public CacheResult((CommandMetadata Command, string[] Args)? command, IEnumerable<CommandMetadata> partialMatches)
{
Commands = command != null ? new[] { command } : null;
Args = args ?? Array.Empty<string>(); // Ensure Args is never null
Commands = command.HasValue ? new[] { command.Value } : null;
PartialMatches = partialMatches;
}
}
Loading
Loading