Compare commits

...

72 Commits

Author SHA1 Message Date
Pieter-Jan Briers
3e5efd5ed0 Add AnimatedTextureRect 2021-01-24 23:00:02 +01:00
Pieter-Jan Briers
e1110eadb4 Better unified handling of SpriteSpecifiers
Add more utilities, most notably IRsiStateLike, to make working with SpriteSpecifiers easier. Especially when animated.
2021-01-24 22:59:47 +01:00
Pieter-Jan Briers
06ace83a73 Allow audio playback without grids. 2021-01-24 16:04:23 +01:00
Pieter-Jan Briers
cd01ca924b Fix broken PanelContainer due to previous commit.
God I am tired.
2021-01-24 04:20:51 +01:00
Pieter-Jan Briers
38ad8ce132 Fix UI scaling weirdness with PanelContainer. 2021-01-23 21:51:35 +01:00
20kdc
ee440c2df9 Make the lighting manager much more configurable, including a console lock (#1506) 2021-01-23 21:17:49 +01:00
Pieter-Jan Briers
32f3c863fb Fix UI scaling bugs with ProgressBar 2021-01-23 20:15:45 +01:00
Pieter-Jan Briers
b00e0bef5a Fix UI scaling bug with RichTextLabel 2021-01-23 16:02:31 +01:00
Pieter-Jan Briers
0a09b27918 Fix UI scaling bug with GridContainer 2021-01-23 16:02:23 +01:00
Paul
c9f6a4e32a fixes enum VV 2021-01-21 15:33:42 +01:00
Pieter-Jan Briers
b205a14f69 Exception tolerance for NetManager.OnDisconnect 2021-01-20 21:07:02 +01:00
Pieter-Jan Briers
d5f3292e0a Unregister OnSessionOnPlayerStatusChanged on bound user interfaces.
I am frankly flabbergasted this is only a problem now.
2021-01-20 21:00:24 +01:00
Pieter-Jan Briers
561e4b330e Fix ALL components memory leaking.
:irrational:
2021-01-20 20:45:02 +01:00
Paul
36a5d102ff prevent one error from killing the entire namegenerator from running 2021-01-17 17:17:02 +01:00
Pieter-Jan Briers
b9c39e0953 Fix reconnecting. 2021-01-17 16:08:48 +01:00
Pieter-Jan Briers
ad4c8be132 Add system for preserving Map UIDs across edits. 2021-01-17 15:51:32 +01:00
kira-er
988cbf9a87 VV Enum (#1503) 2021-01-17 01:50:26 +01:00
Acruid
e26512001a Completely removed MsgSetTickRate, the NetConfigurationManager replaces the functionality. This builds on top of the previous commit.
Fixes bug where server was not sending the entire set of replicated cvars.
2021-01-16 15:33:44 -08:00
Pieter-Jan Briers
8e97982f1e Fix net.tickrate not being replicated correctly to clients. 2021-01-16 21:46:10 +01:00
Paul
3ca686298e Fixes ViewVariablesManager using the wrong VVPropEditor for Type in shared not annotated with [NetSerializable] 2021-01-16 20:05:15 +01:00
20kdc
5e914cb13a PointLightComponent: Don't dirty if enabled is being set to what it's already set to. (#1507) 2021-01-16 00:13:55 +11:00
Pieter-Jan Briers
a1bdfca8ba Fix SimplePredictReconcileTest. 2021-01-15 11:03:52 +01:00
20kdc
79deaca409 Polar Coordinates: simplify maths, any-angle occluders are no longer evil (#1504)
Someone please performance-test this, but I think the fragment shader code simplifications speak for themselves.

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

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

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

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

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

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

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

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

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

So, to fix this...

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

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

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

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

* I blame Remie

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

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

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

Use MSBuild dependencies to reduce unecessary xamlil builds.
2020-12-23 12:01:35 +01:00
Pieter-Jan Briers
835b6ebdba Probably fix build forgetting that nuget exists. 2020-12-21 12:14:28 +01:00
Pieter-Jan Briers
0ecabd6553 Fix XamlIL locking build. 2020-12-21 12:13:45 +01:00
119 changed files with 2743 additions and 939 deletions

View File

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

View File

@@ -1,24 +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" />
<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" />
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false"/>
</ItemGroup>
<UsingTask TaskName="CompileRobustXamlTask" AssemblyFile="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(Configuration)\netstandard2.0\Robust.Client.Injectors.dll" />
<Target Name="CompileRobustXaml" AfterTargets="AfterCompile">
<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 File="$(RobustXamlReferencesTemporaryFilePath)" Lines="@(ReferencePathWithRefAssemblies)" Overwrite="true" />
<CompileRobustXamlTask AssemblyFile="@(IntermediateAssembly)" ReferencesFilePath="$(RobustXamlReferencesTemporaryFilePath)" OriginalCopyPath="$(RobustXamlOriginalCopyFilePath)" ProjectDirectory="$(MSBuildProjectDirectory)" AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)" SignAssembly="$(SignAssembly)" DelaySign="$(DelaySign)" />
<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>

View File

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

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
@@ -48,6 +49,19 @@ namespace Robust.Build.Tasks
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;
}
@@ -64,6 +78,7 @@ namespace Robust.Build.Tasks
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; }

View File

@@ -31,6 +31,16 @@ namespace Robust.Build.Tasks
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);

View File

@@ -153,7 +153,7 @@ namespace {nameSpace}
DiagnosticSeverity.Error,
true),
typeSymbol.Locations[0]));
return;
continue;
}
var txt = relevantXamlFile.GetText()?.ToString();
@@ -169,7 +169,7 @@ namespace {nameSpace}
DiagnosticSeverity.Error,
true),
Location.Create(xamlFileName, new TextSpan(0,0), new LinePositionSpan(new LinePosition(0,0),new LinePosition(0,0)))));
return;
continue;
}
try
@@ -189,7 +189,7 @@ namespace {nameSpace}
DiagnosticSeverity.Error,
true),
typeSymbol.Locations[0]));
return;
continue;
}
}
}

