Opening the process cache's catalog writable in the editor was the wrong cut. It made catalog read-only only by convention, so a stray catalog write in play succeeded and a later flush could land play-mutated instances in Aetheria.cc. - The process cache opens the catalog read-only in every build again, so the missing-global check runs for it as it did beforedb83cd29. The writable empty-seed exemption fromab4ced0fstays for AetherDb and fixtures. - Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It opens its own short-lived cache over the catalog, writable, commits the one preset conditionally on its key and disposes it. No other cache or instance is touched. The running game sees the preset only after the catalog reloads. CultLib at a0813c6 allows this: a read-only single-file store takes no lock and holds no handle, and the writer's commit takes the exclusive sidecar <path>.lock and replaces the file atomically. - Capture refuses, writing nothing, when Aetheria.cc is missing (it never creates the catalog), when a preset of that name exists under another key (CultCache does not reject duplicate names on write), or when the key holds a preset and replace is not given. - The console refuses a command with any character outside letters, digits, spaces and hyphens instead of stripping it, so "Mk.II" can no longer become "MkII". capturepreset refuses a second argument other than replace, so an unquoted capturepreset My Ship refuses instead of saving "My". - The capture log line says the session sees the preset after a reload, and that a Studio session opened earlier overwrites captured presets on save. - Tests: capture while the process cache holds the catalog read-only leaves it unchanged; a missing catalog refuses and creates nothing; a same name under a Studio-minted key refuses, with or without replace; a game-shaped read-only open missing a global throws. The existing preset tests now commit through the catalog path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
3.7 KiB
C#
125 lines
3.7 KiB
C#
/// <summary>
|
|
/// Handles parsing and execution of console commands, as well as collecting log output.
|
|
/// </summary>
|
|
using UnityEngine;
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using UnityEngine.SceneManagement;
|
|
using Object = UnityEngine.Object;
|
|
|
|
public class ConsoleController
|
|
{
|
|
public static Component MessageReceiver;
|
|
private static ConsoleController _instance;
|
|
private static Regex _permittedCharacters = new Regex("[^a-zA-Z0-9 -]");
|
|
private static Dictionary<string, Action<string[]>> _commands = new Dictionary<string, Action<string[]>>();
|
|
|
|
private ConsoleController()
|
|
{
|
|
}
|
|
|
|
public static ConsoleController Instance
|
|
{
|
|
get => _instance ?? (_instance = new ConsoleController());
|
|
}
|
|
|
|
public static void ClearCommands()
|
|
{
|
|
_commands.Clear();
|
|
}
|
|
|
|
public static void AddCommand(string command, Action<string[]> action)
|
|
{
|
|
var lower = command.ToLowerInvariant();
|
|
if(!_commands.ContainsKey(lower))
|
|
_commands.Add(lower, action);
|
|
}
|
|
|
|
#region Event declarations
|
|
// Used to communicate with ConsoleView
|
|
public delegate void LogChangedHandler(string[] log);
|
|
public event LogChangedHandler LogChanged;
|
|
|
|
public delegate void VisibilityChangedHandler(bool visible);
|
|
public event VisibilityChangedHandler VisibilityChanged;
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// How many log lines should be retained?
|
|
/// Note that strings submitted to appendLogLine with embedded newlines will be counted as a single line.
|
|
/// </summary>
|
|
private const int ScrollbackSize = 100;
|
|
|
|
private readonly Queue<string> _scrollback = new Queue<string>(ScrollbackSize);
|
|
private readonly List<string> _commandHistory = new List<string>();
|
|
|
|
public string[] Log { get; private set; } //Copy of scrollback as an array for easier use by ConsoleView
|
|
|
|
public void AppendLogLine(string line) {
|
|
if (_scrollback.Count >= ScrollbackSize) {
|
|
_scrollback.Dequeue();
|
|
}
|
|
_scrollback.Enqueue(line);
|
|
|
|
Log = _scrollback.ToArray();
|
|
LogChanged?.Invoke(Log);
|
|
}
|
|
|
|
public void RunCommandString(string commandString) {
|
|
AppendLogLine($"$ {commandString}");
|
|
|
|
string[] commandSplit = ParseArguments(commandString);
|
|
string[] args = {"",""};
|
|
if (commandSplit == null) {
|
|
AppendLogLine("Refused: commands take only letters, digits, spaces and hyphens");
|
|
return;
|
|
}
|
|
if (commandSplit.Length < 1) {
|
|
AppendLogLine($"Unable to process command '{commandString}'");
|
|
return;
|
|
|
|
}
|
|
if (commandSplit.Length >= 2) {
|
|
int numArgs = commandSplit.Length - 1;
|
|
args = new string[numArgs];
|
|
Array.Copy(commandSplit, 1, args, 0, numArgs);
|
|
}
|
|
|
|
var cmd = commandSplit[0].ToLower();
|
|
if(_commands.ContainsKey(cmd))
|
|
_commands[cmd].Invoke(args);
|
|
else AppendLogLine("Invalid command!");
|
|
//MessageReceiver.SendMessage(commandSplit[0].ToLower(), args, SendMessageOptions.DontRequireReceiver);
|
|
_commandHistory.Add(commandString);
|
|
}
|
|
|
|
static string[] ParseArguments(string commandString)
|
|
{
|
|
var parmChars = new LinkedList<char>(commandString.ToCharArray());
|
|
var inQuote = false;
|
|
var node = parmChars.First;
|
|
while (node != null)
|
|
{
|
|
var next = node.Next;
|
|
if (node.Value == '"') {
|
|
inQuote = !inQuote;
|
|
parmChars.Remove(node);
|
|
}
|
|
if (!inQuote && node.Value == ' ') {
|
|
node.Value = '\n';
|
|
}
|
|
node = next;
|
|
}
|
|
var parmCharsArr = new char[parmChars.Count];
|
|
parmChars.CopyTo(parmCharsArr, 0);
|
|
var args = new string(parmCharsArr).Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries);
|
|
// An argument with any other character is refused (null), never silently stripped into a different one.
|
|
return args.Any(arg => _permittedCharacters.IsMatch(arg)) ? null : args;
|
|
}
|
|
}
|