Compare commits

..

3 Commits

Author SHA1 Message Date
penev92
54675d6f2b Fixed packaging for Windows missing assembly info
Apparently building/publishing/packaging for Windows on Linux using .NET 6 sets some of the assembly / Portable Executable information for the generated DLL, but nothing for the generated EXE (which isn't the case when building on Windows).
2023-04-14 19:40:08 +03:00
penev92
2101e74470 Added product version to assembly info 2023-04-14 18:42:33 +03:00
penev92
73ccc83971 Added project information in Directory.Build.props 2023-04-14 18:42:29 +03:00
1369 changed files with 12505 additions and 24458 deletions

View File

@@ -612,19 +612,6 @@ dotnet_code_quality.api_surface = all
### Design Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/design-warnings
# Collections should implement generic interface.
#dotnet_code_quality.CA1010.additional_required_generic_interfaces =
dotnet_diagnostic.CA1010.severity = warning
# Abstract types should not have public constructors.
dotnet_diagnostic.CA1012.severity = warning
# Mark attributes with 'AttributeUsageAttribute'.
dotnet_diagnostic.CA1018.severity = warning
# Override methods on comparable types.
dotnet_diagnostic.CA1036.severity = warning
# Provide ObsoleteAttribute message.
dotnet_diagnostic.CA1041.severity = warning
@@ -634,18 +621,9 @@ dotnet_diagnostic.CA1047.severity = warning
# Declare types in namespaces.
dotnet_diagnostic.CA1050.severity = warning
# Static holder types should be 'Static' or 'NotInheritable'.
dotnet_diagnostic.CA1052.severity = warning
# Do not hide base class methods.
dotnet_diagnostic.CA1061.severity = warning
# Exceptions should be public.
dotnet_diagnostic.CA1064.severity = warning
# Implement 'IEquatable' when overriding 'Equals'.
dotnet_diagnostic.CA1066.severity = warning
# Override 'Equals' when implementing 'IEquatable'.
dotnet_diagnostic.CA1067.severity = warning
@@ -692,17 +670,9 @@ dotnet_diagnostic.CA1717.severity = warning
### Performance Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/performance-warnings
# Use Literals Where Appropriate.
#dotnet_code_quality.CA1802.required_modifiers = static
dotnet_diagnostic.CA1802.severity = warning
# Remove empty finalizers.
dotnet_diagnostic.CA1821.severity = warning
# Mark members as static.
dotnet_code_quality.CA1822.api_surface = private,internal
dotnet_diagnostic.CA1822.severity = warning
# Avoid unused private fields.
dotnet_diagnostic.CA1823.severity = warning
@@ -734,9 +704,6 @@ dotnet_diagnostic.CA1832.severity = warning
# Use AsSpan or AsMemory instead of Range-based indexers for getting Span or Memory portion of an array.
dotnet_diagnostic.CA1833.severity = warning
# Use StringBuilder.Append(char) for single character strings.
dotnet_diagnostic.CA1834.severity = warning
# Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes.
dotnet_diagnostic.CA1835.severity = warning
@@ -749,15 +716,9 @@ dotnet_diagnostic.CA1837.severity = warning
# Avoid StringBuilder parameters for P/Invokes.
dotnet_diagnostic.CA1838.severity = warning
# Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName.
dotnet_diagnostic.CA1839.severity = warning
# Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId.
dotnet_diagnostic.CA1840.severity = warning
# Prefer Dictionary Contains methods.
dotnet_diagnostic.CA1841.severity = warning
# Do not use 'WhenAll' with a single task.
dotnet_diagnostic.CA1842.severity = warning
@@ -767,30 +728,15 @@ dotnet_diagnostic.CA1843.severity = warning
# Provide memory-based overrides of async methods when subclassing 'Stream'.
dotnet_diagnostic.CA1844.severity = warning
# Use span-based 'string.Concat'. (Not available on mono)
dotnet_diagnostic.CA1845.severity = none
# Prefer AsSpan over Substring.
dotnet_diagnostic.CA1846.severity = warning
# Use span-based 'string.Concat'.
dotnet_diagnostic.CA1845.severity = warning
# Use string.Contains(char) instead of string.Contains(string) with single characters.
dotnet_diagnostic.CA1847.severity = warning
# Call async methods when in an async method.
dotnet_diagnostic.CA1849.severity = warning
# Prefer static HashData method over ComputeHash. (Not available on mono)
dotnet_diagnostic.CA1850.severity = none
# Seal internal types.
dotnet_diagnostic.CA1852.severity = warning
# Unnecessary call to 'Dictionary.ContainsKey(key)'.
dotnet_diagnostic.CA1853.severity = warning
# Prefer the IDictionary.TryGetValue(TKey, out TValue) method.
dotnet_diagnostic.CA1854.severity = warning
# Use Span<T>.Clear() instead of Span<T>.Fill().
dotnet_diagnostic.CA1855.severity = warning
@@ -821,9 +767,6 @@ dotnet_diagnostic.CA2016.severity = warning
# The 'count' argument to Buffer.BlockCopy should specify the number of bytes to copy.
dotnet_diagnostic.CA2018.severity = warning
# ThreadStatic fields should not use inline initialization.
dotnet_diagnostic.CA2019.severity = warning
### Security Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/security-warnings
@@ -833,24 +776,12 @@ dotnet_diagnostic.CA5351.severity = warning
### Usage Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/usage-warnings
# Call GC.SuppressFinalize correctly.
dotnet_diagnostic.CA1816.severity = warning
# Rethrow to preserve stack details.
dotnet_diagnostic.CA2200.severity = warning
# Initialize value type static fields inline.
dotnet_diagnostic.CA2207.severity = warning
# Instantiate argument exceptions correctly.
dotnet_diagnostic.CA2208.severity = warning
# Dispose methods should call base class dispose.
dotnet_diagnostic.CA2215.severity = warning
# Disposable types should declare finalizer.
dotnet_diagnostic.CA2216.severity = warning
# Override GetHashCode on overriding Equals.
dotnet_diagnostic.CA2218.severity = warning

View File

@@ -25,13 +25,12 @@ jobs:
- name: Check Code
run: |
make check
make tests
- name: Check Mods
run: |
sudo apt-get install lua5.1
make check-scripts
make TREAT_WARNINGS_AS_ERRORS=true test
make test
linux-mono:
name: Linux (mono)
@@ -49,7 +48,7 @@ jobs:
- name: Check Mods
run: |
# check-scripts does not depend on .net/mono, so is not needed here
make RUNTIME=mono TREAT_WARNINGS_AS_ERRORS=true test
make RUNTIME=mono test
windows:
name: Windows (.NET 6.0)
@@ -70,12 +69,12 @@ jobs:
# Work around runtime failures on the GH Actions runner
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
.\make.ps1 check
.\make.ps1 tests
dotnet build OpenRA.Test\OpenRA.Test.csproj -c Debug --nologo -p:TargetPlatform=win-x64
dotnet test bin\OpenRA.Test.dll --test-adapter-path:.
- name: Check Mods
run: |
choco install lua --version 5.1.5.52
chocolatey install lua --version 5.1.5.52
$ENV:Path = $ENV:Path + ";C:\Program Files (x86)\Lua\5.1\"
$ENV:TREAT_WARNINGS_AS_ERRORS = "true"
.\make.ps1 check-scripts
.\make.ps1 test

View File

@@ -120,7 +120,6 @@ Also thanks to:
* Mike Gagné (AngryBirdz)
* Muh
* Mustafa Alperen Seki (MustaphaTR)
* Nathan Nichols (cracksmoka420)
* Neil Shivkar (havok13888)
* Nikolay Fomin (netnazgul)
* Nooze

View File

@@ -130,7 +130,7 @@ endif
check-scripts:
@echo
@echo "Checking for Lua syntax errors..."
@find mods/*/maps/ mods/*/scripts/ -iname "*.lua" -print0 | xargs -0n1 luac -p
@find lua/ mods/*/{maps,scripts}/ -iname "*.lua" -print0 | xargs -0n1 luac -p
test: all
@echo
@@ -146,11 +146,6 @@ test: all
@echo "Testing Red Alert mod MiniYAML..."
@./utility.sh ra --check-yaml
tests:
@dotnet build OpenRA.Test/OpenRA.Test.csproj -c Debug --nologo -p:TargetPlatform=$(TARGETPLATFORM)
@echo
@dotnet test bin/OpenRA.Test.dll --test-adapter-path:.
############# LOCAL INSTALLATION AND DOWNSTREAM PACKAGING ##############
#
version: VERSION mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml
@@ -185,7 +180,7 @@ help:
@echo ' make [RUNTIME=net6] TARGETPLATFORM=unix-generic'
@echo
@echo 'to check the official mods for erroneous yaml files, run:'
@echo ' make [RUNTIME=net6] [TREAT_WARNINGS_AS_ERRORS=false] test'
@echo ' make [RUNTIME=net6] test'
@echo
@echo 'to check the engine and official mod dlls for code style violations, run:'
@echo ' make [RUNTIME=net6] check'

View File

@@ -86,7 +86,7 @@ namespace OpenRA.Activities
bool firstRunCompleted;
bool lastRun;
protected Activity()
public Activity()
{
IsInterruptible = true;
ChildHasPriority = true;

View File

@@ -35,9 +35,6 @@ namespace OpenRA
public sealed class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable<Actor>, IDisposable
{
/// <summary>Value used to represent an invalid token.</summary>
public const int InvalidConditionToken = -1;
internal readonly struct SyncHash
{
public readonly ISync Trait;
@@ -81,7 +78,10 @@ namespace OpenRA
public WRot Orientation => facing?.Orientation ?? WRot.None;
sealed class ConditionState
/// <summary>Value used to represent an invalid token.</summary>
public static readonly int InvalidConditionToken = -1;
class ConditionState
{
/// <summary>Delegates that have registered to be notified when this condition changes.</summary>
public readonly List<VariableObserverNotifier> Notifiers = new();

View File

@@ -187,10 +187,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to decrypt string with exception:");
Log.Write("debug", e);
Console.WriteLine("String decryption failed:");
Console.WriteLine(e);
Log.Write("debug", "Failed to decrypt string with exception: {0}", e);
Console.WriteLine("String decryption failed: {0}", e);
return null;
}
}
@@ -213,10 +211,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to sign string with exception");
Log.Write("debug", e);
Console.WriteLine("String signing failed:");
Console.WriteLine(e);
Log.Write("debug", "Failed to sign string with exception: {0}", e);
Console.WriteLine("String signing failed: {0}", e);
return null;
}
}
@@ -239,10 +235,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to verify signature with exception:");
Log.Write("debug", e);
Console.WriteLine("Signature validation failed:");
Console.WriteLine(e);
Log.Write("debug", "Failed to verify signature with exception: {0}", e);
Console.WriteLine("Signature validation failed: {0}", e);
return false;
}
}

View File

@@ -81,8 +81,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", $"Failed to parse mod metadata file '{path}'");
Log.Write("debug", e);
Log.Write("debug", "Failed to parse mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
}
}
}
@@ -174,7 +174,7 @@ namespace OpenRA
catch (Exception e)
{
Log.Write("debug", "Failed to register current mod metadata");
Log.Write("debug", e);
Log.Write("debug", e.ToString());
}
}
}
@@ -213,8 +213,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", $"Failed to parse mod metadata file '{path}'");
Log.Write("debug", e);
Log.Write("debug", "Failed to parse mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
}
// Remove from the ingame mod switcher
@@ -225,12 +225,12 @@ namespace OpenRA
try
{
File.Delete(path);
Log.Write("debug", $"Removed invalid mod metadata file '{path}'");
Log.Write("debug", "Removed invalid mod metadata file '{0}'", path);
}
catch (Exception e)
{
Log.Write("debug", $"Failed to remove mod metadata file '{path}'");
Log.Write("debug", e);
Log.Write("debug", "Failed to remove mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
}
}
}
@@ -251,13 +251,13 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", $"Failed to remove mod metadata file '{path}'");
Log.Write("debug", e);
Log.Write("debug", "Failed to remove mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
}
}
}
static IEnumerable<string> GetSupportDirs(ModRegistration registration)
IEnumerable<string> GetSupportDirs(ModRegistration registration)
{
var sources = new HashSet<string>(4);
if (registration.HasFlag(ModRegistration.System))

View File

@@ -27,6 +27,11 @@ namespace OpenRA
return string.Compare(str.ToUpperInvariant(), str, false) == 0;
}
public static string F(this string format, params object[] args)
{
return string.Format(format, args);
}
public static T WithDefault<T>(T def, Func<T> f)
{
try { return f(); }
@@ -109,23 +114,13 @@ namespace OpenRA
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v)
{
#if NET5_0_OR_GREATER
// SAFETY: Dictionary cannot be modified whilst the ref is alive.
ref var value = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
if (!exists)
value = v;
return value;
#else
if (!d.TryGetValue(k, out var ret))
d.Add(k, ret = v);
return ret;
#endif
}
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, Func<K, V> createFn)
{
// Cannot use CollectionsMarshal.GetValueRefOrAddDefault here,
// the creation function could mutate the dictionary which would invalidate the ref.
if (!d.TryGetValue(k, out var ret))
d.Add(k, ret = createFn(k));
return ret;
@@ -562,11 +557,6 @@ namespace OpenRA
{
return new LineSplitEnumerator(str.AsSpan(), separator);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
}
public ref struct LineSplitEnumerator

View File

@@ -112,19 +112,10 @@ namespace OpenRA
{ typeof(Nullable<>), ParseNullable },
};
static readonly object BoxedTrue = true;
static readonly object BoxedFalse = false;
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseIntegerInvariant(value, out var res))
{
if (res >= 0 && res < BoxedInts.Length)
return BoxedInts[res];
return res;
}
return InvalidValueAction(value, fieldType, fieldName);
}
@@ -370,7 +361,7 @@ namespace OpenRA
static object ParseBool(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (bool.TryParse(value.ToLowerInvariant(), out var result))
return result ? BoxedTrue : BoxedFalse;
return result;
return InvalidValueAction(value, fieldType, fieldName);
}
@@ -478,11 +469,11 @@ namespace OpenRA
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
var set = Activator.CreateInstance(fieldType);
if (value == null)
return Activator.CreateInstance(fieldType);
return set;
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var set = Activator.CreateInstance(fieldType, parts.Length);
var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments);
var addArgs = new object[1];
@@ -497,10 +488,7 @@ namespace OpenRA
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
if (yaml == null)
return Activator.CreateInstance(fieldType);
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Count);
var dict = Activator.CreateInstance(fieldType);
var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(Dictionary<object, object>.Add), arguments);
var addArgs = new object[2];
@@ -539,7 +527,7 @@ namespace OpenRA
public static void Load(object self, MiniYaml my)
{
var loadInfo = TypeLoadInfo[self.GetType()];
List<string> missing = null;
var missing = new List<string>();
Dictionary<string, MiniYaml> md = null;
@@ -554,7 +542,6 @@ namespace OpenRA
val = fli.Loader(my);
else
{
missing ??= new List<string>();
missing.Add(fli.YamlName);
continue;
}
@@ -564,11 +551,7 @@ namespace OpenRA
if (!TryGetValueFromYaml(fli.YamlName, fli.Field, md, out val))
{
if (fli.Attribute.Required)
{
missing ??= new List<string>();
missing.Add(fli.YamlName);
}
continue;
}
}
@@ -576,7 +559,7 @@ namespace OpenRA
fli.Field.SetValue(self, val);
}
if (missing != null)
if (missing.Count > 0)
throw new MissingFieldsException(missing.ToArray());
}
@@ -637,17 +620,12 @@ namespace OpenRA
public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field)
{
return GetValue(fieldName, fieldType, value, null, field);
return GetValue(fieldName, fieldType, new MiniYaml(value), field);
}
public static object GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field)
{
return GetValue(fieldName, fieldType, yaml.Value, yaml, field);
}
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
value = value?.Trim();
var value = yaml.Value?.Trim();
if (fieldType.IsGenericType)
{
if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric))

View File

@@ -67,7 +67,7 @@ namespace OpenRA.FileFormats
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
var bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadByte();
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color)
@@ -75,9 +75,9 @@ namespace OpenRA.FileFormats
Data = new byte[Width * Height * PixelStride];
var compression = ms.ReadUInt8();
/*var filter = */ms.ReadUInt8();
var interlace = ms.ReadUInt8();
var compression = ms.ReadByte();
/*var filter = */ms.ReadByte();
var interlace = ms.ReadByte();
if (compression != 0)
throw new InvalidDataException("Compression method not supported");
@@ -95,7 +95,7 @@ namespace OpenRA.FileFormats
Palette = new Color[256];
for (var i = 0; i < length / 3; i++)
{
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
var r = ms.ReadByte(); var g = ms.ReadByte(); var b = ms.ReadByte();
Palette[i] = Color.FromArgb(r, g, b);
}
@@ -108,7 +108,7 @@ namespace OpenRA.FileFormats
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
for (var i = 0; i < length; i++)
Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
Palette[i] = Color.FromArgb(ms.ReadByte(), Palette[i]);
break;
}
@@ -137,56 +137,21 @@ namespace OpenRA.FileFormats
var pxStride = PixelStride;
var rowStride = Width * pxStride;
Span<byte> prevLine = new byte[rowStride];
var prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++)
{
var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, rowStride);
var line = Data.AsSpan(y * rowStride, rowStride);
var filter = (PngFilter)ds.ReadByte();
var line = ds.ReadBytes(rowStride);
switch (filter)
{
case PngFilter.None:
break;
case PngFilter.Sub:
for (var i = pxStride; i < rowStride; i++)
line[i] += line[i - pxStride];
break;
case PngFilter.Up:
for (var i = 0; i < rowStride; i++)
line[i] += prevLine[i];
break;
case PngFilter.Average:
for (var i = 0; i < pxStride; i++)
line[i] += Average(0, prevLine[i]);
for (var i = pxStride; i < rowStride; i++)
line[i] += Average(line[i - pxStride], prevLine[i]);
break;
case PngFilter.Paeth:
for (var i = 0; i < pxStride; i++)
line[i] += Paeth(0, prevLine[i], 0);
for (var i = pxStride; i < rowStride; i++)
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break;
default:
throw new InvalidOperationException("Unsupported Filter");
}
for (var i = 0; i < rowStride; i++)
line[i] = i < pxStride
? UnapplyFilter(filter, line[i], 0, prevLine[i], 0)
: UnapplyFilter(filter, line[i], line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
Array.Copy(line, 0, Data, y * rowStride, rowStride);
prevLine = line;
}
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
}
}
@@ -263,9 +228,34 @@ namespace OpenRA.FileFormats
return isPng;
}
static byte UnapplyFilter(PngFilter f, byte x, byte a, byte b, byte c)
{
switch (f)
{
case PngFilter.None: return x;
case PngFilter.Sub: return (byte)(x + a);
case PngFilter.Up: return (byte)(x + b);
case PngFilter.Average: return (byte)(x + (a + b) / 2);
case PngFilter.Paeth: return (byte)(x + Paeth(a, b, c));
default:
throw new InvalidOperationException("Unsupported Filter");
}
}
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
[Flags]
enum PngColorType : byte { Indexed = 1, Color = 2, Alpha = 4 }
enum PngFilter : byte { None, Sub, Up, Average, Paeth }
enum PngColorType { Indexed = 1, Color = 2, Alpha = 4 }
enum PngFilter { None, Sub, Up, Average, Paeth }
static bool IsPaletted(byte bitDepth, PngColorType colorType)
{
@@ -281,7 +271,7 @@ namespace OpenRA.FileFormats
throw new InvalidDataException("Unknown pixel format");
}
static void WritePngChunk(Stream output, string type, Stream input)
void WritePngChunk(Stream output, string type, Stream input)
{
input.Position = 0;

View File

@@ -41,11 +41,7 @@ namespace OpenRA.FileSystem
public Stream GetStream(string filename)
{
var combined = Path.Combine(Name, filename);
if (!File.Exists(combined))
return null;
try { return File.OpenRead(combined); }
try { return File.OpenRead(Path.Combine(Name, filename)); }
catch { return null; }
}

View File

@@ -19,7 +19,7 @@ namespace OpenRA.FileSystem
{
public class ZipFileLoader : IPackageLoader
{
const uint ZipSignature = 0x04034b50;
static readonly uint ZipSignature = 0x04034b50;
class ReadOnlyZipFile : IReadOnlyPackage
{
@@ -190,7 +190,7 @@ namespace OpenRA.FileSystem
public void Dispose() { /* nothing to do */ }
}
sealed class StaticStreamDataSource : IStaticDataSource
class StaticStreamDataSource : IStaticDataSource
{
readonly Stream s;
public StaticStreamDataSource(Stream s)

View File

@@ -86,9 +86,8 @@ namespace OpenRA
static void JoinInner(OrderManager om)
{
// Refresh static classes before the game starts.
// Refresh TextNotificationsManager before the game starts.
TextNotificationsManager.Clear();
UnitOrders.Clear();
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining
@@ -582,7 +581,7 @@ namespace OpenRA
Log.Write("debug", "Taking screenshot " + path);
Renderer.SaveScreenshot(path);
TextNotificationsManager.Debug(TranslationProvider.GetString(SavedScreenshot, Translation.Arguments("filename", filename)));
TextNotificationsManager.Debug(ModData.Translation.GetString(SavedScreenshot, Translation.Arguments("filename", filename)));
}
}

View File

@@ -141,20 +141,20 @@ namespace OpenRA
if (unresolved.Count != 0)
{
var exceptionString = "ActorInfo(\"" + Name + "\") failed to initialize because of the following:\n";
var exceptionString = "ActorInfo(\"" + Name + "\") failed to initialize because of the following:\r\n";
var missing = unresolved.SelectMany(u => u.Dependencies.Where(d => !source.Any(s => AreResolvable(d, s.Type)))).Distinct();
exceptionString += "Missing:\n";
exceptionString += "Missing:\r\n";
foreach (var m in missing)
exceptionString += m + " \n";
exceptionString += m + " \r\n";
exceptionString += "Unresolved:\n";
exceptionString += "Unresolved:\r\n";
foreach (var u in unresolved)
{
var deps = u.Dependencies.Where(d => !resolved.Exists(r => r.Type == d));
var optDeps = u.OptionalDependencies.Where(d => !resolved.Exists(r => r.Type == d));
var allDeps = string.Join(", ", deps.Select(o => o.ToString()).Concat(optDeps.Select(o => $"[{o}]")));
exceptionString += $"{u.Type}: {{ {allDeps} }}\n";
exceptionString += $"{u.Type}: {{ {allDeps} }}\r\n";
}
throw new YamlException(exceptionString);

View File

@@ -28,14 +28,14 @@ namespace OpenRA.GameRules
Title = value.Value;
var nd = value.ToDictionary();
if (nd.TryGetValue("Hidden", out var yaml))
bool.TryParse(yaml.Value, out Hidden);
if (nd.ContainsKey("Hidden"))
bool.TryParse(nd["Hidden"].Value, out Hidden);
if (nd.TryGetValue("VolumeModifier", out yaml))
VolumeModifier = FieldLoader.GetValue<float>("VolumeModifier", yaml.Value);
if (nd.ContainsKey("VolumeModifier"))
VolumeModifier = FieldLoader.GetValue<float>("VolumeModifier", nd["VolumeModifier"].Value);
var ext = nd.TryGetValue("Extension", out yaml) ? yaml.Value : "aud";
Filename = (nd.TryGetValue("Filename", out yaml) ? yaml.Value : key) + "." + ext;
var ext = nd.ContainsKey("Extension") ? nd["Extension"].Value : "aud";
Filename = (nd.ContainsKey("Filename") ? nd["Filename"].Value : key) + "." + ext;
}
public void Load(IReadOnlyFileSystem fileSystem)

View File

@@ -244,7 +244,7 @@ namespace OpenRA
}
catch (Exception ex)
{
Log.Write("debug", "Error in AnyFlaggedTraits\n" + ex.ToString());
Log.Write("debug", "Error in AnyFlaggedTraits\r\n" + ex.ToString());
}
}
}