View File

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

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using Robust.Client.Interfaces;
using Robust.Client.Interfaces.Debugging;
@@ -7,15 +7,14 @@ 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;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Utility;
namespace Robust.Client
@@ -25,7 +24,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,18 +49,28 @@ 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;
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged);
_playMan.Initialize();
_debugDrawMan.Initialize();
Reset();
}
private void TickRateChanged(int tickrate)
{
if (GameInfo != null)
{
GameInfo.TickRate = (byte) tickrate;
}
_timing.TickRate = (byte) tickrate;
Logger.InfoS("client", $"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
}
/// <inheritdoc />
public void ConnectToServer(DnsEndPoint endPoint)
{
@@ -98,9 +107,39 @@ 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 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>
@@ -135,6 +174,7 @@ namespace Robust.Client
private void Reset()
{
_configManager.ClearReceivedInitialNwVars();
OnRunLevelChanged(ClientRunLevel.Initialize);
}
@@ -152,6 +192,7 @@ namespace Robust.Client
LastDisconnectReason = args.Reason;
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
_entityManager.Shutdown();
@@ -160,42 +201,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;
Logger.InfoS("client", $"Tickrate changed to: {message.NewTickRate} on tick {_timing.CurTick}");
}
private void OnLocalStatusChanged(object? obj, StatusEventArgs eventArgs)
{
// player finished fully connecting to the server.

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

@@ -709,7 +709,8 @@ namespace Robust.Client.Console.Commands
public bool Execute(IDebugConsole console, params string[] args)
{
var mgr = IoCManager.Resolve<ILightManager>();
mgr.Enabled = !mgr.Enabled;
if (!mgr.LockConsoleAccess)
mgr.Enabled = !mgr.Enabled;
return false;
}
}
@@ -722,10 +723,12 @@ namespace Robust.Client.Console.Commands
public bool Execute(IDebugConsole console, params string[] args)
{
var mgr = IoCManager.Resolve<IEyeManager>();
if (mgr.CurrentEye != null)
mgr.CurrentEye.DrawFov = !mgr.CurrentEye.DrawFov;
return false;
var lmgr = IoCManager.Resolve<ILightManager>();
var mgr = IoCManager.Resolve<IEyeManager>();
if (!lmgr.LockConsoleAccess)
if (mgr.CurrentEye != null)
mgr.CurrentEye.DrawFov = !mgr.CurrentEye.DrawFov;
return false;
}
}
@@ -738,7 +741,8 @@ namespace Robust.Client.Console.Commands
public bool Execute(IDebugConsole console, params string[] args)
{
var mgr = IoCManager.Resolve<ILightManager>();
mgr.DrawHardFov = !mgr.DrawHardFov;
if (!mgr.LockConsoleAccess)
mgr.DrawHardFov = !mgr.DrawHardFov;
return false;
}
}
@@ -752,7 +756,22 @@ namespace Robust.Client.Console.Commands
public bool Execute(IDebugConsole console, params string[] args)
{
var mgr = IoCManager.Resolve<ILightManager>();
mgr.DrawShadows = !mgr.DrawShadows;
if (!mgr.LockConsoleAccess)
mgr.DrawShadows = !mgr.DrawShadows;
return false;
}
}
internal class ToggleLightBuf : IConsoleCommand
{
public string Command => "togglelightbuf";
public string Description => "Toggles lighting rendering. This includes shadows but not FOV.";
public string Help => "togglelightbuf";
public bool Execute(IDebugConsole console, params string[] args)
{
var mgr = IoCManager.Resolve<ILightManager>();
if (!mgr.LockConsoleAccess)
mgr.DrawLighting = !mgr.DrawLighting;
return false;
}
}
@@ -781,6 +800,21 @@ namespace Robust.Client.Console.Commands
}
}
internal class GcFullCommand : IConsoleCommand
{
public string Command => "gcf";
public string Description => "Run the GC, fully, compacting LOH and everything.";
public string Help => "gcf";
public bool Execute(IDebugConsole console, params string[] args)
{
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Forced, true, true);
return false;
}
}
internal class GcModeCommand : IConsoleCommand
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,11 +31,11 @@ namespace Robust.Client.GameObjects
}
}
private static IDirectionalTextureProvider TextureForConfig(ObjectSerializer serializer, IResourceCache resourceCache)
private static IRsiStateLike TextureForConfig(ObjectSerializer serializer, IResourceCache resourceCache)
{
DebugTools.Assert(serializer.Reading);
if (serializer.TryGetCacheData<IDirectionalTextureProvider>(SerializationCache, out var dirTex))
if (serializer.TryGetCacheData<IRsiStateLike>(SerializationCache, out var dirTex))
{
return dirTex;
}
@@ -93,7 +93,7 @@ namespace Robust.Client.GameObjects
}
}
public static IDirectionalTextureProvider? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
public static IRsiStateLike? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
if (!prototype.Components.TryGetValue("Icon", out var mapping))
{

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);
@@ -1043,7 +1046,7 @@ namespace Robust.Client.GameObjects
var rsi = layer.RSI ?? BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
{
state = GetFallbackState();
state = GetFallbackState(resourceCache);
}
var layerSpecificDir = layer.EffectiveDirection(state, worldRotation, overrideDirection);
@@ -1276,7 +1279,7 @@ namespace Robust.Client.GameObjects
var rsi = layer.RSI ?? BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
{
state = GetFallbackState();
state = GetFallbackState(resourceCache);
}
if (!state.IsAnimated)
@@ -1404,7 +1407,7 @@ namespace Robust.Client.GameObjects
var rsi = layer.RSI ?? BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
{
state = GetFallbackState();
state = GetFallbackState(resourceCache);
}
if (state.IsAnimated)
@@ -1415,9 +1418,9 @@ namespace Robust.Client.GameObjects
}
}
private RSI.State GetFallbackState()
internal static RSI.State GetFallbackState(IResourceCache cache)
{
var rsi = resourceCache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
var rsi = cache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
return rsi["error"];
}
@@ -1735,14 +1738,14 @@ namespace Robust.Client.GameObjects
var rsi = ActualRsi;
if (rsi == null)
{
state = _parent.GetFallbackState();
state = GetFallbackState(_parent.resourceCache);
Logger.ErrorS(LogCategory, "No RSI to pull new state from! Trace:\n{0}", Environment.StackTrace);
}
else
{
if (!rsi.TryGetState(stateId, out state))
{
state = _parent.GetFallbackState();
state = GetFallbackState(_parent.resourceCache);
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId,
Environment.StackTrace);
}
@@ -1793,7 +1796,7 @@ namespace Robust.Client.GameObjects
}
}
public IDirectionalTextureProvider? Icon
public IRsiStateLike? Icon
{
get
{
@@ -1803,13 +1806,13 @@ namespace Robust.Client.GameObjects
var texture = layer.Texture;
if (!layer.State.IsValid) return null;
if (!layer.State.IsValid) return texture;
// Pull texture from RSI state instead.
var rsi = layer.RSI ?? BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
{
state = GetFallbackState();
state = GetFallbackState(resourceCache);
}
return state;
@@ -1867,21 +1870,21 @@ namespace Robust.Client.GameObjects
}
public static IDirectionalTextureProvider? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var icon = IconComponent.GetPrototypeIcon(prototype, resourceCache);
if (icon != null) return icon;
if (!prototype.Components.TryGetValue("Sprite", out var spriteNode))
if (!prototype.Components.ContainsKey("Sprite"))
{
return resourceCache.GetFallback<TextureResource>().Texture;
return GetFallbackState(resourceCache);
}
var dummy = new DummyIconEntity() {Prototype = prototype};
var dummy = new DummyIconEntity {Prototype = prototype};
var spriteComponent = dummy.AddComponent<SpriteComponent>();
dummy.Delete();
return spriteComponent?.Icon ?? resourceCache.GetFallback<TextureResource>().Texture;
return spriteComponent.Icon ?? GetFallbackState(resourceCache);
}
#region DummyIconEntity

View File

