Compare commits

...

69 Commits

Author SHA1 Message Date
Paul
3ca686298e Fixes ViewVariablesManager using the wrong VVPropEditor for Type in shared not annotated with [NetSerializable] 2021-01-16 20:05:15 +01:00
20kdc
5e914cb13a PointLightComponent: Don't dirty if enabled is being set to what it's already set to. (#1507) 2021-01-16 00:13:55 +11:00
Pieter-Jan Briers
a1bdfca8ba Fix SimplePredictReconcileTest. 2021-01-15 11:03:52 +01:00
20kdc
79deaca409 Polar Coordinates: simplify maths, any-angle occluders are no longer evil (#1504)
Someone please performance-test this, but I think the fragment shader code simplifications speak for themselves.

Radrark found the "line in polar coordinates" equation I needed and a diagram that explained the variables.

That equation was essentially the missing piece to the whole thing.
2021-01-14 13:44:25 +01:00
20kdc
2eeb21431b Fix FileDialogManager not doing DLL mapping properly in sandbox, and FileDialogManager hard crash on multiple dialogs (#1505) 2021-01-14 13:43:39 +01:00
chairbender
c4062bcae9 #1449 new ControlFocusExited override for allowing controls to know (#1467)
when they lost control focus, separate from keyboard focus
2021-01-13 23:18:45 +01:00
Acruid
cd3a85ea04 Replicated CVars (#1489) 2021-01-13 10:02:08 +01:00
Pieter-Jan Briers
d15b5c7f22 Fix compiler warnings. 2021-01-13 03:10:51 +01:00
Leo
18bbe2271d Adds extension method to read and write colors on NetMessages (#1500) 2021-01-13 01:12:55 +01:00
Pieter-Jan Briers
ee2b7a3a66 Adds gcm function to ScriptGlobalsShared 2021-01-12 21:17:35 +01:00
Pieter-Jan Briers
ca36671131 Fix nullable error in RadioOptions 2021-01-12 21:17:26 +01:00
Pieter-Jan Briers
604a1a6960 Disable CodeQL crap since it STILL doesn't do .NET 5 and I'm sick of the errors. 2021-01-12 17:20:11 +01:00
Pieter-Jan Briers
2898f5396f Account for windows time period latency in Lidgren.
1. Set timeBeginPeriod(3) on the server to reduce scheduler latency in the lidgren thread.
2. Add 16ms of guaranteed lag bias to client prediction calculations to account for scheduler latency.

Both of these changes are to account for how the windows scheduler seems to handle time periods in related to socket polls. See this Discord conversation for why, details down below as well: https://discord.com/channels/310555209753690112/770682801607278632/798309250291204107

Basically Windows has this thing called time periods which determines the precision of sleep operations and such. By default it's like 16ms so a sleep will only be accurate to within 16ms.

Problem: Lidgren polls the socket with a timeout of 1ms.

The way Windows seems to handle this is that:
1. if a message comes into the socket, the poll immediately ends and Lidgren can handle it.
2. If nothing comes in, it takes the whole 16ms time period to actually process stuff.

Oh yeah, and Lidgren's thread needs to keep pumping at a steady rate or else it *won't flush its send queue*. On Windows it seems to normally pump at 65/125 Hz. On Linux it goes like 950 Hz as intended.

Now, the worst part is that (1) causes Lidgren's latency calculation to always read 0 (over localhost) instead of the 30~ms it SHOULD BE (assuming client and server localhost).

That 30ms of unaccounted delay worst caseis enough to cause prediction undershoot and have messages arrive too late. Yikes.

So, to fix this...

On the server we just decrease the tick period and call it a day. Screw your battery life players don't have local servers running anyways.

On the client we bias the prediction calculations to account for this "unmeasurable" lag.

Of course, all this can be configured via CVars.
2021-01-12 02:43:15 +01:00
bgare89
39541639c5 Add RadioOptions.cs (#1484)
Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
2021-01-12 01:45:15 +01:00
metalgearsloth
50981ad1a1 Layered PlacementManager (#1403)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-01-11 22:42:54 +11:00
Pieter-Jan Briers
0cbfbeffae Revert "Analyzer to check if interfacemethods are explicitly marked as such" (#1499)
This reverts commit e603153016.
2021-01-11 12:26:23 +01:00
Paul Ritter
e603153016 Analyzer to check if interfacemethods are explicitly marked as such (#1477)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
2021-01-11 12:14:26 +01:00
Pieter-Jan Briers
e7c417ca0c Remove a couple MiB of memory usage. 2021-01-11 10:58:42 +01:00
Pieter-Jan Briers
a3989f28eb Re-order exception handler in StatusHost to avoid blowing up in some cases ??? 2021-01-11 10:24:29 +01:00
Pieter-Jan Briers
38ace3c348 Fix exception with trying to place entities at... NaN?
This is still a bug but it should not cause an exception on the server
2021-01-11 10:24:29 +01:00
Ygg01
0e00170f45 Make RSI directions default to 1. (#1495)
Add some helper methods, made an `example.rsi` for testing.

Closes #1463
2021-01-11 18:12:34 +11:00
Pieter-Jan Briers
261ee96cad Make client connect 1 second faster.
Whoops.
2021-01-11 02:39:35 +01:00
Pieter-Jan Briers
2c851885db Fix ItemList not working correctly with scaling. 2021-01-11 02:06:25 +01:00
Pieter-Jan Briers
849be86455 Add launchauth command to bootstrap login tokens for connecting to live servers. 2021-01-10 23:55:01 +01:00
Pieter-Jan Briers
ffd5c120be Add PreserveBaseOverridesAttribute to sandbox whitelist.
Used by covariant returns.
2021-01-10 22:03:18 +01:00
Vera Aguilera Puerto
76b15dda70 Client-side addcompc/rmcompc (#1497) 2021-01-10 20:09:14 +01:00
Vera Aguilera Puerto
0bf7f519ad Adds client-side setinputcontext command (#1498) 2021-01-10 20:08:44 +01:00
Vera Aguilera Puerto
f97f325a36 Fixes sprite scale not being taken into account. (#1496)
* Fixes sprite scale not being taken into account.
- Only apply scale if length of scale vector not close or equal to 1.

* I blame Remie

* Fix vector maths
I should spend more time studying maths instead of contributing, to be fair.

* Remie no please nO
2021-01-10 19:53:04 +01:00
metalgearsloth
24315fa787 Avoid unnecessary EntMapIdChangedMessages (#1493)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-01-10 12:46:59 +11:00
Ygg01
792179657b Fix space in dotnet breaking windows builds (#1490)
Who would win?
Thirty developers
-OR-
One sneaky ` ` boi?

On windows default path to dotnet are `C:\Program Files`. Without quoting the
the command if it contains a space, it's going to break Windows builds that
install in `C:\Program Files`.
2021-01-08 13:53:42 +01:00
Paul
9b92bcf911 intermediate fix to garantuee people with dotnet in path to launch commpilerobustxaml 2021-01-05 12:15:03 +01:00
metalgearsloth
86e34ea27b Fix enum caching (#1485)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-01-05 16:17:20 +11:00
Clyybber
62cf778958 Fix build when dotnet is not in the PATH (#1483) 2021-01-04 20:25:43 +01:00
py01
5d667e44c3 Replaces AnchoredChanged C# event with a ComponentMessage (#1482)
Co-authored-by: py01 <pyronetics01@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-01-04 10:16:05 +01:00
metalgearsloth
6d84b8741c Cache enum references (#1480)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-01-03 03:45:02 +01:00
DrSmugleaf
d08ca59b75 Make RobustUnitTest register content cvars and set _isServer (#1481) 2021-01-03 03:44:12 +01:00
Pieter-Jan Briers
f06b046c1c Fix Content-Type not being set for status API. 2021-01-03 03:42:21 +01:00
Vera Aguilera Puerto
bb412a6906 Entity Timer SpawnTimer throws if entity is deleted. 2021-01-01 15:45:13 +01:00
Radrark
1baee3004c Adds new keybind property: AllowSubCombs (#1473)
Co-authored-by: Radrark <null>
2020-12-29 15:59:34 +01:00
Pieter-Jan Briers
f18068c13a Update NetSerializer submodule to improve build times 2020-12-29 15:59:13 +01:00
Pieter-Jan Briers
0ba00a1845 Gcf command to mess with LOH compaction. 2020-12-29 14:10:15 +01:00
Pieter-Jan Briers
28caf0d74c Reduce allocations 2020-12-29 03:58:16 +01:00
Pieter-Jan Briers
ecbb32b70b Clean up various cases of failing to dispose file streams. 2020-12-29 03:58:16 +01:00
Pieter-Jan Briers
8e2a9cc597 Font atlasses are now dynamic and can render all glyphs in the font file. 2020-12-29 03:58:16 +01:00
Vera Aguilera Puerto
a7eb8201c9 Fix MIDI soundfont load error on non-rooted paths. 2020-12-26 22:05:17 +01:00
Pieter-Jan Briers
1f95fe6782 Fix XamlIL problems when publishing 2020-12-25 18:34:19 +01:00
DrSmugleaf
07c1f9e1af Replace MaybeNullWhen(false) with NotNullWhen(true) in AppearanceComponent (#1461) 2020-12-25 15:17:54 +01:00
Vera Aguilera Puerto
826dce6659 Fix custom MIDI soundfont loading when mounting content from zip
Fixes #1466
2020-12-24 03:54:06 +01:00
Vera Aguilera Puerto
cdf714f3ba Fix stereo ogg audio not playing correctly.
Only half of the samples were being read.
2020-12-23 16:21:42 +01:00
Pieter-Jan Briers
671ca7959c Throw debug info if the RichTextEntry assert fails. 2020-12-23 15:12:47 +01:00
Pieter-Jan Briers
b7a1345d3a XAML compilation improvements.
Prevent double XAML-ifying causing corrupt dlls.

Use MSBuild dependencies to reduce unecessary xamlil builds.
2020-12-23 12:01:35 +01:00
Pieter-Jan Briers
835b6ebdba Probably fix build forgetting that nuget exists. 2020-12-21 12:14:28 +01:00
Pieter-Jan Briers
0ecabd6553 Fix XamlIL locking build. 2020-12-21 12:13:45 +01:00
Pieter-Jan Briers
feaa69f825 Fix build of injector. 2020-12-21 04:05:34 +01:00
Pieter-Jan Briers
857904a3d9 Update dependencies of name generator. 2020-12-21 04:05:25 +01:00
Pieter-Jan Briers
0b37418477 Fix injectors UsingTask. 2020-12-21 03:31:11 +01:00
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
126 changed files with 4259 additions and 884 deletions

View File

@@ -11,14 +11,14 @@
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '30 18 * * 6'
#on:
# push:
# branches: [ master ]
# pull_request:
# # The branches below must be a subset of the branches above
# branches: [ master ]
# schedule:
# - cron: '30 18 * * 6'
jobs:
analyze:
@@ -38,12 +38,12 @@ jobs:
uses: actions/checkout@v2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.100
- name: Build
run: dotnet build

6
.gitmodules vendored
View File

@@ -4,6 +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

63
MSBuild/XamlIL.targets Normal file
View File

@@ -0,0 +1,63 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
<PropertyGroup>
<RobustUseExternalMSBuild>true</RobustUseExternalMSBuild>
<_RobustUseExternalMSBuild>$(RobustUseExternalMSBuild)</_RobustUseExternalMSBuild>
<_RobustUseExternalMSBuild Condition="'$(_RobustForceInternalMSBuild)' == 'true'">false</_RobustUseExternalMSBuild>
</PropertyGroup>
<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
Condition="'$(_RobustUseExternalMSBuild)' != 'true' And $(DesignTimeBuild) != true"
TaskName="CompileRobustXamlTask"
AssemblyFile="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(Configuration)\netstandard2.0\Robust.Client.Injectors.dll"/>
<Target
Name="CompileRobustXaml"
Condition="Exists('@(IntermediateAssembly)')"
AfterTargets="AfterCompile"
Inputs="@(IntermediateAssembly);@(ReferencePathWithRefAssemblies)"
Outputs="$(IntermediateOutputPath)XAML/doot">
<PropertyGroup>
<RobustXamlReferencesTemporaryFilePath Condition="'$(RobustXamlReferencesTemporaryFilePath)' == ''">$(IntermediateOutputPath)XAML/references</RobustXamlReferencesTemporaryFilePath>
<RobustXamlOriginalCopyFilePath Condition="'$(RobustXamlOriginalCopyFilePath)' == ''">$(IntermediateOutputPath)XAML/original.dll</RobustXamlOriginalCopyFilePath>
</PropertyGroup>
<WriteLinesToFile
Condition="'$(_RobustForceInternalMSBuild)' != 'true'"
File="$(RobustXamlReferencesTemporaryFilePath)"
Lines="@(ReferencePathWithRefAssemblies)"
Overwrite="true"/>
<!--
UpdateBuildIndicator is done so that we can use MSBuild Inputs and Outputs on the target
to avoid unecessary execution of this target
Saves compile time if e.g. ONLY Robust.Client changes (Content.Client doesn't have to re-xaml).
-->
<CompileRobustXamlTask
Condition="'$(_RobustUseExternalMSBuild)' != 'true'"
AssemblyFile="@(IntermediateAssembly)"
ReferencesFilePath="$(RobustXamlReferencesTemporaryFilePath)"
OriginalCopyPath="$(RobustXamlOriginalCopyFilePath)"
ProjectDirectory="$(MSBuildProjectDirectory)"
AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)"
SignAssembly="$(SignAssembly)"
DelaySign="$(DelaySign)"
UpdateBuildIndicator="$(IntermediateOutputPath)XAML/doot"/>
<PropertyGroup>
<DOTNET_HOST_PATH Condition="'$(DOTNET_HOST_PATH)' == ''">dotnet</DOTNET_HOST_PATH>
</PropertyGroup>
<Exec
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
Command="&quot;$(DOTNET_HOST_PATH)&quot; msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
</Target>
</Project>

1
ManagedHttpListener Submodule

Submodule ManagedHttpListener added at f2aa590fec

View File

@@ -17,6 +17,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
// On net472, we rely on Mono's DllMap for this. See the .dll.config file.
NativeLibrary.SetDllImportResolver(typeof(GLFWNative).Assembly, (name, assembly, path) =>
{
// Please keep in sync with what Robust.Shared/DllMapHelper.cs does.
if (name != "glfw3.dll")
{
return IntPtr.Zero;

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,101 @@
using System;
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);
}
if (!string.IsNullOrEmpty(UpdateBuildIndicator))
{
if (!File.Exists(UpdateBuildIndicator))
{
File.Create(UpdateBuildIndicator).Dispose();
}
else
{
File.SetLastWriteTime(UpdateBuildIndicator, DateTime.Now);
}
}
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 UpdateBuildIndicator { 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>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</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,327 @@
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;
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
{
// If this type exists, the assembly has already been processed by us.
// Do not run again, it would corrupt the file.
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
// but better safe than sorry eh?
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
return (true, false);
}
var compileRes = CompileCore(engine, typeSystem);
if (compileRes == null)
return (true, false);
if (compileRes == false)
return (false, false);
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
if (!string.IsNullOrWhiteSpace(strongNameKey))
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
asm.Write(output, writerParameters);
return (true, true);
}
static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
{
var asm = typeSystem.TargetAssemblyDefinition;
var embrsc = new EmbeddedResources(asm);
if (embrsc.Resources.Count(CheckXamlName) == 0)
// Nothing to do
return null;
var xamlLanguage = new XamlLanguageTypeMappings(typeSystem)
{
XmlnsAttributes =
{
typeSystem.GetType("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" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" 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

@@ -104,7 +104,7 @@ namespace Robust.Client.Audio.Midi
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
private const string FallbackSoundfont = "/Resources/Midi/fallback.sf2";
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
@@ -207,13 +207,10 @@ namespace Robust.Client.Audio.Midi
var renderer = new MidiRenderer(_settings!, soundfontLoader);
foreach (var file in _resourceManager.ContentFindFiles(new ResourcePath("/Audio/MidiCustom/")))
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
{
if (file.Extension != "sf2" && file.Extension != "dls") continue;
if (_resourceManager.TryGetDiskFilePath(file, out var path))
{
renderer.LoadSoundfont(path);
}
renderer.LoadSoundfont(file.ToString());
}
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
@@ -382,10 +379,18 @@ namespace Robust.Client.Audio.Midi
public override IntPtr Open(string filename)
{
Stream? stream;
if (filename.StartsWith("/Resources/"))
if (string.IsNullOrEmpty(filename))
{
if (!IoCManager.Resolve<IResourceCache>().TryContentFileRead(filename.Substring(10), out stream))
return IntPtr.Zero;
}
Stream? stream;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resourcePath = new ResourcePath(filename);
if (resourcePath.IsRooted && resourceCache.ContentFileExists(filename))
{
if (!resourceCache.TryContentFileRead(filename, out stream))
return IntPtr.Zero;
}
else if (File.Exists(filename))

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using Robust.Client.Interfaces;
using Robust.Client.Interfaces.Debugging;
@@ -7,8 +7,8 @@ using Robust.Client.Interfaces.GameStates;
using Robust.Client.Interfaces.Utility;
using Robust.Client.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing;
@@ -25,7 +25,7 @@ namespace Robust.Client
{
[Dependency] private readonly IClientNetManager _net = default!;
[Dependency] private readonly IPlayerManager _playMan = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly INetConfigurationManager _configManager = default!;
[Dependency] private readonly IClientEntityManager _entityManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
@@ -50,9 +50,7 @@ namespace Robust.Client
/// <inheritdoc />
public void Initialize()
{
_net.RegisterNetMessage<MsgServerInfo>(MsgServerInfo.NAME, HandleServerInfo);
_net.RegisterNetMessage<MsgSetTickRate>(MsgSetTickRate.NAME, HandleSetTickRate);
_net.RegisterNetMessage<MsgServerInfoReq>(MsgServerInfoReq.NAME);
_net.Connected += OnConnected;
_net.ConnectFailed += OnConnectFailed;
_net.Disconnect += OnNetDisconnect;
@@ -98,9 +96,44 @@ namespace Robust.Client
private void OnConnected(object? sender, NetChannelArgs args)
{
// request base info about the server
var msgInfo = _net.CreateNetMessage<MsgServerInfoReq>();
_net.ClientSendMessage(msgInfo);
_configManager.SyncWithServer();
_configManager.ReceivedInitialNwVars += OnReceivedClientData;
}
private void OnReceivedClientData(object? sender, EventArgs e)
{
_configManager.ReceivedInitialNwVars -= OnReceivedClientData;
var info = GameInfo;
var serverName = _configManager.GetCVar<string>("game.hostname");
if (info == null)
{
GameInfo = info = new ServerInfo(serverName);
}
else
{
info.ServerName = serverName;
}
var maxPlayers = _configManager.GetCVar<int>("game.maxplayers");
info.ServerMaxPlayers = maxPlayers;
var tickrate = _configManager.GetCVar<int>("net.tickrate");
info.TickRate = (byte) tickrate;
_timing.TickRate = (byte) tickrate;
Logger.InfoS("client", $"Tickrate changed to: {tickrate}");
var userName = _net.ServerChannel!.UserName;
var userId = _net.ServerChannel.UserId;
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString());
// start up player management
_playMan.Startup(_net.ServerChannel!);
_playMan.LocalPlayer!.UserId = userId;
_playMan.LocalPlayer.Name = userName;
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
}
/// <summary>
@@ -152,6 +185,7 @@ namespace Robust.Client
LastDisconnectReason = args.Reason;
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
_entityManager.Shutdown();
@@ -160,36 +194,6 @@ namespace Robust.Client
Reset();
}
private void HandleServerInfo(MsgServerInfo msg)
{
var info = GameInfo;
if (info == null)
{
GameInfo = info = new ServerInfo(msg.ServerName);
}
else
{
info.ServerName = msg.ServerName;
}
info.ServerMaxPlayers = msg.ServerMaxPlayers;
info.TickRate = msg.TickRate;
_timing.TickRate = msg.TickRate;
Logger.InfoS("client", $"Tickrate changed to: {msg.TickRate}");
var userName = msg.MsgChannel.UserName;
var userId = msg.MsgChannel.UserId;
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString());
// start up player management
_playMan.Startup(_net.ServerChannel!);
_playMan.LocalPlayer!.UserId = userId;
_playMan.LocalPlayer.Name = userName;
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
}
private void HandleSetTickRate(MsgSetTickRate message)
{
_timing.TickRate = message.NewTickRate;

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

@@ -0,0 +1,72 @@
using JetBrains.Annotations;
using Robust.Client.Interfaces.Console;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
internal sealed class AddCompCommand : IConsoleCommand
{
public string Command => "addcompc";
public string Description => "Adds a component to an entity on the client";
public string Help => "addcompc <uid> <componentName>";
public bool Execute(IDebugConsole shell, params string[] args)
{
if (args.Length != 2)
{
shell.AddLine("Wrong number of arguments");
return false;
}
var entityUid = EntityUid.Parse(args[0]);
var componentName = args[1];
var compManager = IoCManager.Resolve<IComponentManager>();
var compFactory = IoCManager.Resolve<IComponentFactory>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var entity = entityManager.GetEntity(entityUid);
var component = (Component) compFactory.GetComponent(componentName);
component.Owner = entity;
compManager.AddComponent(entity, component);
return false;
}
}
[UsedImplicitly]
internal sealed class RemoveCompCommand : IConsoleCommand
{
public string Command => "rmcompc";
public string Description => "Removes a component from an entity.";
public string Help => "rmcompc <uid> <componentName>";
public bool Execute(IDebugConsole shell, string[] args)
{
if (args.Length != 2)
{
shell.AddLine("Wrong number of arguments");
return false;
}
var entityUid = EntityUid.Parse(args[0]);
var componentName = args[1];
var compManager = IoCManager.Resolve<IComponentManager>();
var compFactory = IoCManager.Resolve<IComponentFactory>();
var registration = compFactory.GetRegistration(componentName);
compManager.RemoveComponent(entityUid, registration.Type);
return false;
}
}
}

View File

@@ -781,6 +781,21 @@ namespace Robust.Client.Console.Commands
}
}
internal class GcFullCommand : IConsoleCommand
{
public string Command => "gcf";
public string Description => "Run the GC, fully, compacting LOH and everything.";
public string Help => "gcf";
public bool Execute(IDebugConsole console, params string[] args)
{
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Forced, true, true);
return false;
}
}
internal class GcModeCommand : IConsoleCommand
{

View File

@@ -0,0 +1,69 @@
#if !FULL_RELEASE
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Robust.Client.Interfaces.Console;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
internal sealed class LauncherAuthCommand : IConsoleCommand
{
public string Command => "launchauth";
public string Description => "Load authentication tokens from launcher data to aid in testing of live servers";
public string Help => "launchauth [account name]";
public bool Execute(IDebugConsole console, params string[] args)
{
var wantName = args.Length > 0 ? args[0] : null;
var basePath = Path.GetDirectoryName(UserDataDir.GetUserDataDir())!;
var cfgPath = Path.Combine(basePath, "launcher", "launcher_config.json");
var data = JsonSerializer.Deserialize<LauncherConfig>(File.ReadAllText(cfgPath))!;
var login = wantName != null
? data.Logins.FirstOrDefault(p => p.Username == wantName)
: data.Logins.FirstOrDefault();
if (login == null)
{
console.AddLine("Unable to find a matching login");
return false;
}
var token = login.Token.Token;
var userId = login.UserId;
var cfg = IoCManager.Resolve<IConfigurationManagerInternal>();
cfg.SetSecureCVar(CVars.AuthUserId, userId);
cfg.SetSecureCVar(CVars.AuthToken, token);
return false;
}
private sealed class LauncherConfig
{
[JsonInclude] [JsonPropertyName("logins")]
public LauncherLogin[] Logins = default!;
}
private sealed class LauncherLogin
{
[JsonInclude] public string Username = default!;
[JsonInclude] public string UserId = default!;
[JsonInclude] public LauncherToken Token = default!;
}
private sealed class LauncherToken
{
[JsonInclude] public string Token = default!;
}
}
}
#endif

View File

@@ -0,0 +1,35 @@
using JetBrains.Annotations;
using Robust.Client.Interfaces.Console;
using Robust.Client.Interfaces.Input;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
public class SetInputContextCommand : IConsoleCommand
{
public string Command => "setinputcontext";
public string Description => "Sets the active input context.";
public string Help => "setinputcontext <context>";
public bool Execute(IDebugConsole console, params string[] args)
{
if (args.Length != 1)
{
console.AddLine("Invalid number of arguments!");
return false;
}
var inputMan = IoCManager.Resolve<IInputManager>();
if (!inputMan.Contexts.Exists(args[0]))
{
console.AddLine("Context not found!");
return false;
}
inputMan.Contexts.SetActiveContext(args[0]);
return false;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
@@ -164,6 +164,7 @@ namespace Robust.Client
_userInterfaceManager.Initialize();
_networkManager.Initialize(false);
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();

View File

@@ -48,11 +48,21 @@ namespace Robust.Client.GameObjects.Components.Animations
return;
}
List<string>? toRemove = null;
// TODO: Get rid of this ToArray() allocation.
foreach (var (key, playback) in _playingAnimations.ToArray())
{
var keep = UpdatePlayback(Owner, playback, frameTime);
if (!keep)
{
toRemove ??= new List<string>();
toRemove.Add(key);
}
}
if (toRemove != null)
{
foreach (var key in toRemove)
{
_playingAnimations.Remove(key);
AnimationCompleted?.Invoke(key);

View File

@@ -54,17 +54,17 @@ namespace Robust.Client.GameObjects
return (T) data[key];
}
public override bool TryGetData<T>(Enum key, [MaybeNullWhen(false)] out T data)
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
public override bool TryGetData<T>(string key, [MaybeNullWhen(false)] out T data)
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
internal bool TryGetData<T>(object key, [MaybeNullWhen(false)] out T data)
internal bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
{
if (this.data.TryGetValue(key, out var dat))
{
@@ -72,7 +72,7 @@ namespace Robust.Client.GameObjects
return true;
}
data = default;
data = default!;
return false;
}

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

@@ -991,6 +991,9 @@ namespace Robust.Client.GameObjects
var mRotation = Matrix3.CreateRotation(angle);
Matrix3.Multiply(ref mRotation, ref mOffset, out var transform);
// Only apply scale if needed.
if(!Scale.EqualsApprox(Vector2.One)) transform.Multiply(Matrix3.CreateScale(Scale));
transform.Multiply(worldTransform);
RenderInternal(drawingHandle, worldRotation, overrideDirection, transform);

View File

@@ -128,7 +128,8 @@ namespace Robust.Client.GameObjects.EntitySystems
if (!entity.TryGetComponent(out InputComponent? inputComp))
{
Logger.DebugS("input.context", $"AttachedEnt has no InputComponent: entId={entity.Uid}, entProto={entity.Prototype}");
Logger.DebugS("input.context", $"AttachedEnt has no InputComponent: entId={entity.Uid}, entProto={entity.Prototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
return;
}
@@ -138,7 +139,8 @@ namespace Robust.Client.GameObjects.EntitySystems
}
else
{
Logger.ErrorS("input.context", $"Unknown context: entId={entity.Uid}, entProto={entity.Prototype}, context={inputComp.ContextName}");
Logger.ErrorS("input.context", $"Unknown context: entId={entity.Uid}, entProto={entity.Prototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
}
}

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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Client.GameObjects.EntitySystems;
using Robust.Client.Interfaces;
@@ -11,6 +11,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
using Robust.Client.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.Configuration;
@@ -41,7 +42,7 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly INetConfigurationManager _config = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
@@ -57,7 +58,8 @@ namespace Robust.Client.GameStates
public bool Predicting { get; private set; }
public int PredictSize { get; private set; }
public int PredictTickBias { get; private set; }
public float PredictLagBias { get; private set; }
public int StateBufferMergeThreshold { get; private set; }
@@ -82,14 +84,16 @@ namespace Robust.Client.GameStates
_config.OnValueChanged(CVars.NetInterpRatio, i => _processor.InterpRatio = i, true);
_config.OnValueChanged(CVars.NetLogging, b => _processor.Logging = b, true);
_config.OnValueChanged(CVars.NetPredict, b => Predicting = b, true);
_config.OnValueChanged(CVars.NetPredictSize, i => PredictSize = i, true);
_config.OnValueChanged(CVars.NetPredictTickBias, i => PredictTickBias = i, true);
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
_processor.InterpRatio = _config.GetCVar(CVars.NetInterpRatio);
_processor.Logging = _config.GetCVar(CVars.NetLogging);
Predicting = _config.GetCVar(CVars.NetPredict);
PredictSize = _config.GetCVar(CVars.NetPredictSize);
PredictTickBias = _config.GetCVar(CVars.NetPredictTickBias);
PredictLagBias = _config.GetCVar(CVars.NetPredictLagBias);
}
/// <inheritdoc />
@@ -174,7 +178,7 @@ namespace Robust.Client.GameStates
var i = 0;
for (; i < applyCount; i++)
{
_timing.CurTick = _lastProcessedTick + 1;
_timing.LastRealTick = _timing.CurTick = _lastProcessedTick + 1;
// TODO: We could theoretically communicate with the GameStateProcessor better here.
// Since game states are sliding windows, it is possible that we need less than applyCount applies here.
@@ -256,9 +260,9 @@ namespace Robust.Client.GameStates
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
var ping = _network.ServerChannel!.Ping / 1000f; // seconds.
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictSize;
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
@@ -377,6 +381,7 @@ namespace Robust.Client.GameStates
private List<EntityUid> ApplyGameState(GameState curState, GameState? nextState)
{
_config.TickProcessMessages();
_mapManager.ApplyGameStatePre(curState.MapData);
var createdEntities = _entities.ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
nextState?.EntityStates);

View File

@@ -18,7 +18,7 @@ namespace Robust.Client.Graphics.Clyde
while (readSamples < totalSamples)
{
var read = vorbis.ReadSamples(buffer, readSamples * channels, (int)totalSamples - readSamples);
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
if (read == 0)
{
break;

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

@@ -140,7 +140,7 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
// Check on original format is NOT a bug, this is so srgb emulation works
textureObject = GenTexture(texture, size, format.ColorFormat == RTCF.Rgba8Srgb, name == null ? null : $"{name}-color");
textureObject = GenTexture(texture, size, format.ColorFormat == RTCF.Rgba8Srgb, name == null ? null : $"{name}-color", TexturePixelType.RenderTarget);
}
// Depth/stencil buffers.

View File

@@ -11,6 +11,9 @@ using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using PIF = OpenToolkit.Graphics.OpenGL4.PixelInternalFormat;
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
namespace Robust.Client.Graphics.Clyde
{
@@ -20,8 +23,7 @@ namespace Robust.Client.Graphics.Clyde
private ClydeTexture _stockTextureBlack = default!;
private ClydeTexture _stockTextureTransparent = default!;
private readonly Dictionary<ClydeHandle, LoadedTexture> _loadedTextures =
new();
private readonly Dictionary<ClydeHandle, LoadedTexture> _loadedTextures = new();
private readonly ConcurrentQueue<ClydeHandle> _textureDisposeQueue = new();
@@ -69,85 +71,129 @@ namespace Robust.Client.Graphics.Clyde
// Flip image because OpenGL reads images upside down.
var copy = FlipClone(image);
var texture = new GLHandle((uint) GL.GenTexture());
CheckGlError();
GL.BindTexture(TextureTarget.Texture2D, texture.Handle);
CheckGlError();
ApplySampleParameters(actualParams.SampleParameters);
PixelInternalFormat internalFormat;
PixelFormat pixelDataFormat;
PixelType pixelDataType;
bool isActuallySrgb = false;
if (pixelType == typeof(Rgba32))
{
// Note that if _hasGLSrgb is off, we import an sRGB texture as non-sRGB.
// Shaders are expected to compensate for this
internalFormat = (actualParams.Srgb && _hasGLSrgb) ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8;
isActuallySrgb = actualParams.Srgb;
pixelDataFormat = PixelFormat.Rgba;
pixelDataType = PixelType.UnsignedByte;
}
else if (pixelType == typeof(A8))
{
if (image.Width % 4 != 0 || image.Height % 4 != 0)
{
throw new ArgumentException("Alpha8 images must have multiple of 4 sizes.");
}
internalFormat = PixelInternalFormat.R8;
pixelDataFormat = PixelFormat.Red;
pixelDataType = PixelType.UnsignedByte;
unsafe
{
// TODO: Does it make sense to default to 1 for RGB parameters?
// It might make more sense to pass some options to change swizzling.
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.One);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.One);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.One);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.Red);
}
}
else if (pixelType == typeof(L8) && !actualParams.Srgb)
{
// Can only use R8 for L8 if sRGB is OFF.
// Because OpenGL doesn't provide sRGB single/dual channel image formats.
// Vulkan when?
if (copy.Width % 4 != 0 || copy.Height % 4 != 0)
{
throw new ArgumentException("L8 non-sRGB images must have multiple of 4 sizes.");
}
internalFormat = PixelInternalFormat.R8;
pixelDataFormat = PixelFormat.Red;
pixelDataType = PixelType.UnsignedByte;
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.Red);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.One);
}
else
{
throw new NotImplementedException($"Unable to handle pixel type '{pixelType.Name}'");
}
var texture = CreateBaseTextureInternal<T>(image.Width, image.Height, actualParams, name);
unsafe
{
var span = copy.GetPixelSpan();
fixed (T* ptr = span)
{
GL.TexImage2D(TextureTarget.Texture2D, 0, internalFormat, copy.Width, copy.Height, 0,
pixelDataFormat, pixelDataType, (IntPtr) ptr);
CheckGlError();
// Still bound.
DoTexUpload(copy.Width, copy.Height, actualParams.Srgb, ptr);
}
}
var pressureEst = EstPixelSize(internalFormat) * copy.Width * copy.Height;
return texture;
}
return GenTexture(texture, (copy.Width, copy.Height), isActuallySrgb, name, pressureEst);
public unsafe OwnedTexture CreateBlankTexture<T>(
Vector2i size,
string? name = null,
in TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>
{
var actualParams = loadParams ?? TextureLoadParameters.Default;
if (!_hasGLTextureSwizzle)
{
// Actually create RGBA32 texture if missing texture swizzle.
// This is fine (TexturePixelType that's stored) because all other APIs do the same.
if (typeof(T) == typeof(A8) || typeof(T) == typeof(L8))
{
return CreateBlankTexture<Rgba32>(size, name, loadParams);
}
}
var texture = CreateBaseTextureInternal<T>(
size.X, size.Y,
actualParams,
name);
// Texture still bound, run glTexImage2D with null data param to specify bounds.
DoTexUpload<T>(size.X, size.Y, actualParams.Srgb, null);
return texture;
}
private unsafe void DoTexUpload<T>(int width, int height, bool srgb, T* ptr) where T : unmanaged, IPixel<T>
{
if (sizeof(T) < 4)
{
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
CheckGlError();
}
var (pif, pf, pt) = PixelEnums<T>(srgb);
GL.TexImage2D(TextureTarget.Texture2D, 0, pif, width, height, 0, pf, pt, (IntPtr) ptr);
CheckGlError();
if (sizeof(T) < 4)
{
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
CheckGlError();
}
}
private ClydeTexture CreateBaseTextureInternal<T>(
int width, int height,
in TextureLoadParameters loadParams,
string? name = null)
where T : unmanaged, IPixel<T>
{
var texture = new GLHandle((uint) GL.GenTexture());
CheckGlError();
GL.BindTexture(TextureTarget.Texture2D, texture.Handle);
CheckGlError();
ApplySampleParameters(loadParams.SampleParameters);
var (pif, _, _) = PixelEnums<T>(loadParams.Srgb);
var pixelType = typeof(T);
var texPixType = GetTexturePixelType<T>();
var isActuallySrgb = false;
if (pixelType == typeof(Rgba32))
{
isActuallySrgb = loadParams.Srgb;
}
else if (pixelType == typeof(A8))
{
DebugTools.Assert(_hasGLTextureSwizzle);
// TODO: Does it make sense to default to 1 for RGB parameters?
// It might make more sense to pass some options to change swizzling.
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.One);
CheckGlError();
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.One);
CheckGlError();
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.One);
CheckGlError();
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.Red);
CheckGlError();
}
else if (pixelType == typeof(L8) && !loadParams.Srgb)
{
DebugTools.Assert(_hasGLTextureSwizzle);
// Can only use R8 for L8 if sRGB is OFF.
// Because OpenGL doesn't provide sRGB single/dual channel image formats.
// Vulkan when?
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int) All.Red);
CheckGlError();
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int) All.Red);
CheckGlError();
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int) All.Red);
CheckGlError();
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int) All.One);
CheckGlError();
}
else
{
throw new NotSupportedException($"Unable to handle pixel type '{pixelType.Name}'");
}
var pressureEst = EstPixelSize(pif) * width * height;
return GenTexture(texture, (width, height), isActuallySrgb, name, texPixType, pressureEst);
}
private void ApplySampleParameters(TextureSampleParameters? sampleParameters)
@@ -201,10 +247,30 @@ namespace Robust.Client.Graphics.Clyde
default:
throw new ArgumentOutOfRangeException();
}
CheckGlError();
}
private ClydeTexture GenTexture(GLHandle glHandle, Vector2i size, bool srgb, string? name, long memoryPressure=0)
private (PIF pif, PF pf, PT pt) PixelEnums<T>(bool srgb)
where T : unmanaged, IPixel<T>
{
return default(T) switch
{
// Note that if _hasGLSrgb is off, we import an sRGB texture as non-sRGB.
// Shaders are expected to compensate for this
Rgba32 => (srgb && _hasGLSrgb ? PIF.Srgb8Alpha8 : PIF.Rgba8, PF.Rgba, PT.UnsignedByte),
A8 or L8 => (PIF.R8, PF.Red, PT.UnsignedByte),
_ => throw new NotSupportedException("Unsupported pixel type."),
};
}
private ClydeTexture GenTexture(
GLHandle glHandle,
Vector2i size,
bool srgb,
string? name,
TexturePixelType pixType,
long memoryPressure = 0)
{
if (name != null)
{
@@ -222,7 +288,8 @@ namespace Robust.Client.Graphics.Clyde
Height = height,
IsSrgb = srgb,
Name = name,
MemoryPressure = memoryPressure
MemoryPressure = memoryPressure,
TexturePixelType = pixType
// TextureInstance = new WeakReference<ClydeTexture>(instance)
};
@@ -246,6 +313,97 @@ namespace Robust.Client.Graphics.Clyde
//GC.RemoveMemoryPressure(loadedTexture.MemoryPressure);
}
private unsafe void SetSubImage<T>(
ClydeTexture texture,
Vector2i dstTl,
Image<T> srcImage,
in UIBox2i srcBox)
where T : unmanaged, IPixel<T>
{
if (!_hasGLTextureSwizzle)
{
if (typeof(T) == typeof(A8))
{
SetSubImage(texture, dstTl, ApplyA8Swizzle((Image<A8>) (object) srcImage), srcBox);
}
if (typeof(T) == typeof(L8))
{
SetSubImage(texture, dstTl, ApplyL8Swizzle((Image<L8>) (object) srcImage), srcBox);
}
}
var loaded = _loadedTextures[texture.TextureId];
var pixType = GetTexturePixelType<T>();
if (pixType != loaded.TexturePixelType)
{
if (loaded.TexturePixelType == TexturePixelType.RenderTarget)
throw new InvalidOperationException("Cannot modify texture for render target directly.");
throw new InvalidOperationException("Mismatching pixel type for texture.");
}
if (loaded.Width < dstTl.X + srcBox.Width || loaded.Height < dstTl.Y + srcBox.Height)
throw new ArgumentOutOfRangeException(nameof(srcBox), "Destination rectangle out of bounds.");
if (srcBox.Left < 0 || srcBox.Top < 0 || srcBox.Right > srcImage.Width || srcBox.Bottom > srcImage.Height)
throw new ArgumentOutOfRangeException(nameof(srcBox), "Source rectangle out of bounds.");
if (sizeof(T) != 4)
{
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
CheckGlError();
}
// sRGB doesn't matter since that only changes the internalFormat, which we don't need here.
var (_, pf, pt) = PixelEnums<T>(srgb: false);
GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
CheckGlError();
var size = srcBox.Width * srcBox.Height;
var copyBuffer = size < 16 * 16 ? stackalloc T[size] : new T[size];
for (var y = 0; y < srcBox.Height; y++)
for (var x = 0; x < srcBox.Width; x++)
{
copyBuffer[(srcBox.Height - y - 1) * srcBox.Width + x] = srcImage[x + srcBox.Left, srcBox.Top + y];
}
fixed (T* aPtr = copyBuffer)
{
var dstY = loaded.Height - dstTl.Y - srcBox.Height;
GL.TexSubImage2D(
TextureTarget.Texture2D,
0,
dstTl.X, dstY,
srcBox.Width, srcBox.Height,
pf, pt,
(IntPtr) aPtr);
CheckGlError();
}
if (sizeof(T) != 4)
{
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
CheckGlError();
}
}
private static TexturePixelType GetTexturePixelType<T>() where T : unmanaged, IPixel<T>
{
return default(T) switch
{
Rgba32 => TexturePixelType.Rgba32,
L8 => TexturePixelType.L8,
A8 => TexturePixelType.A8,
_ => throw new NotSupportedException("Unsupported pixel type."),
};
}
private void LoadStockTextures()
{
var white = new Image<Rgba32>(1, 1);
@@ -350,10 +508,20 @@ namespace Robust.Client.Graphics.Clyde
public bool IsSrgb;
public string? Name;
public long MemoryPressure;
public TexturePixelType TexturePixelType;
public Vector2i Size => (Width, Height);
// public WeakReference<ClydeTexture> TextureInstance;
}
private enum TexturePixelType : byte
{
RenderTarget = 0,
Rgba32,
A8,
L8,
}
private void FlushTextureDispose()
{
while (_textureDisposeQueue.TryDequeue(out var handle))
@@ -369,6 +537,11 @@ namespace Robust.Client.Graphics.Clyde
internal ClydeHandle TextureId { get; }
public override void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
{
_clyde.SetSubImage(this, topLeft, sourceImage, sourceRegion);
}
protected override void Dispose(bool disposing)
{
if (disposing)

View File

@@ -49,6 +49,7 @@ namespace Robust.Client.Graphics.Clyde
public string GetKeyName(Keyboard.Key key) => string.Empty;
public string GetKeyNameScanCode(int scanCode) => string.Empty;
public int GetKeyScanCode(Keyboard.Key key) => default;
public void Shutdown()
{
// Nada.
@@ -77,8 +78,8 @@ namespace Robust.Client.Graphics.Clyde
public override event Action<WindowResizedEventArgs> OnWindowResized
{
add {}
remove {}
add { }
remove { }
}
public void Render()
@@ -111,6 +112,15 @@ namespace Robust.Client.Graphics.Clyde
return new DummyTexture((image.Width, image.Height));
}
public OwnedTexture CreateBlankTexture<T>(
Vector2i size,
string? name = null,
in TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>
{
return new DummyTexture(size);
}
public IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters = null, string? name = null)
{
@@ -174,7 +184,7 @@ namespace Robust.Client.Graphics.Clyde
return DummyAudioSource.Instance;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
@@ -296,6 +306,11 @@ namespace Robust.Client.Graphics.Clyde
public DummyTexture(Vector2i size) : base(size)
{
}
public override void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
{
// Just do nothing on mutate.
}
}
private sealed class DummyShaderInstance : ShaderInstance
@@ -434,7 +449,8 @@ namespace Robust.Client.Graphics.Clyde
{
}
public IRenderTexture RenderTarget { get; } = new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
public IRenderTexture RenderTarget { get; } =
new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
public IEye? Eye { get; set; }
public Vector2i Size { get; }

View File

@@ -1,24 +1,16 @@
// xy: A, zw: B
varying highp vec4 fragPos;
// x: actual angle, y: horizontal (1) / vertical (-1)
varying highp vec2 fragAngle;
// x: Angle being queried, y: Angle of closest point of line (is of 90-degree angle to line angle), z: Distance at y
varying highp vec3 fragControl;
void main()
{
// Stuff that needs to be inferred to avoid interpolation issues.
highp vec2 rayNormal = vec2(cos(fragAngle.x), -sin(fragAngle.x));
// Depth calculation accounting for interpolation.
highp float dist;
if (fragAngle.y > 0.0) {
// Line is horizontal
dist = abs(fragPos.y / rayNormal.y);
} else {
// Line is vertical
dist = abs(fragPos.x / rayNormal.x);
}
// Thanks to Radrark for finding this for me. There's also a useful diagram, but this is text, so:
// r = p / cos(theta - phi)
// r: Distance to line *given angle theta*
// p: Distance to closest point of line
// theta: Angle being queried
// phi: Angle of closest point of line - inherently on 90-degree angle to line angle
highp float dist = abs(fragControl.z / cos(fragControl.x - fragControl.y));
// Main body.
#ifdef HAS_DFDX

View File

@@ -11,10 +11,8 @@ attribute vec4 aPos;
// x: deflection(0=A/1=B) y: height
attribute vec2 subVertex;
// xy: A, zw: B
varying vec4 fragPos;
// x: actual angle, y: horizontal (1) / vertical (-1)
varying vec2 fragAngle;
// x: actual angle, y: line angle + 90 degrees, z: Distance at y
varying vec3 fragControl;
// Note: This is *not* the standard projectionMatrix!
uniform vec2 shadowLightCentre;
@@ -70,8 +68,19 @@ void main()
}
}
fragPos = vec4(pA, pB);
fragAngle = vec2(mix(xA, xB, subVertex.x), abs(pA.x - pB.x) - abs(pA.y - pB.y));
float targetAngle = mix(xA, xB, subVertex.x);
// Calculate the necessary control data for the fragment shader.
vec2 lineNormal = pB - pA; // hypothetical: <- would have negative X, zero Y
lineNormal /= length(lineNormal);
fragControl = vec3(
// Angle
targetAngle,
// Angle Out
atan(lineNormal.x, lineNormal.y),
// Distance @ Angle Out
dot(vec2(lineNormal.y, -lineNormal.x), pA)
);
// Depth divide MUST be implemented here no matter what,
// because GLES SL 1.00 doesn't have gl_FragDepth.
@@ -79,5 +88,5 @@ void main()
// and we don't really need to have correction
float zbufferDepth = 1.0 - (1.0 / (length(mix(pA, pB, subVertex.x)) + DEPTH_ZBUFFER_PREDIV_BIAS));
gl_Position = vec4(mix(xA, xB, subVertex.x) / PI, mix(1.0, -1.0, subVertex.y), zbufferDepth, 1.0);
gl_Position = vec4(targetAngle / PI, mix(1.0, -1.0, subVertex.y), zbufferDepth, 1.0);
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Interfaces.Graphics;
using Robust.Client.Utility;
@@ -17,7 +18,11 @@ namespace Robust.Client.Graphics
{
internal sealed class FontManager : IFontManagerInternal
{
private const int SheetWidth = 256;
private const int SheetHeight = 256;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IClyde _clyde = default!;
private uint BaseFontDPI;
@@ -51,8 +56,7 @@ namespace Robust.Client.Graphics
return instance;
}
var glyphMap = _generateGlyphMap(fontFaceHandle.Face);
instance = new FontInstanceHandle(this, size, glyphMap, fontFaceHandle);
instance = new FontInstanceHandle(this, size, fontFaceHandle);
_loadedInstances.Add((fontFaceHandle, size), instance);
return instance;
@@ -67,135 +71,128 @@ namespace Robust.Client.Graphics
var descent = -ftFace.Size.Metrics.Descender.ToInt32();
var lineHeight = ftFace.Size.Metrics.Height.ToInt32();
var (atlas, metricsMap) = _generateAtlas(instance, scale);
var data = new ScaledFontData(ascent, descent, ascent + descent, lineHeight);
return new ScaledFontData(metricsMap, ascent, descent, ascent + descent, lineHeight, atlas);
return data;
}
private (FontTextureAtlas, Dictionary<uint, CharMetrics> metricsMap)
_generateAtlas(FontInstanceHandle instance, float scale)
private void CacheGlyph(FontInstanceHandle instance, ScaledFontData scaled, float scale, uint glyph)
{
// TODO: This could use a better box packing algorithm.
// Right now we treat each glyph bitmap as having the max size among all glyphs.
// So we can divide the atlas into equal-size rectangles.
// This wastes a lot of space though because there's a lot of tiny glyphs.
// Check if already cached.
if (scaled.AtlasData.ContainsKey(glyph))
return;
var face = instance.FaceHandle.Face;
face.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
var maxGlyphSize = Vector2i.Zero;
var count = 0;
var glyphMetrics = face.Glyph.Metrics;
var metrics = new CharMetrics(glyphMetrics.HorizontalBearingX.ToInt32(),
glyphMetrics.HorizontalBearingY.ToInt32(),
glyphMetrics.HorizontalAdvance.ToInt32(),
glyphMetrics.Width.ToInt32(),
glyphMetrics.Height.ToInt32());
var metricsMap = new Dictionary<uint, CharMetrics>();
foreach (var glyph in instance.GlyphMap.Values)
using var bitmap = face.Glyph.Bitmap;
if (bitmap.Pitch < 0)
{
if (metricsMap.ContainsKey(glyph))
{
continue;
}
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
var glyphMetrics = face.Glyph.Metrics;
var metrics = new CharMetrics(glyphMetrics.HorizontalBearingX.ToInt32(),
glyphMetrics.HorizontalBearingY.ToInt32(),
glyphMetrics.HorizontalAdvance.ToInt32(),
glyphMetrics.Width.ToInt32(),
glyphMetrics.Height.ToInt32());
metricsMap.Add(glyph, metrics);
maxGlyphSize = Vector2i.ComponentMax(maxGlyphSize,
new Vector2i(face.Glyph.Bitmap.Width, face.Glyph.Bitmap.Rows));
count += 1;
throw new NotImplementedException();
}
// Make atlas.
// This is the same algorithm used for RSIs. Tries to keep width and height as close as possible,
// but preferring to increase width if necessary.
var atlasEntriesHorizontal = (int) Math.Ceiling(Math.Sqrt(count));
var atlasEntriesVertical =
(int) Math.Ceiling(count / (float) atlasEntriesHorizontal);
var atlasDimX =
(int) Math.Ceiling(atlasEntriesHorizontal * maxGlyphSize.X / 4f) * 4;
var atlasDimY =
(int) Math.Ceiling(atlasEntriesVertical * maxGlyphSize.Y / 4f) * 4;
using (var atlas = new Image<A8>(atlasDimX, atlasDimY))
if (bitmap.Pitch != 0)
{
var atlasRegions = new Dictionary<uint, UIBox2>();
count = 0;
foreach (var glyph in metricsMap.Keys)
Image<A8> img;
switch (bitmap.PixelMode)
{
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
var bitmap = face.Glyph.Bitmap;
if (bitmap.Pitch == 0)
case PixelMode.Mono:
{
count += 1;
continue;
img = MonoBitMapToImage(bitmap);
break;
}
if (bitmap.Pitch < 0)
case PixelMode.Gray:
{
ReadOnlySpan<A8> span;
unsafe
{
span = new ReadOnlySpan<A8>((void*) bitmap.Buffer, bitmap.Pitch * bitmap.Rows);
}
img = new Image<A8>(bitmap.Width, bitmap.Rows);
span.Blit(
bitmap.Pitch,
UIBox2i.FromDimensions(0, 0, bitmap.Pitch, bitmap.Rows),
img,
(0, 0));
break;
}
case PixelMode.Gray2:
case PixelMode.Gray4:
case PixelMode.Lcd:
case PixelMode.VerticalLcd:
case PixelMode.Bgra:
throw new NotImplementedException();
}
var column = count % atlasEntriesHorizontal;
var row = count / atlasEntriesVertical;
var offsetX = column * maxGlyphSize.X;
var offsetY = row * maxGlyphSize.Y;
count += 1;
atlasRegions.Add(glyph, UIBox2i.FromDimensions(offsetX, offsetY, bitmap.Width, bitmap.Rows));
switch (bitmap.PixelMode)
{
case PixelMode.Mono:
{
using (var bitmapImage = MonoBitMapToImage(bitmap))
{
bitmapImage.Blit(new UIBox2i(0, 0, bitmapImage.Width, bitmapImage.Height), atlas,
(offsetX, offsetY));
}
break;
}
case PixelMode.Gray:
{
ReadOnlySpan<A8> span;
unsafe
{
span = new ReadOnlySpan<A8>((void*) bitmap.Buffer, bitmap.Pitch * bitmap.Rows);
}
span.Blit(bitmap.Pitch, UIBox2i.FromDimensions(0, 0, bitmap.Pitch, bitmap.Rows), atlas,
(offsetX, offsetY));
break;
}
case PixelMode.Gray2:
case PixelMode.Gray4:
case PixelMode.Lcd:
case PixelMode.VerticalLcd:
case PixelMode.Bgra:
throw new NotImplementedException();
default:
throw new ArgumentOutOfRangeException();
}
default:
throw new ArgumentOutOfRangeException();
}
var atlasDictionary = new Dictionary<uint, AtlasTexture>();
var texture = Texture.LoadFromImage(atlas, $"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}");
OwnedTexture sheet;
if (scaled.AtlasTextures.Count == 0)
sheet = GenSheet();
else
sheet = scaled.AtlasTextures[^1];
foreach (var (glyph, region) in atlasRegions)
var (sheetW, sheetH) = sheet.Size;
if (sheetW - scaled.CurSheetX < img.Width)
{
atlasDictionary.Add(glyph, new AtlasTexture(texture, region));
scaled.CurSheetX = 0;
scaled.CurSheetY = scaled.CurSheetMaxY;
}
return (new FontTextureAtlas(texture, atlasDictionary), metricsMap);
if (sheetH - scaled.CurSheetY < img.Height)
{
// Make new sheet.
scaled.CurSheetY = 0;
scaled.CurSheetX = 0;
scaled.CurSheetMaxY = 0;
sheet = GenSheet();
}
sheet.SetSubImage((scaled.CurSheetX, scaled.CurSheetY), img);
var atlasTexture = new AtlasTexture(
sheet,
UIBox2.FromDimensions(
scaled.CurSheetX,
scaled.CurSheetY,
bitmap.Width,
bitmap.Rows));
scaled.AtlasData.Add(glyph, atlasTexture);
scaled.CurSheetMaxY = Math.Max(scaled.CurSheetMaxY, scaled.CurSheetY + bitmap.Rows);
scaled.CurSheetX += bitmap.Width;
}
else
{
scaled.AtlasData.Add(glyph, null);
}
scaled.MetricsMap.Add(glyph, metrics);
OwnedTexture GenSheet()
{
var sheet = _clyde.CreateBlankTexture<A8>((SheetWidth, SheetHeight),
$"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}-sheet{scaled.AtlasTextures.Count}");
scaled.AtlasTextures.Add(sheet);
return sheet;
}
}
@@ -226,46 +223,6 @@ namespace Robust.Client.Graphics
return bitmapImage;
}
private Dictionary<char, uint> _generateGlyphMap(Face face)
{
var map = new Dictionary<char, uint>();
// TODO: Render more than extended ASCII, Cyrillic and Greek. somehow.
// Does it make sense to just render every glyph in the font?
// Render all the extended ASCII characters.
// Yeah I know "extended ASCII" isn't a real thing get off my back.
for (var i = 32u; i <= 255; i++)
{
_addGlyph(i, face, map);
}
// Render basic cyrillic.
for (var i = 0x0410u; i <= 0x044F; i++)
{
_addGlyph(i, face, map);
}
// Render greek.
for (var i = 0x03B1u; i <= 0x03C9; i++)
{
_addGlyph(i, face, map);
}
return map;
}
private static void _addGlyph(uint codePoint, Face face, Dictionary<char, uint> map)
{
var glyphIndex = face.GetCharIndex(codePoint);
if (glyphIndex == 0)
{
return;
}
map.Add((char) codePoint, glyphIndex);
}
private class FontFaceHandle : IFontFaceHandle
{
public Face Face { get; }
@@ -282,78 +239,84 @@ namespace Robust.Client.Graphics
public FontFaceHandle FaceHandle { get; }
public int Size { get; }
private readonly Dictionary<float, ScaledFontData> _scaledData = new();
public readonly IReadOnlyDictionary<char, uint> GlyphMap;
private readonly FontManager _fontManager;
public readonly Dictionary<Rune, uint> GlyphMap;
public FontInstanceHandle(FontManager fontManager, int size, IReadOnlyDictionary<char, uint> glyphMap,
FontFaceHandle faceHandle)
public FontInstanceHandle(FontManager fontManager, int size, FontFaceHandle faceHandle)
{
GlyphMap = new Dictionary<Rune, uint>();
_fontManager = fontManager;
Size = size;
GlyphMap = glyphMap;
FaceHandle = faceHandle;
}
public Texture? GetCharTexture(char chr, float scale)
public Texture? GetCharTexture(Rune codePoint, float scale)
{
var glyph = _getGlyph(chr);
var glyph = GetGlyph(codePoint);
if (glyph == 0)
{
return null;
}
var scaled = _getScaleDatum(scale);
scaled.Atlas.AtlasData.TryGetValue(glyph, out var texture);
var scaled = GetScaleDatum(scale);
_fontManager.CacheGlyph(this, scaled, scale, glyph);
scaled.AtlasData.TryGetValue(glyph, out var texture);
return texture;
}
public CharMetrics? GetCharMetrics(char chr, float scale)
public CharMetrics? GetCharMetrics(Rune codePoint, float scale)
{
var glyph = _getGlyph(chr);
var glyph = GetGlyph(codePoint);
if (glyph == 0)
{
return null;
}
var scaled = _getScaleDatum(scale);
var scaled = GetScaleDatum(scale);
_fontManager.CacheGlyph(this, scaled, scale, glyph);
return scaled.MetricsMap[glyph];
}
public int GetAscent(float scale)
{
var scaled = _getScaleDatum(scale);
var scaled = GetScaleDatum(scale);
return scaled.Ascent;
}
public int GetDescent(float scale)
{
var scaled = _getScaleDatum(scale);
var scaled = GetScaleDatum(scale);
return scaled.Descent;
}
public int GetHeight(float scale)
{
var scaled = _getScaleDatum(scale);
var scaled = GetScaleDatum(scale);
return scaled.Height;
}
public int GetLineHeight(float scale)
{
var scaled = _getScaleDatum(scale);
var scaled = GetScaleDatum(scale);
return scaled.LineHeight;
}
private uint _getGlyph(char chr)
private uint GetGlyph(Rune chr)
{
if (GlyphMap.TryGetValue(chr, out var glyph))
{
return glyph;
}
return 0;
// Check FreeType to see if it exists.
var index = FaceHandle.Face.GetCharIndex((uint) chr.Value);
GlyphMap.Add(chr, index);
return index;
}
private ScaledFontData _getScaleDatum(float scale)
private ScaledFontData GetScaleDatum(float scale)
{
if (_scaledData.TryGetValue(scale, out var datum))
{
@@ -368,37 +331,25 @@ namespace Robust.Client.Graphics
private class ScaledFontData
{
public ScaledFontData(IReadOnlyDictionary<uint, CharMetrics> metricsMap, int ascent, int descent,
int height, int lineHeight, FontTextureAtlas atlas)
public ScaledFontData(int ascent, int descent, int height, int lineHeight)
{
MetricsMap = metricsMap;
Ascent = ascent;
Descent = descent;
Height = height;
LineHeight = lineHeight;
Atlas = atlas;
}
public IReadOnlyDictionary<uint, CharMetrics> MetricsMap { get; }
public int Ascent { get; }
public int Descent { get; }
public int Height { get; }
public int LineHeight { get; }
public FontTextureAtlas Atlas { get; }
}
public readonly List<OwnedTexture> AtlasTextures = new();
public readonly Dictionary<uint, AtlasTexture?> AtlasData = new();
public readonly Dictionary<uint, CharMetrics> MetricsMap = new();
public readonly int Ascent;
public readonly int Descent;
public readonly int Height;
public readonly int LineHeight;
private class FontTextureAtlas
{
public FontTextureAtlas(Texture mainTexture, Dictionary<uint, AtlasTexture> atlasData)
{
MainTexture = mainTexture;
AtlasData = atlasData;
}
public Texture MainTexture { get; }
// Maps glyph index to atlas.
public Dictionary<uint, AtlasTexture> AtlasData { get; }
public int CurSheetX;
public int CurSheetY;
public int CurSheetMaxY;
}
}
}

View File

@@ -1,5 +1,7 @@
using System;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Robust.Client.Graphics
{
@@ -12,21 +14,42 @@ namespace Robust.Client.Graphics
{
}
/// <summary>
/// Modifies a sub area of the texture with new data.
/// </summary>
/// <param name="topLeft">The top left corner of the area to modify.</param>
/// <param name="sourceImage">The image from which to copy pixel data.</param>
/// <param name="sourceRegion">The rectangle inside <paramref name="sourceImage"/> from which to copy.</param>
/// <typeparam name="T">
/// The type of pixels being used.
/// This must match the type used when creating the texture.
/// </typeparam>
public abstract void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage, in UIBox2i sourceRegion)
where T : unmanaged, IPixel<T>;
/// <summary>
/// Modifies a sub area of the texture with new data.
/// </summary>
/// <param name="topLeft">The top left corner of the area to modify.</param>
/// <param name="sourceImage">The image to paste onto the texture.</param>
/// <typeparam name="T">
/// The type of pixels being used.
/// This must match the type used when creating the texture.
/// </typeparam>
public void SetSubImage<T>(Vector2i topLeft, Image<T> sourceImage)
where T : unmanaged, IPixel<T>
{
SetSubImage(topLeft, sourceImage, UIBox2i.FromDimensions(0, 0, sourceImage.Width, sourceImage.Height));
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
[Obsolete("Use Dispose() instead")]
public void Delete()
{
Dispose();
}
protected virtual void Dispose(bool disposing)
{
}
~OwnedTexture()

View File

@@ -31,7 +31,7 @@
}
}
},
"required": ["name","directions"] //'delays' is marked as optional in the spec
"required": ["name"] //'delays' is marked as optional in the spec
}
},
"properties": {

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -134,7 +134,8 @@ namespace Robust.Client.Input
Priority = p.Priority,
Type = p.BindingType,
CanFocus = p.CanFocus,
CanRepeat = p.CanRepeat
CanRepeat = p.CanRepeat,
AllowSubCombs = p.AllowSubCombs
}).ToArray();
var leaveEmpty = _modifiedKeyFunctions
@@ -203,6 +204,7 @@ namespace Robust.Client.Input
var bindsDown = new List<KeyBinding>();
var hasCanFocus = false;
var hasAllowSubCombs = false;
// bindings are ordered with larger combos before single key bindings so combos have priority.
foreach (var binding in _bindings)
@@ -221,12 +223,22 @@ namespace Robust.Client.Input
matchedCombo = binding.PackedKeyCombo;
bindsDown.Add(binding);
hasCanFocus |= binding.CanFocus;
hasAllowSubCombs |= binding.AllowSubCombs;
}
else if (PackedIsSubPattern(matchedCombo, binding.PackedKeyCombo))
{
// kill any lower level matches
UpBind(binding);
if (hasAllowSubCombs)
{
bindsDown.Add(binding);
}
else
{
// kill any lower level matches
UpBind(binding);
}
}
}
}
@@ -378,8 +390,8 @@ namespace Robust.Client.Input
{
for (var i = 0; i < 32; i += 8)
{
var key = (Key) (subPackedCombo.Packed >> i);
if (!PackedContainsKey(packedCombo, key))
var key = (Key) ((subPackedCombo.Packed >> i) & 0b_1111_1111);
if (key != Key.Unknown && !PackedContainsKey(packedCombo, key))
{
return false;
}
@@ -453,7 +465,7 @@ namespace Robust.Client.Input
public IKeyBinding RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
{
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false,
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false, false,
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
RegisterBinding(binding);
@@ -464,7 +476,7 @@ namespace Robust.Client.Input
public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true)
{
var binding = new KeyBinding(this, reg.Function, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
reg.AllowSubCombs, reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
RegisterBinding(binding, markModified);
@@ -619,12 +631,18 @@ namespace Robust.Client.Input
[ViewVariables]
public bool CanRepeat { get; internal set; }
/// <summary>
/// Whether the Bound Key Combination allows Sub Combinations of it to trigger.
/// </summary>
[ViewVariables]
public bool AllowSubCombs { get; internal set; }
[ViewVariables] public int Priority { get; internal set; }
public KeyBinding(InputManager inputManager, BoundKeyFunction function,
KeyBindingType bindingType,
Key baseKey,
bool canFocus, bool canRepeat, int priority, Key mod1 = Key.Unknown,
bool canFocus, bool canRepeat, bool allowSubCombs, int priority, Key mod1 = Key.Unknown,
Key mod2 = Key.Unknown,
Key mod3 = Key.Unknown)
{
@@ -632,6 +650,7 @@ namespace Robust.Client.Input
BindingType = bindingType;
CanFocus = canFocus;
CanRepeat = canRepeat;
AllowSubCombs = allowSubCombs;
Priority = priority;
_inputManager = inputManager;

View File

@@ -34,6 +34,29 @@ namespace Robust.Client.Interfaces.Graphics
Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>;
/// <summary>
/// Creates a blank texture of the specified parameters.
/// This texture can later be modified using <see cref="OwnedTexture.SetSubImage{T}"/>
/// </summary>
/// <param name="size">The size of the new texture, in pixels.</param>
/// <param name="name">A name for the texture that can show up in debugging tools like renderdoc.</param>
/// <param name="loadParams">
/// Load parameters for the texture describing stuff such as sample mode.
/// </param>
/// <typeparam name="T">
/// The type of pixels to "store" in the texture.
/// This is the same type you should pass to <see cref="OwnedTexture.SetSubImage{T}"/>,
/// and also determines how the texture is stored internally.
/// </typeparam>
/// <returns>
/// An owned, mutable texture object.
/// </returns>
OwnedTexture CreateBlankTexture<T>(
Vector2i size,
string? name = null,
in TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>;
IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters = null, string? name = null);

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Text;
using Robust.Client.Graphics;
namespace Robust.Client.Interfaces.Graphics
@@ -22,8 +23,13 @@ namespace Robust.Client.Interfaces.Graphics
internal interface IFontInstanceHandle
{
Texture? GetCharTexture(char chr, float scale);
CharMetrics? GetCharMetrics(char chr, float scale);
Texture? GetCharTexture(Rune codePoint, float scale);
Texture? GetCharTexture(char chr, float scale) => GetCharTexture((Rune) chr, scale);
CharMetrics? GetCharMetrics(Rune codePoint, float scale);
CharMetrics? GetCharMetrics(char chr, float scale) => GetCharMetrics((Rune) chr, scale);
int GetAscent(float scale);
int GetDescent(float scale);
int GetHeight(float scale);

View File

@@ -16,6 +16,7 @@ namespace Robust.Client.Interfaces.Input
bool CanFocus { get; }
bool CanRepeat { get; }
bool AllowSubCombs { get; }
int Priority { get; }
/// <summary>

View File

@@ -1,4 +1,4 @@
using Robust.Client.Input;
using Robust.Client.Input;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
@@ -16,6 +16,7 @@ namespace Robust.Client.Interfaces.Input
public int Priority;
public bool CanFocus;
public bool CanRepeat;
public bool AllowSubCombs;
public void ExposeData(ObjectSerializer serializer)
{
@@ -28,6 +29,7 @@ namespace Robust.Client.Interfaces.Input
serializer.DataField(ref Priority, "priority", 0);
serializer.DataField(ref CanFocus, "canFocus", false);
serializer.DataField(ref CanRepeat, "canRepeat", false);
serializer.DataField(ref AllowSubCombs, "allowSubCombs", false);
}
}
}

View File

@@ -21,8 +21,22 @@ namespace Robust.Client.Interfaces.UserInterface
/// </summary>
Stylesheet? Stylesheet { get; set; }
/// <summary>
/// A control can have "keyboard focus" separate from ControlFocused, obtained when calling
/// Control.GrabKeyboardFocus. Corresponding events in Control are KeyboardFocusEntered/Exited
/// </summary>
Control? KeyboardFocused { get; }
/// <summary>
/// A control gets "ControlFocused" when a mouse button (or any KeyBinding which has CanFocus = true) is
/// pressed down on the control. While it is focused, it will receive mouse hover events and the corresponding
/// keyup event if it still has focus when that occurs (it will NOT receive the keyup if focus has
/// been taken by another control). Focus is removed when a different control takes focus
/// (such as by pressing a different mouse button down over a different control) or when the keyup event
/// happens. When focus is lost on a control, it always fires Control.ControlFocusExited.
/// </summary>
Control? ControlFocused { get; }
LayoutContainer StateRoot { get; }
LayoutContainer WindowRoot { get; }

View File

@@ -98,9 +98,9 @@ namespace Robust.Client.Placement
public bool Eraser { get; private set; }
/// <summary>
/// The texture we use to show from our placement manager to represent the entity to place
/// The texture we use to show from our placement manager to represent the entity to place
/// </summary>
public IDirectionalTextureProvider? CurrentBaseSprite { get; set; }
public List<IDirectionalTextureProvider>? CurrentTextures { get; set; }
/// <summary>
/// Which of the placement orientations we are trying to place with
@@ -311,7 +311,7 @@ namespace Robust.Client.Placement
{
PlacementChanged?.Invoke(this, EventArgs.Empty);
Hijack = null;
CurrentBaseSprite = null;
CurrentTextures = null;
CurrentPrototype = null;
CurrentPermission = null;
CurrentMode = null;
@@ -555,7 +555,7 @@ namespace Robust.Client.Placement
{
var prototype = _prototypeManager.Index<EntityPrototype>(templateName);
CurrentBaseSprite = SpriteComponent.GetPrototypeIcon(prototype, ResourceCache);
CurrentTextures = SpriteComponent.GetPrototypeTextures(prototype, ResourceCache).ToList();
CurrentPrototype = prototype;
IsActive = true;
@@ -563,8 +563,9 @@ namespace Robust.Client.Placement
private void PreparePlacementTile()
{
CurrentBaseSprite = ResourceCache
.GetResource<TextureResource>(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png")).Texture;
CurrentTextures = new List<IDirectionalTextureProvider>
{ResourceCache
.GetResource<TextureResource>(new ResourcePath("/Textures/UserInterface/tilebuildoverlay.png")).Texture};
IsActive = true;
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.Graphics.ClientEye;
using Robust.Client.Graphics.Drawing;
@@ -28,9 +29,9 @@ namespace Robust.Client.Placement
public EntityCoordinates MouseCoords { get; set; }
/// <summary>
/// Texture resource to draw to represent the entity we are tryign to spawn
/// Texture resources to draw to represent the entity we are trying to spawn
/// </summary>
public Texture? SpriteToDraw { get; set; }
public List<Texture>? TexturesToDraw { get; set; }
/// <summary>
/// Color set to the ghost entity when it has a valid spawn position
@@ -85,12 +86,15 @@ namespace Robust.Client.Placement
public virtual void Render(DrawingHandleWorld handle)
{
if (SpriteToDraw == null)
if (TexturesToDraw == null)
{
SetSprite();
DebugTools.AssertNotNull(SpriteToDraw);
DebugTools.AssertNotNull(TexturesToDraw);
}
if (TexturesToDraw == null || TexturesToDraw.Count == 0)
return;
IEnumerable<EntityCoordinates> locationcollection;
switch (pManager.PlacementType)
{
@@ -108,13 +112,17 @@ namespace Robust.Client.Placement
break;
}
var size = SpriteToDraw!.Size;
var size = TexturesToDraw[0].Size;
foreach (var coordinate in locationcollection)
{
var worldPos = coordinate.ToMapPos(pManager.EntityManager);
var pos = worldPos - (size/(float)EyeManager.PixelsPerMeter) / 2f;
var color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
handle.DrawTexture(SpriteToDraw, pos, color);
foreach (var texture in TexturesToDraw)
{
handle.DrawTexture(texture, pos, color);
}
}
}
@@ -188,7 +196,10 @@ namespace Robust.Client.Placement
public void SetSprite()
{
SpriteToDraw = pManager.CurrentBaseSprite!.TextureFor(pManager.Direction);
if (pManager.CurrentTextures == null)
return;
TexturesToDraw = pManager.CurrentTextures.Select(o => o.TextureFor(pManager.Direction)).ToList();
}
/// <summary>

View File

@@ -13,12 +13,15 @@ namespace Robust.Client.ResourceManagement
public override void Load(IResourceCache cache, ResourcePath path)
{
if (!cache.ContentFileExists(path))
if (!cache.TryContentFileRead(path, out var stream))
{
throw new FileNotFoundException("Content file does not exist for font");
}
FontFaceHandle = IoCManager.Resolve<IFontManagerInternal>().Load(cache.ContentFileRead(path));
using (stream)
{
FontFaceHandle = IoCManager.Resolve<IFontManagerInternal>().Load(stream);
}
}
public VectorFont MakeDefault()

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.Utility;
@@ -83,7 +82,12 @@ namespace Robust.Client.ResourceManagement
{
// Load image from disk.
var texPath = path / (stateObject.StateId + ".png");
var image = Image.Load<Rgba32>(cache.ContentFileRead(texPath));
var stream = cache.ContentFileRead(texPath);
Image<Rgba32> image;
using (stream)
{
image = Image.Load<Rgba32>(stream);
}
var sheetSize = new Vector2i(image.Width, image.Height);
if (sheetSize.X % frameSize.X != 0 || sheetSize.Y % frameSize.Y != 0)
@@ -341,15 +345,25 @@ namespace Robust.Client.ResourceManagement
foreach (var stateObject in manifestJson["states"]!.Cast<JObject>())
{
var stateName = stateObject["name"]!.ToObject<string>()!;
var dirValue = stateObject["directions"]!.ToObject<int>();
RSI.State.DirectionType directions;
int dirValue;
var directions = dirValue switch
if (stateObject.TryGetValue("directions", out var dirJToken))
{
1 => RSI.State.DirectionType.Dir1,
4 => RSI.State.DirectionType.Dir4,
8 => RSI.State.DirectionType.Dir8,
_ => throw new RSILoadException($"Invalid direction: {dirValue}")
};
dirValue= dirJToken.ToObject<int>();
directions = dirValue switch
{
1 => RSI.State.DirectionType.Dir1,
4 => RSI.State.DirectionType.Dir4,
8 => RSI.State.DirectionType.Dir8,
_ => throw new RSILoadException($"Invalid direction: {dirValue} expected 1, 4 or 8")
};
}
else
{
dirValue = 1;
directions = RSI.State.DirectionType.Dir1;
}
// We can ignore selectors and flags for now,
// because they're not used yet!

View File

@@ -26,20 +26,23 @@ namespace Robust.Client.ResourceManagement
throw new FileNotFoundException("Content file does not exist for texture");
}
// Primarily for tracking down iCCP sRGB errors in the image files.
Logger.DebugS("res.tex", $"Loading texture {path}.");
var loadParameters = _tryLoadTextureParameters(cache, path) ?? TextureLoadParameters.Default;
var manager = IoCManager.Resolve<IClyde>();
using var image = Image.Load<Rgba32>(stream);
Texture = manager.LoadTextureFromImage(image, path.ToString(), loadParameters);
if (cache is IResourceCacheInternal cacheInternal)
using (stream)
{
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(path, image, this));
// Primarily for tracking down iCCP sRGB errors in the image files.
Logger.DebugS("res.tex", $"Loading texture {path}.");
var loadParameters = _tryLoadTextureParameters(cache, path) ?? TextureLoadParameters.Default;
var manager = IoCManager.Resolve<IClyde>();
using var image = Image.Load<Rgba32>(stream);
Texture = manager.LoadTextureFromImage(image, path.ToString(), loadParameters);
if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(path, image, this));
}
}
}
@@ -48,21 +51,25 @@ namespace Robust.Client.ResourceManagement
var metaPath = path.WithName(path.Filename + ".yml");
if (cache.TryContentFileRead(metaPath, out var stream))
{
YamlDocument yamlData;
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
using (stream)
{
var yamlStream = new YamlStream();
yamlStream.Load(reader);
if (yamlStream.Documents.Count == 0)
YamlDocument yamlData;
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
{
return null;
var yamlStream = new YamlStream();
yamlStream.Load(reader);
if (yamlStream.Documents.Count == 0)
{
return null;
}
yamlData = yamlStream.Documents[0];
}
yamlData = yamlStream.Documents[0];
return TextureLoadParameters.FromYaml((YamlMappingNode) yamlData.RootNode);
}
return TextureLoadParameters.FromYaml((YamlMappingNode)yamlData.RootNode);
}
return null;
}

View File

@@ -45,4 +45,10 @@
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Engine.targets" />
<PropertyGroup>
<RobustToolsPath>../Tools</RobustToolsPath>
</PropertyGroup>
<Target Name="RobustAfterBuild" AfterTargets="Build" />
<Import Project="..\MSBuild\XamlIL.targets" />
</Project>

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>
@@ -394,6 +437,7 @@ namespace Robust.Client.UserInterface
UserInterfaceManagerInternal = IoCManager.Resolve<IUserInterfaceManagerInternal>();
StyleClasses = new StyleClassCollection(this);
Children = new OrderedChildCollection(this);
XamlChildren = Children;
}
/// <summary>
@@ -477,7 +521,7 @@ namespace Robust.Client.UserInterface
{
DebugTools.Assert(!Disposed, "Control has been disposed.");
foreach (var child in Children.ToList())
foreach (var child in Children.ToArray())
{
RemoveChild(child);
}
@@ -713,14 +757,32 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Called when this control receives keyboard focus.
/// </summary>
protected internal virtual void FocusEntered()
protected internal virtual void KeyboardFocusEntered()
{
}
/// <summary>
/// Called when this control loses keyboard focus.
/// Called when this control loses keyboard focus (corresponds to UserInterfaceManager.KeyboardFocused).
/// </summary>
protected internal virtual void FocusExited()
protected internal virtual void KeyboardFocusExited()
{
}
/// <summary>
/// Fired when a control loses control focus for any reason. See <see cref="IUserInterfaceManager.ControlFocused"/>.
/// </summary>
/// <remarks>
/// Controls which have some sort of drag / drop behavior should usually implement this method (typically by cancelling the drag drop).
/// Otherwise, if a user clicks down LMB over one control to initiate a drag, then clicks RMB down
/// over a different control while still holding down LMB, the control being dragged will now lose focus
/// and will no longer receive the keyup for the LMB, thus won't cancel the drag.
/// This should also be considered for controls which have any special KeyBindUp behavior - consider
/// what would happen if the control lost focus and never received the KeyBindUp.
///
/// There is no corresponding ControlFocusEntered - if a control wants to handle that situation they should simply
/// handle KeyBindDown as that's the only way a control would gain focus.
/// </remarks>
protected internal virtual void ControlFocusExited()
{
}

View File

@@ -433,7 +433,7 @@ namespace Robust.Client.UserInterface.Controls
if (item.Region == null)
continue;
if (!item.Region.Value.Contains(args.RelativePosition))
if (!item.Region.Value.Contains(args.RelativePixelPosition))
continue;
if (item.Selectable && !item.Disabled)

View File

@@ -589,18 +589,18 @@ namespace Robust.Client.UserInterface.Controls
return index;
}
protected internal override void FocusEntered()
protected internal override void KeyboardFocusEntered()
{
base.FocusEntered();
base.KeyboardFocusEntered();
// Reset this so the cursor is always visible immediately after gaining focus..
_resetCursorBlink();
OnFocusEnter?.Invoke(new LineEditEventArgs(this, _text));
}
protected internal override void FocusExited()
protected internal override void KeyboardFocusExited()
{
base.FocusExited();
base.KeyboardFocusExited();
OnFocusExit?.Invoke(new LineEditEventArgs(this, _text));
}

View File

@@ -0,0 +1,264 @@
using Robust.Client.Graphics;
using System;
using System.Linq;
using System.Collections.Generic;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Robust.Client.UserInterface.Controls
{
public enum RadioOptionsLayout { Horizontal, Vertical }
public class RadioOptions<T> : Control
{
private int internalIdCount = 0;
private readonly List<RadioOptionButtonData<T>> _buttonDataList = new();
//private readonly Dictionary<int, int> _idMap = new();
private ButtonGroup _buttonGroup = new();
private Container _container;
public string ButtonStyle = string.Empty;
public string FirstButtonStyle = string.Empty;
public string LastButtonStyle = string.Empty;
public int ItemCount => _buttonDataList.Count;
/// <summary>
/// Called whenever you select a button.
///
/// Note: You should add optionButtons.Select(args.Id); if you want to actually select the button.
/// </summary>
public event Action<RadioOptionItemSelectedEventArgs<T>>? OnItemSelected;
public RadioOptions(RadioOptionsLayout layout)
{
switch (layout)
{
case RadioOptionsLayout.Vertical:
_container = new VBoxContainer();
break;
case RadioOptionsLayout.Horizontal:
default:
_container = new HBoxContainer();
break;
}
this.AddChild(_container);
}
public int AddItem(string label, T value, Action<RadioOptionItemSelectedEventArgs<T>>? itemSelectedAction = null)
{
var button = new Button
{
Text = label,
Group = _buttonGroup
};
button.OnPressed += ButtonOnPressed;
var data = new RadioOptionButtonData<T>(label, value, button)
{
Id = internalIdCount++
};
if (itemSelectedAction != null)
{
data.OnItemSelected += itemSelectedAction;
}
_buttonDataList.Add(data);
_container.AddChild(button);
UpdateFirstAndLastButtonStyle();
if (_buttonDataList.Count == 1)
{
Select(data.Id);
}
return data.Id;
}
/// <summary>
/// This is triggered when the button is pressed via the UI
/// </summary>
/// <param name="obj"></param>
private void ButtonOnPressed(ButtonEventArgs obj)
{
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Button == obj.Button);
if (buttonData != null)
{
InvokeItemSelected(new RadioOptionItemSelectedEventArgs<T>(buttonData.Id, this));
return;
}
// Not reachable.
throw new InvalidOperationException();
}
public void Clear()
{
foreach (var buttonDatum in _buttonDataList)
{
buttonDatum.Button.OnPressed -= ButtonOnPressed;
}
_buttonDataList.Clear();
_container.Children.Clear();
SelectedId = 0;
}
public object? GetItemMetadata(int idx)
{
return _buttonDataList.FirstOrDefault(bd => bd.Id == idx)?.Metadata;
}
public int SelectedId { get; private set; }
public RadioOptionButtonData<T> SelectedButtonData => _buttonDataList.First(bd => bd.Id == SelectedId);
public Button SelectedButton => SelectedButtonData.Button;
public string SelectedText => SelectedButtonData.Text;
public T SelectedValue => SelectedButtonData.Value;
/// <summary>
/// Always will return true if itemId is not found.
/// </summary>
/// <param name="idx"></param>
/// <returns></returns>
public bool IsItemDisabled(int idx)
{
return _buttonDataList.FirstOrDefault(bd => bd.Id == idx)?.Disabled ?? true;
}
public void RemoveItem(int idx)
{
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
if (data!= null)
{
data.Button.OnPressed -= ButtonOnPressed;
_container.RemoveChild(data.Button);
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
if (buttonData != null)
_buttonDataList.Remove(buttonData);
UpdateFirstAndLastButtonStyle();
}
}
public void Select(int idx)
{
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
if (data != null)
{
SelectedId = data.Id;
data.Button.Pressed = true;
return;
}
// Not found.
}
public void SelectByValue(T value)
{
var data = _buttonDataList.FirstOrDefault(bd => EqualityComparer<T>.Default.Equals(bd.Value, value));
if (data != null)
{
Select(data.Id);
}
}
public void InvokeItemSelected(RadioOptionItemSelectedEventArgs<T> args)
{
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Id == args.Id);
if (buttonData == null) return;
if (buttonData.HasOnItemSelectedEvent)
buttonData.InvokeItemSelected(args);
else
OnItemSelected?.Invoke(args);
}
public void UpdateFirstAndLastButtonStyle()
{
for (int i = 0; i < _buttonDataList.Count; i++)
{
var buttonData = _buttonDataList[i];
if (buttonData.Button == null) continue;
buttonData.Button.StyleClasses.Remove(ButtonStyle);
buttonData.Button.StyleClasses.Remove(LastButtonStyle);
buttonData.Button.StyleClasses.Remove(FirstButtonStyle);
if (i == 0)
buttonData.Button.StyleClasses.Add(FirstButtonStyle);
else if (i == _buttonDataList.Count - 1)
buttonData.Button.StyleClasses.Add(LastButtonStyle);
else
buttonData.Button.StyleClasses.Add(ButtonStyle);
}
}
public void SetItemDisabled(int idx, bool disabled)
{
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
if (data != null)
{
data.Disabled = disabled;
data.Button.Disabled = disabled;
}
}
public void SetItemMetadata(int idx, object metadata)
{
var buttonData = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
if (buttonData != null)
buttonData.Metadata = metadata;
}
public void SetItemText(int idx, string text)
{
var data = _buttonDataList.FirstOrDefault(bd => bd.Id == idx);
if (data != null)
{
data.Text = text;
data.Button.Text = text;
}
}
}
public class RadioOptionItemSelectedEventArgs<T> : EventArgs
{
public RadioOptions<T> Button { get; }
/// <summary>
/// The ID of the item that has been selected.
/// </summary>
public int Id { get; }
public RadioOptionItemSelectedEventArgs(int id, RadioOptions<T> button)
{
Id = id;
Button = button;
}
}
public sealed class RadioOptionButtonData<T>
{
public int Id;
public string Text;
public T Value;
public bool Disabled;
public object? Metadata;
public Button Button;
public RadioOptionButtonData(string text, T value, Button button)
{
Text = text;
this.Button = button;
Value = value;
}
public event Action<RadioOptionItemSelectedEventArgs<T>>? OnItemSelected;
public bool HasOnItemSelectedEvent => OnItemSelected != null;
public void InvokeItemSelected(RadioOptionItemSelectedEventArgs<T> args)
{
OnItemSelected?.Invoke(args);
}
}
}

