Compare commits

...

89 Commits

Author SHA1 Message Date
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
Pieter-Jan Briers
7ef2cec121 Fix names parsed from build.json 2020-12-17 15:28:01 +01:00
Pieter-Jan Briers
40bff81017 Fix nullable warning. 2020-12-17 00:58:26 +01:00
Pieter-Jan Briers
f7c28992f8 Disable string map caching to hopefully fix connect. 2020-12-17 00:48:15 +01:00
Pieter-Jan Briers
920ae58019 Fix LoaderApiLoader.FindFiles() 2020-12-17 00:33:39 +01:00
Pieter-Jan Briers
5bb21e07de Engine versioning. 2020-12-16 23:53:51 +01:00
Pieter-Jan Briers
78ceaa50d5 Update Lidgren submodule. 2020-12-16 18:18:40 +01:00
Pieter-Jan Briers
7473b6dae1 Optimize assembly type checking.
It's now parallelized which cuts off ~200ms on its own for me.
Config is now shared between multiple loads which saves a lot as well.

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

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

It removes  download_natives.py which simplifies the build process.

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

* Added deleted check in FixCollide

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

View File

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

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

9
.gitmodules vendored
View File

@@ -4,3 +4,12 @@
[submodule "Lidgren.Network"]
path = Lidgren.Network/Lidgren.Network
url = https://github.com/space-wizards/lidgren-network-gen3.git
[submodule "XamlX"]
path = XamlX
url = https://github.com/space-wizards/XamlX
[submodule "Robust.LoaderApi"]
path = Robust.LoaderApi
url = https://github.com/space-wizards/Robust.LoaderApi.git
[submodule "ManagedHttpListener"]
path = ManagedHttpListener
url = https://github.com/space-wizards/ManagedHttpListener.git

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1,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,6 +1,6 @@
namespace Robust.Client.Graphics.Shaders
{
public enum ShaderParamType
public enum ShaderParamType : byte
{
// Can this even happen?
Void = 0,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -772,14 +791,14 @@ namespace Robust.Client.Input
}
}
public enum KeyBindingType
public enum KeyBindingType : byte
{
Unknown = 0,
State,
Toggle,
}
public enum CommandState
public enum CommandState : byte
{
Unknown = 0,
Enabled,

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
@@ -570,7 +570,7 @@ namespace Robust.Client.UserInterface.Controls
}
}
public enum ItemListSelectMode
public enum ItemListSelectMode : byte
{
None,
Single,

View File

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

View File

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

View File

@@ -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));
}
@@ -711,7 +711,7 @@ namespace Robust.Client.UserInterface.Controls
return CharClass.Other;
}
private enum CharClass
private enum CharClass : byte
{
Other,
AlphaNumeric,

View File

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

View File

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

View File

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

View File

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

View File

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

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