@@ -56,12 +56,12 @@ namespace Robust.Client.GameObjects.EntitySystems
private void PlayAudioPositionalHandler(PlayAudioPositionalMessage ev)
{
var gridId = ev.Coordinates.GetGridId(_entityManager);
if (!_mapManager.GridExists(gridId))
var mapId = ev.Coordinates.GetMapId(_entityManager);
if (!_mapManager.MapExists(mapId))
{
Logger.Error(
$"Server tried to play sound on grid {gridId}, which does not exist. Ignoring.");
$"Server tried to play sound on map {mapId}, which does not exist. Ignoring.");
return;
}
@@ -111,13 +111,13 @@ namespace Robust.Client.GameObjects.EntitySystems
if (stream.TrackingCoordinates != null)
{
var coords = stream.TrackingCoordinates.Value;
if (_mapManager.GridExists(coords.GetGridId(_entityManager)))
if (_mapManager.MapExists(coords.GetMapId(_entityManager)))
{
mapPos = stream.TrackingCoordinates.Value.ToMap(_entityManager);
}
else
{
// Grid no longer exists, delete stream.
// Map no longer exists, delete stream.
StreamDone(stream);
continue;
}

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

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

@@ -27,8 +27,6 @@ namespace Robust.Client.Graphics.Clyde
private const string UniProjViewMatrices = "projectionViewMatrices";
private const string UniUniformConstants = "uniformConstants";
private static readonly Color AmbientLightColor = Color.Black;
private const int BindingIndexProjView = 0;
private const int BindingIndexUniformConstants = 1;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Buffers;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
@@ -30,7 +31,11 @@ namespace Robust.Client.Graphics.Clyde
// Horizontal width, in pixels, of the shadow maps used to render FOV.
// I figured this was more accuracy sensitive than lights so resolution is significantly higher.
private const int FovMapSize = 2048;
private const int MaxLightsPerScene = 128;
// The maximum possible amount of lights in the light list.
// In the average case, the only cost of increasing this value is memory.
// If you are ever in a situation where this value needs to be increased, however, it will also implicitly cost some CPU time to sort the additional lights.
private const int LightsToRenderListSize = 2048;
private ClydeShaderInstance _fovDebugShaderInstance = default!;
@@ -80,16 +85,18 @@ namespace Robust.Client.Graphics.Clyde
// For depth calculation of lighting shadows.
private RenderTexture _shadowRenderTarget = default!;
// Used because otherwise a MaxLightsPerScene change callback getting hit on startup causes interesting issues (read: bugs)
private bool _shadowRenderTargetCanInitializeSafely = false;
// Proxies to textures of the above render targets.
private ClydeTexture FovTexture => _fovRenderTarget.Texture;
private ClydeTexture ShadowTexture => _shadowRenderTarget.Texture;
private readonly (PointLightComponent light, Vector2 pos)[] _lightsToRenderList
= new (PointLightComponent light, Vector2 pos)[MaxLightsPerScene];
private (PointLightComponent light, Vector2 pos, float distanceSquared)[] _lightsToRenderList = new (PointLightComponent light, Vector2 pos, float distanceSquared)[LightsToRenderListSize];
private unsafe void InitLighting()
{
// Other...
LoadLightingShaders();
{
@@ -160,10 +167,8 @@ namespace Robust.Client.Graphics.Clyde
}
// Shadow FBO.
_shadowRenderTarget = CreateRenderTarget((ShadowMapSize, MaxLightsPerScene),
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
new TextureSampleParameters {WrapMode = TextureWrapMode.Repeat, Filter = true},
nameof(_shadowRenderTarget));
_shadowRenderTargetCanInitializeSafely = true;
MaxLightsPerSceneChanged(_maxLightsPerScene);
}
private void LoadLightingShaders()
@@ -334,6 +339,14 @@ namespace Robust.Client.Graphics.Clyde
DrawFov(viewport, eye);
if (!_lightManager.DrawLighting)
{
BindRenderTargetFull(viewport.RenderTarget);
GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y);
CheckGlError();
return;
}
using (DebugGroup("Draw shadow depth"))
{
PrepareDepthDraw(RtToLoaded(_shadowRenderTarget));
@@ -344,7 +357,7 @@ namespace Robust.Client.Graphics.Clyde
{
for (var i = 0; i < count; i++)
{
var (light, lightPos) = lights[i];
var (light, lightPos, _) = lights[i];
DrawOcclusionDepth(lightPos, ShadowMapSize, light.Radius, i);
}
@@ -355,7 +368,7 @@ namespace Robust.Client.Graphics.Clyde
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
CheckGlError();
GLClearColor(Color.FromSrgb(AmbientLightColor));
GLClearColor(_lightManager.AmbientLightColor);
GL.Clear(ClearBufferMask.ColorBufferBit);
CheckGlError();
@@ -382,7 +395,7 @@ namespace Robust.Client.Graphics.Clyde
for (var i = 0; i < count; i++)
{
var (component, lightPos) = lights[i];
var (component, lightPos, _) = lights[i];
var transform = component.Owner.Transform;
@@ -473,25 +486,24 @@ namespace Robust.Client.Graphics.Clyde
_lightingReady = true;
}
private ((PointLightComponent light, Vector2 pos)[] lights, int count, Box2 expandedBounds)
private ((PointLightComponent light, Vector2 pos, float distanceSquared)[] lights, int count, Box2 expandedBounds)
GetLightsToRender(MapId map, in Box2 worldBounds)
{
// When culling occluders later, we can't just remove any occluders outside the worldBounds.
// As they could still affect the shadows of (large) light sources.
// We expand the world bounds so that it encompasses the center of every light source.
// This should make it so no culled occluder can make a difference.
// (if the occluder is in the current lights at all, it's still not between the light and the world bounds).
var expandedBounds = worldBounds;
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
var lightTree = renderingTreeSystem.GetLightTreeForMap(map);
var state = (this, expandedBounds, count: 0);
var state = (this, worldBounds, count: 0);
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 expandedBounds, int count) state, in PointLightComponent light) =>
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldBounds, int count) state, in PointLightComponent light) =>
{
var transform = light.Owner.Transform;
if (state.count >= LightsToRenderListSize)
{
// There are too many lights to fit in the static memory.
return false;
}
if (!light.Enabled || light.ContainerOccluded)
{
return true;
@@ -501,26 +513,45 @@ namespace Robust.Client.Graphics.Clyde
var circle = new Circle(lightPos, light.Radius);
if (!circle.Intersects(state.expandedBounds))
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
if (!circle.Intersects(state.worldBounds))
{
return true;
}
state.clyde._lightsToRenderList[state.count] = (light, lightPos);
state.count += 1;
state.expandedBounds = state.expandedBounds.ExtendToContain(lightPos);
if (state.count == MaxLightsPerScene)
{
// TODO: Allow more than MaxLightsPerScene lights.
return false;
}
float distanceSquared = (state.worldBounds.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
return true;
}, expandedBounds);
}, worldBounds);
return (_lightsToRenderList, state.count, state.expandedBounds);
if (state.count > _maxLightsPerScene)
{
// There are too many lights to fit in the scene.
// This check must occur before occluder expansion, or else bad things happen.
// Sort lights by distance.
Array.Sort(_lightsToRenderList, 0, state.count, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}));
// Then effectively delete the furthest lights.
state.count = _maxLightsPerScene;
}
// When culling occluders later, we can't just remove any occluders outside the worldBounds.
// As they could still affect the shadows of (large) light sources.
// We expand the world bounds so that it encompasses the center of every light source.
// This should make it so no culled occluder can make a difference.
// (if the occluder is in the current lights at all, it's still not between the light and the world bounds).
var expandedBounds = worldBounds;
for (var i = 0; i < state.count; i++)
{
var (_, lightPos, _) = _lightsToRenderList[i];
expandedBounds = expandedBounds.ExtendToContain(lightPos);
}
return (_lightsToRenderList, state.count, expandedBounds);
}
private void BlurOntoWalls(Viewport viewport, IEye eye)
@@ -959,6 +990,25 @@ namespace Robust.Client.Graphics.Clyde
RegenAllLightRts();
}
protected override void MaxLightsPerSceneChanged(int newValue)
{
_maxLightsPerScene = newValue;
// This guard is in place because otherwise the shadow FBO is initialized before GL is initialized.
if (!_shadowRenderTargetCanInitializeSafely)
return;
if (_shadowRenderTarget != null)
{
DeleteRenderTexture(_shadowRenderTarget.Handle);
}
// Shadow FBO.
_shadowRenderTarget = CreateRenderTarget((ShadowMapSize, _maxLightsPerScene),
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
new TextureSampleParameters {WrapMode = TextureWrapMode.Repeat, Filter = true},
nameof(_shadowRenderTarget));
}
protected override void SoftShadowsChanged(bool newValue)
{
_enableSoftShadows = newValue;

View File

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

View File

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

View File

@@ -63,6 +63,7 @@ namespace Robust.Client.Graphics.Clyde
private GLShaderProgram? _currentProgram;
private int _lightmapDivider = 2;
private int _maxLightsPerScene = 128;
private bool _enableSoftShadows = true;
private bool _checkGLErrors;
@@ -133,6 +134,7 @@ namespace Robust.Client.Graphics.Clyde
{
base.ReadConfig();
_lightmapDivider = _configurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = _configurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = _configurationManager.GetCVar(CVars.DisplaySoftShadows);
}

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ namespace Robust.Client.Graphics
_configurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
_configurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
return true;
@@ -76,6 +77,10 @@ namespace Robust.Client.Graphics
{
}
protected virtual void MaxLightsPerSceneChanged(int newValue)
{
}
protected virtual void SoftShadowsChanged(bool newValue)
{
}

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

@@ -0,0 +1,12 @@
namespace Robust.Client.Graphics
{
public interface IRsiStateLike : IDirectionalTextureProvider
{
RSI.State.DirectionType Directions { get; }
bool IsAnimated { get; }
int AnimationFrameCount { get; }
float GetDelay(int frame);
Texture GetFrame(RSI.State.Direction dir, int frame);
}
}

View File

@@ -1,4 +1,5 @@
using Robust.Client.Interfaces.Graphics.Lighting;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics.Lighting
{
@@ -7,5 +8,8 @@ namespace Robust.Client.Graphics.Lighting
public bool Enabled { get; set; } = true;
public bool DrawShadows { get; set; } = true;
public bool DrawHardFov { get; set; } = true;
public bool DrawLighting { get; set; } = true;
public bool LockConsoleAccess { get; set; } = false;
public Color AmbientLightColor { get; set; } = Color.FromSrgb(Color.Black);
}
}

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