View File

@@ -31,6 +31,11 @@ namespace Robust.Client.UserInterface.CustomControls
{
base.FrameUpdate(args);
if (!VisibleInTree)
{
return;
}
_label.Text = string.Join("\n", _inputManager.DownKeyFunctions);
}
}

View File

@@ -64,6 +64,11 @@ namespace Robust.Client.UserInterface.CustomControls
return;
}
if (!VisibleInTree)
{
return;
}
if (!NetManager.IsConnected)
{
contents.Text = "Not connected to server.";

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

@@ -179,7 +179,17 @@ namespace Robust.Client.UserInterface
return tcs.Task;
#else
// Luckily, GTK Linux and COM Windows are both happily threaded. Yay!
return Task.Run(action);
// * Actual attempts to have multiple file dialogs up at the same time, and the resulting crashes,
// have shown that at least for GTK+ (Linux), just because it can handle being on any thread doesn't mean it handle being on two at the same time.
// Testing system was Ubuntu 20.04.
// COM on Windows might handle this, but honestly, who exactly wants to risk it?
// In particular this could very well be an swnfd issue.
return Task.Run(() =>
{
lock (this) {
return action();
}
});
#endif
}

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

@@ -4,6 +4,7 @@ using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Drawing;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -172,8 +173,29 @@ namespace Robust.Client.UserInterface
// This needs to happen because word wrapping doesn't get checked for the last word.
if (posX > maxSizeX)
{
DebugTools.Assert(wordStartBreakIndex.HasValue,
"wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line.");
if (!wordStartBreakIndex.HasValue)
{
Logger.Error(
"Assert fail inside RichTextEntry.Update, " +
"wordStartBreakIndex is null on method end w/ word wrap required. " +
"Dumping relevant stuff. Send this to PJB.");
Logger.Error($"Message: {Message}");
Logger.Error($"maxSizeX: {maxSizeX}");
Logger.Error($"maxUsedWidth: {maxUsedWidth}");
Logger.Error($"breakIndexCounter: {breakIndexCounter}");
Logger.Error("wordStartBreakIndex: null (duh)");
Logger.Error($"wordSizePixels: {wordSizePixels}");
Logger.Error($"posX: {posX}");
Logger.Error($"lastChar: {lastChar}");
Logger.Error($"forceSplitData: {forceSplitData}");
Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}");
throw new Exception(
"wordStartBreakIndex can only be null if the word begins at a new line," +
"in which case this branch shouldn't be reached as" +
"the word would be split due to being longer than a single line.");
}
LineBreaks.Add(wordStartBreakIndex!.Value.index);
Height += font.GetLineHeight(uiScale);
maxUsedWidth = Math.Max(maxUsedWidth, wordStartBreakIndex.Value.lineSize);