View File

@@ -30,7 +30,7 @@ namespace OpenRA.Graphics
bool backwards;
bool tickAlways;
int timeUntilNextFrame;
Action tickFunc;
Action tickFunc = () => { };
public Animation(World world, string name)
: this(world, name, () => WAngle.Zero) { }
@@ -164,7 +164,7 @@ namespace OpenRA.Graphics
if (frame >= CurrentSequence.Length)
{
frame = CurrentSequence.Length - 1;
tickFunc = null;
tickFunc = () => { };
after?.Invoke();
}
};
@@ -212,13 +212,13 @@ namespace OpenRA.Graphics
public void Tick(int t)
{
if (tickAlways)
tickFunc?.Invoke();
tickFunc();
else
{
timeUntilNextFrame -= t;
while (timeUntilNextFrame <= 0)
{
tickFunc?.Invoke();
tickFunc();
timeUntilNextFrame += CurrentSequenceTickOrDefault();
}
}

View File

@@ -263,13 +263,13 @@ namespace OpenRA.Graphics
if (!collections.TryGetValue(collectionName, out var collection))
{
Log.Write("debug", $"Could not find collection '{collectionName}'");
Log.Write("debug", "Could not find collection '{0}'", collectionName);
return new Size(0, 0);
}
if (collection.PanelRegion == null || collection.PanelRegion.Length != 8)
{
Log.Write("debug", $"Collection '{collectionName}' does not define a valid PanelRegion");
Log.Write("debug", "Collection '{0}' does not define a valid PanelRegion", collectionName);
return new Size(0, 0);
}

View File

@@ -17,7 +17,7 @@ namespace OpenRA.Graphics
{
public sealed class CursorManager
{
sealed class Cursor
class Cursor
{
public string Name;
public int2 PaddedSize;

View File

@@ -34,13 +34,12 @@ namespace OpenRA.Graphics
var cursorSprites = cache[cursorSrc];
Frames = cursorSprites.Skip(Start).ToArray();
if ((d.TryGetValue("Length", out var yaml) && yaml.Value == "*") ||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
if ((d.ContainsKey("Length") && d["Length"].Value == "*") || (d.ContainsKey("End") && d["End"].Value == "*"))
Length = Frames.Length;
else if (d.TryGetValue("Length", out yaml))
Length = Exts.ParseIntegerInvariant(yaml.Value);
else if (d.TryGetValue("End", out yaml))
Length = Exts.ParseIntegerInvariant(yaml.Value) - Start;
else if (d.ContainsKey("Length"))
Length = Exts.ParseIntegerInvariant(d["Length"].Value);
else if (d.ContainsKey("End"))
Length = Exts.ParseIntegerInvariant(d["End"].Value) - Start;
else
Length = 1;
@@ -52,15 +51,15 @@ namespace OpenRA.Graphics
if (Length > cursorSprites.Length)
throw new YamlException($"Cursor {name}: {nameof(Length)} is greater than the length of the sprite sequence.");
if (d.TryGetValue("X", out yaml))
if (d.ContainsKey("X"))
{
Exts.TryParseIntegerInvariant(yaml.Value, out var x);
Exts.TryParseIntegerInvariant(d["X"].Value, out var x);
Hotspot = Hotspot.WithX(x);
}
if (d.TryGetValue("Y", out yaml))
if (d.ContainsKey("Y"))
{
Exts.TryParseIntegerInvariant(yaml.Value, out var y);
Exts.TryParseIntegerInvariant(d["Y"].Value, out var y);
Hotspot = Hotspot.WithY(y);
}
}

View File

@@ -79,9 +79,6 @@ namespace OpenRA.Graphics
CopyPaletteToBuffer(index, p);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Performance", "CA1854:Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method",
Justification = "False positive - indexer is a set not a get.")]
public void ReplacePalette(string name, IPalette p)
{
if (mutablePalettes.ContainsKey(name))

View File

@@ -62,7 +62,7 @@ namespace OpenRA.Graphics
{
public Action<string> OnMissingModelError { get; set; }
sealed class PlaceholderModelCache : IModelCache
class PlaceholderModelCache : IModelCache
{
public IVertexBuffer<Vertex> VertexBuffer => throw new NotImplementedException();

View File

@@ -302,7 +302,7 @@ namespace OpenRA.Graphics
return fbo;
}
static void DisableFrameBuffer(IFrameBuffer fbo)
void DisableFrameBuffer(IFrameBuffer fbo)
{
Game.Renderer.Flush();
Game.Renderer.Context.DisableDepthBuffer();

View File

@@ -40,7 +40,7 @@ namespace OpenRA.Graphics
return new ReadOnlyPalette(palette);
}
sealed class ReadOnlyPalette : IPalette
class ReadOnlyPalette : IPalette
{
readonly IPalette palette;
public ReadOnlyPalette(IPalette palette) { this.palette = palette; }

View File

@@ -80,7 +80,7 @@ namespace OpenRA.Graphics
/// Will behave badly if the lines are parallel.
/// Z position is the average of a and b (ignores actual intersection point if it exists).
/// </summary>
static float3 IntersectionOf(in float3 a, in float3 da, in float3 b, in float3 db)
float3 IntersectionOf(in float3 a, in float3 da, in float3 b, in float3 db)
{
var crossA = a.X * (a.Y + da.Y) - a.Y * (a.X + da.X);
var crossB = b.X * (b.Y + db.Y) - b.Y * (b.X + db.X);

View File

@@ -18,7 +18,7 @@ using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public sealed class SpriteCache : IDisposable
public class SpriteCache : IDisposable
{
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
readonly ISpriteLoader[] loaders;

View File

@@ -123,7 +123,7 @@ namespace OpenRA.Graphics
}
}
static float2 Rotate(float2 v, float sina, float cosa, float2 offset)
float2 Rotate(float2 v, float sina, float cosa, float2 offset)
{
return new float2(
v.X * cosa - v.Y * sina + offset.X,
@@ -427,7 +427,7 @@ namespace OpenRA.Graphics
}
}
sealed class GlyphInfo
class GlyphInfo
{
public float Advance;
public int2 Offset;

View File

@@ -114,7 +114,7 @@ namespace OpenRA.Graphics
return new int2(sheetIndex, secondarySheetIndex);
}
static float ResolveTextureIndex(Sprite s, PaletteReference pal)
float ResolveTextureIndex(Sprite s, PaletteReference pal)
{
if (pal == null)
return 0;

View File

@@ -114,7 +114,7 @@ namespace OpenRA.Graphics
// transparent for isometric tiles
var tl = worldRenderer.TerrainLighting;
var pos = map.CenterOfCell(uv.ToCPos(map));
var step = map.Grid.TileScale / 2;
var step = map.Grid.Type == MapGridType.RectangularIsometric ? 724 : 512;
var weights = new[]
{
tl.TintAt(pos + new WVec(-step, -step, 0)),

View File

@@ -172,7 +172,7 @@ namespace OpenRA.Graphics
UpdateViewportZooms();
}
static float CalculateMinimumZoom(float minHeight, float maxHeight)
float CalculateMinimumZoom(float minHeight, float maxHeight)
{
var h = Game.Renderer.NativeResolution.Height;

View File

@@ -49,7 +49,7 @@ namespace OpenRA.Graphics
{
World = world;
TileSize = World.Map.Grid.TileSize;
TileScale = World.Map.Grid.TileScale;
TileScale = World.Map.Grid.Type == MapGridType.RectangularIsometric ? 1448 : 1024;
Viewport = new Viewport(this, world.Map);
createPaletteReference = CreatePaletteReference;
@@ -87,7 +87,7 @@ namespace OpenRA.Graphics
{
// HACK: This is working around the fact that palettes are defined on traits rather than sequences
// and can be removed once this has been fixed.
return string.IsNullOrEmpty(name) ? null : palettes.GetOrAdd(name, createPaletteReference);
return name == null ? null : palettes.GetOrAdd(name, createPaletteReference);
}
public void AddPalette(string name, ImmutablePalette pal, bool allowModifiers = false, bool allowOverwrite = false)
@@ -109,8 +109,8 @@ namespace OpenRA.Graphics
palette.ReplacePalette(name, pal);
// Update cached PlayerReference if one exists
if (palettes.TryGetValue(name, out var paletteReference))
paletteReference.Palette = pal;
if (palettes.ContainsKey(name))
palettes[name].Palette = pal;
}
public void SetPaletteColorShift(string name, float hueOffset, float satOffset, float valueModifier, float minHue, float maxHue)

View File

@@ -35,7 +35,7 @@ namespace OpenRA
foreach (var kv in settings)
{
if (definitions.TryGetValue(kv.Key, out var definition) && !definition.Readonly)
if (definitions.ContainsKey(kv.Key) && !definitions[kv.Key].Readonly)
keys[kv.Key] = kv.Value;
}
@@ -43,9 +43,6 @@ namespace OpenRA
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Performance", "CA1854:Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method",
Justification = "Func must perform a live lookup in the collection, as the lookup value can change.")]
internal Func<Hotkey> GetHotkeyReference(string name)
{
// Is this a mod-defined hotkey?

View File

@@ -13,8 +13,7 @@ using System.Collections.Generic;
namespace OpenRA
{
// List of keycodes. Duplicated from SDL 2.0.1, with the addition
// of MOUSE4 and MOUSE5.
// List of keycodes, duplicated from SDL 2.0.1
public enum Keycode
{
UNKNOWN = 0,
@@ -253,8 +252,6 @@ namespace OpenRA
KBDILLUMUP = 280 | (1 << 30),
EJECT = 281 | (1 << 30),
SLEEP = 282 | (1 << 30),
MOUSE4 = 283 | (1 << 30),
MOUSE5 = 284 | (1 << 30)
}
public static class KeycodeExts
@@ -497,8 +494,6 @@ namespace OpenRA
{ Keycode.KBDILLUMUP, "KBDIllumUp" },
{ Keycode.EJECT, "Eject" },
{ Keycode.SLEEP, "Sleep" },
{ Keycode.MOUSE4, "Mouse 4" },
{ Keycode.MOUSE5, "Mouse 5" },
};
public static string DisplayString(Keycode k)

View File

@@ -54,7 +54,7 @@ namespace OpenRA
return mods;
}
static Manifest LoadMod(string id, string path)
Manifest LoadMod(string id, string path)
{
IReadOnlyPackage package = null;
try
@@ -79,7 +79,7 @@ namespace OpenRA
return null;
}
static Dictionary<string, Manifest> GetInstalledMods(IEnumerable<string> searchPaths, IEnumerable<string> explicitPaths)
Dictionary<string, Manifest> GetInstalledMods(IEnumerable<string> searchPaths, IEnumerable<string> explicitPaths)
{
var ret = new Dictionary<string, Manifest>();
var candidates = GetCandidateMods(searchPaths)

View File

@@ -66,10 +66,8 @@ namespace OpenRA
}
catch (Exception e)
{
Console.WriteLine("Failed to load keys:");
Console.WriteLine(e);
Log.Write("debug", $"Failed to load player keypair from `{filePath}` with exception:");
Log.Write("debug", e);
Console.WriteLine("Failed to load keys: {0}", e);
Log.Write("debug", "Failed to load player keypair from `{0}` with exception: {1}", filePath, e);
}
}
@@ -93,7 +91,7 @@ namespace OpenRA
innerData = FieldLoader.Load<PlayerProfile>(yaml.Value);
if (innerData.KeyRevoked)
{
Log.Write("debug", $"Revoking key with fingerprint {Fingerprint}");
Log.Write("debug", "Revoking key with fingerprint {0}", Fingerprint);
DeleteKeypair();
}
else
@@ -104,8 +102,7 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to parse player data result with exception:");
Log.Write("debug", e);
Log.Write("debug", "Failed to parse player data result with exception: {0}", e);
innerState = LinkState.ConnectionFailed;
}
finally
@@ -139,10 +136,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to generate keypair with exception:");
Log.Write("debug", e);
Console.WriteLine("Key generation failed:");
Console.WriteLine(e);
Log.Write("debug", "Failed to generate keypair with exception: {1}", e);
Console.WriteLine("Key generation failed: {0}", e);
innerState = LinkState.Uninitialized;
}
@@ -157,10 +152,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to delete keypair with exception:");
Log.Write("debug", e);
Console.WriteLine("Key deletion failed:");
Console.WriteLine(e);
Log.Write("debug", "Failed to delete keypair with exception: {1}", e);
Console.WriteLine("Key deletion failed: {0}", e);
}
innerState = LinkState.Uninitialized;

26
OpenRA.Game/LogProxy.cs Normal file
View File

@@ -0,0 +1,26 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
namespace OpenRA
{
public interface ILog
{
void Write(string channel, string format, params object[] args);
}
public class LogProxy : ILog
{
public void Write(string channel, string format, params object[] args)
{
Log.Write(channel, format, args);
}
}
}

View File

@@ -65,7 +65,7 @@ namespace OpenRA
}
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
public sealed class Manifest : IDisposable
public class Manifest : IDisposable
{
public readonly string Id;
public readonly IReadOnlyPackage Package;
@@ -157,25 +157,25 @@ namespace OpenRA
// Allow inherited mods to import parent maps.
var compat = new List<string> { Id };
if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
if (yaml.ContainsKey("SupportsMapsFrom"))
compat.AddRange(yaml["SupportsMapsFrom"].Value.Split(',').Select(c => c.Trim()));
MapCompatibility = compat.ToArray();
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
DefaultOrderGenerator = entry.Value;
if (yaml.ContainsKey("DefaultOrderGenerator"))
DefaultOrderGenerator = yaml["DefaultOrderGenerator"].Value;
if (yaml.TryGetValue("PackageFormats", out entry))
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
if (yaml.ContainsKey("PackageFormats"))
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", yaml["PackageFormats"].Value);
if (yaml.TryGetValue("SoundFormats", out entry))
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", entry.Value);
if (yaml.ContainsKey("SoundFormats"))
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", yaml["SoundFormats"].Value);
if (yaml.TryGetValue("SpriteFormats", out entry))
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", entry.Value);
if (yaml.ContainsKey("SpriteFormats"))
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", yaml["SpriteFormats"].Value);
if (yaml.TryGetValue("VideoFormats", out entry))
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
if (yaml.ContainsKey("VideoFormats"))
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", yaml["VideoFormats"].Value);
}
public void LoadCustomData(ObjectCreator oc)

