Files
OpenRA/OpenRA.Game/Network/SyncReport.cs
RoosterDragon bb17cfa179 Expose mod.yaml content to localisation.
Mod metadata, load screens and mod content is all now sourced from ftl files, allowing these items to be translated.

Translations are now initialized as part of ModData creation, as currently they are made available too late for the usage we need here.

The "modcontent" mod learns a new parameter for "Content.TranslationFile" - this allows a mod to provide the path of a translation file to the mod which it can load. This allows mods such as ra, cnc, d2k, ts to own the translations for their ModContent, yet still make them accessible to the modcontent mod.

CheckFluentReference learns to validate all these new fields to ensure translations have been set.
2024-10-07 12:38:40 +03:00

343 lines
11 KiB
C#

#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;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using OpenRA.Primitives;
namespace OpenRA.Network
{
sealed class SyncReport
{
const int NumSyncReports = 7;
static readonly Cache<Type, TypeInfo> TypeInfoCache = new(t => new TypeInfo(t));
readonly OrderManager orderManager;
readonly Report[] syncReports = new Report[NumSyncReports];
int curIndex = 0;
static (string[] Names, Values Values) DumpSyncTrait(ISync sync)
{
var type = sync.GetType();
TypeInfo typeInfo;
lock (TypeInfoCache)
typeInfo = TypeInfoCache[type];
var values = new Values(typeInfo.Names.Length);
var index = 0;
foreach (var func in typeInfo.SerializableCopyOfMemberFunctions)
values[index++] = func(sync);
return (typeInfo.Names, values);
}
public SyncReport(OrderManager orderManager)
{
this.orderManager = orderManager;
for (var i = 0; i < NumSyncReports; i++)
syncReports[i] = new Report();
}
internal void UpdateSyncReport(IEnumerable<OrderManager.ClientOrder> orders)
{
GenerateSyncReport(syncReports[curIndex], orders);
curIndex = ++curIndex % NumSyncReports;
}
void GenerateSyncReport(Report report, IEnumerable<OrderManager.ClientOrder> orders)
{
report.Frame = orderManager.NetFrameNumber;
report.SyncedRandom = orderManager.World.SharedRandom.Last;
report.TotalCount = orderManager.World.SharedRandom.TotalCount;
report.Traits.Clear();
report.Effects.Clear();
report.Orders.Clear();
report.Orders.AddRange(orders);
foreach (var actor in orderManager.World.ActorsHavingTrait<ISync>())
{
foreach (var syncHash in actor.SyncHashes)
{
var hash = syncHash.Hash();
if (hash != 0)
{
report.Traits.Add(new TraitReport()
{
ActorID = actor.ActorID,
Type = actor.Info.Name,
Owner = (actor.Owner == null) ? "null" : actor.Owner.PlayerName,
Trait = syncHash.Trait.GetType().Name,
Hash = hash,
NamesValues = DumpSyncTrait(syncHash.Trait)
});
}
}
}
foreach (var sync in orderManager.World.SyncedEffects)
{
var hash = Sync.Hash(sync);
if (hash != 0)
{
report.Effects.Add(new EffectReport()
{
Name = sync.GetType().Name,
Hash = hash,
NamesValues = DumpSyncTrait(sync)
});
}
}
}
internal void DumpSyncReport(int frame)
{
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture);
var reportName = $"syncreport-{timestamp}-{orderManager.LocalClient?.Index}.log";
Log.AddChannel("sync", reportName);
var recordedFrames = new List<int>();
var desyncFrameFound = false;
foreach (var r in syncReports)
{
recordedFrames.Add(r.Frame);
if (r.Frame == frame)
{
desyncFrameFound = true;
var mod = Game.ModData.Manifest.Metadata;
Log.Write("sync", $"Player: {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.TitleTranslated} at Version {mod.Version})");
Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
Log.Write("sync", "Synced Traits:");
foreach (var a in r.Traits)
{
Log.Write("sync", $"\t {a.ActorID} {a.Type} {a.Owner} {a.Trait} ({a.Hash})");
var nvp = a.NamesValues;
for (var i = 0; i < nvp.Names.Length; i++)
if (nvp.Values[i] != null)
Log.Write("sync", $"\t\t {nvp.Names[i]}: {nvp.Values[i]}");
}
Log.Write("sync", "Synced Effects:");
foreach (var e in r.Effects)
{
Log.Write("sync", $"\t {e.Name} ({e.Hash})");
var nvp = e.NamesValues;
for (var i = 0; i < nvp.Names.Length; i++)
if (nvp.Values[i] != null)
Log.Write("sync", $"\t\t {nvp.Names[i]}: {nvp.Values[i]}");
}
Log.Write("sync", "Orders Issued:");
foreach (var o in r.Orders)
Log.Write("sync", $"\t {o}");
}
}
Log.Write("sync", "Sync Report System Info:");
Log.Write("sync", $"Out of sync frame: {frame}");
Log.Write("sync", "Recorded frames: " + string.Join(",", recordedFrames));
if (!desyncFrameFound)
Log.Write("sync", $"Recorded frames do not contain the frame {frame}. No sync report available!");
}
sealed class Report
{
public int Frame;
public int SyncedRandom;
public int TotalCount;
public readonly List<TraitReport> Traits = new();
public readonly List<EffectReport> Effects = new();
public readonly List<OrderManager.ClientOrder> Orders = new();
}
struct TraitReport
{
public uint ActorID;
public string Type;
public string Owner;
public string Trait;
public int Hash;
public (string[] Names, Values Values) NamesValues;
}
struct EffectReport
{
public string Name;
public int Hash;
public (string[] Names, Values Values) NamesValues;
}
readonly struct TypeInfo
{
static readonly ParameterExpression SyncParam = Expression.Parameter(typeof(ISync), "sync");
static readonly ConstantExpression NullString = Expression.Constant(null, typeof(string));
static readonly ConstantExpression TrueString = Expression.Constant(bool.TrueString, typeof(string));
static readonly ConstantExpression FalseString = Expression.Constant(bool.FalseString, typeof(string));
public readonly Func<ISync, object>[] SerializableCopyOfMemberFunctions;
public readonly string[] Names;
public TypeInfo(Type type)
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var fields = type.GetFields(Flags)
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
.ToList();
var properties = type.GetProperties(Flags)
.Where(pi => pi.HasAttribute<SyncAttribute>())
.ToList();
foreach (var prop in properties)
if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
throw new InvalidOperationException(
"Properties using the Sync attribute must be readable and must not use index parameters.\n" +
"Invalid Property: " + prop.DeclaringType.FullName + "." + prop.Name);
var sync = Expression.Convert(SyncParam, type);
SerializableCopyOfMemberFunctions = fields
.Select(fi => SerializableCopyOfMember(Expression.Field(sync, fi), fi.FieldType, fi.Name))
.Concat(properties.Select(pi => SerializableCopyOfMember(Expression.Property(sync, pi), pi.PropertyType, pi.Name)))
.ToArray();
Names = fields.Select(fi => fi.Name).Concat(properties.Select(pi => pi.Name)).ToArray();
}
static Func<ISync, object> SerializableCopyOfMember(MemberExpression getMember, Type memberType, string name)
{
// We need to serialize a copy of the current value so if the sync report is generated, the values can
// be dumped as strings.
if (memberType.IsValueType)
{
// PERF: For value types we can avoid the overhead of calling ToString immediately. We can instead
// just box a copy of the current value into an object. This is faster than calling ToString. We
// can call ToString later when we generate the report. Most of the time, the sync report is never
// generated so we successfully avoid the overhead to calling ToString.
if (memberType == typeof(bool))
{
// PERF: If the member is a Boolean, we can also avoid the allocation caused by boxing it.
// Instead, we can just return the resulting strings directly.
var getBoolString = Expression.Condition(getMember, TrueString, FalseString);
return Expression.Lambda<Func<ISync, string>>(getBoolString, name, new[] { SyncParam }).Compile();
}
var boxedCopy = Expression.Convert(getMember, typeof(object));
return Expression.Lambda<Func<ISync, object>>(boxedCopy, name, new[] { SyncParam }).Compile();
}
// For reference types, we have to call ToString right away to get a snapshot of the value. We cannot
// delay, as calling ToString later may produce different results.
return MemberToString(getMember, memberType, name);
}
static Func<ISync, string> MemberToString(MemberExpression getMember, Type memberType, string name)
{
// The lambda generated is shown below.
// TSync is actual type of the ISync object. Foo is a field or property with the Sync attribute applied.
var toString = memberType.GetMethod(nameof(object.ToString), Type.EmptyTypes);
Expression getString;
if (memberType.IsValueType)
{
// (ISync sync) => { return ((TSync)sync).Foo.ToString(); }
getString = Expression.Call(getMember, toString);
}
else
{
// (ISync sync) => { var foo = ((TSync)sync).Foo; return foo == null ? null : foo.ToString(); }
var memberVariable = Expression.Variable(memberType, getMember.Member.Name);
var assignMemberVariable = Expression.Assign(memberVariable, getMember);
var member = Expression.Block(new[] { memberVariable }, assignMemberVariable);
getString = Expression.Call(member, toString);
var nullMember = Expression.Constant(null, memberType);
getString = Expression.Condition(Expression.Equal(member, nullMember), NullString, getString);
}
return Expression.Lambda<Func<ISync, string>>(getString, name, new[] { SyncParam }).Compile();
}
}
/// <summary>
/// Holds up to 4 objects directly, or else allocates an array to hold the items. This allows us to record
/// trait values for traits with up to 4 sync members inline without having to allocate extra memory.
/// </summary>
struct Values
{
static readonly object Sentinel = new();
object item1OrArray;
object item2OrSentinel;
object item3;
object item4;
public Values(int size)
{
item1OrArray = null;
item2OrSentinel = null;
item3 = null;
item4 = null;
if (size > 4)
{
item1OrArray = new object[size];
item2OrSentinel = Sentinel;
}
}
public object this[int index]
{
readonly get
{
if (item2OrSentinel == Sentinel)
return ((object[])item1OrArray)[index];
switch (index)
{
case 0: return item1OrArray;
case 1: return item2OrSentinel;
case 2: return item3;
case 3: return item4;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
set
{
if (item2OrSentinel == Sentinel)
{
((object[])item1OrArray)[index] = value;
return;
}
switch (index)
{
case 0: item1OrArray = value; break;
case 1: item2OrSentinel = value; break;
case 2: item3 = value; break;
case 3: item4 = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
}
}
}