mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d41958853 | ||
|
|
cecc4dfcf2 | ||
|
|
4ac40f2e90 | ||
|
|
3e12d44173 | ||
|
|
22affccf24 | ||
|
|
028724c47b | ||
|
|
0114bff2fc | ||
|
|
4ddbd644eb | ||
|
|
f0366531ef |
20
Robust.Client/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
20
Robust.Client/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
internal sealed class ReloadLocalizationsCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "rldloc";
|
||||
public string Description => "Reloads localization (client & server)";
|
||||
public string Help => "Usage: rldloc";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
IoCManager.Resolve<ILocalizationManager>().ReloadLocalizations();
|
||||
|
||||
shell.RemoteExecuteCommand("sudo rldloc");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#if CLIENT_SCRIPTING
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.CodeAnalysis;
|
||||
@@ -36,6 +38,8 @@ namespace Robust.Client.Console
|
||||
private readonly ScriptGlobals _globals;
|
||||
private ScriptState? _state;
|
||||
|
||||
private (string[] imports, string code)? _autoImportRepeatBuffer;
|
||||
|
||||
public ScriptConsoleClient()
|
||||
{
|
||||
Title = Loc.GetString("Robust C# Interactive (CLIENT)");
|
||||
@@ -54,38 +58,56 @@ namespace Robust.Client.Console
|
||||
var code = InputBar.Text;
|
||||
InputBar.Clear();
|
||||
|
||||
// Remove > or . at the end of the output panel.
|
||||
OutputPanel.RemoveEntry(^1);
|
||||
|
||||
_inputBuffer.AppendLine(code);
|
||||
_linesEntered += 1;
|
||||
|
||||
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(_inputBuffer.ToString()), ScriptInstanceShared.ParseOptions);
|
||||
|
||||
if (!SyntaxFactory.IsCompleteSubmission(tree))
|
||||
if (_autoImportRepeatBuffer.HasValue && code == "y")
|
||||
{
|
||||
if (_linesEntered == 1)
|
||||
var (imports, repeatCode) = _autoImportRepeatBuffer.Value;
|
||||
var sb = new StringBuilder();
|
||||
foreach (var import in imports)
|
||||
{
|
||||
OutputPanel.AddText($"> {code}");
|
||||
sb.AppendFormat("using {0};\n", import);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputPanel.AddText($". {code}");
|
||||
}
|
||||
OutputPanel.AddText(".");
|
||||
return;
|
||||
|
||||
sb.Append(repeatCode);
|
||||
|
||||
code = sb.ToString();
|
||||
}
|
||||
|
||||
code = _inputBuffer.ToString().Trim();
|
||||
|
||||
// Remove echo of partial submission from the output panel.
|
||||
for (var i = 1; i < _linesEntered; i++)
|
||||
else
|
||||
{
|
||||
// Remove > or . at the end of the output panel.
|
||||
OutputPanel.RemoveEntry(^1);
|
||||
}
|
||||
|
||||
_inputBuffer.Clear();
|
||||
_linesEntered = 0;
|
||||
_inputBuffer.AppendLine(code);
|
||||
_linesEntered += 1;
|
||||
|
||||
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(_inputBuffer.ToString()),
|
||||
ScriptInstanceShared.ParseOptions);
|
||||
|
||||
if (!SyntaxFactory.IsCompleteSubmission(tree))
|
||||
{
|
||||
if (_linesEntered == 1)
|
||||
{
|
||||
OutputPanel.AddText($"> {code}");
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputPanel.AddText($". {code}");
|
||||
}
|
||||
|
||||
OutputPanel.AddText(".");
|
||||
return;
|
||||
}
|
||||
|
||||
code = _inputBuffer.ToString().Trim();
|
||||
|
||||
// Remove echo of partial submission from the output panel.
|
||||
for (var i = 1; i < _linesEntered; i++)
|
||||
{
|
||||
OutputPanel.RemoveEntry(^1);
|
||||
}
|
||||
|
||||
_inputBuffer.Clear();
|
||||
_linesEntered = 0;
|
||||
}
|
||||
|
||||
Script newScript;
|
||||
|
||||
@@ -135,6 +157,8 @@ namespace Robust.Client.Console
|
||||
|
||||
OutputPanel.AddMessage(msg);
|
||||
OutputPanel.AddText(">");
|
||||
|
||||
PromptAutoImports(e.Diagnostics, code);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,6 +179,17 @@ namespace Robust.Client.Console
|
||||
OutputPanel.AddText(">");
|
||||
}
|
||||
|
||||
private void PromptAutoImports(IEnumerable<Diagnostic> diags, string code)
|
||||
{
|
||||
if (!ScriptInstanceShared.CalcAutoImports(_reflectionManager, diags, out var found))
|
||||
return;
|
||||
|
||||
OutputPanel.AddText($"Auto-import {string.Join(", ", found)} (enter 'y')?");
|
||||
|
||||
_autoImportRepeatBuffer = (found.ToArray(), code);
|
||||
}
|
||||
|
||||
|
||||
private sealed class ScriptGlobalsImpl : ScriptGlobals
|
||||
{
|
||||
private readonly ScriptConsoleClient _owner;
|
||||
|
||||
@@ -318,7 +318,7 @@ namespace Robust.Client
|
||||
logManager.GetSawmill("discord").Level = LogLevel.Warning;
|
||||
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
|
||||
logManager.GetSawmill("szr").Level = LogLevel.Info;
|
||||
logManager.GetSawmill("Loc").Level = LogLevel.Error;
|
||||
logManager.GetSawmill("loc").Level = LogLevel.Error;
|
||||
|
||||
#if DEBUG_ONLY_FCE_INFO
|
||||
#if DEBUG_ONLY_FCE_LOG
|
||||
|
||||
@@ -945,6 +945,31 @@ namespace Robust.Client.GameObjects
|
||||
LayerSetAutoAnimated(layer, autoAnimated);
|
||||
}
|
||||
|
||||
public void LayerSetOffset(int layer, Vector2 layerOffset)
|
||||
{
|
||||
if (Layers.Count <= layer)
|
||||
{
|
||||
Logger.ErrorS(LogCategory,
|
||||
"Layer with index '{0}' does not exist, cannot set offset! Trace:\n{1}",
|
||||
layer, Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
Layers[layer].SetOffset(layerOffset);
|
||||
}
|
||||
|
||||
public void LayerSetOffset(object layerKey, Vector2 layerOffset)
|
||||
{
|
||||
if (!LayerMapTryGet(layerKey, out var layer))
|
||||
{
|
||||
Logger.ErrorS(LogCategory, "Layer with key '{0}' does not exist, cannot set offset! Trace:\n{1}",
|
||||
layerKey, Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
LayerSetOffset(layer, layerOffset);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RSI.StateId LayerGetState(int layer)
|
||||
{
|
||||
@@ -1057,7 +1082,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
var layerColor = color * layer.Color;
|
||||
|
||||
var position = -(Vector2) texture.Size / (2f * EyeManager.PixelsPerMeter);
|
||||
var position = -(Vector2) texture.Size / (2f * EyeManager.PixelsPerMeter) + layer.Offset;
|
||||
var textureSize = texture.Size / (float) EyeManager.PixelsPerMeter;
|
||||
var quad = Box2.FromDimensions(position, textureSize);
|
||||
|
||||
@@ -1140,7 +1165,7 @@ namespace Robust.Client.GameObjects
|
||||
serializer.DataFieldCached(ref color, "color", Color.White);
|
||||
serializer.DataFieldCached(ref _visible, "visible", true);
|
||||
serializer.DataFieldCached(ref _directional, "directional", true); //TODO: Kill ME
|
||||
serializer.DataFieldCached(ref _screenLock, "noRot", false);
|
||||
serializer.DataFieldCached(ref _screenLock, "noRot", true);
|
||||
serializer.DataFieldCached(ref _enableOverrideDirection, "enableOverrideDir", false);
|
||||
serializer.DataFieldCached(ref _overrideDirection, "overrideDir", Direction.East);
|
||||
|
||||
@@ -1620,16 +1645,25 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 Scale { get; set; } = Vector2.One;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Angle Rotation { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Visible = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Color Color { get; set; } = Color.White;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool AutoAnimated = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 Offset { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public DirectionOffset DirOffset { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public RSI? ActualRsi => RSI ?? _parent.BaseRSI;
|
||||
|
||||
@@ -1850,6 +1884,11 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
_parent.UpdateIsInert();
|
||||
}
|
||||
|
||||
public void SetOffset(Vector2 offset)
|
||||
{
|
||||
Offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
void IAnimationProperties.SetAnimatableProperty(string name, object value)
|
||||
|
||||
18
Robust.Server/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
18
Robust.Server/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
internal sealed class ReloadLocalizationsCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "rldloc";
|
||||
public string Description => "Reloads localization (client & server)";
|
||||
public string Help => "Usage: rldloc";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
IoCManager.Resolve<ILocalizationManager>().ReloadLocalizations();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ namespace Robust.Server
|
||||
mgr.RootSawmill.AddHandler(handler);
|
||||
mgr.GetSawmill("res.typecheck").Level = LogLevel.Info;
|
||||
mgr.GetSawmill("go.sys").Level = LogLevel.Info;
|
||||
mgr.GetSawmill("Loc").Level = LogLevel.Error;
|
||||
mgr.GetSawmill("loc").Level = LogLevel.Error;
|
||||
// mgr.GetSawmill("szr").Level = LogLevel.Info;
|
||||
|
||||
#if DEBUG_ONLY_FCE_INFO
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.CodeAnalysis;
|
||||
@@ -76,7 +77,7 @@ namespace Robust.Server.Scripting
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_conGroupController.CanViewVar(session))
|
||||
if (!_conGroupController.CanScript(session))
|
||||
{
|
||||
Logger.WarningS("script", "Client {0} tried to access Scripting without permissions.", session);
|
||||
_netManager.ServerSendMessage(reply, message.MsgChannel);
|
||||
@@ -108,7 +109,7 @@ namespace Robust.Server.Scripting
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_conGroupController.CanViewVar(session))
|
||||
if (!_conGroupController.CanScript(session))
|
||||
{
|
||||
Logger.WarningS("script", "Client {0} tried to access Scripting without permissions.", session);
|
||||
return;
|
||||
@@ -125,23 +126,40 @@ namespace Robust.Server.Scripting
|
||||
|
||||
var code = message.Code;
|
||||
|
||||
instance.InputBuffer.AppendLine(code);
|
||||
|
||||
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(instance.InputBuffer.ToString()),
|
||||
ScriptInstanceShared.ParseOptions);
|
||||
|
||||
if (!SyntaxFactory.IsCompleteSubmission(tree))
|
||||
if (code == "y" && instance.AutoImportRepeatBuffer.HasValue)
|
||||
{
|
||||
replyMessage.WasComplete = false;
|
||||
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
|
||||
return;
|
||||
var (imports, repeatCode) = instance.AutoImportRepeatBuffer.Value;
|
||||
var sb = new StringBuilder();
|
||||
foreach (var import in imports)
|
||||
{
|
||||
sb.AppendFormat("using {0};\n", import);
|
||||
}
|
||||
|
||||
sb.Append(repeatCode);
|
||||
|
||||
code = sb.ToString();
|
||||
replyMessage.WasComplete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
instance.InputBuffer.AppendLine(code);
|
||||
|
||||
replyMessage.WasComplete = true;
|
||||
var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(instance.InputBuffer.ToString()),
|
||||
ScriptInstanceShared.ParseOptions);
|
||||
|
||||
code = instance.InputBuffer.ToString().Trim();
|
||||
if (!SyntaxFactory.IsCompleteSubmission(tree))
|
||||
{
|
||||
replyMessage.WasComplete = false;
|
||||
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
|
||||
return;
|
||||
}
|
||||
|
||||
instance.InputBuffer.Clear();
|
||||
replyMessage.WasComplete = true;
|
||||
|
||||
code = instance.InputBuffer.ToString().Trim();
|
||||
|
||||
instance.InputBuffer.Clear();
|
||||
}
|
||||
|
||||
Script newScript;
|
||||
|
||||
@@ -188,6 +206,8 @@ namespace Robust.Server.Scripting
|
||||
msg.AddText("\n");
|
||||
}
|
||||
|
||||
PromptAutoImports(e.Diagnostics, code, msg, instance);
|
||||
|
||||
replyMessage.Response = msg;
|
||||
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
|
||||
return;
|
||||
@@ -217,6 +237,21 @@ namespace Robust.Server.Scripting
|
||||
_netManager.ServerSendMessage(replyMessage, message.MsgChannel);
|
||||
}
|
||||
|
||||
private void PromptAutoImports(
|
||||
IEnumerable<Diagnostic> diags,
|
||||
string code,
|
||||
FormattedMessage output,
|
||||
ScriptInstance instance)
|
||||
{
|
||||
if (!ScriptInstanceShared.CalcAutoImports(_reflectionManager, diags, out var found))
|
||||
return;
|
||||
|
||||
output.AddText($"Auto-import {string.Join(", ", found)} (enter 'y')?");
|
||||
|
||||
instance.AutoImportRepeatBuffer = (found.ToArray(), code);
|
||||
}
|
||||
|
||||
|
||||
private sealed class ScriptInstance
|
||||
{
|
||||
public Workspace HighlightWorkspace { get; } = new AdhocWorkspace();
|
||||
@@ -227,6 +262,8 @@ namespace Robust.Server.Scripting
|
||||
public ScriptGlobals Globals { get; }
|
||||
public ScriptState? State { get; set; }
|
||||
|
||||
public (string[] imports, string code)? AutoImportRepeatBuffer;
|
||||
|
||||
public ScriptInstance()
|
||||
{
|
||||
Globals = new ScriptGlobalsImpl(this);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading.Tasks;
|
||||
using Lidgren.Network;
|
||||
using Microsoft.CodeAnalysis;
|
||||
@@ -23,6 +25,7 @@ namespace Robust.Shared.Scripting
|
||||
new(kind: SourceCodeKind.Script, languageVersion: LanguageVersion.Latest);
|
||||
|
||||
private static readonly Func<Script, bool> _hasReturnValue;
|
||||
private static readonly Func<Diagnostic, IReadOnlyList<object?>?> _getDiagnosticArguments;
|
||||
|
||||
private static readonly string[] _defaultImports =
|
||||
{
|
||||
@@ -47,10 +50,23 @@ namespace Robust.Shared.Scripting
|
||||
// Fallback path in case they remove that.
|
||||
// The method literally has a // TODO: remove
|
||||
_hasReturnValue = _ => true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_hasReturnValue = (Func<Script, bool>) Delegate.CreateDelegate(typeof(Func<Script, bool>), method);
|
||||
}
|
||||
|
||||
_hasReturnValue = (Func<Script, bool>) Delegate.CreateDelegate(typeof(Func<Script, bool>), method);
|
||||
// Also internal and we need it.
|
||||
var prop = typeof(Diagnostic).GetProperty("Arguments", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (prop == null)
|
||||
{
|
||||
_getDiagnosticArguments = _ => null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var moment = prop.GetMethod!;
|
||||
_getDiagnosticArguments = moment.CreateDelegate<Func<Diagnostic, IReadOnlyList<object?>?>>();
|
||||
}
|
||||
|
||||
// Run this async so that Roslyn can "warm up" in another thread while you're typing in your first line,
|
||||
// so the hang when you hit enter is less bad.
|
||||
@@ -79,6 +95,11 @@ namespace Robust.Shared.Scripting
|
||||
return _hasReturnValue(script);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<object?>? GetDiagnosticArgs(Diagnostic diag)
|
||||
{
|
||||
return _getDiagnosticArguments(diag);
|
||||
}
|
||||
|
||||
public static void AddWithSyntaxHighlighting(Script script, FormattedMessage msg, string code,
|
||||
Workspace workspace)
|
||||
{
|
||||
@@ -139,6 +160,60 @@ namespace Robust.Shared.Scripting
|
||||
return list;
|
||||
}
|
||||
|
||||
private static IEnumerable<Assembly> GetAutoImportAssemblies(IReflectionManager refl)
|
||||
{
|
||||
return GetDefaultReferences(refl).Union(
|
||||
AssemblyLoadContext.Default.Assemblies.Where(c => c.GetName().Name!.StartsWith("System."))
|
||||
);
|
||||
}
|
||||
|
||||
public static bool CalcAutoImports(
|
||||
IReflectionManager refl,
|
||||
IEnumerable<Diagnostic> diags,
|
||||
[NotNullWhen(true)] out HashSet<string>? found)
|
||||
{
|
||||
var missing = new List<string>();
|
||||
foreach (var diag in diags.Where(c => c.Id == "CS0103" || c.Id == "CS0246"))
|
||||
{
|
||||
var args = GetDiagnosticArgs(diag);
|
||||
if (args == null)
|
||||
{
|
||||
found = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
missing.Add((string) args[0]!);
|
||||
}
|
||||
|
||||
if (missing.Count == 0)
|
||||
{
|
||||
found = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
found = new HashSet<string>();
|
||||
var assemblies = ScriptInstanceShared.GetAutoImportAssemblies(refl).ToArray();
|
||||
foreach (var m in missing)
|
||||
{
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
foreach (var type in assembly.DefinedTypes)
|
||||
{
|
||||
if (type.IsPublic && type.Name == m)
|
||||
{
|
||||
found.Add(type.Namespace!);
|
||||
goto nextMissing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextMissing: ;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public static ScriptOptions GetScriptOptions(IReflectionManager reflectionManager)
|
||||
{
|
||||
return ScriptOptions.Default
|
||||
|
||||
@@ -864,6 +864,7 @@ Types:
|
||||
IDisposable: { All: True }
|
||||
IEquatable`1: { }
|
||||
IFormatProvider: { All: True }
|
||||
IFormattable: { All: True }
|
||||
IndexOutOfRangeException: { All: True }
|
||||
Int16: { All: True }
|
||||
Int32: { All: True }
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
internal class SharedTransformSystem : EntitySystem
|
||||
{
|
||||
private readonly List<MoveEvent> _deferredMoveEvents = new();
|
||||
private readonly Queue<MoveEvent> _gridMoves = new();
|
||||
private readonly Queue<MoveEvent> _otherMoves = new();
|
||||
|
||||
public void DeferMoveEvent(MoveEvent moveEvent)
|
||||
{
|
||||
_deferredMoveEvents.Add(moveEvent);
|
||||
if (moveEvent.Sender.HasComponent<IMapGridComponent>())
|
||||
_gridMoves.Enqueue(moveEvent);
|
||||
else
|
||||
_otherMoves.Enqueue(moveEvent);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var events = _deferredMoveEvents
|
||||
.OrderBy(e => e.Sender.HasComponent<IMapGridComponent>())
|
||||
.ToArray();
|
||||
// Process grid moves first.
|
||||
Process(_gridMoves);
|
||||
Process(_otherMoves);
|
||||
|
||||
foreach (var ev in events)
|
||||
void Process(Queue<MoveEvent> queue)
|
||||
{
|
||||
ev.Sender.EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
|
||||
ev.Handled = true;
|
||||
}
|
||||
while (queue.TryDequeue(out var ev))
|
||||
{
|
||||
if (ev.Sender.Deleted)
|
||||
continue;
|
||||
|
||||
_deferredMoveEvents.RemoveAll(e => e.Handled);
|
||||
RaiseLocalEvent(ev);
|
||||
ev.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,13 @@ namespace Robust.Shared.Localization
|
||||
/// </returns>
|
||||
string GetString(string messageId);
|
||||
|
||||
/// <summary>
|
||||
/// Try- version of <see cref="GetString(string)"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not log a warning if the message does not exist.
|
||||
/// Does however log errors if any occur while formatting.
|
||||
/// </remarks>
|
||||
bool TryGetString(string messageId, [NotNullWhen(true)] out string? value);
|
||||
|
||||
/// <summary>
|
||||
@@ -38,6 +45,13 @@ namespace Robust.Shared.Localization
|
||||
/// </summary>
|
||||
string GetString(string messageId, params (string, object)[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Try- version of <see cref="GetString(string, ValueTuple{string, object}[])"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not log a warning if the message does not exist.
|
||||
/// Does however log errors if any occur while formatting.
|
||||
/// </remarks>
|
||||
bool TryGetString(string messageId, [NotNullWhen(true)] out string? value, params (string, object)[] args);
|
||||
|
||||
/// <summary>
|
||||
@@ -51,10 +65,27 @@ namespace Robust.Shared.Localization
|
||||
/// </summary>
|
||||
/// <param name="resourceManager"></param>
|
||||
/// <param name="culture"></param>
|
||||
void LoadCulture(IResourceManager resourceManager, CultureInfo culture);
|
||||
[Obsolete("Use LoadCulture without IResourceManager overload instead.")]
|
||||
void LoadCulture(IResourceManager resourceManager, CultureInfo culture) => LoadCulture(culture);
|
||||
|
||||
/// <summary>
|
||||
/// Load data for a culture.
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
void LoadCulture(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// Immediately reload ALL localizations from resources.
|
||||
/// </summary>
|
||||
void ReloadLocalizations();
|
||||
|
||||
/// <summary>
|
||||
/// Add a function that can be called from Fluent localizations.
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture to add the function instance for.</param>
|
||||
/// <param name="name">The name of the function.</param>
|
||||
/// <param name="function">The function itself.</param>
|
||||
void AddFunction(CultureInfo culture, string name, LocFunction function);
|
||||
|
||||
/// <summary>
|
||||
/// Remnants of the old Localization system.
|
||||
|
||||
@@ -61,8 +61,8 @@ namespace Robust.Shared.Localization
|
||||
/// Load data for a culture.
|
||||
/// </summary>
|
||||
/// <param name="resourceManager"></param>
|
||||
/// <param name="macroFactory"></param>
|
||||
/// <param name="culture"></param>
|
||||
[Obsolete("Use ILocalizationManager directly for setup methods.")]
|
||||
public static void LoadCulture(IResourceManager resourceManager, CultureInfo culture)
|
||||
{
|
||||
LocalizationManager.LoadCulture(resourceManager, culture);
|
||||
|
||||
202
Robust.Shared/Localization/LocFunction.cs
Normal file
202
Robust.Shared/Localization/LocFunction.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Fluent.Net;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
{
|
||||
// Basically Fluent.Net is based on an ancient pre-1.0 version of fluent.js.
|
||||
// Said version of fluent.js was also a complete garbage fire implementation wise.
|
||||
// So Fluent.Net is garbage implementation wise.
|
||||
// ...yay
|
||||
// (the current implementation of fluent.js is written in TS and actually sane)
|
||||
//
|
||||
// Because of this, we can't expose it to content, so we have to wrap everything related to functions.
|
||||
// This basically mimics the modern typescript fluent.js API. Somewhat.
|
||||
|
||||
/// <summary>
|
||||
/// Function signature runnable by localizations.
|
||||
/// </summary>
|
||||
/// <param name="args">Contains arguments and options passed to the function by the calling localization.</param>
|
||||
public delegate ILocValue LocFunction(LocArgs args);
|
||||
|
||||
[PublicAPI]
|
||||
public readonly struct LocContext
|
||||
{
|
||||
public CultureInfo Culture => Context.Culture;
|
||||
|
||||
internal readonly MessageContext Context;
|
||||
|
||||
internal LocContext(MessageContext ctx)
|
||||
{
|
||||
Context = ctx;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arguments and options passed to a localization function.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public readonly struct LocArgs
|
||||
{
|
||||
public LocArgs(IReadOnlyList<ILocValue> args, IReadOnlyDictionary<string, ILocValue> options)
|
||||
{
|
||||
Args = args;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positional arguments passed to the function, in order.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ILocValue> Args { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Key-value options passed to the function.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, ILocValue> Options { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A value passed around in the localization system.
|
||||
/// </summary>
|
||||
/// <seealso cref="LocValue{T}"/>
|
||||
public interface ILocValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Format this value to a string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used when this value is interpolated directly in localizations.
|
||||
/// </remarks>
|
||||
/// <param name="ctx">Context containing data like culture used.</param>
|
||||
/// <returns>The formatted string.</returns>
|
||||
string Format(LocContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Boxed value stored by this instance.
|
||||
/// </summary>
|
||||
object? Value { get; }
|
||||
|
||||
// Matches API doesn't work because Fluent.Net is crap.
|
||||
// RIP.
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Checks if this value matches a string in a select expression.
|
||||
/// </summary>
|
||||
bool Matches(LocContext ctx, string matchValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of a localization value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The idea is that inheritors could add extra data like formatting parameters
|
||||
/// and then use those by overriding <see cref="Format"/>.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of value stored.</typeparam>
|
||||
[PublicAPI]
|
||||
public abstract record LocValue<T> : ILocValue
|
||||
{
|
||||
/// <summary>
|
||||
/// The stored value.
|
||||
/// </summary>
|
||||
public T Value { get; init; }
|
||||
|
||||
object? ILocValue.Value => Value;
|
||||
|
||||
protected LocValue(T val)
|
||||
{
|
||||
Value = val;
|
||||
}
|
||||
|
||||
public abstract string Format(LocContext ctx);
|
||||
|
||||
/*
|
||||
public virtual bool Matches(LocContext ctx, string matchValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public sealed record LocValueNumber(double Value) : LocValue<double>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString(ctx.Culture);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LocValueDateTime(DateTime Value) : LocValue<DateTime>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString(ctx.Culture);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LocValueString(string Value) : LocValue<string>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an "invalid" string value. Produced by e.g. unresolved variable references.
|
||||
/// </summary>
|
||||
public sealed record LocValueNone(string Value) : LocValue<string>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LocValueEntity(IEntity Value) : LocValue<IEntity>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Matching on these doesn't work so just passing these as string for now.
|
||||
|
||||
/*public sealed record LocValueBool(bool Value) : LocValue<bool>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString(ctx.Culture);
|
||||
}
|
||||
|
||||
/*
|
||||
public override bool Matches(LocContext ctx, string matchValue)
|
||||
{
|
||||
var word = Value ? "true" : "false";
|
||||
return word.Equals(matchValue, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
#1#
|
||||
}
|
||||
|
||||
public sealed record LocValueEnum(Enum Value) : LocValue<Enum>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
/*public override bool Matches(LocContext ctx, string matchValue)
|
||||
{
|
||||
return matchValue.Equals(Value.ToString(), StringComparison.InvariantCultureIgnoreCase);
|
||||
}#1#
|
||||
}*/
|
||||
}
|
||||
@@ -3,22 +3,34 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Fluent.Net;
|
||||
using Fluent.Net.RuntimeAst;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
{
|
||||
internal sealed class LocalizationManager : ILocalizationManagerInternal
|
||||
internal sealed class LocalizationManager : ILocalizationManagerInternal, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
|
||||
private ISawmill _logSawmill = default!;
|
||||
private readonly Dictionary<CultureInfo, MessageContext> _contexts = new();
|
||||
|
||||
private CultureInfo? _defaultCulture;
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_logSawmill = _log.GetSawmill("loc");
|
||||
}
|
||||
|
||||
public string GetString(string messageId)
|
||||
{
|
||||
if (_defaultCulture == null)
|
||||
@@ -26,7 +38,7 @@ namespace Robust.Shared.Localization
|
||||
|
||||
if (!TryGetString(messageId, out var msg))
|
||||
{
|
||||
Logger.WarningS("Loc", $"Unknown messageId ({_defaultCulture.IetfLanguageTag}): {messageId}");
|
||||
_logSawmill.Warning("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
|
||||
msg = messageId;
|
||||
}
|
||||
|
||||
@@ -41,8 +53,7 @@ namespace Robust.Shared.Localization
|
||||
return false;
|
||||
}
|
||||
|
||||
value = context.Format(node, null, null);
|
||||
return true;
|
||||
return DoFormat(messageId, out value, context, node);
|
||||
}
|
||||
|
||||
public string GetString(string messageId, params (string, object)[] args0)
|
||||
@@ -52,14 +63,15 @@ namespace Robust.Shared.Localization
|
||||
|
||||
if (!TryGetString(messageId, out var msg, args0))
|
||||
{
|
||||
Logger.WarningS("Loc", $"Unknown messageId ({_defaultCulture.IetfLanguageTag}): {messageId}");
|
||||
_logSawmill.Warning("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
|
||||
msg = messageId;
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value, params (string, object)[] args0)
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value,
|
||||
params (string, object)[] args0)
|
||||
{
|
||||
if (!TryGetNode(messageId, out var context, out var node))
|
||||
{
|
||||
@@ -70,11 +82,20 @@ namespace Robust.Shared.Localization
|
||||
var args = new Dictionary<string, object>();
|
||||
foreach (var (k, v) in args0)
|
||||
{
|
||||
args.Add(k, v);
|
||||
var val = v switch
|
||||
{
|
||||
IEntity entity => new LocValueEntity(entity),
|
||||
bool or Enum => v.ToString()!.ToLowerInvariant(),
|
||||
_ => v,
|
||||
};
|
||||
|
||||
if (val is ILocValue locVal)
|
||||
val = new FluentLocWrapperType(locVal);
|
||||
|
||||
args.Add(k, val);
|
||||
}
|
||||
|
||||
value = context.Format(node, args, null);
|
||||
return false;
|
||||
return DoFormat(messageId, out value, context, node, args);
|
||||
}
|
||||
|
||||
private bool TryGetNode(
|
||||
@@ -109,7 +130,7 @@ namespace Robust.Shared.Localization
|
||||
|
||||
if (attribName != null)
|
||||
{
|
||||
if (!message.Attributes.TryGetValue(attribName, out var attrib))
|
||||
if (message.Attributes == null || !message.Attributes.TryGetValue(attribName, out var attrib))
|
||||
{
|
||||
node = null;
|
||||
return false;
|
||||
@@ -125,6 +146,105 @@ namespace Robust.Shared.Localization
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ReloadLocalizations()
|
||||
{
|
||||
foreach (var (culture, context) in _contexts.ToArray())
|
||||
{
|
||||
// Fluent.Net doesn't allow us to remove messages so...
|
||||
var newContext = new MessageContext(
|
||||
culture.Name,
|
||||
new MessageContextOptions
|
||||
{
|
||||
UseIsolating = false,
|
||||
Functions = context.Functions
|
||||
}
|
||||
);
|
||||
|
||||
_contexts[culture] = newContext;
|
||||
|
||||
_loadData(_res, culture, newContext);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFunction(CultureInfo culture, string name, LocFunction function)
|
||||
{
|
||||
var context = _contexts[culture];
|
||||
|
||||
context.Functions.Add(name, (args, options) => CallFunction(function, args, options));
|
||||
}
|
||||
|
||||
private FluentType CallFunction(
|
||||
LocFunction function,
|
||||
IList<object> fluentArgs, IDictionary<string, object> fluentOptions)
|
||||
{
|
||||
var args = new ILocValue[fluentArgs.Count];
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
args[i] = ValFromFluent(fluentArgs[i]);
|
||||
}
|
||||
|
||||
var options = new Dictionary<string, ILocValue>(fluentOptions.Count);
|
||||
foreach (var (k, v) in fluentOptions)
|
||||
{
|
||||
options.Add(k, ValFromFluent(v));
|
||||
}
|
||||
|
||||
var argStruct = new LocArgs(args, options);
|
||||
|
||||
var ret = function(argStruct);
|
||||
|
||||
return ValToFluent(ret);
|
||||
}
|
||||
|
||||
private static ILocValue ValFromFluent(object arg)
|
||||
{
|
||||
return arg switch
|
||||
{
|
||||
FluentNone none => new LocValueNone(none.Value),
|
||||
FluentNumber number => new LocValueNumber(double.Parse(number.Value)),
|
||||
FluentString str => new LocValueString(str.Value),
|
||||
FluentDateTime dateTime =>
|
||||
new LocValueDateTime(DateTime.Parse(dateTime.Value, null, DateTimeStyles.RoundtripKind)),
|
||||
FluentLocWrapperType wrap => wrap.WrappedValue,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(arg))
|
||||
};
|
||||
}
|
||||
|
||||
private static FluentType ValToFluent(ILocValue arg)
|
||||
{
|
||||
return arg switch
|
||||
{
|
||||
LocValueNone =>
|
||||
throw new NotSupportedException("Cannot currently return LocValueNone from loc functions."),
|
||||
LocValueNumber number => new FluentNumber(number.Value.ToString("R")),
|
||||
LocValueString str => new FluentString(str.Value),
|
||||
LocValueDateTime dateTime => new FluentDateTime(dateTime.Value),
|
||||
_ => new FluentLocWrapperType(arg)
|
||||
};
|
||||
}
|
||||
|
||||
private bool DoFormat(string messageId, out string? value, MessageContext context, Node node, IDictionary<string, object>? args = null)
|
||||
{
|
||||
var errs = new List<FluentError>();
|
||||
try
|
||||
{
|
||||
value = context.Format(node, args, errs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {exception}", _defaultCulture!.Name, messageId, e);
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var err in errs)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {error}", _defaultCulture!.Name, messageId, err);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remnants of the old Localization system.
|
||||
/// It exists to prevent source errors and allow existing game text to *mostly* work
|
||||
@@ -157,16 +277,23 @@ namespace Robust.Shared.Localization
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadCulture(IResourceManager resourceManager, CultureInfo culture)
|
||||
public void LoadCulture(CultureInfo culture)
|
||||
{
|
||||
var context = new MessageContext(
|
||||
culture.Name,
|
||||
new MessageContextOptions {UseIsolating = false}
|
||||
new MessageContextOptions
|
||||
{
|
||||
UseIsolating = false,
|
||||
// Have to pass empty dict here or else Fluent.Net will fuck up
|
||||
// and share the same dict between multiple message contexts.
|
||||
// Yes, you read that right.
|
||||
Functions = new Dictionary<string, Resolver.ExternalFunction>(),
|
||||
}
|
||||
);
|
||||
|
||||
_contexts.Add(culture, context);
|
||||
|
||||
_loadData(resourceManager, culture, context);
|
||||
_loadData(_res, culture, context);
|
||||
if (DefaultCulture == null)
|
||||
{
|
||||
DefaultCulture = culture;
|
||||
@@ -197,31 +324,53 @@ namespace Robust.Shared.Localization
|
||||
*/
|
||||
}
|
||||
|
||||
private static void _loadData(IResourceManager resourceManager, CultureInfo culture, MessageContext context)
|
||||
private void _loadData(IResourceManager resourceManager, CultureInfo culture, MessageContext context)
|
||||
{
|
||||
// Load data from .ftl files.
|
||||
// Data is loaded from /Locale/<language-code>/*
|
||||
|
||||
var root = new ResourcePath($"/Locale/{culture.IetfLanguageTag}/");
|
||||
var root = new ResourcePath($"/Locale/{culture.Name}/");
|
||||
|
||||
foreach (var file in resourceManager.ContentFindFiles(root))
|
||||
var files = resourceManager.ContentFindFiles(root).ToArray();
|
||||
|
||||
var resources = files.AsParallel().Select(path =>
|
||||
{
|
||||
var ftlFile = root / file;
|
||||
_loadFromFile(resourceManager, ftlFile, context);
|
||||
using var fileStream = resourceManager.ContentFileRead(path);
|
||||
using var reader = new StreamReader(fileStream, EncodingHelpers.UTF8);
|
||||
|
||||
var resource = FluentResource.FromReader(reader);
|
||||
return (path, resource);
|
||||
});
|
||||
|
||||
foreach (var (path, resource) in resources)
|
||||
{
|
||||
var errors = context.AddResource(resource);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
_logSawmill.Warning("{path}: {exception}", path, error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void _loadFromFile(IResourceManager resourceManager, ResourcePath filePath,
|
||||
MessageContext context)
|
||||
private sealed class FluentLocWrapperType : FluentType
|
||||
{
|
||||
using (var fileStream = resourceManager.ContentFileRead(filePath))
|
||||
using (var reader = new StreamReader(fileStream, EncodingHelpers.UTF8))
|
||||
public readonly ILocValue WrappedValue;
|
||||
|
||||
public FluentLocWrapperType(ILocValue wrappedValue)
|
||||
{
|
||||
var errors = context.AddMessages(reader);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
Logger.WarningS("Loc", error.Message);
|
||||
}
|
||||
WrappedValue = wrappedValue;
|
||||
}
|
||||
|
||||
public override string Format(MessageContext ctx)
|
||||
{
|
||||
return WrappedValue.Format(new LocContext(ctx));
|
||||
}
|
||||
|
||||
public override bool Match(MessageContext ctx, object obj)
|
||||
{
|
||||
return false;
|
||||
/*var strVal = obj is IFluentType ft ? ft.Value : obj.ToString() ?? "";
|
||||
return WrappedValue.Matches(new LocContext(ctx), strVal);*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Robust.Shared.Prototypes
|
||||
/// </exception>
|
||||
IPrototype Index(Type type, string id);
|
||||
bool HasIndex<T>(string id) where T : IPrototype;
|
||||
bool TryIndex<T>(string id, out T prototype) where T : IPrototype;
|
||||
bool TryIndex<T>(string id, [NotNullWhen(true)] out T? prototype) where T : IPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// Load prototypes from files in a directory, recursively.
|
||||
@@ -464,7 +464,7 @@ namespace Robust.Shared.Prototypes
|
||||
return index.ContainsKey(id);
|
||||
}
|
||||
|
||||
public bool TryIndex<T>(string id, [MaybeNullWhen(false)] out T prototype) where T : IPrototype
|
||||
public bool TryIndex<T>(string id, [NotNullWhen(true)] out T? prototype) where T : IPrototype
|
||||
{
|
||||
if (!prototypes.TryGetValue(typeof(T), out var index))
|
||||
{
|
||||
|
||||
26
Robust.UnitTesting/IocExt.cs
Normal file
26
Robust.UnitTesting/IocExt.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.UnitTesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for setting up IoC in tests.
|
||||
/// </summary>
|
||||
public static class IocExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the log manager with test log handler to a dependency collection.
|
||||
/// </summary>
|
||||
/// <param name="deps">Dependency collection to register into.</param>
|
||||
/// <param name="prefix">Prefix for the test log output.</param>
|
||||
public static void RegisterLogs(this IDependencyCollection deps, string? prefix = null)
|
||||
{
|
||||
deps.Register<ILogManager, LogManager>(() =>
|
||||
{
|
||||
var log = new LogManager();
|
||||
log.RootSawmill.AddHandler(new TestLogHandler(prefix));
|
||||
return log;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Robust.UnitTesting/Shared/Localization/LocalizationTests.cs
Normal file
85
Robust.UnitTesting/Shared/Localization/LocalizationTests.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Globalization;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Localization
|
||||
{
|
||||
[TestFixture]
|
||||
internal sealed class LocalizationTests
|
||||
{
|
||||
private const string FluentCode = @"
|
||||
foo = { BAR($baz) }
|
||||
|
||||
enum-match = { $enum ->
|
||||
[foo] A
|
||||
*[bar] B
|
||||
}
|
||||
";
|
||||
|
||||
private (ILocalizationManager, CultureInfo) BuildLocalizationManager()
|
||||
{
|
||||
var ioc = new DependencyCollection();
|
||||
ioc.Register<ILocalizationManager, LocalizationManager>();
|
||||
ioc.Register<IResourceManager, ResourceManager>();
|
||||
ioc.Register<IResourceManagerInternal, ResourceManager>();
|
||||
ioc.Register<IConfigurationManager, ConfigurationManager>();
|
||||
ioc.RegisterLogs();
|
||||
ioc.BuildGraph();
|
||||
|
||||
var res = ioc.Resolve<IResourceManagerInternal>();
|
||||
res.MountString("/Locale/en-US/a.ftl", FluentCode);
|
||||
|
||||
var loc = ioc.Resolve<ILocalizationManager>();
|
||||
var culture = new CultureInfo("en-US", false);
|
||||
loc.LoadCulture(culture);
|
||||
|
||||
return (loc, culture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomTypes()
|
||||
{
|
||||
var (loc, culture) = BuildLocalizationManager();
|
||||
|
||||
loc.AddFunction(culture, "BAR", Function);
|
||||
|
||||
var ret = loc.GetString("foo", ("baz", new LocValueVector2((-7, 5))));
|
||||
|
||||
Assert.That(ret, Is.EqualTo("5"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnumSelect()
|
||||
{
|
||||
var (loc, _) = BuildLocalizationManager();
|
||||
|
||||
Assert.That(loc.GetString("enum-match", ("enum", TestEnum.Foo)), Is.EqualTo("A"));
|
||||
Assert.That(loc.GetString("enum-match", ("enum", TestEnum.Bar)), Is.EqualTo("B"));
|
||||
Assert.That(loc.GetString("enum-match", ("enum", TestEnum.Baz)), Is.EqualTo("B"));
|
||||
}
|
||||
|
||||
private static ILocValue Function(LocArgs args)
|
||||
{
|
||||
return new LocValueNumber(((LocValueVector2) args.Args[0]).Value.Y);
|
||||
}
|
||||
|
||||
private sealed record LocValueVector2(Vector2 Value) : LocValue<Vector2>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private enum TestEnum
|
||||
{
|
||||
Foo,
|
||||
Bar,
|
||||
Baz
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,17 @@ namespace Robust.UnitTesting
|
||||
|
||||
public sealed class TestLogHandler : ILogHandler, IDisposable
|
||||
{
|
||||
|
||||
private readonly string _prefix;
|
||||
private readonly string? _prefix;
|
||||
|
||||
private readonly TextWriter _writer;
|
||||
|
||||
private readonly Stopwatch _sw = Stopwatch.StartNew();
|
||||
|
||||
public TestLogHandler(string prefix)
|
||||
public TestLogHandler(string? prefix = null)
|
||||
{
|
||||
_prefix = prefix;
|
||||
_writer = TestContext.Out;
|
||||
_writer.WriteLine($"{_prefix}: Started {DateTime.Now:o}");
|
||||
_writer.WriteLine($"{GetPrefix()}Started {DateTime.Now:o}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -34,9 +33,13 @@ namespace Robust.UnitTesting
|
||||
var name = LogMessage.LogLevelToName(message.Level.ToRobust());
|
||||
var seconds = _sw.ElapsedMilliseconds/1000d;
|
||||
var rendered = message.RenderMessage();
|
||||
_writer.WriteLine($"{_prefix}: {seconds:F3}s [{name}] {sawmillName}: {rendered}");
|
||||
_writer.WriteLine($"{GetPrefix()}{seconds:F3}s [{name}] {sawmillName}: {rendered}");
|
||||
}
|
||||
|
||||
private string GetPrefix()
|
||||
{
|
||||
return _prefix != null ? $"{_prefix}: " : "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user