View File

@@ -22,7 +22,7 @@ namespace OpenRA
{
public interface ISuppressInitExport { }
public class ActorReference : IEnumerable<object>
public class ActorReference : IEnumerable
{
public string Type;
readonly Lazy<TypeDictionary> initDict;
@@ -104,9 +104,7 @@ namespace OpenRA
return ret;
}
public IEnumerator<object> GetEnumerator() { return initDict.Value.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
public IEnumerator GetEnumerator() { return initDict.Value.GetEnumerator(); }
public ActorReference Clone()
{

View File

@@ -24,10 +24,10 @@ namespace OpenRA
protected readonly T[] Entries;
protected readonly Rectangle Bounds;
protected CellLayerBase(Map map)
public CellLayerBase(Map map)
: this(map.Grid.Type, new Size(map.MapSize.X, map.MapSize.Y)) { }
protected CellLayerBase(MapGridType gridType, Size size)
public CellLayerBase(MapGridType gridType, Size size)
{
Size = size;
Bounds = new Rectangle(0, 0, Size.Width, Size.Height);

View File

@@ -65,7 +65,7 @@ namespace OpenRA
MissionSelector = 4
}
sealed class MapField
class MapField
{
enum Type { Normal, NodeList, MiniYaml }
readonly FieldInfo field;
@@ -149,7 +149,7 @@ namespace OpenRA
}
}
public sealed class Map : IReadOnlyFileSystem, IDisposable
public class Map : IReadOnlyFileSystem, IDisposable
{
public const int SupportedMapFormat = 11;
public const int CurrentMapFormat = 12;
@@ -167,11 +167,11 @@ namespace OpenRA
new MapField("Bounds"),
new MapField("Visibility"),
new MapField("Categories"),
new MapField("Translations", required: false, ignoreIfValue: ""),
new MapField("LockPreview", required: false, ignoreIfValue: "False"),
new MapField("Players", "PlayerDefinitions"),
new MapField("Actors", "ActorDefinitions"),
new MapField("Rules", "RuleDefinitions", required: false),
new MapField("Translations", "TranslationDefinitions", required: false),
new MapField("Sequences", "SequenceDefinitions", required: false),
new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
new MapField("Weapons", "WeaponDefinitions", required: false),
@@ -193,6 +193,7 @@ namespace OpenRA
public Rectangle Bounds;
public MapVisibility Visibility = MapVisibility.Lobby;
public string[] Categories = { "Conquest" };
public string[] Translations;
public int2 MapSize { get; private set; }
@@ -202,7 +203,6 @@ namespace OpenRA
// Custom map yaml. Public for access by the map importers and lint checks
public readonly MiniYaml RuleDefinitions;
public readonly MiniYaml TranslationDefinitions;
public readonly MiniYaml SequenceDefinitions;
public readonly MiniYaml ModelSequenceDefinitions;
public readonly MiniYaml WeaponDefinitions;
@@ -256,6 +256,8 @@ namespace OpenRA
CellLayer<byte> projectedHeight;
Rectangle projectionSafeBounds;
internal Translation Translation;
public static string ComputeUID(IReadOnlyPackage package)
{
return ComputeUID(package, GetMapFormat(package));
@@ -441,14 +443,14 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", $"Failed to load rules for {Title} with error");
Log.Write("debug", e);
Log.Write("debug", "Failed to load rules for {0} with error {1}", Title, e);
InvalidCustomRules = true;
InvalidCustomRulesException = e;
Rules = Ruleset.LoadDefaultsForTileSet(modData, Tileset);
}
Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
Translation = new Translation(Game.Settings.Player.Language, Translations, this);
var tl = new MPos(0, 0).ToCPos(this);
var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this);
@@ -748,7 +750,7 @@ namespace OpenRA
{
var actorTypes = Rules.Actors.Values.Where(a => a.HasTraitInfo<IMapPreviewSignatureInfo>());
var actors = ActorDefinitions.Where(a => actorTypes.Any(ai => ai.Name == a.Value.Value));
var positions = new List<(MPos Uv, Color Color)>();
var positions = new List<(MPos Position, Color Color)>();
foreach (var actor in actors)
{
var s = new ActorReference(actor.Value.Value, actor.Value.ToDictionary());
@@ -805,17 +807,14 @@ namespace OpenRA
var minimapData = new byte[stride * height];
(Color Left, Color Right) terrainColor = default;
var colorsByPosition = positions
.GroupBy(p => p.Uv)
.ToDictionary(g => g.Key, g => g.First().Color);
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var uv = new MPos(x + Bounds.Left, y + top);
// TryGetValue will return Color.Transparent if not found
colorsByPosition.TryGetValue(uv, out var actorColor);
// FirstOrDefault will return a (MPos.Zero, Color.Transparent) if positions is empty
var actorColor = positions.FirstOrDefault(ap => ap.Position == uv).Color;
if (actorColor.A == 0)
terrainColor = GetTerrainColorPair(uv);
@@ -1208,7 +1207,7 @@ namespace OpenRA
// This shouldn't happen. But if it does, return the original value and hope the caller doesn't explode.
if (unProjected.Count == 0)
{
Log.Write("debug", $"Failed to clamp map cell {uv} to map bounds");
Log.Write("debug", "Failed to clamp map cell {0} to map bounds", uv);
return uv;
}
}
@@ -1280,7 +1279,7 @@ namespace OpenRA
// This shouldn't happen. But if it does, return the original value and hope the caller doesn't explode.
if (unProjected.Count == 0)
{
Log.Write("debug", $"Failed to find closest edge for map cell {uv}");
Log.Write("debug", "Failed to find closest edge for map cell {0}", uv);
return uv;
}
}
@@ -1411,6 +1410,14 @@ namespace OpenRA
return false;
}
public string Translate(string key, IDictionary<string, object> args = null)
{
if (Translation.TryGetString(key, out var message, args))
return message;
return modData.Translation.GetString(key, args);
}
public void Dispose()
{
Sequences.Dispose();

View File

@@ -123,12 +123,10 @@ namespace OpenRA
mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification));
}
// PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML.
var modDataRules = modData.GetRulesYaml();
foreach (var kv in MapLocations)
{
foreach (var map in kv.Key.Contents)
LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules);
LoadMap(map, kv.Key, kv.Value, mapGrid, null);
}
// We only want to track maps in runtime, not at loadtime
@@ -136,11 +134,6 @@ namespace OpenRA
}
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap)
{
LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
}
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, IEnumerable<List<MiniYamlNode>> modDataRules)
{
IReadOnlyPackage mapPackage = null;
try
@@ -151,7 +144,7 @@ namespace OpenRA
if (mapPackage != null)
{
var uid = Map.ComputeUID(mapPackage);
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type);
if (oldMap != uid)
{
@@ -165,12 +158,10 @@ namespace OpenRA
catch (Exception e)
{
mapPackage?.Dispose();
Console.WriteLine($"Failed to load map: {map}");
Console.WriteLine("Details:");
Console.WriteLine(e);
Log.Write("debug", $"Failed to load map: {map}");
Log.Write("debug", "Details:");
Log.Write("debug", e);
Console.WriteLine("Failed to load map: {0}", map);
Console.WriteLine("Details: {0}", e);
Log.Write("debug", "Failed to load map: {0}", map);
Log.Write("debug", "Details: {0}", e);
}
}
@@ -262,9 +253,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Remote map query failed with error:");
Log.Write("debug", e);
Log.Write("debug", $"URL was: {url}");
Log.Write("debug", "Remote map query failed with error: {0}", e);
Log.Write("debug", "URL was: {0}", url);
foreach (var uid in batchUids)
{
@@ -325,8 +315,7 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to load minimap with exception:");
Log.Write("debug", e);
Log.Write("debug", "Failed to load minimap with exception: {0}", e);
}
});
}

View File

