mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f659b2b58c | ||
|
|
b1e13f5b13 | ||
|
|
e5995d4edc | ||
|
|
6eb080a277 | ||
|
|
b0cb41e94a | ||
|
|
23a23f7c22 | ||
|
|
ec3a74d268 | ||
|
|
12b0bc4e0a | ||
|
|
903041dfd1 | ||
|
|
b96419f0b2 | ||
|
|
fe33ad2652 | ||
|
|
057a68b366 | ||
|
|
1a2c9008fe | ||
|
|
cd95929ebe | ||
|
|
6396ec472d | ||
|
|
d7aa5daf6a | ||
|
|
e3819f8245 |
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -54,6 +54,61 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 232.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Obsolete method `AppearanceComponent.TryGetData` is now access-restricted to `SharedAppearanceSystem`; use `SharedAppearanceSystem.TryGetData` instead.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `SharedAppearanceSystem.AppendData`, which appends non-existing `AppearanceData` from one `AppearanceComponent` to another.
|
||||
* Added `AppearanceComponent.AppearanceDataInit`, which can be used to set initial `AppearanceData` entries in .yaml.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix BUI interfaces not deep-copying in state handling.
|
||||
* Add Robust.Xaml.csproj to the solution to fix some XAML issues.
|
||||
|
||||
### Other
|
||||
|
||||
* Serialization will now add type tags (`!type:<T>`) for necessary `NodeData` when writing (currently only for `object` nodes).
|
||||
|
||||
### Internal
|
||||
|
||||
* Added `ObjectSerializer`, which handles serialization of the generic `object` type.
|
||||
|
||||
|
||||
## 231.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed a bug where the client might not add entities to the broadphase/lookup components.
|
||||
* Fixed various toolshed commands not working, including `sort`, `sortdown` `join` (for strings), and `emplace`
|
||||
|
||||
### Other
|
||||
|
||||
* Toolshed command blocks now stop executing if previous errors were not handled / cleared.
|
||||
|
||||
|
||||
## 231.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Network `InterfaceData` on `UserInterfaceComponent`.
|
||||
* Added `System.Decimal` to sandbox.
|
||||
* Added XAML hot reloading.
|
||||
* Added API for content to write custom files into replay through `IReplayFileWriter`.
|
||||
|
||||
### Other
|
||||
|
||||
* Optimized `EntityLookup` and other physics systems.
|
||||
|
||||
### Internal
|
||||
|
||||
* Added more tests related to physics.
|
||||
|
||||
|
||||
## 231.0.1
|
||||
|
||||
### Other
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
using Robust.Xaml;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
@@ -37,10 +37,12 @@ namespace Robust.Build.Tasks
|
||||
var msg = $"CompileRobustXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}";
|
||||
BuildEngine.LogMessage(msg, MessageImportance.High);
|
||||
|
||||
var res = XamlCompiler.Compile(BuildEngine, input,
|
||||
var res = XamlAotCompiler.Compile(
|
||||
BuildEngine, input,
|
||||
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
|
||||
ProjectDirectory, OutputPath,
|
||||
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null);
|
||||
OutputPath,
|
||||
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null
|
||||
);
|
||||
if (!res.success)
|
||||
return false;
|
||||
if (!res.writtentofile)
|
||||
@@ -65,22 +67,24 @@ namespace Robust.Build.Tasks
|
||||
return true;
|
||||
}
|
||||
|
||||
// PYREX NOTE: This project was comically null-unsafe before I touched it. I'm just marking what it did accurately
|
||||
[Required]
|
||||
public string ReferencesFilePath { get; set; }
|
||||
public string ReferencesFilePath { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string ProjectDirectory { get; set; }
|
||||
|
||||
public string ProjectDirectory { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string AssemblyFile { get; set; }
|
||||
public string AssemblyFile { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string OriginalCopyPath { get; set; }
|
||||
public string? OriginalCopyPath { get; set; } = null;
|
||||
|
||||
public string OutputPath { get; set; }
|
||||
public string UpdateBuildIndicator { get; set; }
|
||||
public string? OutputPath { get; set; }
|
||||
public string UpdateBuildIndicator { get; set; } = null!;
|
||||
|
||||
public string AssemblyOriginatorKeyFile { get; set; }
|
||||
public string AssemblyOriginatorKeyFile { get; set; } = null!;
|
||||
public bool SignAssembly { get; set; }
|
||||
public bool DelaySign { get; set; }
|
||||
|
||||
@@ -95,7 +99,7 @@ namespace Robust.Build.Tasks
|
||||
return rv;
|
||||
}
|
||||
|
||||
public IBuildEngine BuildEngine { get; set; }
|
||||
public ITaskHost HostObject { get; set; }
|
||||
public IBuildEngine BuildEngine { get; set; } = null!;
|
||||
public ITaskHost HostObject { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
using System.Linq;
|
||||
using Pidgin;
|
||||
using static Pidgin.Parser;
|
||||
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
public static class MathParsing
|
||||
{
|
||||
public static Parser<char, float> Single { get; } = Real.Select(c => (float) c);
|
||||
|
||||
public static Parser<char, float> Single1 { get; }
|
||||
= Single.Between(SkipWhitespaces);
|
||||
|
||||
public static Parser<char, (float, float)> Single2 { get; }
|
||||
= Single.Before(SkipWhitespaces).Repeat(2).Select(e =>
|
||||
{
|
||||
var arr = e.ToArray();
|
||||
return (arr[0], arr[1]);
|
||||
});
|
||||
|
||||
public static Parser<char, (float, float, float, float)> Single4 { get; }
|
||||
= Single.Before(SkipWhitespaces).Repeat(4).Select(e =>
|
||||
{
|
||||
var arr = e.ToArray();
|
||||
return (arr[0], arr[1], arr[2], arr[3]);
|
||||
});
|
||||
|
||||
public static Parser<char, float[]> Thickness { get; }
|
||||
= SkipWhitespaces.Then(
|
||||
OneOf(
|
||||
Try(Single4.Select(c => new[] {c.Item1, c.Item2, c.Item3, c.Item4})),
|
||||
Try(Single2.Select(c => new[] {c.Item1, c.Item2})),
|
||||
Try(Single1.Select(c => new[] {c}))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,11 @@ namespace Robust.Build.Tasks
|
||||
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties,
|
||||
IDictionary targetOutputs) => throw new NotSupportedException();
|
||||
|
||||
public bool ContinueOnError { get; }
|
||||
public int LineNumberOfTaskNode { get; }
|
||||
public int ColumnNumberOfTaskNode { get; }
|
||||
public string ProjectFileOfTaskNode { get; }
|
||||
// PYREX NOTE: This project was extremely null-unsafe before I touched it. I'm just marking what it did already
|
||||
// Here's the broken interface of IBuildEngine that we started with
|
||||
public bool ContinueOnError => default;
|
||||
public int LineNumberOfTaskNode => default;
|
||||
public int ColumnNumberOfTaskNode => default;
|
||||
public string ProjectFileOfTaskNode => null!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
|
||||
<!--
|
||||
PJB3005 (2024-08-24)
|
||||
So the reason that Robust.Client.Injectors is NS2.0 is that Visual Studio
|
||||
still ships a .NET FX based MSBuild for some godforsaken reason. This means
|
||||
that when having Robust.Client.Injectors loaded directly by the main MSBuild
|
||||
process... that would break.
|
||||
|
||||
Except we don't do that anyways right now due to file locking issues, so maybe
|
||||
it's fine to give up on that. Whatever.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
|
||||
<PackageReference Include="Pidgin" Version="2.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj" />
|
||||
<ProjectReference Include="..\Robust.Xaml\Robust.Xaml.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Cecil.Rocks;
|
||||
using Pidgin;
|
||||
using XamlX;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
using XamlX.Parsers;
|
||||
using XamlX.Transform;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
|
||||
/// Adjusted for our UI-Framework
|
||||
/// </summary>
|
||||
public partial class XamlCompiler
|
||||
{
|
||||
public static (bool success, bool writtentofile) Compile(IBuildEngine engine, string input, string[] references,
|
||||
string projectDirectory, string output, string strongNameKey)
|
||||
{
|
||||
var typeSystem = new CecilTypeSystem(references
|
||||
.Where(r => !r.ToLowerInvariant().EndsWith("robust.build.tasks.dll"))
|
||||
.Concat(new[] { input }), input);
|
||||
|
||||
var asm = typeSystem.TargetAssemblyDefinition;
|
||||
|
||||
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
|
||||
{
|
||||
// If this type exists, the assembly has already been processed by us.
|
||||
// Do not run again, it would corrupt the file.
|
||||
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
|
||||
// but better safe than sorry eh?
|
||||
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
var compileRes = CompileCore(engine, typeSystem);
|
||||
if (compileRes == null)
|
||||
return (true, false);
|
||||
if (compileRes == false)
|
||||
return (false, false);
|
||||
|
||||
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
|
||||
if (!string.IsNullOrWhiteSpace(strongNameKey))
|
||||
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
|
||||
|
||||
asm.Write(output, writerParameters);
|
||||
|
||||
return (true, true);
|
||||
|
||||
}
|
||||
|
||||
static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
|
||||
{
|
||||
var asm = typeSystem.TargetAssemblyDefinition;
|
||||
var embrsc = new EmbeddedResources(asm);
|
||||
|
||||
if (embrsc.Resources.Count(CheckXamlName) == 0)
|
||||
// Nothing to do
|
||||
return null;
|
||||
|
||||
var xamlLanguage = new XamlLanguageTypeMappings(typeSystem)
|
||||
{
|
||||
XmlnsAttributes =
|
||||
{
|
||||
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
|
||||
|
||||
},
|
||||
ContentAttributes =
|
||||
{
|
||||
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
|
||||
},
|
||||
UsableDuringInitializationAttributes =
|
||||
{
|
||||
typeSystem.GetType("Robust.Client.UserInterface.XAML.UsableDuringInitializationAttribute")
|
||||
},
|
||||
DeferredContentPropertyAttributes =
|
||||
{
|
||||
typeSystem.GetType("Robust.Client.UserInterface.XAML.DeferredContentAttribute")
|
||||
},
|
||||
RootObjectProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestRootObjectProvider"),
|
||||
UriContextProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestUriContext"),
|
||||
ProvideValueTarget = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestProvideValueTarget"),
|
||||
};
|
||||
var emitConfig = new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>
|
||||
{
|
||||
ContextTypeBuilderCallback = (b,c) => EmitNameScopeField(xamlLanguage, typeSystem, b, c)
|
||||
};
|
||||
|
||||
var transformerconfig = new TransformerConfiguration(
|
||||
typeSystem,
|
||||
typeSystem.TargetAssembly,
|
||||
xamlLanguage,
|
||||
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), CustomValueConverter);
|
||||
|
||||
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
|
||||
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
|
||||
asm.MainModule.Types.Add(contextDef);
|
||||
var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
|
||||
xamlLanguage, emitConfig);
|
||||
|
||||
var compiler =
|
||||
new RobustXamlILCompiler(transformerconfig, emitConfig, true);
|
||||
|
||||
bool CompileGroup(IResourceGroup group)
|
||||
{
|
||||
var typeDef = new TypeDefinition("CompiledRobustXaml", "!" + group.Name, TypeAttributes.Class,
|
||||
asm.MainModule.TypeSystem.Object);
|
||||
|
||||
//typeDef.CustomAttributes.Add(new CustomAttribute(ed));
|
||||
asm.MainModule.Types.Add(typeDef);
|
||||
var builder = typeSystem.CreateTypeBuilder(typeDef);
|
||||
|
||||
foreach (var res in group.Resources.Where(CheckXamlName))
|
||||
{
|
||||
try
|
||||
{
|
||||
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", MessageImportance.Low);
|
||||
|
||||
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
|
||||
var parsed = XDocumentXamlParser.Parse(xaml);
|
||||
|
||||
var initialRoot = (XamlAstObjectNode) parsed.Root;
|
||||
|
||||
var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
|
||||
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
|
||||
string classname;
|
||||
if (classDirective != null && classDirective.Values[0] is XamlAstTextNode tn)
|
||||
{
|
||||
classname = tn.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
classname = res.Name.Replace(".xaml","");
|
||||
}
|
||||
|
||||
var classType = typeSystem.TargetAssembly.FindType(classname);
|
||||
if (classType == null)
|
||||
throw new Exception($"Unable to find type '{classname}'");
|
||||
|
||||
compiler.Transform(parsed);
|
||||
|
||||
var populateName = $"Populate:{res.Name}";
|
||||
var buildName = $"Build:{res.Name}";
|
||||
|
||||
var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve();
|
||||
|
||||
var populateBuilder = typeSystem.CreateTypeBuilder(classTypeDefinition);
|
||||
|
||||
compiler.Compile(parsed, contextClass,
|
||||
compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
|
||||
classTypeDefinition == null),
|
||||
compiler.DefineBuildMethod(builder, parsed, buildName, true),
|
||||
null,
|
||||
(closureName, closureBaseType) =>
|
||||
populateBuilder.DefineSubType(closureBaseType, closureName, false),
|
||||
res.Uri, res
|
||||
);
|
||||
|
||||
//add compiled populate method
|
||||
var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve().Methods
|
||||
.First(m => m.Name == populateName);
|
||||
|
||||
const string TrampolineName = "!XamlIlPopulateTrampoline";
|
||||
var trampoline = new MethodDefinition(TrampolineName,
|
||||
MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void);
|
||||
trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition));
|
||||
classTypeDefinition.Methods.Add(trampoline);
|
||||
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull));
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod));
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
|
||||
|
||||
var foundXamlLoader = false;
|
||||
// Find RobustXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this)
|
||||
foreach (var method in classTypeDefinition.Methods
|
||||
.Where(m => !m.Attributes.HasFlag(MethodAttributes.Static)))
|
||||
{
|
||||
var i = method.Body.Instructions;
|
||||
for (var c = 1; c < i.Count; c++)
|
||||
{
|
||||
if (i[c].OpCode == OpCodes.Call)
|
||||
{
|
||||
var op = i[c].Operand as MethodReference;
|
||||
|
||||
if (op != null
|
||||
&& op.Name == TrampolineName)
|
||||
{
|
||||
foundXamlLoader = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (op != null
|
||||
&& op.Name == "Load"
|
||||
&& op.Parameters.Count == 1
|
||||
&& op.Parameters[0].ParameterType.FullName == "System.Object"
|
||||
&& op.DeclaringType.FullName == "Robust.Client.UserInterface.XAML.RobustXamlLoader")
|
||||
{
|
||||
if (MatchThisCall(i, c - 1))
|
||||
{
|
||||
i[c].Operand = trampoline;
|
||||
foundXamlLoader = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundXamlLoader)
|
||||
{
|
||||
var ctors = classTypeDefinition.GetConstructors()
|
||||
.Where(c => !c.IsStatic).ToList();
|
||||
// We can inject xaml loader into default constructor
|
||||
if (ctors.Count == 1 && ctors[0].Body.Instructions.Count(o=>o.OpCode != OpCodes.Nop) == 3)
|
||||
{
|
||||
var i = ctors[0].Body.Instructions;
|
||||
var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret));
|
||||
i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline));
|
||||
i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidProgramException(
|
||||
$"No call to RobustXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
|
||||
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
|
||||
}
|
||||
res.Remove();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (embrsc.Resources.Count(CheckXamlName) != 0)
|
||||
{
|
||||
if (!CompileGroup(embrsc))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CustomValueConverter(
|
||||
AstTransformationContext context,
|
||||
IXamlAstValueNode node,
|
||||
IXamlType type,
|
||||
out IXamlAstValueNode result)
|
||||
{
|
||||
if (!(node is XamlAstTextNode textNode))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var text = textNode.Text;
|
||||
var types = context.GetRobustTypes();
|
||||
|
||||
if (type.Equals(types.Vector2))
|
||||
{
|
||||
var foo = MathParsing.Single2.Parse(text);
|
||||
|
||||
if (!foo.Success)
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
|
||||
|
||||
var (x, y) = foo.Value;
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Vector2, types.Vector2ConstructorFull,
|
||||
types.Single, new[] {x, y});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Thickness))
|
||||
{
|
||||
var foo = MathParsing.Thickness.Parse(text);
|
||||
|
||||
if (!foo.Success)
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
|
||||
|
||||
var val = foo.Value;
|
||||
float[] full;
|
||||
if (val.Length == 1)
|
||||
{
|
||||
var u = val[0];
|
||||
full = new[] {u, u, u, u};
|
||||
}
|
||||
else if (val.Length == 2)
|
||||
{
|
||||
var h = val[0];
|
||||
var v = val[1];
|
||||
full = new[] {h, v, h, v};
|
||||
}
|
||||
else // 4
|
||||
{
|
||||
full = val;
|
||||
}
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Thickness, types.ThicknessConstructorFull,
|
||||
types.Single, full);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Thickness))
|
||||
{
|
||||
var foo = MathParsing.Thickness.Parse(text);
|
||||
|
||||
if (!foo.Success)
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
|
||||
|
||||
var val = foo.Value;
|
||||
float[] full;
|
||||
if (val.Length == 1)
|
||||
{
|
||||
var u = val[0];
|
||||
full = new[] {u, u, u, u};
|
||||
}
|
||||
else if (val.Length == 2)
|
||||
{
|
||||
var h = val[0];
|
||||
var v = val[1];
|
||||
full = new[] {h, v, h, v};
|
||||
}
|
||||
else // 4
|
||||
{
|
||||
full = val;
|
||||
}
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Thickness, types.ThicknessConstructorFull,
|
||||
types.Single, full);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Color))
|
||||
{
|
||||
// TODO: Interpret these colors at XAML compile time instead of at runtime.
|
||||
result = new RXamlColorAstNode(node, types, text);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public const string ContextNameScopeFieldName = "RobustNameScope";
|
||||
|
||||
private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder<IXamlILEmitter> typeBuilder, IXamlILEmitter constructor)
|
||||
{
|
||||
var nameScopeType = typeSystem.FindType("Robust.Client.UserInterface.XAML.NameScope");
|
||||
var field = typeBuilder.DefineField(nameScopeType,
|
||||
ContextNameScopeFieldName, true, false);
|
||||
constructor
|
||||
.Ldarg_0()
|
||||
.Newobj(nameScopeType.GetConstructor())
|
||||
.Stfld(field);
|
||||
}
|
||||
}
|
||||
|
||||
interface IResource : IFileSource
|
||||
{
|
||||
string Uri { get; }
|
||||
string Name { get; }
|
||||
void Remove();
|
||||
|
||||
}
|
||||
|
||||
interface IResourceGroup
|
||||
{
|
||||
string Name { get; }
|
||||
IEnumerable<IResource> Resources { get; }
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ using Robust.Client.Upload;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Client.UserInterface.XAML.Proxy;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
using Robust.Shared;
|
||||
@@ -146,6 +147,16 @@ namespace Robust.Client
|
||||
deps.Register<IConfigurationManagerInternal, ClientNetConfigurationManager>();
|
||||
deps.Register<IClientNetConfigurationManager, ClientNetConfigurationManager>();
|
||||
deps.Register<INetConfigurationManagerInternal, ClientNetConfigurationManager>();
|
||||
|
||||
#if TOOLS
|
||||
deps.Register<IXamlProxyManager, XamlProxyManager>();
|
||||
deps.Register<IXamlHotReloadManager, XamlHotReloadManager>();
|
||||
#else
|
||||
deps.Register<IXamlProxyManager, XamlProxyManagerStub>();
|
||||
deps.Register<IXamlHotReloadManager, XamlHotReloadManagerStub>();
|
||||
#endif
|
||||
|
||||
deps.Register<IXamlProxyHelper, XamlProxyHelper>();
|
||||
deps.Register<MarkupTagManager>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ using Robust.Client.State;
|
||||
using Robust.Client.Upload;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML.Proxy;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
using Robust.Client.WebViewHook;
|
||||
@@ -53,6 +54,8 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
|
||||
[Dependency] private readonly IRobustSerializer _serializer = default!;
|
||||
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = default!;
|
||||
[Dependency] private readonly IXamlHotReloadManager _xamlHotReloadManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
@@ -171,6 +174,8 @@ namespace Robust.Client
|
||||
_reflectionManager.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDefaultPrototypes();
|
||||
_xamlProxyManager.Initialize();
|
||||
_xamlHotReloadManager.Initialize();
|
||||
_userInterfaceManager.Initialize();
|
||||
_eyeManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RobustToolsBuild)' == 'True'">
|
||||
<ProjectReference Include="..\Robust.Xaml\Robust.Xaml.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Shader embedding -->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// This service locates the SS14 source tree and watches for changes to its xaml files.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It then reloads them instantly.
|
||||
///
|
||||
/// It depends on <see cref="IXamlProxyManager"/> and is stubbed on non-TOOLS builds.
|
||||
/// </remarks>
|
||||
interface IXamlHotReloadManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize the hot reload manager.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You can't do anything with this once it's started, including turn it off.
|
||||
/// </remarks>
|
||||
void Initialize();
|
||||
}
|
||||
11
Robust.Client/UserInterface/XAML/Proxy/IXamlProxyHelper.cs
Normal file
11
Robust.Client/UserInterface/XAML/Proxy/IXamlProxyHelper.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// Reexport the Populate method of <see cref="IXamlProxyManager"/> and nothing else.
|
||||
/// </summary>
|
||||
public interface IXamlProxyHelper
|
||||
{
|
||||
bool Populate(Type t, object o);
|
||||
}
|
||||
76
Robust.Client/UserInterface/XAML/Proxy/IXamlProxyManager.cs
Normal file
76
Robust.Client/UserInterface/XAML/Proxy/IXamlProxyManager.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// This service provides a proxy for Populate, which is the generated function that
|
||||
/// initializes the UI objects of a Xaml widget.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The proxy can always return false: in that case, a Xaml widget will self-populate
|
||||
/// as usual. This is the behavior on Release builds.
|
||||
///
|
||||
/// However, it can also call into an externally-provided implementation of the Xaml
|
||||
/// widget.
|
||||
///
|
||||
/// No source of externally-provided implementations actually exists, by default --
|
||||
/// you will need to call SetImplementation with a blob of xaml source code to provide
|
||||
/// one. <see cref="IXamlHotReloadManager" /> is an example of a service that calls into
|
||||
/// that functionality.
|
||||
/// </remarks>
|
||||
internal interface IXamlProxyManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize creates the <see cref="IXamlProxyManager"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the <see cref="IXamlProxyManager" /> is not a stub, then it will spy on the
|
||||
/// assembly list (from <see cref="Robust.Shared.Reflection.IReflectionManager" />)
|
||||
/// and find <see cref="XamlMetadataAttribute" /> entries on the loaded types.
|
||||
/// </remarks>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Return true if at least one <see cref="Type"/> in the current project expects its XAML
|
||||
/// to come from a file with the given name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method supports code that is trying to figure out what name the build process
|
||||
/// would have assigned to a resource file. A caller can try a few candidate names and use
|
||||
/// its "yes" to continue.
|
||||
///
|
||||
/// This method is very fast, so it's OK to hammer it!
|
||||
///
|
||||
/// Also, on a non-tools build, this always returns false.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">the filename</param>
|
||||
/// <returns>true if expected</returns>
|
||||
bool CanSetImplementation(string fileName);
|
||||
|
||||
/// <summary>
|
||||
/// Replace the implementation of <paramref name="fileName"/> with <paramref name="fileContent" />,
|
||||
/// compiling it if needed.
|
||||
///
|
||||
/// All types based on <paramref name="fileName" /> will be recompiled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This may fail and the caller won't be notified. (There will usually be logs.)
|
||||
///
|
||||
/// On a non-tools build, this fails silently.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">the name of the file</param>
|
||||
/// <param name="fileContent">the new content of the file</param>
|
||||
void SetImplementation(string fileName, string fileContent);
|
||||
|
||||
/// <summary>
|
||||
/// If we have a JIT version of the XAML code for <paramref name="t" />, then call
|
||||
/// the new implementation on <paramref name="o" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <paramref name="o" /> may be a subclass of <paramref name="t" />.
|
||||
/// </remarks>
|
||||
/// <param name="t">the static type of the object</param>
|
||||
/// <param name="o">the object</param>
|
||||
/// <returns>true if we called a hot reloaded implementation</returns>
|
||||
bool Populate(Type t, object o);
|
||||
}
|
||||
195
Robust.Client/UserInterface/XAML/Proxy/XamlHotReloadManager.cs
Normal file
195
Robust.Client/UserInterface/XAML/Proxy/XamlHotReloadManager.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// The real implementation of <see cref="IXamlHotReloadManager" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Its behavior is described there.
|
||||
/// </remarks>
|
||||
internal sealed class XamlHotReloadManager : IXamlHotReloadManager
|
||||
{
|
||||
private const string MarkerFileName = "SpaceStation14.sln";
|
||||
|
||||
[Dependency] ILogManager _logManager = null!;
|
||||
[Dependency] private readonly IResourceManager _resources = null!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = null!;
|
||||
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = null!;
|
||||
|
||||
private ISawmill _sawmill = null!;
|
||||
private FileSystemWatcher? _watcher;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("xamlhotreload");
|
||||
var codeLocation = InferCodeLocation();
|
||||
|
||||
if (codeLocation == null)
|
||||
{
|
||||
_sawmill.Warning($"could not find code -- where is {MarkerFileName}?");
|
||||
return;
|
||||
}
|
||||
|
||||
_sawmill.Info($"code location: {codeLocation}");
|
||||
|
||||
// must not be gc'ed or else it will stop reporting
|
||||
// therefore: keep a reference
|
||||
_watcher = CreateWatcher(codeLocation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a file system watcher that identifies XAML changes in a given
|
||||
/// location.
|
||||
/// </summary>
|
||||
/// <param name="location">the location (a real path on the OS file system)</param>
|
||||
/// <returns>the new watcher</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">if <see cref="FileSystemWatcher"/> violates its type-related postconditions</exception>
|
||||
private FileSystemWatcher CreateWatcher(string location)
|
||||
{
|
||||
var watcher = new FileSystemWatcher(location)
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite,
|
||||
};
|
||||
|
||||
watcher.Changed += (_, args) =>
|
||||
{
|
||||
switch (args.ChangeType)
|
||||
{
|
||||
case WatcherChangeTypes.Renamed:
|
||||
case WatcherChangeTypes.Deleted:
|
||||
return;
|
||||
case WatcherChangeTypes.Created:
|
||||
case WatcherChangeTypes.Changed:
|
||||
case WatcherChangeTypes.All:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(args));
|
||||
}
|
||||
|
||||
_taskManager.RunOnMainThread(() =>
|
||||
{
|
||||
var resourceFileName =
|
||||
ResourceFileName(location, args.FullPath, _xamlProxyManager.CanSetImplementation);
|
||||
if (resourceFileName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string newText;
|
||||
try
|
||||
{
|
||||
newText = File.ReadAllText(args.FullPath);
|
||||
}
|
||||
catch (IOException ie)
|
||||
{
|
||||
_sawmill.Warning($"error attempting a hot reload -- skipped: {ie}");
|
||||
return;
|
||||
}
|
||||
|
||||
_xamlProxyManager.SetImplementation(resourceFileName, newText);
|
||||
});
|
||||
};
|
||||
watcher.EnableRaisingEvents = true;
|
||||
return watcher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Using the content roots of the project, infer the location of its code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This kind of introspection is almost universally a bad idea, but we don't
|
||||
/// feasibly have other options, so I've buried it in a private method.
|
||||
/// </remarks>
|
||||
/// <returns>the inferred code location or null</returns>
|
||||
private string? InferCodeLocation()
|
||||
{
|
||||
// ascend upwards from each content root until the solution file is found
|
||||
foreach (var contentRoot in _resources.GetContentRoots())
|
||||
{
|
||||
var systemPath = contentRoot.ToRelativeSystemPath();
|
||||
while (true)
|
||||
{
|
||||
var files = Array.Empty<string>();
|
||||
try
|
||||
{
|
||||
files = Directory.GetFiles(systemPath);
|
||||
}
|
||||
catch (IOException) { } // this is allowed to fail, and if so we just keep going up
|
||||
|
||||
if (files.Any(f => Path.GetFileName(f).Equals(MarkerFileName, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
return systemPath;
|
||||
}
|
||||
|
||||
DirectoryInfo? newPath = null;
|
||||
try
|
||||
{
|
||||
newPath = Directory.GetParent(systemPath);
|
||||
}
|
||||
catch (IOException) { } // ditto here. if we don't find it, we're in the wrong place
|
||||
|
||||
if (newPath == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
systemPath = newPath.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infer the name of the resource file associated with the XAML item at the given path.
|
||||
/// </summary>
|
||||
/// <param name="codeLocation">the code location</param>
|
||||
/// <param name="realPath">the real path of the file</param>
|
||||
/// <param name="isDesired">a function returning true if something expects this file</param>
|
||||
/// <returns>the name of a desired resource that matches this file, or null</returns>
|
||||
private string? ResourceFileName(string codeLocation, string realPath, Predicate<string> isDesired)
|
||||
{
|
||||
// start with the name of the file and systematically add each super-directory until we reach
|
||||
// the inferred code location.
|
||||
//
|
||||
// for /home/pyrex/ss14/Content.Client/Instruments/UI/InstrumentMenu.xaml, the following names
|
||||
// will be tried:
|
||||
//
|
||||
// - InstrumentMenu.xaml
|
||||
// - UI.InstrumentMenu.xaml
|
||||
// - Instruments.UI.InstrumentMenu.xaml
|
||||
// - Content.Client.Instruments.UI.InstrumentMenu.xaml
|
||||
var resourceFileName = Path.GetFileName(realPath);
|
||||
var super = Directory.GetParent(realPath);
|
||||
|
||||
var canonicalCodeLocation = Path.GetFullPath(codeLocation);
|
||||
|
||||
while (true)
|
||||
{
|
||||
// did someone want it: OK, jump out
|
||||
if (isDesired(resourceFileName))
|
||||
{
|
||||
return resourceFileName;
|
||||
}
|
||||
|
||||
if (super == null || Path.GetFullPath(super.FullName) == canonicalCodeLocation)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
resourceFileName = super.Name + "." + resourceFileName;
|
||||
super = super.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// A stub implementation of <see cref="XamlHotReloadManager"/>. Its
|
||||
/// behavior is to do nothing.
|
||||
/// </summary>
|
||||
internal sealed class XamlHotReloadManagerStub : IXamlHotReloadManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Do nothing.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Xaml;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// This is a utility class that tracks the relationship between resource file names,
|
||||
/// Xamlx-compatible <see cref="Uri"/>s, <see cref="Type"/>s that are interested in a
|
||||
/// given file, and implementations of Populate.
|
||||
/// </summary>
|
||||
internal sealed class XamlImplementationStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// For each filename, we store its last known <see cref="Uri"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When we compile the new implementation, we will use the same <see cref="Uri"/>.
|
||||
/// </remarks>
|
||||
private readonly Dictionary<string, Uri> _fileUri = new();
|
||||
|
||||
/// <summary>
|
||||
/// For each filename, we store its last known content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is known even for AOT-compiled code -- therefore, we can use this table
|
||||
/// to convert an AOT-compiled Control to a JIT-compiled one.
|
||||
/// </remarks>
|
||||
private readonly Dictionary<string, string> _fileContent = new();
|
||||
|
||||
/// <summary>
|
||||
/// For each filename, we store the type interested in this file.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Type> _fileType = new();
|
||||
|
||||
/// <summary>
|
||||
/// For each type, store the JIT-compiled implementation of Populate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no such implementation exists, then methods that would normally
|
||||
/// find and call a JIT'ed implementation will do nothing and return
|
||||
/// false instead. As an ultimate result, the AOT'ed implementation
|
||||
/// will be used.
|
||||
/// </remarks>
|
||||
private readonly Dictionary<Type, MethodInfo> _populateImplementations = new();
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly XamlJitDelegate _jitDelegate;
|
||||
|
||||
/// <summary>
|
||||
/// Create the storage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It would be weird to call this from any type outside of
|
||||
/// <see cref="Robust.Client.UserInterface.XAML.Proxy" />.
|
||||
/// </remarks>
|
||||
/// <param name="sawmill">the (shared) logger</param>
|
||||
/// <param name="jitDelegate">
|
||||
/// a delegate that calls the
|
||||
/// <see cref="XamlJitCompiler"/>, possibly handling errors
|
||||
/// </param>
|
||||
public XamlImplementationStorage(ISawmill sawmill, XamlJitDelegate jitDelegate)
|
||||
{
|
||||
_sawmill = sawmill;
|
||||
_jitDelegate = jitDelegate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspect <paramref name="assembly" /> for types that declare a <see cref="XamlMetadataAttribute"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We can only do hot reloading if we know this basic information.
|
||||
///
|
||||
/// Note that even release-mode content artifacts contain this attribute.
|
||||
/// </remarks>
|
||||
/// <param name="assembly">the assembly</param>
|
||||
/// <returns>an IEnumerable of types with xaml metadata</returns>
|
||||
private IEnumerable<(Type, XamlMetadataAttribute)> TypesWithXamlMetadata(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (type.GetCustomAttribute<XamlMetadataAttribute>() is not { } attr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return (type, attr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add all Xaml-annotated types from <paramref name="assembly" /> to this storage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We don't JIT these types, but we store enough info that we could JIT
|
||||
/// them if we wanted to.
|
||||
/// </remarks>
|
||||
/// <param name="assembly">an assembly</param>
|
||||
public void Add(Assembly assembly)
|
||||
{
|
||||
foreach (var (type, metadata) in TypesWithXamlMetadata(assembly))
|
||||
{
|
||||
// this can fail, but if it does, that means something is _really_ wrong
|
||||
// with the compiler, or someone tried to write their own Xaml metadata
|
||||
Uri uri;
|
||||
try
|
||||
{
|
||||
uri = new Uri(metadata.Uri);
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
throw new InvalidProgramException(
|
||||
$"XamlImplementationStorage encountered an malformed Uri in the metadata for {type.FullName}: " +
|
||||
$"{metadata.Uri}. this is a bug in XamlAotCompiler"
|
||||
);
|
||||
}
|
||||
|
||||
var fileName = metadata.FileName;
|
||||
var content = metadata.Content;
|
||||
|
||||
_fileUri[fileName] = uri;
|
||||
_fileContent[fileName] = content;
|
||||
|
||||
if (!_fileType.TryAdd(fileName, type))
|
||||
{
|
||||
throw new InvalidProgramException(
|
||||
$"XamlImplementationStorage observed that two types were interested in the same Xaml filename: " +
|
||||
$"{fileName}. ({type.FullName} and {_fileType[fileName].FullName}). this is a bug in XamlAotCompiler"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quietly JIT every type with XAML metadata.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should have no visible effect except that the <see cref="XamlJitDelegate"/>
|
||||
/// may dump some info messages into the terminal about cases where the
|
||||
/// hot reload failed.
|
||||
/// </remarks>
|
||||
public void ForceReloadAll()
|
||||
{
|
||||
foreach (var (fileName, fileContent) in _fileContent)
|
||||
{
|
||||
SetImplementation(fileName, fileContent, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if calling <see cref="SetImplementation" /> on <paramref name="fileName" /> would not be a no-op.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// That is: if some type cares about the contents of <paramref name="fileName" />.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">the filename</param>
|
||||
/// <returns>true if not a no-op</returns>
|
||||
public bool CanSetImplementation(string fileName)
|
||||
{
|
||||
return _fileType.ContainsKey(fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the implementation of <paramref name="fileName"/> by JIT-ing
|
||||
/// <paramref name="fileContent"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If nothing cares about the implementation of <paramref name="fileName"/>, then this will do nothing.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">the name of the file whose implementation should be replaced</param>
|
||||
/// <param name="fileContent">the new implementation</param>
|
||||
/// <param name="quiet">if true, then don't bother to log</param>
|
||||
public void SetImplementation(string fileName, string fileContent, bool quiet)
|
||||
{
|
||||
if (!_fileType.TryGetValue(fileName, out var type))
|
||||
{
|
||||
_sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents");
|
||||
return;
|
||||
}
|
||||
|
||||
var uri =
|
||||
_fileUri.GetValueOrDefault(fileName) ??
|
||||
throw new InvalidProgramException("file URI missing (this is a bug in ImplementationStorage)");
|
||||
|
||||
if (!quiet)
|
||||
{
|
||||
_sawmill.Debug($"replacing {fileName} for {type}");
|
||||
}
|
||||
var impl = _jitDelegate(type, uri, fileName, fileContent);
|
||||
if (impl != null)
|
||||
{
|
||||
_populateImplementations[type] = impl;
|
||||
}
|
||||
_fileContent[fileName] = fileContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call the JITed implementation of Populate on a XAML-associated object <paramref name="o"/>.
|
||||
///
|
||||
/// If no JITed implementation exists, return false.
|
||||
/// </summary>
|
||||
/// <param name="t">the static type of <paramref name="o"/></param>
|
||||
/// <param name="o">an instance of <paramref name="t"/> (can be a subclass)</param>
|
||||
/// <returns>true if a JITed implementation existed</returns>
|
||||
public bool Populate(Type t, object o)
|
||||
{
|
||||
if (!_populateImplementations.TryGetValue(t, out var implementation))
|
||||
{
|
||||
// pop out if we never JITed anything
|
||||
return false;
|
||||
}
|
||||
|
||||
implementation.Invoke(null, [null, o]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
17
Robust.Client/UserInterface/XAML/Proxy/XamlJitDelegate.cs
Normal file
17
Robust.Client/UserInterface/XAML/Proxy/XamlJitDelegate.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// This callback has the approximate type of <see cref="Robust.Xaml.XamlJitCompiler.Compile"/>,
|
||||
/// but it has no error-signaling faculty.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementors of this delegate should inform the users of errors in their own way.
|
||||
///
|
||||
/// Hot reloading failures should not directly take down the process, so implementors
|
||||
/// should not rethrow exceptions unless they have a strong reason to believe they
|
||||
/// will be caught.
|
||||
/// </remarks>
|
||||
internal delegate MethodInfo? XamlJitDelegate(Type type, Uri uri, string filename, string content);
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata to support JIT compilation of XAML resources for a type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We can feed XamlX data from this type, along with new content, to get new XAML
|
||||
/// resources.
|
||||
///
|
||||
/// This type is inert and is generated for release artifacts too, not just debug
|
||||
/// artifacts. Released content should support hot reloading if loaded in a debug
|
||||
/// client, but this is untested.
|
||||
/// </remarks>
|
||||
[AttributeUsage(validOn: AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class XamlMetadataAttribute: System.Attribute
|
||||
{
|
||||
public readonly string Uri;
|
||||
public readonly string FileName;
|
||||
public readonly string Content;
|
||||
|
||||
public XamlMetadataAttribute(string uri, string fileName, string content)
|
||||
{
|
||||
Uri = uri;
|
||||
FileName = fileName;
|
||||
Content = content;
|
||||
}
|
||||
}
|
||||
14
Robust.Client/UserInterface/XAML/Proxy/XamlProxyHelper.cs
Normal file
14
Robust.Client/UserInterface/XAML/Proxy/XamlProxyHelper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
internal sealed class XamlProxyHelper: IXamlProxyHelper
|
||||
{
|
||||
[Dependency] private IXamlProxyManager _xamlProxyManager = default!;
|
||||
|
||||
public bool Populate(Type t, object o)
|
||||
{
|
||||
return _xamlProxyManager.Populate(t, o);
|
||||
}
|
||||
}
|
||||
127
Robust.Client/UserInterface/XAML/Proxy/XamlProxyManager.cs
Normal file
127
Robust.Client/UserInterface/XAML/Proxy/XamlProxyManager.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Xaml;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// The real implementation of <see cref="IXamlProxyManager"/>.
|
||||
/// </summary>
|
||||
public sealed class XamlProxyManager: IXamlProxyManager
|
||||
{
|
||||
ISawmill _sawmill = null!;
|
||||
[Dependency] IReflectionManager _reflectionManager = null!;
|
||||
[Dependency] ILogManager _logManager = null!;
|
||||
|
||||
XamlImplementationStorage _xamlImplementationStorage = null!;
|
||||
|
||||
List<Assembly> _knownAssemblies = [];
|
||||
XamlJitCompiler? _xamlJitCompiler;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize this, subscribing to assembly changes.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("xamlhotreload");
|
||||
_xamlImplementationStorage = new XamlImplementationStorage(_sawmill, Compile);
|
||||
|
||||
AddAssemblies();
|
||||
_reflectionManager.OnAssemblyAdded += (_, _) => { AddAssemblies(); };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if setting the implementation of <paramref name="fileName" />
|
||||
/// would not be a no-op.
|
||||
/// </summary>
|
||||
/// <param name="fileName">the file name</param>
|
||||
/// <returns>true or false</returns>
|
||||
public bool CanSetImplementation(string fileName)
|
||||
{
|
||||
return _xamlImplementationStorage.CanSetImplementation(fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the implementation of <paramref name="fileName" />, failing
|
||||
/// silently if the new content does not compile. (but still logging)
|
||||
/// </summary>
|
||||
/// <param name="fileName">the file name</param>
|
||||
/// <param name="fileContent">the new content</param>
|
||||
public void SetImplementation(string fileName, string fileContent)
|
||||
{
|
||||
_xamlImplementationStorage.SetImplementation(fileName, fileContent, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add all the types from all known assemblies, then force-JIT everything
|
||||
/// again.
|
||||
/// </summary>
|
||||
private void AddAssemblies()
|
||||
{
|
||||
foreach (var a in _reflectionManager.Assemblies)
|
||||
{
|
||||
if (!_knownAssemblies.Contains(a))
|
||||
{
|
||||
_knownAssemblies.Add(a);
|
||||
_xamlImplementationStorage.Add(a);
|
||||
|
||||
_xamlJitCompiler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Always use the JITed versions on debug builds
|
||||
_xamlImplementationStorage.ForceReloadAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate <paramref name="o" /> using the JIT compiler, if possible.
|
||||
/// </summary>
|
||||
/// <param name="t">the static type of <paramref name="o" /></param>
|
||||
/// <param name="o">a <paramref name="t" /> instance or subclass</param>
|
||||
/// <returns>true if there was a JITed implementation</returns>
|
||||
public bool Populate(Type t, object o)
|
||||
{
|
||||
return _xamlImplementationStorage.Populate(t, o);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="XamlJitCompiler.Compile"/> using a stored
|
||||
/// <see cref="XamlJitCompiler"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="t">the <see cref="Type"/> that cares about this Xaml</param>
|
||||
/// <param name="uri">the <see cref="Uri" /> of this xaml (from the type's metadata)</param>
|
||||
/// <param name="fileName">the filename of this xaml (from the type's metadata)</param>
|
||||
/// <param name="content">the new content of the xaml file</param>
|
||||
/// <returns>the MethodInfo for the new JITed implementation</returns>
|
||||
private MethodInfo? Compile(Type t, Uri uri, string fileName, string content)
|
||||
{
|
||||
// initialize XamlJitCompiler lazily because constructing it has
|
||||
// very high CPU cost
|
||||
XamlJitCompiler xjit;
|
||||
lock(this)
|
||||
{
|
||||
xjit = _xamlJitCompiler ??= new XamlJitCompiler();
|
||||
}
|
||||
|
||||
var result = xjit.Compile(t, uri, fileName, content);
|
||||
|
||||
if (result is XamlJitCompilerResult.Error e)
|
||||
{
|
||||
_sawmill.Info($"hot reloading failed: {t.FullName}; {fileName}; {e.Raw.Message} {e.Hint ?? ""}");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result is XamlJitCompilerResult.Success s)
|
||||
{
|
||||
return s.MethodInfo;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"totally unexpected result from compiler operation: {result}");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// The stub implementation of <see cref="IXamlProxyManager"/>.
|
||||
/// </summary>
|
||||
public sealed class XamlProxyManagerStub: IXamlProxyManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Do nothing.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return false. Nothing is ever interested in a Xaml content update when
|
||||
/// hot reloading is off.
|
||||
/// </summary>
|
||||
/// <param name="fileName">the filename</param>
|
||||
/// <returns>false</returns>
|
||||
public bool CanSetImplementation(string fileName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do nothing. A hot reload will always silently fail if hot reloading is off.
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="fileContent"></param>
|
||||
public void SetImplementation(string fileName, string fileContent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return false.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There will never be a JIT-ed implementation of Populate if hot reloading is off.
|
||||
/// </remarks>
|
||||
/// <param name="t">the static type of <paramref name="o" /></param>
|
||||
/// <param name="o">an instance of <paramref name="t" /> or a subclass</param>
|
||||
/// <returns>false</returns>
|
||||
public bool Populate(Type t, object o)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,13 @@ public static class Matrix3Helpers
|
||||
return a.EqualsApprox(b, (float) tolerance);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Box2Rotated TransformBounds(this Matrix3x2 refFromBox, Box2Rotated box)
|
||||
{
|
||||
var matty = Matrix3x2.Multiply(refFromBox, box.Transform);
|
||||
return new Box2Rotated(Vector2.Transform(box.BottomLeft, matty), Vector2.Transform(box.TopRight, matty));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Box2 TransformBox(this Matrix3x2 refFromBox, Box2Rotated box)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,8 @@ AllowedVerifierErrors:
|
||||
- InterfaceMethodNotImplemented
|
||||
|
||||
# EVERYTHING in these namespaces is allowed.
|
||||
# Note that, due to a historical bug in the sandbox, any namespace _prefixed_ with one of these
|
||||
# is also allowed. (For instance, RobustBats.X, or ContentFarm.Y)
|
||||
WhitelistedNamespaces:
|
||||
- Robust
|
||||
- Content
|
||||
@@ -1054,6 +1056,7 @@ Types:
|
||||
DateTime: { All: True }
|
||||
DateTimeKind: { } # Enum
|
||||
DateTimeOffset: { All: True }
|
||||
Decimal: { All: True }
|
||||
Delegate:
|
||||
Methods:
|
||||
- "System.Delegate Combine(System.Delegate, System.Delegate)"
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
@@ -15,6 +16,7 @@ namespace Robust.Shared.GameObjects;
|
||||
/// Visualization works client side with derivatives of the <see cref="Robust.Client.GameObjects.VisualizerSystem">VisualizerSystem</see> class and corresponding components.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedAppearanceSystem))]
|
||||
public sealed partial class AppearanceComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
@@ -32,6 +34,19 @@ public sealed partial class AppearanceComponent : Component
|
||||
|
||||
[ViewVariables] internal Dictionary<Enum, object> AppearanceData = new();
|
||||
|
||||
private Dictionary<Enum, object>? _appearanceDataInit;
|
||||
|
||||
/// <summary>
|
||||
/// Sets starting values for AppearanceData.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Should only be filled in via prototype .yaml; subsequent data must be set via SharedAppearanceSystem.SetData().
|
||||
/// </remarks>
|
||||
[DataField(readOnly: true)] public Dictionary<Enum, object>? AppearanceDataInit {
|
||||
get { return _appearanceDataInit; }
|
||||
set { AppearanceData = value ?? AppearanceData; _appearanceDataInit = value; }
|
||||
}
|
||||
|
||||
[Obsolete("Use SharedAppearanceSystem instead")]
|
||||
public bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,13 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
protected internal BoundUserInterfaceState? State { get; internal set; }
|
||||
|
||||
// Bandaid just for storage :)
|
||||
/// <summary>
|
||||
/// Defers state handling
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public virtual bool DeferredClose { get; } = true;
|
||||
|
||||
protected BoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
@@ -34,16 +34,19 @@ namespace Robust.Shared.GameObjects
|
||||
[Serializable, NetSerializable]
|
||||
internal sealed class UserInterfaceComponentState(
|
||||
Dictionary<Enum, List<NetEntity>> actors,
|
||||
Dictionary<Enum, BoundUserInterfaceState> states)
|
||||
Dictionary<Enum, BoundUserInterfaceState> states,
|
||||
Dictionary<Enum, InterfaceData> data)
|
||||
: IComponentState
|
||||
{
|
||||
public Dictionary<Enum, List<NetEntity>> Actors = actors;
|
||||
|
||||
public Dictionary<Enum, BoundUserInterfaceState> States = states;
|
||||
|
||||
public Dictionary<Enum, InterfaceData> Data = data;
|
||||
}
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
[DataDefinition, Serializable, NetSerializable]
|
||||
public sealed partial class InterfaceData
|
||||
{
|
||||
[DataField("type", required: true)]
|
||||
@@ -65,6 +68,13 @@ namespace Robust.Shared.GameObjects
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public bool RequireInputValidation = true;
|
||||
|
||||
public InterfaceData(InterfaceData data)
|
||||
{
|
||||
ClientType = data.ClientType;
|
||||
InteractionRange = data.InteractionRange;
|
||||
RequireInputValidation = data.RequireInputValidation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -97,12 +98,17 @@ public sealed partial class EntityLookupSystem
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent _, ref EntityQueryState state) =>
|
||||
{
|
||||
state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, state.Transform, state.Flags);
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, localAabb, localTransform, state.Flags);
|
||||
return true;
|
||||
}, approx: true, includeMap: false);
|
||||
|
||||
var mapUid = _map.GetMapOrInvalid(mapId);
|
||||
AddEntitiesIntersecting(mapUid, intersecting, shape, shapeTransform, flags);
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, mapUid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
|
||||
AddEntitiesIntersecting(mapUid, intersecting, shape, localAabb, localTransform, flags);
|
||||
AddContained(intersecting, flags);
|
||||
}
|
||||
|
||||
@@ -110,23 +116,18 @@ public sealed partial class EntityLookupSystem
|
||||
EntityUid lookupUid,
|
||||
HashSet<EntityUid> intersecting,
|
||||
IPhysShape shape,
|
||||
Transform shapeTransform,
|
||||
Box2 localAABB,
|
||||
Transform localShapeTransform,
|
||||
LookupFlags flags,
|
||||
BroadphaseComponent? lookup = null)
|
||||
{
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
var (_, lookupRot, lookupInvMatrix) = _transform.GetWorldPositionRotationInvMatrix(lookupUid);
|
||||
var lookupTransform = new Transform(Vector2.Transform(shapeTransform.Position, lookupInvMatrix),
|
||||
shapeTransform.Quaternion2D.Angle - lookupRot);
|
||||
|
||||
var localAABB = shape.ComputeAABB(lookupTransform, 0);
|
||||
|
||||
var state = new EntityQueryState(
|
||||
intersecting,
|
||||
shape,
|
||||
shapeTransform,
|
||||
localShapeTransform,
|
||||
_fixtures,
|
||||
this,
|
||||
_physics,
|
||||
@@ -167,7 +168,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
if (!approx)
|
||||
{
|
||||
var intersectingTransform = state.Physics.GetPhysicsTransform(value.Entity);
|
||||
var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value.Entity);
|
||||
if (!state.Manifolds.TestOverlap(state.Shape, 0, value.Fixture.Shape, value.ChildIndex, state.Transform, intersectingTransform))
|
||||
{
|
||||
return true;
|
||||
@@ -188,7 +189,7 @@ public sealed partial class EntityLookupSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
var intersectingTransform = state.Physics.GetPhysicsTransform(value);
|
||||
var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value);
|
||||
|
||||
if (state.FixturesQuery.TryGetComponent(value, out var fixtures))
|
||||
{
|
||||
@@ -248,7 +249,10 @@ public sealed partial class EntityLookupSystem
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent _, ref AnyEntityQueryState state) =>
|
||||
{
|
||||
if (state.Lookup.AnyEntitiesIntersecting(uid, state.Shape, state.Transform, state.Flags, ignored: state.Ignored))
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
|
||||
if (state.Lookup.AnyEntitiesIntersecting(uid, state.Shape, localAabb, state.Transform, state.Flags, ignored: state.Ignored))
|
||||
{
|
||||
state.Found = true;
|
||||
return false;
|
||||
@@ -260,7 +264,9 @@ public sealed partial class EntityLookupSystem
|
||||
if (!state.Found)
|
||||
{
|
||||
var mapUid = _map.GetMapOrInvalid(mapId);
|
||||
state.Found = AnyEntitiesIntersecting(mapUid, shape, shapeTransform, flags, ignored);
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, mapUid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
state.Found = AnyEntitiesIntersecting(mapUid, shape, localAabb, shapeTransform, flags, ignored);
|
||||
}
|
||||
|
||||
return state.Found;
|
||||
@@ -268,6 +274,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
private bool AnyEntitiesIntersecting(EntityUid lookupUid,
|
||||
IPhysShape shape,
|
||||
Box2 localAABB,
|
||||
Transform shapeTransform,
|
||||
LookupFlags flags,
|
||||
EntityUid? ignored = null,
|
||||
@@ -287,15 +294,6 @@ public sealed partial class EntityLookupSystem
|
||||
_fixturesQuery,
|
||||
flags);
|
||||
|
||||
// Shape gets passed in via local terms
|
||||
// Transform is in world terms
|
||||
// Need to convert both back to lookup-local for AABB.
|
||||
var (_, lookupRot, lookupInvMatrix) = _transform.GetWorldPositionRotationInvMatrix(lookupUid);
|
||||
var lookupTransform = new Transform(Vector2.Transform(shapeTransform.Position, lookupInvMatrix),
|
||||
shapeTransform.Quaternion2D.Angle - lookupRot);
|
||||
|
||||
var localAABB = shape.ComputeAABB(lookupTransform, 0);
|
||||
|
||||
if ((flags & LookupFlags.Dynamic) != 0x0)
|
||||
{
|
||||
lookup.DynamicTree.QueryAabb(ref state, PhysicsQuery, localAABB, true);
|
||||
@@ -341,7 +339,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
if (!approx)
|
||||
{
|
||||
var intersectingTransform = state.Physics.GetPhysicsTransform(value.Entity);
|
||||
var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value.Entity);
|
||||
if (!state.Manifolds.TestOverlap(state.Shape, 0, value.Fixture.Shape, value.ChildIndex, state.Transform, intersectingTransform))
|
||||
{
|
||||
return true;
|
||||
@@ -365,7 +363,7 @@ public sealed partial class EntityLookupSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
var intersectingTransform = state.Physics.GetPhysicsTransform(value);
|
||||
var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value);
|
||||
|
||||
if (state.FixturesQuery.TryGetComponent(value, out var fixtures))
|
||||
{
|
||||
@@ -408,11 +406,12 @@ public sealed partial class EntityLookupSystem
|
||||
LookupFlags flags,
|
||||
EntityUid? ignored = null)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.Set(worldBounds);
|
||||
var transform = Physics.Transform.Empty;
|
||||
var broadphaseInv = _transform.GetInvWorldMatrix(lookupUid);
|
||||
|
||||
return AnyEntitiesIntersecting(lookupUid, shape, transform, flags, ignored);
|
||||
var localBounds = broadphaseInv.TransformBounds(worldBounds);
|
||||
var shape = new Polygon(localBounds);
|
||||
|
||||
return AnyEntitiesIntersecting(lookupUid, shape, localBounds.CalcBoundingBox(), Physics.Transform.Empty, flags, ignored);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -547,24 +546,38 @@ public sealed partial class EntityLookupSystem
|
||||
var mapUid = _map.GetMapOrInvalid(mapId);
|
||||
|
||||
// Get grid entities
|
||||
var shape = new PolygonShape();
|
||||
shape.Set(worldBounds);
|
||||
var shape = new Polygon(worldBounds);
|
||||
var transform = Physics.Transform.Empty;
|
||||
|
||||
var state = (this, intersecting, shape, flags);
|
||||
var state = (this, _physics, intersecting, transform, shape, flags);
|
||||
|
||||
_mapManager.FindGridsIntersecting(mapUid, shape, Physics.Transform.Empty, ref state, static
|
||||
(EntityUid uid, MapGridComponent _,
|
||||
ref (EntityLookupSystem lookup,
|
||||
HashSet<EntityUid> intersecting,
|
||||
PolygonShape shape,
|
||||
LookupFlags flags) tuple) =>
|
||||
{
|
||||
tuple.lookup.AddEntitiesIntersecting(uid, tuple.intersecting, tuple.shape, Physics.Transform.Empty, tuple.flags);
|
||||
return true;
|
||||
}, approx: true, includeMap: false);
|
||||
_mapManager.FindGridsIntersecting(mapUid, shape, transform, ref state,
|
||||
static (
|
||||
EntityUid uid,
|
||||
MapGridComponent grid,
|
||||
ref (EntityLookupSystem lookup,
|
||||
SharedPhysicsSystem _physics,
|
||||
HashSet<EntityUid> intersecting,
|
||||
Transform transform,
|
||||
Polygon shape, LookupFlags flags) state) =>
|
||||
{
|
||||
var localTransform = state._physics.GetRelativePhysicsTransform(state.transform, uid);
|
||||
var localAabb = state.shape.ComputeAABB(localTransform, 0);
|
||||
|
||||
state.lookup.AddEntitiesIntersecting(uid,
|
||||
state.intersecting,
|
||||
state.shape,
|
||||
localAabb,
|
||||
state.transform,
|
||||
state.flags);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Get map entities
|
||||
AddEntitiesIntersecting(mapUid, intersecting, shape, Physics.Transform.Empty, flags);
|
||||
var localTransform = _physics.GetRelativePhysicsTransform(transform, mapUid);
|
||||
var localAabb = shape.ComputeAABB(localTransform, 0);
|
||||
|
||||
AddEntitiesIntersecting(mapUid, intersecting, shape, localAabb, transform, flags);
|
||||
AddContained(intersecting, flags);
|
||||
|
||||
return intersecting;
|
||||
@@ -591,19 +604,24 @@ public sealed partial class EntityLookupSystem
|
||||
var circle = new PhysShapeCircle(range, mapPos.Position);
|
||||
|
||||
const bool found = false;
|
||||
var state = (this, worldAABB, circle, flags, found, uid);
|
||||
var transform = Physics.Transform.Empty;
|
||||
var state = (this, _physics, transform, circle, flags, found, uid);
|
||||
|
||||
_mapManager.FindGridsIntersecting(mapPos.MapId, worldAABB, ref state, static (
|
||||
EntityUid gridUid,
|
||||
MapGridComponent _, ref (
|
||||
EntityLookupSystem lookup,
|
||||
Box2 worldAABB,
|
||||
SharedPhysicsSystem physics,
|
||||
Transform worldTransform,
|
||||
PhysShapeCircle circle,
|
||||
LookupFlags flags,
|
||||
bool found,
|
||||
EntityUid ignored) tuple) =>
|
||||
{
|
||||
if (tuple.lookup.AnyEntitiesIntersecting(gridUid, tuple.circle, Physics.Transform.Empty, tuple.flags, tuple.ignored))
|
||||
var localTransform = tuple.physics.GetRelativePhysicsTransform(tuple.worldTransform, gridUid);
|
||||
var localAabb = tuple.circle.ComputeAABB(localTransform, 0);
|
||||
|
||||
if (tuple.lookup.AnyEntitiesIntersecting(gridUid, tuple.circle, localAabb, localTransform, tuple.flags, tuple.ignored))
|
||||
{
|
||||
tuple.found = true;
|
||||
return false;
|
||||
@@ -618,7 +636,10 @@ public sealed partial class EntityLookupSystem
|
||||
}
|
||||
|
||||
var mapUid = _map.GetMapOrInvalid(mapPos.MapId);
|
||||
return AnyEntitiesIntersecting(mapUid, circle, Physics.Transform.Empty, flags, uid);
|
||||
var localTransform = _physics.GetRelativePhysicsTransform(transform, uid);
|
||||
var localAabb = circle.ComputeAABB(localTransform, 0);
|
||||
|
||||
return AnyEntitiesIntersecting(mapUid, circle, localAabb, localTransform, flags, uid);
|
||||
}
|
||||
|
||||
public HashSet<EntityUid> GetEntitiesInRange(EntityUid uid, float range, LookupFlags flags = DefaultFlags)
|
||||
@@ -656,15 +677,16 @@ public sealed partial class EntityLookupSystem
|
||||
var worldAABB = GetAABBNoContainer(uid, worldPos, worldRot);
|
||||
var existing = intersecting.Contains(uid);
|
||||
var transform = new Transform(worldPos, worldRot);
|
||||
var state = (uid, transform, intersecting, _fixturesQuery, this, flags);
|
||||
|
||||
var state = (uid, transform, intersecting, _fixturesQuery, this, _physics, flags);
|
||||
|
||||
// Unfortuantely I can't think of a way to de-dupe this with the other ones as it's slightly different.
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid gridUid, MapGridComponent grid,
|
||||
ref (EntityUid entity, Transform transform, HashSet<EntityUid> intersecting,
|
||||
EntityQuery<FixturesComponent> fixturesQuery, EntityLookupSystem lookup, LookupFlags flags) tuple) =>
|
||||
EntityQuery<FixturesComponent> fixturesQuery, EntityLookupSystem lookup, SharedPhysicsSystem physics, LookupFlags flags) state) =>
|
||||
{
|
||||
EntityIntersectingQuery(gridUid, tuple);
|
||||
EntityIntersectingQuery(gridUid, state);
|
||||
|
||||
return true;
|
||||
}, approx: true, includeMap: false);
|
||||
@@ -681,24 +703,29 @@ public sealed partial class EntityLookupSystem
|
||||
return;
|
||||
|
||||
static void EntityIntersectingQuery(EntityUid lookupUid, (EntityUid entity, Transform shapeTransform, HashSet<EntityUid> intersecting,
|
||||
EntityQuery<FixturesComponent> fixturesQuery, EntityLookupSystem lookup, LookupFlags flags) tuple)
|
||||
EntityQuery<FixturesComponent> fixturesQuery, EntityLookupSystem lookup, SharedPhysicsSystem physics, LookupFlags flags) state)
|
||||
{
|
||||
if (tuple.fixturesQuery.TryGetComponent(tuple.entity, out var fixturesComp))
|
||||
var localTransform = state.physics.GetRelativePhysicsTransform(state.shapeTransform, lookupUid);
|
||||
|
||||
if (state.fixturesQuery.TryGetComponent(state.entity, out var fixturesComp))
|
||||
{
|
||||
foreach (var fixture in fixturesComp.Fixtures.Values)
|
||||
{
|
||||
// If our own fixture isn't hard and sensors ignored then ignore it.
|
||||
if (!fixture.Hard && (tuple.flags & LookupFlags.Sensors) == 0x0)
|
||||
if (!fixture.Hard && (state.flags & LookupFlags.Sensors) == 0x0)
|
||||
continue;
|
||||
|
||||
tuple.lookup.AddEntitiesIntersecting(lookupUid, tuple.intersecting, fixture.Shape, tuple.shapeTransform, tuple.flags);
|
||||
var localAabb = fixture.Shape.ComputeAABB(localTransform, 0);
|
||||
state.lookup.AddEntitiesIntersecting(lookupUid, state.intersecting, fixture.Shape, localAabb, localTransform, state.flags);
|
||||
}
|
||||
}
|
||||
// Single point check
|
||||
else
|
||||
{
|
||||
var shape = new PhysShapeCircle(LookupEpsilon);
|
||||
tuple.lookup.AddEntitiesIntersecting(lookupUid, tuple.intersecting, shape, tuple.shapeTransform, tuple.flags);
|
||||
var localAabb = shape.ComputeAABB(localTransform, 0);
|
||||
|
||||
state.lookup.AddEntitiesIntersecting(lookupUid, state.intersecting, shape, localAabb, localTransform, state.flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -840,10 +867,10 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.TryGetComponent(gridId, out var lookup))
|
||||
return;
|
||||
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(worldAABB);
|
||||
var localAABB = _transform.GetInvWorldMatrix(gridId).TransformBox(worldAABB);
|
||||
var shape = new Polygon(localAABB);
|
||||
|
||||
AddEntitiesIntersecting(gridId, intersecting, shape, Physics.Transform.Empty, flags, lookup);
|
||||
AddEntitiesIntersecting(gridId, intersecting, shape, localAABB, Physics.Transform.Empty, flags, lookup);
|
||||
AddContained(intersecting, flags);
|
||||
}
|
||||
|
||||
@@ -852,10 +879,10 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.TryGetComponent(gridId, out var lookup))
|
||||
return;
|
||||
|
||||
var shape = new PolygonShape();
|
||||
shape.Set(worldBounds);
|
||||
var localBounds = _transform.GetInvWorldMatrix(gridId).TransformBounds(worldBounds);
|
||||
var shape = new Polygon(localBounds);
|
||||
|
||||
AddEntitiesIntersecting(gridId, intersecting, shape, Physics.Transform.Empty, flags, lookup);
|
||||
AddEntitiesIntersecting(gridId, intersecting, shape, localBounds.CalcBoundingBox(), Physics.Transform.Empty, flags, lookup);
|
||||
AddContained(intersecting, flags);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
using TerraFX.Interop.Windows;
|
||||
@@ -121,19 +122,17 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
var lookupPoly = new PolygonShape();
|
||||
lookupPoly.SetAsBox(localAABB);
|
||||
var (lookupPos, lookupRot) = _transform.GetWorldPositionRotation(lookupUid);
|
||||
var transform = new Transform(lookupPos, lookupRot);
|
||||
var lookupPoly = new Polygon(localAABB);
|
||||
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, lookupPoly, transform, flags, query, lookup);
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, lookupPoly, localAABB, Physics.Transform.Empty, flags, query, lookup);
|
||||
}
|
||||
|
||||
private void AddEntitiesIntersecting<T>(
|
||||
EntityUid lookupUid,
|
||||
HashSet<Entity<T>> intersecting,
|
||||
IPhysShape shape,
|
||||
Transform shapeTransform,
|
||||
Box2 localAABB,
|
||||
Transform localTransform,
|
||||
LookupFlags flags,
|
||||
EntityQuery<T> query,
|
||||
BroadphaseComponent? lookup = null) where T : IComponent
|
||||
@@ -141,19 +140,10 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
// Shape gets passed in via local terms
|
||||
// Transform is in world terms
|
||||
// Need to convert both back to lookup-local for AABB.
|
||||
var (_, lookupRot, lookupInvMatrix) = _transform.GetWorldPositionRotationInvMatrix(lookupUid);
|
||||
var lookupTransform = new Transform(Vector2.Transform(shapeTransform.Position, lookupInvMatrix),
|
||||
shapeTransform.Quaternion2D.Angle - lookupRot);
|
||||
|
||||
var localAABB = shape.ComputeAABB(lookupTransform, 0);
|
||||
|
||||
var state = new QueryState<T>(
|
||||
intersecting,
|
||||
shape,
|
||||
shapeTransform,
|
||||
localTransform,
|
||||
_fixtures,
|
||||
_physics,
|
||||
_manifoldManager,
|
||||
@@ -195,7 +185,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
if (!state.Approximate)
|
||||
{
|
||||
var intersectingTransform = state.Physics.GetPhysicsTransform(value.Entity);
|
||||
var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value.Entity);
|
||||
if (!state.Manifolds.TestOverlap(state.Shape, 0, value.Fixture.Shape, value.ChildIndex, state.Transform, intersectingTransform))
|
||||
{
|
||||
return true;
|
||||
@@ -217,7 +207,7 @@ public sealed partial class EntityLookupSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
var intersectingTransform = state.Physics.GetPhysicsTransform(value);
|
||||
var intersectingTransform = state.Physics.GetLocalPhysicsTransform(value);
|
||||
|
||||
if (state.FixturesQuery.TryGetComponent(value, out var fixtures))
|
||||
{
|
||||
@@ -261,17 +251,17 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return false;
|
||||
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(localAABB);
|
||||
var shape = new Polygon(localAABB);
|
||||
var (lookupPos, lookupRot) = _transform.GetWorldPositionRotation(lookupUid);
|
||||
var transform = new Transform(lookupPos, lookupRot);
|
||||
|
||||
return AnyComponentsIntersecting(lookupUid, shape, transform, flags, query, ignored, lookup);
|
||||
return AnyComponentsIntersecting(lookupUid, shape, localAABB, transform, flags, query, ignored, lookup);
|
||||
}
|
||||
|
||||
private bool AnyComponentsIntersecting<T>(
|
||||
EntityUid lookupUid,
|
||||
IPhysShape shape,
|
||||
Box2 localAABB,
|
||||
Transform shapeTransform,
|
||||
LookupFlags flags,
|
||||
EntityQuery<T> query,
|
||||
@@ -286,12 +276,6 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return false;
|
||||
|
||||
var (_, lookupRot, lookupInvMatrix) = _transform.GetWorldPositionRotationInvMatrix(lookupUid);
|
||||
var lookupTransform = new Transform(Vector2.Transform(shapeTransform.Position, lookupInvMatrix),
|
||||
shapeTransform.Quaternion2D.Angle - lookupRot);
|
||||
|
||||
var localAABB = shape.ComputeAABB(lookupTransform, 0);
|
||||
|
||||
var state = new AnyQueryState<T>(false,
|
||||
ignored,
|
||||
shape,
|
||||
@@ -438,8 +422,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
public bool AnyComponentsIntersecting(Type type, MapId mapId, Box2 worldAABB, EntityUid? ignored = null, LookupFlags flags = DefaultFlags)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(worldAABB);
|
||||
var shape = new Polygon(worldAABB);
|
||||
var transform = Physics.Transform.Empty;
|
||||
|
||||
return AnyComponentsIntersecting(type, mapId, shape, transform, ignored, flags);
|
||||
@@ -507,8 +490,7 @@ public sealed partial class EntityLookupSystem
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(worldAABB);
|
||||
var shape = new Polygon(worldAABB);
|
||||
var transform = Physics.Transform.Empty;
|
||||
|
||||
GetEntitiesIntersecting(type, mapId, shape, transform, intersecting, flags);
|
||||
@@ -518,8 +500,7 @@ public sealed partial class EntityLookupSystem
|
||||
{
|
||||
if (mapId == MapId.Nullspace) return;
|
||||
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(worldAABB);
|
||||
var shape = new Polygon(worldAABB);
|
||||
var shapeTransform = Physics.Transform.Empty;
|
||||
|
||||
GetEntitiesIntersecting(mapId, shape, shapeTransform, entities, flags);
|
||||
@@ -554,17 +535,22 @@ public sealed partial class EntityLookupSystem
|
||||
var query = EntityManager.GetEntityQuery(type);
|
||||
|
||||
// Get grid entities
|
||||
var state = new GridQueryState<IComponent>(intersecting, shape, shapeTransform, this, flags, query);
|
||||
var state = new GridQueryState<IComponent>(intersecting, shape, shapeTransform, this, _physics, flags, query);
|
||||
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent grid, ref GridQueryState<IComponent> state) =>
|
||||
{
|
||||
state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, state.Transform, state.Flags, state.Query);
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, localAabb, state.Transform, state.Flags, state.Query);
|
||||
return true;
|
||||
}, approx: true, includeMap: false);
|
||||
|
||||
var mapUid = _map.GetMapOrInvalid(mapId);
|
||||
AddEntitiesIntersecting(mapUid, intersecting, shape, shapeTransform, flags, query);
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, mapUid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
|
||||
AddEntitiesIntersecting(mapUid, intersecting, shape, localAabb, shapeTransform, flags, query);
|
||||
|
||||
AddContained(intersecting, flags, query);
|
||||
}
|
||||
@@ -593,19 +579,23 @@ public sealed partial class EntityLookupSystem
|
||||
var query = GetEntityQuery<T>();
|
||||
|
||||
// Get grid entities
|
||||
var state = new GridQueryState<T>(entities, shape, shapeTransform, this, flags, query);
|
||||
var state = new GridQueryState<T>(entities, shape, shapeTransform, this, _physics, flags, query);
|
||||
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent grid, ref GridQueryState<T> state) =>
|
||||
{
|
||||
state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, state.Transform, state.Flags, state.Query);
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, localAabb, state.Transform, state.Flags, state.Query);
|
||||
return true;
|
||||
}, approx: true, includeMap: false);
|
||||
|
||||
// Get map entities
|
||||
var mapUid = _map.GetMapOrInvalid(mapId);
|
||||
AddEntitiesIntersecting(mapUid, entities, shape, shapeTransform, flags, query);
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, mapUid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
|
||||
AddEntitiesIntersecting(mapUid, entities, shape, localAabb, shapeTransform, flags, query);
|
||||
AddContained(entities, flags, query);
|
||||
}
|
||||
}
|
||||
@@ -778,6 +768,20 @@ public sealed partial class EntityLookupSystem
|
||||
AddContained(intersecting, flags, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entities intersecting the specified broadphase entity using a local AABB.
|
||||
/// </summary>
|
||||
public void GetLocalEntitiesIntersecting<T>(
|
||||
Entity<BroadphaseComponent> grid,
|
||||
Box2 localAABB,
|
||||
HashSet<Entity<T>> intersecting,
|
||||
EntityQuery<T> query,
|
||||
LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
{
|
||||
AddLocalEntitiesIntersecting(grid, intersecting, localAABB, flags, query, grid.Comp);
|
||||
AddContained(intersecting, flags, query);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -819,6 +823,7 @@ public sealed partial class EntityLookupSystem
|
||||
IPhysShape Shape,
|
||||
Transform Transform,
|
||||
EntityLookupSystem Lookup,
|
||||
SharedPhysicsSystem Physics,
|
||||
LookupFlags Flags,
|
||||
EntityQuery<T> Query
|
||||
) where T : IComponent;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
@@ -25,12 +26,8 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
var lookupPoly = new PolygonShape();
|
||||
lookupPoly.SetAsBox(localAABB);
|
||||
var (lookupPos, lookupRot) = _transform.GetWorldPositionRotation(lookupUid);
|
||||
var lookupTransform = new Transform(lookupPos, lookupRot);
|
||||
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, lookupPoly, lookupTransform, flags, lookup);
|
||||
var lookupPoly = new Polygon(localAABB);
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, lookupPoly, localAABB, Physics.Transform.Empty, flags, lookup);
|
||||
}
|
||||
|
||||
private void AddLocalEntitiesIntersecting(
|
||||
@@ -43,11 +40,10 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
var shape = new PolygonShape();
|
||||
shape.Set(localBounds);
|
||||
var shape = new Polygon(localBounds);
|
||||
var localAABB = localBounds.CalcBoundingBox();
|
||||
|
||||
var transform = _physics.GetPhysicsTransform(lookupUid);
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, shape, transform, flags);
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, shape, localAABB, Physics.Transform.Empty, flags);
|
||||
}
|
||||
|
||||
public bool AnyLocalEntitiesIntersecting(EntityUid lookupUid,
|
||||
@@ -59,10 +55,8 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return false;
|
||||
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(localAABB);
|
||||
var transform = _physics.GetPhysicsTransform(lookupUid);
|
||||
return AnyEntitiesIntersecting(lookupUid, shape, transform, flags, ignored, lookup);
|
||||
var shape = new Polygon(localAABB);
|
||||
return AnyEntitiesIntersecting(lookupUid, shape, localAABB, Physics.Transform.Empty, flags, ignored, lookup);
|
||||
}
|
||||
|
||||
public HashSet<EntityUid> GetLocalEntitiesIntersecting(EntityUid gridId, Vector2i gridIndices, float enlargement = TileEnlargementRadius, LookupFlags flags = DefaultFlags, MapGridComponent? gridComp = null)
|
||||
@@ -88,7 +82,7 @@ public sealed partial class EntityLookupSystem
|
||||
}
|
||||
|
||||
var localAABB = GetLocalBounds(localTile, tileSize);
|
||||
localAABB = localAABB.Enlarged(TileEnlargementRadius);
|
||||
localAABB = localAABB.Enlarged(enlargement);
|
||||
GetLocalEntitiesIntersecting(gridUid, localAABB, intersecting, flags);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.BroadPhase;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Events;
|
||||
@@ -95,6 +96,10 @@ public sealed partial class EntityLookupSystem : EntitySystem
|
||||
private EntityQuery<PhysicsMapComponent> _mapQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
/// <summary>
|
||||
/// 1 x 1 polygons can overlap neighboring tiles (even without considering the polygon skin around them.
|
||||
/// When querying for specific tile fixtures we shrink the bounds by this amount to avoid this overlap.
|
||||
/// </summary>
|
||||
public const float TileEnlargementRadius = -PhysicsConstants.PolygonRadius * 4f;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -115,6 +115,32 @@ public abstract class SharedAppearanceSystem : EntitySystem
|
||||
Dirty(dest, dest.Comp);
|
||||
QueueUpdate(dest, dest.Comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends appearance data from <c>src</c> to <c>dest</c>. If a key/value pair already exists in <c>dest</c>, it gets replaced.
|
||||
/// If <c>src</c> has no <see cref="AppearanceComponent"/> nothing is done.
|
||||
/// If <c>dest</c> has no <c>AppearanceComponent</c> then it is created.
|
||||
/// </summary>
|
||||
public void AppendData(Entity<AppearanceComponent?> src, Entity<AppearanceComponent?> dest)
|
||||
{
|
||||
if (!Resolve(src, ref src.Comp, false))
|
||||
return;
|
||||
|
||||
AppendData(src.Comp, dest);
|
||||
}
|
||||
|
||||
public void AppendData(AppearanceComponent srcComp, Entity<AppearanceComponent?> dest)
|
||||
{
|
||||
dest.Comp ??= EnsureComp<AppearanceComponent>(dest);
|
||||
|
||||
foreach (var (key, value) in srcComp.AppearanceData)
|
||||
{
|
||||
dest.Comp.AppearanceData[key] = value;
|
||||
}
|
||||
|
||||
Dirty(dest, dest.Comp);
|
||||
QueueUpdate(dest, dest.Comp);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
@@ -39,7 +39,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Defer closing BUIs during state handling so client doesn't spam a BUI constantly during prediction.
|
||||
/// </summary>
|
||||
private HashSet<BoundUserInterface> _queuedCloses = new();
|
||||
private readonly List<BoundUserInterface> _queuedCloses = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -221,10 +221,15 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
// If we're client we want this handled immediately.
|
||||
if (ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
|
||||
{
|
||||
_queuedCloses.Add(cBui);
|
||||
if (cBui.DeferredClose)
|
||||
_queuedCloses.Add(cBui);
|
||||
else
|
||||
{
|
||||
ent.Comp.ClientOpenInterfaces.Remove(key);
|
||||
cBui.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (ent.Comp.Actors.Count == 0)
|
||||
@@ -300,7 +305,15 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
// I.e., don't resend the whole BUI state just because a new user opened it.
|
||||
|
||||
var actors = new Dictionary<Enum, List<NetEntity>>();
|
||||
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, ent.Comp.States);
|
||||
|
||||
var dataCopy = new Dictionary<Enum, InterfaceData>();
|
||||
|
||||
foreach (var (weh, a) in ent.Comp.Interfaces)
|
||||
{
|
||||
dataCopy[weh] = new InterfaceData(a);
|
||||
}
|
||||
|
||||
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, ent.Comp.States, dataCopy);
|
||||
|
||||
// Ensure that only the player that currently has the UI open gets to know what they have it open.
|
||||
if (args.ReplayState)
|
||||
@@ -326,6 +339,13 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (args.Current is not UserInterfaceComponent.UserInterfaceComponentState state)
|
||||
return;
|
||||
|
||||
ent.Comp.Interfaces.Clear();
|
||||
|
||||
foreach (var data in state.Data)
|
||||
{
|
||||
ent.Comp.Interfaces[data.Key] = new(data.Value);
|
||||
}
|
||||
|
||||
foreach (var key in ent.Comp.Actors.Keys)
|
||||
{
|
||||
if (!state.Actors.ContainsKey(key))
|
||||
@@ -371,9 +391,10 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
}
|
||||
|
||||
var attachedEnt = _player.LocalEntity;
|
||||
var clientBuis = new ValueList<Enum>(ent.Comp.ClientOpenInterfaces.Keys);
|
||||
|
||||
// Check if the UI is open by us, otherwise dispose of it.
|
||||
foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces)
|
||||
foreach (var key in clientBuis)
|
||||
{
|
||||
if (ent.Comp.Actors.TryGetValue(key, out var actors) &&
|
||||
(attachedEnt == null || actors.Contains(attachedEnt.Value)))
|
||||
@@ -381,7 +402,15 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
continue;
|
||||
}
|
||||
|
||||
_queuedCloses.Add(bui);
|
||||
var bui = ent.Comp.ClientOpenInterfaces[key];
|
||||
|
||||
if (bui.DeferredClose)
|
||||
_queuedCloses.Add(bui);
|
||||
else
|
||||
{
|
||||
ent.Comp.ClientOpenInterfaces.Remove(key);
|
||||
bui.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// update any states we have open
|
||||
@@ -423,6 +452,15 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
// If it's out BUI open it up and apply the state, otherwise do nothing.
|
||||
var player = Player.LocalEntity;
|
||||
|
||||
// Existing BUI just keep it.
|
||||
if (entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var existing))
|
||||
{
|
||||
if (existing.DeferredClose)
|
||||
_queuedCloses.Remove(existing);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (player == null ||
|
||||
!entity.Comp.Actors.TryGetValue(key, out var actors) ||
|
||||
!actors.Contains(player.Value))
|
||||
@@ -432,13 +470,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
DebugTools.Assert(_netManager.IsClient);
|
||||
|
||||
// Existing BUI just keep it.
|
||||
if (entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var existing))
|
||||
{
|
||||
_queuedCloses.Remove(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try-catch to try prevent error loops / bricked clients that constantly throw exceptions while applying game
|
||||
// states. E.g., stripping UI used to throw NREs in some instances while fetching the identity of unknown
|
||||
// entities.
|
||||
@@ -930,7 +961,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
{
|
||||
foreach (var bui in _queuedCloses)
|
||||
{
|
||||
if (TryComp(bui.Owner, out UserInterfaceComponent? uiComp))
|
||||
if (UIQuery.TryComp(bui.Owner, out var uiComp))
|
||||
{
|
||||
uiComp.ClientOpenInterfaces.Remove(bui.UiKey);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Robust.Shared.Map
|
||||
public void FindGridsIntersecting(MapId mapId, IPhysShape shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, PolygonShape shape, Transform transform, GridCallback callback,
|
||||
public void FindGridsIntersecting(MapId mapId, IPhysShape shape, Transform transform, GridCallback callback,
|
||||
bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = Approximate,
|
||||
@@ -116,7 +116,7 @@ namespace Robust.Shared.Map
|
||||
|
||||
#region MapEnt
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, PolygonShape shape, Transform transform, GridCallback callback,
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Transform transform, GridCallback callback,
|
||||
bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
|
||||
public void FindGridsIntersecting<TState>(EntityUid mapEnt, IPhysShape shape, Transform transform,
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.Map.Enumerators;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
|
||||
namespace Robust.Shared.Map;
|
||||
|
||||
@@ -47,7 +48,7 @@ internal partial class MapManager
|
||||
FindGridsIntersecting(mapEnt, shape, transform, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, PolygonShape shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
public void FindGridsIntersecting(MapId mapId, IPhysShape shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, shape, transform, callback, includeMap, approx);
|
||||
@@ -97,56 +98,40 @@ internal partial class MapManager
|
||||
|
||||
#region MapEnt
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, PolygonShape shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
public void FindGridsIntersecting(
|
||||
EntityUid mapEnt,
|
||||
IPhysShape shape,
|
||||
Transform transform,
|
||||
GridCallback callback,
|
||||
bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (!_gridTreeQuery.TryGetComponent(mapEnt, out var gridTree))
|
||||
return;
|
||||
|
||||
if (includeMap && _gridQuery.TryGetComponent(mapEnt, out var mapGrid))
|
||||
{
|
||||
callback(mapEnt, mapGrid);
|
||||
}
|
||||
|
||||
var worldAABB = shape.ComputeAABB(transform, 0);
|
||||
|
||||
var gridState = new GridQueryState(
|
||||
callback,
|
||||
worldAABB,
|
||||
shape,
|
||||
transform,
|
||||
gridTree.Tree,
|
||||
_mapSystem,
|
||||
this,
|
||||
_transformSystem,
|
||||
approx);
|
||||
|
||||
|
||||
gridTree.Tree.Query(ref gridState, static (ref GridQueryState state, DynamicTree.Proxy proxy) =>
|
||||
{
|
||||
// Even for approximate we'll check if any chunks roughly overlap.
|
||||
var data = state.Tree.GetUserData(proxy);
|
||||
var gridInvMatrix = state.TransformSystem.GetInvWorldMatrix(data.Uid);
|
||||
var localAABB = gridInvMatrix.TransformBox(state.WorldAABB);
|
||||
|
||||
var overlappingChunks = state.MapSystem.GetLocalMapChunks(data.Uid, data.Grid, localAABB);
|
||||
|
||||
if (state.Approximate)
|
||||
{
|
||||
if (!overlappingChunks.MoveNext(out _))
|
||||
return true;
|
||||
}
|
||||
else if (!state.MapManager.IsIntersecting(overlappingChunks, state.Shape, state.Transform, data.Uid))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
state.Callback(data.Uid, data.Grid);
|
||||
|
||||
return true;
|
||||
}, worldAABB);
|
||||
FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(EntityUid mapEnt, IPhysShape shape, Transform transform,
|
||||
private void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Box2 worldAABB, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
// This is here so we don't double up on code.
|
||||
var state = callback;
|
||||
|
||||
FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state,
|
||||
static (EntityUid uid, MapGridComponent grid, ref GridCallback state) => state.Invoke(uid, grid),
|
||||
approx: approx, includeMap: includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(
|
||||
EntityUid mapEnt,
|
||||
IPhysShape shape,
|
||||
Transform transform,
|
||||
ref TState state,
|
||||
GridCallback<TState> callback,
|
||||
bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref state, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
private void FindGridsIntersecting<TState>(EntityUid mapEnt, IPhysShape shape, Box2 worldAABB, Transform transform,
|
||||
ref TState state, GridCallback<TState> callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (!_gridTreeQuery.TryGetComponent(mapEnt, out var gridTree))
|
||||
@@ -157,8 +142,6 @@ internal partial class MapManager
|
||||
callback(mapEnt, mapGrid, ref state);
|
||||
}
|
||||
|
||||
var worldAABB = shape.ComputeAABB(transform, 0);
|
||||
|
||||
var gridState = new GridQueryState<TState>(
|
||||
callback,
|
||||
state,
|
||||
@@ -209,16 +192,22 @@ internal partial class MapManager
|
||||
{
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, shape, transform, ref entities);
|
||||
FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref entities);
|
||||
}
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Box2 worldAABB, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var state = grids;
|
||||
|
||||
FindGridsIntersecting(mapEnt, shape, transform, ref state,
|
||||
FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state,
|
||||
static (EntityUid uid, MapGridComponent grid, ref List<Entity<MapGridComponent>> list) =>
|
||||
{
|
||||
list.Add((uid, grid));
|
||||
@@ -228,51 +217,39 @@ internal partial class MapManager
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(worldAABB);
|
||||
FindGridsIntersecting(mapEnt, shape, Transform.Empty, callback, approx, includeMap);
|
||||
FindGridsIntersecting(mapEnt, new Polygon(worldAABB), worldAABB, Transform.Empty, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(EntityUid mapEnt, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(worldAABB);
|
||||
FindGridsIntersecting(mapEnt, shape, Transform.Empty, ref state, callback, approx, includeMap);
|
||||
FindGridsIntersecting(mapEnt, new Polygon(worldAABB), worldAABB, Transform.Empty, ref state, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List<Entity<MapGridComponent>> grids,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(worldAABB);
|
||||
FindGridsIntersecting(mapEnt, shape, Transform.Empty, ref grids, approx, includeMap);
|
||||
FindGridsIntersecting(mapEnt, new Polygon(worldAABB), worldAABB, Transform.Empty, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.Set(worldBounds);
|
||||
|
||||
FindGridsIntersecting(mapEnt, shape, Transform.Empty, callback, approx, includeMap);
|
||||
var shape = new Polygon(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, shape, worldBounds.CalcBoundingBox(), Transform.Empty, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, GridCallback<TState> callback,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.Set(worldBounds);
|
||||
|
||||
FindGridsIntersecting(mapEnt, shape, Transform.Empty, ref state, callback, approx, includeMap);
|
||||
var shape = new Polygon(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, shape, worldBounds.CalcBoundingBox(), Transform.Empty, ref state, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref List<Entity<MapGridComponent>> grids,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.Set(worldBounds);
|
||||
|
||||
FindGridsIntersecting(mapEnt, shape, Transform.Empty, ref grids, approx, includeMap);
|
||||
var shape = new Polygon(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, shape, worldBounds.CalcBoundingBox(), Transform.Empty, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -28,6 +28,7 @@ using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Physics.Collision;
|
||||
@@ -61,9 +62,18 @@ internal ref struct DistanceProxy
|
||||
break;
|
||||
|
||||
case ShapeType.Polygon:
|
||||
var polygon = (PolygonShape) shape;
|
||||
Vertices = polygon.Vertices;
|
||||
Radius = polygon.Radius;
|
||||
if (shape is Polygon poly)
|
||||
{
|
||||
Vertices = poly.Vertices;
|
||||
Radius = poly.Radius;
|
||||
}
|
||||
else
|
||||
{
|
||||
var polyShape = (PolygonShape) shape;
|
||||
Vertices = polyShape.Vertices;
|
||||
Radius = polyShape.Radius;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case ShapeType.Chain:
|
||||
|
||||
226
Robust.Shared/Physics/Shapes/Polygon.cs
Normal file
226
Robust.Shared/Physics/Shapes/Polygon.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Physics.Shapes;
|
||||
|
||||
// Internal so people don't use it when it will have breaking changes very soon.
|
||||
internal record struct Polygon : IPhysShape
|
||||
{
|
||||
public static Polygon Empty = new(Box2.Empty);
|
||||
|
||||
[DataField]
|
||||
public Vector2[] Vertices;
|
||||
|
||||
public int VertexCount => Vertices.Length;
|
||||
|
||||
public Vector2[] Normals;
|
||||
|
||||
public Vector2 Centroid;
|
||||
|
||||
public int ChildCount => 1;
|
||||
public float Radius { get; set; }
|
||||
public ShapeType ShapeType => ShapeType.Polygon;
|
||||
|
||||
// Hopefully this one is short-lived for a few months
|
||||
public Polygon(IPhysShape shape) : this((PolygonShape) shape)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Polygon(PolygonShape polyShape)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
Vertices = new Vector2[polyShape.VertexCount];
|
||||
Normals = new Vector2[polyShape.Normals.Length];
|
||||
Radius = polyShape.Radius;
|
||||
Centroid = polyShape.Centroid;
|
||||
|
||||
Array.Copy(polyShape.Vertices, Vertices, Vertices.Length);
|
||||
Array.Copy(polyShape.Normals, Normals, Vertices.Length);
|
||||
}
|
||||
|
||||
public Polygon(Box2 aabb)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
Vertices = new Vector2[4];
|
||||
Normals = new Vector2[4];
|
||||
Radius = 0f;
|
||||
|
||||
Vertices[0] = aabb.BottomLeft;
|
||||
Vertices[1] = aabb.BottomRight;
|
||||
Vertices[2] = aabb.TopRight;
|
||||
Vertices[3] = aabb.TopLeft;
|
||||
|
||||
Normals[0] = new Vector2(0.0f, -1.0f);
|
||||
Normals[1] = new Vector2(1.0f, 0.0f);
|
||||
Normals[2] = new Vector2(0.0f, 1.0f);
|
||||
Normals[3] = new Vector2(-1.0f, 0.0f);
|
||||
|
||||
Centroid = aabb.Center;
|
||||
}
|
||||
|
||||
public Polygon(Box2Rotated bounds)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
Radius = 0f;
|
||||
Span<Vector2> verts = stackalloc Vector2[4];
|
||||
verts[0] = bounds.BottomLeft;
|
||||
verts[1] = bounds.BottomRight;
|
||||
verts[2] = bounds.TopRight;
|
||||
verts[3] = bounds.TopLeft;
|
||||
|
||||
var hull = new PhysicsHull(verts, 4);
|
||||
Set(hull);
|
||||
|
||||
Centroid = bounds.Center;
|
||||
}
|
||||
|
||||
public Polygon(Vector2[] vertices)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
var hull = PhysicsHull.ComputeHull(vertices, vertices.Length);
|
||||
|
||||
if (hull.Count < 3)
|
||||
{
|
||||
Vertices = Array.Empty<Vector2>();
|
||||
Normals = Array.Empty<Vector2>();
|
||||
return;
|
||||
}
|
||||
|
||||
Vertices = vertices;
|
||||
Normals = new Vector2[vertices.Length];
|
||||
Set(hull);
|
||||
Centroid = ComputeCentroid(Vertices);
|
||||
}
|
||||
|
||||
public static explicit operator Polygon(PolygonShape polyShape)
|
||||
{
|
||||
return new Polygon(polyShape);
|
||||
}
|
||||
|
||||
private void Set(PhysicsHull hull)
|
||||
{
|
||||
DebugTools.Assert(hull.Count >= 3);
|
||||
var vertexCount = hull.Count;
|
||||
Array.Resize(ref Vertices, vertexCount);
|
||||
Array.Resize(ref Normals, vertexCount);
|
||||
|
||||
for (var i = 0; i < vertexCount; i++)
|
||||
{
|
||||
Vertices[i] = hull.Points[i];
|
||||
}
|
||||
|
||||
// Compute normals. Ensure the edges have non-zero length.
|
||||
for (var i = 0; i < vertexCount; i++)
|
||||
{
|
||||
var next = i + 1 < vertexCount ? i + 1 : 0;
|
||||
var edge = Vertices[next] - Vertices[i];
|
||||
DebugTools.Assert(edge.LengthSquared() > float.Epsilon * float.Epsilon);
|
||||
|
||||
var temp = Vector2Helpers.Cross(edge, 1f);
|
||||
Normals[i] = temp.Normalized();
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 ComputeCentroid(Vector2[] vs)
|
||||
{
|
||||
var count = vs.Length;
|
||||
DebugTools.Assert(count >= 3);
|
||||
|
||||
var c = new Vector2(0.0f, 0.0f);
|
||||
float area = 0.0f;
|
||||
|
||||
// Get a reference point for forming triangles.
|
||||
// Use the first vertex to reduce round-off errors.
|
||||
var s = vs[0];
|
||||
|
||||
const float inv3 = 1.0f / 3.0f;
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
// Triangle vertices.
|
||||
var p1 = vs[0] - s;
|
||||
var p2 = vs[i] - s;
|
||||
var p3 = i + 1 < count ? vs[i+1] - s : vs[0] - s;
|
||||
|
||||
var e1 = p2 - p1;
|
||||
var e2 = p3 - p1;
|
||||
|
||||
float D = Vector2Helpers.Cross(e1, e2);
|
||||
|
||||
float triangleArea = 0.5f * D;
|
||||
area += triangleArea;
|
||||
|
||||
// Area weighted centroid
|
||||
c += (p1 + p2 + p3) * triangleArea * inv3;
|
||||
}
|
||||
|
||||
// Centroid
|
||||
DebugTools.Assert(area > float.Epsilon);
|
||||
c = c * (1.0f / area) + s;
|
||||
return c;
|
||||
}
|
||||
|
||||
public Box2 ComputeAABB(Transform transform, int childIndex)
|
||||
{
|
||||
DebugTools.Assert(childIndex == 0);
|
||||
var lower = Transform.Mul(transform, Vertices[0]);
|
||||
var upper = lower;
|
||||
|
||||
for (var i = 1; i < VertexCount; ++i)
|
||||
{
|
||||
var v = Transform.Mul(transform, Vertices[i]);
|
||||
lower = Vector2.Min(lower, v);
|
||||
upper = Vector2.Max(upper, v);
|
||||
}
|
||||
|
||||
var r = new Vector2(Radius, Radius);
|
||||
return new Box2(lower - r, upper + r);
|
||||
}
|
||||
|
||||
public bool Equals(IPhysShape? other)
|
||||
{
|
||||
if (other is not PolygonShape poly) return false;
|
||||
if (VertexCount != poly.VertexCount) return false;
|
||||
for (var i = 0; i < VertexCount; i++)
|
||||
{
|
||||
var vert = Vertices[i];
|
||||
if (!vert.Equals(poly.Vertices[i])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Equals(PolygonShape? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
return false;
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (!Radius.Equals(other.Radius) || VertexCount != other.VertexCount)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < VertexCount; i++)
|
||||
{
|
||||
var vert = Vertices[i];
|
||||
var otherVert = other.Vertices[i];
|
||||
|
||||
if (!vert.Equals(otherVert))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(VertexCount, Vertices.AsSpan(0, VertexCount).ToArray(), Radius);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Numerics;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Physics.Systems
|
||||
@@ -28,6 +29,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
var distance = worldPoint - center;
|
||||
return Vector2.Dot(distance, distance) <= circle.Radius * circle.Radius;
|
||||
case PolygonShape poly:
|
||||
{
|
||||
var pLocal = Physics.Transform.MulT(xform.Quaternion2D, worldPoint - xform.Position);
|
||||
|
||||
for (var i = 0; i < poly.VertexCount; i++)
|
||||
@@ -37,6 +39,19 @@ namespace Robust.Shared.Physics.Systems
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case Polygon poly:
|
||||
{
|
||||
var pLocal = Physics.Transform.MulT(xform.Quaternion2D, worldPoint - xform.Position);
|
||||
|
||||
for (var i = 0; i < poly.VertexCount; i++)
|
||||
{
|
||||
var dot = Vector2.Dot(poly.Normals[i], pLocal - poly.Vertices[i]);
|
||||
if (dot > 0f) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"No implemented TestPoint for {shape.GetType()}");
|
||||
}
|
||||
|
||||
@@ -692,6 +692,54 @@ public partial class SharedPhysicsSystem
|
||||
|
||||
#endregion
|
||||
|
||||
public Transform GetRelativePhysicsTransform(Transform worldTransform, Entity<TransformComponent?> relative)
|
||||
{
|
||||
if (!_xformQuery.Resolve(relative.Owner, ref relative.Comp))
|
||||
return Physics.Transform.Empty;
|
||||
|
||||
var (_, broadphaseRot, _, broadphaseInv) = _transform.GetWorldPositionRotationMatrixWithInv(relative.Comp);
|
||||
|
||||
return new Transform(Vector2.Transform(worldTransform.Position, broadphaseInv),
|
||||
worldTransform.Quaternion2D.Angle - broadphaseRot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the physics transform relative to another entity.
|
||||
/// </summary>
|
||||
public Transform GetRelativePhysicsTransform(
|
||||
Entity<TransformComponent?> entity,
|
||||
Entity<TransformComponent?> relative)
|
||||
{
|
||||
if (!_xformQuery.Resolve(entity.Owner, ref entity.Comp) ||
|
||||
!_xformQuery.Resolve(relative.Owner, ref relative.Comp))
|
||||
{
|
||||
return Physics.Transform.Empty;
|
||||
}
|
||||
|
||||
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(entity.Comp);
|
||||
var (_, broadphaseRot, _, broadphaseInv) = _transform.GetWorldPositionRotationMatrixWithInv(relative.Comp);
|
||||
|
||||
return new Transform(Vector2.Transform(worldPos, broadphaseInv), worldRot - broadphaseRot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets broadphase relevant transform.
|
||||
/// </summary>
|
||||
public Transform GetLocalPhysicsTransform(EntityUid uid, TransformComponent? xform = null)
|
||||
{
|
||||
if (!_xformQuery.Resolve(uid, ref xform) || xform.Broadphase == null)
|
||||
return Physics.Transform.Empty;
|
||||
|
||||
var broadphase = xform.Broadphase.Value.Uid;
|
||||
|
||||
if (xform.ParentUid == broadphase)
|
||||
{
|
||||
return new Transform(xform.LocalPosition, xform.LocalRotation);
|
||||
}
|
||||
|
||||
return GetRelativePhysicsTransform((uid, xform), broadphase);
|
||||
}
|
||||
|
||||
public Transform GetPhysicsTransform(EntityUid uid, TransformComponent? xform = null)
|
||||
{
|
||||
if (!_xformQuery.Resolve(uid, ref xform))
|
||||
|
||||
@@ -2,6 +2,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -71,8 +72,17 @@ public interface IReplayRecordingManager
|
||||
/// This gets invoked whenever a replay recording is stopping. Subscribers can use this to add extra yaml data to the
|
||||
/// recording's metadata file.
|
||||
/// </summary>
|
||||
/// <seealso cref="RecordingStopped2"/>
|
||||
event Action<MappingDataNode> RecordingStopped;
|
||||
|
||||
/// <summary>
|
||||
/// This gets invoked whenever a replay recording is stopping. Subscribers can use this to add extra data to the replay.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is effectively a more powerful version of <see cref="RecordingStopped"/>.
|
||||
/// </remarks>
|
||||
event Action<ReplayRecordingStopped> RecordingStopped2;
|
||||
|
||||
/// <summary>
|
||||
/// This gets invoked after a replay recording has finished and provides information about where the replay data
|
||||
/// was saved. Note that this only means that all write tasks have started, however some of the file tasks may not
|
||||
@@ -131,6 +141,27 @@ public interface IReplayRecordingManager
|
||||
bool IsWriting();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event object used by <see cref="IReplayRecordingManager.RecordingStopped2"/>.
|
||||
/// Allows modifying metadata and adding more data to replay files.
|
||||
/// </summary>
|
||||
public sealed class ReplayRecordingStopped
|
||||
{
|
||||
/// <summary>
|
||||
/// Mutable metadata that will be saved to the replay's metadata file.
|
||||
/// </summary>
|
||||
public required MappingDataNode Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A writer that allows arbitrary file writing into the replay file.
|
||||
/// </summary>
|
||||
public required IReplayFileWriter Writer { get; init; }
|
||||
|
||||
internal ReplayRecordingStopped()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event data for <see cref="IReplayRecordingManager.RecordingFinished"/>.
|
||||
/// </summary>
|
||||
@@ -148,6 +179,31 @@ public record ReplayRecordingFinished(IWritableDirProvider Directory, ResPath Pa
|
||||
/// <param name="UncompressedSize">The total uncompressed size of the replay data blobs.</param>
|
||||
public record struct ReplayRecordingStats(TimeSpan Time, uint Ticks, long Size, long UncompressedSize);
|
||||
|
||||
/// <summary>
|
||||
/// Allows writing extra files directly into the replay file.
|
||||
/// </summary>
|
||||
/// <seealso cref="ReplayRecordingStopped"/>
|
||||
/// <seealso cref="IReplayRecordingManager.RecordingStopped2"/>
|
||||
public interface IReplayFileWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// The base directory inside the replay directory you should generally be writing to.
|
||||
/// This is equivalent to <see cref="ReplayConstants.ReplayZipFolder"/>.
|
||||
/// </summary>
|
||||
ResPath BaseReplayPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes arbitrary data into a file in the replay.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to write to.</param>
|
||||
/// <param name="bytes">The bytes to write to the file.</param>
|
||||
/// <param name="compressionLevel">How much to compress the file.</param>
|
||||
void WriteBytes(
|
||||
ResPath path,
|
||||
ReadOnlyMemory<byte> bytes,
|
||||
CompressionLevel compressionLevel = CompressionLevel.Optimal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Engine-internal functions for <see cref="IReplayRecordingManager"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -49,6 +49,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
|
||||
|
||||
public event Action<MappingDataNode, List<object>>? RecordingStarted;
|
||||
public event Action<MappingDataNode>? RecordingStopped;
|
||||
public event Action<ReplayRecordingStopped>? RecordingStopped2;
|
||||
public event Action<ReplayRecordingFinished>? RecordingFinished;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
@@ -312,6 +313,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
|
||||
|
||||
// File stream & compression context is always disposed from the worker task.
|
||||
_recState.WriteCommandChannel.Complete();
|
||||
_recState.Done = true;
|
||||
|
||||
_recState = null;
|
||||
}
|
||||
@@ -373,6 +375,11 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
|
||||
{
|
||||
var yamlMetadata = new MappingDataNode();
|
||||
RecordingStopped?.Invoke(yamlMetadata);
|
||||
RecordingStopped2?.Invoke(new ReplayRecordingStopped
|
||||
{
|
||||
Metadata = yamlMetadata,
|
||||
Writer = new ReplayFileWriter(this, recState)
|
||||
});
|
||||
var time = Timing.CurTime - recState.StartTime;
|
||||
yamlMetadata[MetaFinalKeyEndTick] = new ValueDataNode(Timing.CurTick.Value.ToString());
|
||||
yamlMetadata[MetaFinalKeyDuration] = new ValueDataNode(time.ToString());
|
||||
@@ -384,6 +391,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
|
||||
// this just overwrites the previous yml with additional data.
|
||||
var document = new YamlDocument(yamlMetadata.ToYaml());
|
||||
WriteYaml(recState, ReplayZipFolder / FileMetaFinal, document);
|
||||
|
||||
UpdateWriteTasks();
|
||||
Reset();
|
||||
|
||||
@@ -492,6 +500,8 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
|
||||
public long CompressedSize;
|
||||
public long UncompressedSize;
|
||||
|
||||
public bool Done;
|
||||
|
||||
public RecordingState(
|
||||
ZipArchive zip,
|
||||
MemoryStream buffer,
|
||||
@@ -518,4 +528,23 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
|
||||
WriteCommandChannel = writeCommandChannel;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ReplayFileWriter(SharedReplayRecordingManager manager, RecordingState state)
|
||||
: IReplayFileWriter
|
||||
{
|
||||
public ResPath BaseReplayPath => ReplayZipFolder;
|
||||
|
||||
public void WriteBytes(ResPath path, ReadOnlyMemory<byte> bytes, CompressionLevel compressionLevel)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
manager.WriteBytes(state, path, bytes, compressionLevel);
|
||||
}
|
||||
|
||||
private void CheckDisposed()
|
||||
{
|
||||
if (state.Done)
|
||||
throw new ObjectDisposedException(nameof(ReplayFileWriter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +260,12 @@ public sealed partial class SerializationManager
|
||||
return ValueDataNode.Null();
|
||||
}
|
||||
|
||||
return GetOrCreateWriteGenericDelegate(value, notNullableOverride)(value, alwaysWrite, context);
|
||||
var node = GetOrCreateWriteGenericDelegate(value, notNullableOverride)(value, alwaysWrite, context);
|
||||
|
||||
if (typeof(T) == typeof(object))
|
||||
node.Tag = "!type:" + value.GetType().Name;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public DataNode WriteValue<T>(ITypeWriter<T> writer, T value, bool alwaysWrite = false,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
|
||||
|
||||
[TypeSerializer]
|
||||
public sealed class ObjectSerializer : ITypeSerializer<object, ValueDataNode>, ITypeCopier<object>
|
||||
{
|
||||
#region Validate
|
||||
|
||||
public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node,
|
||||
IDependencyCollection dependencies, ISerializationContext? context = null)
|
||||
{
|
||||
var reflection = dependencies.Resolve<IReflectionManager>();
|
||||
|
||||
if (node.Tag != null)
|
||||
{
|
||||
string? typeString = node.Tag[6..];
|
||||
|
||||
if (!reflection.TryLooseGetType(typeString, out var type))
|
||||
{
|
||||
return new ErrorNode(node, $"Unable to find type for {typeString}");
|
||||
}
|
||||
|
||||
return serializationManager.ValidateNode(type, node, context);
|
||||
}
|
||||
return new ErrorNode(node, $"Unable to find type for {node}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read
|
||||
public object Read(ISerializationManager serializationManager, ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx, ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<object>? instanceProvider = null)
|
||||
{
|
||||
var reflection = dependencies.Resolve<IReflectionManager>();
|
||||
var value = instanceProvider != null ? instanceProvider() : new object();
|
||||
|
||||
if (node.Tag != null)
|
||||
{
|
||||
string? typeString = node.Tag[6..];
|
||||
|
||||
if (!reflection.TryLooseGetType(typeString, out var type))
|
||||
throw new NullReferenceException($"Found null type for {typeString}");
|
||||
|
||||
value = serializationManager.Read(type, node, hookCtx, context);
|
||||
|
||||
if (value == null)
|
||||
throw new NullReferenceException($"Found null data for {node}, expected {type}");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Write
|
||||
public DataNode Write(ISerializationManager serializationManager, object value,
|
||||
IDependencyCollection dependencies, bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
var node = serializationManager.WriteValue(value.GetType(), value);
|
||||
|
||||
if (node == null)
|
||||
throw new NullReferenceException($"Attempted to write node with type {value.GetType()}, node returned null");
|
||||
|
||||
return node;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CopyTo
|
||||
|
||||
public void CopyTo(
|
||||
ISerializationManager serializationManager,
|
||||
object source,
|
||||
ref object target,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
target = source;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public sealed class EmplaceCommand : ToolshedCommand
|
||||
public override Type[] TypeParameterParsers => new[] {typeof(Type)};
|
||||
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
TOut Emplace<TIn, TOut>(
|
||||
TOut Emplace<TOut, TIn>(
|
||||
[CommandInvocationContext] IInvocationContext ctx,
|
||||
[PipedArgument] TIn value,
|
||||
[CommandArgument] Block<TOut> block
|
||||
@@ -27,7 +27,7 @@ public sealed class EmplaceCommand : ToolshedCommand
|
||||
}
|
||||
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
IEnumerable<TOut> Emplace<TIn, TOut>(
|
||||
IEnumerable<TOut> Emplace<TOut, TIn>(
|
||||
[CommandInvocationContext] IInvocationContext ctx,
|
||||
[PipedArgument] IEnumerable<TIn> value,
|
||||
[CommandArgument] Block<TOut> block
|
||||
|
||||
@@ -5,11 +5,9 @@ using Robust.Shared.Toolshed.Syntax;
|
||||
|
||||
namespace Robust.Shared.Toolshed.Commands.Generic.Ordering;
|
||||
|
||||
[ToolshedCommand, MapLikeCommand]
|
||||
[ToolshedCommand]
|
||||
public sealed class SortCommand : ToolshedCommand
|
||||
{
|
||||
public override Type[] TypeParameterParsers => new[] {typeof(Type)};
|
||||
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public IEnumerable<T> Sort<T>(
|
||||
[CommandInvocationContext] IInvocationContext ctx,
|
||||
|
||||
@@ -5,11 +5,9 @@ using Robust.Shared.Toolshed.Syntax;
|
||||
|
||||
namespace Robust.Shared.Toolshed.Commands.Generic.Ordering;
|
||||
|
||||
[ToolshedCommand, MapLikeCommand]
|
||||
[ToolshedCommand]
|
||||
public sealed class SortDownCommand : ToolshedCommand
|
||||
{
|
||||
public override Type[] TypeParameterParsers => new[] {typeof(Type)};
|
||||
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public IEnumerable<T> Sort<T>(
|
||||
[CommandInvocationContext] IInvocationContext ctx,
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Robust.Shared.Toolshed.Commands.Math;
|
||||
[ToolshedCommand]
|
||||
public sealed class JoinCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
[CommandImplementation]
|
||||
public string Join(
|
||||
[CommandInvocationContext] IInvocationContext ctx,
|
||||
[PipedArgument] string x,
|
||||
@@ -18,7 +18,7 @@ public sealed class JoinCommand : ToolshedCommand
|
||||
if (yVal is null)
|
||||
return x;
|
||||
|
||||
return x + y;
|
||||
return x + yVal;
|
||||
}
|
||||
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
|
||||
@@ -74,6 +74,18 @@ public sealed class CommandRun
|
||||
|
||||
public object? Invoke(object? input, IInvocationContext ctx, bool reportErrors = true)
|
||||
{
|
||||
// TODO TOOLSHED
|
||||
// improve error handling. Most expression invokers don't bother to check for errors.
|
||||
// This especially applies to all map / emplace / sort commands.
|
||||
// A simple error while enumerating entities could lock up the server.
|
||||
|
||||
if (ctx.GetErrors().Any())
|
||||
{
|
||||
// Attempt to prevent O(n^2) growth in errors due to people repeatedly evaluating expressions without
|
||||
// checking for errors.
|
||||
throw new Exception($"Improperly handled Toolshed errors");
|
||||
}
|
||||
|
||||
var ret = input;
|
||||
foreach (var (cmd, span) in Commands)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ using Robust.Client.GameStates;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML.Proxy;
|
||||
using Robust.Server;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.GameStates;
|
||||
@@ -929,6 +930,8 @@ namespace Robust.UnitTesting
|
||||
deps.Register<IClientConsoleHost, TestingClientConsoleHost>(true);
|
||||
deps.Register<IConsoleHost, TestingClientConsoleHost>(true);
|
||||
deps.Register<IConsoleHostInternal, TestingClientConsoleHost>(true);
|
||||
deps.Register<IXamlProxyManager, XamlProxyManagerStub>(true);
|
||||
deps.Register<IXamlHotReloadManager, XamlHotReloadManagerStub>(true);
|
||||
Options?.InitIoC?.Invoke();
|
||||
deps.BuildGraph();
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.UnitTesting.Shared
|
||||
@@ -11,8 +16,178 @@ namespace Robust.UnitTesting.Shared
|
||||
[TestFixture, TestOf(typeof(EntityLookupSystem))]
|
||||
public sealed class EntityLookupTest
|
||||
{
|
||||
[Test]
|
||||
public void AnyIntersecting()
|
||||
private static readonly MapId MapId = new MapId(1);
|
||||
|
||||
private static readonly TestCaseData[] InRangeCases = new[]
|
||||
{
|
||||
new TestCaseData(true, new MapCoordinates(Vector2.One, MapId), new MapCoordinates(Vector2.Zero, MapId), 0.5f, false),
|
||||
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9.5f, 9.5f), MapId), 0.5f, true),
|
||||
// Close but no cigar
|
||||
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9f, 9f), MapId), 0.5f, false),
|
||||
// Large area so useboundsquery
|
||||
new TestCaseData(true, new MapCoordinates(new Vector2(0f, 0f), MapId), new MapCoordinates(new Vector2(0f, 1000f), MapId), 999f, false),
|
||||
new TestCaseData(true, new MapCoordinates(new Vector2(0f, 0f), MapId), new MapCoordinates(new Vector2(0f, 999f), MapId), 999f, true),
|
||||
|
||||
// NoFixturecases
|
||||
new TestCaseData(false, new MapCoordinates(Vector2.One, MapId), new MapCoordinates(Vector2.Zero, MapId), 0.5f, false),
|
||||
new TestCaseData(false, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9.5f, 9.5f), MapId), 0.5f, false),
|
||||
// Close but no cigar
|
||||
new TestCaseData(false, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9f, 9f), MapId), 0.5f, false),
|
||||
};
|
||||
|
||||
// Remember this test data is relative.
|
||||
private static readonly TestCaseData[] Box2Cases = new[]
|
||||
{
|
||||
new TestCaseData(true, new MapCoordinates(Vector2.One, MapId), Box2.UnitCentered, false),
|
||||
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), Box2.UnitCentered, true),
|
||||
};
|
||||
|
||||
private static readonly TestCaseData[] TileCases = new[]
|
||||
{
|
||||
new TestCaseData(true, new MapCoordinates(Vector2.One, MapId), Vector2i.Zero, false),
|
||||
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), Vector2i.Zero, true),
|
||||
// Need to make sure we don't pull out neighbor fixtures even if they barely touch our tile
|
||||
new TestCaseData(true, new MapCoordinates(new Vector2(11f + 0.35f, 10f), MapId), Vector2i.Zero, false),
|
||||
};
|
||||
|
||||
private EntityUid GetPhysicsEntity(IEntityManager entManager, MapCoordinates spawnPos)
|
||||
{
|
||||
var ent = entManager.SpawnEntity(null, spawnPos);
|
||||
var physics = entManager.AddComponent<PhysicsComponent>(ent);
|
||||
entManager.System<FixtureSystem>().TryCreateFixture(ent, new PhysShapeCircle(0.35f, Vector2.Zero), "fix1");
|
||||
entManager.System<SharedPhysicsSystem>().SetCanCollide(ent, true, body: physics);
|
||||
return ent;
|
||||
}
|
||||
|
||||
private Entity<MapGridComponent> SetupGrid(MapId mapId, SharedMapSystem mapSystem, IEntityManager entManager, IMapManager mapManager)
|
||||
{
|
||||
var grid = mapManager.CreateGridEntity(mapId);
|
||||
entManager.System<SharedTransformSystem>().SetLocalPosition(grid.Owner, new Vector2(10f, 10f));
|
||||
mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1));
|
||||
return grid;
|
||||
}
|
||||
|
||||
#region Entity
|
||||
|
||||
/*
|
||||
* We double these tests just because these have slightly different codepaths at the moment.
|
||||
*
|
||||
*/
|
||||
|
||||
[Test, TestCaseSource(nameof(Box2Cases))]
|
||||
public void TestEntityAnyIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
|
||||
mapSystem.CreateMap(spawnPos.MapId);
|
||||
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
var outcome = lookup.AnyLocalEntitiesIntersecting(grid.Owner, queryBounds, LookupFlags.All);
|
||||
|
||||
Assert.That(outcome, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(Box2Cases))]
|
||||
public void TestEntityAnyLocalIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
|
||||
mapSystem.CreateMap(spawnPos.MapId);
|
||||
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
var outcome = lookup.AnyLocalEntitiesIntersecting(grid.Owner, queryBounds, LookupFlags.All);
|
||||
|
||||
Assert.That(outcome, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests Box2 local queries for a particular lookup ID.
|
||||
/// </summary>
|
||||
[Test, TestCaseSource(nameof(Box2Cases))]
|
||||
public void TestEntityGridLocalIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
|
||||
mapSystem.CreateMap(spawnPos.MapId);
|
||||
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
var entities = new HashSet<Entity<TransformComponent>>();
|
||||
lookup.GetLocalEntitiesIntersecting(grid.Owner, queryBounds, entities);
|
||||
|
||||
Assert.That(entities.Count > 0, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests Box2 local queries for a particular lookup ID.
|
||||
/// </summary>
|
||||
[Test, TestCaseSource(nameof(TileCases))]
|
||||
public void TestEntityGridTileIntersecting(bool physics, MapCoordinates spawnPos, Vector2i queryTile, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
|
||||
mapSystem.CreateMap(spawnPos.MapId);
|
||||
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
var entities = new HashSet<Entity<TransformComponent>>();
|
||||
lookup.GetLocalEntitiesIntersecting(grid.Owner, queryTile, entities);
|
||||
|
||||
Assert.That(entities.Count > 0, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EntityUid
|
||||
|
||||
[Test, TestCaseSource(nameof(InRangeCases))]
|
||||
public void TestMapInRange(bool physics, MapCoordinates spawnPos, MapCoordinates queryPos, float range, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
@@ -21,15 +196,121 @@ namespace Robust.UnitTesting.Shared
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
|
||||
var mapId = server.CreateMap().MapId;
|
||||
entManager.System<SharedMapSystem>().CreateMap(spawnPos.MapId);
|
||||
|
||||
var theMapSpotBeingUsed = new Box2(Vector2.Zero, Vector2.One);
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
var dummy = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
|
||||
Assert.That(lookup.AnyEntitiesIntersecting(mapId, theMapSpotBeingUsed));
|
||||
mapManager.DeleteMap(mapId);
|
||||
Assert.That(lookup.GetEntitiesInRange(queryPos.MapId, queryPos.Position, range).Count > 0, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(InRangeCases))]
|
||||
public void TestGridInRange(bool physics, MapCoordinates spawnPos, MapCoordinates queryPos, float range, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
|
||||
mapSystem.CreateMap(spawnPos.MapId);
|
||||
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
_ = entManager.SpawnEntity(null, spawnPos);
|
||||
Assert.That(lookup.GetEntitiesInRange(queryPos.MapId, queryPos.Position, range).Count > 0, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(InRangeCases))]
|
||||
public void TestMapNoFixtureInRange(bool physics, MapCoordinates spawnPos, MapCoordinates queryPos, float range, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
|
||||
entManager.System<SharedMapSystem>().CreateMap(spawnPos.MapId);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
Assert.That(lookup.GetEntitiesInRange(queryPos.MapId, queryPos.Position, range).Count > 0, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests Box2 local queries for a particular lookup ID.
|
||||
/// </summary>
|
||||
[Test, TestCaseSource(nameof(Box2Cases))]
|
||||
public void TestGridAnyIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
|
||||
mapSystem.CreateMap(spawnPos.MapId);
|
||||
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
var outcome = lookup.AnyLocalEntitiesIntersecting(grid.Owner, queryBounds, LookupFlags.All);
|
||||
|
||||
Assert.That(outcome, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests Box2 local queries for a particular lookup ID.
|
||||
/// </summary>
|
||||
[Test, TestCaseSource(nameof(Box2Cases))]
|
||||
public void TestGridLocalIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation();
|
||||
var server = sim.InitializeInstance();
|
||||
|
||||
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
|
||||
var entManager = server.Resolve<IEntityManager>();
|
||||
var mapManager = server.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
|
||||
mapSystem.CreateMap(spawnPos.MapId);
|
||||
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
|
||||
|
||||
if (physics)
|
||||
GetPhysicsEntity(entManager, spawnPos);
|
||||
else
|
||||
entManager.Spawn(null, spawnPos);
|
||||
|
||||
var entities = new HashSet<EntityUid>();
|
||||
lookup.GetLocalEntitiesIntersecting(grid.Owner, queryBounds, entities);
|
||||
|
||||
Assert.That(entities.Count > 0, Is.EqualTo(result));
|
||||
mapManager.DeleteMap(spawnPos.MapId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Is the entity correctly removed / added to EntityLookup when anchored
|
||||
/// </summary>
|
||||
@@ -49,11 +330,11 @@ namespace Robust.UnitTesting.Shared
|
||||
var theMapSpotBeingUsed = new Box2(Vector2.Zero, Vector2.One);
|
||||
grid.Comp.SetTile(new Vector2i(), new Tile(1));
|
||||
|
||||
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList().Count, Is.EqualTo(0));
|
||||
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList(), Is.Empty);
|
||||
|
||||
// Setup and check it actually worked
|
||||
var dummy = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
|
||||
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList().Count, Is.EqualTo(1));
|
||||
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList(), Has.Count.EqualTo(1));
|
||||
|
||||
var xform = entManager.GetComponent<TransformComponent>(dummy);
|
||||
|
||||
|
||||
79
Robust.UnitTesting/Shared/Map/Query_Tests.cs
Normal file
79
Robust.UnitTesting/Shared/Map/Query_Tests.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Map;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class Query_Tests
|
||||
{
|
||||
private static readonly TestCaseData[] Box2Data = new[]
|
||||
{
|
||||
new TestCaseData(
|
||||
Vector2.Zero,
|
||||
0f,
|
||||
Box2.UnitCentered.Translated(new Vector2(0f, 10f)),
|
||||
true
|
||||
),
|
||||
|
||||
new TestCaseData(
|
||||
Vector2.Zero,
|
||||
MathF.PI,
|
||||
Box2.UnitCentered.Translated(new Vector2(0f, 10f)),
|
||||
false
|
||||
),
|
||||
|
||||
new TestCaseData(
|
||||
Vector2.Zero,
|
||||
MathF.PI,
|
||||
Box2.UnitCentered.Translated(new Vector2(0f, -10f)),
|
||||
true
|
||||
),
|
||||
|
||||
new TestCaseData(
|
||||
Vector2.Zero,
|
||||
MathF.PI / 2f,
|
||||
Box2.UnitCentered.Translated(new Vector2(-10f, 0f)),
|
||||
true
|
||||
),
|
||||
|
||||
new TestCaseData(
|
||||
Vector2.Zero,
|
||||
MathF.PI / 4f,
|
||||
Box2.UnitCentered.Translated(new Vector2(-5f, 5f)),
|
||||
true
|
||||
),
|
||||
};
|
||||
|
||||
[Test, TestCaseSource(nameof(Box2Data))]
|
||||
public void TestBox2GridIntersection(Vector2 position, float radians, Box2 worldAABB, bool result)
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
|
||||
var entManager = sim.Resolve<IEntityManager>();
|
||||
var mapManager = sim.Resolve<IMapManager>();
|
||||
var mapSystem = entManager.System<SharedMapSystem>();
|
||||
var xformSystem = entManager.System<SharedTransformSystem>();
|
||||
|
||||
var map = mapSystem.CreateMap();
|
||||
var grid = mapManager.CreateGridEntity(map);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
mapSystem.SetTile(grid, new Vector2i(0, i), new Tile(1));
|
||||
}
|
||||
|
||||
xformSystem.SetWorldRotation(grid.Owner, radians);
|
||||
|
||||
var grids = new List<Entity<MapGridComponent>>();
|
||||
mapManager.FindGridsIntersecting(map, worldAABB, ref grids);
|
||||
|
||||
Assert.That(grids.Count > 0, Is.EqualTo(result));
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,47 @@ namespace Robust.UnitTesting.Shared.Maths
|
||||
(new Box2(-1, 1, 1, 2), new Vector2(0, 0), -Math.PI/2, new Box2(1, -1, 2, 1)),
|
||||
};
|
||||
|
||||
private static TestCaseData[] MatrixCases = new[]
|
||||
{
|
||||
new TestCaseData(Matrix3x2.Identity,
|
||||
Box2Rotated.UnitCentered,
|
||||
Box2Rotated.UnitCentered),
|
||||
new TestCaseData(Matrix3x2.CreateRotation(MathF.PI),
|
||||
Box2Rotated.UnitCentered,
|
||||
new Box2Rotated(new Vector2(0.5f, 0.5f), new Vector2(-0.5f, -0.5f))),
|
||||
new TestCaseData(Matrix3x2.CreateTranslation(Vector2.One),
|
||||
Box2Rotated.UnitCentered,
|
||||
new Box2Rotated(new Vector2(0.5f, 0.5f), new Vector2(1.5f, 1.5f))),
|
||||
};
|
||||
|
||||
[Test, TestCaseSource(nameof(MatrixCases))]
|
||||
public void TestBox2RotatedMatrices(Matrix3x2 matrix, Box2Rotated bounds, Box2Rotated result)
|
||||
{
|
||||
Assert.That(matrix.TransformBounds(bounds), Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private static TestCaseData[] MatrixBox2Cases = new[]
|
||||
{
|
||||
new TestCaseData(Matrix3x2.Identity,
|
||||
Box2Rotated.UnitCentered,
|
||||
Box2Rotated.UnitCentered.CalcBoundingBox()),
|
||||
new TestCaseData(Matrix3x2.CreateRotation(MathF.PI),
|
||||
Box2Rotated.UnitCentered,
|
||||
new Box2Rotated(new Vector2(0.5f, 0.5f), new Vector2(-0.5f, -0.5f)).CalcBoundingBox()),
|
||||
new TestCaseData(Matrix3x2.CreateTranslation(Vector2.One),
|
||||
Box2Rotated.UnitCentered,
|
||||
new Box2Rotated(new Vector2(0.5f, 0.5f), new Vector2(1.5f, 1.5f)).CalcBoundingBox()),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Tests that transforming a Box2Rotated into a Box2 works.
|
||||
/// </summary>
|
||||
[Test, TestCaseSource(nameof(MatrixBox2Cases))]
|
||||
public void TestBox2Matrices(Matrix3x2 matrix, Box2Rotated bounds, Box2 result)
|
||||
{
|
||||
Assert.That(matrix.TransformBox(bounds), Is.EqualTo(result));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCalcBoundingBox([ValueSource(nameof(CalcBoundingBoxData))]
|
||||
(Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected) dat)
|
||||
|
||||
@@ -65,6 +65,25 @@ namespace Robust.UnitTesting.Shared.Maths
|
||||
10.0f
|
||||
};
|
||||
|
||||
private static TestCaseData[] MatrixCases = new[]
|
||||
{
|
||||
new TestCaseData(Matrix3x2.Identity,
|
||||
Box2.UnitCentered,
|
||||
Box2.UnitCentered),
|
||||
new TestCaseData(Matrix3x2.CreateRotation(MathF.PI),
|
||||
Box2.UnitCentered,
|
||||
new Box2(new Vector2(-0.5f, -0.5f), new Vector2(0.5f, 0.5f))),
|
||||
new TestCaseData(Matrix3x2.CreateTranslation(Vector2.One),
|
||||
Box2.UnitCentered,
|
||||
new Box2(new Vector2(0.5f, 0.5f), new Vector2(1.5f, 1.5f))),
|
||||
};
|
||||
|
||||
[Test, TestCaseSource(nameof(MatrixCases))]
|
||||
public void TestBox2Matrices(Matrix3x2 matrix, Box2 bounds, Box2 result)
|
||||
{
|
||||
Assert.That(matrix.TransformBox(bounds), Is.EqualTo(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the sources list has correct data.
|
||||
/// That is, no boxes where left > right or top > bottom.
|
||||
|
||||
95
Robust.UnitTesting/Shared/Physics/PhysicsHull_Test.cs
Normal file
95
Robust.UnitTesting/Shared/Physics/PhysicsHull_Test.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Physics;
|
||||
|
||||
internal sealed class PhysicsHull_Test
|
||||
{
|
||||
private static readonly TestCaseData[] CollinearHulls = new TestCaseData[]
|
||||
{
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.One,
|
||||
Vector2.UnitY,
|
||||
}, 3),
|
||||
// Same points
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.One,
|
||||
Vector2.One,
|
||||
Vector2.UnitY,
|
||||
}, 3),
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.UnitX / 2f,
|
||||
Vector2.UnitX,
|
||||
Vector2.UnitY,
|
||||
}, 3),
|
||||
};
|
||||
|
||||
[Test, TestCaseSource(nameof(CollinearHulls))]
|
||||
public void CollinearTest(Vector2[] vertices, int count)
|
||||
{
|
||||
var hull = PhysicsHull.ComputeHull(vertices.AsSpan(), vertices.Length);
|
||||
Assert.That(hull.Count, Is.EqualTo(count));
|
||||
}
|
||||
|
||||
private static readonly TestCaseData[] ValidateHulls = new TestCaseData[]
|
||||
{
|
||||
new TestCaseData(Array.Empty<Vector2>(), false),
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.One,
|
||||
Vector2.UnitY,
|
||||
}, true),
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.UnitX,
|
||||
Vector2.One,
|
||||
Vector2.UnitY,
|
||||
}, true),
|
||||
// Same point
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.One,
|
||||
Vector2.One,
|
||||
Vector2.UnitY,
|
||||
}, false),
|
||||
// Collinear point
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.One / 2f,
|
||||
Vector2.One,
|
||||
}, false),
|
||||
// Too many verts
|
||||
new TestCaseData(new Vector2[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
Vector2.UnitX,
|
||||
Vector2.One * 1f,
|
||||
Vector2.One * 2f,
|
||||
Vector2.One * 3f,
|
||||
Vector2.One * 4f,
|
||||
Vector2.One * 5f,
|
||||
Vector2.One * 6f,
|
||||
Vector2.One * 7f,
|
||||
Vector2.One * 8f,
|
||||
}, false),
|
||||
};
|
||||
|
||||
[Test, TestCaseSource(nameof(ValidateHulls))]
|
||||
public void ValidationTest(Vector2[] vertices, bool result)
|
||||
{
|
||||
var hull = new PhysicsHull(vertices.AsSpan(), vertices.Length);
|
||||
Assert.That(PhysicsHull.ValidateHull(hull), Is.EqualTo(result));
|
||||
}
|
||||
}
|
||||
46
Robust.UnitTesting/Shared/Physics/Shapes/Polygon_Test.cs
Normal file
46
Robust.UnitTesting/Shared/Physics/Shapes/Polygon_Test.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Numerics;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Physics;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class Polygon_Test
|
||||
{
|
||||
[Test]
|
||||
public void TestAABB()
|
||||
{
|
||||
var shape = new Polygon(Box2.UnitCentered.Translated(Vector2.One));
|
||||
|
||||
Assert.That(shape.ComputeAABB(Transform.Empty, 0), Is.EqualTo(Box2.UnitCentered.Translated(Vector2.One)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBox2()
|
||||
{
|
||||
var shape = new Polygon(Box2.UnitCentered.Translated(Vector2.One));
|
||||
Assert.That(shape.Vertices, Is.EqualTo(new Vector2[]
|
||||
{
|
||||
new Vector2(0.5f, 0.5f),
|
||||
new Vector2(1.5f, 0.5f),
|
||||
new Vector2(1.5f, 1.5f),
|
||||
new Vector2(0.5f, 1.5f),
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBox2Rotated()
|
||||
{
|
||||
var shape = new Polygon(new Box2Rotated(Box2.UnitCentered, Angle.FromDegrees(90)));
|
||||
|
||||
Assert.That(shape.Vertices, Is.EqualTo(new Vector2[]
|
||||
{
|
||||
new Vector2(0.5f, -0.5f),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
new Vector2(-0.5f, 0.5f),
|
||||
new Vector2(-0.5f, -0.5f),
|
||||
}));
|
||||
}
|
||||
}
|
||||
4
Robust.Xaml/AssemblyInfo.cs
Normal file
4
Robust.Xaml/AssemblyInfo.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Robust.Client")]
|
||||
[assembly: InternalsVisibleTo("Robust.Client.Injectors")]
|
||||
32
Robust.Xaml/CecilExtensions.cs
Normal file
32
Robust.Xaml/CecilExtensions.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace Robust.Xaml;
|
||||
|
||||
/// <summary>
|
||||
/// Source: https://github.com/jbevain/cecil/blob/master/Test/Mono.Cecil.Tests/Extensions.cs
|
||||
/// </summary>
|
||||
internal static class CecilExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Specialize some generic parameters of a reference to a generic method. The return value
|
||||
/// is a more monomorphized version.
|
||||
/// </summary>
|
||||
/// <param name="self">the original MethodReference</param>
|
||||
/// <param name="arguments">the specialized arguments</param>
|
||||
/// <returns>the monomorphic MethodReference</returns>
|
||||
/// <exception cref="ArgumentException">if the number of passed arguments is wrong</exception>
|
||||
public static MethodReference MakeGenericMethod(this MethodReference self, params TypeReference[] arguments)
|
||||
{
|
||||
if (self.GenericParameters.Count != arguments.Length)
|
||||
throw new ArgumentException();
|
||||
|
||||
var instance = new GenericInstanceMethod(self);
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
instance.GenericArguments.Add(argument);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using Microsoft.Build.Framework;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
namespace Robust.Xaml
|
||||
{
|
||||
/// <summary>
|
||||
/// Taken from https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/Extensions.cs
|
||||
/// </summary>
|
||||
public static class Extensions
|
||||
internal static class Extensions
|
||||
{
|
||||
//shamefully copied from avalonia
|
||||
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp)
|
||||
249
Robust.Xaml/LowLevelCustomizations.cs
Normal file
249
Robust.Xaml/LowLevelCustomizations.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Cecil.Rocks;
|
||||
using Mono.Collections.Generic;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml;
|
||||
|
||||
/// <summary>
|
||||
/// Class that performs find/replace operations on IL in assemblies that contain
|
||||
/// SS14 content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This code used to live in Robust.Client.Injectors.
|
||||
///
|
||||
/// Paul Ritter wrote a lot of code that does low-level Cecil based patching
|
||||
/// of AoT-compiled XamlX code.
|
||||
///
|
||||
/// That's "fine" (it's not actually fine) -- this class just moves that all
|
||||
/// to one place, and removes the extremely verbose Cecil-based type lookups
|
||||
/// to a separate shared location.
|
||||
/// </remarks>
|
||||
internal sealed class LowLevelCustomizations
|
||||
{
|
||||
public const string TrampolineName = "!XamlIlPopulateTrampoline";
|
||||
public const int ExpectedNMetadataArgs = 3;
|
||||
|
||||
private readonly CecilTypeSystem _typeSystem;
|
||||
private readonly AssemblyDefinition _asm;
|
||||
|
||||
private readonly TypeDefinition _iocManager;
|
||||
private readonly TypeDefinition _iXamlProxyHelper;
|
||||
private readonly TypeDefinition _systemType;
|
||||
private readonly TypeDefinition _stringType;
|
||||
private readonly TypeDefinition _xamlMetadataAttributeType;
|
||||
|
||||
private readonly MethodReference _resolveXamlProxyHelperMethod;
|
||||
private readonly MethodReference _populateMethod;
|
||||
private readonly MethodReference _getTypeFromHandleMethod;
|
||||
private readonly MethodReference _xamlMetadataAttributeConstructor;
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="LowLevelCustomizations"/> object.
|
||||
/// </summary>
|
||||
/// <param name="typeSystem">the <see cref="CecilTypeSystem" /></param>
|
||||
/// <exception cref="NullReferenceException">if some needed types were undefined</exception>
|
||||
public LowLevelCustomizations(CecilTypeSystem typeSystem)
|
||||
{
|
||||
// resolve every type that we look for or substitute in when doing surgery
|
||||
// what a mess!
|
||||
_typeSystem = typeSystem;
|
||||
_asm = typeSystem.TargetAssemblyDefinition;
|
||||
|
||||
TypeDefinition ResolveType(string name) =>
|
||||
typeSystem.GetTypeReference(_typeSystem.FindType(name)).Resolve()
|
||||
?? throw new NullReferenceException($"type must exist: {name}");
|
||||
|
||||
_iocManager = ResolveType("Robust.Shared.IoC.IoCManager");
|
||||
_iXamlProxyHelper = ResolveType(
|
||||
"Robust.Client.UserInterface.XAML.Proxy.IXamlProxyHelper"
|
||||
);
|
||||
_resolveXamlProxyHelperMethod = _asm.MainModule.ImportReference(
|
||||
_iocManager.Methods
|
||||
.First(m => m.Name == "Resolve")
|
||||
.MakeGenericMethod(_iXamlProxyHelper)
|
||||
);
|
||||
|
||||
_populateMethod = _asm.MainModule.ImportReference(
|
||||
_iXamlProxyHelper.Methods
|
||||
.First(m => m.Name == "Populate")
|
||||
);
|
||||
|
||||
_systemType = ResolveType("System.Type");
|
||||
|
||||
_getTypeFromHandleMethod = _asm.MainModule.ImportReference(
|
||||
_systemType.Resolve()
|
||||
.Methods
|
||||
.First(m => m.Name == "GetTypeFromHandle")
|
||||
);
|
||||
|
||||
_stringType = ResolveType("System.String");
|
||||
|
||||
_xamlMetadataAttributeType = ResolveType(
|
||||
"Robust.Client.UserInterface.XAML.Proxy.XamlMetadataAttribute"
|
||||
);
|
||||
|
||||
_xamlMetadataAttributeConstructor = _asm.MainModule.ImportReference(
|
||||
_xamlMetadataAttributeType
|
||||
.GetConstructors()
|
||||
.First(
|
||||
c => c.Parameters.Count == ExpectedNMetadataArgs &&
|
||||
c.Parameters.All(p => p.ParameterType.FullName == "System.String")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a "trampoline" -- this is a method on the given subject which has the following general logic:
|
||||
///
|
||||
/// <code>
|
||||
/// void TrampolineName(Subject subject) {
|
||||
/// if (IoCManager.Resolve{XamlProxyHelper}().Populate(typeof(Subject), subject)) {
|
||||
/// return;
|
||||
/// }
|
||||
/// aotPopulateMethod(null, subject)
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="subject">the type to create a trampoline on</param>
|
||||
/// <param name="aotPopulateMethod">the populate method to call if XamlProxyHelper's Populate method returns false</param>
|
||||
/// <returns>the new trampoline method</returns>
|
||||
private MethodDefinition CreateTrampoline(TypeDefinition subject, MethodDefinition aotPopulateMethod)
|
||||
{
|
||||
var trampoline = new MethodDefinition(
|
||||
TrampolineName,
|
||||
MethodAttributes.Static | MethodAttributes.Private,
|
||||
_asm.MainModule.TypeSystem.Void
|
||||
);
|
||||
trampoline.Parameters.Add(new ParameterDefinition(subject));
|
||||
subject.Methods.Add(trampoline);
|
||||
|
||||
void Emit(Instruction i) => trampoline.Body.Instructions.Add(i);
|
||||
|
||||
Emit(Instruction.Create(OpCodes.Call, _resolveXamlProxyHelperMethod));
|
||||
Emit(Instruction.Create(OpCodes.Ldtoken, subject));
|
||||
Emit(Instruction.Create(OpCodes.Call, _getTypeFromHandleMethod));
|
||||
Emit(Instruction.Create(OpCodes.Ldarg_0));
|
||||
Emit(Instruction.Create(OpCodes.Callvirt, _populateMethod));
|
||||
|
||||
var ret = Instruction.Create(OpCodes.Ret);
|
||||
Emit(Instruction.Create(OpCodes.Brtrue_S, ret));
|
||||
Emit(Instruction.Create(OpCodes.Ldnull));
|
||||
Emit(Instruction.Create(OpCodes.Ldarg_0));
|
||||
Emit(Instruction.Create(OpCodes.Call, aotPopulateMethod));
|
||||
Emit(ret);
|
||||
|
||||
return trampoline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trampoline on <paramref name="subject" />, then replaces
|
||||
/// calls to RobustXamlLoader.Load with calls to the generated trampoline.
|
||||
/// Returns true if the patching succeeded.
|
||||
/// </summary>
|
||||
/// <param name="subject">the subject</param>
|
||||
/// <param name="aotPopulateMethod">the populate method</param>
|
||||
/// <returns>true</returns>
|
||||
public bool TrampolineCallsToXamlLoader(TypeDefinition subject, MethodDefinition aotPopulateMethod)
|
||||
{
|
||||
// PYREX NOTE: This logic is brittle and has a lot of cases
|
||||
// I do not understand all of them, but I have faithfully ported them
|
||||
// Paul Ritter wrote most of this
|
||||
var trampoline = CreateTrampoline(subject, aotPopulateMethod);
|
||||
|
||||
var foundXamlLoader = false;
|
||||
// Find RobustXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this)
|
||||
foreach (var method in subject.Methods
|
||||
.Where(m => !m.Attributes.HasFlag(MethodAttributes.Static)))
|
||||
{
|
||||
var i = method.Body.Instructions;
|
||||
for (var c = 1; c < i.Count; c++)
|
||||
{
|
||||
if (i[c].OpCode == OpCodes.Call)
|
||||
{
|
||||
var op = i[c].Operand as MethodReference;
|
||||
|
||||
if (op != null
|
||||
&& op.Name == TrampolineName)
|
||||
{
|
||||
foundXamlLoader = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (op != null
|
||||
&& op.Name == "Load"
|
||||
&& op.Parameters.Count == 1
|
||||
&& op.Parameters[0].ParameterType.FullName == "System.Object"
|
||||
&& op.DeclaringType.FullName == "Robust.Client.UserInterface.XAML.RobustXamlLoader")
|
||||
{
|
||||
if (MatchThisCall(i, c - 1))
|
||||
{
|
||||
i[c].Operand = trampoline;
|
||||
foundXamlLoader = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundXamlLoader)
|
||||
{
|
||||
var ctors = subject.GetConstructors()
|
||||
.Where(c => !c.IsStatic)
|
||||
.ToList();
|
||||
// We can inject xaml loader into default constructor
|
||||
if (ctors.Count == 1 && ctors[0].Body.Instructions.Count(o => o.OpCode != OpCodes.Nop) == 3)
|
||||
{
|
||||
var i = ctors[0].Body.Instructions;
|
||||
var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret));
|
||||
i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline));
|
||||
i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0));
|
||||
foundXamlLoader = true;
|
||||
}
|
||||
}
|
||||
|
||||
return foundXamlLoader;
|
||||
}
|
||||
|
||||
private static bool MatchThisCall(Collection<Instruction> instructions, int idx)
|
||||
{
|
||||
var i = instructions[idx];
|
||||
// A "normal" way of passing `this` to a static method:
|
||||
|
||||
// ldarg.0
|
||||
// call void [uAvalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object)
|
||||
|
||||
return i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a XamlMetadataAttribute to a given type, containing all the compiler
|
||||
/// parameters for its Populate method.
|
||||
/// </summary>
|
||||
/// <param name="subject">the subject type</param>
|
||||
/// <param name="uri">the URI we generated</param>
|
||||
/// <param name="filename">the filename</param>
|
||||
/// <param name="content">the new content</param>
|
||||
public void AddXamlMetadata(TypeDefinition subject, Uri uri, string filename, string content)
|
||||
{
|
||||
var attribute = new CustomAttribute(_xamlMetadataAttributeConstructor);
|
||||
var args = new string[ExpectedNMetadataArgs] // reference this so that changing the number is a compile error
|
||||
{
|
||||
uri.ToString(), filename, content
|
||||
};
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
attribute.ConstructorArguments.Add(
|
||||
new CustomAttributeArgument(_stringType, arg)
|
||||
);
|
||||
}
|
||||
|
||||
subject.CustomAttributes.Add(attribute);
|
||||
}
|
||||
|
||||
}
|
||||
113
Robust.Xaml/MathParsing.cs
Normal file
113
Robust.Xaml/MathParsing.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Robust.Xaml;
|
||||
|
||||
internal static class MathParsing
|
||||
{
|
||||
private static float[]? ParseSingleArr(string input)
|
||||
{
|
||||
// Transliteration note: The original patterns in this file were Pidgin parsers
|
||||
// All of them were variations on Real.Select(c => (float c)).Between(SkipWhiteSpaces).Repeat(n)
|
||||
// They somehow handled commas too, but I don't know how
|
||||
//
|
||||
// SkipWhitespace splits based on char.IsWhitespace:
|
||||
// https://github.com/benjamin-hodgson/Pidgin/blob/cc72abb/Pidgin/Parser.Whitespace.cs#L30
|
||||
var items = SplitStringByFunction(input, (c) => c == ',' || char.IsWhiteSpace(c));
|
||||
var outs = new float[items.Count];
|
||||
|
||||
for (var i = 0; i < outs.Length; i++)
|
||||
{
|
||||
// Parser.Real ultimately resorts to double.TryParse
|
||||
// https://github.com/benjamin-hodgson/Pidgin/blob/cc72abb/Pidgin/Parser.Number.cs#L222
|
||||
var parsed = double.TryParse(
|
||||
items[i],
|
||||
NumberStyles.Float | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var d
|
||||
);
|
||||
if (!parsed)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
outs[i] = (float)d;
|
||||
}
|
||||
|
||||
return outs;
|
||||
}
|
||||
|
||||
private static List<string> SplitStringByFunction(string s, Func<char, bool> isSeparator)
|
||||
{
|
||||
// we want to split by commas _or_ char.IsWhitespace
|
||||
// C#'s Split() can do one but not both
|
||||
var splitItems = new List<string>();
|
||||
var itemInProgress = new StringBuilder();
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (isSeparator(c))
|
||||
{
|
||||
if (itemInProgress.Length > 0)
|
||||
{
|
||||
splitItems.Add(itemInProgress.ToString());
|
||||
itemInProgress.Clear();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
itemInProgress.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (itemInProgress.Length > 0)
|
||||
{
|
||||
splitItems.Add(itemInProgress.ToString());
|
||||
}
|
||||
|
||||
return splitItems;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a vector of two floats separated by commas or spaces, such as
|
||||
/// "1,2" or "1.5 2.5"
|
||||
/// </summary>
|
||||
/// <param name="s">the string representation of the vector</param>
|
||||
/// <returns>the parsed floats, or null if parsing failed</returns>
|
||||
public static (float, float)? ParseVector2(string s)
|
||||
{
|
||||
var arr = ParseSingleArr(s);
|
||||
if (arr == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (arr.Length == 2)
|
||||
{
|
||||
return (arr[0], arr[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a vector of one, two, or four floats separated by commas or
|
||||
/// spaces, such as "1", "1e2,2e3" or ".1,.2,.3,.4"
|
||||
/// </summary>
|
||||
/// <param name="s">the string representation of the vector</param>
|
||||
/// <returns>the parsed floats, or null if parsing failed</returns>
|
||||
public static float[]? ParseThickness(string s)
|
||||
{
|
||||
var arr = ParseSingleArr(s);
|
||||
if (arr == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (arr.Length == 1 || arr.Length == 2 || arr.Length == 4)
|
||||
{
|
||||
return arr;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
using System.Reflection.Emit;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
namespace Robust.Xaml
|
||||
{
|
||||
internal class RXamlColorAstNode
|
||||
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
|
||||
@@ -6,9 +6,9 @@ using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
namespace Robust.Xaml
|
||||
{
|
||||
public abstract class RXamlVecLikeConstAstNode<T>
|
||||
internal abstract class RXamlVecLikeConstAstNode<T>
|
||||
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
|
||||
where T : unmanaged
|
||||
{
|
||||
@@ -47,7 +47,7 @@ namespace Robust.Build.Tasks
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
|
||||
internal sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
|
||||
{
|
||||
public RXamlSingleVecLikeConstAstNode(
|
||||
IXamlLineInfo lineInfo,
|
||||
@@ -69,7 +69,7 @@ namespace Robust.Build.Tasks
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
|
||||
internal sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
|
||||
{
|
||||
public RXamlInt32VecLikeConstAstNode(
|
||||
IXamlLineInfo lineInfo,
|
||||
@@ -2,9 +2,9 @@
|
||||
using XamlX.Transform;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
namespace Robust.Xaml
|
||||
{
|
||||
class RXamlWellKnownTypes
|
||||
internal class RXamlWellKnownTypes
|
||||
{
|
||||
public XamlTypeWellKnownTypes XamlIlTypes { get; }
|
||||
public IXamlType Single { get; }
|
||||
17
Robust.Xaml/Robust.Xaml.csproj
Normal file
17
Robust.Xaml/Robust.Xaml.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using XamlX;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
@@ -7,7 +6,7 @@ using XamlX.IL;
|
||||
using XamlX.Transform;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
namespace Robust.Xaml
|
||||
{
|
||||
/// <summary>
|
||||
/// Emitters & Transformers based on:
|
||||
@@ -15,7 +14,7 @@ namespace Robust.Build.Tasks
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/afb8ae6f3c517dae912729511483995b16cb31af/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs
|
||||
/// </summary>
|
||||
public class RobustXamlILCompiler : XamlILCompiler
|
||||
internal class RobustXamlILCompiler : XamlILCompiler
|
||||
{
|
||||
public RobustXamlILCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings, bool fillWithDefaults) : base(configuration, emitMappings, fillWithDefaults)
|
||||
{
|
||||
@@ -41,8 +40,9 @@ namespace Robust.Build.Tasks
|
||||
&& mg.Children.OfType<RobustNameScopeRegistrationXamlIlNode>().Any())
|
||||
return node;
|
||||
|
||||
IXamlAstValueNode value = null;
|
||||
IXamlAstValueNode? value = null;
|
||||
for (var c = 0; c < pa.Values.Count; c++)
|
||||
{
|
||||
if (pa.Values[c].Type.GetClrType().Equals(context.Configuration.WellKnownTypes.String))
|
||||
{
|
||||
value = pa.Values[c];
|
||||
@@ -57,6 +57,7 @@ namespace Robust.Build.Tasks
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
@@ -84,9 +85,9 @@ namespace Robust.Build.Tasks
|
||||
class RobustNameScopeRegistrationXamlIlNode : XamlAstNode, IXamlAstManipulationNode
|
||||
{
|
||||
public IXamlAstValueNode Name { get; set; }
|
||||
public IXamlType TargetType { get; }
|
||||
public IXamlType? TargetType { get; }
|
||||
|
||||
public RobustNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType targetType) : base(name)
|
||||
public RobustNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType? targetType) : base(name)
|
||||
{
|
||||
TargetType = targetType;
|
||||
Name = name;
|
||||
@@ -104,7 +105,7 @@ namespace Robust.Build.Tasks
|
||||
{
|
||||
|
||||
var scopeField = context.RuntimeContext.ContextType.Fields.First(f =>
|
||||
f.Name == XamlCompiler.ContextNameScopeFieldName);
|
||||
f.Name == XamlCustomizations.ContextNameScopeFieldName);
|
||||
var namescopeRegisterFunction = context.Configuration.TypeSystem
|
||||
.FindType("Robust.Client.UserInterface.XAML.NameScope").Methods
|
||||
.First(m => m.Name == "Register");
|
||||
@@ -128,7 +129,7 @@ namespace Robust.Build.Tasks
|
||||
|
||||
return XamlILNodeEmitResult.Void(1);
|
||||
}
|
||||
return default;
|
||||
return default!; // PYREX NOTE: This doesn't seem safe! But it's what we were doing before Nullable
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +162,7 @@ namespace Robust.Build.Tasks
|
||||
{
|
||||
if (!(node is HandleRootObjectScopeNode))
|
||||
{
|
||||
return null;
|
||||
return null!; // PYREX NOTE: This doesn't seem safe, but it predates Nullable on this file
|
||||
}
|
||||
|
||||
var controlType = context.Configuration.TypeSystem.FindType("Robust.Client.UserInterface.Control");
|
||||
@@ -170,7 +171,7 @@ namespace Robust.Build.Tasks
|
||||
var dontAbsorb = codeGen.DefineLabel();
|
||||
var end = codeGen.DefineLabel();
|
||||
var contextScopeField = context.RuntimeContext.ContextType.Fields.First(f =>
|
||||
f.Name == XamlCompiler.ContextNameScopeFieldName);
|
||||
f.Name == XamlCustomizations.ContextNameScopeFieldName);
|
||||
var controlNameScopeField = controlType.Fields.First(f => f.Name == "NameScope");
|
||||
var nameScopeType = context.Configuration.TypeSystem
|
||||
.FindType("Robust.Client.UserInterface.XAML.NameScope");
|
||||
@@ -1,33 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Collections.Generic;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
namespace Robust.Xaml
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers taken from:
|
||||
/// Helpers taken from AvaloniaUI on GitHub.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
|
||||
/// </summary>
|
||||
public partial class XamlCompiler
|
||||
/// </remarks>
|
||||
internal partial class XamlAotCompiler
|
||||
{
|
||||
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
|
||||
|| r.Name.ToLowerInvariant().EndsWith(".paml")
|
||||
|| r.Name.ToLowerInvariant().EndsWith(".axaml");
|
||||
private static readonly string[] NameSuffixes = {".xaml", ".paml", ".axaml"};
|
||||
|
||||
private static bool MatchThisCall(Collection<Instruction> instructions, int idx)
|
||||
{
|
||||
var i = instructions[idx];
|
||||
// A "normal" way of passing `this` to a static method:
|
||||
|
||||
// ldarg.0
|
||||
// call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object)
|
||||
|
||||
return i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true);
|
||||
}
|
||||
static bool CheckXamlName(IResource r) =>
|
||||
NameSuffixes.Any(suffix => r.Name.ToLowerInvariant().EndsWith(suffix));
|
||||
|
||||
interface IResource : IFileSource
|
||||
{
|
||||
209
Robust.Xaml/XamlAotCompiler.cs
Normal file
209
Robust.Xaml/XamlAotCompiler.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
using Mono.Cecil;
|
||||
using XamlX;
|
||||
using XamlX.Ast;
|
||||
using XamlX.IL;
|
||||
using XamlX.Parsers;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class: holds scope information for a Microsoft.Build.Framework
|
||||
/// build in order to AOT-compile the XAML resources for an assembly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Also embed enough information to support future JIT attempts on those same resources.
|
||||
///
|
||||
/// Code primarily by Paul Ritter, touched by Pyrex in 2024.
|
||||
///
|
||||
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
|
||||
/// Adjusted for our UI Framework
|
||||
/// </remarks>
|
||||
internal partial class XamlAotCompiler
|
||||
{
|
||||
/// <summary>
|
||||
/// Update the assembly whose name is <paramref name="input" />, then
|
||||
/// save an updated assembly to <paramref name="output"/>.
|
||||
/// </summary>
|
||||
/// <param name="engine">the Microsoft build engine (used for logging)</param>
|
||||
/// <param name="input">the input assembly by name</param>
|
||||
/// <param name="references">all the assemblies that the input Xaml is allowed to reference</param>
|
||||
/// <param name="output">the place to put the output assembly</param>
|
||||
/// <param name="strongNameKey">
|
||||
/// a file to use in order to generate a "strong name" for the assembly
|
||||
/// (https://learn.microsoft.com/en-us/dotnet/standard/assembly/strong-named)
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// true if this succeeds and
|
||||
/// true if the result was written to <paramref name="output"/>
|
||||
/// </returns>
|
||||
public static (bool success, bool writtentofile) Compile(IBuildEngine engine, string input, string[] references,
|
||||
string output, string? strongNameKey)
|
||||
{
|
||||
var typeSystem = new CecilTypeSystem(references
|
||||
.Where(r => !r.ToLowerInvariant().EndsWith("robust.build.tasks.dll"))
|
||||
.Concat(new[] { input }), input);
|
||||
|
||||
var asm = typeSystem.TargetAssemblyDefinition;
|
||||
|
||||
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
|
||||
{
|
||||
// If this type exists, the assembly has already been processed by us.
|
||||
// Do not run again, it would corrupt the file.
|
||||
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
|
||||
// but better safe than sorry eh?
|
||||
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
var compileRes = CompileCore(engine, typeSystem);
|
||||
if (!compileRes)
|
||||
return (false, false);
|
||||
|
||||
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
|
||||
if (!string.IsNullOrWhiteSpace(strongNameKey))
|
||||
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
|
||||
|
||||
asm.Write(output, writerParameters);
|
||||
|
||||
return (true, true);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For each XAML resource, identify its affiliated class, invoke the
|
||||
/// AOT compiler, update the class to call into the generated code,
|
||||
/// and write down metadata for future JIT compiles.
|
||||
/// </summary>
|
||||
/// <param name="engine">the Microsoft build engine (for logging)</param>
|
||||
/// <param name="typeSystem">the type system (which includes info about the target assembly)</param>
|
||||
/// <returns>true if compilation succeeded in every case</returns>
|
||||
static bool CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
|
||||
{
|
||||
var asm = typeSystem.TargetAssemblyDefinition;
|
||||
var embrsc = new EmbeddedResources(asm);
|
||||
|
||||
var xaml = new XamlCustomizations(typeSystem, typeSystem.TargetAssembly);
|
||||
var lowLevel = new LowLevelCustomizations(typeSystem);
|
||||
|
||||
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
|
||||
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
|
||||
asm.MainModule.Types.Add(contextDef);
|
||||
var contextClass = XamlILContextDefinition.GenerateContextClass(
|
||||
typeSystem.CreateTypeBuilder(contextDef), typeSystem,
|
||||
xaml.TypeMappings, xaml.EmitMappings
|
||||
);
|
||||
|
||||
bool CompileGroup(IResourceGroup group)
|
||||
{
|
||||
var typeDef = new TypeDefinition("CompiledRobustXaml", "!" + group.Name, TypeAttributes.Class,
|
||||
asm.MainModule.TypeSystem.Object);
|
||||
|
||||
asm.MainModule.Types.Add(typeDef);
|
||||
|
||||
foreach (var res in group.Resources.Where(CheckXamlName))
|
||||
{
|
||||
try
|
||||
{
|
||||
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", MessageImportance.Low);
|
||||
|
||||
var xamlText = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
|
||||
var parsed = XDocumentXamlParser.Parse(xamlText);
|
||||
|
||||
var initialRoot = (XamlAstObjectNode) parsed.Root;
|
||||
|
||||
var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
|
||||
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
|
||||
string classname;
|
||||
if (classDirective != null && classDirective.Values[0] is XamlAstTextNode tn)
|
||||
{
|
||||
classname = tn.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
classname = res.Name.Replace(".xaml","");
|
||||
}
|
||||
|
||||
var classType = typeSystem.TargetAssembly.FindType(classname);
|
||||
if (classType == null)
|
||||
throw new InvalidProgramException($"Unable to find type '{classname}'");
|
||||
|
||||
xaml.ILCompiler.Transform(parsed);
|
||||
|
||||
var populateName = $"Populate:{res.Name}";
|
||||
|
||||
var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve()!;
|
||||
var populateBuilder = typeSystem.CreateTypeBuilder(classTypeDefinition);
|
||||
|
||||
xaml.ILCompiler.Compile(parsed, contextClass,
|
||||
xaml.ILCompiler.DefinePopulateMethod(populateBuilder, parsed, populateName, true),
|
||||
null,
|
||||
null,
|
||||
(closureName, closureBaseType) =>
|
||||
populateBuilder.DefineSubType(closureBaseType, closureName, false),
|
||||
res.Uri, res
|
||||
);
|
||||
|
||||
var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve().Methods
|
||||
.First(m => m.Name == populateName);
|
||||
|
||||
lowLevel.AddXamlMetadata(classTypeDefinition, new Uri(res.Uri), res.FilePath, xamlText);
|
||||
var foundXamlLoader = lowLevel.TrampolineCallsToXamlLoader(classTypeDefinition, compiledPopulateMethod);
|
||||
|
||||
if (!foundXamlLoader)
|
||||
{
|
||||
throw new InvalidProgramException(
|
||||
$"No call to RobustXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
|
||||
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
|
||||
}
|
||||
res.Remove();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (embrsc.Resources.Count(CheckXamlName) != 0)
|
||||
{
|
||||
if (!CompileGroup(embrsc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is <see cref="IFileSource"/> from XamlX, augmented with the other
|
||||
/// arguments that the XAML compiler wants.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We store these later in the build process inside a XamlMetadataAttribute,
|
||||
/// in order to support JIT compilation.
|
||||
/// </remarks>
|
||||
interface IResource : IFileSource
|
||||
{
|
||||
string Uri { get; }
|
||||
string Name { get; }
|
||||
void Remove();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A named collection of <see cref="IResource"/>s.
|
||||
/// </summary>
|
||||
interface IResourceGroup
|
||||
{
|
||||
string Name { get; }
|
||||
IEnumerable<IResource> Resources { get; }
|
||||
}
|
||||
218
Robust.Xaml/XamlCustomizations.cs
Normal file
218
Robust.Xaml/XamlCustomizations.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using XamlX;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
using XamlX.Transform;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml;
|
||||
|
||||
/// <summary>
|
||||
/// Shared XAML config info that both the AOT and JIT compiler can use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a bunch of code primarily written by PJB that originally appeared in XamlAotCompiler.cs.
|
||||
/// </remarks>
|
||||
internal sealed class XamlCustomizations
|
||||
{
|
||||
public const string ContextNameScopeFieldName = "RobustNameScope";
|
||||
public readonly IXamlTypeSystem TypeSystem;
|
||||
public readonly XamlLanguageTypeMappings TypeMappings;
|
||||
public readonly XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> EmitMappings;
|
||||
public readonly TransformerConfiguration TransformerConfiguration;
|
||||
public readonly RobustXamlILCompiler ILCompiler;
|
||||
|
||||
/// <summary>
|
||||
/// Create and hold a bunch of resources related to SS14's particular dialect of XAML.
|
||||
/// </summary>
|
||||
/// <param name="typeSystem">
|
||||
/// the type system for XamlX to use
|
||||
/// (both <see cref="CecilTypeSystem"/> and <see cref="CecilTypeSystem"/> work)
|
||||
/// </param>
|
||||
/// <param name="defaultAssembly">the default assembly (for unqualified names to be looked up in)</param>
|
||||
public XamlCustomizations(IXamlTypeSystem typeSystem, IXamlAssembly defaultAssembly)
|
||||
{
|
||||
TypeSystem = typeSystem;
|
||||
TypeMappings = new XamlLanguageTypeMappings(typeSystem)
|
||||
{
|
||||
XmlnsAttributes =
|
||||
{
|
||||
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
|
||||
|
||||
},
|
||||
ContentAttributes =
|
||||
{
|
||||
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
|
||||
},
|
||||
UsableDuringInitializationAttributes =
|
||||
{
|
||||
typeSystem.GetType("Robust.Client.UserInterface.XAML.UsableDuringInitializationAttribute")
|
||||
},
|
||||
DeferredContentPropertyAttributes =
|
||||
{
|
||||
typeSystem.GetType("Robust.Client.UserInterface.XAML.DeferredContentAttribute")
|
||||
},
|
||||
RootObjectProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestRootObjectProvider"),
|
||||
UriContextProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestUriContext"),
|
||||
ProvideValueTarget = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestProvideValueTarget"),
|
||||
};
|
||||
|
||||
EmitMappings = new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>
|
||||
{
|
||||
ContextTypeBuilderCallback = EmitNameScopeField
|
||||
};
|
||||
TransformerConfiguration = new TransformerConfiguration(
|
||||
typeSystem,
|
||||
defaultAssembly,
|
||||
TypeMappings,
|
||||
XamlXmlnsMappings.Resolve(typeSystem, TypeMappings),
|
||||
CustomValueConverter
|
||||
);
|
||||
ILCompiler = new RobustXamlILCompiler(TransformerConfiguration, EmitMappings, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a field of type NameScope that contains a new NameScope, then
|
||||
/// alter the type's constructor to initialize that field.
|
||||
/// </summary>
|
||||
/// <param name="typeBuilder">the type to alter</param>
|
||||
/// <param name="constructor">the constructor to alter</param>
|
||||
private void EmitNameScopeField(
|
||||
IXamlTypeBuilder<IXamlILEmitter> typeBuilder,
|
||||
IXamlILEmitter constructor
|
||||
)
|
||||
{
|
||||
var nameScopeType = TypeSystem.FindType("Robust.Client.UserInterface.XAML.NameScope");
|
||||
var field = typeBuilder.DefineField(nameScopeType,
|
||||
ContextNameScopeFieldName,
|
||||
true,
|
||||
false);
|
||||
constructor
|
||||
.Ldarg_0()
|
||||
.Newobj(nameScopeType.GetConstructor())
|
||||
.Stfld(field);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Convert a <see cref="XamlAstTextNode"/> to some other kind of node,
|
||||
/// if the purpose of the node appears to be to represent one of various
|
||||
/// builtin types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// (See, for instance, <see cref="RXamlColorAstNode"/>.)
|
||||
///
|
||||
/// The arguments here come from an interface built into XamlX.
|
||||
/// </remarks>
|
||||
/// <param name="context">context object that holds the TransformerConfiguration</param>
|
||||
/// <param name="node">the node to consider rewriting</param>
|
||||
/// <param name="type">the type of that node</param>
|
||||
/// <param name="result">results get written to here</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="XamlLoadException">if the literal for a type is poorly spelled for that type</exception>
|
||||
private static bool CustomValueConverter(
|
||||
AstTransformationContext context,
|
||||
IXamlAstValueNode node,
|
||||
IXamlType type,
|
||||
out IXamlAstValueNode? result)
|
||||
{
|
||||
if (!(node is XamlAstTextNode textNode))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var text = textNode.Text;
|
||||
var types = context.GetRobustTypes();
|
||||
|
||||
if (type.Equals(types.Vector2))
|
||||
{
|
||||
var foo = MathParsing.ParseVector2(text);
|
||||
|
||||
if (foo == null)
|
||||
{
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
|
||||
}
|
||||
|
||||
var (x, y) = foo.Value;
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Vector2,
|
||||
types.Vector2ConstructorFull,
|
||||
types.Single,
|
||||
new[] { x, y });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Thickness))
|
||||
{
|
||||
var foo = MathParsing.ParseThickness(text);
|
||||
|
||||
if (foo == null)
|
||||
{
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
|
||||
}
|
||||
|
||||
var val = foo;
|
||||
float[] full;
|
||||
if (val.Length == 1)
|
||||
{
|
||||
var u = val[0];
|
||||
full = new[] { u, u, u, u };
|
||||
}
|
||||
else if (val.Length == 2)
|
||||
{
|
||||
var h = val[0];
|
||||
var v = val[1];
|
||||
full = new[] { h, v, h, v };
|
||||
}
|
||||
else // 4
|
||||
{
|
||||
full = val;
|
||||
}
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Thickness,
|
||||
types.ThicknessConstructorFull,
|
||||
types.Single,
|
||||
full);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Color))
|
||||
{
|
||||
// TODO: Interpret these colors at XAML compile time instead of at runtime.
|
||||
result = new RXamlColorAstNode(node, types, text);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap the <paramref name="filePath"/> and <paramref name="contents"/>
|
||||
/// from a Xaml file or from a XamlMetadataAttribute.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface is the primary input format that XamlX expects.
|
||||
/// </remarks>
|
||||
/// <param name="filePath">the resource file path</param>
|
||||
/// <param name="contents">the contents</param>
|
||||
/// <returns>IFileSource</returns>
|
||||
public IFileSource CreateFileSource(string filePath, byte[] contents)
|
||||
{
|
||||
return new InternalFileSource(filePath, contents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A trivial implementation of <see cref="IFileSource"/>.
|
||||
/// </summary>
|
||||
private class InternalFileSource(string filePath, byte[] contents) : IFileSource
|
||||
{
|
||||
public string FilePath { get; } = filePath;
|
||||
public byte[] FileContents { get; } = contents;
|
||||
}
|
||||
}
|
||||
180
Robust.Xaml/XamlJitCompiler.cs
Normal file
180
Robust.Xaml/XamlJitCompiler.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using XamlX.IL;
|
||||
using XamlX.Parsers;
|
||||
|
||||
namespace Robust.Xaml;
|
||||
|
||||
/// <summary>
|
||||
/// A JIT compiler for Xaml.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses <see cref="System.Reflection.Emit"/>, which can find types
|
||||
/// at runtime without looking for their assemblies on disk.
|
||||
/// </remarks>
|
||||
internal sealed class XamlJitCompiler
|
||||
{
|
||||
private readonly SreTypeSystem _typeSystem;
|
||||
|
||||
private static int _assemblyId;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a XamlJitCompiler.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// No configuration is needed or possible.
|
||||
///
|
||||
/// This is a pretty expensive function because it creates an
|
||||
/// <see cref="SreTypeSystem"/>, which requires going through the loaded
|
||||
/// assembly list.
|
||||
/// </remarks>
|
||||
public XamlJitCompiler()
|
||||
{
|
||||
_typeSystem = new SreTypeSystem();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a name for a new dynamic assembly.
|
||||
/// </summary>
|
||||
/// <returns>the new name</returns>
|
||||
private static string GenerateAssemblyName()
|
||||
{
|
||||
// make the name unique (even though C# possibly doesn't care)
|
||||
return
|
||||
$"{nameof(XamlJitCompiler)}_{Interlocked.Increment(ref _assemblyId)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile the Populate method for <paramref name="t" />, using the given
|
||||
/// uri/path/contents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These values (except for contents) are generated during the AOT compile
|
||||
/// process.
|
||||
///
|
||||
/// It is not enforced that they be the same after JIT -- the JITed code has
|
||||
/// no knowledge of the state of the AOT'ed code -- but our code and
|
||||
/// documentation do assume that.
|
||||
/// </remarks>
|
||||
/// <param name="t">the type whose Populate method should be generated</param>
|
||||
/// <param name="uri">the Uri associated with the Control</param>
|
||||
/// <param name="filePath">the resource file path for the control</param>
|
||||
/// <param name="contents">the contents of the new XAML file</param>
|
||||
/// <returns>Success or Failure depending on whether an error was thrown</returns>
|
||||
public XamlJitCompilerResult Compile(
|
||||
Type t,
|
||||
Uri uri,
|
||||
string filePath,
|
||||
string contents
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = CompileOrCrash(t, uri, filePath, contents);
|
||||
return new XamlJitCompilerResult.Success(result);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new XamlJitCompilerResult.Error(
|
||||
e,
|
||||
e.Message.StartsWith("Unable to resolve type")
|
||||
? "Is the type internal? (hot reloading can't handle that right now!)"
|
||||
: null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private MethodInfo CompileOrCrash(
|
||||
Type t,
|
||||
Uri uri,
|
||||
string filePath,
|
||||
string contents
|
||||
)
|
||||
{
|
||||
|
||||
var xaml = new XamlCustomizations(_typeSystem, _typeSystem.FindAssembly(t.Assembly.FullName));
|
||||
|
||||
// attempt to parse the code
|
||||
var document = XDocumentXamlParser.Parse(contents);
|
||||
|
||||
// generate a toy assembly to contain everything we make
|
||||
var assemblyNameString = GenerateAssemblyName();
|
||||
var assemblyName = new AssemblyName(assemblyNameString);
|
||||
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
|
||||
assemblyName,
|
||||
AssemblyBuilderAccess.RunAndCollect
|
||||
);
|
||||
var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyNameString);
|
||||
|
||||
var contextClassRawBuilder = moduleBuilder.DefineType("ContextClass");
|
||||
var populateClassRawBuilder = moduleBuilder.DefineType("PopulateClass");
|
||||
|
||||
var contextClassBuilder = _typeSystem.CreateTypeBuilder(contextClassRawBuilder);
|
||||
var populateClassBuilder = _typeSystem.CreateTypeBuilder(populateClassRawBuilder);
|
||||
|
||||
var contextClass = XamlILContextDefinition.GenerateContextClass(
|
||||
contextClassBuilder,
|
||||
_typeSystem,
|
||||
xaml.TypeMappings,
|
||||
xaml.EmitMappings
|
||||
);
|
||||
var populateName = "!Populate";
|
||||
|
||||
xaml.ILCompiler.Transform(document);
|
||||
xaml.ILCompiler.Compile(
|
||||
document,
|
||||
contextClass,
|
||||
xaml.ILCompiler.DefinePopulateMethod(
|
||||
populateClassBuilder,
|
||||
document,
|
||||
populateName,
|
||||
true
|
||||
),
|
||||
null,
|
||||
null,
|
||||
(closureName, closureBaseType) =>
|
||||
contextClassBuilder.DefineSubType(closureBaseType, closureName, false),
|
||||
uri.ToString(),
|
||||
xaml.CreateFileSource(filePath, Encoding.UTF8.GetBytes(contents))
|
||||
);
|
||||
|
||||
contextClassBuilder.CreateType();
|
||||
|
||||
var populateClass = populateClassRawBuilder.CreateTypeInfo()!;
|
||||
var implementation = populateClass.GetMethod(populateName);
|
||||
|
||||
if (implementation == null)
|
||||
{
|
||||
throw new NullReferenceException("populate method should have existed");
|
||||
}
|
||||
|
||||
return implementation;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An enum containing either <see cref="Success" /> (with a <see cref="MethodInfo" />)
|
||||
/// or <see cref="Error" />. (with an <see cref="Exception" />, and an optional hint
|
||||
/// for how to fix it)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is not guaranteed that the <see cref="Exception" /> ever appeared on the stack.
|
||||
/// That is an implementation detail of <see cref="XamlJitCompiler.Compile"/>.
|
||||
/// </remarks>
|
||||
public abstract class XamlJitCompilerResult
|
||||
{
|
||||
public class Success(MethodInfo methodInfo) : XamlJitCompilerResult
|
||||
{
|
||||
public MethodInfo MethodInfo { get; } = methodInfo;
|
||||
}
|
||||
|
||||
public class Error(Exception raw, string? hint) : XamlJitCompilerResult
|
||||
{
|
||||
public Exception Raw { get; } = raw;
|
||||
public string? Hint { get; } = hint;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cefglue", "cefglue", "{2D78
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CefGlue", "cefglue\CefGlue\CefGlue.csproj", "{6BC71226-BA9C-4CD6-9838-03AC076F9518}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Xaml", "Robust.Xaml\Robust.Xaml.csproj", "{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -229,6 +231,14 @@ Global
|
||||
{6BC71226-BA9C-4CD6-9838-03AC076F9518}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6BC71226-BA9C-4CD6-9838-03AC076F9518}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6BC71226-BA9C-4CD6-9838-03AC076F9518}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user