@@ -16,7 +16,7 @@ namespace Robust.Client.Graphics
/// RSIs are folded into a single set of animation timings when loaded.
/// This is to simplify animation playback code in-engine.
/// </remarks>
public sealed class State : IDirectionalTextureProvider
public sealed class State : IRsiStateLike
{
// List of delays for the frame to reach the next frame.
private readonly float[] Delays;
@@ -80,6 +80,8 @@ namespace Robust.Client.Graphics
/// </summary>
public bool IsAnimated => DelayCount > 1;
int IRsiStateLike.AnimationFrameCount => DelayCount;
public Texture GetFrame(Direction direction, int frame)
{
return Icons[(int) direction][frame];

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

@@ -15,7 +15,7 @@ namespace Robust.Client.Graphics
/// Contains a texture used for drawing things.
/// </summary>
[PublicAPI]
public abstract class Texture : IDirectionalTextureProvider
public abstract class Texture : IRsiStateLike
{
/// <summary>
/// The width of the texture, in pixels.
@@ -85,6 +85,26 @@ namespace Robust.Client.Graphics
{
return this;
}
RSI.State.DirectionType IRsiStateLike.Directions => RSI.State.DirectionType.Dir1;
bool IRsiStateLike.IsAnimated => false;
int IRsiStateLike.AnimationFrameCount => 0;
float IRsiStateLike.GetDelay(int frame)
{
if (frame != 0)
throw new IndexOutOfRangeException();
return 0;
}
Texture IRsiStateLike.GetFrame(RSI.State.Direction dir, int frame)
{
if (frame != 0)
throw new IndexOutOfRangeException();
return this;
}
}
/// <summary>

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,32 @@
using Robust.Shared.Maths;
namespace Robust.Client.Interfaces.Graphics.Lighting
{
public interface ILightManager
{
/// <summary>
/// Enables/disables the entire light manager.
/// </summary>
bool Enabled { get; set; }
/// <summary>
/// Enables/disables shadows, but lights are still functional.
/// </summary>
bool DrawShadows { get; set; }
/// <summary>
/// Enables/disables hard FOV.
/// </summary>
bool DrawHardFov { get; set; }
/// <summary>
/// Enables/disables everything to do with the lighting buffer, without interfering with hard FOV.
/// </summary>
bool DrawLighting { get; set; }
/// <summary>
/// This is useful to prevent players messing with lighting setup when they shouldn't.
/// </summary>
bool LockConsoleAccess { get; set; }
/// <summary>
/// Ambient light. This is in linear-light, i.e. when providing a fixed colour, you must use Color.FromSrgb(Color.Black)!
/// </summary>
Color AmbientLightColor { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.Controls
{
/// <summary>
/// A more complex control wrapping <see cref="TextureRect"/> that can do RSI directions and animations.
/// </summary>
public sealed class AnimatedTextureRect : Control
{
private IRsiStateLike? _state;
private int _curFrame;
private float _curFrameTime;
/// <summary>
/// Internal TextureRect used to do actual drawing of the texture.
/// You can use this property to change shaders or styling or such.
/// </summary>
public TextureRect DisplayRect { get; }
public RSI.State.Direction RsiDirection { get; } = RSI.State.Direction.South;
public AnimatedTextureRect()
{
IoCManager.InjectDependencies(this);
DisplayRect = new TextureRect();
AddChild(DisplayRect);
}
public void SetFromSpriteSpecifier(SpriteSpecifier specifier)
{
_curFrame = 0;
_state = specifier.RsiStateLike();
_curFrameTime = _state.GetDelay(0);
DisplayRect.Texture = _state.GetFrame(RsiDirection, 0);
}
protected override void FrameUpdate(FrameEventArgs args)
{
if (!VisibleInTree || _state == null || !_state.IsAnimated)
return;
var oldFrame = _curFrame;
_curFrameTime -= args.DeltaSeconds;
while (_curFrameTime < _state.GetDelay(_curFrame))
{
_curFrame = (_curFrame + 1) % _state.AnimationFrameCount;
_curFrameTime += _state.GetDelay(_curFrame);
}
if (_curFrame != oldFrame)
{
DisplayRect.Texture = _state.GetFrame(RsiDirection, _curFrame);
}
}
}
}

View File

@@ -446,8 +446,8 @@ namespace Robust.Client.UserInterface.Controls
var (vSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
var hSep = _limitDimension == Dimension.Column ? hSepActual : vSepActual;
var vSep = _limitDimension == Dimension.Column ? vSepActual : hSepActual;
var width = _limitDimension == Dimension.Column ? Width : Height;
var height = _limitDimension == Dimension.Column ? Height : Width;
var width = _limitDimension == Dimension.Column ? PixelWidth : PixelHeight;
var height = _limitDimension == Dimension.Column ? PixelHeight : PixelWidth;
var stretchMaxX = width - hSep * (cols - 1);
var stretchMaxY = height - vSep * (rows - 1);

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
using JetBrains.Annotations;
using Robust.Client.Graphics.Drawing;
using Robust.Shared.Maths;
@@ -20,7 +19,7 @@ namespace Robust.Client.UserInterface.Controls
protected override void LayoutUpdateOverride()
{
var contentBox = _getStyleBox()?.GetContentBox(PixelSizeBox) ?? SizeBox;
var contentBox = _getStyleBox()?.GetContentBox(PixelSizeBox) ?? PixelSizeBox;
foreach (var child in Children)
{

View File

@@ -61,7 +61,7 @@ namespace Robust.Client.UserInterface.Controls
base.Draw(handle);
var bg = _getBackground();
bg?.Draw(handle, SizeBox);
bg?.Draw(handle, PixelSizeBox);
var fg = _getForeground();
if (fg == null)
@@ -69,10 +69,10 @@ namespace Robust.Client.UserInterface.Controls
return;
}
var minSize = fg.MinimumSize;
var size = Width * GetAsRatio() - minSize.X;
var size = PixelWidth * GetAsRatio() - minSize.X;
if (size > 0)
{
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, Height));
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight));
}
}

View File

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

View File

@@ -51,7 +51,7 @@ namespace Robust.Client.UserInterface.Controls
{
var oldHeight = _entry.Height;
var oldWidth = _entry.Width;
_entry.Update(font, MaxWidth ?? Width, UIScale);
_entry.Update(font, (MaxWidth ?? Width) * UIScale, UIScale);
if (oldHeight != _entry.Height || MaxWidth != null && _entry.Width != oldWidth)
{
MinimumSizeChanged();

View File

@@ -9,6 +9,7 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// Simple control that draws a single texture using a variety of possible stretching modes.
/// </summary>
/// <seealso cref="AnimatedTextureRect"/>
public class TextureRect : Control
{
public const string StylePropertyTexture = "texture";
@@ -28,8 +29,13 @@ namespace Robust.Client.UserInterface.Controls
get => _texture;
set
{
var oldSize = _texture?.Size;
_texture = value;
MinimumSizeChanged();
if (value?.Size != oldSize)
{
MinimumSizeChanged();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Renderable;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
@@ -11,66 +12,67 @@ using Robust.Shared.Utility;
namespace Robust.Client.Utility
{
/// <summary>
/// Helper methods for resolving <see cref="SpriteSpecifier"/>s.
/// </summary>
public static class SpriteSpecifierExt
{
public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache)
{
return cache
.GetResource<TextureResource>(SharedSpriteComponent.TextureRoot / texSpecifier.TexturePath)
.Texture;
}
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
{
if (cache.TryGetResource<RSIResource>(
SharedSpriteComponent.TextureRoot / rsiSpecifier.RsiPath,
out var theRsi))
{
if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
{
return state;
}
}
Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return SpriteComponent.GetFallbackState(cache);
}
public static Texture Frame0(this SpriteSpecifier specifier)
{
var resc = IoCManager.Resolve<IResourceCache>();
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return resc.GetResource<TextureResource>(SpriteComponent.TextureRoot / tex.TexturePath).Texture;
case SpriteSpecifier.Rsi rsi:
if (resc.TryGetResource<RSIResource>(SpriteComponent.TextureRoot / rsi.RsiPath, out var theRsi))
{
if (theRsi.RSI.TryGetState(rsi.RsiState, out var state))
{
return state.Frame0;
}
}
Logger.Error("Failed to load RSI {0}", rsi.RsiPath);
return resc.GetFallback<TextureResource>().Texture;
case SpriteSpecifier.EntityPrototype prototypeIcon:
var protMgr = IoCManager.Resolve<IPrototypeManager>();
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load EntityPrototype for EntityPrototypeId {0}", prototypeIcon.EntityPrototypeId);
return resc.GetFallback<TextureResource>().Texture;
}
return SpriteComponent.GetPrototypeIcon(prototype, resc)?.Default ?? resc.GetFallback<TextureResource>().Texture;
default:
throw new NotImplementedException();
}
return specifier.RsiStateLike().Default;
}
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
{
var resc = IoCManager.Resolve<IResourceCache>();
return specifier.RsiStateLike();
}
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
{
var resC = IoCManager.Resolve<IResourceCache>();
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return resc.GetResource<TextureResource>(SpriteComponent.TextureRoot / tex.TexturePath).Texture;
return tex.GetTexture(resC);
case SpriteSpecifier.Rsi rsi:
if (resc.TryGetResource<RSIResource>(SpriteComponent.TextureRoot / rsi.RsiPath, out var theRsi))
{
if (theRsi.RSI.TryGetState(rsi.RsiState, out var state))
{
return state;
}
}
return resc.GetFallback<TextureResource>().Texture;
return rsi.GetState(resC);
case SpriteSpecifier.EntityPrototype prototypeIcon:
var protMgr = IoCManager.Resolve<IPrototypeManager>();
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
return resc.GetFallback<TextureResource>().Texture;
return SpriteComponent.GetFallbackState(resC);
}
return SpriteComponent.GetPrototypeIcon(prototype, resc) ?? resc.GetFallback<TextureResource>().Texture;
return SpriteComponent.GetPrototypeIcon(prototype, resC);
default:
throw new NotImplementedException();
throw new NotSupportedException();
}
}
}

View File

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

View File

@@ -1,10 +1,12 @@
using System;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.ViewVariables.Editors
{
@@ -13,48 +15,32 @@ namespace Robust.Client.ViewVariables.Editors
protected override Control MakeUI(object? value)
{
DebugTools.Assert(value!.GetType().IsEnum);
var enumVal = (Enum)value;
var enumType = value.GetType();
var enumStorageType = enumType.GetEnumUnderlyingType();
var enumList = Enum.GetValues(enumType);
var hBox = new HBoxContainer
var optionButton = new OptionButton();
foreach (var val in enumList)
{
CustomMinimumSize = new Vector2(200, 0)
};
var label = val?.ToString();
if (label == null)
continue;
optionButton.AddItem(label, Convert.ToInt32(val));
}
var lineEdit = new LineEdit
{
Text = enumVal.ToString(),
Editable = !ReadOnly,
SizeFlagsHorizontal = Control.SizeFlags.FillExpand
};
optionButton.SelectId(Convert.ToInt32(value));
optionButton.Disabled = ReadOnly;
if (!ReadOnly)
{
lineEdit.OnTextEntered += e =>
var underlyingType = Enum.GetUnderlyingType(value.GetType());
optionButton.OnItemSelected += e =>
{
var parseSig = new []{typeof(string), typeof(NumberStyles), typeof(CultureInfo), enumStorageType.MakeByRefType()};
var parseMethod = enumStorageType.GetMethod("TryParse", parseSig);
DebugTools.AssertNotNull(parseMethod);
var parameters = new object?[] {e.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, null};
var parseWorked = (bool)parseMethod!.Invoke(null, parameters)!;
if (parseWorked) // textbox was the underlying type
{
DebugTools.AssertNotNull(parameters[3]);
ValueChanged(parameters[3]);
}
else if(Enum.TryParse(enumType, e.Text, true, out var enumValue))
{
var underlyingVal = Convert.ChangeType(enumValue, enumStorageType);
ValueChanged(underlyingVal);
}
optionButton.SelectId(e.Id);
ValueChanged(Convert.ChangeType(e.Id, underlyingType));
};
}
hBox.AddChild(lineEdit);
return hBox;
return optionButton;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Prometheus;
using Robust.Server.Console;
@@ -37,6 +38,7 @@ using Robust.Shared;
using Robust.Shared.Network.Messages;
using Robust.Server.DataMetrics;
using Robust.Server.Log;
using Robust.Server.Utility;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Serilog.Debugging;
@@ -236,7 +238,6 @@ namespace Robust.Server
{
netMan.Initialize(true);
netMan.StartServer();
netMan.RegisterNetMessage<MsgSetTickRate>(MsgSetTickRate.NAME);
}
catch (Exception e)
{
@@ -286,6 +287,8 @@ namespace Robust.Server
// Initialize Tier 2 services
IoCManager.Resolve<IGameTiming>().InSimulation = true;
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_stateManager.Initialize();
IoCManager.Resolve<IPlayerManager>().Initialize(MaxPlayers);
@@ -320,6 +323,11 @@ namespace Robust.Server
_stringSerializer.LockStrings();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeBeginPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}
return false;
}
@@ -459,7 +467,6 @@ namespace Robust.Server
_time.TickRate = b;
Logger.InfoS("game", $"Tickrate changed to: {b} on tick {_time.CurTick}");
SendTickRateUpdateToClients(b);
});
_time.TickRate = (byte) _config.GetCVar(CVars.NetTickrate);
@@ -469,17 +476,11 @@ namespace Robust.Server
Logger.InfoS("srv", $"Max players: {MaxPlayers}");
}
private void SendTickRateUpdateToClients(byte newTickRate)
{
var msg = _network.CreateNetMessage<MsgSetTickRate>();
msg.NewTickRate = newTickRate;
_network.ServerSendToAll(msg);
}
// called right before main loop returns, do all saving/cleanup in here
private void Cleanup()
{
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
// shut down networking, kicking all players.
_network.Shutdown($"Server shutting down: {_shutdownReason}");
@@ -500,6 +501,11 @@ namespace Robust.Server
AppDomain.CurrentDomain.ProcessExit -= ProcessExiting;
//TODO: This should prob shutdown all managers in a loop.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeEndPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}
}
private string UpdateBps()
@@ -528,6 +534,9 @@ namespace Robust.Server
ServerCurTick.Set(_time.CurTick.Value);
ServerCurTime.Set(_time.CurTime.TotalSeconds);
// These are always the same on the server, there is no prediction.
_time.LastRealTick = _time.CurTick;
UpdateTitle();
using (TickUsage.WithLabels("PreEngine").NewTimer())
@@ -535,6 +544,11 @@ namespace Robust.Server
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
}
using (TickUsage.WithLabels("NetworkedCVar").NewTimer())
{
IoCManager.Resolve<INetConfigurationManager>().TickProcessMessages();
}
using (TickUsage.WithLabels("Timers").NewTimer())
{
timerManager.UpdateTimers(frameEventArgs);

View File

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

View File

@@ -5,6 +5,7 @@ using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Maps;
using Robust.Server.Interfaces.Player;
using Robust.Server.Interfaces.Timing;
using Robust.Server.Maps;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
@@ -111,7 +112,7 @@ namespace Robust.Server.Console.Commands
{
public string Command => "loadbp";
public string Description => "Loads a blueprint from disk into the game.";
public string Help => "loadbp <MapID> <Path>";
public string Help => "loadbp <MapID> <Path> [storeUids]";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
@@ -141,8 +142,14 @@ namespace Robust.Server.Console.Commands
return;
}
var loadOptions = new MapLoadOptions();
if (args.Length > 2)
{
loadOptions.StoreMapUids = bool.Parse(args[2]);
}
var mapLoader = IoCManager.Resolve<IMapLoader>();
mapLoader.LoadBlueprint(mapId, args[1]);
mapLoader.LoadBlueprint(mapId, args[1], loadOptions);
}
}

