diff --git a/.gitmodules b/.gitmodules index d89e42270..c9728a6fc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,9 @@ [submodule "Lidgren.Network"] path = Lidgren.Network/Lidgren.Network url = https://github.com/space-wizards/lidgren-network-gen3.git +[submodule "XamlX"] + path = XamlX + url = https://github.com/space-wizards/XamlX [submodule "Robust.LoaderApi"] path = Robust.LoaderApi url = https://github.com/space-wizards/Robust.LoaderApi.git diff --git a/MSBuild/XamlIL.targets b/MSBuild/XamlIL.targets new file mode 100644 index 000000000..ac0b11c79 --- /dev/null +++ b/MSBuild/XamlIL.targets @@ -0,0 +1,24 @@ + + + + %(Filename) + + + + + + + + + + + + + + $(IntermediateOutputPath)XAML/references + $(IntermediateOutputPath)XAML/original.dll + + + + + diff --git a/Robust.Client.Injectors/CompileRobustXamlTask.cs b/Robust.Client.Injectors/CompileRobustXamlTask.cs new file mode 100644 index 000000000..ae2465f47 --- /dev/null +++ b/Robust.Client.Injectors/CompileRobustXamlTask.cs @@ -0,0 +1,86 @@ +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; + +namespace Robust.Build.Tasks +{ + /// + /// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs + /// + public class CompileRobustXamlTask : ITask + { + public bool Execute() + { + //Debugger.Launch(); + OutputPath = OutputPath ?? AssemblyFile; + var outputPdb = GetPdbPath(OutputPath); + var input = AssemblyFile; + var inputPdb = GetPdbPath(input); + // Make a copy and delete the original file to prevent MSBuild from thinking that everything is OK + if (OriginalCopyPath != null) + { + File.Copy(AssemblyFile, OriginalCopyPath, true); + input = OriginalCopyPath; + File.Delete(AssemblyFile); + + if (File.Exists(inputPdb)) + { + var copyPdb = GetPdbPath(OriginalCopyPath); + File.Copy(inputPdb, copyPdb, true); + File.Delete(inputPdb); + inputPdb = copyPdb; + } + } + + var msg = $"CompileRobustXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}"; + BuildEngine.LogMessage(msg, MessageImportance.High); + + var res = XamlCompiler.Compile(BuildEngine, input, + File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), + ProjectDirectory, OutputPath, + (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null); + if (!res.success) + return false; + if (!res.writtentofile) + { + File.Copy(input, OutputPath, true); + if(File.Exists(inputPdb)) + File.Copy(inputPdb, outputPdb, true); + } + return true; + } + + [Required] + public string ReferencesFilePath { get; set; } + + [Required] + public string ProjectDirectory { get; set; } + + [Required] + public string AssemblyFile { get; set; } + + [Required] + public string OriginalCopyPath { get; set; } + + public string OutputPath { get; set; } + + public string AssemblyOriginatorKeyFile { get; set; } + public bool SignAssembly { get; set; } + public bool DelaySign { get; set; } + + // shamelessly copied from avalonia + string GetPdbPath(string p) + { + var d = Path.GetDirectoryName(p); + var f = Path.GetFileNameWithoutExtension(p); + var rv = f + ".pdb"; + if (d != null) + rv = Path.Combine(d, rv); + return rv; + } + + public IBuildEngine BuildEngine { get; set; } + public ITaskHost HostObject { get; set; } + } +} diff --git a/Robust.Client.Injectors/Extensions.cs b/Robust.Client.Injectors/Extensions.cs new file mode 100644 index 000000000..31d80cca7 --- /dev/null +++ b/Robust.Client.Injectors/Extensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Build.Framework; + +namespace Robust.Build.Tasks +{ + /// + /// Taken from https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/Extensions.cs + /// + public static class Extensions + { + //shamefully copied from avalonia + public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp) + { + engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp)); + } + } +} diff --git a/Robust.Client.Injectors/Program.cs b/Robust.Client.Injectors/Program.cs new file mode 100644 index 000000000..c3bf6e5b7 --- /dev/null +++ b/Robust.Client.Injectors/Program.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.IO; +using Microsoft.Build.Framework; + +namespace Robust.Build.Tasks +{ + /// + /// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/Program.cs + /// + class Program + { + static int Main(string[] args) + { + if (args.Length != 3) + { + Console.Error.WriteLine("expected: input references output"); + return 1; + } + + return new CompileRobustXamlTask + { + AssemblyFile = args[0], + ReferencesFilePath = args[1], + OutputPath = args[2], + BuildEngine = new ConsoleBuildEngine(), + ProjectDirectory = Directory.GetCurrentDirectory() + }.Execute() ? 0 : 2; + } + } + + class ConsoleBuildEngine : IBuildEngine + { + public void LogErrorEvent(BuildErrorEventArgs e) + { + Console.WriteLine($"ERROR: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); + } + + public void LogWarningEvent(BuildWarningEventArgs e) + { + Console.WriteLine($"WARNING: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); + } + + public void LogMessageEvent(BuildMessageEventArgs e) + { + Console.WriteLine($"MESSAGE: {e.Code} {e.Message} in {e.File} {e.LineNumber}:{e.ColumnNumber}-{e.EndLineNumber}:{e.EndColumnNumber}"); + } + + public void LogCustomEvent(CustomBuildEventArgs e) + { + Console.WriteLine($"CUSTOM: {e.Message}"); + } + + 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; } + } +} diff --git a/Robust.Client.Injectors/Robust.Client.Injectors.csproj b/Robust.Client.Injectors/Robust.Client.Injectors.csproj new file mode 100644 index 000000000..151201139 --- /dev/null +++ b/Robust.Client.Injectors/Robust.Client.Injectors.csproj @@ -0,0 +1,16 @@ + + + + Exe + net5.0 + + + + + + + + + + + diff --git a/Robust.Client.Injectors/RobustXamlILCompiler.cs b/Robust.Client.Injectors/RobustXamlILCompiler.cs new file mode 100644 index 000000000..11862a73b --- /dev/null +++ b/Robust.Client.Injectors/RobustXamlILCompiler.cs @@ -0,0 +1,201 @@ +using System.Linq; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Robust.Build.Tasks +{ + /// + /// Emitters & Transformers based on: + /// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs + /// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs + /// + public class RobustXamlILCompiler : XamlILCompiler + { + public RobustXamlILCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings, bool fillWithDefaults) : base(configuration, emitMappings, fillWithDefaults) + { + Transformers.Add(new AddNameScopeRegistration()); + Transformers.Add(new RobustMarkRootObjectScopeNode()); + + Emitters.Add(new AddNameScopeRegistration.Emitter()); + Emitters.Add(new RobustMarkRootObjectScopeNode.Emitter()); + } + + class AddNameScopeRegistration : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlPropertyAssignmentNode pa) + { + if (pa.Property.Name == "Name" + && pa.Property.DeclaringType.FullName == "Robust.Client.UserInterface.Control") + { + if (context.ParentNodes().FirstOrDefault() is XamlManipulationGroupNode mg + && mg.Children.OfType().Any()) + return node; + + 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]; + if (!(value is XamlAstTextNode)) + { + var local = new XamlAstCompilerLocalNode(value); + // Wrap original in local initialization + pa.Values[c] = new XamlAstLocalInitializationNodeEmitter(value, value, local); + // Use local + value = local; + } + + break; + } + + if (value != null) + { + var objectType = context.ParentNodes().OfType().FirstOrDefault()?.Type.GetClrType(); + return new XamlManipulationGroupNode(pa) + { + Children = + { + pa, + new RobustNameScopeRegistrationXamlIlNode(value, objectType) + } + }; + } + } + /*else if (pa.Property.CustomAttributes.Select(attr => attr.Type).Intersect(context.Configuration.TypeMappings.DeferredContentPropertyAttributes).Any()) + { + pa.Values[pa.Values.Count - 1] = + new NestedScopeMetadataNode(pa.Values[pa.Values.Count - 1]); + }*/ + } + + return node; + } + + class RobustNameScopeRegistrationXamlIlNode : XamlAstNode, IXamlAstManipulationNode + { + public IXamlAstValueNode Name { get; set; } + public IXamlType TargetType { get; } + + public RobustNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType targetType) : base(name) + { + TargetType = targetType; + Name = name; + } + + public override void VisitChildren(IXamlAstVisitor visitor) + => Name = (IXamlAstValueNode)Name.Visit(visitor); + } + + internal class Emitter : IXamlAstLocalsNodeEmitter + { + public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContextWithLocals context, IXamlILEmitter codeGen) + { + if (node is RobustNameScopeRegistrationXamlIlNode registration) + { + + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == XamlCompiler.ContextNameScopeFieldName); + var namescopeRegisterFunction = context.Configuration.TypeSystem + .FindType("Robust.Client.UserInterface.XAML.NameScope").Methods + .First(m => m.Name == "Register"); + + using (var targetLoc = context.GetLocalOfType(context.Configuration.TypeSystem.FindType("Robust.Client.UserInterface.Control"))) + { + + codeGen + // var target = {pop} + .Stloc(targetLoc.Local) + // _context.NameScope.Register(Name, target) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField); + + context.Emit(registration.Name, codeGen, registration.Name.Type.GetClrType()); + + codeGen + .Ldloc(targetLoc.Local) + .EmitCall(namescopeRegisterFunction, true); + } + + return XamlILNodeEmitResult.Void(1); + } + return default; + } + } + } + + class RobustMarkRootObjectScopeNode : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!context.ParentNodes().Any() + && node is XamlValueWithManipulationNode mnode) + { + mnode.Manipulation = new XamlManipulationGroupNode(mnode, + new[] + { + mnode.Manipulation, + new HandleRootObjectScopeNode(mnode) + }); + } + return node; + } + class HandleRootObjectScopeNode : XamlAstNode, IXamlAstManipulationNode + { + public HandleRootObjectScopeNode(IXamlLineInfo lineInfo) : base(lineInfo) + { + } + } + internal class Emitter : IXamlILAstNodeEmitter + { + public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContext context, IXamlILEmitter codeGen) + { + if (!(node is HandleRootObjectScopeNode)) + { + return null; + } + + var controlType = context.Configuration.TypeSystem.FindType("Robust.Client.UserInterface.Control"); + + var next = codeGen.DefineLabel(); + var dontAbsorb = codeGen.DefineLabel(); + var end = codeGen.DefineLabel(); + var contextScopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == XamlCompiler.ContextNameScopeFieldName); + var controlNameScopeField = controlType.Fields.First(f => f.Name == "NameScope"); + var nameScopeType = context.Configuration.TypeSystem + .FindType("Robust.Client.UserInterface.XAML.NameScope"); + var nameScopeCompleteMethod = nameScopeType.Methods.First(m => m.Name == "Complete"); + var nameScopeAbsorbMethod = nameScopeType.Methods.First(m => m.Name == "Absorb"); + using (var local = codeGen.LocalsPool.GetLocal(controlType)) + { + codeGen + .Isinst(controlType) + .Dup() + .Stloc(local.Local) //store control in local field + .Brfalse(next) //if control is null, move to next (this should never happen but whatev, avalonia does it) + .Ldloc(context.ContextLocal) + .Ldfld(contextScopeField) + .Ldloc(local.Local) //load control from local field + .Ldfld(controlNameScopeField) //load namescope field from control + .EmitCall(nameScopeAbsorbMethod, true) + .Ldloc(local.Local) //load control + .Ldloc(context.ContextLocal) //load contextObject + .Ldfld(contextScopeField) //load namescope field from context obj + .Stfld(controlNameScopeField) //store namescope field in control + .MarkLabel(next) + .Ldloc(context.ContextLocal) + .Ldfld(contextScopeField) + .EmitCall(nameScopeCompleteMethod, true); //set the namescope as complete + } + + return XamlILNodeEmitResult.Void(1); + } + } + } + } +} diff --git a/Robust.Client.Injectors/XamlCompiler.Helpers.cs b/Robust.Client.Injectors/XamlCompiler.Helpers.cs new file mode 100644 index 000000000..73a5f72ce --- /dev/null +++ b/Robust.Client.Injectors/XamlCompiler.Helpers.cs @@ -0,0 +1,78 @@ +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 +{ + /// + /// Helpers taken from: + /// - 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 + /// + public partial class XamlCompiler + { + static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") + || r.Name.ToLowerInvariant().EndsWith(".paml") + || r.Name.ToLowerInvariant().EndsWith(".axaml"); + + private static bool MatchThisCall(Collection 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); + } + + interface IResource : IFileSource + { + string Uri { get; } + string Name { get; } + void Remove(); + + } + + interface IResourceGroup + { + string Name { get; } + IEnumerable Resources { get; } + } + + class EmbeddedResources : IResourceGroup + { + private readonly AssemblyDefinition _asm; + public string Name => "EmbeddedResource"; + + public IEnumerable Resources => _asm.MainModule.Resources.OfType() + .Select(r => new WrappedResource(_asm, r)).ToList(); + + public EmbeddedResources(AssemblyDefinition asm) + { + _asm = asm; + } + class WrappedResource : IResource + { + private readonly AssemblyDefinition _asm; + private readonly EmbeddedResource _res; + + public WrappedResource(AssemblyDefinition asm, EmbeddedResource res) + { + _asm = asm; + _res = res; + } + + public string Uri => $"resm:{Name}?assembly={_asm.Name.Name}"; + public string Name => _res.Name; + public string FilePath => Name; + public byte[] FileContents => _res.GetResourceData(); + + public void Remove() => _asm.MainModule.Resources.Remove(_res); + } + } + } +} diff --git a/Robust.Client.Injectors/XamlCompiler.cs b/Robust.Client.Injectors/XamlCompiler.cs new file mode 100644 index 000000000..f7d3bd047 --- /dev/null +++ b/Robust.Client.Injectors/XamlCompiler.cs @@ -0,0 +1,317 @@ +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 XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Parsers; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Robust.Build.Tasks +{ + /// + /// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs + /// Adjusted for our UI-Framework + /// + 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; + + 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("Robust.Client.UserInterface.XAML.XmlnsDefinitionAttribute"), + + }, + ContentAttributes = + { + typeSystem.GetType("Robust.Client.UserInterface.XAML.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 + { + ContextTypeBuilderCallback = (b,c) => EmitNameScopeField(xamlLanguage, typeSystem, b, c) + }; + + var transformerconfig = new TransformerConfiguration( + typeSystem, + typeSystem.TargetAssembly, + xamlLanguage, + XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage)); + + 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); + + var loaderDispatcherDef = new TypeDefinition("CompiledRobustXaml", "!XamlLoader", + TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + + var loaderDispatcherMethod = new MethodDefinition("TryLoad", + MethodAttributes.Static | MethodAttributes.Public, + asm.MainModule.TypeSystem.Object) + { + Parameters = {new ParameterDefinition(asm.MainModule.TypeSystem.String)} + }; + loaderDispatcherDef.Methods.Add(loaderDispatcherMethod); + asm.MainModule.Types.Add(loaderDispatcherDef); + + var stringEquals = asm.MainModule.ImportReference(asm.MainModule.TypeSystem.String.Resolve().Methods.First( + m => + m.IsStatic && m.Name == "Equals" && m.Parameters.Count == 2 && + m.ReturnType.FullName == "System.Boolean" + && m.Parameters[0].ParameterType.FullName == "System.String" + && m.Parameters[1].ParameterType.FullName == "System.String")); + + 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() + .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."); + } + } + + //add compiled build method + var compiledBuildMethod = typeSystem.GetTypeReference(builder).Resolve().Methods + .First(m => m.Name == buildName); + var parameterlessCtor = classTypeDefinition.GetConstructors() + .FirstOrDefault(c => c.IsPublic && !c.IsStatic && !c.HasParameters); + + if (compiledBuildMethod != null && parameterlessCtor != null) + { + var i = loaderDispatcherMethod.Body.Instructions; + var nop = Instruction.Create(OpCodes.Nop); + i.Add(Instruction.Create(OpCodes.Ldarg_0)); + i.Add(Instruction.Create(OpCodes.Ldstr, res.Uri)); + i.Add(Instruction.Create(OpCodes.Call, stringEquals)); + i.Add(Instruction.Create(OpCodes.Brfalse, nop)); + if (parameterlessCtor != null) + i.Add(Instruction.Create(OpCodes.Newobj, parameterlessCtor)); + else + { + i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod)); + } + + i.Add(Instruction.Create(OpCodes.Ret)); + i.Add(nop); + } + } + catch (Exception e) + { + engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", res.Uri, 0, 0, 0, 0, + e.ToString(), "", "CompileRobustXaml")); + } + } + return true; + } + + if (embrsc.Resources.Count(CheckXamlName) != 0) + { + if (!CompileGroup(embrsc)) + return false; + } + + loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull)); + loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + return true; + } + + public const string ContextNameScopeFieldName = "RobustNameScope"; + + private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder 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 Resources { get; } + } +} diff --git a/Robust.Client.NameGenerator/NameReferenceSyntaxReceiver.cs b/Robust.Client.NameGenerator/NameReferenceSyntaxReceiver.cs new file mode 100644 index 000000000..ac8d83176 --- /dev/null +++ b/Robust.Client.NameGenerator/NameReferenceSyntaxReceiver.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Robust.Client.NameGenerator +{ + /// + /// Taken from https://github.com/AvaloniaUI/Avalonia.NameGenerator/blob/ecc9677a23de5cbc90af07ccac14e31c0da41d6a/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs + /// + internal class NameReferenceSyntaxReceiver : ISyntaxReceiver + { + public List CandidateClasses { get; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && + classDeclarationSyntax.AttributeLists.Count > 0) + CandidateClasses.Add(classDeclarationSyntax); + } + } +} diff --git a/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj b/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj new file mode 100644 index 000000000..e93e53ee4 --- /dev/null +++ b/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/Robust.Client.NameGenerator/RoslynTypeSystem.cs b/Robust.Client.NameGenerator/RoslynTypeSystem.cs new file mode 100644 index 000000000..1d6af521b --- /dev/null +++ b/Robust.Client.NameGenerator/RoslynTypeSystem.cs @@ -0,0 +1,287 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using XamlX.TypeSystem; + +namespace Robust.Client.NameGenerator +{ + /// + /// Taken from https://github.com/AvaloniaUI/Avalonia.NameGenerator/blob/ecc9677a23de5cbc90af07ccac14e31c0da41d6a/src/Avalonia.NameGenerator/Infrastructure/RoslynTypeSystem.cs + /// + public class RoslynTypeSystem : IXamlTypeSystem + { + private readonly List _assemblies = new List(); + + public RoslynTypeSystem(CSharpCompilation compilation) + { + _assemblies.Add(new RoslynAssembly(compilation.Assembly)); + + var assemblySymbols = compilation + .References + .Select(compilation.GetAssemblyOrModuleSymbol) + .OfType() + .Select(assembly => new RoslynAssembly(assembly)) + .ToList(); + + _assemblies.AddRange(assemblySymbols); + } + + public IReadOnlyList Assemblies => _assemblies; + + public IXamlAssembly FindAssembly(string substring) => _assemblies[0]; + + public IXamlType FindType(string name) + { + foreach (var assembly in _assemblies) + { + var type = assembly.FindType(name); + if (type != null) + return type; + } + + return null; + } + + public IXamlType FindType(string name, string assembly) + { + foreach (var assemblyInstance in _assemblies) + { + var type = assemblyInstance.FindType(name); + if (type != null) + return type; + } + + return null; + } + } + + public class RoslynAssembly : IXamlAssembly + { + private readonly IAssemblySymbol _symbol; + + public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol; + + public bool Equals(IXamlAssembly other) => + other is RoslynAssembly roslynAssembly && + SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol); + + public string Name => _symbol.Name; + + public IReadOnlyList CustomAttributes => + _symbol.GetAttributes() + .Select(data => new RoslynAttribute(data, this)) + .ToList(); + + public IXamlType FindType(string fullName) + { + var type = _symbol.GetTypeByMetadataName(fullName); + return type is null ? null : new RoslynType(type, this); + } + } + + public class RoslynAttribute : IXamlCustomAttribute + { + private readonly AttributeData _data; + private readonly RoslynAssembly _assembly; + + public RoslynAttribute(AttributeData data, RoslynAssembly assembly) + { + _data = data; + _assembly = assembly; + } + + public bool Equals(IXamlCustomAttribute other) => + other is RoslynAttribute attribute && + _data == attribute._data; + + public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly); + + public List Parameters => + _data.ConstructorArguments + .Select(argument => argument.Value) + .ToList(); + + public Dictionary Properties => + _data.NamedArguments.ToDictionary( + pair => pair.Key, + pair => pair.Value.Value); + } + + public class RoslynType : IXamlType + { + private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeVariance); + + private readonly RoslynAssembly _assembly; + private readonly INamedTypeSymbol _symbol; + + public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlType other) => + other is RoslynType roslynType && + SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol); + + public object Id => _symbol; + + public string Name => _symbol.Name; + + public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat); + + public string FullName => $"{Namespace}.{Name}"; + + public IXamlAssembly Assembly => _assembly; + + public IReadOnlyList Properties => + _symbol.GetMembers() + .Where(member => member.Kind == SymbolKind.Property) + .OfType() + .Select(property => new RoslynProperty(property, _assembly)) + .ToList(); + + public IReadOnlyList Events { get; } = new List(); + + public IReadOnlyList Fields { get; } = new List(); + + public IReadOnlyList Methods { get; } = new List(); + + public IReadOnlyList Constructors => + _symbol.Constructors + .Select(method => new RoslynConstructor(method, _assembly)) + .ToList(); + + public IReadOnlyList CustomAttributes { get; } = new List(); + + public IReadOnlyList GenericArguments { get; } = new List(); + + public bool IsAssignableFrom(IXamlType type) => type == this; + + public IXamlType MakeGenericType(IReadOnlyList typeArguments) => this; + + public IXamlType GenericTypeDefinition => this; + + public bool IsArray => false; + + public IXamlType ArrayElementType { get; } = null; + + public IXamlType MakeArrayType(int dimensions) => null; + + public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly); + + public bool IsValueType { get; } = false; + + public bool IsEnum { get; } = false; + + public IReadOnlyList Interfaces => + _symbol.AllInterfaces + .Select(abstraction => new RoslynType(abstraction, _assembly)) + .ToList(); + + public bool IsInterface => _symbol.IsAbstract; + + public IXamlType GetEnumUnderlyingType() => null; + + public IReadOnlyList GenericParameters { get; } = new List(); + } + + public class RoslynConstructor : IXamlConstructor + { + private readonly IMethodSymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlConstructor other) => + other is RoslynConstructor roslynConstructor && + SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol); + + public bool IsPublic => true; + + public bool IsStatic => false; + + public IReadOnlyList Parameters => + _symbol.Parameters + .Select(parameter => parameter.Type) + .OfType() + .Select(type => new RoslynType(type, _assembly)) + .ToList(); + } + + public class RoslynProperty : IXamlProperty + { + private readonly IPropertySymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlProperty other) => + other is RoslynProperty roslynProperty && + SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol); + + public string Name => _symbol.Name; + + public IXamlType PropertyType => + _symbol.Type is INamedTypeSymbol namedTypeSymbol + ? new RoslynType(namedTypeSymbol, _assembly) + : null; + + public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly); + + public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly); + + public IReadOnlyList CustomAttributes { get; } = new List(); + + public IReadOnlyList IndexerParameters { get; } = new List(); + } + + public class RoslynMethod : IXamlMethod + { + private readonly IMethodSymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlMethod other) => + other is RoslynMethod roslynMethod && + SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol); + + public string Name => _symbol.Name; + + public bool IsPublic => true; + + public bool IsStatic => false; + + public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly); + + public IReadOnlyList Parameters => + _symbol.Parameters.Select(parameter => parameter.Type) + .OfType() + .Select(type => new RoslynType(type, _assembly)) + .ToList(); + + public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly); + + public IXamlMethod MakeGenericMethod(IReadOnlyList typeArguments) => null; + + public IReadOnlyList CustomAttributes { get; } = new List(); + } +} diff --git a/Robust.Client.NameGenerator/XamlUiPartialClassGenerator.cs b/Robust.Client.NameGenerator/XamlUiPartialClassGenerator.cs new file mode 100644 index 000000000..0da99ac9f --- /dev/null +++ b/Robust.Client.NameGenerator/XamlUiPartialClassGenerator.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Parsers; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Robust.Client.NameGenerator +{ + /// + /// Based on https://github.com/AvaloniaUI/Avalonia.NameGenerator/blob/ecc9677a23de5cbc90af07ccac14e31c0da41d6a/src/Avalonia.NameGenerator/NameReferenceGenerator.cs + /// Adjusted for our UI-Framework & needs. + /// + [Generator] + public class XamlUiPartialClassGenerator : ISourceGenerator + { + private const string AttributeName = "Robust.Client.AutoGenerated.GenerateTypedNameReferencesAttribute"; + private const string AttributeFile = "GenerateTypedNameReferencesAttribute"; + private const string AttributeCode = @"// +using System; +namespace Robust.Client.AutoGenerated +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class GenerateTypedNameReferencesAttribute : Attribute { } +} +"; + + class NameVisitor : IXamlAstVisitor + { + private List<(string name, string type)> _names = new List<(string name, string type)>(); + + public static List<(string name, string type)> GetNames(IXamlAstNode node) + { + var visitor = new NameVisitor(); + node.Visit(visitor); + return visitor._names; + } + + private bool IsControl(IXamlType type) => type.FullName != "System.Object" && + (type.FullName == "Robust.Client.UserInterface.Control" || + IsControl(type.BaseType)); + + public IXamlAstNode Visit(IXamlAstNode node) + { + if (node is XamlAstObjectNode objectNode) + { + var clrtype = objectNode.Type.GetClrType(); + var isControl = IsControl(clrtype); + //clrtype.Interfaces.Any(i => + //i.IsInterface && i.FullName == "Robust.Client.UserInterface.IControl"); + + if (!isControl) + return node; + + foreach (var child in objectNode.Children) + { + if (child is XamlAstXamlPropertyValueNode propertyValueNode && + propertyValueNode.Property is XamlAstNamePropertyReference namedProperty && + namedProperty.Name == "Name" && + propertyValueNode.Values.Count > 0 && + propertyValueNode.Values[0] is XamlAstTextNode text) + { + var reg = (text.Text, $@"{clrtype.Namespace}.{clrtype.Name}"); + if (!_names.Contains(reg)) + { + _names.Add(reg); + } + } + } + } + + return node; + } + + public void Push(IXamlAstNode node) { } + + public void Pop() { } + } + + private static string GenerateSourceCode( + INamedTypeSymbol classSymbol, + string xamlFile, + CSharpCompilation comp) + { + var className = classSymbol.Name; + var nameSpace = classSymbol.ContainingNamespace.ToDisplayString(); + var parsed = XDocumentXamlParser.Parse(xamlFile); + var typeSystem = new RoslynTypeSystem(comp); + var compiler = + new XamlILCompiler( + new TransformerConfiguration(typeSystem, typeSystem.Assemblies[0], + new XamlLanguageTypeMappings(typeSystem)), + new XamlLanguageEmitMappings(), false); + compiler.Transformers.Add(new TypeReferenceResolver()); + compiler.Transform(parsed); + var initialRoot = (XamlAstObjectNode) parsed.Root; + var names = NameVisitor.GetNames(initialRoot); + //var names = NameVisitor.GetNames((XamlAstObjectNode)XDocumentXamlParser.Parse(xamlFile).Root); + var namedControls = names.Select(info => " " + + $"protected global::{info.type} {info.name} => " + + $"this.FindControl(\"{info.name}\");"); + return $@"// +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +namespace {nameSpace} +{{ + partial class {className} + {{ +{string.Join("\n", namedControls)} + }} +}} +"; + } + + + public void Execute(GeneratorExecutionContext context) + { + var comp = (CSharpCompilation) context.Compilation; + if(comp.GetTypeByMetadataName(AttributeName) == null) + context.AddSource(AttributeFile, SourceText.From(AttributeCode, Encoding.UTF8)); + if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver)) + { + return; + } + + var symbols = UnpackAnnotatedTypes(context, comp, receiver); + if(symbols == null) + return; + + foreach (var typeSymbol in symbols) + { + var xamlFileName = $"{typeSymbol.Name}.xaml"; + var relevantXamlFile = context.AdditionalFiles.FirstOrDefault(t => t.Path.EndsWith(xamlFileName)); + + if (relevantXamlFile == null) + { + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + "RXN0001", + $"Unable to discover the relevant Robust XAML file for {typeSymbol}.", + "Unable to discover the relevant Robust XAML file " + + $"expected at {xamlFileName}", + "Usage", + DiagnosticSeverity.Error, + true), + typeSymbol.Locations[0])); + return; + } + + var txt = relevantXamlFile.GetText()?.ToString(); + if (txt == null) + { + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + "RXN0002", + $"Unexpected empty Xaml-File was found at {xamlFileName}", + "Expected Content due to a Class with the same name being annotated with [GenerateTypedNameReferences].", + "Usage", + DiagnosticSeverity.Error, + true), + Location.Create(xamlFileName, new TextSpan(0,0), new LinePositionSpan(new LinePosition(0,0),new LinePosition(0,0))))); + return; + } + + try + { + var sourceCode = GenerateSourceCode(typeSymbol, txt, comp); + context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); + } + catch (Exception e) + { + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + "AXN0003", + "Unhandled exception occured while generating typed Name references.", + $"Unhandled exception occured while generating typed Name references: {e}", + "Usage", + DiagnosticSeverity.Error, + true), + typeSymbol.Locations[0])); + return; + } + } + } + + private IReadOnlyList UnpackAnnotatedTypes(in GeneratorExecutionContext context, CSharpCompilation comp, NameReferenceSyntaxReceiver receiver) + { + var options = (CSharpParseOptions) comp.SyntaxTrees[0].Options; + var compilation = + comp.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(AttributeCode, Encoding.UTF8), options)); + var symbols = new List(); + var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName); + foreach (var candidateClass in receiver.CandidateClasses) + { + var model = compilation.GetSemanticModel(candidateClass.SyntaxTree); + var typeSymbol = (INamedTypeSymbol) model.GetDeclaredSymbol(candidateClass); + var relevantAttribute = typeSymbol.GetAttributes().FirstOrDefault(attr => + attr.AttributeClass != null && + attr.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); + + if (relevantAttribute == null) + { + continue; + } + + var isPartial = candidateClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); + + if (isPartial) + { + symbols.Add(typeSymbol); + } + else + { + var missingPartialKeywordMessage = + $"The type {typeSymbol.Name} should be declared with the 'partial' keyword " + + "as it is annotated with the [GenerateTypedNameReferences] attribute."; + + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + "RXN0004", + missingPartialKeywordMessage, + missingPartialKeywordMessage, + "Usage", + DiagnosticSeverity.Error, + true), + Location.None)); + return null; + } + } + + return symbols; + } + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver()); + } + } +} diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index 4edcfcb6e..bd9ab2d59 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -115,6 +115,7 @@ namespace Robust.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + //IoCManager.Register(); } } } diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj index 6cc8b6db9..afe06a1f5 100644 --- a/Robust.Client/Robust.Client.csproj +++ b/Robust.Client/Robust.Client.csproj @@ -45,4 +45,10 @@ + + + ../Tools + + + diff --git a/Robust.Client/UserInterface/Control.cs b/Robust.Client/UserInterface/Control.cs index e90b2fae9..1efdf4695 100644 --- a/Robust.Client/UserInterface/Control.cs +++ b/Robust.Client/UserInterface/Control.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using Robust.Client.Graphics.Drawing; using Robust.Client.Interfaces.Graphics; using Robust.Client.Interfaces.UserInterface; +using Robust.Client.UserInterface.XAML; using Robust.Shared.Animations; using Robust.Shared.IoC; using Robust.Shared.Maths; @@ -49,6 +50,45 @@ namespace Robust.Client.UserInterface [ViewVariables] public Control? Parent { get; private set; } + public NameScope? NameScope; + + //public void AttachNameScope(Dictionary nameScope) + //{ + // _nameScope = nameScope; + //} + + public NameScope? FindNameScope() + { + foreach (var control in this.GetSelfAndLogicalAncestors()) + { + if (control.NameScope != null) return control.NameScope; + } + + return null; + } + + public T FindControl(string name) where T : Control + { + var nameScope = FindNameScope(); + if (nameScope == null) + { + throw new ArgumentException("No Namespace found for Control"); + } + + var value = nameScope.Find(name); + if (value == null) + { + throw new ArgumentException($"No Control with the name {name} found"); + } + + if (value is not T ret) + { + throw new ArgumentException($"Control with name {name} had invalid type {value.GetType()}"); + } + + return ret; + } + internal IUserInterfaceManagerInternal UserInterfaceManagerInternal { get; } /// @@ -62,6 +102,9 @@ namespace Robust.Client.UserInterface [ViewVariables] public OrderedChildCollection Children { get; } + [Content] + public virtual ICollection XamlChildren { get; protected set; } + [ViewVariables] public int ChildCount => _orderedChildren.Count; /// @@ -394,6 +437,7 @@ namespace Robust.Client.UserInterface UserInterfaceManagerInternal = IoCManager.Resolve(); StyleClasses = new StyleClassCollection(this); Children = new OrderedChildCollection(this); + XamlChildren = Children; } /// diff --git a/Robust.Client/UserInterface/CustomControls/SS14Window.xaml b/Robust.Client/UserInterface/CustomControls/SS14Window.xaml new file mode 100644 index 000000000..d777166f5 --- /dev/null +++ b/Robust.Client/UserInterface/CustomControls/SS14Window.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/Robust.Client/UserInterface/CustomControls/SS14Window.cs b/Robust.Client/UserInterface/CustomControls/SS14Window.xaml.cs similarity index 55% rename from Robust.Client/UserInterface/CustomControls/SS14Window.cs rename to Robust.Client/UserInterface/CustomControls/SS14Window.xaml.cs index f2383228d..cd9bb8295 100644 --- a/Robust.Client/UserInterface/CustomControls/SS14Window.cs +++ b/Robust.Client/UserInterface/CustomControls/SS14Window.xaml.cs @@ -1,12 +1,17 @@ -using System; +using System.Collections; +using System.Collections.Generic; +using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; using Robust.Shared.Maths; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Robust.Client.UserInterface.CustomControls { + [GenerateTypedNameReferences] // ReSharper disable once InconsistentNaming - public class SS14Window : BaseWindow + public partial class SS14Window : BaseWindow { public const string StyleClassWindowTitle = "windowTitle"; public const string StyleClassWindowPanel = "windowPanel"; @@ -17,70 +22,19 @@ namespace Robust.Client.UserInterface.CustomControls public SS14Window() { + RobustXamlLoader.Load(this); MouseFilter = MouseFilterMode.Stop; - AddChild(new PanelContainer - { - StyleClasses = {StyleClassWindowPanel} - }); + WindowHeader.CustomMinimumSize = (0, HEADER_SIZE_Y); - AddChild(new VBoxContainer - { - SeparationOverride = 0, - Children = - { - new PanelContainer - { - StyleClasses = {StyleClassWindowHeader}, - CustomMinimumSize = (0, HEADER_SIZE_Y), - Children = - { - new HBoxContainer - { - Children = - { - new MarginContainer - { - MarginLeftOverride = 5, - SizeFlagsHorizontal = SizeFlags.FillExpand, - Children = - { - (TitleLabel = new Label - { - StyleIdentifier = "foo", - ClipText = true, - Text = "Exemplary Window Title Here", - VAlign = Label.VAlignMode.Center, - StyleClasses = {StyleClassWindowTitle} - }) - } - }, - (CloseButton = new TextureButton - { - StyleClasses = {StyleClassWindowCloseButton}, - SizeFlagsVertical = SizeFlags.ShrinkCenter - }) - } - } - } - }, - (Contents = new MarginContainer - { - MarginBottomOverride = 10, - MarginLeftOverride = 10, - MarginRightOverride = 10, - MarginTopOverride = 10, - RectClipContent = true, - SizeFlagsVertical = SizeFlags.FillExpand - }) - } - }); + Contents = ContentsContainer; CloseButton.OnPressed += CloseButtonPressed; + XamlChildren = new SS14ContentCollection(this); } public MarginContainer Contents { get; private set; } - private TextureButton CloseButton; + //private TextureButton CloseButton; private const int DRAG_MARGIN_SIZE = 7; @@ -103,7 +57,7 @@ namespace Robust.Client.UserInterface.CustomControls } } - private Label TitleLabel; + //private Label TitleLabel; public string? Title { @@ -177,5 +131,91 @@ namespace Robust.Client.UserInterface.CustomControls return mode; } + + public class SS14ContentCollection : ICollection, IReadOnlyCollection + { + private readonly SS14Window Owner; + + public SS14ContentCollection(SS14Window owner) + { + Owner = owner; + } + + public Enumerator GetEnumerator() + { + return new(Owner); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(Control item) + { + Owner.Contents.AddChild(item); + } + + public void Clear() + { + Owner.Contents.RemoveAllChildren(); + } + + public bool Contains(Control item) + { + return item?.Parent == Owner.Contents; + } + + public void CopyTo(Control[] array, int arrayIndex) + { + Owner.Contents.Children.CopyTo(array, arrayIndex); + } + + public bool Remove(Control item) + { + if (item?.Parent != Owner.Contents) + { + return false; + } + + DebugTools.AssertNotNull(Owner?.Contents); + Owner!.Contents.RemoveChild(item); + + return true; + } + + int ICollection.Count => Owner.Contents.ChildCount; + int IReadOnlyCollection.Count => Owner.Contents.ChildCount; + + public bool IsReadOnly => false; + + + public struct Enumerator : IEnumerator + { + private OrderedChildCollection.Enumerator _enumerator; + + internal Enumerator(SS14Window ss14Window) + { + _enumerator = ss14Window.Contents.Children.GetEnumerator(); + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + ((IEnumerator) _enumerator).Reset(); + } + + public Control Current => _enumerator.Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _enumerator.Dispose(); + } + } + } } } diff --git a/Robust.Client/UserInterface/LogicalExtensions.cs b/Robust.Client/UserInterface/LogicalExtensions.cs new file mode 100644 index 000000000..7f6109188 --- /dev/null +++ b/Robust.Client/UserInterface/LogicalExtensions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Robust.Client.UserInterface +{ + public static class LogicalExtensions + { + public static IEnumerable GetSelfAndLogicalAncestors(this Control control) + { + Control? c = control; + while (c != null) + { + yield return c; + c = c.Parent; + } + } + } +} diff --git a/Robust.Client/UserInterface/XAML/Attributes.cs b/Robust.Client/UserInterface/XAML/Attributes.cs new file mode 100644 index 000000000..c7adfa331 --- /dev/null +++ b/Robust.Client/UserInterface/XAML/Attributes.cs @@ -0,0 +1,42 @@ +using System; + +namespace Robust.Client.UserInterface.XAML +{ + public class ContentAttribute : Attribute + { + } + + public class XmlnsDefinitionAttribute : Attribute + { + public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace) + { + } + } + + public class UsableDuringInitializationAttribute : Attribute + { + public UsableDuringInitializationAttribute(bool usable) + { + } + } + + public class DeferredContentAttribute : Attribute + { + } + + public interface ITestRootObjectProvider + { + object RootObject { get; } + } + + public interface ITestProvideValueTarget + { + object TargetObject { get; } + object TargetProperty { get; } + } + + public interface ITestUriContext + { + Uri BaseUri { get; set; } + } +} diff --git a/Robust.Client/UserInterface/XAML/NameScope.cs b/Robust.Client/UserInterface/XAML/NameScope.cs new file mode 100644 index 000000000..389ed36a6 --- /dev/null +++ b/Robust.Client/UserInterface/XAML/NameScope.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + +namespace Robust.Client.UserInterface.XAML +{ +/// + /// Implements a name scope. + /// + public class NameScope + { + public bool IsCompleted { get; private set; } + + private readonly Dictionary _inner = new Dictionary(); + + public void Register(string name, Control element) + { + if (IsCompleted) + throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + if (_inner.TryGetValue(name, out Control? existing)) + { + if (existing != element) + { + throw new ArgumentException($"Control with the name '{name}' already registered."); + } + } + else + { + _inner.Add(name, element); + } + } + + public void Absorb(NameScope? nameScope) + { + if (nameScope == null) return; + + foreach (var (name, control) in nameScope._inner) + { + try + { + Register(name, control); + } + catch (Exception e) + { + throw new ArgumentException($"Exception occured when trying to absorb NameScope (at name {name})", e); + } + } + } + + public Control? Find(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + _inner.TryGetValue(name, out var result); + return result; + } + + public void Complete() + { + IsCompleted = true; + } + } +} diff --git a/Robust.Client/UserInterface/XAML/RobustXamlLoader.cs b/Robust.Client/UserInterface/XAML/RobustXamlLoader.cs new file mode 100644 index 000000000..d7a5fad25 --- /dev/null +++ b/Robust.Client/UserInterface/XAML/RobustXamlLoader.cs @@ -0,0 +1,13 @@ +using System; + +namespace Robust.Client.UserInterface.XAML +{ + public class RobustXamlLoader + { + public static void Load(object obj) + { + throw new Exception( + $"No precompiled XAML found for {obj.GetType()}, make sure to specify Class or name your class the same as your .xaml "); + } + } +} diff --git a/Robust.Shared/ContentPack/AssemblyTypeChecker.cs b/Robust.Shared/ContentPack/AssemblyTypeChecker.cs index 81acf8202..0f00ba280 100644 --- a/Robust.Shared/ContentPack/AssemblyTypeChecker.cs +++ b/Robust.Shared/ContentPack/AssemblyTypeChecker.cs @@ -260,6 +260,11 @@ namespace Robust.Shared.ContentPack // See II.14.2 in ECMA-335. return; } + case MTypeDefined: + { + // Valid for this to show up, safe to ignore. + return; + } default: { throw new ArgumentOutOfRangeException(); @@ -471,6 +476,20 @@ namespace Robust.Shared.ContentPack break; } + case HandleKind.TypeDefinition: + { + try + { + parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle) memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new SandboxError(u)); + return null; + } + + break; + } case HandleKind.TypeSpecification: { var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle) memRef.Parent); @@ -501,7 +520,7 @@ namespace Robust.Shared.ContentPack default: { errors.Add(new SandboxError( - $"Unsupported member ref parent type: {memRef.Parent}. Name: {memName}")); + $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); return null; } } diff --git a/Robust.Shared/ContentPack/Sandbox.yml b/Robust.Shared/ContentPack/Sandbox.yml index c6ef3961a..af416109b 100644 --- a/Robust.Shared/ContentPack/Sandbox.yml +++ b/Robust.Shared/ContentPack/Sandbox.yml @@ -297,6 +297,12 @@ Types: IEnumerable: { All: True } IEnumerator: { All: True } IReadOnlyList`1: { All: True } + System.ComponentModel: + PropertyDescriptor: { } + ISite: { All: True } + IComponent: { All: True } + IContainer: { All: True } + ITypeDescriptorContext: { All: True } System.Diagnostics.CodeAnalysis: AllowNullAttribute: { All: True } DisallowNullAttribute: { All: True } @@ -651,6 +657,7 @@ Types: CancellationTokenSource: { All: True } Interlocked: { All: True } System: + IServiceProvider: { All: True } Action: { All: True } Action`1: { All: True } Action`2: { All: True } @@ -1132,6 +1139,7 @@ Types: # Content should never touch that. Methods: - "bool Equals(object)" + - "bool Equals(System.Type)" - "bool get_ContainsGenericParameters()" - "bool get_HasElementType()" - "bool get_IsAbstract()" @@ -1251,6 +1259,7 @@ Types: UInt32: { All: True } UInt64: { All: True } UIntPtr: { All: True } + Uri: { All: True } ValueTuple: { All: True } ValueTuple`1: { All: True } ValueTuple`2: { All: True } diff --git a/RobustToolbox.sln b/RobustToolbox.sln index 5ea0f7483..532f42542 100644 --- a/RobustToolbox.sln +++ b/RobustToolbox.sln @@ -29,6 +29,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Robust.LoaderApi", "Robust. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.LoaderApi", "Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj", "{4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Client.Injectors", "Robust.Client.Injectors\Robust.Client.Injectors.csproj", "{EEF2C805-5E03-41EA-A916-49C1DD15EF41}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Client.NameGenerator", "Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj", "{EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XamlX", "XamlX", "{1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlX", "XamlX\src\XamlX\XamlX.csproj", "{D73768A2-BFCD-4916-8F52-4034C28F345C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlX.IL.Cecil", "XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj", "{1CDC9C4F-668E-47A3-8A44-216E95644BEB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlX.Runtime", "XamlX\src\XamlX.Runtime\XamlX.Runtime.csproj", "{B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -125,6 +137,46 @@ Global {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|Any CPU.Build.0 = Release|Any CPU {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|x64.ActiveCfg = Release|Any CPU {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|x64.Build.0 = Release|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|x64.ActiveCfg = Debug|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|x64.Build.0 = Debug|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|Any CPU.Build.0 = Release|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|x64.ActiveCfg = Release|Any CPU + {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|x64.Build.0 = Release|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|x64.ActiveCfg = Debug|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|x64.Build.0 = Debug|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|Any CPU.Build.0 = Release|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|x64.ActiveCfg = Release|Any CPU + {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|x64.Build.0 = Release|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|x64.Build.0 = Debug|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|Any CPU.Build.0 = Release|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|x64.ActiveCfg = Release|Any CPU + {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|x64.Build.0 = Release|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|x64.Build.0 = Debug|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|Any CPU.Build.0 = Release|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|x64.ActiveCfg = Release|Any CPU + {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|x64.Build.0 = Release|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|x64.ActiveCfg = Debug|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|x64.Build.0 = Debug|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|Any CPU.Build.0 = Release|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|x64.ActiveCfg = Release|Any CPU + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -132,6 +184,9 @@ Global GlobalSection(NestedProjects) = preSolution {ECBCE1D8-05C2-4881-9446-197C4C8E1C14} = {9143C8DD-A989-4089-9149-C50D12189FE4} {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15} = {805C8FD2-0C32-4DA8-BC4B-143BA5D48FF4} + {D73768A2-BFCD-4916-8F52-4034C28F345C} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0} + {1CDC9C4F-668E-47A3-8A44-216E95644BEB} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0} + {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {57757344-0FF4-4842-8A68-141CAA18A35D} diff --git a/XamlX b/XamlX new file mode 160000 index 000000000..dca5a5f8c --- /dev/null +++ b/XamlX @@ -0,0 +1 @@ +Subproject commit dca5a5f8c2759b940a87449584724ec71aa0dd19