Compare commits

...

17 Commits

Author SHA1 Message Date
metalgearsloth
f659b2b58c Version: 232.0.0 2024-08-29 12:55:51 +10:00
metalgearsloth
b1e13f5b13 Fix BUI interfaces not deep copying (#5410)
* Fix BUI interfaces not deep copying

Didn't think I'd need to on a getstate but client state moment.

* less shitcodey
2024-08-29 12:47:32 +10:00
SlamBamActionman
e5995d4edc Add ObjectSerializer, AppearanceComponent.AppearanceDataInit, and AppearanceSystem.AppendData (#5324)
* V1 commit

* V2 Commit, ObjectSerializer

* Make sure write for objects have the !type:<T> set

* Added AppearanceDataInit

* Change to AppearanceDataInit setting to AppearanceData the moment it itself gets set; ComponentInit is too late. Forgive me sloth.

* RELEASE-NOTES.md

* Fix release notes

* Fix release-notes for realsies
2024-08-28 22:43:58 +10:00
Winkarst
6eb080a277 Add Robust.Xaml.csproj to the solution (#5408) 2024-08-28 13:49:42 +02:00
metalgearsloth
b0cb41e94a Version: 231.1.1 2024-08-28 12:23:04 +10:00
Leon Friedrich
23a23f7c22 Misc toolshed fixes (#5340)
* Prevent map/emplace command errors from locking up the server

* Fix EmplaceCommand

* Fix sort commands

* Fix JoinCommand

* changelog

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-08-28 12:22:47 +10:00
Pieter-Jan Briers
ec3a74d268 Version: 231.1.0 2024-08-27 17:47:25 +02:00
Pieter-Jan Briers
12b0bc4e0a Add way for content to write arbitrary files into replay. (#5405)
Added a new RecordingStopped2 event that receives a IReplayFileWriter object that can be used to write arbitrary files into the replay zip file.

Fixes #5261
2024-08-27 17:38:48 +02:00
metalgearsloth
903041dfd1 Add storage BUI bandaid (#5401) 2024-08-27 17:36:54 +02:00
metalgearsloth
b96419f0b2 Add mapmanager query tests (#5403)
Sanity
2024-08-28 00:24:24 +10:00
metalgearsloth
fe33ad2652 Add physicshull tests (#5404) 2024-08-27 23:24:23 +10:00
metalgearsloth
057a68b366 Minor allocs reductions (#5330)
* Minor allocs reductions

Added a poly struct with the intention of replacing the existing one whenever I finish box2c port.

* fix merges

* Revert some stuff

* Poly tests
2024-08-27 22:58:42 +10:00
metalgearsloth
1a2c9008fe Add Box matrix tests (#5402)
Thought we had but apparently not.
2024-08-27 22:21:48 +10:00
metalgearsloth
cd95929ebe Heavily optimise entitylookup (#5400)
* Heavily optimise entitylookup

Previously I made it so MOST of entitylookup goes through the same 4 or 5 codepaths and uses shapes. The downside was some codepaths were made much slower in the process.

This fixes most of the going up and down lookups that some codepaths did. It should also be faster than the pre-shapes version because GetLocalPhysicsTransform is being used for the non-approx queries and most entities are parented directly to their broadphase.

* Tests and confidence

* code

* dang
2024-08-27 21:32:50 +10:00
Nyeogmi
6396ec472d XAML hot reloading (#5350)
* Move RobustXaml to a shared package

In a near-future change, I'll make it possible to optionally link to
this from Robust.Client, which will allow JIT compiling XAML.

Also upgrade it to a version of .NET that supports nullability
annotations.

* Re-namespace packages

* Add a JIT compiler, plus hooks that call into it

In Debug, after this change, all XAML will be hot reloaded once every
time an assembly is reloaded.

The new code is compiled with SRE and is _not_ sandboxed -- this is not
suitable to run against prod.

In Release, the hot reload path is totally skipped, using the same trick
as SmugLeaf used in an earlier attempt to implement this functionality.

* Hot reload: watcher

This is a bit of a horror, but there's not in-engine support for
identifying the source tree or the XAML files in it.

* Put everything dangerous behind conditional comp

* Code cleanup, docs

* Fix a bad comment

* Deal a little better with crashes in the watcher

* Make reload failures Info, since they're expected

They were previously causing the integration tests to flag, even though
"a few types fail hot reloading because they're internal" is expected
behavior.

* Fix an unnecessary null check

I removed the ability for CompileCore to return null.

* injectors: null! strings, default primitives

* Tidy documentation (thanks, PJB!)

* Reinstate netstandard2.0, abolish Pidgin

* Internal-ize all of Robust.Xaml

* Add a cautionary note to Sandbox.yml

* Shuffle around where conditional compilation occurs

* Privatize fields in XamlImplementationStorage

* Internalize XamlJitDelegate

* Inline some remarks. No cond. comp in Robust.Xaml

* Use file-scoped namespaces

They aren't allowed at Language Level 8.0. (which I arbitrarily picked
for Robust.Xaml because it's the oldest one that would work)

* Bump language level for R.Xaml, file namespaces

* Force hot reloading off for integration tests

* Fix bizarre comment/behavior in XamlImplementationStorage

* Consistently use interfaces, even in generated code

* Update Robust.Client/ClientIoC.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2024-08-27 02:16:57 +02:00
Stalen
d7aa5daf6a Add decimal type to sandbox whitelist (#5396) 2024-08-27 01:35:42 +02:00
metalgearsloth
e3819f8245 Network interfacedata (#5399)
If UIs are dynamically changed this fixes it.
2024-08-26 18:48:15 +10:00
69 changed files with 3342 additions and 717 deletions

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -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

View File

@@ -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!;
}
}

View File

@@ -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}))
));
}
}

View File

@@ -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!;
}
}

View File

@@ -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>

View File

@@ -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; }
}
}

View File

@@ -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>();
}
}

View File

@@ -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();

View File

@@ -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\*" />

View File

@@ -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();
}

View 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);
}

View 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);
}

View 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

View File

@@ -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()
{
}
}

View File

@@ -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

View 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);

View File

@@ -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;
}
}

View 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);
}
}

View 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

View File

@@ -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;
}
}

View File

@@ -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)
{

View File

@@ -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)"

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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]

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View 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);
}
}

View File

@@ -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()}");
}

View File

@@ -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))

View File

@@ -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>

View File

@@ -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));
}
}
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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]

View File

@@ -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)
{

View File

@@ -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();

View File

@@ -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);

View 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));
}
}

View File

@@ -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)

View File

@@ -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.

View 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));
}
}

View 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),
}));
}
}

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.Client.Injectors")]

View 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;
}
}

View File

@@ -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)

View 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
View 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;
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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; }

View 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>

View File

@@ -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");

View File

@@ -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
{

View 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; }
}

View 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;
}
}

View 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;
}
}

View File

@@ -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