View File

@@ -57,9 +57,7 @@ namespace Robust.Client.UserInterface
public Control? KeyboardFocused { get; private set; }
// When a control receives a mouse down it must also receive a mouse up and mouse moves, always.
// So we keep track of which control is "focused" by the mouse.
private Control? _controlFocused;
public Control? ControlFocused { get; private set; }
public LayoutContainer StateRoot { get; private set; } = default!;
public PopupContainer ModalRoot { get; private set; } = default!;
@@ -244,7 +242,8 @@ namespace Robust.Client.UserInterface
RemoveModal(top);
else
{
_controlFocused = top;
ControlFocused?.ControlFocusExited();
ControlFocused = top;
return false; // prevent anything besides the top modal control from receiving input
}
}
@@ -260,12 +259,12 @@ namespace Robust.Client.UserInterface
{
return false;
}
ControlFocused?.ControlFocusExited();
ControlFocused = control;
_controlFocused = control;
if (_controlFocused.CanKeyboardFocus && _controlFocused.KeyboardFocusOnClick)
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
{
_controlFocused.GrabKeyboardFocus();
ControlFocused.GrabKeyboardFocus();
}
return true;
@@ -273,7 +272,8 @@ namespace Robust.Client.UserInterface
public void HandleCanFocusUp()
{
_controlFocused = null;
ControlFocused?.ControlFocusExited();
ControlFocused = null;
}
public void KeyBindDown(BoundKeyEventArgs args)
@@ -290,7 +290,7 @@ namespace Robust.Client.UserInterface
return;
}
var control = _controlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
if (control == null)
{
@@ -311,7 +311,7 @@ namespace Robust.Client.UserInterface
public void KeyBindUp(BoundKeyEventArgs args)
{
var control = _controlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position);
if (control == null)
{
return;
@@ -352,7 +352,7 @@ namespace Robust.Client.UserInterface
_needUpdateActiveCursor = true;
}
var target = _controlFocused ?? newHovered;
var target = ControlFocused ?? newHovered;
if (target != null)
{
var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / UIScale,
@@ -368,7 +368,7 @@ namespace Robust.Client.UserInterface
private void UpdateActiveCursor()
{
// Consider mouse input focus first so that dragging windows don't act up etc.
var cursorTarget = _controlFocused ?? CurrentlyHovered;
var cursorTarget = ControlFocused ?? CurrentlyHovered;
if (cursorTarget == null)
{
@@ -478,13 +478,13 @@ namespace Robust.Client.UserInterface
KeyboardFocused = control;
KeyboardFocused.FocusEntered();
KeyboardFocused.KeyboardFocusEntered();
}
public void ReleaseKeyboardFocus()
{
var oldFocused = KeyboardFocused;
oldFocused?.FocusExited();
oldFocused?.KeyboardFocusExited();
KeyboardFocused = null;
}
@@ -528,10 +528,9 @@ namespace Robust.Client.UserInterface
_clearTooltip();
}
if (control == _controlFocused)
{
_controlFocused = null;
}
if (control != ControlFocused) return;
ControlFocused?.ControlFocusExited();
ControlFocused = null;
}
public void PushModal(Control modal)
@@ -569,7 +568,7 @@ namespace Robust.Client.UserInterface
public void CursorChanged(Control control)
{
if (control == _controlFocused || control == CurrentlyHovered)
if (control == ControlFocused || control == CurrentlyHovered)
{
_needUpdateActiveCursor = true;
}

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

@@ -4,7 +4,7 @@ using JetBrains.Annotations;
namespace Robust.Client.Utility
{
public static class UserDataDir
internal static class UserDataDir
{
[Pure]
public static string GetUserDataDir()

View File

@@ -8,6 +8,7 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables.Traits;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -51,7 +52,7 @@ namespace Robust.Client.ViewVariables.Instances
private bool _serverLoaded;
public ViewVariablesInstanceEntity(IViewVariablesManagerInternal vvm, IResourceCache resCache, IEntityManager entityManager) : base(vvm, resCache)
public ViewVariablesInstanceEntity(IViewVariablesManagerInternal vvm, IEntityManager entityManager, IRobustSerializer robustSerializer) : base(vvm, robustSerializer)
{
_entityManager = entityManager;
}
@@ -117,7 +118,7 @@ namespace Robust.Client.ViewVariables.Instances
_tabs.SetTabTitle(TabClientVars, "Client Variables");
var first = true;
foreach (var group in LocalPropertyList(obj, ViewVariablesManager, _resourceCache))
foreach (var group in LocalPropertyList(obj, ViewVariablesManager, _robustSerializer))
{
ViewVariablesTraitMembers.CreateMemberGroupHeader(
ref first,
@@ -202,7 +203,7 @@ namespace Robust.Client.ViewVariables.Instances
button.Visible = false;
continue;
}
button.Visible = true;
}
}
@@ -231,7 +232,7 @@ namespace Robust.Client.ViewVariables.Instances
button.Visible = false;
continue;
}
if (!button.Text.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase))
{
button.Visible = false;
@@ -314,7 +315,7 @@ namespace Robust.Client.ViewVariables.Instances
foreach (var propertyData in groupMembers)
{
var propertyEdit = new ViewVariablesPropertyControl(ViewVariablesManager, _resourceCache);
var propertyEdit = new ViewVariablesPropertyControl(ViewVariablesManager, _robustSerializer);
propertyEdit.SetStyle(otherStyle = !otherStyle);
var editor = propertyEdit.SetProperty(propertyData);
var selectorChain = new object[] {new ViewVariablesMemberSelector(propertyData.PropertyIndex)};
@@ -328,7 +329,7 @@ namespace Robust.Client.ViewVariables.Instances
var componentsBlob = await ViewVariablesManager.RequestData<ViewVariablesBlobEntityComponents>(_entitySession, new ViewVariablesRequestEntityComponents());
_serverComponents.DisposeAllChildren();
_serverComponents.AddChild(_serverComponentsSearchBar = new LineEdit
{
PlaceHolder = Loc.GetString("Search"),
@@ -336,7 +337,7 @@ namespace Robust.Client.ViewVariables.Instances
});
_serverComponentsSearchBar.OnTextChanged += OnServerComponentsSearchBarChanged;
componentsBlob.ComponentTypes.Sort();
var componentTypes = componentsBlob.ComponentTypes.AsEnumerable();

View File

@@ -4,6 +4,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables.Traits;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.ViewVariables;
using Robust.Shared.Utility;
@@ -19,8 +20,8 @@ namespace Robust.Client.ViewVariables.Instances
public ViewVariablesRemoteSession? Session { get; private set; }
public object? Object { get; private set; }
public ViewVariablesInstanceObject(IViewVariablesManagerInternal vvm, IResourceCache resCache)
: base(vvm, resCache) { }
public ViewVariablesInstanceObject(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
: base(vvm, robustSerializer) { }
public override void Initialize(SS14Window window, object obj)
{
@@ -113,7 +114,7 @@ namespace Robust.Client.ViewVariables.Instances
var list = new List<ViewVariablesTrait>(traitData.Count);
if (traitData.Contains(ViewVariablesTraits.Members))
{
list.Add(new ViewVariablesTraitMembers(ViewVariablesManager, _resourceCache));
list.Add(new ViewVariablesTraitMembers(ViewVariablesManager, _robustSerializer));
}
if (traitData.Contains(ViewVariablesTraits.Enumerable))

View File

@@ -2,6 +2,7 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.ViewVariables.Instances;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -11,7 +12,7 @@ namespace Robust.Client.ViewVariables.Traits
internal class ViewVariablesTraitMembers : ViewVariablesTrait
{
private readonly IViewVariablesManagerInternal _vvm;
private readonly IResourceCache _resourceCache;
private readonly IRobustSerializer _robustSerializer;
private VBoxContainer _memberList = default!;
@@ -22,9 +23,9 @@ namespace Robust.Client.ViewVariables.Traits
instance.AddTab("Members", _memberList);
}
public ViewVariablesTraitMembers(IViewVariablesManagerInternal vvm, IResourceCache resourceCache)
public ViewVariablesTraitMembers(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
{
_resourceCache = resourceCache;
_robustSerializer = robustSerializer;
_vvm = vvm;
}
@@ -36,7 +37,7 @@ namespace Robust.Client.ViewVariables.Traits
{
var first = true;
foreach (var group in ViewVariablesInstance.LocalPropertyList(Instance.Object,
Instance.ViewVariablesManager, _resourceCache))
Instance.ViewVariablesManager, _robustSerializer))
{
CreateMemberGroupHeader(
ref first,
@@ -64,7 +65,7 @@ namespace Robust.Client.ViewVariables.Traits
foreach (var propertyData in groupMembers)
{
var propertyEdit = new ViewVariablesPropertyControl(_vvm, _resourceCache);
var propertyEdit = new ViewVariablesPropertyControl(_vvm, _robustSerializer);
propertyEdit.SetStyle(otherStyle = !otherStyle);
var editor = propertyEdit.SetProperty(propertyData);

View File

@@ -7,6 +7,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables.Traits;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -19,12 +20,12 @@ namespace Robust.Client.ViewVariables
internal abstract class ViewVariablesInstance
{
public readonly IViewVariablesManagerInternal ViewVariablesManager;
protected readonly IResourceCache _resourceCache;
protected readonly IRobustSerializer _robustSerializer;
protected ViewVariablesInstance(IViewVariablesManagerInternal vvm, IResourceCache resCache)
protected ViewVariablesInstance(IViewVariablesManagerInternal vvm, IRobustSerializer robustSerializer)
{
ViewVariablesManager = vvm;
_resourceCache = resCache;
_robustSerializer = robustSerializer;
}
/// <summary>
@@ -54,7 +55,7 @@ namespace Robust.Client.ViewVariables
}
protected internal static IEnumerable<IGrouping<Type, Control>> LocalPropertyList(object obj, IViewVariablesManagerInternal vvm,
IResourceCache resCache)
IRobustSerializer robustSerializer)
{
var styleOther = false;
var type = obj.GetType();
@@ -104,7 +105,7 @@ namespace Robust.Client.ViewVariables
Value = value
};
var propertyEdit = new ViewVariablesPropertyControl(vvm, resCache);
var propertyEdit = new ViewVariablesPropertyControl(vvm, robustSerializer);
propertyEdit.SetStyle(styleOther = !styleOther);
var editor = propertyEdit.SetProperty(data);
editor.OnValueChanged += onValueChanged;

View File

@@ -23,7 +23,7 @@ namespace Robust.Client.ViewVariables
internal class ViewVariablesManager : ViewVariablesManagerShared, IViewVariablesManagerInternal
{
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IRobustSerializer _robustSerializer = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private uint _nextReqId = 1;
@@ -214,11 +214,11 @@ namespace Robust.Client.ViewVariables
ViewVariablesInstance instance;
if (obj is IEntity entity && !entity.Deleted)
{
instance = new ViewVariablesInstanceEntity(this, _resourceCache, _entityManager);
instance = new ViewVariablesInstanceEntity(this, _entityManager, _robustSerializer);
}
else
{
instance = new ViewVariablesInstanceObject(this, _resourceCache);
instance = new ViewVariablesInstanceObject(this, _robustSerializer);
}
var window = new SS14Window {Title = "View Variables"};
@@ -255,11 +255,11 @@ namespace Robust.Client.ViewVariables
ViewVariablesInstance instance;
if (type != null && typeof(IEntity).IsAssignableFrom(type))
{
instance = new ViewVariablesInstanceEntity(this, _resourceCache, _entityManager);
instance = new ViewVariablesInstanceEntity(this, _entityManager, _robustSerializer);
}
else
{
instance = new ViewVariablesInstanceObject(this, _resourceCache);
instance = new ViewVariablesInstanceObject(this, _robustSerializer);
}
loadingLabel.Dispose();

View File

@@ -7,6 +7,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.ViewVariables.Editors;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -23,14 +24,14 @@ namespace Robust.Client.ViewVariables
private readonly Label _bottomLabel;
private readonly IViewVariablesManagerInternal _viewVariablesManager;
private readonly IResourceCache _resourceCache;
private readonly IRobustSerializer _robustSerializer;
public ViewVariablesPropertyControl(IViewVariablesManagerInternal viewVars, IResourceCache resourceCache)
public ViewVariablesPropertyControl(IViewVariablesManagerInternal viewVars, IRobustSerializer robustSerializer)
{
MouseFilter = MouseFilterMode.Pass;
_viewVariablesManager = viewVars;
_resourceCache = resourceCache;
_robustSerializer = robustSerializer;
MouseFilter = MouseFilterMode.Pass;
ToolTip = "Click to expand";
@@ -68,11 +69,11 @@ namespace Robust.Client.ViewVariables
_bottomLabel.Text = $"Type: {member.TypePretty}";
VVPropEditor editor;
if (type == null)
if (type == null || !_robustSerializer.CanSerialize(type))
{
// Type is server-side only.
// Info whether it's reference or value type can be figured out from the sent value.
if (member.Value is ViewVariablesBlobMembers.ServerValueTypeToken)
if (type?.IsValueType == true || member.Value is ViewVariablesBlobMembers.ServerValueTypeToken)
{
// Value type, just display it stringified read-only.
editor = new VVPropEditorDummy();
@@ -80,7 +81,7 @@ namespace Robust.Client.ViewVariables
else
{
// Has to be a reference type at this point.
DebugTools.Assert(member.Value is ViewVariablesBlobMembers.ReferenceToken || member.Value == null);
DebugTools.Assert(member.Value is ViewVariablesBlobMembers.ReferenceToken || member.Value == null || type?.IsClass == true || type?.IsInterface == true);
editor = _viewVariablesManager.PropertyFor(typeof(object));
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Prometheus;
using Robust.Server.Console;
@@ -37,6 +38,7 @@ using Robust.Shared;
using Robust.Shared.Network.Messages;
using Robust.Server.DataMetrics;
using Robust.Server.Log;
using Robust.Server.Utility;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Serilog.Debugging;
@@ -286,6 +288,8 @@ namespace Robust.Server
// Initialize Tier 2 services
IoCManager.Resolve<IGameTiming>().InSimulation = true;
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_stateManager.Initialize();
IoCManager.Resolve<IPlayerManager>().Initialize(MaxPlayers);
@@ -320,6 +324,11 @@ namespace Robust.Server
_stringSerializer.LockStrings();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeBeginPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}
return false;
}
@@ -480,6 +489,8 @@ namespace Robust.Server
// called right before main loop returns, do all saving/cleanup in here
private void Cleanup()
{
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
// shut down networking, kicking all players.
_network.Shutdown($"Server shutting down: {_shutdownReason}");
@@ -500,6 +511,11 @@ namespace Robust.Server
AppDomain.CurrentDomain.ProcessExit -= ProcessExiting;
//TODO: This should prob shutdown all managers in a loop.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeEndPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}
}
private string UpdateBps()
@@ -528,6 +544,9 @@ namespace Robust.Server
ServerCurTick.Set(_time.CurTick.Value);
ServerCurTime.Set(_time.CurTime.TotalSeconds);
// These are always the same on the server, there is no prediction.
_time.LastRealTick = _time.CurTick;
UpdateTitle();
using (TickUsage.WithLabels("PreEngine").NewTimer())
@@ -535,6 +554,11 @@ namespace Robust.Server
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
}
using (TickUsage.WithLabels("NetworkedCVar").NewTimer())
{
IoCManager.Resolve<INetConfigurationManager>().TickProcessMessages();
}
using (TickUsage.WithLabels("Timers").NewTimer())
{
timerManager.UpdateTimers(frameEventArgs);

View File

@@ -38,6 +38,7 @@ namespace Robust.Server.Console.Commands
}
}
[UsedImplicitly]
internal sealed class RemoveCompCommand : IClientCommand
{
public string Command => "rmcomp";

View File

@@ -40,17 +40,17 @@ namespace Robust.Server.GameObjects
return (T)data[key];
}
public override bool TryGetData<T>(Enum key, [MaybeNullWhen(false)] out T data)
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
public override bool TryGetData<T>(string key, [MaybeNullWhen(false)] out T data)
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
{
return TryGetData(key, out data);
}
bool TryGetData<T>(object key, [MaybeNullWhen(false)] out T data)
private bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
{
if (this.data.TryGetValue(key, out var dat))
{

View File

@@ -32,8 +32,11 @@ namespace Robust.Server.GameObjects
get => _enabled;
set
{
_enabled = value;
Dirty();
if (_enabled != value)
{
_enabled = value;
Dirty();
}
}
}

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

@@ -11,6 +11,7 @@ using System.Linq;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network.Messages;
@@ -88,6 +89,12 @@ namespace Robust.Server.Placement
var coordinates = msg.EntityCoordinates;
if (!coordinates.IsValid(_entityManager))
{
Logger.WarningS("placement",
$"{session} tried to place {msg.ObjType} at invalid coordinate {coordinates}");
return;
}
/* TODO: Redesign permission system, or document what this is supposed to be doing
var permission = GetPermission(session.attachedEntity.Uid, alignRcv);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Prometheus;
using Robust.Server.Interfaces;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Input;
@@ -91,8 +92,6 @@ namespace Robust.Server.Player
MaxPlayers = maxPlayers;
_network.RegisterNetMessage<MsgServerInfoReq>(MsgServerInfoReq.NAME, HandleWelcomeMessageReq);
_network.RegisterNetMessage<MsgServerInfo>(MsgServerInfo.NAME);
_network.RegisterNetMessage<MsgPlayerListReq>(MsgPlayerListReq.NAME, HandlePlayerListReq);
_network.RegisterNetMessage<MsgPlayerList>(MsgPlayerList.NAME);
@@ -378,6 +377,8 @@ namespace Robust.Server.Player
}
PlayerCountMetric.Set(PlayerCount);
IoCManager.Resolve<INetConfigurationManager>().SyncConnectingClient(args.Channel);
}
private void OnPlayerStatusChanged(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus)
@@ -414,18 +415,6 @@ namespace Robust.Server.Player
Dirty();
}
private void HandleWelcomeMessageReq(MsgServerInfoReq message)
{
var channel = message.MsgChannel;
var netMsg = channel.CreateNetMessage<MsgServerInfo>();
netMsg.ServerName = _baseServer.ServerName;
netMsg.ServerMaxPlayers = _baseServer.MaxPlayers;
netMsg.TickRate = _timing.TickRate;
channel.SendMessage(netMsg);
}
private void HandlePlayerListReq(MsgPlayerListReq message)
{
var channel = message.MsgChannel;

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,69 +17,46 @@ 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 downloadUrl = _configurationManager.GetCVar(CVars.BuildDownloadUrl);
JObject? buildInfo;
@@ -126,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,28 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
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 +36,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 +44,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,12 +65,12 @@ 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);
_httpSawmill.Error($"Exception in StatusHost: {e}");
apiContext.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
}
/*
@@ -85,6 +95,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;
@@ -146,15 +160,22 @@ namespace Robust.Server.ServerStatus
var buildInfo = File.ReadAllText(PathHelpers.ExecutableRelativeFile("build.json"));
var info = JsonConvert.DeserializeObject<BuildInfo>(buildInfo);
_configurationManager.SetCVar(CVars.BuildEngineVersion, info.EngineVersion);
_configurationManager.SetCVar(CVars.BuildForkId, info.ForkId);
_configurationManager.SetCVar(CVars.BuildVersion, info.Version);
_configurationManager.SetCVar(CVars.BuildDownloadUrl, info.Download);
_configurationManager.SetCVar(CVars.BuildHash, info.Hash ?? "");
// 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)
{
}
void SetCVarIfUnmodified(CVarDef<string> cvar, string val)
{
if (_configurationManager.GetCVar(cvar) == "")
_configurationManager.SetCVar(cvar, val);
}
}
public void Dispose()
@@ -168,14 +189,91 @@ namespace Robust.Server.ServerStatus
_listener!.Stop();
}
#pragma warning disable CS0649
[JsonObject(ItemRequired = Required.DisallowNull)]
private sealed class BuildInfo
{
[JsonProperty("engine_version")] public string EngineVersion = default!;
[JsonProperty("hash")] public string? Hash;
[JsonProperty("download")] public string Download = default!;
[JsonProperty("download")] public string? Download = default;
[JsonProperty("fork_id")] public string ForkId = default!;
[JsonProperty("version")] public string Version = default!;
}
#pragma warning restore CS0649
private sealed class ContextImpl : IStatusHandlerContext
{
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 = MediaTypeNames.Text.Plain)
{
Respond(text, (int) code, contentType);
}
public void Respond(string text, int code = 200, string contentType = MediaTypeNames.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);
_context.Response.ContentType = MediaTypeNames.Application.Json;
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

@@ -0,0 +1,38 @@
using System;
using System.Runtime.InteropServices;
namespace Robust.Server.Utility
{
internal static class WindowsTickPeriod
{
private const uint TIMERR_NOERROR = 0;
// This is an actual error code my god.
private const uint TIMERR_NOCANDO = 97;
public static void TimeBeginPeriod(uint period)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new InvalidOperationException();
var ret = timeBeginPeriod(period);
if (ret != TIMERR_NOERROR)
throw new InvalidOperationException($"timeBeginPeriod returned error: {ret}");
}
public static void TimeEndPeriod(uint period)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new InvalidOperationException();
var ret = timeEndPeriod(period);
if (ret != TIMERR_NOERROR)
throw new InvalidOperationException($"timeEndPeriod returned error: {ret}");
}
[DllImport("Winmm.dll")]
private static extern uint timeBeginPeriod(uint uPeriod);
[DllImport("Winmm.dll")]
private static extern uint timeEndPeriod(uint uPeriod);
}
}

View File

@@ -38,7 +38,7 @@ namespace Robust.Shared.Scripting
{
return new EntityCoordinates(EntityUid.Invalid, ((float) x, (float) y));
}
return new EntityCoordinates(grid.GridEntityId, ((float) x, (float) y));
}
@@ -52,6 +52,11 @@ namespace Robust.Shared.Scripting
return getent(eid(i));
}
public T gcm<T>(int i)
{
return getent(i).GetComponent<T>();
}
public IEntity getent(EntityUid uid)
{
return ent.GetEntity(uid);

View File

@@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
@@ -59,8 +60,19 @@ namespace Robust.Shared
public static readonly CVarDef<bool> NetPredict =
CVarDef.Create("net.predict", true, CVar.ARCHIVE);
public static readonly CVarDef<int> NetPredictSize =
CVarDef.Create("net.predict_size", 1, CVar.ARCHIVE);
public static readonly CVarDef<int> NetPredictTickBias =
CVarDef.Create("net.predict_tick_bias", 1, CVar.ARCHIVE);
// On Windows we default this to 16ms lag bias, to account for time period lag in the Lidgren thread.
// Basically due to how time periods work on Windows, messages are (at worst) time period-delayed when sending.
// BUT! Lidgren's latency calculation *never* measures this due to how it works.
// This broke some prediction calculations quite badly so we bias them to mask it.
// This is not necessary on Linux because Linux, for better or worse,
// just has the Lidgren thread go absolute brr polling.
public static readonly CVarDef<float> NetPredictLagBias = CVarDef.Create(
"net.predict_lag_bias",
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 0.016f : 0,
CVar.ARCHIVE);
public static readonly CVarDef<int> NetStateBufMergeThreshold =
CVarDef.Create("net.state_buf_merge_threshold", 5, CVar.ARCHIVE);
@@ -77,6 +89,8 @@ namespace Robust.Shared
public static readonly CVarDef<int> NetTickrate =
CVarDef.Create("net.tickrate", 60, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
public static readonly CVarDef<int> SysWinTickPeriod =
CVarDef.Create("sys.win_tick_period", 3, CVar.SERVERONLY);
#if DEBUG
public static readonly CVarDef<float> NetFakeLoss = CVarDef.Create("net.fakeloss", 0f, CVar.CHEAT);
@@ -151,10 +165,10 @@ namespace Robust.Shared
*/
public static readonly CVarDef<int> GameMaxPlayers =
CVarDef.Create("game.maxplayers", 32, CVar.ARCHIVE | CVar.SERVERONLY);
CVarDef.Create("game.maxplayers", 32, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
public static readonly CVarDef<string> GameHostName =
CVarDef.Create("game.hostname", "MyServer", CVar.ARCHIVE | CVar.SERVERONLY);
CVarDef.Create("game.hostname", "MyServer", CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
/*
* LOG
@@ -233,10 +247,10 @@ namespace Robust.Shared
CVarDef.Create("display.height", 720, CVar.CLIENTONLY);
public static readonly CVarDef<int> DisplayLightMapDivider =
CVarDef.Create("display.lightmapdivider", 2, CVar.CLIENTONLY);
CVarDef.Create("display.lightmapdivider", 2, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<bool> DisplaySoftShadows =
CVarDef.Create("display.softshadows", true, CVar.CLIENTONLY);
CVarDef.Create("display.softshadows", true, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<float> DisplayUIScale =
CVarDef.Create("display.uiScale", 0f, CVar.ARCHIVE | CVar.CLIENTONLY);
@@ -261,7 +275,7 @@ namespace Robust.Shared
CVarDef.Create("audio.device", string.Empty, CVar.CLIENTONLY);
public static readonly CVarDef<float> AudioMasterVolume =
CVarDef.Create("audio.mastervolume", 1.0f, CVar.CLIENTONLY);
CVarDef.Create("audio.mastervolume", 1.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
/*
* PLAYER

View File

@@ -1,4 +1,4 @@
using Nett;
using Nett;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Log;
using System;
@@ -13,12 +13,12 @@ namespace Robust.Shared.Configuration
/// <summary>
/// Stores and manages global configuration variables.
/// </summary>
internal sealed class ConfigurationManager : IConfigurationManagerInternal
internal class ConfigurationManager : IConfigurationManagerInternal
{
private const char TABLE_DELIMITER = '.';
private readonly Dictionary<string, ConfigVar> _configVars = new();
protected readonly Dictionary<string, ConfigVar> _configVars = new();
private string? _configFile;
private bool _isServer;
protected bool _isServer;
/// <summary>
/// Constructs a new ConfigurationManager.
@@ -79,6 +79,7 @@ namespace Robust.Shared.Configuration
else // this is a key, add CVar
{
// if the CVar has already been registered
var tomlValue = TypeConvert(obj);
if (_configVars.TryGetValue(tablePath, out var cfgVar))
{
if ((cfgVar.Flags & CVar.SECURE) != 0)
@@ -90,13 +91,14 @@ namespace Robust.Shared.Configuration
return;
}
// overwrite the value with the saved one
cfgVar.Value = TypeConvert(obj);
cfgVar.Value = tomlValue;
cfgVar.ValueChanged?.Invoke(cfgVar.Value);
}
else
{
//or add another unregistered CVar
cfgVar = new ConfigVar(tablePath, null, CVar.NONE) { Value = TypeConvert(obj) };
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
cfgVar = new ConfigVar(tablePath, 0, CVar.NONE) { Value = tomlValue };
_configVars.Add(tablePath, cfgVar);
}
@@ -131,6 +133,8 @@ namespace Robust.Shared.Configuration
continue;
}
// Don't write if Archive flag is not set.
// Don't write if the cVar is the default value.
if (!cVar.ConfigModified &&
(cVar.Flags & CVar.ARCHIVE) == 0 || value.Equals(cVar.DefaultValue))
{
@@ -192,6 +196,7 @@ namespace Robust.Shared.Configuration
}
public void RegisterCVar<T>(string name, T defaultValue, CVar flags = CVar.NONE, Action<T>? onValueChanged = null)
where T : notnull
{
Action<object>? valueChangedDelegate = null;
if (onValueChanged != null)
@@ -202,7 +207,7 @@ namespace Robust.Shared.Configuration
RegisterCVar(name, typeof(T), defaultValue, flags, valueChangedDelegate);
}
private void RegisterCVar(string name, Type type, object? defaultValue, CVar flags, Action<object>? onValueChanged)
private void RegisterCVar(string name, Type type, object defaultValue, CVar flags, Action<object>? onValueChanged)
{
DebugTools.Assert(!type.IsEnum || type.GetEnumUnderlyingType() == typeof(int),
$"{name}: Enum cvars must have int as underlying type.");
@@ -305,10 +310,15 @@ namespace Robust.Shared.Configuration
}
/// <inheritdoc />
public void SetCVar(string name, object value)
public virtual void SetCVar(string name, object value)
{
SetCVarInternal(name, value);
}
private void SetCVarInternal(string name, object value, bool allowSecure = false)
{
//TODO: Make flags work, required non-derpy net system.
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered && (cVar.Flags & CVar.SECURE) == 0)
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered && (allowSecure || (cVar.Flags & CVar.SECURE) == 0))
{
if (!Equals(cVar.OverrideValueParsed ?? cVar.Value, value))
{
@@ -348,6 +358,18 @@ namespace Robust.Shared.Configuration
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
}
public void SetSecureCVar(string name, object value)
{
SetCVarInternal(name, value, allowSecure: true);
}
public void SetSecureCVar<T>(CVarDef<T> def, T value) where T : notnull
{
SetSecureCVar(def.Name, value);
}
public T GetCVar<T>(CVarDef<T> def) where T : notnull
{
return GetCVar<T>(def.Name);
@@ -377,13 +399,14 @@ namespace Robust.Shared.Configuration
else
{
//or add another unregistered CVar
var cVar = new ConfigVar(key, null, CVar.NONE) { OverrideValue = value };
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
var cVar = new ConfigVar(key, 0, CVar.NONE) { OverrideValue = value };
_configVars.Add(key, cVar);
}
}
}
private object ParseOverrideValue(string value, Type? type)
private static object ParseOverrideValue(string value, Type? type)
{
if (type == typeof(int))
{
@@ -434,7 +457,7 @@ namespace Robust.Shared.Configuration
/// <summary>
/// Holds the data for a single configuration variable.
/// </summary>
private class ConfigVar
protected class ConfigVar
{
/// <summary>
/// Constructs a CVar.
@@ -444,7 +467,7 @@ namespace Robust.Shared.Configuration
/// everything after is the CVar name in the TOML document.</param>
/// <param name="defaultValue">The default value of this CVar.</param>
/// <param name="flags">Optional flags to modify the behavior of this CVar.</param>
public ConfigVar(string name, object? defaultValue, CVar flags)
public ConfigVar(string name, object defaultValue, CVar flags)
{
Name = name;
DefaultValue = defaultValue;
@@ -459,7 +482,7 @@ namespace Robust.Shared.Configuration
/// <summary>
/// The default value of this CVar.
/// </summary>
public object? DefaultValue { get; set; }
public object DefaultValue { get; set; }
/// <summary>
/// Optional flags to modify the behavior of this CVar.

View File

@@ -0,0 +1,311 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Utility;
namespace Robust.Shared.Configuration
{
/// <summary>
/// A networked configuration manager that controls the replication of
/// console variables between client and server.
/// </summary>
public interface INetConfigurationManager : IConfigurationManager
{
/// <summary>
/// Sets up the networking for the config manager.
/// </summary>
void SetupNetworking();
/// <summary>
/// Get a replicated client CVar for a specific client.
/// </summary>
/// <typeparam name="T">CVar type.</typeparam>
/// <param name="channel">channel of the connected client.</param>
/// <param name="name">Name of the CVar.</param>
/// <returns>Replicated CVar of the client.</returns>
T GetClientCVar<T>(INetChannel channel, string name);
/// <summary>
/// Synchronize the CVars marked with <see cref="CVar.REPLICATED"/> with the client.
/// This needs to be called once during the client connection.
/// </summary>
/// <param name="client">Client's NetChannel to sync replicated CVars with.</param>
void SyncConnectingClient(INetChannel client);
/// <summary>
/// Synchronize the CVars marked with <see cref="CVar.REPLICATED"/> with the server.
/// This needs to be called once when connecting.
/// </summary>
void SyncWithServer();
/// <summary>
/// Called every tick to process any incoming network messages.
/// </summary>
void TickProcessMessages();
/// <summary>
/// Flushes any NwCVar messages in the receive buffer.
/// </summary>
void FlushMessages();
public event EventHandler ReceivedInitialNwVars;
}
/// <inheritdoc cref="INetConfigurationManager"/>
internal class NetConfigurationManager : ConfigurationManager, INetConfigurationManager
{
[Dependency] private readonly INetManager _netManager = null!;
[Dependency] private readonly IGameTiming _timing = null!;
private readonly Dictionary<INetChannel, Dictionary<string, object>> _replicatedCVars = new();
private readonly List<MsgConVars> _netVarsMessages = new();
public event EventHandler? ReceivedInitialNwVars;
private bool _receivedInitialNwVars;
/// <inheritdoc />
public void SetupNetworking()
{
if(_isServer)
{
_netManager.Connected += PeerConnected;
_netManager.Disconnect += PeerDisconnected;
}
_netManager.RegisterNetMessage<MsgConVars>(MsgConVars.NAME, HandleNetVarMessage);
}
private void PeerConnected(object? sender, NetChannelArgs e)
{
_replicatedCVars.Add(e.Channel, new Dictionary<string, object>());
}
private void PeerDisconnected(object? sender, NetDisconnectedArgs e)
{
_replicatedCVars.Remove(e.Channel);
}
private void HandleNetVarMessage(MsgConVars message)
{
if(!_receivedInitialNwVars)
ReceivedInitialNwVars?.Invoke(this, EventArgs.Empty);
_receivedInitialNwVars = true;
_netVarsMessages.Add(message);
}
/// <inheritdoc />
public void TickProcessMessages()
{
if(!_timing.InSimulation || _timing.InPrediction)
return;
for (var i = 0; i < _netVarsMessages.Count; i++)
{
var msg = _netVarsMessages[i];
if (msg.Tick > _timing.LastRealTick)
continue;
ApplyNetVarChange(msg.MsgChannel, msg.NetworkedVars);
if(msg.Tick < _timing.LastRealTick)
Logger.WarningS("cfg", $"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.LastRealTick} ).");
_netVarsMessages.RemoveSwap(i);
i--;
}
}
/// <inheritdoc />
public void FlushMessages()
{
_netVarsMessages.Sort(((a, b) => a.Tick.Value.CompareTo(b.Tick.Value)));
foreach (var msg in _netVarsMessages)
{
ApplyNetVarChange(msg.MsgChannel, msg.NetworkedVars);
}
_netVarsMessages.Clear();
}
private void ApplyNetVarChange(INetChannel msgChannel, List<(string name, object value)> networkedVars)
{
Logger.DebugS("cfg", "Handling replicated cvars...");
foreach (var (name, value) in networkedVars)
{
if (_netManager.IsClient) // Server sent us a CVar update.
{
// Actually set the CVar
base.SetCVar(name, value);
Logger.DebugS("cfg", $"name={name}, val={value}");
}
else // Client sent us a CVar update
{
if (!_configVars.TryGetValue(name, out var cVar))
{
Logger.WarningS("cfg", $"{msgChannel} tried to replicate an unknown CVar '{name}.'");
continue;
}
if (!cVar.Registered)
{
Logger.WarningS("cfg", $"{msgChannel} tried to replicate an unregistered CVar '{name}.'");
continue;
}
if((cVar.Flags & CVar.REPLICATED) != 0)
{
var clientCVars = _replicatedCVars[msgChannel];
if (clientCVars.ContainsKey(name))
clientCVars[name] = value;
else
clientCVars.Add(name, value);
Logger.DebugS("cfg", $"name={name}, val={value}");
}
else
{
Logger.WarningS("cfg", $"{msgChannel} tried to replicate an un-replicated CVar '{name}.'");
}
}
}
}
/// <inheritdoc />
public T GetClientCVar<T>(INetChannel channel, string name)
{
if (!_configVars.TryGetValue(name, out var cVar) || !cVar.Registered)
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
if (_replicatedCVars.TryGetValue(channel, out var clientCVars) && clientCVars.TryGetValue(name, out var value))
{
return (T)value;
}
return (T)(cVar.DefaultValue!);
}
/// <inheritdoc />
public override void SetCVar(string name, object value)
{
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
{
if (_netManager.IsClient)
{
if (_netManager.IsConnected)
{
if ((cVar.Flags & CVar.NOT_CONNECTED) != 0)
{
Logger.WarningS("cfg", $"'{name}' can only be changed when not connected to a server.");
return;
}
}
if ((cVar.Flags & CVar.SERVER) != 0)
{
Logger.WarningS("cfg", $"Only the server can change '{name}'.");
return;
}
}
}
else
{
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
}
// Actually set the CVar
base.SetCVar(name, value);
var cvar = _configVars[name];
// replicate if needed
if (_netManager.IsClient)
{
if ((cvar.Flags & CVar.REPLICATED) == 0)
return;
var msg = _netManager.CreateNetMessage<MsgConVars>();
msg.Tick = _timing.CurTick;
msg.NetworkedVars = new List<(string name, object value)>
{
(name, value)
};
_netManager.ClientSendMessage(msg);
}
else // Server
{
if ((cvar.Flags & CVar.REPLICATED) == 0)
return;
var msg = _netManager.CreateNetMessage<MsgConVars>();
msg.Tick = _timing.CurTick;
msg.NetworkedVars = new List<(string name, object value)>
{
(name, value)
};
_netManager.ServerSendToAll(msg);
}
}
/// <inheritdoc />
public void SyncConnectingClient(INetChannel client)
{
DebugTools.Assert(_netManager.IsConnected);
DebugTools.Assert(_netManager.IsServer);
Logger.InfoS("cfg", $"{client}: Sending server info...");
var msg = _netManager.CreateNetMessage<MsgConVars>();
msg.Tick = _timing.CurTick;
msg.NetworkedVars = GetReplicatedVars();
_netManager.ServerSendMessage(msg, client);
}
/// <inheritdoc />
public void SyncWithServer()
{
DebugTools.Assert(_netManager.IsConnected);
DebugTools.Assert(_netManager.IsClient);
Logger.InfoS("cfg", "Sending client info...");
var msg = _netManager.CreateNetMessage<MsgConVars>();
msg.Tick = _timing.CurTick;
msg.NetworkedVars = GetReplicatedVars();
_netManager.ClientSendMessage(msg);
}
private List<(string name, object value)> GetReplicatedVars()
{
var nwVars = new List<(string name, object value)>();
foreach (var cVar in _configVars.Values)
{
if (!cVar.Registered)
return nwVars;
if ((cVar.Flags & CVar.REPLICATED) == 0)
continue;
if (_netManager.IsClient && (cVar.Flags & CVar.SERVER) != 0)
continue;
nwVars.Add((cVar.Name, cVar.Value ?? cVar.DefaultValue));
Logger.DebugS("cfg", $"name={cVar.Name}, val={(cVar.Value ?? cVar.DefaultValue)}");
}
return nwVars;
}
}
}

View File

@@ -260,6 +260,11 @@ namespace Robust.Shared.ContentPack
// See II.14.2 in ECMA-335.
return;
}
case MTypeDefined:
{
// Valid for this to show up, safe to ignore.
return;
}
default:
{
throw new ArgumentOutOfRangeException();
@@ -471,6 +476,20 @@ namespace Robust.Shared.ContentPack
break;
}
case HandleKind.TypeDefinition:
{
try
{
parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle) memRef.Parent);
}
catch (UnsupportedMetadataException u)
{
errors.Add(new SandboxError(u));
return null;
}
break;
}
case HandleKind.TypeSpecification:
{
var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle) memRef.Parent);
@@ -501,7 +520,7 @@ namespace Robust.Shared.ContentPack
default:
{
errors.Add(new SandboxError(
$"Unsupported member ref parent type: {memRef.Parent}. Name: {memName}"));
$"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}"));
return null;
}
}

View File

@@ -77,7 +77,7 @@ namespace Robust.Shared.ContentPack
var fullPath = mountPath / filePath;
Logger.DebugS("res.mod", $"Found module '{fullPath}'");
var asmFile = _res.ContentFileRead(fullPath);
using var asmFile = _res.ContentFileRead(fullPath);
var (asmRefs, asmName) = GetAssemblyReferenceData(asmFile);
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
@@ -118,8 +118,8 @@ namespace Robust.Shared.ContentPack
}
else
{
var assemblyStream = _res.ContentFileRead(path);
var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
using var assemblyStream = _res.ContentFileRead(path);
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
}
}
@@ -241,16 +241,34 @@ namespace Robust.Shared.ContentPack
if (_res.TryContentFileRead(dllPath, out var gameDll))
{
Logger.DebugS("srv", $"Loading {assemblyName} DLL");
// see if debug info is present
if (_res.TryContentFileRead(new ResourcePath($@"/Assemblies/{assemblyName}.pdb"),
out var gamePdb))
using (gameDll)
{
Logger.DebugS("srv", $"Loading {assemblyName} DLL");
// see if debug info is present
if (_res.TryContentFileRead(new ResourcePath($@"/Assemblies/{assemblyName}.pdb"),
out var gamePdb))
{
using (gamePdb)
{
try
{
// load the assembly into the process, and bootstrap the GameServer entry point.
LoadGameAssembly(gameDll, gamePdb);
return true;
}
catch (Exception e)
{
Logger.ErrorS("srv", $"Exception loading DLL {assemblyName}.dll: {e.ToStringBetter()}");
return false;
}
}
}
try
{
// load the assembly into the process, and bootstrap the GameServer entry point.
LoadGameAssembly(gameDll, gamePdb);
LoadGameAssembly(gameDll);
return true;
}
catch (Exception e)
@@ -259,18 +277,6 @@ namespace Robust.Shared.ContentPack
return false;
}
}
try
{
// load the assembly into the process, and bootstrap the GameServer entry point.
LoadGameAssembly(gameDll);
return true;
}
catch (Exception e)
{
Logger.ErrorS("srv", $"Exception loading DLL {assemblyName}.dll: {e.ToStringBetter()}");
return false;
}
}
Logger.WarningS("eng", $"Could not load {assemblyName} DLL: {dllPath} does not exist in the VFS.");

View File

@@ -216,7 +216,13 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
public bool ContentFileExists(ResourcePath path)
{
return TryContentFileRead(path, out var _);
if (TryContentFileRead(path, out var stream))
{
stream.Dispose();
return true;
}
return false;
}
/// <inheritdoc />

View File

@@ -297,6 +297,12 @@ Types:
IEnumerable: { All: True }
IEnumerator: { All: True }
IReadOnlyList`1: { All: True }
System.ComponentModel:
PropertyDescriptor: { }
ISite: { All: True }
IComponent: { All: True }
IContainer: { All: True }
ITypeDescriptorContext: { All: True }
System.Diagnostics.CodeAnalysis:
AllowNullAttribute: { All: True }
DisallowNullAttribute: { All: True }
@@ -436,6 +442,7 @@ Types:
IsExternalInit: { All: True }
IsReadOnlyAttribute: { All: True }
IteratorStateMachineAttribute: { All: True }
PreserveBaseOverridesAttribute: { All: True }
RuntimeCompatibilityAttribute: { All: True }
RuntimeHelpers: { All: True }
TaskAwaiter: { All: True }
@@ -651,6 +658,7 @@ Types:
CancellationTokenSource: { All: True }
Interlocked: { All: True }
System:
IServiceProvider: { All: True }
Action: { All: True }
Action`1: { All: True }
Action`2: { All: True }
@@ -845,6 +853,7 @@ Types:
Func`15: { All: True }
Func`16: { All: True }
Func`17: { All: True }
Guid: { All: True }
HashCode: { All: True }
IAsyncDisposable: { All: True }
IAsyncResult: { }
@@ -1131,6 +1140,7 @@ Types:
# Content should never touch that.
Methods:
- "bool Equals(object)"
- "bool Equals(System.Type)"
- "bool get_ContainsGenericParameters()"
- "bool get_HasElementType()"
- "bool get_IsAbstract()"
@@ -1250,6 +1260,7 @@ Types:
UInt32: { All: True }
UInt64: { All: True }
UIntPtr: { All: True }
Uri: { All: True }
ValueTuple: { All: True }
ValueTuple`1: { All: True }
ValueTuple`2: { All: True }

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