@@ -128,14 +128,10 @@ namespace OpenRA
internal readonly CVec[][] TilesByDistance;
public int TileScale { get; }
public MapGrid(MiniYaml yaml)
{
FieldLoader.Load(this, yaml);
TileScale = Type == MapGridType.RectangularIsometric ? 1448 : 1024;
// The default subcell index defaults to the middle entry
var defaultSubCellIndex = (byte)DefaultSubCell;
if (defaultSubCellIndex == byte.MaxValue)

View File

@@ -61,10 +61,10 @@ namespace OpenRA
public readonly int mapformat;
}
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
public class MapPreview : IDisposable, IReadOnlyFileSystem
{
/// <summary>Wrapper that enables map data to be replaced in an atomic fashion.</summary>
sealed class InnerData
class InnerData
{
public int MapFormat;
public string Title;
@@ -90,7 +90,6 @@ namespace OpenRA
public MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions;
public Translation Translation { get; private set; }
public ActorInfo WorldActorInfo { get; private set; }
public ActorInfo PlayerActorInfo { get; private set; }
@@ -111,7 +110,7 @@ namespace OpenRA
return key == "world" || key == "player";
}
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, IEnumerable<List<MiniYamlNode>> modDataRules)
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml)
{
RuleDefinitions = LoadRuleSection(yaml, "Rules");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
@@ -121,27 +120,20 @@ namespace OpenRA
SequenceDefinitions = LoadRuleSection(yaml, "Sequences");
ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences");
Translation = yaml.TryGetValue("Translations", out var node) && node != null
? new Translation(Game.Settings.Player.Language, FieldLoader.GetValue<string[]>("value", node.Value), fileSystem)
: null;
try
{
// PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time
// This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^)
if (RuleDefinitions != null)
{
modDataRules ??= modData.GetRulesYaml();
var files = Enumerable.Empty<string>();
var files = modData.Manifest.Rules.AsEnumerable();
if (RuleDefinitions.Value != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
files = files.Append(mapFiles);
}
var sources =
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()));
var sources = files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList());
if (RuleDefinitions.Nodes.Count > 0)
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
@@ -153,8 +145,7 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", $"Failed to load rules for `{Title}` with error:");
Log.Write("debug", e);
Log.Write("debug", "Failed to load rules for `{0}` with error: {1}", Title, e.Message);
}
WorldActorInfo = modData.DefaultRules.Actors[SystemActors.World];
@@ -203,19 +194,6 @@ namespace OpenRA
public long DownloadBytes { get; private set; }
public int DownloadPercentage { get; private set; }
/// <summary>
/// Functionality mirrors <see cref="TranslationProvider.GetString"/>, except instead of using
/// loaded <see cref="Map"/>'s translations as backup, we use this <see cref="MapPreview"/>'s.
/// </summary>
public string GetLocalisedString(string key, IDictionary<string, object> args = null)
{
// PERF: instead of loading mod level Translation per each MapPreview, reuse the already loaded one in TranslationProvider.
if (TranslationProvider.TryGetModString(key, out var message, args))
return message;
return innerData.Translation?.GetString(key, args) ?? key;
}
Sprite minimap;
bool generatingMinimap;
public Sprite GetMinimap()
@@ -315,17 +293,16 @@ namespace OpenRA
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{
{ "Rules", map.RuleDefinitions },
{ "Translations", map.TranslationDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
{ "Notifications", map.NotificationDefinitions },
{ "Sequences", map.SequenceDefinitions },
{ "ModelSequences", map.ModelSequenceDefinitions }
}, null);
});
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType)
{
Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml"))
@@ -415,7 +392,7 @@ namespace OpenRA
newData.Status = MapStatus.Unavailable;
}
newData.SetCustomRules(modData, this, yaml, modDataRules);
newData.SetCustomRules(modData, this, yaml);
if (cache.LoadPreviewImages && p.Contains("map.png"))
using (var dataStream = p.GetStream("map.png"))
@@ -467,8 +444,7 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver minimap response:");
Log.Write("debug", e);
Log.Write("debug", "Failed parsing mapserver minimap response: {0}", e);
newData.Preview = null;
}
}
@@ -478,12 +454,11 @@ namespace OpenRA
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml, null);
newData.SetCustomRules(modData, this, rulesYaml);
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver response:");
Log.Write("debug", e);
Log.Write("debug", "Failed parsing mapserver response: {0}", e);
}
// Commit updated data before running the callbacks
@@ -550,21 +525,20 @@ namespace OpenRA
await response.ReadAsStreamWithProgress(fileStream, OnDownloadProgress, CancellationToken.None);
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
Log.Write("debug", $"Downloaded map to '{mapFilename}'");
Log.Write("debug", "Downloaded map to '{0}'", mapFilename);
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (package == null)
innerData.Status = MapStatus.DownloadError;
else
{
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType);
Game.RunAfterTick(onSuccess);
}
}
catch (Exception e)
{
Log.Write("debug", "Map installation failed with error:");
Log.Write("debug", e);
Log.Write("debug", "Map installation failed with error: {0}", e);
innerData.Status = MapStatus.DownloadError;
}
});

View File

@@ -69,7 +69,7 @@ namespace OpenRA
// HACK: Temporary placeholder to avoid having to change all the traits that reference this constant.
// This can be removed after the palette references have been moved from traits to sequences.
public static class TileSet
public class TileSet
{
public const string TerrainPaletteInternalName = "terrain";
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
@@ -39,19 +38,11 @@ namespace OpenRA
}
}
public sealed class MiniYamlNode
public class MiniYamlNode
{
public readonly struct SourceLocation
public struct SourceLocation
{
public readonly string Filename;
public readonly int Line;
public SourceLocation(string filename, int line)
{
Filename = filename;
Line = line;
}
public string Filename; public int Line;
public override string ToString() { return $"{Filename}:{Line}"; }
}
@@ -96,7 +87,7 @@ namespace OpenRA
}
}
public sealed class MiniYaml
public class MiniYaml
{
const int SpacesPerLevel = 4;
static readonly Func<string, string> StringIdentity = s => s;
@@ -130,8 +121,14 @@ namespace OpenRA
{
var key = keySelector(y.Key);
var element = elementSelector(y.Value);
if (!ret.TryAdd(key, element))
throw new InvalidDataException($"Duplicate key '{y.Key}' in {y.Location}");
try
{
ret.Add(key, element);
}
catch (ArgumentException ex)
{
throw new InvalidDataException($"Duplicate key '{y.Key}' in {y.Location}", ex);
}
}
return ret;
@@ -149,7 +146,7 @@ namespace OpenRA
public static List<MiniYamlNode> NodesOrEmpty(MiniYaml y, string s)
{
var nd = y.ToDictionary();
return nd.TryGetValue(s, out var v) ? v.Nodes : new List<MiniYamlNode>();
return nd.ContainsKey(s) ? nd[s].Nodes : new List<MiniYamlNode>();
}
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
@@ -175,7 +172,7 @@ namespace OpenRA
ReadOnlySpan<char> key = default;
ReadOnlySpan<char> value = default;
ReadOnlySpan<char> comment = default;
var location = new MiniYamlNode.SourceLocation(filename, lineNo);
var location = new MiniYamlNode.SourceLocation { Filename = filename, Line = lineNo };
if (line.Length > 0)
{
@@ -315,12 +312,10 @@ namespace OpenRA
public static List<MiniYamlNode> Merge(IEnumerable<List<MiniYamlNode>> sources)
{
var sourcesList = sources.ToList();
if (sourcesList.Count == 0)
if (!sources.Any())
return new List<MiniYamlNode>();
var tree = sourcesList
.Where(s => s != null)
var tree = sources.Where(s => s != null)
.Select(MergeSelfPartial)
.Aggregate(MergePartial)
.Where(n => n.Key != null)
@@ -329,35 +324,39 @@ namespace OpenRA
var resolved = new Dictionary<string, MiniYaml>(tree.Count);
foreach (var kv in tree)
{
// Inheritance is tracked from parent->child, but not from child->parentsiblings.
var inherited = ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty.Add(kv.Key, default);
var inherited = new Dictionary<string, MiniYamlNode.SourceLocation>
{
{ kv.Key, default }
};
var children = ResolveInherits(kv.Value, tree, inherited);
resolved.Add(kv.Key, new MiniYaml(kv.Value.Value, children));
}
// Resolve any top-level removals (e.g. removing whole actor blocks)
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
return ResolveInherits(nodes, tree, new Dictionary<string, MiniYamlNode.SourceLocation>());
}
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes,
Dictionary<string, MiniYaml> tree, Dictionary<string, MiniYamlNode.SourceLocation> inherited)
{
if (existingNodeKeys.Add(overrideNode.Key))
{
existingNodes.Add(overrideNode.Clone());
return;
}
var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
if (existingNode != null)
{
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
}
else
existingNodes.Add(overrideNode.Clone());
}
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, Dictionary<string, MiniYamlNode.SourceLocation> inherited)
{
var resolved = new List<MiniYamlNode>(node.Nodes.Count);
var resolvedKeys = new HashSet<string>(node.Nodes.Count);
// Inheritance is tracked from parent->child, but not from child->parentsiblings.
inherited = new Dictionary<string, MiniYamlNode.SourceLocation>(inherited);
foreach (var n in node.Nodes)
{
@@ -367,27 +366,21 @@ namespace OpenRA
throw new YamlException(
$"{n.Location}: Parent type `{n.Value.Value}` not found");
try
{
inherited = inherited.Add(n.Value.Value, n.Location);
}
catch (ArgumentException)
{
if (inherited.ContainsKey(n.Value.Value))
throw new YamlException($"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
}
inherited.Add(n.Value.Value, n.Location);
foreach (var r in ResolveInherits(parent, tree, inherited))
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
MergeIntoResolved(r, resolved, tree, inherited);
}
else if (n.Key.StartsWith("-", StringComparison.Ordinal))
{
var removed = n.Key[1..];
if (resolved.RemoveAll(r => r.Key == removed) == 0)
throw new YamlException($"{n.Location}: There are no elements with key `{removed}` to remove");
resolvedKeys.Remove(removed);
}
else
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
MergeIntoResolved(n, resolved, tree, inherited);
}
resolved.TrimExcess();
@@ -420,9 +413,6 @@ namespace OpenRA
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
{
existingNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
overrideNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
if (existingNodes == null)
return overrideNodes;
@@ -441,45 +431,21 @@ namespace OpenRA
return existingNodes;
var ret = new List<MiniYamlNode>(existingNodes.Count + overrideNodes.Count);
var plainKeys = new HashSet<string>(existingNodes.Count + overrideNodes.Count);
foreach (var node in existingNodes)
MergeNode(node);
foreach (var node in overrideNodes)
MergeNode(node);
var existingDict = existingNodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
var overrideDict = overrideNodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
var allKeys = existingDict.Keys.Union(overrideDict.Keys);
void MergeNode(MiniYamlNode node)
foreach (var key in allKeys)
{
// Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith("-", StringComparison.Ordinal))
{
ret.Add(node);
return;
}
existingDict.TryGetValue(key, out var existingNode);
overrideDict.TryGetValue(key, out var overrideNode);
// If no previous node with this key is present, it is new and can just be appended.
if (plainKeys.Add(node.Key))
{
ret.Add(node);
return;
}
// A Removal node is closer than the previous node.
// We should not merge the new node, as the data being merged will jump before the Removal.
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
var removalKey = $"-{node.Key}";
var previousNodeIndex = ret.FindLastIndex(n => n.Key == node.Key);
var previousRemovalNodeIndex = ret.FindLastIndex(n => n.Key == removalKey);
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
{
ret.Add(node);
return;
}
// A previous node is present with no intervening Removal.
// We should merge the new one into it, in place.
ret[previousNodeIndex] = new MiniYamlNode(node.Key, MergePartial(ret[previousNodeIndex].Value, node.Value), node.Comment, node.Location);
var loc = overrideNode?.Location ?? default;
var comment = (overrideNode ?? existingNode).Comment;
var merged = (existingNode == null || overrideNode == null) ? overrideNode ?? existingNode :
new MiniYamlNode(key, MergePartial(existingNode.Value, overrideNode.Value), comment, loc);
ret.Add(merged);
}
ret.TrimExcess();

View File

@@ -36,6 +36,7 @@ namespace OpenRA
public readonly IModelSequenceLoader ModelSequenceLoader;
public readonly IVideoLoader[] VideoLoaders;
public readonly HotkeyManager Hotkeys;
public readonly Translation Translation;
public ILoadScreen LoadScreen { get; }
public CursorProvider CursorProvider { get; private set; }
public FS ModFiles;
@@ -101,6 +102,8 @@ namespace OpenRA
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
Translation = new Translation(Game.Settings.Player.Language, Manifest.Translations, DefaultFileSystem);
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
defaultTerrainInfo = Exts.Lazy(() =>
{
@@ -134,7 +137,6 @@ namespace OpenRA
// horribly when you use ModData in unexpected ways.
ChromeMetrics.Initialize(this);
ChromeProvider.Initialize(this);
TranslationProvider.Initialize(this, fileSystem);
Game.Sound.Initialize(SoundLoaders, fileSystem);
@@ -166,11 +168,6 @@ namespace OpenRA
return map;
}
public List<MiniYamlNode>[] GetRulesYaml()
{
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s)).ToArray();
}
public void Dispose()
{
LoadScreen?.Dispose();

View File

@@ -284,40 +284,39 @@ namespace OpenRA.Network
// - File offset of metadata start marker
// - File offset of custom trait data
// - Metadata end marker
using (var file = File.Create(path))
{
ordersStream.Seek(0, SeekOrigin.Begin);
ordersStream.CopyTo(file);
file.Write(BitConverter.GetBytes(MetadataMarker), 0, 4);
file.Write(BitConverter.GetBytes(LastOrdersFrame), 0, 4);
file.Write(BitConverter.GetBytes(LastSyncFrame), 0, 4);
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);
var file = File.Create(path);
var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
file.WriteString(Encoding.UTF8, globalSettingsNodes.WriteToString());
ordersStream.Seek(0, SeekOrigin.Begin);
ordersStream.CopyTo(file);
file.Write(BitConverter.GetBytes(MetadataMarker), 0, 4);
file.Write(BitConverter.GetBytes(LastOrdersFrame), 0, 4);
file.Write(BitConverter.GetBytes(LastSyncFrame), 0, 4);
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);
var slotNodes = Slots
.Select(s => s.Value.Serialize())
.ToList();
file.WriteString(Encoding.UTF8, slotNodes.WriteToString());
var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
file.WriteString(Encoding.UTF8, globalSettingsNodes.WriteToString());
var slotClientNodes = SlotClients
.Select(s => s.Value.Serialize(s.Key))
.ToList();
file.WriteString(Encoding.UTF8, slotClientNodes.WriteToString());
var slotNodes = Slots
.Select(s => s.Value.Serialize())
.ToList();
file.WriteString(Encoding.UTF8, slotNodes.WriteToString());
var traitDataOffset = file.Length;
file.Write(BitConverter.GetBytes(TraitDataMarker), 0, 4);
var slotClientNodes = SlotClients
.Select(s => s.Value.Serialize(s.Key))
.ToList();
file.WriteString(Encoding.UTF8, slotClientNodes.WriteToString());
var traitDataNodes = TraitData
.Select(kv => new MiniYamlNode(kv.Key.ToString(), kv.Value))
.ToList();
file.WriteString(Encoding.UTF8, traitDataNodes.WriteToString());
var traitDataOffset = file.Length;
file.Write(BitConverter.GetBytes(TraitDataMarker), 0, 4);
file.Write(BitConverter.GetBytes(ordersStream.Length), 0, 4);
file.Write(BitConverter.GetBytes(traitDataOffset), 0, 4);
file.Write(BitConverter.GetBytes(EOFMarker), 0, 4);
}
var traitDataNodes = TraitData
.Select(kv => new MiniYamlNode(kv.Key.ToString(), kv.Value))
.ToList();
file.WriteString(Encoding.UTF8, traitDataNodes.WriteToString());
file.Write(BitConverter.GetBytes(ordersStream.Length), 0, 4);
file.Write(BitConverter.GetBytes(traitDataOffset), 0, 4);
file.Write(BitConverter.GetBytes(EOFMarker), 0, 4);
}
}
}

View File

@@ -19,9 +19,9 @@ using ICSharpCode.SharpZipLib.Zip;
namespace OpenRA.Network
{
public static class GeoIP
public class GeoIP
{
sealed class IP2LocationReader
class IP2LocationReader
{
public readonly DateTime Date;
readonly Stream stream;
@@ -128,8 +128,7 @@ namespace OpenRA.Network
}
catch (Exception e)
{
Log.Write("geoip", "DatabaseReader failed:");
Log.Write("geoip", e);
Log.Write("geoip", "DatabaseReader failed: {0}", e);
}
}
@@ -143,8 +142,7 @@ namespace OpenRA.Network
}
catch (Exception e)
{
Log.Write("geoip", "LookupCountry failed:");
Log.Write("geoip", e);
Log.Write("geoip", "LookupCountry failed: {0}", e);
}
}

View File

@@ -76,7 +76,7 @@ namespace OpenRA.Network
return arguments.ToArray();
}
public LocalizedMessage(MiniYaml yaml)
public LocalizedMessage(ModData modData, MiniYaml yaml)
{
// Let the FieldLoader do the dirty work of loading the public fields.
FieldLoader.Load(this, yaml);
@@ -95,7 +95,7 @@ namespace OpenRA.Network
argumentDictionary.Add(argument.Key, argument.Value);
}
TranslatedText = TranslationProvider.GetString(Key, argumentDictionary);
TranslatedText = modData.Translation.GetString(Key, argumentDictionary);
}
public static string Serialize(string key, Dictionary<string, object> arguments = null)

View File