View File

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

View File

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

View File

@@ -162,7 +162,7 @@ namespace Robust.Server.GameObjects.Components.UserInterface
_stateDirty = true;
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
@@ -183,8 +183,8 @@ namespace Robust.Server.GameObjects.Components.UserInterface
}
}
/// <summary>
/// Opens this interface for a specific client.
/// </summary>
@@ -263,6 +263,7 @@ namespace Robust.Server.GameObjects.Components.UserInterface
OnClosed?.Invoke(session);
_subscribedSessions.Remove(session);
_playerStateOverrides.Remove(session);
session.PlayerStatusChanged -= OnSessionOnPlayerStatusChanged;
if (_subscribedSessions.Count == 0)
{

View File

@@ -0,0 +1,18 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
/// <summary>
/// Metadata component used to keep consistent UIDs inside map files cross saving.
/// </summary>
/// <remarks>
/// This component stores the previous map UID of entities from map load.
/// This can then be used to re-serialize the entity with the same UID for the merge driver to recognize.
/// </remarks>
public sealed class MapSaveIdComponent : Component
{
public override string Name => "MapSaveId";
public int Uid { get; set; }
}
}

View File

@@ -73,6 +73,8 @@ namespace Robust.Server.GameObjects
Register<DebugExceptionInitializeComponent>();
Register<DebugExceptionStartupComponent>();
#endif
Register<MapSaveIdComponent>();
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Server.Maps;
using Robust.Shared.Map;
using YamlDotNet.RepresentationModel;
@@ -7,9 +8,11 @@ namespace Robust.Server.Interfaces.Maps
public interface IMapLoader
{
IMapGrid? LoadBlueprint(MapId mapId, string path);
IMapGrid? LoadBlueprint(MapId mapId, string path, MapLoadOptions options);
void SaveBlueprint(GridId gridId, string yamlPath);
void LoadMap(MapId mapId, string path);
void LoadMap(MapId mapId, string path, MapLoadOptions options);
void SaveMap(MapId mapId, string yamlPath);
event Action<YamlStream, string> LoadedMapData;

View File

@@ -0,0 +1,11 @@
namespace Robust.Server.Maps
{
public sealed class MapLoadOptions
{
/// <summary>
/// If true, UID components will be created for loaded entities
/// to maintain consistency upon subsequent savings.
/// </summary>
public bool StoreMapUids { get; set; }
}
}

View File

@@ -16,6 +16,7 @@ using Robust.Shared.GameObjects;
using System.Globalization;
using Robust.Shared.Interfaces.GameObjects;
using System.Linq;
using Robust.Server.GameObjects;
using Robust.Server.Interfaces.Timing;
using Robust.Shared.GameObjects.Components.Map;
using Robust.Shared.Interfaces.Serialization;
@@ -29,6 +30,8 @@ namespace Robust.Server.Maps
/// </summary>
public class MapLoader : IMapLoader
{
private static readonly MapLoadOptions DefaultLoadOptions = new();
private const int MapFormatVersion = 2;
[Dependency] private readonly IResourceManager _resMan = default!;
@@ -46,7 +49,8 @@ namespace Robust.Server.Maps
{
var grid = _mapManager.GetGrid(gridId);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
_componentManager, _prototypeManager);
context.RegisterGrid(grid);
var root = context.Serialize();
var document = new YamlDocument(root);
@@ -68,6 +72,11 @@ namespace Robust.Server.Maps
/// <inheritdoc />
public IMapGrid? LoadBlueprint(MapId mapId, string path)
{
return LoadBlueprint(mapId, path, DefaultLoadOptions);
}
public IMapGrid? LoadBlueprint(MapId mapId, string path, MapLoadOptions options)
{
TextReader reader;
var resPath = new ResourcePath(path).ToRootedPath();
@@ -108,7 +117,8 @@ namespace Robust.Server.Maps
throw new InvalidDataException("Cannot instance map with multiple grids as blueprint.");
}
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager, (YamlMappingNode)data.RootNode, mapId);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
_componentManager, _prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
context.Deserialize();
grid = context.Grids[0];
@@ -128,7 +138,8 @@ namespace Robust.Server.Maps
public void SaveMap(MapId mapId, string yamlPath)
{
Logger.InfoS("map", $"Saving map {mapId} to {yamlPath}");
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
_componentManager, _prototypeManager);
foreach (var grid in _mapManager.GetAllMapGrids(mapId))
{
context.RegisterGrid(grid);
@@ -149,11 +160,16 @@ namespace Robust.Server.Maps
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
}
}
Logger.InfoS("map", "Save completed!");
}
/// <inheritdoc />
public void LoadMap(MapId mapId, string path)
{
LoadMap(mapId, path, DefaultLoadOptions);
}
public void LoadMap(MapId mapId, string path, MapLoadOptions options)
{
TextReader reader;
var resPath = new ResourcePath(path).ToRootedPath();
@@ -188,7 +204,8 @@ namespace Robust.Server.Maps
LoadedMapData?.Invoke(data.Stream, resPath.ToString());
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _componentManager, _prototypeManager, (YamlMappingNode)data.RootNode, mapId);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
_componentManager, _prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
context.Deserialize();
if (!context.MapIsPostInit && _pauseManager.IsMapInitialized(mapId))
@@ -213,6 +230,7 @@ namespace Robust.Server.Maps
private readonly IComponentManager _componentManager;
private readonly IPrototypeManager _prototypeManager;
private readonly MapLoadOptions? _loadOptions;
private readonly Dictionary<GridId, int> GridIDMap = new();
public readonly List<IMapGrid> Grids = new();
@@ -225,8 +243,6 @@ namespace Robust.Server.Maps
private bool IsBlueprintMode => GridIDMap.Count == 1;
private int uidCounter;
private readonly YamlMappingNode RootNode;
private readonly MapId TargetMap;
@@ -239,7 +255,9 @@ namespace Robust.Server.Maps
public bool MapIsPostInit { get; private set; }
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities, IPauseManager pauseManager, IComponentManager componentManager, IPrototypeManager prototypeManager)
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities, IPauseManager pauseManager, IComponentManager componentManager,
IPrototypeManager prototypeManager)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
@@ -251,14 +269,17 @@ namespace Robust.Server.Maps
RootNode = new YamlMappingNode();
}
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs, IServerEntityManagerInternal entities,
IPauseManager pauseManager, IComponentManager componentManager, IPrototypeManager prototypeManager, YamlMappingNode node, MapId targetMapId)
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities,
IPauseManager pauseManager, IComponentManager componentManager, IPrototypeManager prototypeManager,
YamlMappingNode node, MapId targetMapId, MapLoadOptions options)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_pauseManager = pauseManager;
_componentManager = componentManager;
_loadOptions = options;
RootNode = node;
TargetMap = targetMapId;
@@ -438,8 +459,8 @@ namespace Robust.Server.Maps
var newId = new GridId?();
YamlGridSerializer.DeserializeGrid(
_mapManager, TargetMap, ref newId,
(YamlMappingNode)grid["settings"],
(YamlSequenceNode)grid["chunks"],
(YamlMappingNode) grid["settings"],
(YamlSequenceNode) grid["chunks"],
_tileMap!,
_tileDefinitionManager
);
@@ -489,6 +510,12 @@ namespace Robust.Server.Maps
Entities.Add(entity);
UidEntityMap.Add(uid, entity.Uid);
_entitiesToDeserialize.Add((entity, entityDef));
if (_loadOptions!.StoreMapUids)
{
var comp = entity.AddComponent<MapSaveIdComponent>();
comp.Uid = uid;
}
}
}
@@ -501,7 +528,7 @@ namespace Robust.Server.Maps
{
foreach (var compData in componentList)
{
CurrentReadingEntityComponents[compData["type"].AsString()] = (YamlMappingNode)compData;
CurrentReadingEntityComponents[compData["type"].AsString()] = (YamlMappingNode) compData;
}
}
@@ -605,15 +632,55 @@ namespace Robust.Server.Maps
private void PopulateEntityList()
{
var withUid = new List<MapSaveIdComponent>();
var withoutUid = new List<IEntity>();
var takenIds = new HashSet<int>();
foreach (var entity in _serverEntityManager.GetEntities())
{
if (IsMapSavable(entity))
{
var uid = uidCounter++;
EntityUidMap.Add(entity.Uid, uid);
Entities.Add(entity);
if (entity.TryGetComponent(out MapSaveIdComponent? mapSaveId))
{
withUid.Add(mapSaveId);
}
else
{
withoutUid.Add(entity);
}
}
}
// Go over entities with a MapSaveIdComponent and assign those.
foreach (var mapIdComp in withUid)
{
var uid = mapIdComp.Uid;
if (takenIds.Contains(uid))
{
// Duplicate ID. Just pretend it doesn't have an ID and use the without path.
withoutUid.Add(mapIdComp.Owner);
}
else
{
EntityUidMap.Add(mapIdComp.Owner.Uid, uid);
takenIds.Add(uid);
}
}
var uidCounter = 0;
foreach (var entity in withoutUid)
{
while (takenIds.Contains(uidCounter))
{
// Find next available UID.
uidCounter += 1;
}
EntityUidMap.Add(entity.Uid, uidCounter);
takenIds.Add(uidCounter);
}
}
private void WriteEntitySection()
@@ -621,7 +688,7 @@ namespace Robust.Server.Maps
var entities = new YamlSequenceNode();
RootNode.Add("entities", entities);
foreach (var entity in Entities)
foreach (var entity in Entities.OrderBy(e => EntityUidMap[e.Uid]))
{
CurrentWritingEntity = entity;
var mapping = new YamlMappingNode
@@ -638,6 +705,9 @@ namespace Robust.Server.Maps
// See engine#636 for why the Distinct() call.
foreach (var component in entity.GetAllComponents())
{
if (component is MapSaveIdComponent)
continue;
var compMapping = new YamlMappingNode();
CurrentWritingComponent = component.Name;
var compSerializer = YamlObjectSerializer.NewWriter(compMapping, this);
@@ -683,6 +753,7 @@ namespace Robust.Server.Maps
return true;
}
}
if (type == typeof(EntityUid))
{
if (node.AsString() == "null")
@@ -694,7 +765,8 @@ namespace Robust.Server.Maps
var val = node.AsInt();
if (val >= Entities.Count)
{
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val);
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.",
val);
}
else
{
@@ -702,12 +774,14 @@ namespace Robust.Server.Maps
return true;
}
}
if (typeof(IEntity).IsAssignableFrom(type))
{
var val = node.AsInt();
if (val >= Entities.Count)
{
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val);
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.",
val);
}
else
{
@@ -715,6 +789,7 @@ namespace Robust.Server.Maps
return true;
}
}
obj = null;
return false;
}
@@ -766,6 +841,7 @@ namespace Robust.Server.Maps
return true;
}
}
node = null;
return false;
}
@@ -878,7 +954,7 @@ namespace Robust.Server.Maps
}
Stream = stream;
GridCount = ((YamlSequenceNode)RootNode["grids"]).Children.Count;
GridCount = ((YamlSequenceNode) RootNode["grids"]).Children.Count;
}
}
}

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
@@ -68,8 +69,8 @@ namespace Robust.Server.ServerStatus
}
catch (Exception e)
{
apiContext.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
_httpSawmill.Error($"Exception in StatusHost: {e}");
apiContext.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
}
/*
@@ -188,15 +189,17 @@ namespace Robust.Server.ServerStatus
_listener!.Stop();
}
#pragma warning disable CS0649
[JsonObject(ItemRequired = Required.DisallowNull)]
private sealed class BuildInfo
{
[JsonProperty("engine_version")] public string EngineVersion = default!;
[JsonProperty("hash")] public string? Hash;
[JsonProperty("download")] public string? Download;
[JsonProperty("download")] public string? Download = default;
[JsonProperty("fork_id")] public string ForkId = default!;
[JsonProperty("version")] public string Version = default!;
}
#pragma warning restore CS0649
private sealed class ContextImpl : IStatusHandlerContext
{
@@ -234,12 +237,12 @@ namespace Robust.Server.ServerStatus
return serializer.Deserialize<T>(jsonReader);
}
public void Respond(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = "text/plain")
public void Respond(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = MediaTypeNames.Text.Plain)
{
Respond(text, (int) code, contentType);
}
public void Respond(string text, int code = 200, string contentType = "text/plain")
public void Respond(string text, int code = 200, string contentType = MediaTypeNames.Text.Plain)
{
_context.Response.StatusCode = code;
_context.Response.ContentType = contentType;
@@ -263,6 +266,8 @@ namespace Robust.Server.ServerStatus
{
using var streamWriter = new StreamWriter(_context.Response.OutputStream, EncodingHelpers.UTF8);
_context.Response.ContentType = MediaTypeNames.Application.Json;
using var jsonWriter = new JsonTextWriter(streamWriter);
JsonSerializer.Serialize(jsonWriter, jsonData);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -442,6 +442,7 @@ Types:
IsExternalInit: { All: True }
IsReadOnlyAttribute: { All: True }
IteratorStateMachineAttribute: { All: True }
PreserveBaseOverridesAttribute: { All: True }
RuntimeCompatibilityAttribute: { All: True }
RuntimeHelpers: { All: True }
TaskAwaiter: { All: True }

View File

@@ -14,45 +14,7 @@ namespace Robust.Shared
// On .NET Framework this doesn't need to run because:
// On Windows, the DLL names should check out correctly to just work.
// On Linux/macOS, Mono's DllMap handles it for us.
#if NETCOREAPP
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// DLL names should line up on Windows by default.
// So a hook won't do anything.
return;
}
NativeLibrary.SetDllImportResolver(assembly, (name, _, __) =>
{
if (name == $"{baseName}.dll")
{
var assemblyDir = Path.GetDirectoryName(assembly.Location);
if (assemblyDir == null)
{
return IntPtr.Zero;
}
string libName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
libName = $"lib{baseName}.so";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
libName = $"lib{baseName}.dylib";
}
else
{
throw new NotSupportedException();
}
return NativeLibrary.Load(Path.Combine(assemblyDir, libName));
}
return IntPtr.Zero;
});
#endif
RegisterExplicitMap(assembly, $"{baseName}.dll", $"lib{baseName}.so", $"lib{baseName}.dylib");
}
[Conditional("NETCOREAPP")]
@@ -62,32 +24,24 @@ namespace Robust.Shared
// On Windows, the DLL names should check out correctly to just work.
// On Linux/macOS, Mono's DllMap handles it for us.
#if NETCOREAPP
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
NativeLibrary.SetDllImportResolver(assembly, (name, assembly, path) =>
{
// DLL names should line up on Windows by default.
// So a hook won't do anything.
return;
}
NativeLibrary.SetDllImportResolver(assembly, (name, _, __) =>
{
if (name == baseName)
// Please keep in sync with what GLFWNative does.
// This particular API is only really used by the MIDI instruments stuff in SS14 right now,
// which means when it breaks people don't notice or report.
if (name != baseName)
{
string libName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
libName = linuxName;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
libName = macName;
}
else
{
throw new NotSupportedException();
}
return IntPtr.Zero;
}
return NativeLibrary.Load(libName);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return NativeLibrary.Load(linuxName, assembly, path);
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return NativeLibrary.Load(macName, assembly, path);
}
return IntPtr.Zero;

View File

@@ -206,8 +206,6 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public void RemoveComponents(EntityUid uid)
{
_entCompIndex.Remove(uid);
foreach (var comp in InSafeOrder(_entCompIndex[uid]))
{
RemoveComponentDeferred(comp, uid, false);
@@ -221,6 +219,10 @@ namespace Robust.Shared.GameObjects
{
RemoveComponentDeferred(comp, uid, true);
}
// DisposeComponents means the entity is getting deleted.
// Safe to wipe the entity out of the index.
_entCompIndex.Remove(uid);
}
private void RemoveComponentDeferred(Component component, EntityUid uid, bool removeProtected)
@@ -306,6 +308,7 @@ namespace Robust.Shared.GameObjects
var netId = component.NetID.Value;
_entNetIdDict[netId].Remove(entityUid);
_entCompIndex.Remove(entityUid, component);
// mark the owning entity as dirty for networking
component.Owner.Dirty();

View File

@@ -22,8 +22,8 @@ namespace Robust.Shared.GameObjects.Components.Appearance
public abstract T GetData<T>(string key);
public abstract T GetData<T>(Enum key);
public abstract bool TryGetData<T>(string key, [MaybeNullWhen(false)] out T data);
public abstract bool TryGetData<T>(Enum key, [MaybeNullWhen(false)] out T data);
public abstract bool TryGetData<T>(string key, [NotNullWhen(true)] out T data);
public abstract bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data);
[Serializable, NetSerializable]
protected class AppearanceComponentState : ComponentState

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Interfaces.Physics;
@@ -41,6 +41,7 @@ namespace Robust.Shared.GameObjects.Components
/// </summary>
bool Anchored { get; set; }
[Obsolete("Use AnchoredChangedMessage instead")]
event Action? AnchoredChanged;
bool Predict { get; set; }
@@ -344,11 +345,15 @@ namespace Robust.Shared.GameObjects.Components
return;
_anchored = value;
#pragma warning disable 618
AnchoredChanged?.Invoke();
#pragma warning restore 618
SendMessage(new AnchoredChangedMessage(Anchored));
Dirty();
}
}
[Obsolete("Use AnchoredChangedMessage instead")]
public event Action? AnchoredChanged;
[ViewVariables(VVAccess.ReadWrite)]
@@ -518,4 +523,14 @@ namespace Robust.Shared.GameObjects.Components
return !Anchored && Mass > 0;
}
}
public class AnchoredChangedMessage : ComponentMessage
{
public readonly bool Anchored;
public AnchoredChangedMessage(bool anchored)
{
Anchored = anchored;
}
}
}

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