Compare commits
3 Commits
prep-2307
...
devtest-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54675d6f2b | ||
|
|
2101e74470 | ||
|
|
73ccc83971 |
@@ -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
|
||||
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@@ -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
|
||||
|
||||
9
Makefile
9
Makefile
@@ -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'
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace OpenRA.Activities
|
||||
bool firstRunCompleted;
|
||||
bool lastRun;
|
||||
|
||||
protected Activity()
|
||||
public Activity()
|
||||
{
|
||||
IsInterruptible = true;
|
||||
ChildHasPriority = true;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class CursorManager
|
||||
{
|
||||
sealed class Cursor
|
||||
class Cursor
|
||||
{
|
||||
public string Name;
|
||||
public int2 PaddedSize;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
26
OpenRA.Game/LogProxy.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace OpenRA.Server
|
||||
}
|
||||
}
|
||||
|
||||
static long Median(long[] a)
|
||||
long Median(long[] a)
|
||||
{
|
||||
Array.Sort(a);
|
||||
var n = a.Length;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace OpenRA.Support
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ManagedLoadContext : AssemblyLoadContext
|
||||
class ManagedLoadContext : AssemblyLoadContext
|
||||
{
|
||||
readonly string basePath;
|
||||
readonly Dictionary<string, ManagedLibrary> managedAssemblies;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
108
OpenRA.Game/Support/Evaluator.cs
Normal file
108
OpenRA.Game/Support/Evaluator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Linq;
|
||||
|
||||
namespace OpenRA.UtilityCommands
|
||||
{
|
||||
sealed class ClearInvalidModRegistrationsCommand : IUtilityCommand
|
||||
class ClearInvalidModRegistrationsCommand : IUtilityCommand
|
||||
{
|
||||
string IUtilityCommand.Name => "--clear-invalid-mod-registrations";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using System.Linq;
|
||||
|
||||
namespace OpenRA.UtilityCommands
|
||||
{
|
||||
sealed class RegisterModCommand : IUtilityCommand
|
||||
class RegisterModCommand : IUtilityCommand
|
||||
{
|
||||
string IUtilityCommand.Name => "--register-mod";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using System.Linq;
|
||||
|
||||
namespace OpenRA.UtilityCommands
|
||||
{
|
||||
sealed class UnregisterModCommand : IUtilityCommand
|
||||
class UnregisterModCommand : IUtilityCommand
|
||||
{
|
||||
string IUtilityCommand.Name => "--unregister-mod";
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user