@@ -17,7 +17,7 @@ namespace OpenRA.Network
{
public enum NatStatus { Enabled, Disabled, NotSupported }
public static class Nat
public class Nat
{
public static NatStatus Status => NatUtility.IsSearching ? natDevice != null ? NatStatus.Enabled : NatStatus.NotSupported : NatStatus.Disabled;
@@ -49,8 +49,8 @@ namespace OpenRA.Network
// Only interact with one at a time. Some support both UPnP and NAT-PMP.
natDevice = args.Device;
Log.Write("nat", $"Device found: {natDevice.DeviceEndpoint}");
Log.Write("nat", $"Type: {natDevice.NatProtocol}");
Log.Write("nat", "Device found: {0}", natDevice.DeviceEndpoint);
Log.Write("nat", "Type: {0}", natDevice.NatProtocol);
}
finally
{

View File

@@ -209,7 +209,7 @@ namespace OpenRA
default:
{
Log.Write("debug", $"Received unknown order with type {type}");
Log.Write("debug", "Received unknown order with type {0}", type);
return null;
}
}
@@ -217,7 +217,7 @@ namespace OpenRA
catch (Exception e)
{
Log.Write("debug", "Caught exception while processing order");
Log.Write("debug", e);
Log.Write("debug", e.ToString());
// HACK: this can hopefully go away in the future
TextNotificationsManager.Debug("Ignoring malformed order that would have crashed the game");

View File

@@ -18,7 +18,7 @@ namespace OpenRA.Network
{
public sealed class ReplayConnection : IConnection
{
sealed class Chunk
class Chunk
{
public int Frame;
public (int ClientId, byte[] Packet)[] Packets;

View File

@@ -19,7 +19,7 @@ using OpenRA.Primitives;
namespace OpenRA.Network
{
sealed class SyncReport
class SyncReport
{
const int NumSyncReports = 7;
static readonly Cache<Type, TypeInfo> TypeInfoCache = new(t => new TypeInfo(t));
@@ -119,12 +119,12 @@ namespace OpenRA.Network
{
desyncFrameFound = true;
var mod = Game.ModData.Manifest.Metadata;
Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})");
Log.Write("sync", "Player: {0} ({1} {2} {3})", Game.Settings.Player.Name, Platform.CurrentPlatform, Environment.OSVersion, Platform.RuntimeVersion);
if (Game.IsHost)
Log.Write("sync", "Player is host.");
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.Title} at Version {mod.Version})");
Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
Log.Write("sync", "Game ID: {0} (Mod: {1} at Version {2})", orderManager.LobbyInfo.GlobalSettings.GameUid, mod.Title, mod.Version);
Log.Write("sync", "Sync for net frame {0} -------------", r.Frame);
Log.Write("sync", "SharedRandom: {0} (#{1})", r.SyncedRandom, r.TotalCount);
Log.Write("sync", "Synced Traits:");
foreach (var a in r.Traits)
{
@@ -139,7 +139,7 @@ namespace OpenRA.Network
Log.Write("sync", "Synced Effects:");
foreach (var e in r.Effects)
{
Log.Write("sync", $"\t {e.Name} ({e.Hash})");
Log.Write("sync", "\t {0} ({1})", e.Name, e.Hash);
var nvp = e.NamesValues;
for (var i = 0; i < nvp.Names.Length; i++)
@@ -149,7 +149,7 @@ namespace OpenRA.Network
Log.Write("sync", "Orders Issued:");
foreach (var o in r.Orders)
Log.Write("sync", $"\t {o}");
Log.Write("sync", "\t {0}", o.ToString());
}
}
@@ -161,7 +161,7 @@ namespace OpenRA.Network
Log.Write("sync", $"Recorded frames do not contain the frame {frame}. No sync report available!");
}
sealed class Report
class Report
{
public int Frame;
public int SyncedRandom;

View File

@@ -20,15 +20,11 @@ namespace OpenRA.Network
{
public const int ChatMessageMaxLength = 2500;
public static int? KickVoteTarget { get; internal set; }
static Player FindPlayerByClient(this World world, Session.Client c)
{
return world.Players.FirstOrDefault(p => p.ClientIndex == c.Index && p.PlayerReference.Playable);
}
static bool OrderNotFromServerOrWorldIsReplay(int clientId, World world) => clientId != 0 || (world != null && world.IsReplay);
internal static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
{
switch (order.OrderString)
@@ -47,7 +43,7 @@ namespace OpenRA.Network
var yaml = MiniYaml.FromString(order.TargetString);
foreach (var node in yaml)
{
var localizedMessage = new LocalizedMessage(node.Value);
var localizedMessage = new LocalizedMessage(Game.ModData, node.Value);
TextNotificationsManager.AddSystemLine(localizedMessage.TranslatedText);
}
@@ -56,7 +52,9 @@ namespace OpenRA.Network
case "DisableChatEntry":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
// Order must originate from the server
// Don't disable chat in replays
if (clientId != 0 || (world != null && world.IsReplay))
break;
// Server may send MaxValue to indicate that it is disabled until further notice
@@ -68,26 +66,6 @@ namespace OpenRA.Network
break;
}
case "StartKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
KickVoteTarget = (int)order.ExtraData;
break;
}
case "EndKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
if (KickVoteTarget == (int)order.ExtraData)
KickVoteTarget = null;
break;
}
case "Chat":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
@@ -387,10 +365,5 @@ namespace OpenRA.Network
if (world.OrderValidators.All(vo => vo.OrderValidation(orderManager, world, clientId, order)))
order.Subject.ResolveOrder(order);
}
public static void Clear()
{
KickVoteTarget = null;
}
}
}

View File

@@ -49,15 +49,13 @@ namespace OpenRA
assemblies = assemblyList.SelectMany(asm => asm.GetNamespaces().Select(ns => (asm, ns))).ToArray();
}
static void LoadAssembly(List<Assembly> assemblyList, string resolvedPath)
void LoadAssembly(List<Assembly> assemblyList, string resolvedPath)
{
// .NET doesn't provide any way of querying the metadata of an assembly without either:
// (a) loading duplicate data into the application domain, breaking the world.
// (b) crashing if the assembly has already been loaded.
// We can't check the internal name of the assembly, so we'll work off the data instead
string hash;
using (var stream = File.OpenRead(resolvedPath))
hash = CryptoUtil.SHA1Hash(stream);
var hash = CryptoUtil.SHA1Hash(File.ReadAllBytes(resolvedPath));
if (!ResolvedAssemblies.TryGetValue(hash, out var assembly))
{

View File

@@ -6,12 +6,9 @@
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="6.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(MSBuildRuntimeType)'=='Mono'">
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Linguini.Bundle" Version="0.5.0" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.22" />
<PackageReference Include="Linguini.Bundle" Version="0.4.0" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.20" />
<PackageReference Include="Mono.NAT" Version="3.0.4" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />

View File

@@ -79,72 +79,6 @@ namespace OpenRA
}
}
public static string OperatingSystem
{
get
{
if (CurrentPlatform == PlatformType.Linux)
{
var desktopType = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP");
var sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE");
string suffix;
if (!string.IsNullOrEmpty(desktopType) && !string.IsNullOrEmpty(sessionType))
suffix = $" ({desktopType};{sessionType})";
else if (!string.IsNullOrEmpty(desktopType))
suffix = $" ({desktopType})";
else if (!string.IsNullOrEmpty(sessionType))
suffix = $" ({sessionType})";
else
suffix = "";
try
{
var psi = new ProcessStartInfo("hostnamectl", "status")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var p = Process.Start(psi);
string line;
while ((line = p.StandardOutput.ReadLine()) != null)
if (line.StartsWith("Operating System: "))
return line[18..] + suffix;
}
catch { }
if (File.Exists("/etc/os-release"))
foreach (var line in File.ReadLines("/etc/os-release"))
if (line.StartsWith("PRETTY_NAME="))
return line[13..^1] + suffix;
}
else if (CurrentPlatform == PlatformType.OSX)
{
try
{
var psi = new ProcessStartInfo("system_profiler", "SPSoftwareDataType")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var p = Process.Start(psi);
string line;
while ((line = p.StandardOutput.ReadLine()) != null)
{
line = line.Trim();
if (line.StartsWith("System Version: "))
return line[16..];
}
}
catch { }
}
return Environment.OSVersion.ToString();
}
}
/// <summary>
/// Directory containing user-specific support files (settings, maps, replays, game data, etc).
/// </summary>

View File

@@ -216,7 +216,7 @@ namespace OpenRA
{
var logic = PlayerActor.TraitsImplementing<IBot>().FirstOrDefault(b => b.Info.Type == BotType);
if (logic == null)
Log.Write("debug", $"Invalid bot type: {BotType}");
Log.Write("debug", "Invalid bot type: {0}", BotType);
else
logic.Activate(this);
}

View File

