Compare commits

...

38 Commits

Author SHA1 Message Date
Pieter-Jan Briers
f234ecb2c3 Make Robust.Client.Injectors NS2.0
So that it works out of the box with Framework MSBuild.
2020-12-21 03:16:13 +01:00
Pieter-Jan Briers
b449959865 Clean up bad project reference in Robust.Server 2020-12-21 03:15:45 +01:00
Pieter-Jan Briers
8f870403d2 Managed implementation of HttpListener. (#1460) 2020-12-21 02:51:04 +01:00
Paul Ritter
d94f702601 Xaml UI (#1446)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2020-12-20 23:52:36 +01:00
DrSmugleaf
e78ab8f922 Add YamlObjectSerializer.NodeToType with generic argument (#1456) 2020-12-20 20:46:56 +01:00
20kdc
6972000293 Make the "softness" of soft shadows adjustible per-light. (#1454)
Note: Thanks to the nature of YAML properties in RobustToolbox, this commit is only an API blocker if the Softness property is directly manipulated from code, which is unlikely.
2020-12-19 21:44:47 +01:00
DrSmugleaf
58560f589f Defer MoveEvent out of TransformComponent.HandleComponentState (#1453)
* Defer MoveEvent out of TransformComponent.HandleComponentState

* Imports

* Make the update loop more readable and call ToArray

* Fix tests

* Fix tests HALLELUJAH
2020-12-19 13:09:16 +01:00
Pieter-Jan Briers
6e931ac175 Fix some CVars not saving. 2020-12-19 02:31:46 +01:00
Pieter-Jan Briers
a7eb5e8115 Use nvidia GPU on optimus laptops.
With an undocumented crappy hack, of course.
2020-12-19 02:25:10 +01:00
metalgearsloth
712e4acc66 Cache TryLooseGetType (#1448)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2020-12-19 01:42:51 +01:00
Pieter-Jan Briers
fdcfdffc0b Provide fallback for /status API if content does not override it. 2020-12-19 00:43:46 +01:00
Pieter-Jan Briers
74eb8e3e8d Allow build.json contents to be overriden by --cvar. 2020-12-17 17:13:07 +01:00
Pieter-Jan Briers
ae4c764e4f Whitelist System.Guid for sandbox. 2020-12-17 16:37:20 +01:00
Pieter-Jan Briers
7ef2cec121 Fix names parsed from build.json 2020-12-17 15:28:01 +01:00
Pieter-Jan Briers
40bff81017 Fix nullable warning. 2020-12-17 00:58:26 +01:00
Pieter-Jan Briers
f7c28992f8 Disable string map caching to hopefully fix connect. 2020-12-17 00:48:15 +01:00
Pieter-Jan Briers
920ae58019 Fix LoaderApiLoader.FindFiles() 2020-12-17 00:33:39 +01:00
Pieter-Jan Briers
5bb21e07de Engine versioning. 2020-12-16 23:53:51 +01:00
Pieter-Jan Briers
78ceaa50d5 Update Lidgren submodule. 2020-12-16 18:18:40 +01:00
Pieter-Jan Briers
7473b6dae1 Optimize assembly type checking.
It's now parallelized which cuts off ~200ms on its own for me.
Config is now shared between multiple loads which saves a lot as well.

All in all, pretty good.
2020-12-14 16:34:33 +01:00
Pieter-Jan Briers
c335170fc1 Add non-generic System.Nullable to sandbox whitelist. 2020-12-13 21:33:22 +01:00
Pieter-Jan Briers
13e9fe12ce Further fixes to loader exe.
Fix ordering of loads.
Fix loads.
2020-12-13 16:12:32 +01:00
Pieter-Jan Briers
7ef2fd46da Hail NuGet 2020-12-13 01:14:50 +01:00
Pieter-Jan Briers
f048209bf5 FUCK BOMs 2020-12-13 01:10:21 +01:00
chairbender
1bf9e2e87a Multiselect option button, tooltip delay (Action Hotbar Support) (#1435) 2020-12-13 01:01:00 +01:00
Pieter-Jan Briers
fd4f45e670 Use NuGet packages for engine natives.
Fixes #1434

This means that adding support for new architectures (e.g. ARM) is MUCH easier.

It removes  download_natives.py which simplifies the build process.

It's also way less painful to maintain.
2020-12-13 00:46:23 +01:00
Pieter-Jan Briers
f15c1c7a95 Allow engine to be loaded from a zip file itself. 2020-12-12 11:12:37 +01:00
DrSmugleaf
50f0a4389e Fix the server not setting IsConnected to false for disconnecting clients in integration tests (#1442) 2020-12-12 00:53:10 +01:00
komunre
cab6277b2d FixClipping() now check if entity is deleted (bug fix) (#1441)
* check for deletion in CanMove()

* Added deleted check in FixCollide

* Removed Owner.Deleted check from CanMove()
2020-12-12 04:37:32 +11:00
Pieter-Jan Briers
797fa9cffa Fix server failing to start due to non-int LogLevel enum. 2020-12-10 15:22:29 +01:00
20kdc
a20245d623 Fix grid bounds going out of sync with chunk collision regeneration (#1440)
Fixes #1439
2020-12-10 14:38:03 +01:00
Pieter-Jan Briers
04cc1f616d Permissive markup parsing. 2020-12-09 13:08:06 +01:00
Pieter-Jan Briers
8cd6f63f17 Make FormattedMessage tags records, clean up tests. 2020-12-09 13:08:06 +01:00
Ygg01
ad8b0b3c83 Add bytes or sbytes to enum where available (#1430)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2020-12-08 12:46:30 +01:00
Paul Ritter
f157cdce02 Rotatable bounding boxes (#1360)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2020-12-07 17:01:57 +01:00
DrSmugleaf
2504a42f88 Fix typo in exception message 2020-12-07 15:21:38 +01:00
Pieter-Jan Briers
d0191e063a Fix all cases of member references to array (not vector) types.
Yeah generics aren't the only one since you can do [,][,].
2020-12-07 00:12:05 +01:00
Pieter-Jan Briers
b96bcbd357 Fix member ref handling of non-vector generic arrays in type checker. 2020-12-05 23:13:04 +01:00
161 changed files with 3365 additions and 832 deletions

View File

@@ -1,11 +1,11 @@
root = true
root = true
[*]
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
charset = utf-8-bom
charset = utf-8
[*.{csproj,xml,yml,dll.config,targets,props}]
indent_size = 2

9
.gitmodules vendored
View File

@@ -4,3 +4,12 @@
[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
[submodule "ManagedHttpListener"]
path = ManagedHttpListener
url = https://github.com/space-wizards/ManagedHttpListener.git

View File

@@ -1,12 +1,2 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
<PropertyGroup>
<RobustToolsPath>$(MSBuildThisFileDirectory)/../Tools/</RobustToolsPath>
</PropertyGroup>
<Target Name="CopyClientNatives">
<CombinePath BasePath="$(RobustToolsPath)" Paths="download_natives.py">
<Output TaskParameter="CombinedPaths" PropertyName="ScriptPath" />
</CombinePath>
<Exec Command="$(Python) &quot;$(ScriptPath)&quot; $(Platform) $(TargetOS) Client $(OutputPath)" CustomErrorRegularExpression="^Error" />
</Target>
<Target Name="ClientAfterBuild" DependsOnTargets="CopyClientNatives" />
</Project>

24
MSBuild/XamlIL.targets Normal file
View File

@@ -0,0 +1,24 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<EmbeddedResource Include="**\*.xaml" />
<AdditionalFiles Include="**\*.xaml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<UsingTask TaskName="CompileRobustXamlTask" AssemblyFile="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(Configuration)\net5.0\Robust.Client.Injectors.dll" />
<Target Name="CompileRobustXaml" AfterTargets="AfterCompile">
<PropertyGroup>
<RobustXamlReferencesTemporaryFilePath Condition="'$(RobustXamlReferencesTemporaryFilePath)' == ''">$(IntermediateOutputPath)XAML/references</RobustXamlReferencesTemporaryFilePath>
<RobustXamlOriginalCopyFilePath Condition="'$(RobustXamlOriginalCopyFilePath)' == ''">$(IntermediateOutputPath)XAML/original.dll</RobustXamlOriginalCopyFilePath>
</PropertyGroup>
<WriteLinesToFile File="$(RobustXamlReferencesTemporaryFilePath)" Lines="@(ReferencePathWithRefAssemblies)" Overwrite="true" />
<CompileRobustXamlTask AssemblyFile="@(IntermediateAssembly)" ReferencesFilePath="$(RobustXamlReferencesTemporaryFilePath)" OriginalCopyPath="$(RobustXamlOriginalCopyFilePath)" ProjectDirectory="$(MSBuildProjectDirectory)" AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)" SignAssembly="$(SignAssembly)" DelaySign="$(DelaySign)" />
</Target>
</Project>

1
ManagedHttpListener Submodule

Submodule ManagedHttpListener added at f2aa590fec

View File

@@ -13,7 +13,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// Defines event information for <see cref="GLFWCallbacks.KeyCallback"/>
/// or <see cref="GLFWCallbacks.MouseButtonCallback"/>.
/// </summary>
public enum InputAction
public enum InputAction : byte
{
/// <summary>
/// The key or mouse button was released.

View File

@@ -15,7 +15,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// Key modifiers, such as Shift or CTRL.
/// </summary>
[Flags]
public enum KeyModifiers
public enum KeyModifiers : byte
{
/// <summary>
/// if one or more Shift keys were held down.

View File

@@ -12,7 +12,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// <summary>
/// Specifies key codes and modifiers in US keyboard layout.
/// </summary>
public enum Keys
public enum Keys : short
{
/// <summary>
/// An unknown key.

View File

@@ -3,7 +3,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// <summary>
/// Specifies the buttons of a mouse.
/// </summary>
public enum MouseButton
public enum MouseButton : byte
{
/// <summary>
/// The first button.

View File

@@ -13,7 +13,7 @@ highp float createOcclusion(highp vec2 diff)
// Change soft shadow size based on distance from primary occluder.
highp float distRatio = (ourDist - occlDist.x) / occlDist.x / 2.0;
perpendicular *= distRatio;
perpendicular *= distRatio * lightSoftness;
// Totally not hacky PCF on top of VSM.
highp float occlusion = smoothstep(0.1, 1.0, ChebyshevUpperBound(occlDist, ourDist));

View File

@@ -13,6 +13,7 @@ uniform highp vec4 lightColor;
uniform highp vec2 lightCenter;
uniform highp float lightRange;
uniform highp float lightPower;
uniform highp float lightSoftness;
uniform highp float lightIndex;
uniform sampler2D shadowMap;

View File

@@ -0,0 +1,86 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
namespace Robust.Build.Tasks
{
/// <summary>
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
/// </summary>
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; }
}
}

View File

@@ -0,0 +1,16 @@
using Microsoft.Build.Framework;
namespace Robust.Build.Tasks
{
/// <summary>
/// Taken from https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/Extensions.cs
/// </summary>
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));
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections;
using System.IO;
using Microsoft.Build.Framework;
namespace Robust.Build.Tasks
{
/// <summary>
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/Program.cs
/// </summary>
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; }
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="16.8.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj" />
</ItemGroup>
</Project>

View File

@@ -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
{
/// <summary>
/// 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
/// </summary>
public class RobustXamlILCompiler : XamlILCompiler
{
public RobustXamlILCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> 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<RobustNameScopeRegistrationXamlIlNode>().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<XamlAstConstructableObjectNode>().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<IXamlILEmitter, XamlILNodeEmitResult>
{
public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> 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<IXamlILEmitter, XamlILNodeEmitResult> 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);
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// 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
/// </summary>
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<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);
}
interface IResource : IFileSource
{
string Uri { get; }
string Name { get; }
void Remove();
}
interface IResourceGroup
{
string Name { get; }
IEnumerable<IResource> Resources { get; }
}
class EmbeddedResources : IResourceGroup
{
private readonly AssemblyDefinition _asm;
public string Name => "EmbeddedResource";
public IEnumerable<IResource> Resources => _asm.MainModule.Resources.OfType<EmbeddedResource>()
.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);
}
}
}
}

View File

@@ -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
{
/// <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;
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<IXamlILEmitter, XamlILNodeEmitResult>
{
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<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.");
}
}
//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<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

@@ -0,0 +1,21 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Robust.Client.NameGenerator
{
/// <summary>
/// Taken from https://github.com/AvaloniaUI/Avalonia.NameGenerator/blob/ecc9677a23de5cbc90af07ccac14e31c0da41d6a/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs
/// </summary>
internal class NameReferenceSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
classDeclarationSyntax.AttributeLists.Count > 0)
CandidateClasses.Add(classDeclarationSyntax);
}
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-3.final" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Link="XamlX\filename" Include="../XamlX/src/XamlX/**/*.cs" />
<Compile Remove="../XamlX/src/XamlX/**/SreTypeSystem.cs" />
<Compile Remove="../XamlX/src/XamlX/obj/**" />
</ItemGroup>
</Project>

View File

@@ -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
{
/// <summary>
/// Taken from https://github.com/AvaloniaUI/Avalonia.NameGenerator/blob/ecc9677a23de5cbc90af07ccac14e31c0da41d6a/src/Avalonia.NameGenerator/Infrastructure/RoslynTypeSystem.cs
/// </summary>
public class RoslynTypeSystem : IXamlTypeSystem
{
private readonly List<IXamlAssembly> _assemblies = new List<IXamlAssembly>();
public RoslynTypeSystem(CSharpCompilation compilation)
{
_assemblies.Add(new RoslynAssembly(compilation.Assembly));
var assemblySymbols = compilation
.References
.Select(compilation.GetAssemblyOrModuleSymbol)
.OfType<IAssemblySymbol>()
.Select(assembly => new RoslynAssembly(assembly))
.ToList();
_assemblies.AddRange(assemblySymbols);
}
public IReadOnlyList<IXamlAssembly> 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<IXamlCustomAttribute> 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<object> Parameters =>
_data.ConstructorArguments
.Select(argument => argument.Value)
.ToList();
public Dictionary<string, object> 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<IXamlProperty> Properties =>
_symbol.GetMembers()
.Where(member => member.Kind == SymbolKind.Property)
.OfType<IPropertySymbol>()
.Select(property => new RoslynProperty(property, _assembly))
.ToList();
public IReadOnlyList<IXamlEventInfo> Events { get; } = new List<IXamlEventInfo>();
public IReadOnlyList<IXamlField> Fields { get; } = new List<IXamlField>();
public IReadOnlyList<IXamlMethod> Methods { get; } = new List<IXamlMethod>();
public IReadOnlyList<IXamlConstructor> Constructors =>
_symbol.Constructors
.Select(method => new RoslynConstructor(method, _assembly))
.ToList();
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
public IReadOnlyList<IXamlType> GenericArguments { get; } = new List<IXamlType>();
public bool IsAssignableFrom(IXamlType type) => type == this;
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> 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<IXamlType> Interfaces =>
_symbol.AllInterfaces
.Select(abstraction => new RoslynType(abstraction, _assembly))
.ToList();
public bool IsInterface => _symbol.IsAbstract;
public IXamlType GetEnumUnderlyingType() => null;
public IReadOnlyList<IXamlType> GenericParameters { get; } = new List<IXamlType>();
}
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<IXamlType> Parameters =>
_symbol.Parameters
.Select(parameter => parameter.Type)
.OfType<INamedTypeSymbol>()
.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<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
public IReadOnlyList<IXamlType> IndexerParameters { get; } = new List<IXamlType>();
}
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<IXamlType> Parameters =>
_symbol.Parameters.Select(parameter => parameter.Type)
.OfType<INamedTypeSymbol>()
.Select(type => new RoslynType(type, _assembly))
.ToList();
public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly);
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => null;
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
}
}

View File

@@ -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
{
/// <summary>
/// Based on https://github.com/AvaloniaUI/Avalonia.NameGenerator/blob/ecc9677a23de5cbc90af07ccac14e31c0da41d6a/src/Avalonia.NameGenerator/NameReferenceGenerator.cs
/// Adjusted for our UI-Framework & needs.
/// </summary>
[Generator]
public class XamlUiPartialClassGenerator : ISourceGenerator
{
private const string AttributeName = "Robust.Client.AutoGenerated.GenerateTypedNameReferencesAttribute";
private const string AttributeFile = "GenerateTypedNameReferencesAttribute";
private const string AttributeCode = @"// <auto-generated />
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<IXamlILEmitter, XamlILNodeEmitResult>(), 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<global::{info.type}>(\"{info.name}\");");
return $@"// <auto-generated />
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<INamedTypeSymbol> 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<INamedTypeSymbol>();
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());
}
}
}

View File

@@ -14,7 +14,7 @@ using MidiEvent = NFluidsynth.MidiEvent;
namespace Robust.Client.Audio.Midi
{
public enum MidiRendererStatus
public enum MidiRendererStatus : byte
{
None,
Input,

View File

@@ -223,7 +223,7 @@ namespace Robust.Client
/// <summary>
/// Enumeration of the run levels of the BaseClient.
/// </summary>
public enum ClientRunLevel
public enum ClientRunLevel : byte
{
Error = 0,

View File

@@ -115,6 +115,7 @@ namespace Robust.Client
IoCManager.Register<IViewVariablesManagerInternal, ViewVariablesManager>();
IoCManager.Register<IClientConGroupController, ClientConGroupController>();
IoCManager.Register<IScriptClient, ScriptClient>();
//IoCManager.Register<IXamlCompiler, XamlCompiler>();
}
}
}

View File

@@ -231,6 +231,11 @@ namespace Robust.Client.Debugging
_handle.DrawRect(box, color);
}
public override void DrawRect(in Box2Rotated box, in Color color)
{
_handle.DrawRect(box, color);
}
public override void DrawCircle(Vector2 origin, float radius, in Color color)
{
_handle.DrawCircle(origin, radius, color);

View File

@@ -17,6 +17,7 @@ using Robust.Client.Interfaces.Utility;
using Robust.Client.Player;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
@@ -31,6 +32,7 @@ using Robust.Shared.Interfaces.Timers;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -64,9 +66,12 @@ namespace Robust.Client
[Dependency] private readonly IScriptClient _scriptClient = default!;
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
private CommandLineArgs? _commandLineArgs;
private bool _disableAssemblyLoadContext;
// Arguments for loader-load. Not used otherwise.
private IMainArgs? _loaderArgs;
public InitialLaunchState LaunchState { get; private set; } = default!;
@@ -119,7 +124,13 @@ namespace Robust.Client
_resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null);
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client");
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null);
if (_loaderArgs != null)
{
_stringSerializer.EnableCaching = false;
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
// Bring display up as soon as resources are mounted.
if (!_clyde.Initialize())
@@ -185,6 +196,18 @@ namespace Robust.Client
return true;
}
private Stream? VerifierExtraLoadHandler(string arg)
{
DebugTools.AssertNotNull(_loaderArgs);
if (_loaderArgs!.FileApi.TryOpen(arg, out var stream))
{
return stream;
}
return null;
}
private void ReadInitialLaunchState()
{
if (_commandLineArgs == null)
@@ -347,7 +370,7 @@ namespace Robust.Client
}
internal enum DisplayMode
internal enum DisplayMode : byte
{
Headless,
Clyde,

View File

@@ -0,0 +1,18 @@
using Robust.Client;
using Robust.LoaderApi;
[assembly: LoaderEntryPoint(typeof(GameController.LoaderEntryPoint))]
namespace Robust.Client
{
internal partial class GameController
{
internal class LoaderEntryPoint : ILoaderEntryPoint
{
public void Main(IMainArgs args)
{
GameController.Start(args.Args, contentStart: false, args);
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
using System.Threading;
using Robust.Client.Interfaces;
using Robust.LoaderApi;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -21,7 +21,7 @@ namespace Robust.Client
Start(args);
}
public static void Start(string[] args, bool contentStart = false)
public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null)
{
if (_hasStarted)
{
@@ -32,11 +32,11 @@ namespace Robust.Client
if (CommandLineArgs.TryParse(args, out var parsed))
{
ParsedMain(parsed, contentStart);
ParsedMain(parsed, contentStart, loaderArgs);
}
}
private static void ParsedMain(CommandLineArgs args, bool contentStart)
private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs)
{
IoCManager.InitThread();
@@ -46,6 +46,7 @@ namespace Robust.Client
var gc = (GameController) IoCManager.Resolve<IGameController>();
gc.SetCommandLineArgs(args);
gc._loaderArgs = loaderArgs;
// When the game is ran with the startup executable being content,
// we have to disable the separate load context.

View File

@@ -83,6 +83,18 @@ namespace Robust.Client.GameObjects
set => _energy = value;
}
/// <summary>
/// Soft shadow strength multiplier.
/// Has no effect if soft shadows are not enabled.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public float Softness
{
get => _softness;
set => _softness = value;
}
[ViewVariables(VVAccess.ReadWrite)]
public bool VisibleNested
{
@@ -115,6 +127,7 @@ namespace Robust.Client.GameObjects
private bool _maskAutoRotate;
private Angle _rotation;
private float _energy;
private float _softness;
/// <summary>
/// Radius, in meters.
@@ -167,6 +180,7 @@ namespace Robust.Client.GameObjects
serializer.DataFieldCached(ref _color, "color", Color.White);
serializer.DataFieldCached(ref _enabled, "enabled", true);
serializer.DataFieldCached(ref _energy, "energy", 1f);
serializer.DataFieldCached(ref _softness, "softness", 1f);
serializer.DataFieldCached(ref _maskAutoRotate, "autoRot", false);
serializer.DataFieldCached(ref _visibleNested, "nestedvisible", true);

View File

@@ -16,7 +16,7 @@ namespace Robust.Client.GameObjects.EntitySystems
/// Handles interpolation of transform positions.
/// </summary>
[UsedImplicitly]
internal sealed class TransformSystem : EntitySystem
internal sealed class TransformSystem : SharedTransformSystem
{
// Max distance per tick how far an entity can move before it is considered teleporting.
// TODO: Make these values somehow dependent on server TPS.

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.Graphics.Clyde
// To be clear: You shouldn't change this. This just helps with understanding where Primitive Restart is being used.
private const ushort PrimitiveRestartIndex = ushort.MaxValue;
private enum Renderer
private enum Renderer : short
{
// Default: Try all supported renderers (not necessarily the renderers shown here)
Default = default,

View File

@@ -11,6 +11,22 @@ namespace Robust.Client.Graphics.Clyde
{
static Clyde()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
try
{
// We force load nvapi64.dll so nvidia gives us the dedicated GPU on optimus laptops.
// This is 100x easier than nvidia's documented approach of NvOptimusEnablement,
// and works while developing.
NativeLibrary.Load("nvapi64.dll");
}
catch (Exception)
{
// If this fails whatever.
}
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;

View File

@@ -377,6 +377,7 @@ namespace Robust.Client.Graphics.Clyde
var lastRange = float.NaN;
var lastPower = float.NaN;
var lastColor = new Color(float.NaN, float.NaN, float.NaN, float.NaN);
var lastSoftness = float.NaN;
Texture? lastMask = null;
for (var i = 0; i < count; i++)
@@ -424,6 +425,12 @@ namespace Robust.Client.Graphics.Clyde
lightShader.SetUniformMaybe("lightColor", lastColor);
}
if (_enableSoftShadows && !MathHelper.CloseTo(lastSoftness, component.Softness))
{
lastSoftness = component.Softness;
lightShader.SetUniformMaybe("lightSoftness", lastSoftness);
}
lightShader.SetUniformMaybe("lightCenter", lightPos);
lightShader.SetUniformMaybe("lightIndex", (i + 0.5f) / ShadowTexture.Height);

View File

@@ -970,7 +970,7 @@ namespace Robust.Client.Graphics.Clyde
public Color Color;
}
private enum RenderCommandType
private enum RenderCommandType : byte
{
DrawBatch,

View File

@@ -8,7 +8,7 @@ using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
public enum WindowMode
public enum WindowMode : byte
{
Windowed = 0,
Fullscreen = 1,

View File

@@ -6,7 +6,7 @@ namespace Robust.Client.Graphics.Drawing
/// <remarks>
/// See <see href="https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#drawing-point-lists">Vulkan's documentation</see> for descriptions of all these modes.
/// </remarks>
public enum DrawPrimitiveTopology
public enum DrawPrimitiveTopology : byte
{
PointList,
TriangleList,

View File

@@ -318,7 +318,7 @@ namespace Robust.Client.Graphics.Drawing
/// Describes margins of a style box.
/// </summary>
[Flags]
public enum Margin
public enum Margin : byte
{
None = 0,

View File

@@ -329,7 +329,7 @@ namespace Robust.Client.Graphics.Drawing
/// <summary>
/// Specifies how to stretch the sides and center of the style box.
/// </summary>
public enum StretchMode
public enum StretchMode : byte
{
Stretch,
Tile,

View File

@@ -98,7 +98,7 @@ namespace Robust.Client.Graphics.Overlays
/// Determines in which canvas layers an overlay gets drawn.
/// </summary>
[Flags]
public enum OverlaySpace
public enum OverlaySpace : byte
{
/// <summary>
/// Used for matching bit flags.

View File

@@ -1,6 +1,6 @@
namespace Robust.Client.Graphics.Shaders
{
public enum ShaderParamType
public enum ShaderParamType : byte
{
// Can this even happen?
Void = 0,

View File

@@ -106,7 +106,7 @@ namespace Robust.Client.Graphics.Shaders
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum ShaderDataType
internal enum ShaderDataType : byte
{
Void,
Bool,
@@ -160,7 +160,7 @@ namespace Robust.Client.Graphics.Shaders
}
return Type.GetNativeType();
}
public bool TypePrecisionConsistent()
{
return Type.TypeHasPrecision() == (Precision != ShaderPrecisionQualifier.None);
@@ -190,7 +190,7 @@ namespace Robust.Client.Graphics.Shaders
throw new ArgumentOutOfRangeException(nameof(qualifier), qualifier, null);
}
}
public static bool TypeHasPrecision(this ShaderDataType type)
{
return
@@ -232,13 +232,13 @@ namespace Robust.Client.Graphics.Shaders
};
}
internal enum ShaderLightMode
internal enum ShaderLightMode : byte
{
Default = 0,
Unshaded = 1,
}
internal enum ShaderBlendMode
internal enum ShaderBlendMode : byte
{
None,
Mix,
@@ -247,7 +247,7 @@ namespace Robust.Client.Graphics.Shaders
Multiply
}
internal enum ShaderPreset
internal enum ShaderPreset : byte
{
Default,
Raw
@@ -255,7 +255,7 @@ namespace Robust.Client.Graphics.Shaders
// Yeah I had no idea what to name this.
[Flags]
internal enum ShaderParameterQualifiers
internal enum ShaderParameterQualifiers : byte
{
None = 0,
In = 1,
@@ -263,7 +263,7 @@ namespace Robust.Client.Graphics.Shaders
Inout = 3,
}
internal enum ShaderPrecisionQualifier
internal enum ShaderPrecisionQualifier : byte
{
None = 0,
Low = 1,

View File

@@ -593,7 +593,7 @@ namespace Robust.Client.Graphics.Shaders
public Symbols Symbol { get; }
}
private enum Symbols
private enum Symbols : byte
{
Semicolon,
Comma,

View File

@@ -296,7 +296,7 @@ namespace Robust.Client.Graphics.Shaders
}
}
private enum ShaderKind
private enum ShaderKind : byte
{
Source,
Canvas

View File

@@ -189,7 +189,7 @@ namespace Robust.Client.Graphics
/// Controls behavior when reading texture coordinates outside 0-1, which usually wraps the texture somehow.
/// </summary>
[PublicAPI]
public enum TextureWrapMode
public enum TextureWrapMode : byte
{
/// <summary>
/// Do not wrap, instead clamp to edge.

View File

@@ -10,7 +10,7 @@ namespace Robust.Client.Input
/// <summary>
/// Represents one of three mouse buttons.
/// </summary>
public enum Button
public enum Button : byte
{
Left = 1,
Middle = 2,

View File

@@ -772,14 +772,14 @@ namespace Robust.Client.Input
}
}
public enum KeyBindingType
public enum KeyBindingType : byte
{
Unknown = 0,
State,
Toggle,
}
public enum CommandState
public enum CommandState : byte
{
Unknown = 0,
Enabled,

View File

@@ -1,6 +1,6 @@
namespace Robust.Client.Interfaces.Graphics
{
internal enum ClydeDebugLayers
internal enum ClydeDebugLayers : byte
{
None,
Fov,

View File

@@ -1,6 +1,6 @@
namespace Robust.Client.Interfaces.Graphics
{
internal enum ClydeStockTexture
internal enum ClydeStockTexture : byte
{
White,
Black,

View File

@@ -3,7 +3,7 @@ namespace Robust.Client.Interfaces.Graphics
/// <summary>
/// Formats for the color component of a render target.
/// </summary>
public enum RenderTargetColorFormat
public enum RenderTargetColorFormat : byte
{
/// <summary>
/// 8 bits per channel linear RGBA.

View File

@@ -1,6 +1,6 @@
namespace Robust.Client.Interfaces.Graphics
{
public enum ScreenshotType
public enum ScreenshotType : byte
{
BeforeUI,
AfterUI

View File

@@ -3,7 +3,7 @@ namespace Robust.Client.Interfaces.Graphics
/// <summary>
/// OS-standard cursor shapes.
/// </summary>
public enum StandardCursorShape
public enum StandardCursorShape : byte
{
/// <summary>
/// The standard arrow shape. Used in almost all situations.

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.ResourceManagement;
using Robust.LoaderApi;
using Robust.Shared.Interfaces.Resources;
using Robust.Shared.Utility;
@@ -47,5 +48,7 @@ namespace Robust.Client.Interfaces.ResourceManagement
{
void TextureLoaded(TextureLoadedEventArgs eventArgs);
void RsiLoaded(RsiLoadedEventArgs eventArgs);
void MountLoaderApi(IFileApi api, string apiPrefix, ResourcePath? prefix=null);
}
}

View File

@@ -138,6 +138,13 @@ namespace Robust.Client.Interfaces.UserInterface
/// Hides the tooltip for the indicated control, if tooltip for that control is currently showing.
/// </summary>
void HideTooltipFor(Control control);
/// <summary>
/// If the control is currently showing a tooltip,
/// gets the tooltip that was supplied via TooltipSupplier (null if tooltip
/// was not supplied by tooltip supplier or tooltip is not showing for the control).
/// </summary>
Control? GetSuppliedTooltipFor(Control control);
}
}

View File

@@ -618,7 +618,7 @@ namespace Robust.Client.Placement
NetworkManager.ClientSendMessage(message);
}
public enum PlacementTypes
public enum PlacementTypes : byte
{
None = 0,
Line = 1,

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.LoaderApi;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
{
internal partial class ResourceCache
{
private sealed class LoaderApiLoader : IContentRoot
{
private readonly IFileApi _api;
private readonly string _prefix;
public LoaderApiLoader(IFileApi api, string prefix)
{
_api = api;
_prefix = prefix;
}
public void Mount()
{
}
public bool TryGetFile(ResourcePath relPath, [NotNullWhen(true)] out Stream? stream)
{
if (_api.TryOpen($"{_prefix}{relPath}", out stream))
{
return true;
}
stream = null;
return false;
}
public IEnumerable<ResourcePath> FindFiles(ResourcePath path)
{
foreach (var relPath in _api.AllFiles)
{
if (!relPath.StartsWith(_prefix))
continue;
var resP = new ResourcePath(relPath[_prefix.Length..]);
if (resP.TryRelativeTo(path, out _))
{
yield return resP;
}
}
}
public IEnumerable<string> GetRelativeFilePaths()
{
return _api.AllFiles;
}
}
}
}

View File

@@ -7,10 +7,11 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.LoaderApi;
namespace Robust.Client.ResourceManagement
{
internal class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
internal partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
{
private readonly Dictionary<Type, Dictionary<ResourcePath, BaseResource>> CachedResources =
new();
@@ -210,5 +211,12 @@ namespace Robust.Client.ResourceManagement
{
OnRsiLoaded?.Invoke(eventArgs);
}
public void MountLoaderApi(IFileApi api, string apiPrefix, ResourcePath? prefix=null)
{
prefix ??= ResourcePath.Root;
var root = new LoaderApiLoader(api, apiPrefix);
AddRoot(prefix, root);
}
}
}

View File

@@ -21,6 +21,7 @@
<PackageReference Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageReference Include="OpenToolkit.OpenAL" Version="4.0.0-pre9.1" />
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" />
<PackageReference Include="Robust.Natives" Version="0.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="3.8.0" />
@@ -32,6 +33,7 @@
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\OpenToolkit.GraphicsLibraryFramework\OpenToolkit.GraphicsLibraryFramework.csproj" />
<ProjectReference Include="..\Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj" />
<ProjectReference Include="..\Robust.Physics\Robust.Physics.csproj" />
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
@@ -47,5 +49,6 @@
<PropertyGroup>
<RobustToolsPath>../Tools</RobustToolsPath>
</PropertyGroup>
<Target Name="RobustAfterBuild" DependsOnTargets="ClientAfterBuild" AfterTargets="Build" />
<Target Name="RobustAfterBuild" AfterTargets="Build" />
<Import Project="..\MSBuild\XamlIL.targets" />
</Project>

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Default common cursor shapes available in the UI.
/// </summary>
public enum CursorShape
public enum CursorShape: byte
{
Arrow,
IBeam,

View File

@@ -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<string, Control> nameScope)
//{
// _nameScope = nameScope;
//}
public NameScope? FindNameScope()
{
foreach (var control in this.GetSelfAndLogicalAncestors())
{
if (control.NameScope != null) return control.NameScope;
}
return null;
}
public T FindControl<T>(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; }
/// <summary>
@@ -62,6 +102,9 @@ namespace Robust.Client.UserInterface
[ViewVariables]
public OrderedChildCollection Children { get; }
[Content]
public virtual ICollection<Control> XamlChildren { get; protected set; }
[ViewVariables] public int ChildCount => _orderedChildren.Count;
/// <summary>
@@ -193,7 +236,8 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Simple text tooltip that is shown when the mouse is hovered over this control for a bit.
/// See <see cref="OnShowTooltip"/> for a more customizable alternative.
/// See <see cref="TooltipSupplier"/> or <see cref="OnShowTooltip"/> for a more customizable alternative.
/// No effect when TooltipSupplier is specified.
/// </summary>
/// <remarks>
/// If empty or null, no tooltip is shown in the first place (but OnShowTooltip and OnHideTooltip
@@ -201,9 +245,37 @@ namespace Robust.Client.UserInterface
/// </remarks>
public string? ToolTip { get; set; }
/// <summary>
/// Overrides the global tooltip delay, showing the tooltip for this
/// control within the specified number of seconds.
/// </summary>
public float? TooltipDelay { get; set; }
/// <summary>
/// When a tooltip should be shown for this control, this will be invoked to
/// produce a control which will serve as the tooltip (doing nothing if null is returned).
/// This is the generally recommended way to implement custom tooltips for controls, as it takes
/// care of the various edge cases for showing / hiding the tooltip.
/// For an even more customizable approach, <see cref="OnShowTooltip"/>
///
/// The returned control will be added to PopupRoot, and positioned
/// within the user interface under the current mouse position to avoid going off the edge of the
/// screen. When the tooltip should be hidden, the control will be hidden by removing it from the tree.
///
/// It is expected that the returned control remains within PopupRoot. Other classes should
/// not move it around in the tree or move it out of PopupRoot, but may access and modify
/// the control and its children via <see cref="SuppliedTooltip"/>.
/// </summary>
/// <remarks>
/// Returning a new instance of a tooltip control every time is usually fine. If for some
/// reason constructing the tooltip control is expensive, it MAY be fine to cache + reuse a single instance but this
/// approach has not yet been tested.
/// </remarks>
public TooltipSupplier? TooltipSupplier { get; set; }
/// <summary>
/// Invoked when the mouse is hovered over this control for a bit and a tooltip
/// should be shown. Can be used as an alternative to ToolTip to perform custom tooltip
/// should be shown. Can be used as an alternative to ToolTip or TooltipSupplier to perform custom tooltip
/// logic such as showing a more complex tooltip control.
///
/// Any custom tooltip controls should typically be added
@@ -213,6 +285,23 @@ namespace Robust.Client.UserInterface
/// </summary>
public event EventHandler? OnShowTooltip;
/// <summary>
/// If this control is currently showing a tooltip provided via TooltipSupplier,
/// returns that tooltip. Do not move this control within the tree, it should remain in PopupRoot.
/// Also, as it may be hidden (removed from tree) at any time, saving a reference to this is a Bad Idea.
/// </summary>
public Control? SuppliedTooltip => UserInterfaceManagerInternal.GetSuppliedTooltipFor(this);
/// <summary>
/// Manually hide the tooltip currently being shown for this control, if there is one.
/// </summary>
public void HideTooltip()
{
UserInterfaceManagerInternal.HideTooltipFor(this);
}
internal void PerformShowTooltip()
{
OnShowTooltip?.Invoke(this, EventArgs.Empty);
@@ -228,6 +317,7 @@ namespace Robust.Client.UserInterface
OnHideTooltip?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// The mode that controls how mouse filtering works. See the enum for how it functions.
/// </summary>
@@ -347,6 +437,7 @@ namespace Robust.Client.UserInterface
UserInterfaceManagerInternal = IoCManager.Resolve<IUserInterfaceManagerInternal>();
StyleClasses = new StyleClassCollection(this);
Children = new OrderedChildCollection(this);
XamlChildren = Children;
}
/// <summary>
@@ -757,7 +848,7 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Mode that will be tested when testing controls to invoke mouse button events on.
/// </summary>
public enum MouseFilterMode
public enum MouseFilterMode : byte
{
/// <summary>
/// The control will be able to receive mouse buttons events.
@@ -865,4 +956,6 @@ namespace Robust.Client.UserInterface
}
}
}
public delegate Control? TooltipSupplier(Control sender);
}

View File

@@ -318,7 +318,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
public enum DrawModeEnum
public enum DrawModeEnum : byte
{
Normal = 0,
Pressed = 1,
@@ -361,7 +361,7 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// For use with <see cref="BaseButton.Mode"/>.
/// </summary>
public enum ActionMode
public enum ActionMode : byte
{
/// <summary>
/// <see cref="BaseButton.OnPressed"/> fires when the mouse button causing them is pressed down.

View File

@@ -210,7 +210,7 @@ namespace Robust.Client.UserInterface.Controls
return new Vector2(minWidth, minHeight);
}
public enum AlignMode
public enum AlignMode : byte
{
/// <summary>
/// Controls are laid out from the begin of the box container.

View File

@@ -557,13 +557,13 @@ namespace Robust.Client.UserInterface.Controls
}
}
public enum Dimension
public enum Dimension : byte
{
Column,
Row
}
public enum LimitType
public enum LimitType : byte
{
/// <summary>
/// Defined number of rows or columns

View File

@@ -570,7 +570,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
public enum ItemListSelectMode
public enum ItemListSelectMode : byte
{
None,
Single,

View File

@@ -194,7 +194,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
public enum AlignMode
public enum AlignMode : byte
{
Left = 0,
Center = 1,
@@ -202,7 +202,7 @@ namespace Robust.Client.UserInterface.Controls
Fill = 3
}
public enum VAlignMode
public enum VAlignMode : byte
{
Top = 0,
Center = 1,

View File

@@ -126,7 +126,16 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// Sets an anchor AND a margin preset. This is most likely the method you want.
///
/// </summary>
/// <remarks>
/// Note that the current size and minimum size of the control affects how
/// each of the margins will be set, so if your control needs to shrink beyond its
/// current size / min size, you should either not call this method or only call it when your
/// control has a size of (0, 0). Otherwise your control's size will never be able
/// to go below the size implied by the margins set in this method.
/// </remarks>
public static void SetAnchorAndMarginPreset(Control control, LayoutPreset preset,
LayoutPresetMode mode = LayoutPresetMode.MinSize,
int margin = 0)
@@ -274,6 +283,12 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// Changes all the margins of a control at once to common presets.
/// The result is that the control is laid out as specified by the preset.
///
/// Note that the current size and minimum size of the control affects how
/// each of the margins will be set, so if your control needs to shrink beyond its
/// current size / min size, you should either not call this method or only call it when your
/// control has a size of (0, 0). Otherwise your control's size will never be able
/// to go below the size implied by the margins set in this method.
/// </summary>
/// <param name="preset"></param>
/// <param name="resizeMode"></param>

View File

@@ -711,7 +711,7 @@ namespace Robust.Client.UserInterface.Controls
return CharClass.Other;
}
private enum CharClass
private enum CharClass : byte
{
Other,
AlphaNumeric,

View File

@@ -0,0 +1,325 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls
{
/// <summary>
/// Option button which allows toggling multiple elements.
/// </summary>
/// <typeparam name="TKey">type to use as the unique key for each option. Functions similarly
/// to dictionary key, so the type should make sure to respect dictionary key semantics.</typeparam>
public class MultiselectOptionButton<TKey> : ContainerButton where TKey : notnull
{
public const string StyleClassOptionButton = "optionButton";
public const string StyleClassOptionTriangle = "optionTriangle";
private List<ButtonData> _buttonData = new();
// map from key to buttondata index
private Dictionary<TKey, int> _keyMap = new();
private readonly Popup _popup;
private readonly VBoxContainer _popupVBox;
private readonly Label _label;
public event Action<ItemPressedEventArgs>? OnItemSelected;
/// <summary>
/// Tracks the order in which items were selected, latest going at the end.
/// </summary>
private List<TKey> _selectedKeys = new();
/// <summary>
/// Ids of all currently selected items, ordered by most recently selected = last
/// </summary>
public IReadOnlyList<TKey> SelectedKeys => _selectedKeys;
public int ItemCount => _buttonData.Count;
/// <summary>
/// Labels of all currently selected items, ordered by most recently selected = last
/// </summary>
public IEnumerable<string?> SelectedLabels => _selectedKeys
.Select(key => _buttonData[_keyMap[key]].Button.Label.Text);
/// <summary>
/// Metadata of all currently selected items, ordered by most recently selected = last
/// </summary>
public IEnumerable<object?> SelectedMetadata => _selectedKeys
.Select(key => _buttonData[_keyMap[key]].Metadata);
public string? Label
{
get => _label.Text;
set => _label.Text = value;
}
public MultiselectOptionButton()
{
AddStyleClass(StyleClassButton);
OnPressed += OnPressedInternal;
var hBox = new HBoxContainer();
AddChild(hBox);
_popup = new Popup();
_popupVBox = new VBoxContainer();
_popup.AddChild(_popupVBox);
_popup.OnPopupHide += OnPopupHide;
_label = new Label
{
StyleClasses = { StyleClassOptionButton },
SizeFlagsHorizontal = SizeFlags.FillExpand,
};
hBox.AddChild(_label);
var textureRect = new TextureRect
{
StyleClasses = { StyleClassOptionTriangle },
SizeFlagsVertical = SizeFlags.ShrinkCenter,
};
hBox.AddChild(textureRect);
}
public void AddItem(Texture icon, string label, TKey key)
{
AddItem(label, key);
}
public void AddItem(string label, TKey key)
{
if (_keyMap.ContainsKey(key))
{
throw new ArgumentException("An item with the same key already exists.");
}
var button = new Button
{
Text = label,
ToggleMode = true
};
button.OnPressed += ButtonOnPressed;
var data = new ButtonData(label, button, key);
_keyMap.Add(key, _buttonData.Count);
_buttonData.Add(data);
_popupVBox.AddChild(button);
}
private void TogglePopup(bool show)
{
if (show)
{
var globalPos = GlobalPosition;
var (minX, minY) = _popupVBox.CombinedMinimumSize;
var box = UIBox2.FromDimensions(globalPos, (Math.Max(minX, Width), minY));
UserInterfaceManager.ModalRoot.AddChild(_popup);
_popup.Open(box);
}
else
{
_popup.Close();
}
}
private void OnPopupHide()
{
UserInterfaceManager.ModalRoot.RemoveChild(_popup);
}
private void ButtonOnPressed(ButtonEventArgs obj)
{
TogglePopup(false);
foreach (var buttonData in _buttonData)
{
if (buttonData.Button == obj.Button)
{
if (obj.Button.Pressed)
{
_selectedKeys.Add(buttonData.Key);
}
else
{
_selectedKeys.Remove(buttonData.Key);
}
OnItemSelected?.Invoke(new ItemPressedEventArgs(buttonData.Key, obj.Button.Pressed, this));
return;
}
}
// Not reachable.
throw new InvalidOperationException();
}
public void Clear()
{
_keyMap.Clear();
foreach (var buttonDatum in _buttonData)
{
buttonDatum.Button.OnPressed -= ButtonOnPressed;
}
_buttonData.Clear();
_popupVBox.DisposeAllChildren();
_selectedKeys = new List<TKey>();
}
public TKey GetItemKey(int idx)
{
return _buttonData[idx].Key;
}
public object? GetItemMetadata(int idx)
{
return _buttonData[idx].Metadata;
}
public bool IsItemDisabled(int idx)
{
return _buttonData[idx].Disabled;
}
public void RemoveItem(int idx)
{
var data = _buttonData[idx];
data.Button.OnPressed -= ButtonOnPressed;
_keyMap.Remove(data.Key);
_popupVBox.RemoveChild(data.Button);
_buttonData.RemoveAt(idx);
var newIdx = 0;
foreach (var buttonData in _buttonData)
{
_keyMap[buttonData.Key] = newIdx++;
}
}
public void Select(int idx)
{
var data = _buttonData[idx];
if (data.Button.Pressed) return;
_selectedKeys.Add(data.Key);
data.Button.Pressed = true;
}
public void SelectKey(TKey key)
{
Select(GetIdx(key));
}
public void DeselectAll()
{
foreach (var buttonData in _buttonData)
{
Deselect(buttonData);
}
}
public void Deselect(int idx)
{
Deselect(_buttonData[idx]);
}
public void DeselectKey(TKey key)
{
Deselect(GetIdx(key));
}
private void Deselect(ButtonData data)
{
if (!data.Button.Pressed) return;
_selectedKeys.Remove(data.Key);
data.Button.Pressed = false;
}
public int GetIdx(TKey key)
{
return _keyMap[key];
}
public void SetItemDisabled(int idx, bool disabled)
{
var data = _buttonData[idx];
data.Disabled = disabled;
data.Button.Disabled = disabled;
}
public void SetItemKey(int idx, TKey key)
{
if (_keyMap.TryGetValue(key, out var existIdx) && existIdx != idx)
{
throw new InvalidOperationException("An item with said key already exists.");
}
var data = _buttonData[idx];
_keyMap.Remove(data.Key);
_keyMap.Add(key, idx);
data.Key = key;
}
public void SetItemMetadata(int idx, object metadata)
{
_buttonData[idx].Metadata = metadata;
}
public void SetItemText(int idx, string text)
{
var data = _buttonData[idx];
data.Text = text;
data.Button.Text = text;
}
private void OnPressedInternal(ButtonEventArgs args)
{
TogglePopup(true);
}
protected override void ExitedTree()
{
base.ExitedTree();
TogglePopup(false);
}
public class ItemPressedEventArgs : EventArgs
{
public readonly MultiselectOptionButton<TKey> Button;
/// <summary>
/// True if item is being selected, false if being unselected
/// </summary>
public readonly bool Selected;
/// <summary>
/// True if item is being deselected, false if being selected
/// </summary>
public bool Deselected => !Selected;
/// <summary>
/// The key of the item that has been selected or deselected.
/// </summary>
public readonly TKey Key;
public ItemPressedEventArgs(TKey key, bool selected, MultiselectOptionButton<TKey> button)
{
Key = key;
Selected = selected;
Button = button;
}
}
private sealed class ButtonData
{
public string Text;
public bool Disabled;
public object? Metadata;
public TKey Key;
public Button Button;
public ButtonData(string text, Button button, TKey key)
{
Text = text;
Button = button;
Key = key;
}
}
}
}

View File

@@ -10,29 +10,31 @@ namespace Robust.Client.UserInterface.Controls
public const string StyleClassOptionButton = "optionButton";
public const string StyleClassOptionTriangle = "optionTriangle";
private List<ButtonData> _buttonData = new();
private Dictionary<int, int> _idMap = new();
private Popup _popup;
private VBoxContainer _popupVBox;
private Label _label;
private readonly List<ButtonData> _buttonData = new();
private readonly Dictionary<int, int> _idMap = new();
private readonly Popup _popup;
private readonly VBoxContainer _popupVBox;
private readonly Label _label;
public int ItemCount => _buttonData.Count;
public event Action<ItemSelectedEventArgs>? OnItemSelected;
public string Prefix { get; set; }
public OptionButton() : base()
public OptionButton()
{
AddStyleClass(StyleClassButton);
Prefix = "";
OnPressed += _onPressed;
OnPressed += OnPressedInternal;
var hBox = new HBoxContainer();
AddChild(hBox);
_popup = new Popup();
UserInterfaceManager.ModalRoot.AddChild(_popup);
_popupVBox = new VBoxContainer();
_popup.AddChild(_popupVBox);
_popup.OnPopupHide += OnPopupHide;
_label = new Label
{
@@ -85,10 +87,31 @@ namespace Robust.Client.UserInterface.Controls
}
}
private void TogglePopup(bool show)
{
if (show)
{
var globalPos = GlobalPosition;
var (minX, minY) = _popupVBox.CombinedMinimumSize;
var box = UIBox2.FromDimensions(globalPos, (Math.Max(minX, Width), minY));
UserInterfaceManager.ModalRoot.AddChild(_popup);
_popup.Open(box);
}
else
{
_popup.Close();
}
}
private void OnPopupHide()
{
UserInterfaceManager.ModalRoot.RemoveChild(_popup);
}
private void ButtonOnPressed(ButtonEventArgs obj)
{
obj.Button.Pressed = false;
_popup.Visible = false;
TogglePopup(false);
foreach (var buttonData in _buttonData)
{
if (buttonData.Button == obj.Button)
@@ -105,13 +128,15 @@ namespace Robust.Client.UserInterface.Controls
public void Clear()
{
_idMap.Clear();
foreach (var buttonDatum in _buttonData)
{
buttonDatum.Button.OnPressed -= ButtonOnPressed;
}
_buttonData.Clear();
_popupVBox.DisposeAllChildren();
SelectedId = 0;
}
public int ItemCount => _buttonData.Count;
public int GetItemId(int idx)
{
return _buttonData[idx].Id;
@@ -134,9 +159,15 @@ namespace Robust.Client.UserInterface.Controls
public void RemoveItem(int idx)
{
var data = _buttonData[idx];
data.Button.OnPressed -= ButtonOnPressed;
_idMap.Remove(data.Id);
_popupVBox.RemoveChild(data.Button);
_buttonData.RemoveAt(idx);
var newIdx = 0;
foreach (var buttonData in _buttonData)
{
_idMap[buttonData.Id] = newIdx++;
}
}
public void Select(int idx)
@@ -168,13 +199,9 @@ namespace Robust.Client.UserInterface.Controls
data.Button.Disabled = disabled;
}
public void SetItemIcon(int idx, Texture texture)
{
}
public void SetItemId(int idx, int id)
{
if (_idMap.TryGetValue(id, out var existIdx) && existIdx != id)
if (_idMap.TryGetValue(id, out var existIdx) && existIdx != idx)
{
throw new InvalidOperationException("An item with said ID already exists.");
}
@@ -202,19 +229,15 @@ namespace Robust.Client.UserInterface.Controls
data.Button.Text = text;
}
private void _onPressed(ButtonEventArgs args)
private void OnPressedInternal(ButtonEventArgs args)
{
var globalPos = GlobalPosition;
var (minX, minY) = _popupVBox.CombinedMinimumSize;
var box = UIBox2.FromDimensions(globalPos, (Math.Max(minX, Width), minY));
_popup.Open(box);
TogglePopup(true);
}
protected override void Dispose(bool disposing)
protected override void ExitedTree()
{
base.Dispose(disposing);
_popup?.Dispose();
base.ExitedTree();
TogglePopup(false);
}
public class ItemSelectedEventArgs : EventArgs

View File

@@ -21,7 +21,10 @@ namespace Robust.Client.UserInterface.Controls
UserInterfaceManagerInternal.RemoveModal(this);
}
if (box != null && _desiredSize != box.Value.Size)
if (box != null &&
(_desiredSize != box.Value.Size ||
PopupContainer.GetPopupOrigin(this) != box.Value.TopLeft ||
PopupContainer.GetAltOrigin(this) != altPos))
{
PopupContainer.SetPopupOrigin(this, box.Value.TopLeft);
PopupContainer.SetAltOrigin(this, altPos);
@@ -34,6 +37,13 @@ namespace Robust.Client.UserInterface.Controls
UserInterfaceManagerInternal.PushModal(this);
}
public void Close()
{
if (!Visible) return;
UserInterfaceManagerInternal.RemoveModal(this);
}
protected internal override void ModalRemoved()
{
base.ModalRemoved();

View File

@@ -35,6 +35,16 @@ namespace Robust.Client.UserInterface.Controls
control.SetValue(PopupOriginProperty, origin);
}
public static Vector2 GetPopupOrigin(Control control)
{
return control.GetValue<Vector2>(PopupOriginProperty);
}
public static Vector2? GetAltOrigin(Control control)
{
return control.GetValue<Vector2?>(AltOriginProperty);
}
public static void SetAltOrigin(Control control, Vector2? origin)
{
control.SetValue(AltOriginProperty, origin);

View File

@@ -225,7 +225,7 @@ namespace Robust.Client.UserInterface.Controls
return _getGrabberStyleBox()?.MinimumSize ?? Vector2.Zero;
}
protected enum OrientationMode
protected enum OrientationMode : byte
{
Horizontal,
Vertical

View File

@@ -230,7 +230,7 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// Defines how user-initiated moving of the split should work
/// </summary>
public enum SplitResizeMode
public enum SplitResizeMode : sbyte
{
/// <summary>
/// Don't allow user to move the split.
@@ -249,7 +249,7 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// Defines how the split position should be determined
/// </summary>
private enum SplitState
private enum SplitState : byte
{
/// <summary>
/// Automatically adjust the split based on the width of the children

View File

@@ -173,7 +173,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
public enum StretchMode
public enum StretchMode : byte
{
/// <summary>
/// The texture is stretched to fit the entire area of the control.

View File

@@ -255,7 +255,7 @@ namespace Robust.Client.UserInterface.CustomControls
}
[Flags]
protected enum DragMode
protected enum DragMode : byte
{
None = 0,
Move = 1,

View File

@@ -0,0 +1,15 @@
<cc:SS14Window xmlns:cc="clr-namespace:Robust.Client.UserInterface.CustomControls"
xmlns:c="clr-namespace:Robust.Client.UserInterface.Controls">
<c:PanelContainer StyleClasses="windowPanel"/>
<c:VBoxContainer SeparationOverride="0">
<c:PanelContainer Name="WindowHeader" StyleClasses="windowHeader">
<c:HBoxContainer>
<c:MarginContainer MarginLeftOverride="5" SizeFlagsHorizontal="FillExpand">
<c:Label Name="TitleLabel" StyleIdentifier="foo" ClipText="True" Text="Exemplary Window Title Here" VAlign="Center" StyleClasses="windowTitle"/>
</c:MarginContainer>
<c:TextureButton Name="CloseButton" StyleClasses="windowCloseButton" SizeFlagsVertical="ShrinkCenter"></c:TextureButton>
</c:HBoxContainer>
</c:PanelContainer>
<c:MarginContainer Name="ContentsContainer" MarginBottomOverride="10" MarginLeftOverride="10" MarginRightOverride="10" MarginTopOverride="10" RectClipContent="True" SizeFlagsVertical="FillExpand" />
</c:VBoxContainer>
</cc:SS14Window>

View File

@@ -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<Control>, IReadOnlyCollection<Control>
{
private readonly SS14Window Owner;
public SS14ContentCollection(SS14Window owner)
{
Owner = owner;
}
public Enumerator GetEnumerator()
{
return new(Owner);
}
IEnumerator<Control> IEnumerable<Control>.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<Control>.Count => Owner.Contents.ChildCount;
int IReadOnlyCollection<Control>.Count => Owner.Contents.ChildCount;
public bool IsReadOnly => false;
public struct Enumerator : IEnumerator<Control>
{
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();
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace Robust.Client.UserInterface
{
public static class LogicalExtensions
{
public static IEnumerable<Control> GetSelfAndLogicalAncestors(this Control control)
{
Control? c = control;
while (c != null)
{
yield return c;
c = c.Parent;
}
}
}
}

View File

@@ -77,8 +77,11 @@ namespace Robust.Client.UserInterface
private bool _rendering = true;
private float _tooltipTimer;
// set to null when not counting down
private float? _tooltipDelay;
private Tooltip _tooltip = default!;
private bool showingTooltip;
private Control? _suppliedTooltip;
private const float TooltipDelay = 1;
private readonly Queue<Control> _styleUpdateQueue = new();
@@ -208,10 +211,15 @@ namespace Robust.Client.UserInterface
control.DoLayoutUpdate();
}
_tooltipTimer -= args.DeltaSeconds;
if (_tooltipTimer <= 0)
// count down tooltip delay if we're not showing one yet and
// are hovering the mouse over a control without moving it
if (_tooltipDelay != null && !showingTooltip)
{
_showTooltip();
_tooltipTimer += args.DeltaSeconds;
if (_tooltipTimer >= _tooltipDelay)
{
_showTooltip();
}
}
if (_needUpdateActiveCursor)
@@ -332,6 +340,14 @@ namespace Robust.Client.UserInterface
CurrentlyHovered?.MouseExited();
CurrentlyHovered = newHovered;
CurrentlyHovered?.MouseEntered();
if (CurrentlyHovered != null)
{
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
}
else
{
_tooltipDelay = null;
}
_needUpdateActiveCursor = true;
}
@@ -509,6 +525,7 @@ namespace Robust.Client.UserInterface
{
control.MouseExited();
CurrentlyHovered = null;
_clearTooltip();
}
if (control == _controlFocused)
@@ -695,6 +712,11 @@ namespace Robust.Client.UserInterface
{
if (!showingTooltip) return;
_tooltip.Visible = false;
if (_suppliedTooltip != null)
{
PopupRoot.RemoveChild(_suppliedTooltip);
_suppliedTooltip = null;
}
CurrentlyHovered?.PerformHideTooltip();
_resetTooltipTimer();
showingTooltip = false;
@@ -709,9 +731,14 @@ namespace Robust.Client.UserInterface
}
}
public Control? GetSuppliedTooltipFor(Control control)
{
return CurrentlyHovered == control ? _suppliedTooltip : null;
}
private void _resetTooltipTimer()
{
_tooltipTimer = TooltipDelay;
_tooltipTimer = 0;
}
private void _showTooltip()
@@ -724,9 +751,19 @@ namespace Robust.Client.UserInterface
return;
}
// show simple tooltip if there is one
if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
// show supplied tooltip if there is one
if (hovered.TooltipSupplier != null)
{
_suppliedTooltip = hovered.TooltipSupplier.Invoke(hovered);
if (_suppliedTooltip != null)
{
PopupRoot.AddChild(_suppliedTooltip);
Tooltips.PositionTooltip(_suppliedTooltip);
}
}
else if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
{
// show simple tooltip if there is one
_tooltip.Visible = true;
_tooltip.Text = hovered.ToolTip;
Tooltips.PositionTooltip(_tooltip);

View File

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

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
namespace Robust.Client.UserInterface.XAML
{
/// <summary>
/// Implements a name scope.
/// </summary>
public class NameScope
{
public bool IsCompleted { get; private set; }
private readonly Dictionary<string, Control> _inner = new Dictionary<string, Control>();
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;
}
}
}

View File

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

View File

@@ -67,7 +67,7 @@ namespace Robust.Client.ViewVariables.Editors
return convert.ToString(CultureInfo.InvariantCulture);
}
public enum NumberType
public enum NumberType : byte
{
Byte,
SByte,

View File

@@ -174,7 +174,7 @@ namespace Robust.Client.ViewVariables.Editors
return hBoxContainer;
}
public enum BoxType
public enum BoxType : byte
{
Box2,
Box2i,

1
Robust.LoaderApi Submodule

Submodule Robust.LoaderApi added at 0d5c015792

View File

@@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;
namespace Robust.Server.Interfaces.ServerStatus
{
public delegate bool StatusHostHandler(
HttpMethod method,
HttpListenerRequest request,
HttpListenerResponse response);
IStatusHandlerContext context);
public interface IStatusHost
{
@@ -32,4 +33,30 @@ namespace Robust.Server.Interfaces.ServerStatus
/// </summary>
event Action<JObject> OnInfoRequest;
}
public interface IStatusHandlerContext
{
HttpMethod RequestMethod { get; }
IPEndPoint RemoteEndPoint { get; }
Uri Url { get; }
bool IsGetLike { get; }
IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
[return: MaybeNull]
public T RequestBodyJson<T>();
void Respond(
string text,
HttpStatusCode code = HttpStatusCode.OK,
string contentType = "text/plain");
void Respond(
string text,
int code = 200,
string contentType = "text/plain");
void RespondError(HttpStatusCode code);
void RespondJson(object jsonData, HttpStatusCode code = HttpStatusCode.OK);
}
}

View File

@@ -14,9 +14,11 @@
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.0" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Loki" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\ManagedHttpListener\src\System.Net.HttpListener.csproj" />
<ProjectReference Include="..\Robust.Physics\Robust.Physics.csproj" />
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />

View File

@@ -1,11 +1,8 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Robust.Server.Interfaces.ServerStatus;
using Robust.Shared;
using Robust.Shared.Utility;
namespace Robust.Server.ServerStatus
{
@@ -20,95 +17,69 @@ namespace Robust.Server.ServerStatus
AddHandler(HandleInfo);
}
private static bool HandleTeapot(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private static bool HandleTeapot(IStatusHandlerContext context)
{
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/teapot")
if (!context.IsGetLike || context.Url!.AbsolutePath != "/teapot")
{
return false;
}
response.Respond(method, "I am a teapot.", (HttpStatusCode) 418);
context.Respond("I am a teapot.", (HttpStatusCode) 418);
return true;
}
private bool HandleStatus(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool HandleStatus(IStatusHandlerContext context)
{
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/status")
if (!context.IsGetLike || context.Url!.AbsolutePath != "/status")
{
return false;
}
if (OnStatusRequest == null)
var jObject = new JObject
{
_httpSawmill.Warning("OnStatusRequest is not set, responding with a 501.");
response.Respond(method, "Not Implemented", HttpStatusCode.NotImplemented);
return true;
}
response.StatusCode = (int) HttpStatusCode.OK;
response.ContentType = "application/json";
if (method == HttpMethod.Head)
{
return true;
}
var jObject = new JObject();
// We need to send at LEAST name and player count to have the launcher work with us.
// Content can override these if it wants (e.g. stealthmins).
["name"] = _serverNameCache,
["players"] = _playerManager.PlayerCount
};
OnStatusRequest?.Invoke(jObject);
using var streamWriter = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);
JsonSerializer.Serialize(jsonWriter, jObject);
jsonWriter.Flush();
context.RespondJson(jObject);
return true;
}
private bool HandleInfo(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool HandleInfo(IStatusHandlerContext context)
{
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/info")
if (!context.IsGetLike || context.Url!.AbsolutePath != "/info")
{
return false;
}
response.StatusCode = (int) HttpStatusCode.OK;
response.ContentType = "application/json";
if (method == HttpMethod.Head)
{
return true;
}
var downloadUrlWindows = _configurationManager.GetCVar(CVars.BuildDownloadUrlWindows);
var downloadUrl = _configurationManager.GetCVar(CVars.BuildDownloadUrl);
JObject? buildInfo;
if (string.IsNullOrEmpty(downloadUrlWindows))
if (string.IsNullOrEmpty(downloadUrl))
{
buildInfo = null;
}
else
{
var hash = _configurationManager.GetCVar(CVars.BuildHash);
if (hash == "")
{
hash = null;
}
buildInfo = new JObject
{
["download_urls"] = new JObject
{
["Windows"] = downloadUrlWindows,
["MacOS"] = _configurationManager.GetCVar(CVars.BuildDownloadUrlMacOS),
["Linux"] = _configurationManager.GetCVar(CVars.BuildDownloadUrlLinux)
},
["engine_version"] = _configurationManager.GetCVar(CVars.BuildEngineVersion),
["fork_id"] = _configurationManager.GetCVar(CVars.BuildForkId),
["version"] = _configurationManager.GetCVar(CVars.BuildVersion),
["hashes"] = new JObject
{
["Windows"] = _configurationManager.GetCVar(CVars.BuildHashWindows),
["MacOS"] = _configurationManager.GetCVar(CVars.BuildHashMacOS),
["Linux"] = _configurationManager.GetCVar(CVars.BuildHashLinux),
},
["download_url"] = downloadUrl,
["hash"] = hash,
};
}
@@ -129,13 +100,7 @@ namespace Robust.Server.ServerStatus
OnInfoRequest?.Invoke(jObject);
using var streamWriter = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);
JsonSerializer.Serialize(jsonWriter, jObject);
jsonWriter.Flush();
context.RespondJson(jObject);
return true;
}

View File

@@ -1,20 +1,27 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Robust.Server.Interfaces.Player;
using Robust.Server.Interfaces.ServerStatus;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using HttpListener = ManagedHttpListener.HttpListener;
using HttpListenerContext = ManagedHttpListener.HttpListenerContext;
// This entire file is NIHing a REST server because pulling in libraries is effort.
// Also it was fun to write.
@@ -28,6 +35,7 @@ namespace Robust.Server.ServerStatus
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private static readonly JsonSerializer JsonSerializer = new();
private readonly List<StatusHostHandler> _handlers = new();
@@ -35,19 +43,20 @@ namespace Robust.Server.ServerStatus
private TaskCompletionSource? _stopSource;
private ISawmill _httpSawmill = default!;
private string? _serverNameCache;
public Task ProcessRequestAsync(HttpListenerContext context)
{
var response = context.Response;
var request = context.Request;
var method = new HttpMethod(request.HttpMethod);
var apiContext = (IStatusHandlerContext) new ContextImpl(context);
_httpSawmill.Info($"{method} {context.Request.Url?.PathAndQuery} from {request.RemoteEndPoint}");
_httpSawmill.Info(
$"{apiContext.RequestMethod} {apiContext.Url.PathAndQuery} from {apiContext.RemoteEndPoint}");
try
{
foreach (var handler in _handlers)
{
if (handler(method, request, response))
if (handler(apiContext))
{
return Task.CompletedTask;
}
@@ -55,11 +64,11 @@ namespace Robust.Server.ServerStatus
// No handler returned true, assume no handlers care about this.
// 404.
response.Respond(method, "Not Found", HttpStatusCode.NotFound);
apiContext.Respond("Not Found", HttpStatusCode.NotFound);
}
catch (Exception e)
{
response.Respond(method, "Internal Server Error", HttpStatusCode.InternalServerError);
apiContext.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
_httpSawmill.Error($"Exception in StatusHost: {e}");
}
@@ -85,6 +94,10 @@ namespace Robust.Server.ServerStatus
_httpSawmill = Logger.GetSawmill($"{Sawmill}.http");
RegisterCVars();
// Cache this in a field to avoid thread safety shenanigans.
// Writes/reads of references are atomic in C# so no further synchronization necessary.
_configurationManager.OnValueChanged(CVars.GameHostName, n => _serverNameCache = n);
if (!_configurationManager.GetCVar(CVars.StatusEnabled))
{
return;
@@ -141,24 +154,27 @@ namespace Robust.Server.ServerStatus
private void RegisterCVars()
{
BuildInfo? info = null;
try
{
var buildInfo = File.ReadAllText(PathHelpers.ExecutableRelativeFile("build.json"));
info = JsonConvert.DeserializeObject<BuildInfo>(buildInfo);
var info = JsonConvert.DeserializeObject<BuildInfo>(buildInfo);
// Don't replace cvars with contents of build.json if overriden by --cvar or such.
SetCVarIfUnmodified(CVars.BuildEngineVersion, info.EngineVersion);
SetCVarIfUnmodified(CVars.BuildForkId, info.ForkId);
SetCVarIfUnmodified(CVars.BuildVersion, info.Version);
SetCVarIfUnmodified(CVars.BuildDownloadUrl, info.Download ?? "");
SetCVarIfUnmodified(CVars.BuildHash, info.Hash ?? "");
}
catch (FileNotFoundException)
{
}
_configurationManager.SetCVar(CVars.BuildForkId, info?.ForkId ?? "");
_configurationManager.SetCVar(CVars.BuildVersion, info?.Version ?? "");
_configurationManager.SetCVar(CVars.BuildDownloadUrlWindows, info?.Downloads.Windows ?? "");
_configurationManager.SetCVar(CVars.BuildDownloadUrlMacOS, info?.Downloads.MacOS ?? "");
_configurationManager.SetCVar(CVars.BuildDownloadUrlLinux, info?.Downloads.Linux ?? "");
_configurationManager.SetCVar(CVars.BuildHashWindows, info?.Hashes.Windows ?? "");
_configurationManager.SetCVar(CVars.BuildHashMacOS, info?.Hashes.MacOS ?? "");
_configurationManager.SetCVar(CVars.BuildHashLinux, info?.Hashes.Linux ?? "");
void SetCVarIfUnmodified(CVarDef<string> cvar, string val)
{
if (_configurationManager.GetCVar(cvar) == "")
_configurationManager.SetCVar(cvar, val);
}
}
public void Dispose()
@@ -175,18 +191,84 @@ namespace Robust.Server.ServerStatus
[JsonObject(ItemRequired = Required.DisallowNull)]
private sealed class BuildInfo
{
[JsonProperty("hashes")] public PlatformData Hashes { get; set; } = default!;
[JsonProperty("downloads")] public PlatformData Downloads { get; set; } = default!;
[JsonProperty("fork_id")] public string ForkId { get; set; } = default!;
[JsonProperty("version")] public string Version { get; set; } = default!;
[JsonProperty("engine_version")] public string EngineVersion = default!;
[JsonProperty("hash")] public string? Hash;
[JsonProperty("download")] public string? Download;
[JsonProperty("fork_id")] public string ForkId = default!;
[JsonProperty("version")] public string Version = default!;
}
[JsonObject(ItemRequired = Required.DisallowNull)]
private sealed class PlatformData
private sealed class ContextImpl : IStatusHandlerContext
{
[JsonProperty("windows")] public string Windows { get; set; } = default!;
[JsonProperty("linux")] public string Linux { get; set; } = default!;
[JsonProperty("macos")] public string MacOS { get; set; } = default!;
private readonly HttpListenerContext _context;
public HttpMethod RequestMethod { get; }
public IPEndPoint RemoteEndPoint => _context.Request.RemoteEndPoint!;
public Uri Url => _context.Request.Url!;
public bool IsGetLike => RequestMethod == HttpMethod.Head || RequestMethod == HttpMethod.Get;
public IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
public ContextImpl(HttpListenerContext context)
{
_context = context;
RequestMethod = new HttpMethod(context.Request.HttpMethod!);
var headers = new Dictionary<string, StringValues>();
foreach (string? key in context.Request.Headers.Keys)
{
if (key == null)
continue;
headers.Add(key, context.Request.Headers.GetValues(key));
}
RequestHeaders = headers;
}
[return: MaybeNull]
public T RequestBodyJson<T>()
{
using var streamReader = new StreamReader(_context.Request.InputStream, EncodingHelpers.UTF8);
using var jsonReader = new JsonTextReader(streamReader);
var serializer = new JsonSerializer();
return serializer.Deserialize<T>(jsonReader);
}
public void Respond(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = "text/plain")
{
Respond(text, (int) code, contentType);
}
public void Respond(string text, int code = 200, string contentType = "text/plain")
{
_context.Response.StatusCode = code;
_context.Response.ContentType = contentType;
if (RequestMethod == HttpMethod.Head)
{
return;
}
using var writer = new StreamWriter(_context.Response.OutputStream, EncodingHelpers.UTF8);
writer.Write(text);
}
public void RespondError(HttpStatusCode code)
{
Respond(code.ToString(), code);
}
public void RespondJson(object jsonData, HttpStatusCode code = HttpStatusCode.OK)
{
using var streamWriter = new StreamWriter(_context.Response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);
JsonSerializer.Serialize(jsonWriter, jsonData);
jsonWriter.Flush();
}
}
}
}

View File

@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Http;
using Newtonsoft.Json;
using Robust.Shared.Utility;
@@ -9,41 +8,6 @@ namespace Robust.Server.ServerStatus
{
public static class StatusHostHelpers
{
public static bool IsGetLike(this HttpMethod method)
{
return method == HttpMethod.Get || method == HttpMethod.Head;
}
public static void Respond(
this HttpListenerResponse response,
HttpMethod method,
string text,
HttpStatusCode code = HttpStatusCode.OK,
string contentType = "text/plain")
{
response.Respond(method, text, (int) code, contentType);
}
public static void Respond(
this HttpListenerResponse response,
HttpMethod method,
string text,
int code = 200,
string contentType = "text/plain")
{
response.StatusCode = code;
response.ContentType = contentType;
if (method == HttpMethod.Head)
{
return;
}
using var writer = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
writer.Write(text);
}
[return: MaybeNull]
public static T GetFromJson<T>(this HttpListenerRequest request)
{

View File

@@ -45,9 +45,9 @@ namespace Robust.Server.ServerStatus
_statusHost.AddHandler(ShutdownHandler);
}
private bool UpdateHandler(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool UpdateHandler(IStatusHandlerContext context)
{
if (method != HttpMethod.Post || request.Url!.AbsolutePath != "/update")
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/update")
{
return false;
}
@@ -58,26 +58,26 @@ namespace Robust.Server.ServerStatus
return false;
}
var auth = request.Headers["WatchdogToken"];
var auth = context.RequestHeaders["WatchdogToken"];
if (auth != _watchdogToken)
{
// Holy shit nobody read these logs please.
_sawmill.Info(@"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
response.StatusCode = (int) HttpStatusCode.Unauthorized;
context.RespondError(HttpStatusCode.Unauthorized);
return true;
}
_taskManager.RunOnMainThread(() => UpdateReceived?.Invoke());
response.StatusCode = (int) HttpStatusCode.OK;
context.Respond("Success", HttpStatusCode.OK);
return true;
}
private bool ShutdownHandler(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool ShutdownHandler(IStatusHandlerContext context)
{
if (method != HttpMethod.Post || request.Url!.AbsolutePath != "/shutdown")
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/shutdown")
{
return false;
}
@@ -88,22 +88,21 @@ namespace Robust.Server.ServerStatus
return false;
}
var auth = request.Headers["WatchdogToken"];
var auth = context.RequestHeaders["WatchdogToken"];
if (auth != _watchdogToken)
{
_sawmill.Warning(
"received POST /shutdown with invalid authentication token. Ignoring {0}, {1}", auth,
_watchdogToken);
response.StatusCode = (int) HttpStatusCode.Unauthorized;
context.RespondError(HttpStatusCode.Unauthorized);
return true;
}
ShutdownParameters? parameters = null;
try
{
parameters = request.GetFromJson<ShutdownParameters>();
parameters = context.RequestBodyJson<ShutdownParameters>();
}
catch (JsonSerializationException)
{
@@ -112,14 +111,14 @@ namespace Robust.Server.ServerStatus
if (parameters == null)
{
response.StatusCode = (int) HttpStatusCode.BadRequest;
context.RespondError(HttpStatusCode.BadRequest);
return true;
}
_taskManager.RunOnMainThread(() => _baseServer.Shutdown(parameters.Reason));
response.StatusCode = (int) HttpStatusCode.OK;
context.Respond("Success", HttpStatusCode.OK);
return true;
}

View File

@@ -87,8 +87,10 @@ namespace Robust.Shared.Maths
public Vector2 RotateVec(in Vector2 vec)
{
var (x, y) = vec;
var dx = Math.Cos(Theta) * x - Math.Sin(Theta) * y;
var dy = Math.Sin(Theta) * x + Math.Cos(Theta) * y;
var cos = Math.Cos(Theta);
var sin = Math.Sin(Theta);
var dx = cos * x - sin * y;
var dy = sin * x + cos * y;
return new Vector2((float)dx, (float)dy);
}
@@ -184,6 +186,11 @@ namespace Robust.Shared.Maths
return !(a == b);
}
public Angle Opposite()
{
return new Angle(FlipPositive(Theta-Math.PI));
}
public Angle FlipPositive()
{
return new(FlipPositive(Theta));

View File

@@ -10,7 +10,7 @@ namespace Robust.Shared.Maths
/// </summary>
[Serializable]
[StructLayout(LayoutKind.Explicit)]
public struct Box2 : IEquatable<Box2>
public struct Box2 : IEquatable<Box2>, IApproxEquatable<Box2>
{
/// <summary>
/// The X coordinate of the left edge of the box.
@@ -336,5 +336,21 @@ namespace Robust.Shared.Maths
return new Vector2(cx, cy);
}
public bool EqualsApprox(Box2 other)
{
return MathHelper.CloseTo(Left, other.Left)
&& MathHelper.CloseTo(Bottom, other.Bottom)
&& MathHelper.CloseTo(Right, other.Right)
&& MathHelper.CloseTo(Top, other.Top);
}
public bool EqualsApprox(Box2 other, double tolerance)
{
return MathHelper.CloseTo(Left, other.Left, tolerance)
&& MathHelper.CloseTo(Bottom, other.Bottom, tolerance)
&& MathHelper.CloseTo(Right, other.Right, tolerance)
&& MathHelper.CloseTo(Top, other.Top, tolerance);
}
}
}

View File

@@ -1,4 +1,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace Robust.Shared.Maths
{
@@ -10,6 +13,7 @@ namespace Robust.Shared.Maths
{
public Box2 Box;
public Angle Rotation;
/// <summary>
/// The point about which the rotation occurs.
/// </summary>
@@ -26,13 +30,19 @@ namespace Robust.Shared.Maths
public readonly Vector2 BottomLeft => Origin + Rotation.RotateVec(Box.BottomLeft - Origin);
public Box2Rotated(Vector2 bottomLeft, Vector2 topRight)
: this(new Box2(bottomLeft, topRight)) { }
: this(new Box2(bottomLeft, topRight))
{
}
public Box2Rotated(Box2 box)
: this(box, 0) { }
: this(box, 0)
{
}
public Box2Rotated(Box2 box, Angle rotation)
: this(box, rotation, Vector2.Zero) { }
: this(box, rotation, Vector2.Zero)
{
}
public Box2Rotated(Box2 box, Angle rotation, Vector2 origin)
{
@@ -46,24 +56,97 @@ namespace Robust.Shared.Maths
/// </summary>
public readonly Box2 CalcBoundingBox()
{
// https://stackoverflow.com/a/19830964
if (Sse.IsSupported && NumericsHelpers.Enabled)
{
return CalcBoundingBoxSse();
}
var (X0, Y0) = Box.BottomLeft;
var (X1, Y1) = Box.TopRight;
return CalcBoundingBoxSlow();
}
var Fi = Rotation.Theta;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly unsafe Box2 CalcBoundingBoxSse()
{
Vector128<float> boxVec;
fixed (float* lPtr = &Box.Left)
{
boxVec = Sse.LoadVector128(lPtr);
}
var CX = (X0 + X1) / 2; //Center point
var CY = (Y0 + Y1) / 2;
var WX = (X1 - X0) / 2; //Half-width
var WY = (Y1 - Y0) / 2;
var originX = Vector128.Create(Origin.X);
var originY = Vector128.Create(Origin.Y);
var SF = Math.Sin(Fi);
var CF = Math.Cos(Fi);
var cos = Vector128.Create((float) Math.Cos(Rotation));
var sin = Vector128.Create((float) Math.Sin(Rotation));
var NH = Math.Abs(WX * SF) + Math.Abs(WY * CF); //boundrect half-height
var NW = Math.Abs(WX * CF) + Math.Abs(WY * SF); //boundrect half-width
return new Box2((float) (CX - NW), (float) (CY - NH), (float) (CX + NW), (float) (CY + NH)); //draw bound rectangle
var allX = Sse.Shuffle(boxVec, boxVec, 0b10_10_00_00);
var allY = Sse.Shuffle(boxVec, boxVec, 0b01_11_11_01);
allX = Sse.Subtract(allX, originX);
allY = Sse.Subtract(allY, originY);
var modX = Sse.Subtract(Sse.Multiply(allX, cos), Sse.Multiply(allY, sin));
var modY = Sse.Add(Sse.Multiply(allX, sin), Sse.Multiply(allY, cos));
allX = Sse.Add(modX, originX);
allY = Sse.Add(modY, originY);
var l = SimdHelpers.MinHorizontalSse(allX);
var b = SimdHelpers.MinHorizontalSse(allY);
var r = SimdHelpers.MaxHorizontalSse(allX);
var t = SimdHelpers.MaxHorizontalSse(allY);
var lb = Sse.UnpackLow(l, b);
var rt = Sse.UnpackLow(r, t);
var lbrt = Sse.Shuffle(lb, rt, 0b11_10_01_00);
return Unsafe.As<Vector128<float>, Box2>(ref lbrt);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal readonly unsafe Box2 CalcBoundingBoxSlow()
{
Span<float> allX = stackalloc float[4];
Span<float> allY = stackalloc float[4];
(allX[0], allY[0]) = BottomLeft;
(allX[1], allY[1]) = TopRight;
(allX[2], allY[2]) = TopLeft;
(allX[3], allY[3]) = BottomRight;
var X0 = allX[0];
var X1 = allX[0];
for (int i = 1; i < allX.Length; i++)
{
if (allX[i] > X1)
{
X1 = allX[i];
continue;
}
if (allX[i] < X0)
{
X0 = allX[i];
}
}
var Y0 = allY[0];
var Y1 = allY[0];
for (int i = 1; i < allY.Length; i++)
{
if (allY[i] > Y1)
{
Y1 = allY[i];
continue;
}
if (allY[i] < Y0)
{
Y0 = allY[i];
}
}
return new Box2(X0, Y0, X1, Y1);
}
#region Equality

View File

@@ -1009,7 +1009,7 @@ namespace Robust.Shared.Maths
}
[PublicAPI]
public enum BlendFactor
public enum BlendFactor : byte
{
Zero,
One,

View File

@@ -3,7 +3,7 @@
namespace Robust.Shared.Maths
{
[Flags]
public enum Direction
public enum Direction : sbyte
{
Invalid = -1,
East = 0,

View File

@@ -3,3 +3,6 @@
#if NET5_0
[module: SkipLocalsInit]
#endif
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

View File

@@ -0,0 +1,29 @@
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace Robust.Shared.Maths
{
/// <summary>
/// Helper stuff for SIMD code.
/// </summary>
internal static class SimdHelpers
{
/// <returns>The min value is broadcast to the whole vector.</returns>
public static Vector128<float> MinHorizontalSse(Vector128<float> v)
{
var b = Sse.Shuffle(v, v, 0b10_11_00_01);
var m = Sse.Min(b, v);
var c = Sse.Shuffle(m, m, 0b01_00_11_10);
return Sse.Min(c, m);
}
/// <returns>The max value is broadcast to the whole vector.</returns>
public static Vector128<float> MaxHorizontalSse(Vector128<float> v)
{
var b = Sse.Shuffle(v, v, 0b10_11_00_01);
var m = Sse.Max(b, v);
var c = Sse.Shuffle(m, m, 0b01_00_11_10);
return Sse.Max(c, m);
}
}
}

View File

@@ -3,7 +3,7 @@ namespace Robust.Shared.Animations
/// <summary>
/// Specifies how animated properties are interpolated between two keyframes.
/// </summary>
public enum AnimationInterpolationMode
public enum AnimationInterpolationMode: byte
{
/// <summary>
/// Use a linear interpolation for supported values.

Some files were not shown because too many files have changed in this diff Show More