@@ -56,27 +56,17 @@ namespace OpenRA.Primitives
int Index(DelayedAction action)
{
// Returns the index of the next action with a strictly greater time.
var index = actions.BinarySearch(action, DelayedAction.TimeComparer);
var index = actions.BinarySearch(action);
if (index < 0)
return ~index;
while (index < actions.Count && DelayedAction.TimeComparer.Compare(action, actions[index]) >= 0)
while (index < actions.Count && action.CompareTo(actions[index]) >= 0)
index++;
return index;
}
}
readonly struct DelayedAction
readonly struct DelayedAction : IComparable<DelayedAction>
{
sealed class DelayedActionTimeComparer : IComparer<DelayedAction>
{
public int Compare(DelayedAction x, DelayedAction y)
{
return x.Time.CompareTo(y.Time);
}
}
public static IComparer<DelayedAction> TimeComparer = new DelayedActionTimeComparer();
public readonly long Time;
public readonly Action Action;
@@ -86,6 +76,11 @@ namespace OpenRA.Primitives
Time = time;
}
public int CompareTo(DelayedAction other)
{
return Time.CompareTo(other.Time);
}
public override string ToString()
{
return "Time: " + Time + " Action: " + Action;

View File

@@ -168,13 +168,13 @@ namespace OpenRA.Primitives
return false;
byte alpha = 255;
if (!byte.TryParse(value.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var red)
|| !byte.TryParse(value.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var green)
|| !byte.TryParse(value.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var blue))
if (!byte.TryParse(value[0..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var red)
|| !byte.TryParse(value[2..4], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var green)
|| !byte.TryParse(value[4..6], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var blue))
return false;
if (value.Length == 8
&& !byte.TryParse(value.AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out alpha))
&& !byte.TryParse(value[6..8], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out alpha))
return false;
color = FromArgb(alpha, red, green, blue);

View File

@@ -30,7 +30,7 @@ namespace OpenRA.Primitives
itemBoundsBins = Exts.MakeArray(rows * cols, _ => new Dictionary<T, Rectangle>());
}
static void ValidateBounds(T actor, Rectangle bounds)
void ValidateBounds(T actor, Rectangle bounds)
{
if (bounds.Width == 0 || bounds.Height == 0)
throw new ArgumentException($"Bounds of actor {actor} are empty.", nameof(bounds));

View File

@@ -16,7 +16,7 @@ using System.Linq;
namespace OpenRA.Primitives
{
public class TypeDictionary : IEnumerable<object>
public class TypeDictionary : IEnumerable
{
static readonly Func<Type, List<object>> CreateList = type => new List<object>();
readonly Dictionary<Type, List<object>> data = new();
@@ -101,20 +101,14 @@ namespace OpenRA.Primitives
public void TrimExcess()
{
data.TrimExcess();
foreach (var objs in data.Values)
objs.TrimExcess();
}
public IEnumerator<object> GetEnumerator()
public IEnumerator GetEnumerator()
{
return WithInterface<object>().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public static class TypeExts

View File

@@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Eluant;
@@ -32,7 +33,6 @@ namespace OpenRA.Scripting
}
// For traitinfos that provide actor / player commands
[AttributeUsage(AttributeTargets.Class)]
public sealed class ScriptPropertyGroupAttribute : Attribute
{
public readonly string Category;
@@ -40,18 +40,15 @@ namespace OpenRA.Scripting
}
// For property groups that are safe to initialize invoke on destroyed actors
[AttributeUsage(AttributeTargets.Class)]
public sealed class ExposedForDestroyedActors : Attribute { }
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
public sealed class ScriptActorPropertyActivityAttribute : Attribute { }
public abstract class ScriptActorProperties
{
protected readonly Actor Self;
protected readonly ScriptContext Context;
protected ScriptActorProperties(ScriptContext context, Actor self)
public ScriptActorProperties(ScriptContext context, Actor self)
{
Self = self;
Context = context;
@@ -62,8 +59,7 @@ namespace OpenRA.Scripting
{
protected readonly Player Player;
protected readonly ScriptContext Context;
protected ScriptPlayerProperties(ScriptContext context, Player player)
public ScriptPlayerProperties(ScriptContext context, Player player)
{
Player = player;
Context = context;
@@ -89,8 +85,7 @@ namespace OpenRA.Scripting
protected override string MemberNotFoundError(string memberName) { return $"Table '{Name}' does not define a property '{memberName}'"; }
public readonly string Name;
protected ScriptGlobal(ScriptContext context)
public ScriptGlobal(ScriptContext context)
: base(context)
{
// GetType resolves the actual (subclass) type
@@ -120,7 +115,6 @@ namespace OpenRA.Scripting
}
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class ScriptGlobalAttribute : Attribute
{
public readonly string Name;
@@ -145,8 +139,6 @@ namespace OpenRA.Scripting
public readonly Cache<ActorInfo, Type[]> ActorCommands;
public readonly Type[] PlayerCommands;
public string ErrorMessage;
bool disposed;
public ScriptContext(World world, WorldRenderer worldRenderer,
@@ -169,106 +161,64 @@ namespace OpenRA.Scripting
.ToArray();
PlayerCommands = FilterCommands(world.Map.Rules.Actors[SystemActors.Player], knownPlayerCommands);
// Safe functions for http://lua-users.org/wiki/SandBoxes
// assert, error have been removed as well as albeit safe
var allowedGlobals = new string[]
{
"ipairs", "next", "pairs",
"pcall", "select", "tonumber", "tostring", "type", "unpack", "xpcall",
"math", "string", "table"
};
foreach (var fieldName in runtime.Globals.Keys)
if (!allowedGlobals.Contains(fieldName.ToString()))
runtime.Globals[fieldName] = null;
var forbiddenMath = new string[]
{
"random", // not desync safe, unsuitable
"randomseed" // maybe unsafe as it affects the host RNG
};
var mathGlobal = (LuaTable)runtime.Globals["math"];
foreach (var mathFunction in mathGlobal.Keys)
if (forbiddenMath.Contains(mathFunction.ToString()))
mathGlobal[mathFunction] = null;
runtime.Globals["EngineDir"] = Platform.EngineDir;
runtime.DoBuffer(File.Open(Path.Combine(Platform.EngineDir, "lua", "scriptwrapper.lua"), FileMode.Open, FileAccess.Read).ReadAllText(), "scriptwrapper.lua").Dispose();
tick = (LuaFunction)runtime.Globals["Tick"];
// Register globals
runtime.Globals["EngineDir"] = Platform.EngineDir;
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)FatalError))
runtime.Globals["FatalError"] = fn;
runtime.Globals["MaxUserScriptInstructions"] = MaxUserScriptInstructions;
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)LogDebugMessage))
runtime.Globals["print"] = fn;
// Register global tables
var bindings = Game.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>();
foreach (var b in bindings)
using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"])
{
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)LogDebugMessage))
registerGlobal.Call("print", fn).Dispose();
// Register global tables
var bindings = Game.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>();
foreach (var b in bindings)
{
var p = c.GetParameters();
return p.Length == 1 && p.First().ParameterType == typeof(ScriptContext);
});
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var p = c.GetParameters();
return p.Length == 1 && p.First().ParameterType == typeof(ScriptContext);
});
if (ctor == null)
throw new InvalidOperationException($"{b.Name} must define a constructor that takes a {nameof(ScriptContext)} context parameter");
if (ctor == null)
throw new InvalidOperationException($"{b.Name} must define a constructor that takes a ScriptContext context parameter");
var binding = (ScriptGlobal)ctor.Invoke(new[] { this });
using (var obj = binding.ToLuaValue(this))
runtime.Globals.Add(binding.Name, obj);
var binding = (ScriptGlobal)ctor.Invoke(new[] { this });
using (var obj = binding.ToLuaValue(this))
registerGlobal.Call(binding.Name, obj).Dispose();
}
}
// System functions do not count towards the memory limit
runtime.MaxMemoryUse = runtime.MemoryUse + MaxUserScriptMemory;
try
using (var loadScript = (LuaFunction)runtime.Globals["ExecuteSandboxedScript"])
{
foreach (var script in scripts)
runtime.DoBuffer(world.Map.Open(script).ReadAllText(), script).Dispose();
foreach (var s in scripts)
loadScript.Call(s, world.Map.Open(s).ReadAllText()).Dispose();
}
catch (Exception e)
{
FatalError(e);
return;
}
tick = runtime.Globals["Tick"] as LuaFunction;
}
void LogDebugMessage(string message)
{
Console.WriteLine($"Lua debug: {message}");
Console.WriteLine("Lua debug: {0}", message);
Log.Write("lua", message);
}
public bool FatalErrorOccurred { get; private set; }
public void FatalError(Exception e)
{
ErrorMessage = e.Message;
Console.WriteLine($"Fatal Lua Error: {e.Message}");
Console.WriteLine(e.StackTrace);
Log.Write("lua", $"Fatal Lua Error: {e.Message}");
Log.Write("lua", e.StackTrace);
FatalErrorOccurred = true;
World.AddFrameEndTask(w => World.EndGame());
}
void FatalError(string message)
public void FatalError(string message)
{
var stacktrace = new StackTrace().ToString();
Console.WriteLine($"Fatal Lua Error: {message}");
Console.WriteLine("Fatal Lua Error: {0}", message);
Console.WriteLine(stacktrace);
Log.Write("lua", message);
Log.Write("lua", "Fatal Lua Error: {0}", message);
Log.Write("lua", stacktrace);
FatalErrorOccurred = true;
@@ -278,46 +228,32 @@ namespace OpenRA.Scripting
public void RegisterMapActor(string name, Actor a)
{
if (runtime.Globals.ContainsKey(name))
throw new LuaException($"The global name '{name}' is reserved, and may not be used by a map actor");
using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"])
{
if (runtime.Globals.ContainsKey(name))
throw new LuaException($"The global name '{name}' is reserved, and may not be used by a map actor");
using (var obj = a.ToLuaValue(this))
runtime.Globals.Add(name, obj);
using (var obj = a.ToLuaValue(this))
registerGlobal.Call(name, obj).Dispose();
}
}
public void WorldLoaded()
{
if (FatalErrorOccurred || runtime.Globals["WorldLoaded"] is not LuaFunction worldLoaded)
if (FatalErrorOccurred)
return;
try
{
using (var worldLoaded = (LuaFunction)runtime.Globals["WorldLoaded"])
worldLoaded.Call().Dispose();
}
catch (LuaException e)
{
FatalError(e);
}
finally
{
worldLoaded?.Dispose();
}
}
public void Tick()
{
if (FatalErrorOccurred || disposed || tick == null)
if (FatalErrorOccurred || disposed)
return;
try
{
using (new PerfSample("tick_lua"))
tick.Call().Dispose();
}
catch (LuaException e)
{
FatalError(e);
}
using (new PerfSample("tick_lua"))
tick.Call().Dispose();
}
public void Dispose()

View File

@@ -50,7 +50,7 @@ namespace OpenRA.Scripting
try
{
if (!IsMethod)
throw new LuaException($"Trying to invoke a {nameof(ScriptMemberWrapper)} that isn't a method!");
throw new LuaException("Trying to invoke a ScriptMemberWrapper that isn't a method!");
var mi = (MethodInfo)Member;
var pi = mi.GetParameters();

View File

@@ -28,7 +28,7 @@ namespace OpenRA.Scripting
readonly List<string> membersToRemove = new List<string>();
#endif
protected ScriptObjectWrapper(ScriptContext context)
public ScriptObjectWrapper(ScriptContext context)
{
Context = context;
}

View File

@@ -39,7 +39,7 @@ namespace OpenRA.Server
void OnLintFailure(string message)
{
Log.Write("server", $"Map {map.Title} failed lint with error: {message}");
Log.Write("server", "Map {0} failed lint with error: {1}", map.Title, message);
failed = true;
}
@@ -84,13 +84,13 @@ namespace OpenRA.Server
}
catch (Exception e)
{
Log.Write("server", $"Failed to load rules for `{map.Title}` with error: {e.Message}");
Log.Write("server", "Failed to load rules for `{0}` with error :{1}", map.Title, e.Message);
status = Session.MapStatus.Incompatible;
}
if (map.Players.Players.Count > MapPlayers.MaximumPlayerCount)
{
Log.Write("server", $"Failed to load `{map.Title}`: Player count exceeds maximum ({map.Players.Players.Count}/{MapPlayers.MaximumPlayerCount}).");
Log.Write("server", "Failed to load `{0}`: Player count exceeds maximum ({1}/{2}).", map.Title, map.Players.Players.Count, MapPlayers.MaximumPlayerCount);
status = Session.MapStatus.Incompatible;
}

View File

@@ -112,7 +112,7 @@ namespace OpenRA.Server
}
}
static long Median(long[] a)
long Median(long[] a)
{
Array.Sort(a);
var n = a.Length;

View File

@@ -14,7 +14,7 @@ using System.Collections.Generic;
namespace OpenRA.Server
{
sealed class PlayerMessageTracker
class PlayerMessageTracker
{
[TranslationReference("remaining")]
const string ChatTemporaryDisabled = "notification-chat-temp-disabled";

View File

@@ -148,8 +148,6 @@ namespace OpenRA.Server
GameInformation gameInfo;
readonly List<GameInformation.Player> worldPlayers = new();
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
public readonly VoteKickTracker VoteKickTracker;
readonly PlayerMessageTracker playerMessageTracker;
public ServerState State
@@ -320,7 +318,6 @@ namespace OpenRA.Server
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
VoteKickTracker = new VoteKickTracker(this);
LobbyInfo = new Session
{
@@ -705,7 +702,7 @@ namespace OpenRA.Server
}
}
static byte[] CreateFrame(int client, int frame, byte[] data)
byte[] CreateFrame(int client, int frame, byte[] data)
{
var ms = new MemoryStream(data.Length + 12);
ms.WriteArray(BitConverter.GetBytes(data.Length + 4));
@@ -715,7 +712,7 @@ namespace OpenRA.Server
return ms.GetBuffer();
}
static byte[] CreateAckFrame(int frame, byte count)
byte[] CreateAckFrame(int frame, byte count)
{
var ms = new MemoryStream(14);
ms.WriteArray(BitConverter.GetBytes(6));
@@ -726,7 +723,7 @@ namespace OpenRA.Server
return ms.GetBuffer();
}
static byte[] CreateTickScaleFrame(float scale)
byte[] CreateTickScaleFrame(float scale)
{
var ms = new MemoryStream(17);
ms.WriteArray(BitConverter.GetBytes(9));
@@ -961,7 +958,7 @@ namespace OpenRA.Server
DispatchServerOrdersToClients(Order.FromTargetString("LocalizedMessage", text, true));
if (Type == ServerType.Dedicated)
WriteLineWithTimeStamp(TranslationProvider.GetString(key, arguments));
WriteLineWithTimeStamp(ModData.Translation.GetString(key, arguments));
}
public void SendLocalizedMessageTo(Connection conn, string key, Dictionary<string, object> arguments = null)
@@ -1166,8 +1163,15 @@ namespace OpenRA.Server
return LobbyInfo.ClientWithIndex(conn.PlayerIndex);
}
public bool HasClientWonOrLost(Session.Client client) =>
worldPlayers.FirstOrDefault(p => p?.ClientIndex == client.Index)?.Outcome != WinState.Undefined;
/// <summary>Does not check if client is admin.</summary>
public bool CanKickClient(Session.Client kickee)
{
if (State != ServerState.GameStarted || kickee.IsObserver)
return true;
var player = worldPlayers.FirstOrDefault(p => p?.ClientIndex == kickee.Index);
return player != null && player.Outcome != WinState.Undefined;
}
public void DropClient(Connection toDrop)
{
@@ -1308,7 +1312,7 @@ namespace OpenRA.Server
{
lock (LobbyInfo)
{
WriteLineWithTimeStamp(TranslationProvider.GetString(GameStarted));
WriteLineWithTimeStamp(ModData.Translation.GetString(GameStarted));
// Drop any players who are not ready
foreach (var c in Conns.Where(c => !c.Validated || GetClient(c).IsInvalid).ToArray())
@@ -1437,7 +1441,7 @@ namespace OpenRA.Server
interface IServerEvent { void Invoke(Server server); }
sealed class ConnectionConnectEvent : IServerEvent
class ConnectionConnectEvent : IServerEvent
{
readonly Socket socket;
public ConnectionConnectEvent(Socket socket)
@@ -1451,7 +1455,7 @@ namespace OpenRA.Server
}
}
sealed class ConnectionDisconnectEvent : IServerEvent
class ConnectionDisconnectEvent : IServerEvent
{
readonly Connection connection;
public ConnectionDisconnectEvent(Connection connection)
@@ -1465,7 +1469,7 @@ namespace OpenRA.Server
}
}
sealed class ConnectionPacketEvent : IServerEvent
class ConnectionPacketEvent : IServerEvent
{
readonly Connection connection;
readonly int frame;
@@ -1484,7 +1488,7 @@ namespace OpenRA.Server
}
}
sealed class ConnectionPingEvent : IServerEvent
class ConnectionPingEvent : IServerEvent
{
readonly Connection connection;
readonly int[] pingHistory;
@@ -1507,7 +1511,7 @@ namespace OpenRA.Server
}
}
sealed class CallbackEvent : IServerEvent
class CallbackEvent : IServerEvent
{
readonly Action action;

View File

@@ -1,223 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Diagnostics;
using OpenRA.Network;
namespace OpenRA.Server
{
public sealed class VoteKickTracker
{
[TranslationReference("kickee")]
const string InsufficientVotes = "notification-insufficient-votes-to-kick";
[TranslationReference]
const string AlreadyVoted = "notification-kick-already-voted";
[TranslationReference("kicker", "kickee")]
const string VoteKickStarted = "notification-vote-kick-started";
[TranslationReference]
const string UnableToStartAVote = "notification-unable-to-start-a-vote";
[TranslationReference("kickee", "percentage")]
const string VoteKickProgress = "notification-vote-kick-in-progress";
[TranslationReference("kickee")]
const string VoteKickEnded = "notification-vote-kick-ended";
readonly Dictionary<int, bool> voteTracker = new();
readonly Dictionary<Session.Client, long> failedVoteKickers = new();
readonly Server server;
Stopwatch voteKickTimer;
(Session.Client Client, Connection Conn) kickee;
(Session.Client Client, Connection Conn) voteKickerStarter;
public VoteKickTracker(Server server)
{
this.server = server;
}
// Only admins and alive players can participate in a vote kick.
bool ClientHasPower(Session.Client client) => client.IsAdmin || (!client.IsObserver && !server.HasClientWonOrLost(client));
public void Tick()
{
if (voteKickTimer == null)
return;
if (!server.Conns.Contains(kickee.Conn))
{
EndKickVote();
return;
}
if (voteKickTimer.ElapsedMilliseconds > server.Settings.VoteKickTimer)
EndKickVoteAndBlockKicker();
}
public bool VoteKick(Connection conn, Session.Client kicker, Connection kickeeConn, Session.Client kickee, int kickeeID, bool vote)
{
var voteInProgress = voteKickTimer != null;
if (server.State != ServerState.GameStarted
|| (kickee.IsAdmin && server.Type != ServerType.Dedicated)
|| (!voteInProgress && !vote) // Disallow starting a vote with a downvote
|| (voteInProgress && this.kickee.Client != kickee) // Disallow starting new votes when one is already ongoing.
|| !ClientHasPower(kicker))
{
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
return false;
}
short eligiblePlayers = 0;
var isKickeeOnline = false;
var adminIsDeadButOnline = false;
foreach (var c in server.Conns)
{
var client = server.GetClient(c);
if (client != kickee && ClientHasPower(client))
eligiblePlayers++;
if (c == kickeeConn)
isKickeeOnline = true;
if (client.IsAdmin && (client.IsObserver || server.HasClientWonOrLost(client)))
adminIsDeadButOnline = true;
}
if (!isKickeeOnline)
{
EndKickVote();
return false;
}
if (eligiblePlayers < 2 || (adminIsDeadButOnline && !kickee.IsAdmin && eligiblePlayers < 3))
{
if (!kickee.IsObserver && !server.HasClientWonOrLost(kickee))
{
// Vote kick cannot be the sole deciding factor for a game.
server.SendLocalizedMessageTo(conn, InsufficientVotes, Translation.Arguments("kickee", kickee.Name));
EndKickVote();
return false;
}
else if (vote)
{
// If only a single player is playing, allow him to kick observers.
EndKickVote(false);
return true;
}
}
if (!voteInProgress)
{
// Prevent vote kick spam abuse.
if (failedVoteKickers.TryGetValue(kicker, out var time))
{
if (time + server.Settings.VoteKickerCooldown > kickeeConn.ConnectionTimer.ElapsedMilliseconds)
{
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
return false;
}
else
failedVoteKickers.Remove(kicker);
}
Log.Write("server", $"Vote kick started on {kickeeID}.");
voteKickTimer = Stopwatch.StartNew();
server.SendLocalizedMessage(VoteKickStarted, Translation.Arguments("kicker", kicker.Name, "kickee", kickee.Name));
server.DispatchServerOrdersToClients(new Order("StartKickVote", null, false) { ExtraData = (uint)kickeeID }.Serialize());
this.kickee = (kickee, kickeeConn);
voteKickerStarter = (kicker, conn);
}
if (!voteTracker.ContainsKey(conn.PlayerIndex))
voteTracker[conn.PlayerIndex] = vote;
else
{
server.SendLocalizedMessageTo(conn, AlreadyVoted, null);
return false;
}
short votesFor = 0;
short votesAgainst = 0;
foreach (var c in voteTracker)
{
if (c.Value)
votesFor++;
else
votesAgainst++;
}
// Include the kickee in eligeablePlayers, so that in a 2v2 or any other even team
// matchup one team could not vote out the other team's player.
if (ClientHasPower(kickee))
{
eligiblePlayers++;
votesAgainst++;
}
var votesNeeded = eligiblePlayers / 2 + 1;
server.SendLocalizedMessage(VoteKickProgress, Translation.Arguments(
"kickee", kickee.Name,
"percentage", votesFor * 100 / eligiblePlayers));
// If a player or players during a vote lose or disconnect, it is possible that a downvote will
// kick a client. Guard against that situation.
if (vote && (votesFor >= votesNeeded))
{
EndKickVote(false);
return true;
}
// End vote if it can never succeed.
if (eligiblePlayers - votesAgainst < votesNeeded)
{
EndKickVoteAndBlockKicker();
return false;
}
voteKickTimer.Restart();
return false;
}
void EndKickVoteAndBlockKicker()
{
// Make sure vote kick is in progress.
if (voteKickTimer == null)
return;
if (server.Conns.Contains(voteKickerStarter.Conn))
failedVoteKickers[voteKickerStarter.Client] = voteKickerStarter.Conn.ConnectionTimer.ElapsedMilliseconds;
EndKickVote();
}
void EndKickVote(bool sendMessage = true)
{
// Make sure vote kick is in progress.
if (voteKickTimer == null)
return;
if (sendMessage)
server.SendLocalizedMessage(VoteKickEnded, Translation.Arguments("kickee", kickee.Client.Name));
server.DispatchServerOrdersToClients(new Order("EndKickVote", null, false) { ExtraData = (uint)kickee.Client.Index }.Serialize());
voteKickTimer = null;
voteKickerStarter = (null, null);
kickee = (null, null);
voteTracker.Clear();
}
}
}

View File

@@ -114,15 +114,6 @@ namespace OpenRA
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
public int FloodLimitCooldown = 15000;
[Desc("Can players vote to kick other players?")]
public bool EnableVoteKick = true;
[Desc("After how much time in miliseconds should the vote kick fail after idling?")]
public int VoteKickTimer = 30000;
[Desc("If a vote kick was unsuccessful for how long should the player who started the vote not be able to start new votes?")]
public int VoteKickerCooldown = 120000;
public ServerSettings Clone()
{
return (ServerSettings)MemberwiseClone();

View File

@@ -60,7 +60,7 @@ namespace OpenRA
{
if (!fileSystem.Exists(filename))
{
Log.Write("sound", $"LoadSound, file does not exist: {filename}");
Log.Write("sound", "LoadSound, file does not exist: {0}", filename);
return default;
}
@@ -356,7 +356,7 @@ namespace OpenRA
public float VideoSeekPosition => video?.SeekPosition ?? 0;
// Returns true if played successfully
public bool PlayPredefined(SoundType soundType, Ruleset ruleset, Player player, Actor voicedActor, string type, string definition, string variant,
public bool PlayPredefined(SoundType soundType, Ruleset ruleset, Player p, Actor voicedActor, string type, string definition, string variant,
bool relative, WPos pos, float volumeModifier, bool attenuateVolume)
{
if (ruleset == null)
@@ -399,16 +399,16 @@ namespace OpenRA
if (variant != null)
{
if (rules.Variants.TryGetValue(variant, out var v) && !rules.DisableVariants.Contains(definition))
suffix = v[id % v.Length];
if (rules.Prefixes.TryGetValue(variant, out var p) && !rules.DisablePrefixes.Contains(definition))
prefix = p[id % p.Length];
if (rules.Variants.ContainsKey(variant) && !rules.DisableVariants.Contains(definition))
suffix = rules.Variants[variant][id % rules.Variants[variant].Length];
if (rules.Prefixes.ContainsKey(variant) && !rules.DisablePrefixes.Contains(definition))
prefix = rules.Prefixes[variant][id % rules.Prefixes[variant].Length];
}
var name = prefix + clip + suffix;
var actorId = voicedActor != null && voicedActor.World.Selection.Contains(voicedActor) ? 0 : id;
if (!string.IsNullOrEmpty(name) && (player == null || player == player.World.LocalPlayer))
if (!string.IsNullOrEmpty(name) && (p == null || p == p.World.LocalPlayer))
{
ISound PlaySound()
{

View File

@@ -97,7 +97,7 @@ namespace OpenRA.Support
}
}
sealed class ManagedLoadContext : AssemblyLoadContext
class ManagedLoadContext : AssemblyLoadContext
{
readonly string basePath;
readonly Dictionary<string, ManagedLibrary> managedAssemblies;

View File

@@ -10,11 +10,10 @@
#endregion
using System.Collections.Generic;
using System.Globalization;
namespace OpenRA.Support
{
sealed class Benchmark
class Benchmark
{
readonly string prefix;
readonly Dictionary<string, List<BenchmarkPoint>> samples = new();
@@ -30,7 +29,7 @@ namespace OpenRA.Support
samples.GetOrAdd(item.Key).Add(new BenchmarkPoint(localTick, item.Value.LastValue));
}
sealed class BenchmarkPoint
class BenchmarkPoint
{
public int Tick { get; }
public double Value { get; }
@@ -51,7 +50,7 @@ namespace OpenRA.Support
Log.Write(name, "tick,time [ms]");
foreach (var point in sample.Value)
Log.Write(name, $"{point.Tick},{point.Value.ToString(CultureInfo.InvariantCulture)}");
Log.Write(name, $"{point.Tick},{point.Value}");
}
}

View File

@@ -0,0 +1,108 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
namespace OpenRA.Support
{
public static class Evaluator
{
public static int Evaluate(string expr)
{
return Evaluate(expr, new Dictionary<string, int>());
}
public static int Evaluate(string expr, Dictionary<string, int> syms)
{
var toks = Tokens(expr, "+-*/()");
var postfix = ToPostfix(toks, syms);
var s = new Stack<int>();
foreach (var t in postfix)
{
switch (t[0])
{
case '+': ApplyBinop(s, (x, y) => y + x); break;
case '-': ApplyBinop(s, (x, y) => y - x); break;
case '*': ApplyBinop(s, (x, y) => y * x); break;
case '/': ApplyBinop(s, (x, y) => y / x); break;
default: s.Push(Exts.ParseIntegerInvariant(t)); break;
}
}
return s.Pop();
}
static void ApplyBinop(Stack<int> s, Func<int, int, int> f)
{
var x = s.Pop();
var y = s.Pop();
s.Push(f(x, y));
}
static IEnumerable<string> ToPostfix(IEnumerable<string> toks, Dictionary<string, int> syms)
{
var s = new Stack<string>();
foreach (var t in toks)
{
if (t[0] == '(') s.Push(t);
else if (t[0] == ')')
{
var temp = "";
while ((temp = s.Pop()) != "(") yield return temp;
}
else if (char.IsNumber(t[0])) yield return t;
else if (char.IsLetter(t[0]))
{
if (!syms.ContainsKey(t))
throw new InvalidOperationException($"Substitution `{t}` undefined");
yield return syms[t].ToString();
}
else
{
while (s.Count > 0 && Prec[t] <= Prec[s.Peek()]) yield return s.Pop();
s.Push(t);
}
}
while (s.Count > 0) yield return s.Pop();
}
static readonly Dictionary<string, int> Prec
= new() { { "+", 0 }, { "-", 0 }, { "*", 1 }, { "/", 1 }, { "(", -1 } };
static IEnumerable<string> Tokens(string expr, string ops)
{
var s = "";
foreach (var c in expr)
{
if (char.IsWhiteSpace(c))
{
if (s != "") yield return s;
s = "";
}
else if (ops.Contains(c))
{
if (s != "") yield return s;
s = "";
yield return "" + c;
}
else
s += c;
}
if (s != "") yield return s;
}
}
}

View File

@@ -43,7 +43,7 @@ namespace OpenRA
Log.Write("exception", $"Date: {DateTime.UtcNow:u}");
Log.Write("exception", $"Operating System: {Platform.CurrentPlatform} ({Platform.CurrentArchitecture}, {Environment.OSVersion})");
Log.Write("exception", $"Runtime Version: {Platform.RuntimeVersion}");
Log.Write("exception", $"Runtime Version: {Platform.RuntimeVersion}", Platform.RuntimeVersion);
Log.Write("exception", $"Installed Language: {CultureInfo.InstalledUICulture.TwoLetterISOLanguageName} (Installed) {CultureInfo.CurrentCulture.TwoLetterISOLanguageName} (Current) {CultureInfo.CurrentUICulture.TwoLetterISOLanguageName} (Current UI)");
var rpt = BuildExceptionReport(ex).ToString();

View File

@@ -14,7 +14,7 @@ using System.Net.Http;
namespace OpenRA.Support
{
public static class HttpClientFactory
public class HttpClientFactory
{
#if NET5_0_OR_GREATER
const int MaxConnectionPerServer = 20;

View File

@@ -16,10 +16,10 @@ using System.Text;
namespace OpenRA.Support
{
public class HttpQueryBuilder : IEnumerable<KeyValuePair<string, string>>
public class HttpQueryBuilder : IEnumerable
{
readonly string url;
readonly List<KeyValuePair<string, string>> parameters = new();
readonly List<Parameter> parameters = new();
public HttpQueryBuilder(string url)
{
@@ -28,31 +28,34 @@ namespace OpenRA.Support
public void Add(string name, object value)
{
parameters.Add(KeyValuePair.Create(
name,
Uri.EscapeDataString(value.ToString())));
parameters.Add(new Parameter
{
Name = name,
Value = Uri.EscapeDataString(value.ToString())
});
}
public override string ToString()
{
var builder = new StringBuilder(url);
builder.Append('?');
builder.Append("?");
foreach (var parameter in parameters)
builder.Append($"{parameter.Key}={parameter.Value}&");
builder.Append($"{parameter.Name}={parameter.Value}&");
return builder.ToString();
}
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
class Parameter
{
return parameters.GetEnumerator();
public string Name { get; set; }
public string Value { get; set; }
}
IEnumerator IEnumerable.GetEnumerator()
public IEnumerator GetEnumerator()
{
return GetEnumerator();
throw new NotImplementedException();
}
}
}

View File

@@ -60,7 +60,7 @@ namespace OpenRA
}
catch (Exception ex)
{
Log.Write("client", $"Failed to parse Launch.URI or Launch.Connect: {ex.Message}");
Log.Write("client", "Failed to parse Launch.URI or Launch.Connect: {0}", ex.Message);
return null;
}
}

View File

@@ -176,6 +176,11 @@ namespace OpenRA
ChannelWriter.TryWrite(new ChannelData(channelName, $"{e.Message}{Environment.NewLine}{e.StackTrace}"));
}
public static void Write(string channelName, string format, params object[] args)
{
ChannelWriter.TryWrite(new ChannelData(channelName, format.F(args)));
}
public static void Dispose()
{
CancellationToken.Cancel();

View File

@@ -13,43 +13,36 @@ using System.Diagnostics;
namespace OpenRA.Support
{
public static class PerfTickLogger
public sealed class PerfTickLogger
{
public const long TimestampDisabled = 0L;
readonly DebugSettings settings = Game.Settings.Debug;
readonly long threshold = PerfTimer.LongTickThresholdInStopwatchTicks;
long start;
long current;
bool enabled;
static float durationThresholdMs = Game.Settings.Debug.LongTickThresholdMs;
static long durationThresholdTicks = PerfTimer.MillisToTicks(Game.Settings.Debug.LongTickThresholdMs);
long CurrentTimestamp => enabled ? Stopwatch.GetTimestamp() : 0L;
/// <summary>Retrieve the current timestamp.</summary>
/// <returns>TimestampDisabled if performance logging is disabled.</returns>
public static long GetTimestamp()
public void Start()
{
var settings = Game.Settings.Debug;
if (!settings.EnableSimulationPerfLogging)
return TimestampDisabled;
// TODO: Let settings notify listeners on changes
if (durationThresholdMs != settings.LongTickThresholdMs)
{
durationThresholdMs = Game.Settings.Debug.LongTickThresholdMs;
durationThresholdTicks = PerfTimer.MillisToTicks(durationThresholdMs);
}
return Stopwatch.GetTimestamp();
enabled = settings.EnableSimulationPerfLogging;
start = CurrentTimestamp;
}
/// <summary>Logs an entry in the performance log when the current time since the start tick exceeds the game debug setting `LongTickThresholdMs`.</summary>
/// <returns>TimestampDisabled if performance logging is disabled.</returns>
public static long LogLongTick(long startTimestamp, string name, object item)
public void LogTickAndRestartTimer(string name, object item)
{
if (startTimestamp == TimestampDisabled)
return TimestampDisabled;
if (!enabled)
return;
var currentTimetamp = Stopwatch.GetTimestamp();
if (currentTimetamp - startTimestamp > durationThresholdTicks)
PerfTimer.LogLongTick(startTimestamp, currentTimetamp, name, item);
current = CurrentTimestamp;
if (current - start > threshold)
{
PerfTimer.LogLongTick(start, current, name, item);
start = CurrentTimestamp;
return;
}
return currentTimetamp;
start = current;
}
}
}

View File

@@ -24,9 +24,8 @@ namespace OpenRA.Support
const string IndentationString = "| ";
const string FormatSeperation = " ms ";
static readonly string FormatString = "{0," + Digits + ":0}" + FormatSeperation + "{1}";
static readonly string FormatStringLongTick = "{0," + Digits + ":0}" + FormatSeperation + "[{1}] {2}: {3}";
readonly string name;
readonly long thresholdTicks;
readonly float thresholdMs;
readonly byte depth;
readonly PerfTimer parent;
List<PerfTimer> children;
@@ -37,7 +36,7 @@ namespace OpenRA.Support
public PerfTimer(string name, float thresholdMs = 0)
{
this.name = name;
thresholdTicks = MillisToTicks(thresholdMs);
this.thresholdMs = thresholdMs;
parent = ParentThreadLocal.Value;
depth = parent == null ? (byte)0 : (byte)(parent.depth + 1);
@@ -54,7 +53,7 @@ namespace OpenRA.Support
if (parent == null)
Write();
else if (ticks > thresholdTicks)
else if (ElapsedMs > thresholdMs)
{
parent.children ??= new List<PerfTimer>();
parent.children.Add(this);
@@ -68,15 +67,10 @@ namespace OpenRA.Support
Log.Write("perf", GetHeader(Indentation, name));
foreach (var child in children)
child.Write();
Log.Write("perf", string.Format(FormatString, ElapsedMs, GetFooter(Indentation)));
Log.Write("perf", FormatString, ElapsedMs, GetFooter(Indentation));
}
else if (ticks >= thresholdTicks)
Log.Write("perf", string.Format(FormatString, ElapsedMs, Indentation + name));
}
public static long MillisToTicks(float millis)
{
return (long)(Stopwatch.Frequency * millis / 1000f);
else if (ElapsedMs >= thresholdMs)
Log.Write("perf", FormatString, ElapsedMs, Indentation + name);
}
float ElapsedMs => 1000f * ticks / Stopwatch.Frequency;
@@ -85,13 +79,13 @@ namespace OpenRA.Support
{
var type = item.GetType();
var label = type == typeof(string) || type.IsGenericType ? item.ToString() : type.Name;
Log.Write("perf", string.Format(FormatStringLongTick,
Log.Write("perf", FormatString,
1000f * (endStopwatchTicks - startStopwatchTicks) / Stopwatch.Frequency,
Game.LocalTick,
name,
label));
"[" + Game.LocalTick + "] " + name + ": " + label);
}
public static long LongTickThresholdInStopwatchTicks => (long)(Stopwatch.Frequency * Game.Settings.Debug.LongTickThresholdMs / 1000f);
#region Formatting helpers
static string GetHeader(string indentation, string label)
{

View File

@@ -551,7 +551,7 @@ namespace OpenRA.Support
}
}
sealed class VariableToken : Token
class VariableToken : Token
{
public readonly string Name;
@@ -561,7 +561,7 @@ namespace OpenRA.Support
: base(TokenType.Variable, index) { Name = symbol; }
}
sealed class NumberToken : Token
class NumberToken : Token
{
public readonly int Value;
readonly string symbol;
@@ -576,7 +576,7 @@ namespace OpenRA.Support
}
}
protected VariableExpression(string expression)
public VariableExpression(string expression)
{
Expression = expression;
}
@@ -714,7 +714,7 @@ namespace OpenRA.Support
return Expressions.Expression.Condition(test, ifTrue, ifFalse);
}
sealed class AstStack
class AstStack
{
readonly List<Expression> expressions = new();
readonly List<ExpressionType> types = new();
@@ -772,7 +772,7 @@ namespace OpenRA.Support
}
}
sealed class Compiler
class Compiler
{
readonly AstStack ast = new();

View File

@@ -57,8 +57,8 @@ namespace OpenRA
static void EmitSyncOpcodes(Type type, ILGenerator il)
{
if (CustomHashFunctions.TryGetValue(type, out var hashFunction))
il.EmitCall(OpCodes.Call, hashFunction, null);
if (CustomHashFunctions.ContainsKey(type))
il.EmitCall(OpCodes.Call, CustomHashFunctions[type], null);
else if (type == typeof(bool))
{
var l = il.DefineLabel();

View File

@@ -17,7 +17,7 @@ namespace OpenRA
{
public static class TextNotificationsManager
{
public const int SystemClientId = -1;
public static readonly int SystemClientId = -1;
static readonly string SystemMessageLabel;
public static long ChatDisabledUntil { get; internal set; }

View File

@@ -39,7 +39,7 @@ namespace OpenRA
/// <summary>
/// Provides efficient ways to query a set of actors by their traits.
/// </summary>
sealed class TraitDictionary
class TraitDictionary
{
static readonly Func<Type, ITraitContainer> CreateTraitContainer = t =>
(ITraitContainer)typeof(TraitContainer<>).MakeGenericType(t).GetConstructor(Type.EmptyTypes).Invoke(null);
@@ -60,7 +60,7 @@ namespace OpenRA
{
Log.AddChannel("traitreport", "traitreport.log");
foreach (var t in traits.OrderByDescending(t => t.Value.Queries).TakeWhile(t => t.Value.Queries > 0))
Log.Write("traitreport", $"{t.Key.Name}: {t.Value.Queries}");
Log.Write("traitreport", "{0}: {1}", t.Key.Name, t.Value.Queries);
}
public void AddTrait(Actor actor, object val)
@@ -141,10 +141,11 @@ namespace OpenRA
int Queries { get; }
}
sealed class TraitContainer<T> : ITraitContainer
class TraitContainer<T> : ITraitContainer
{
readonly List<Actor> actors = new();
readonly List<T> traits = new();
readonly PerfTickLogger perfLogger = new();
public int Queries { get; private set; }
@@ -184,7 +185,7 @@ namespace OpenRA
return new MultipleEnumerable(this, actor);
}
sealed class MultipleEnumerable : IEnumerable<T>
class MultipleEnumerable : IEnumerable<T>
{
readonly TraitContainer<T> container;
readonly uint actor;
@@ -304,14 +305,14 @@ namespace OpenRA
public void ApplyToAllTimed(Action<Actor, T> action, string text)
{
var start = PerfTickLogger.GetTimestamp();
perfLogger.Start();
for (var i = 0; i < actors.Count; i++)
{
var actor = actors[i];
var trait = traits[i];
action(actor, trait);
start = PerfTickLogger.LogLongTick(start, text, trait);
perfLogger.LogTickAndRestartTimer(text, trait);
}
}
}

View File

@@ -19,19 +19,21 @@ namespace OpenRA.Traits
public static Activity RunActivity(Actor self, Activity act)
{
// PERF: This is a hot path and must run with minimal added overhead.
// If there are no activities we can bail straight away and save ourselves the overhead of setting up the perf logging.
if (act == null)
return act;
var start = PerfTickLogger.GetTimestamp();
do
var perfLogger = new PerfTickLogger();
perfLogger.Start();
while (act != null)
{
var prev = act;
act = act.TickOuter(self);
start = PerfTickLogger.LogLongTick(start, "Activity", prev);
perfLogger.LogTickAndRestartTimer("Activity", prev);
if (act == prev)
break;
}
while (act != null);
return act;
}

View File

@@ -96,11 +96,15 @@ namespace OpenRA.Traits
.ToArray();
if (Footprint.Length == 0)
throw new ArgumentException("This frozen actor has no footprint.\n" +
$"Actor Name: {actor.Info.Name}\n" +
$"Actor Location: {actor.Location}\n" +
$"Input footprint: [{footprint.Select(p => p.ToString()).JoinWith("|")}]\n" +
$"Input footprint (after shroud.Contains): [{footprint.Select(p => shroud.Contains(p).ToString()).JoinWith("|")}]");
throw new ArgumentException(("This frozen actor has no footprint.\n" +
"Actor Name: {0}\n" +
"Actor Location: {1}\n" +
"Input footprint: [{2}]\n" +
"Input footprint (after shroud.Contains): [{3}]")
.F(actor.Info.Name,
actor.Location.ToString(),
footprint.Select(p => p.ToString()).JoinWith("|"),
footprint.Select(p => shroud.Contains(p).ToString()).JoinWith("|")));
CenterPosition = actor.CenterPosition;
@@ -278,7 +282,7 @@ namespace OpenRA.Traits
frozenActorsById.Remove(fa.ID);
}
static Rectangle FootprintBounds(FrozenActor fa)
Rectangle FootprintBounds(FrozenActor fa)
{
var p1 = fa.Footprint[0];
var minU = p1.U;

View File

@@ -60,9 +60,11 @@ namespace OpenRA.Traits
IEnumerable<LobbyOption> ILobbyOptions.LobbyOptions(MapPreview map)
{
yield return new LobbyBooleanOption(map, "explored", ExploredMapCheckboxLabel, ExploredMapCheckboxDescription,
yield return new LobbyBooleanOption("explored", Game.ModData.Translation.GetString(ExploredMapCheckboxLabel),
Game.ModData.Translation.GetString(ExploredMapCheckboxDescription),
ExploredMapCheckboxVisible, ExploredMapCheckboxDisplayOrder, ExploredMapCheckboxEnabled, ExploredMapCheckboxLocked);
yield return new LobbyBooleanOption(map, "fog", FogCheckboxLabel, FogCheckboxDescription,
yield return new LobbyBooleanOption("fog", Game.ModData.Translation.GetString(FogCheckboxLabel),
Game.ModData.Translation.GetString(FogCheckboxDescription),
FogCheckboxVisible, FogCheckboxDisplayOrder, FogCheckboxEnabled, FogCheckboxLocked);
}
@@ -76,7 +78,7 @@ namespace OpenRA.Traits
public int RevealedCells { get; private set; }
enum ShroudCellType : byte { Shroud, Fog, Visible }
sealed class ShroudSource
class ShroudSource
{
public readonly SourceType Type;
public readonly PPos[] ProjectedCells;

View File

@@ -16,7 +16,7 @@ using System.Linq;
namespace OpenRA.Traits
{
public enum TargetType : byte { Invalid, Actor, Terrain, FrozenActor }
public readonly struct Target : IEquatable<Target>
public readonly struct Target
{
public static readonly Target[] None = Array.Empty<Target>();
public static readonly Target Invalid = default;

View File

@@ -13,7 +13,6 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using OpenRA.Activities;
using OpenRA.FileSystem;
using OpenRA.GameRules;
@@ -24,7 +23,6 @@ using OpenRA.Support;
namespace OpenRA.Traits
{
[AttributeUsage(AttributeTargets.Interface)]
public sealed class RequireExplicitImplementationAttribute : Attribute { }
[Flags]
@@ -544,15 +542,15 @@ namespace OpenRA.Traits
public readonly bool IsVisible;
public readonly int DisplayOrder;
public LobbyOption(MapPreview map, string id, string name, string description, bool visible, int displayorder,
public LobbyOption(string id, string name, string description, bool visible, int displayorder,
IReadOnlyDictionary<string, string> values, string defaultValue, bool locked)
{
Id = id;
Name = map.GetLocalisedString(name);
Description = description != null ? map.GetLocalisedString(description) : null;
Name = Game.ModData.Translation.GetString(name);
Description = Game.ModData.Translation.GetString(description);
IsVisible = visible;
DisplayOrder = displayorder;
Values = values.ToDictionary(v => v.Key, v => map.GetLocalisedString(v.Value));
Values = values;
DefaultValue = defaultValue;
IsLocked = locked;
}
@@ -571,8 +569,8 @@ namespace OpenRA.Traits
{ false.ToString(), "Disabled" }
};
public LobbyBooleanOption(MapPreview map, string id, string name, string description, bool visible, int displayorder, bool defaultValue, bool locked)
: base(map, id, name, description, visible, displayorder, new ReadOnlyDictionary<string, string>(BoolValues), defaultValue.ToString(), locked) { }
public LobbyBooleanOption(string id, string name, string description, bool visible, int displayorder, bool defaultValue, bool locked)
: base(id, name, description, visible, displayorder, new ReadOnlyDictionary<string, string>(BoolValues), defaultValue.ToString(), locked) { }
public override string Label(string newValue)
{

View File

@@ -18,9 +18,7 @@ using Linguini.Bundle;
using Linguini.Bundle.Builder;
using Linguini.Shared.Types.Bundle;
using Linguini.Syntax.Parser;
using Linguini.Syntax.Parser.Error;
using OpenRA.FileSystem;
using OpenRA.Traits;
namespace OpenRA
{
@@ -28,7 +26,6 @@ namespace OpenRA
public sealed class TranslationReferenceAttribute : Attribute
{
public readonly string[] RequiredVariableNames;
public readonly LintDictionaryReference DictionaryReference;
public TranslationReferenceAttribute() { }
@@ -36,11 +33,6 @@ namespace OpenRA
{
RequiredVariableNames = requiredVariableNames;
}
public TranslationReferenceAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
{
DictionaryReference = dictionaryReference;
}
}
public class Translation
@@ -48,41 +40,21 @@ namespace OpenRA
readonly FluentBundle bundle;
public Translation(string language, string[] translations, IReadOnlyFileSystem fileSystem)
: this(language, translations, fileSystem, error => Log.Write("debug", error.ToString())) { }
public Translation(string language, string[] translations, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
{
if (translations == null || translations.Length == 0)
return;
bundle = LinguiniBuilder.Builder()
.CultureInfo(new CultureInfo(language))
.CultureInfo(CultureInfo.InvariantCulture)
.SkipResources()
.SetUseIsolating(false)
.UseConcurrent()
.UncheckedBuild();
ParseTranslations(language, translations, fileSystem, onError);
ParseTranslations(language, translations, fileSystem);
}
public Translation(string language, string text, Action<ParseError> onError)
{
var parser = new LinguiniParser(text);
var resource = parser.Parse();
foreach (var error in resource.Errors)
onError(error);
bundle = LinguiniBuilder.Builder()
.CultureInfo(new CultureInfo(language))
.SkipResources()
.SetUseIsolating(false)
.UseConcurrent()
.UncheckedBuild();
bundle.AddResourceOverriding(resource);
}
void ParseTranslations(string language, string[] translations, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
void ParseTranslations(string language, string[] translations, IReadOnlyFileSystem fileSystem)
{
// Always load english strings to provide a fallback for missing translations.
// It is important to load the english files first so the chosen language's files can override them.
@@ -99,7 +71,7 @@ namespace OpenRA
var parser = new LinguiniParser(reader);
var resource = parser.Parse();
foreach (var error in resource.Errors)
onError(error);
Log.Write("debug", error.ToString());
bundle.AddResourceOverriding(resource);
}
@@ -116,8 +88,8 @@ namespace OpenRA
public bool TryGetString(string key, out string value, IDictionary<string, object> arguments = null)
{
if (key == null)
throw new ArgumentNullException(nameof(key));
if (string.IsNullOrEmpty(key))
throw new ArgumentException("A translation key must not be null or empty.", nameof(key));
try
{
@@ -138,10 +110,10 @@ namespace OpenRA
return result;
}
catch (Exception)
catch (Exception e)
{
Log.Write("debug", $"Failed translation: {key}");
Log.Write("debug", $"Translation of {key} failed:");
Log.Write("debug", e);
value = null;
return false;
}

View File

@@ -1,76 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using OpenRA.FileSystem;
namespace OpenRA
{
public static class TranslationProvider
{
// Ensure thread-safety.
static readonly object SyncObject = new();
static Translation modTranslation;
static Translation mapTranslation;
public static void Initialize(ModData modData, IReadOnlyFileSystem fileSystem)
{
lock (SyncObject)
{
modTranslation = new Translation(Game.Settings.Player.Language, modData.Manifest.Translations, fileSystem);
mapTranslation = fileSystem is Map map && map.TranslationDefinitions != null
? new Translation(Game.Settings.Player.Language, FieldLoader.GetValue<string[]>("value", map.TranslationDefinitions.Value), fileSystem)
: null;
}
}
public static string GetString(string key, IDictionary<string, object> args = null)
{
lock (SyncObject)
{
// By prioritizing mod-level translations we prevent maps from overwriting translation keys. We do not want to
// allow maps to change the UI nor any other strings not exposed to the map.
if (modTranslation.TryGetString(key, out var message, args))
return message;
if (mapTranslation != null)
return mapTranslation.GetString(key, args);
return key;
}
}
public static bool TryGetString(string key, out string message, IDictionary<string, object> args = null)
{
lock (SyncObject)
{
// By prioritizing mod-level translations we prevent maps from overwriting translation keys. We do not want to
// allow maps to change the UI nor any other strings not exposed to the map.
if (modTranslation.TryGetString(key, out message, args))
return true;
if (mapTranslation != null && mapTranslation.TryGetString(key, out message, args))
return true;
return false;
}
}
/// <summary>Should only be used by <see cref="MapPreview"/>.</summary>
internal static bool TryGetModString(string key, out string message, IDictionary<string, object> args = null)
{
lock (SyncObject)
{
return modTranslation.TryGetString(key, out message, args);
}
}
}
}

View File

@@ -13,7 +13,7 @@ using System.Linq;
namespace OpenRA.UtilityCommands
{
sealed class ClearInvalidModRegistrationsCommand : IUtilityCommand
class ClearInvalidModRegistrationsCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--clear-invalid-mod-registrations";

View File

@@ -13,7 +13,7 @@ using System.Linq;
namespace OpenRA.UtilityCommands
{
sealed class RegisterModCommand : IUtilityCommand
class RegisterModCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--register-mod";

View File

@@ -13,7 +13,7 @@ using System.Linq;
namespace OpenRA.UtilityCommands
{
sealed class UnregisterModCommand : IUtilityCommand
class UnregisterModCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--unregister-mod";

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Network;
@@ -190,10 +189,10 @@ namespace OpenRA.Widgets
// Info defined in YAML
public string Id = null;
public IntegerExpression X;
public IntegerExpression Y;
public IntegerExpression Width;
public IntegerExpression Height;
public string X = "0";
public string Y = "0";
public string Width = "0";
public string Height = "0";
public string[] Logic = Array.Empty<string>();
public ChromeLogic[] LogicObjects { get; private set; }
public bool Visible = true;
@@ -204,10 +203,9 @@ namespace OpenRA.Widgets
public Rectangle Bounds;
public Widget Parent = null;
public Func<bool> IsVisible;
public Widget() { IsVisible = () => Visible; }
protected Widget() { IsVisible = () => Visible; }
protected Widget(Widget widget)
public Widget(Widget widget)
{
Id = widget.Id;
X = widget.X;
@@ -264,8 +262,8 @@ namespace OpenRA.Widgets
? new Rectangle(0, 0, Game.Renderer.Resolution.Width, Game.Renderer.Resolution.Height)
: Parent.Bounds;
var substitutions = args.TryGetValue("substitutions", out var subs) ?
new Dictionary<string, int>((Dictionary<string, int>)subs) :
var substitutions = args.ContainsKey("substitutions") ?
new Dictionary<string, int>((Dictionary<string, int>)args["substitutions"]) :
new Dictionary<string, int>();
substitutions.Add("WINDOW_RIGHT", Game.Renderer.Resolution.Width);
@@ -274,17 +272,16 @@ namespace OpenRA.Widgets
substitutions.Add("PARENT_LEFT", parentBounds.Left);
substitutions.Add("PARENT_TOP", parentBounds.Top);
substitutions.Add("PARENT_BOTTOM", parentBounds.Height);
var readOnlySubstitutions = new ReadOnlyDictionary<string, int>(substitutions);
var width = Width != null ? Width.Evaluate(readOnlySubstitutions) : 0;
var height = Height != null ? Height.Evaluate(readOnlySubstitutions) : 0;
var width = Evaluator.Evaluate(Width, substitutions);
var height = Evaluator.Evaluate(Height, substitutions);
substitutions.Add("WIDTH", width);
substitutions.Add("HEIGHT", height);
var x = X != null ? X.Evaluate(readOnlySubstitutions) : 0;
var y = Y != null ? Y.Evaluate(readOnlySubstitutions) : 0;
Bounds = new Rectangle(x, y, width, height);
Bounds = new Rectangle(Evaluator.Evaluate(X, substitutions),
Evaluator.Evaluate(Y, substitutions),
width,
height);
}
public void PostInit(WidgetArgs args)

View File

@@ -66,13 +66,14 @@ namespace OpenRA
{
// PERF: This is a hot path and must run with minimal added overhead, so we enumerate manually
// to allow us to call PerfTickLogger only once per iteration in the normal case.
var perfLogger = new PerfTickLogger();
using (var enumerator = e.GetEnumerator())
{
var start = PerfTickLogger.GetTimestamp();
perfLogger.Start();
while (enumerator.MoveNext())
{
a(enumerator.Current);
start = PerfTickLogger.LogLongTick(start, text, enumerator.Current);
perfLogger.LogTickAndRestartTimer(text, enumerator.Current);
}
}
}

View File

@@ -18,7 +18,7 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Cnc.Activities
{
sealed class Infiltrate : Enter
class Infiltrate : Enter
{
readonly Infiltrates infiltrates;
readonly INotifyInfiltration[] notifiers;
@@ -64,6 +64,9 @@ namespace OpenRA.Mods.Cnc.Activities
foreach (var t in targetActor.TraitsImplementing<INotifyInfiltrated>())
t.Infiltrated(targetActor, self, infiltrates.Info.Types);
var exp = self.Owner.PlayerActor.TraitOrDefault<PlayerExperience>();
exp?.GiveExperience(infiltrates.Info.PlayerExperience);
if (!string.IsNullOrEmpty(infiltrates.Info.Notification))
Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech",
infiltrates.Info.Notification, self.Owner.Faction.InternalName);

Some files were not shown because too many files have changed in this diff Show More