mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6a9113144 | ||
|
|
c76d1d8c5c | ||
|
|
0e289873f5 | ||
|
|
3c7f87b8c3 | ||
|
|
d6c9420a74 | ||
|
|
43e67c0db9 | ||
|
|
ca68041199 | ||
|
|
2aa5e8c07f | ||
|
|
f8b2412855 | ||
|
|
04782b83ab | ||
|
|
e2f2b3a26d | ||
|
|
5a8464b518 | ||
|
|
5f1feb9bb1 | ||
|
|
196e2bb427 | ||
|
|
f8bebee904 | ||
|
|
6515b08b41 | ||
|
|
6e2f18d0d8 | ||
|
|
a2ecd63e9d | ||
|
|
6da9176410 | ||
|
|
ae9b771c8c | ||
|
|
d1e206864c | ||
|
|
f812eb1e27 | ||
|
|
1dec0dd980 | ||
|
|
7316d9e950 | ||
|
|
f7c2305bce | ||
|
|
ebc0fc9c60 | ||
|
|
d157aab786 | ||
|
|
70a5d1bad6 | ||
|
|
2471bf8b4b | ||
|
|
e7c5706b04 | ||
|
|
7b6d8a1465 | ||
|
|
31b750bd5b | ||
|
|
6ebd0eb4ae | ||
|
|
37eac8c73f | ||
|
|
44649eea1c | ||
|
|
caa0212282 | ||
|
|
7f9d08c8f9 | ||
|
|
1c128e6b74 | ||
|
|
099e7c5c48 | ||
|
|
2f76908efb | ||
|
|
5adde7d588 | ||
|
|
7b1bb7df47 | ||
|
|
8e5eb6ebbb | ||
|
|
87ef010348 | ||
|
|
229d1c248b | ||
|
|
8db606c4e4 | ||
|
|
6b5181269b | ||
|
|
17b84c3520 | ||
|
|
42875fc101 | ||
|
|
72a7bc2ae7 | ||
|
|
bef4f75419 | ||
|
|
216509f89d | ||
|
|
b7991204f1 | ||
|
|
02987ac703 | ||
|
|
5cf8cb262f | ||
|
|
49dfca169c | ||
|
|
33008a2bce | ||
|
|
c2e90132c0 | ||
|
|
9859b5b090 | ||
|
|
20aec0a8f9 | ||
|
|
a9ee78e40d | ||
|
|
69f36aac6f | ||
|
|
173b41ab9e | ||
|
|
7796d7f065 | ||
|
|
175c111be9 | ||
|
|
279cc0f83f | ||
|
|
b334d927a5 | ||
|
|
bdb9b9af2b | ||
|
|
5aa634b5eb | ||
|
|
60d7430fe7 | ||
|
|
a9aeff9b78 | ||
|
|
e7a0409645 | ||
|
|
3e718575ff | ||
|
|
746ec9eab7 | ||
|
|
60e6ecb0cc | ||
|
|
3a00e0d497 | ||
|
|
24a5020b42 | ||
|
|
bbb9e94ce9 | ||
|
|
2bea9576f0 | ||
|
|
6e0be2b8c9 | ||
|
|
0a348dd1be | ||
|
|
a95283913b | ||
|
|
ebc73a71c2 | ||
|
|
05321f0381 | ||
|
|
6554144c42 | ||
|
|
28e6cdc92f | ||
|
|
2ec715b70e | ||
|
|
424e0768c8 | ||
|
|
e1b9327ec0 | ||
|
|
80308a799f | ||
|
|
e30f8f3e69 | ||
|
|
302b910cf3 | ||
|
|
5a6a16e7dc | ||
|
|
8d84c56a72 | ||
|
|
13f821be5f | ||
|
|
d647ab1c61 | ||
|
|
b902ef290a | ||
|
|
058d897529 | ||
|
|
8e173a7a18 | ||
|
|
5443f77526 | ||
|
|
b406526592 | ||
|
|
48697da450 | ||
|
|
06f20ea722 | ||
|
|
427378f94d | ||
|
|
6cf5021efa | ||
|
|
ad9bda2efe | ||
|
|
f0bf251acf | ||
|
|
21f5fed32f | ||
|
|
cc67a47c32 | ||
|
|
e78e7bacfe | ||
|
|
6301008ac3 | ||
|
|
dfd572a0aa | ||
|
|
97fab99971 | ||
|
|
25d6bd908b | ||
|
|
fd5a8bf207 | ||
|
|
772076826a | ||
|
|
52d669e032 | ||
|
|
75fc9089c3 | ||
|
|
0972601a43 | ||
|
|
603c252c48 | ||
|
|
d5b1c044b7 | ||
|
|
4600f0531d | ||
|
|
c88498eca9 | ||
|
|
f15f1eb345 | ||
|
|
5be3ced05a | ||
|
|
7f03e88e97 | ||
|
|
8e3fa3e52d | ||
|
|
f9ae3e1fc2 | ||
|
|
bf9e95fa8a | ||
|
|
030a7d265b | ||
|
|
df70e94743 | ||
|
|
d68cd4d7eb | ||
|
|
d098881bff | ||
|
|
b8fbe32c27 | ||
|
|
02d2bd31e7 | ||
|
|
bd0dba0df0 |
Submodule Lidgren.Network/Lidgren.Network updated: dd285c9246...1dd5c1f333
4
MSBuild/Robust.Engine.Version.props
Normal file
4
MSBuild/Robust.Engine.Version.props
Normal file
@@ -0,0 +1,4 @@
|
||||
<Project>
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<PropertyGroup><Version>0.7.22</Version></PropertyGroup>
|
||||
</Project>
|
||||
@@ -6,4 +6,5 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<Import Project="Robust.Engine.Version.props" />
|
||||
</Project>
|
||||
|
||||
122
MSBuild/Robust.Trimming.targets
Normal file
122
MSBuild/Robust.Trimming.targets
Normal file
@@ -0,0 +1,122 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
|
||||
<!--
|
||||
Stuff for using ILLink trimming without self-contained deployments.
|
||||
This is not something officially supported by the .NET SDK currently, but we can simply run ILLink ourselves.
|
||||
-->
|
||||
|
||||
<!--
|
||||
A lot of stuff taken from Microsoft.NET.ILLink.targets in the SDK files.
|
||||
-->
|
||||
|
||||
<ItemDefinitionGroup>
|
||||
<RobustLinkRoots>
|
||||
<Visible>false</Visible>
|
||||
</RobustLinkRoots>
|
||||
<RobustLinkAssemblies>
|
||||
<Visible>false</Visible>
|
||||
</RobustLinkAssemblies>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<Target Name="RobustILLink"
|
||||
BeforeTargets="ILLink"
|
||||
Condition="'$(PublishTrimmed)' != 'true' And
|
||||
'$(RobustILLink)' == 'true'"
|
||||
DependsOnTargets="_ComputeAssembliesToPostprocessOnPublish">
|
||||
|
||||
<ComputeManagedAssemblies Assemblies="@(ResolvedFileToPublish)">
|
||||
<Output TaskParameter="ManagedAssemblies" ItemName="_ResolvedFileToPublishFiltered" />
|
||||
</ComputeManagedAssemblies>
|
||||
|
||||
<JoinItems Left="@(_ResolvedFileToPublishFiltered)" LeftKey="FileName" LeftMetadata="*"
|
||||
Right="@(RobustLinkRoots)"
|
||||
ItemSpecToUse="Left">
|
||||
<Output TaskParameter="JoinResult" ItemName="_RobustLinkRootsJoined" />
|
||||
</JoinItems>
|
||||
|
||||
<JoinItems Left="@(_ResolvedFileToPublishFiltered)" LeftKey="FileName" LeftMetadata="*"
|
||||
Right="@(RobustLinkAssemblies)"
|
||||
ItemSpecToUse="Left">
|
||||
<Output TaskParameter="JoinResult" ItemName="_RobustLinkAssembliesJoined" />
|
||||
</JoinItems>
|
||||
|
||||
<PropertyGroup>
|
||||
<TrimMode Condition=" '$(TrimMode)' == '' ">link</TrimMode>
|
||||
<TrimmerDefaultAction Condition=" '$(TrimmerDefaultAction)' == '' ">copy</TrimmerDefaultAction>
|
||||
<_ExtraTrimmerArgs>--skip-unresolved true $(_ExtraTrimmerArgs)</_ExtraTrimmerArgs>
|
||||
<ILLinkTreatWarningsAsErrors Condition=" '$(ILLinkTreatWarningsAsErrors)' == '' ">$(TreatWarningsAsErrors)</ILLinkTreatWarningsAsErrors>
|
||||
<TrimmerSingleWarn Condition=" '$(TrimmerSingleWarn)' == '' ">true</TrimmerSingleWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<RobustAssemblyToLink Include="@(_RobustLinkRootsJoined)">
|
||||
<TrimMode>Copy</TrimMode>
|
||||
</RobustAssemblyToLink>
|
||||
<RobustAssemblyToLink Include="@(_RobustLinkAssembliesJoined)">
|
||||
<TrimMode>Link</TrimMode>
|
||||
</RobustAssemblyToLink>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The linker implicitly picks up PDBs next to input assemblies. We will filter these out of the publish set. -->
|
||||
<__PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(RobustAssemblyToLink->'%(RelativeDir)%(Filename).pdb')" />
|
||||
<_PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(__PDBToLink)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_LinkedResolvedFileToPublishCandidate Include="@(RobustAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" />
|
||||
<_LinkedResolvedFileToPublishCandidate Include="@(_PDBToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--<Message Text="@(ResolvedFileToPublish)" Importance="high" />-->
|
||||
|
||||
<ItemGroup>
|
||||
<_TrimmerFeatureSettings Include="@(RuntimeHostConfigurationOption)"
|
||||
Condition="'%(RuntimeHostConfigurationOption.Trim)' == 'true'" />
|
||||
</ItemGroup>
|
||||
|
||||
<Delete Files="@(_LinkedResolvedFileToPublishCandidate)" />
|
||||
<ILLink AssemblyPaths="@(RobustAssemblyToLink)"
|
||||
ReferenceAssemblyPaths="@(ReferencePath)"
|
||||
RootAssemblyNames="@(RobustLinkRoots)"
|
||||
TrimMode="Skip"
|
||||
DefaultAction="$(TrimmerDefaultAction)"
|
||||
RemoveSymbols="false"
|
||||
FeatureSettings="@(_TrimmerFeatureSettings)"
|
||||
CustomData="@(_TrimmerCustomData)"
|
||||
|
||||
BeforeFieldInit="$(_TrimmerBeforeFieldInit)"
|
||||
OverrideRemoval="$(_TrimmerOverrideRemoval)"
|
||||
UnreachableBodies="$(_TrimmerUnreachableBodies)"
|
||||
UnusedInterfaces="$(_TrimmerUnusedInterfaces)"
|
||||
IPConstProp="$(_TrimmerIPConstProp)"
|
||||
Sealer="$(_TrimmerSealer)"
|
||||
|
||||
Warn="$(ILLinkWarningLevel)"
|
||||
NoWarn="$(NoWarn)"
|
||||
TreatWarningsAsErrors="$(ILLinkTreatWarningsAsErrors)"
|
||||
WarningsAsErrors="$(WarningsAsErrors)"
|
||||
WarningsNotAsErrors="$(WarningsNotAsErrors)"
|
||||
SingleWarn="$(TrimmerSingleWarn)"
|
||||
|
||||
CustomSteps="@(_TrimmerCustomSteps)"
|
||||
RootDescriptorFiles="@(TrimmerRootDescriptor)"
|
||||
OutputDirectory="$(IntermediateLinkDir)"
|
||||
DumpDependencies="$(_TrimmerDumpDependencies)"
|
||||
ExtraArgs="$(_ExtraTrimmerArgs)"
|
||||
ToolExe="$(_DotNetHostFileName)"
|
||||
ToolPath="$(_DotNetHostDirectory)"
|
||||
ContinueOnError="ErrorAndContinue">
|
||||
<Output TaskParameter="ExitCode" PropertyName="_ILLinkExitCode" />
|
||||
</ILLink>
|
||||
|
||||
<Touch Files="$(_LinkSemaphore)" AlwaysCreate="true" Condition=" '$(_ILLinkExitCode)' == '0' " />
|
||||
|
||||
<ItemGroup>
|
||||
<_LinkedResolvedFileToPublish Include="@(_LinkedResolvedFileToPublishCandidate)" Condition="Exists('%(Identity)')" />
|
||||
<ResolvedFileToPublish Remove="@(RobustAssemblyToLink)" />
|
||||
<ResolvedFileToPublish Remove="@(_PDBToLink)" />
|
||||
<ResolvedFileToPublish Include="@(_LinkedResolvedFileToPublish)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -18,20 +18,18 @@ namespace OpenToolkit.GraphicsLibraryFramework
|
||||
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;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return NativeLibrary.Load("libglfw.so.3", assembly, path);
|
||||
}
|
||||
string rName = null;
|
||||
if (OperatingSystem.IsLinux()) rName = "libglfw.so.3";
|
||||
else if (OperatingSystem.IsMacOS()) rName = "libglfw.3.dylib";
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return NativeLibrary.Load("libglfw.3.dylib", assembly, path);
|
||||
}
|
||||
if ((rName != null) && NativeLibrary.TryLoad(rName, assembly, path, out var handle))
|
||||
return handle;
|
||||
|
||||
return IntPtr.Zero;
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ Use the [content repo](https://github.com/space-wizards/space-station-14) for ac
|
||||
|
||||
## Documentation/Wiki
|
||||
|
||||
The [HackMD Wiki](https://hackmd.io/@ss14/docs/wiki) has documentation on SS14s content, engine, game design and more. We also have lots of resources for new contributors to the project.
|
||||
The [wiki](https://docs.spacestation14.io/) has documentation on SS14s content, engine, game design and more. We also have lots of resources for new contributors to the project.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -7,3 +7,8 @@
|
||||
- type: shader
|
||||
id: shaded
|
||||
kind: canvas
|
||||
|
||||
- type: shader
|
||||
id: bgra
|
||||
kind: source
|
||||
path: "/Shaders/Internal/bgra.swsl"
|
||||
|
||||
6
Resources/Shaders/Internal/bgra.swsl
Normal file
6
Resources/Shaders/Internal/bgra.swsl
Normal file
@@ -0,0 +1,6 @@
|
||||
// Swaps B and R channel so you can render BGRA stuff without swizzling at upload time.
|
||||
// Currently used by CEF.
|
||||
|
||||
void fragment() {
|
||||
COLOR = zTexture(UV).bgra;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using BenchmarkDotNet.Running;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Running;
|
||||
using System;
|
||||
|
||||
namespace Robust.Benchmarks
|
||||
{
|
||||
@@ -8,7 +10,14 @@ namespace Robust.Benchmarks
|
||||
// --anyCategories=ctg1,ctg2
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("\nWARNING: YOU ARE RUNNING A DEBUG BUILD, USE A RELEASE BUILD FOR AN ACCURATE BENCHMARK");
|
||||
Console.WriteLine("THE DEBUG BUILD IS ONLY GOOD FOR FIXING A CRASHING BENCHMARK\n");
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
|
||||
#else
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal sealed class ImageBuffer
|
||||
{
|
||||
public Image<Bgra32> Buffer { get; private set; } = new(1, 1);
|
||||
public Image<Rgba32> Buffer { get; private set; } = new(1, 1);
|
||||
|
||||
public unsafe void UpdateBuffer(int width, int height, IntPtr buffer, CefRectangle dirtyRect)
|
||||
{
|
||||
if (width != Buffer.Width || height != Buffer.Height)
|
||||
UpdateSize(width, height);
|
||||
|
||||
var span = new ReadOnlySpan<Bgra32>((void*) buffer, width * height);
|
||||
// NOTE: Image data from CEF is actually BGRA32, not RGBA32.
|
||||
// OpenGL ES does not allow uploading BGRA data, so we pretend it's RGBA32 and use a shader to swizzle it.
|
||||
var span = new ReadOnlySpan<Rgba32>((void*) buffer, width * height);
|
||||
|
||||
ImageSharpExt.Blit(
|
||||
span,
|
||||
@@ -28,7 +30,7 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
private void UpdateSize(int width, int height)
|
||||
{
|
||||
Buffer = new Image<Bgra32>(width, height);
|
||||
Buffer = new Image<Rgba32>(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
|
||||
{
|
||||
var impl = new ControlImpl(owner);
|
||||
var shader = _prototypeManager.Index<ShaderPrototype>("bgra");
|
||||
var shaderInstance = shader.Instance();
|
||||
var impl = new ControlImpl(owner, shaderInstance);
|
||||
_dependencyCollection.InjectDependencies(impl);
|
||||
return impl;
|
||||
}
|
||||
@@ -131,10 +133,12 @@ namespace Robust.Client.WebView.Cef
|
||||
[Dependency] private readonly IInputManager _inputMgr = default!;
|
||||
|
||||
public readonly WebViewControl Owner;
|
||||
private readonly ShaderInstance _shaderInstance;
|
||||
|
||||
public ControlImpl(WebViewControl owner)
|
||||
public ControlImpl(WebViewControl owner, ShaderInstance shaderInstance)
|
||||
{
|
||||
Owner = owner;
|
||||
_shaderInstance = shaderInstance;
|
||||
}
|
||||
|
||||
private const int ScrollSpeed = 50;
|
||||
@@ -183,7 +187,7 @@ namespace Robust.Client.WebView.Cef
|
||||
// Create the web browser! And by default, we go to about:blank.
|
||||
var browser = CefBrowserHost.CreateBrowserSync(info, client, settings, _startUrl);
|
||||
|
||||
var texture = _clyde.CreateBlankTexture<Bgra32>(Vector2i.One);
|
||||
var texture = _clyde.CreateBlankTexture<Rgba32>(Vector2i.One);
|
||||
|
||||
_data = new LiveData(texture, client, browser, renderer);
|
||||
}
|
||||
@@ -386,7 +390,7 @@ namespace Robust.Client.WebView.Cef
|
||||
_data.Browser.GetHost().NotifyMoveOrResizeStarted();
|
||||
_data.Browser.GetHost().WasResized();
|
||||
_data.Texture.Dispose();
|
||||
_data.Texture = _clyde.CreateBlankTexture<Bgra32>((Owner.PixelWidth, Owner.PixelHeight));
|
||||
_data.Texture = _clyde.CreateBlankTexture<Rgba32>((Owner.PixelWidth, Owner.PixelHeight));
|
||||
}
|
||||
|
||||
public void Draw(DrawingHandleScreen handle)
|
||||
@@ -404,6 +408,7 @@ namespace Robust.Client.WebView.Cef
|
||||
Math.Min(Owner.PixelWidth, bufImg.Width),
|
||||
Math.Min(Owner.PixelHeight, bufImg.Height)));
|
||||
|
||||
handle.UseShader(_shaderInstance);
|
||||
handle.DrawTexture(_data.Texture, Vector2.Zero);
|
||||
}
|
||||
|
||||
@@ -533,8 +538,7 @@ namespace Robust.Client.WebView.Cef
|
||||
if (_control.Owner.Disposed)
|
||||
return false;
|
||||
|
||||
// TODO CEF: Get actual scale factor?
|
||||
screenInfo.DeviceScaleFactor = 1.0f;
|
||||
screenInfo.DeviceScaleFactor = _control.Owner.UIScale;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
@@ -12,6 +13,7 @@ namespace Robust.Client.WebView.Cef
|
||||
private CefApp _app = default!;
|
||||
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputType>WinExe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -297,7 +297,7 @@ namespace Robust.Client.Audio.Midi
|
||||
set
|
||||
{
|
||||
lock (_playerStateLock)
|
||||
_player?.SetLoop(value ? -1 : 1);
|
||||
_player?.SetLoop(value ? -1 : 0);
|
||||
_loopMidi = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Client.Debugging;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Audio;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
@@ -82,8 +83,9 @@ namespace Robust.Client
|
||||
case GameController.DisplayMode.Headless:
|
||||
IoCManager.Register<IClyde, ClydeHeadless>();
|
||||
IoCManager.Register<IClipboardManager, ClydeHeadless>();
|
||||
IoCManager.Register<IClydeAudio, ClydeHeadless>();
|
||||
IoCManager.Register<IClydeInternal, ClydeHeadless>();
|
||||
IoCManager.Register<IClydeAudio, ClydeAudioHeadless>();
|
||||
IoCManager.Register<IClydeAudioInternal, ClydeAudioHeadless>();
|
||||
IoCManager.Register<IInputManager, InputManager>();
|
||||
IoCManager.Register<IFileDialogManager, DummyFileDialogManager>();
|
||||
IoCManager.Register<IUriOpener, UriOpenerDummy>();
|
||||
@@ -91,8 +93,9 @@ namespace Robust.Client
|
||||
case GameController.DisplayMode.Clyde:
|
||||
IoCManager.Register<IClyde, Clyde>();
|
||||
IoCManager.Register<IClipboardManager, Clyde>();
|
||||
IoCManager.Register<IClydeAudio, Clyde>();
|
||||
IoCManager.Register<IClydeInternal, Clyde>();
|
||||
IoCManager.Register<IClydeAudio, FallbackProxyClydeAudio>();
|
||||
IoCManager.Register<IClydeAudioInternal, FallbackProxyClydeAudio>();
|
||||
IoCManager.Register<IInputManager, ClydeInputManager>();
|
||||
IoCManager.Register<IFileDialogManager, FileDialogManager>();
|
||||
IoCManager.Register<IUriOpener, UriOpener>();
|
||||
|
||||
@@ -4,177 +4,186 @@ using Robust.Shared;
|
||||
using Robust.Shared.Utility;
|
||||
using C = System.Console;
|
||||
|
||||
namespace Robust.Client
|
||||
namespace Robust.Client;
|
||||
|
||||
internal sealed class CommandLineArgs
|
||||
{
|
||||
internal sealed class CommandLineArgs
|
||||
public MountOptions MountOptions { get; }
|
||||
public bool Headless { get; }
|
||||
public bool SelfContained { get; }
|
||||
public bool Connect { get; }
|
||||
public string ConnectAddress { get; }
|
||||
public string? Ss14Address { get; }
|
||||
public bool Launcher { get; }
|
||||
public string? Username { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> CVars { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
|
||||
public IReadOnlyList<string> ExecCommands { get; set; }
|
||||
|
||||
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
|
||||
// Also I don't like spending 100ms parsing command line args. Do you?
|
||||
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
|
||||
{
|
||||
public MountOptions MountOptions { get; }
|
||||
public bool Headless { get; }
|
||||
public bool SelfContained { get; }
|
||||
public bool Connect { get; }
|
||||
public string ConnectAddress { get; }
|
||||
public string? Ss14Address { get; }
|
||||
public bool Launcher { get; }
|
||||
public string? Username { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> CVars { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
|
||||
parsed = null;
|
||||
var headless = false;
|
||||
var selfContained = false;
|
||||
var connect = false;
|
||||
var connectAddress = "localhost";
|
||||
string? ss14Address = null;
|
||||
var launcher = false;
|
||||
string? username = null;
|
||||
var cvars = new List<(string, string)>();
|
||||
var logLevels = new List<(string, string)>();
|
||||
var mountOptions = new MountOptions();
|
||||
var execCommands = new List<string>();
|
||||
|
||||
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
|
||||
// Also I don't like spending 100ms parsing command line args. Do you?
|
||||
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
|
||||
using var enumerator = args.GetEnumerator();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
parsed = null;
|
||||
var headless = false;
|
||||
var selfContained = false;
|
||||
var connect = false;
|
||||
var connectAddress = "localhost";
|
||||
string? ss14Address = null;
|
||||
var launcher = false;
|
||||
string? username = null;
|
||||
var cvars = new List<(string, string)>();
|
||||
var logLevels = new List<(string, string)>();
|
||||
var mountOptions = new MountOptions();
|
||||
|
||||
using var enumerator = args.GetEnumerator();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
var arg = enumerator.Current;
|
||||
if (arg == "--connect")
|
||||
{
|
||||
var arg = enumerator.Current;
|
||||
if (arg == "--connect")
|
||||
connect = true;
|
||||
}
|
||||
else if (arg == "--connect-address")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
connect = true;
|
||||
}
|
||||
else if (arg == "--connect-address")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing connection address.");
|
||||
return false;
|
||||
}
|
||||
|
||||
connectAddress = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--ss14-address")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing SS14 address.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ss14Address = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--self-contained")
|
||||
{
|
||||
selfContained = true;
|
||||
}
|
||||
else if (arg == "--launcher")
|
||||
{
|
||||
launcher = true;
|
||||
}
|
||||
else if (arg == "--headless")
|
||||
{
|
||||
headless = true;
|
||||
}
|
||||
else if (arg == "--username")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing username.");
|
||||
return false;
|
||||
}
|
||||
|
||||
username = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--cvar")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing cvar value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var cvar = enumerator.Current;
|
||||
DebugTools.AssertNotNull(cvar);
|
||||
var pos = cvar.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in cvar.");
|
||||
return false;
|
||||
}
|
||||
|
||||
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
|
||||
}
|
||||
else if (arg == "--mount-zip")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.ZipMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--mount-dir")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.DirMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--loglevel")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing loglevel sawmill.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var loglevel = enumerator.Current;
|
||||
DebugTools.AssertNotNull(loglevel);
|
||||
var pos = loglevel.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in loglevel.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
|
||||
}
|
||||
else if (arg == "--help")
|
||||
{
|
||||
PrintHelp();
|
||||
C.WriteLine("Missing connection address.");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
C.WriteLine("Unknown argument: {0}", arg);
|
||||
}
|
||||
|
||||
connectAddress = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--ss14-address")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing SS14 address.");
|
||||
return false;
|
||||
}
|
||||
|
||||
parsed = new CommandLineArgs(
|
||||
headless,
|
||||
selfContained,
|
||||
connect,
|
||||
launcher,
|
||||
username,
|
||||
cvars,
|
||||
logLevels,
|
||||
connectAddress,
|
||||
ss14Address,
|
||||
mountOptions);
|
||||
ss14Address = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--self-contained")
|
||||
{
|
||||
selfContained = true;
|
||||
}
|
||||
else if (arg == "--launcher")
|
||||
{
|
||||
launcher = true;
|
||||
}
|
||||
else if (arg == "--headless")
|
||||
{
|
||||
headless = true;
|
||||
}
|
||||
else if (arg == "--username")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing username.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
username = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--cvar")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing cvar value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var cvar = enumerator.Current;
|
||||
DebugTools.AssertNotNull(cvar);
|
||||
var pos = cvar.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in cvar.");
|
||||
return false;
|
||||
}
|
||||
|
||||
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
|
||||
}
|
||||
else if (arg == "--mount-zip")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.ZipMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--mount-dir")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.DirMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--loglevel")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing loglevel sawmill.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var loglevel = enumerator.Current;
|
||||
DebugTools.AssertNotNull(loglevel);
|
||||
var pos = loglevel.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in loglevel.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
|
||||
}
|
||||
else if (arg == "--help")
|
||||
{
|
||||
PrintHelp();
|
||||
return false;
|
||||
}
|
||||
else if (arg.StartsWith("+"))
|
||||
{
|
||||
execCommands.Add(arg[1..]);
|
||||
}
|
||||
else
|
||||
{
|
||||
C.WriteLine("Unknown argument: {0}", arg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
C.WriteLine(@"
|
||||
parsed = new CommandLineArgs(
|
||||
headless,
|
||||
selfContained,
|
||||
connect,
|
||||
launcher,
|
||||
username,
|
||||
cvars,
|
||||
logLevels,
|
||||
connectAddress,
|
||||
ss14Address,
|
||||
mountOptions,
|
||||
execCommands);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
C.WriteLine(@"
|
||||
Usage: Robust.Client [options] [+command [+command]]
|
||||
|
||||
Options:
|
||||
--headless Run without graphics/audio/input.
|
||||
--self-contained Store data relative to executable instead of user-global locations.
|
||||
@@ -189,30 +198,34 @@ Options:
|
||||
--mount-dir Resource directory to mount.
|
||||
--mount-zip Resource zip to mount.
|
||||
--help Display this help text and exit.
|
||||
");
|
||||
}
|
||||
|
||||
private CommandLineArgs(
|
||||
bool headless,
|
||||
bool selfContained,
|
||||
bool connect,
|
||||
bool launcher,
|
||||
string? username,
|
||||
IReadOnlyCollection<(string key, string value)> cVars,
|
||||
IReadOnlyCollection<(string key, string value)> logLevels,
|
||||
string connectAddress, string? ss14Address,
|
||||
MountOptions mountOptions)
|
||||
{
|
||||
Headless = headless;
|
||||
SelfContained = selfContained;
|
||||
Connect = connect;
|
||||
Launcher = launcher;
|
||||
Username = username;
|
||||
CVars = cVars;
|
||||
LogLevels = logLevels;
|
||||
ConnectAddress = connectAddress;
|
||||
Ss14Address = ss14Address;
|
||||
MountOptions = mountOptions;
|
||||
}
|
||||
+command: You can pass a set of commands, prefixed by +,
|
||||
to be executed in the console in order after the game has finished initializing.
|
||||
");
|
||||
}
|
||||
|
||||
private CommandLineArgs(
|
||||
bool headless,
|
||||
bool selfContained,
|
||||
bool connect,
|
||||
bool launcher,
|
||||
string? username,
|
||||
IReadOnlyCollection<(string key, string value)> cVars,
|
||||
IReadOnlyCollection<(string key, string value)> logLevels,
|
||||
string connectAddress, string? ss14Address,
|
||||
MountOptions mountOptions,
|
||||
IReadOnlyList<string> execCommands)
|
||||
{
|
||||
Headless = headless;
|
||||
SelfContained = selfContained;
|
||||
Connect = connect;
|
||||
Launcher = launcher;
|
||||
Username = username;
|
||||
CVars = cVars;
|
||||
LogLevels = logLevels;
|
||||
ConnectAddress = connectAddress;
|
||||
Ss14Address = ss14Address;
|
||||
MountOptions = mountOptions;
|
||||
ExecCommands = execCommands;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace Robust.Client.Console.Commands
|
||||
case "aabbs":
|
||||
system.Flags ^= PhysicsDebugFlags.AABBs;
|
||||
break;
|
||||
case "com":
|
||||
system.Flags ^= PhysicsDebugFlags.COM;
|
||||
break;
|
||||
case "contactnormals":
|
||||
system.Flags ^= PhysicsDebugFlags.ContactNormals;
|
||||
break;
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace Robust.Client.Debugging
|
||||
ShapeInfo = 1 << 3,
|
||||
Joints = 1 << 4,
|
||||
AABBs = 1 << 5,
|
||||
COM = 1 << 6,
|
||||
}
|
||||
|
||||
internal sealed class PhysicsDebugOverlay : Overlay
|
||||
@@ -190,13 +191,13 @@ namespace Robust.Client.Debugging
|
||||
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||
}
|
||||
|
||||
private void DrawWorld(DrawingHandleWorld worldHandle)
|
||||
private void DrawWorld(DrawingHandleWorld worldHandle, OverlayDrawArgs args)
|
||||
{
|
||||
var viewport = _eyeManager.GetWorldViewport();
|
||||
var viewBounds = _eyeManager.GetWorldViewbounds();
|
||||
var viewBounds = args.WorldBounds;
|
||||
var viewAABB = args.WorldAABB;
|
||||
var mapId = _eyeManager.CurrentMap;
|
||||
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.Shapes) != 0 && !viewport.IsEmpty())
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.Shapes) != 0)
|
||||
{
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
|
||||
{
|
||||
@@ -237,7 +238,32 @@ namespace Robust.Client.Debugging
|
||||
}
|
||||
}
|
||||
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.AABBs) != 0 && !viewport.IsEmpty())
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.COM) != 0)
|
||||
{
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
|
||||
{
|
||||
Color color;
|
||||
const float Alpha = 0.25f;
|
||||
float size;
|
||||
|
||||
if (physBody.Owner.HasComponent<MapGridComponent>())
|
||||
{
|
||||
color = Color.Orange.WithAlpha(Alpha);
|
||||
size = 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
color = Color.Purple.WithAlpha(Alpha);
|
||||
size = 0.2f;
|
||||
}
|
||||
|
||||
var transform = physBody.GetTransform();
|
||||
|
||||
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), size, color);
|
||||
}
|
||||
}
|
||||
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.AABBs) != 0)
|
||||
{
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
|
||||
{
|
||||
@@ -271,7 +297,7 @@ namespace Robust.Client.Debugging
|
||||
{
|
||||
if (jointComponent.JointCount == 0 ||
|
||||
!_entityManager.TryGetComponent(jointComponent.Owner.Uid, out TransformComponent? xf1) ||
|
||||
!viewport.Contains(xf1.WorldPosition)) continue;
|
||||
!viewAABB.Contains(xf1.WorldPosition)) continue;
|
||||
|
||||
foreach (var (_, joint) in jointComponent.Joints)
|
||||
{
|
||||
@@ -312,7 +338,7 @@ namespace Robust.Client.Debugging
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawScreen(DrawingHandleScreen screenHandle)
|
||||
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
|
||||
{
|
||||
var mapId = _eyeManager.CurrentMap;
|
||||
var mousePos = _inputManager.MouseScreenPosition;
|
||||
@@ -359,10 +385,10 @@ namespace Robust.Client.Debugging
|
||||
switch (args.Space)
|
||||
{
|
||||
case OverlaySpace.ScreenSpace:
|
||||
DrawScreen((DrawingHandleScreen) args.DrawingHandle);
|
||||
DrawScreen((DrawingHandleScreen) args.DrawingHandle, args);
|
||||
break;
|
||||
case OverlaySpace.WorldSpace:
|
||||
DrawWorld((DrawingHandleWorld) args.DrawingHandle);
|
||||
DrawWorld((DrawingHandleWorld) args.DrawingHandle, args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -382,8 +408,8 @@ namespace Robust.Client.Debugging
|
||||
|
||||
if (edge.OneSided)
|
||||
{
|
||||
worldHandle.DrawCircle(v1, 0.5f, color);
|
||||
worldHandle.DrawCircle(v2, 0.5f, color);
|
||||
worldHandle.DrawCircle(v1, 0.1f, color);
|
||||
worldHandle.DrawCircle(v2, 0.1f, color);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -416,11 +442,45 @@ namespace Robust.Client.Debugging
|
||||
var p1 = matrix1.Transform(joint.LocalAnchorA);
|
||||
var p2 = matrix2.Transform(joint.LocalAnchorB);
|
||||
|
||||
var xfa = new Transform(xf1, xform1.WorldRotation);
|
||||
var xfb = new Transform(xf2, xform2.WorldRotation);
|
||||
|
||||
switch (joint)
|
||||
{
|
||||
case DistanceJoint:
|
||||
worldHandle.DrawLine(xf1, xf2, JointColor);
|
||||
break;
|
||||
case PrismaticJoint prisma:
|
||||
var pA = Transform.Mul(xfa, joint.LocalAnchorA);
|
||||
var pB = Transform.Mul(xfb, joint.LocalAnchorB);
|
||||
|
||||
var axis = Transform.Mul(xfa.Quaternion2D, prisma._localXAxisA);
|
||||
|
||||
Color c1 = new(0.7f, 0.7f, 0.7f);
|
||||
Color c2 = new(0.3f, 0.9f, 0.3f);
|
||||
Color c3 = new(0.9f, 0.3f, 0.3f);
|
||||
Color c4 = new(0.3f, 0.3f, 0.9f);
|
||||
Color c5 = new(0.4f, 0.4f, 0.4f);
|
||||
|
||||
worldHandle.DrawLine(pA, pB, c5);
|
||||
|
||||
if (prisma.EnableLimit)
|
||||
{
|
||||
var lower = pA + axis * prisma.LowerTranslation;
|
||||
var upper = pA + axis * prisma.UpperTranslation;
|
||||
var perp = Transform.Mul(xfa.Quaternion2D, prisma._localYAxisA);
|
||||
worldHandle.DrawLine(lower, upper, c1);
|
||||
worldHandle.DrawLine(lower - perp * 0.5f, lower + perp * 0.5f, c2);
|
||||
worldHandle.DrawLine(upper - perp * 0.5f, upper + perp * 0.5f, c3);
|
||||
}
|
||||
else
|
||||
{
|
||||
worldHandle.DrawLine(pA - axis * 1.0f, pA + axis * 1.0f, c1);
|
||||
}
|
||||
|
||||
worldHandle.DrawCircle(pA, 0.5f, c1);
|
||||
worldHandle.DrawCircle(pB, 0.5f, c4);
|
||||
break;
|
||||
default:
|
||||
worldHandle.DrawLine(xf1, p1, JointColor);
|
||||
worldHandle.DrawLine(p1, p2, JointColor);
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IViewVariablesManagerInternal _viewVariablesManager = default!;
|
||||
[Dependency] private readonly IDiscordRichPresence _discord = default!;
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;
|
||||
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
|
||||
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
|
||||
[Dependency] private readonly IScriptClient _scriptClient = default!;
|
||||
@@ -86,6 +87,7 @@ namespace Robust.Client
|
||||
internal bool StartupContinue(DisplayMode displayMode)
|
||||
{
|
||||
_clyde.InitializePostWindowing();
|
||||
_clydeAudio.InitializePostWindowing();
|
||||
_clyde.SetWindowTitle(Options.DefaultWindowTitle);
|
||||
|
||||
_taskManager.Initialize();
|
||||
@@ -198,6 +200,8 @@ namespace Robust.Client
|
||||
_client.ConnectToServer(LaunchState.ConnectEndpoint);
|
||||
}
|
||||
|
||||
ProgramShared.RunExecCommands(_console, _commandLineArgs?.ExecCommands);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -437,6 +441,7 @@ namespace Robust.Client
|
||||
private void Update(FrameEventArgs frameEventArgs)
|
||||
{
|
||||
_webViewHook?.Update();
|
||||
_clydeAudio.FrameProcess(frameEventArgs);
|
||||
_clyde.FrameProcess(frameEventArgs);
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePreEngine, frameEventArgs);
|
||||
_stateManager.FrameUpdate(frameEventArgs);
|
||||
@@ -535,6 +540,7 @@ namespace Robust.Client
|
||||
IoCManager.Resolve<IEntityLookup>().Shutdown();
|
||||
_entityManager.Shutdown();
|
||||
_clyde.Shutdown();
|
||||
_clydeAudio.Shutdown();
|
||||
}
|
||||
|
||||
private sealed record ResourceManifestData(string[] Modules);
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace Robust.Client.GameObjects
|
||||
RegisterClass<ClientOccluderComponent>();
|
||||
RegisterClass<OccluderTreeComponent>();
|
||||
RegisterClass<EyeComponent>();
|
||||
RegisterClass<AppearanceComponent>();
|
||||
RegisterClass<AnimationPlayerComponent>();
|
||||
RegisterClass<TimerComponent>();
|
||||
|
||||
|
||||
@@ -40,12 +40,12 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
void IClientEntityManagerInternal.InitializeEntity(IEntity entity)
|
||||
{
|
||||
base.InitializeEntity((Entity)entity);
|
||||
base.InitializeEntity(entity);
|
||||
}
|
||||
|
||||
void IClientEntityManagerInternal.StartEntity(IEntity entity)
|
||||
{
|
||||
base.StartEntity((Entity)entity);
|
||||
base.StartEntity(entity);
|
||||
}
|
||||
|
||||
#region IEntityNetworkManager impl
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(SharedAppearanceComponent))]
|
||||
public sealed class AppearanceComponent : SharedAppearanceComponent
|
||||
{
|
||||
[ViewVariables]
|
||||
private Dictionary<object, object> data = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("visuals")]
|
||||
internal List<AppearanceVisualizer> Visualizers = new();
|
||||
|
||||
[ViewVariables]
|
||||
private bool _appearanceDirty;
|
||||
|
||||
public override void SetData(string key, object value)
|
||||
{
|
||||
SetData(key, value);
|
||||
}
|
||||
|
||||
public override void SetData(Enum key, object value)
|
||||
{
|
||||
SetData(key, value);
|
||||
}
|
||||
|
||||
public override T GetData<T>(string key)
|
||||
{
|
||||
return (T) data[key];
|
||||
}
|
||||
|
||||
public override T GetData<T>(Enum key)
|
||||
{
|
||||
return (T) data[key];
|
||||
}
|
||||
|
||||
internal T GetData<T>(object key)
|
||||
{
|
||||
return (T) data[key];
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
internal bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
if (this.data.TryGetValue(key, out var dat))
|
||||
{
|
||||
data = (T) dat;
|
||||
return true;
|
||||
}
|
||||
|
||||
data = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetData(object key, object value)
|
||||
{
|
||||
if (data.TryGetValue(key, out var existing) && existing.Equals(value)) return;
|
||||
|
||||
data[key] = value;
|
||||
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
if (curState is not AppearanceComponentState actualState)
|
||||
return;
|
||||
|
||||
var stateDiff = data.Count != actualState.Data.Count;
|
||||
|
||||
if (!stateDiff)
|
||||
{
|
||||
foreach (var (key, value) in data)
|
||||
{
|
||||
if (!actualState.Data.TryGetValue(key, out var stateValue) ||
|
||||
!value.Equals(stateValue))
|
||||
{
|
||||
stateDiff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stateDiff) return;
|
||||
|
||||
data = actualState.Data;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
internal void MarkDirty()
|
||||
{
|
||||
if (_appearanceDirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EntitySystem.Get<AppearanceSystem>().EnqueueUpdate(this);
|
||||
_appearanceDirty = true;
|
||||
}
|
||||
|
||||
internal void UnmarkDirty()
|
||||
{
|
||||
_appearanceDirty = false;
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
foreach (var visual in Visualizers)
|
||||
{
|
||||
visual.InitializeEntity(Owner);
|
||||
}
|
||||
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the visualization of data inside of an appearance component.
|
||||
/// Implementations of this class are NOT bound to a specific entity, they are flyweighted across multiple.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class AppearanceVisualizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an entity to be managed by this appearance controller.
|
||||
/// DO NOT assume this is your only entity. Visualizers are shared.
|
||||
/// </summary>
|
||||
public virtual void InitializeEntity(IEntity entity)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever appearance data for an entity changes.
|
||||
/// Update its visuals here.
|
||||
/// </summary>
|
||||
/// <param name="component">The appearance component of the entity that might need updating.</param>
|
||||
public virtual void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the visualization of data inside of an appearance component.
|
||||
/// Implementations of this class are NOT bound to a specific entity, they are flyweighted across multiple.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class AppearanceVisualizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an entity to be managed by this appearance controller.
|
||||
/// DO NOT assume this is your only entity. Visualizers are shared.
|
||||
/// </summary>
|
||||
public virtual void InitializeEntity(IEntity entity)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever appearance data for an entity changes.
|
||||
/// Update its visuals here.
|
||||
/// </summary>
|
||||
/// <param name="component">The appearance component of the entity that might need updating.</param>
|
||||
public virtual void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// This is the client instance of <see cref="AppearanceComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(AppearanceComponent))]
|
||||
public sealed class ClientAppearanceComponent : AppearanceComponent
|
||||
{
|
||||
[ViewVariables]
|
||||
private bool _appearanceDirty;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("visuals")]
|
||||
internal List<AppearanceVisualizer> Visualizers = new();
|
||||
|
||||
protected override void MarkDirty()
|
||||
{
|
||||
if (_appearanceDirty)
|
||||
return;
|
||||
|
||||
EntitySystem.Get<AppearanceSystem>().EnqueueUpdate(this);
|
||||
_appearanceDirty = true;
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
foreach (var visual in Visualizers)
|
||||
{
|
||||
visual.InitializeEntity(Owner);
|
||||
}
|
||||
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
internal void UnmarkDirty()
|
||||
{
|
||||
_appearanceDirty = false;
|
||||
}
|
||||
}
|
||||
@@ -74,10 +74,10 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
|
||||
var position = Owner.Transform.Coordinates;
|
||||
void CheckDir(Direction dir, OccluderDir oclDir)
|
||||
{
|
||||
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
|
||||
var position = Owner.Transform.Coordinates;
|
||||
foreach (var neighbor in grid.GetInDir(position, dir))
|
||||
{
|
||||
if (Owner.EntityManager.TryGetComponent(neighbor, out ClientOccluderComponent? comp) && comp.Enabled)
|
||||
@@ -88,10 +88,20 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
CheckDir(Direction.North, OccluderDir.North);
|
||||
CheckDir(Direction.East, OccluderDir.East);
|
||||
CheckDir(Direction.South, OccluderDir.South);
|
||||
CheckDir(Direction.West, OccluderDir.West);
|
||||
var angle = Owner.Transform.LocalRotation;
|
||||
var dirRolling = angle.GetCardinalDir();
|
||||
// dirRolling starts at effective south
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.South);
|
||||
dirRolling = dirRolling.GetClockwise90Degrees();
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.West);
|
||||
dirRolling = dirRolling.GetClockwise90Degrees();
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.North);
|
||||
dirRolling = dirRolling.GetClockwise90Degrees();
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.East);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
@@ -213,6 +213,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
layer.Color = layerDatum.Color;
|
||||
layer.Rotation = layerDatum.Rotation;
|
||||
layer._offset = layerDatum.Offset;
|
||||
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
|
||||
layer.Visible = anyTextureAttempted && layerDatum.Visible;
|
||||
layer.Scale = layerDatum.Scale;
|
||||
@@ -1259,7 +1260,7 @@ namespace Robust.Client.GameObjects
|
||||
if (worldRotation.Theta < 0)
|
||||
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
|
||||
|
||||
var localMatrix = GetLocalMatrix();
|
||||
var spriteMatrix = GetLocalMatrix();
|
||||
|
||||
foreach (var layer in Layers)
|
||||
{
|
||||
@@ -1270,9 +1271,10 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
var numDirs = GetLayerDirectionCount(layer);
|
||||
var layerRotation = worldRotation + layer.Rotation;
|
||||
var layerPosition = worldPosition + layerRotation.RotateVec(layer._offset);
|
||||
|
||||
CalcModelMatrix(numDirs, eyeRotation, layerRotation, worldPosition, out var modelMatrix);
|
||||
Matrix3.Multiply(ref localMatrix, ref modelMatrix, out var transformMatrix);
|
||||
CalcModelMatrix(numDirs, eyeRotation, layerRotation, layerPosition, out var modelMatrix);
|
||||
Matrix3.Multiply(ref spriteMatrix, ref modelMatrix, out var transformMatrix);
|
||||
drawingHandle.SetTransform(in transformMatrix);
|
||||
|
||||
RenderLayer(drawingHandle, layer, eyeRotation, layerRotation, overrideDirection);
|
||||
@@ -1290,7 +1292,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
var layerColor = color * layer.Color;
|
||||
|
||||
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter) + layer.Offset;
|
||||
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter);
|
||||
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
|
||||
var quad = Box2.FromDimensions(position, textureSize);
|
||||
|
||||
@@ -1723,7 +1725,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 _offset;
|
||||
internal Vector2 _offset;
|
||||
|
||||
[ViewVariables]
|
||||
public DirectionOffset DirOffset { get; set; }
|
||||
|
||||
@@ -14,8 +14,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private static Box2 SpriteAabbFunc(in SpriteComponent value)
|
||||
{
|
||||
var worldPos = value.Owner.Transform.WorldPosition;
|
||||
var worldRot = value.Owner.Transform.WorldRotation;
|
||||
var (worldPos, worldRot) = value.Owner.Transform.GetWorldPositionRotation();
|
||||
var bounds = new Box2Rotated(value.CalculateBoundingBox(worldPos), worldRot, worldPos);
|
||||
var tree = RenderingTreeSystem.GetRenderTree(value.Owner);
|
||||
|
||||
@@ -50,7 +49,6 @@ namespace Robust.Client.GameObjects
|
||||
internal static Box2 LightAabbFunc(PointLightComponent value, Vector2? worldPos = null)
|
||||
{
|
||||
// Lights are circles so don't need entity's rotation
|
||||
|
||||
worldPos ??= value.Owner.Transform.WorldPosition;
|
||||
var tree = RenderingTreeSystem.GetRenderTree(value.Owner);
|
||||
var boxSize = value.Radius * 2;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -7,9 +7,9 @@ namespace Robust.Client.GameObjects
|
||||
[UsedImplicitly]
|
||||
internal sealed class AppearanceSystem : EntitySystem
|
||||
{
|
||||
private readonly Queue<AppearanceComponent> _queuedUpdates = new();
|
||||
private readonly Queue<ClientAppearanceComponent> _queuedUpdates = new();
|
||||
|
||||
public void EnqueueUpdate(AppearanceComponent component)
|
||||
public void EnqueueUpdate(ClientAppearanceComponent component)
|
||||
{
|
||||
_queuedUpdates.Enqueue(component);
|
||||
}
|
||||
|
||||
@@ -440,9 +440,6 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int DefaultSoundRange => 25;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects
|
||||
SubscribeNetworkEvent<EffectSystemMessage>(CreateEffect);
|
||||
SubscribeLocalEvent<EffectSystemMessage>(CreateEffect);
|
||||
|
||||
var overlay = new EffectOverlay(this, prototypeManager, _mapManager, _playerManager, _entityManager);
|
||||
var overlay = new EffectOverlay(this, prototypeManager, _playerManager, _entityManager);
|
||||
overlayManager.AddOverlay(overlay);
|
||||
}
|
||||
|
||||
@@ -335,14 +335,12 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private readonly ShaderInstance _unshadedShader;
|
||||
private readonly EffectSystem _owner;
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
|
||||
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager)
|
||||
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IPlayerManager playerMan, IEntityManager entityManager)
|
||||
{
|
||||
_owner = owner;
|
||||
_unshadedShader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
|
||||
_mapManager = mapMan;
|
||||
_playerManager = playerMan;
|
||||
_entityManager = entityManager;
|
||||
}
|
||||
@@ -371,13 +369,18 @@ namespace Robust.Client.GameObjects
|
||||
currentShader = newShader;
|
||||
}
|
||||
|
||||
// TODO: Should be doing matrix transformations
|
||||
var effectSprite = effect.EffectSprite;
|
||||
var effectOrigin = effect.AttachedEntity?.Transform.MapPosition.Position + effect.AttachedOffset ??
|
||||
effect.Coordinates.ToMapPos(_entityManager);
|
||||
|
||||
var coordinates =
|
||||
(effect.AttachedEntity?.Transform.Coordinates ?? effect.Coordinates)
|
||||
.Offset(effect.AttachedOffset);
|
||||
|
||||
var rotation = _entityManager.GetComponent<TransformComponent>(coordinates.EntityId).WorldRotation;
|
||||
var effectOrigin = coordinates.ToMapPos(_entityManager);
|
||||
|
||||
var effectArea = Box2.CenteredAround(effectOrigin, effect.Size);
|
||||
|
||||
var rotatedBox = new Box2Rotated(effectArea, effect.Rotation, effectOrigin);
|
||||
var rotatedBox = new Box2Rotated(effectArea, effect.Rotation - rotation, effectOrigin);
|
||||
|
||||
worldHandle.DrawTextureRect(effectSprite, rotatedBox, ToColor(effect.Color));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -61,23 +64,40 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
var currentMap = _eyeManager.CurrentMap;
|
||||
var viewport = _eyeManager.GetWorldViewport();
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
|
||||
{
|
||||
var gridEnt = _entityManager.GetEntity(grid.GridEntityId);
|
||||
|
||||
if (!_entityManager.TryGetComponent<PhysicsComponent>(gridEnt.Uid, out var body)) continue;
|
||||
var gridInternal = (IMapGridInternal)grid;
|
||||
var worldMatrix = _entityManager.GetComponent<TransformComponent>(gridEnt.Uid).WorldMatrix;
|
||||
worldHandle.SetTransform(worldMatrix);
|
||||
var transform = new Transform(Vector2.Zero, Angle.Zero);
|
||||
|
||||
var transform = body.GetTransform();
|
||||
gridInternal.GetMapChunks(viewport, out var chunkEnumerator);
|
||||
|
||||
foreach (var fixture in body.Fixtures)
|
||||
while (chunkEnumerator.MoveNext(out var chunk))
|
||||
{
|
||||
for (var i = 0; i < fixture.Shape.ChildCount; i++)
|
||||
foreach (var fixture in chunk.Fixtures)
|
||||
{
|
||||
var aabb = fixture.Shape.ComputeAABB(transform, i);
|
||||
var poly = (PolygonShape) fixture.Shape;
|
||||
|
||||
args.WorldHandle.DrawRect(aabb, Color.Green.WithAlpha(0.2f));
|
||||
args.WorldHandle.DrawRect(aabb, Color.Red.WithAlpha(0.5f), false);
|
||||
var verts = new Vector2[poly.Vertices.Length];
|
||||
|
||||
for (var i = 0; i < poly.Vertices.Length; i++)
|
||||
{
|
||||
verts[i] = Transform.Mul(transform, poly.Vertices[i]);
|
||||
}
|
||||
|
||||
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Green.WithAlpha(0.2f));
|
||||
|
||||
for (var i = 0; i < fixture.Shape.ChildCount; i++)
|
||||
{
|
||||
var aabb = fixture.Shape.ComputeAABB(transform, i);
|
||||
|
||||
args.WorldHandle.DrawRect(aabb, Color.Red.WithAlpha(0.5f), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
internal static RenderingTreeComponent? GetRenderTree(IEntity entity)
|
||||
{
|
||||
if (entity.Transform.MapID == MapId.Nullspace ||
|
||||
if (entity.Deleted || entity.Transform.MapID == MapId.Nullspace ||
|
||||
entity.HasComponent<RenderingTreeComponent>()) return null;
|
||||
|
||||
var parent = entity.Transform.Parent?.Owner;
|
||||
@@ -265,7 +265,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private bool IsVisible(SpriteComponent component)
|
||||
{
|
||||
return component.Visible && !component.ContainerOccluded;
|
||||
return component.Visible && !component.ContainerOccluded && !component.Deleted;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
@@ -312,7 +312,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
light.TreeUpdateQueued = false;
|
||||
|
||||
if (!light.Enabled || light.ContainerOccluded)
|
||||
if (light.Deleted || !light.Enabled || light.ContainerOccluded)
|
||||
{
|
||||
ClearLight(light);
|
||||
continue;
|
||||
|
||||
@@ -18,6 +18,7 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
@@ -39,6 +40,8 @@ namespace Robust.Client.GameStates
|
||||
_pendingSystemMessages
|
||||
= new();
|
||||
|
||||
private readonly Dictionary<EntityUid, MapId> _hiddenEntities = new();
|
||||
|
||||
private uint _metaCompNetId;
|
||||
|
||||
[Dependency] private readonly IComponentFactory _compFactory = default!;
|
||||
@@ -379,7 +382,6 @@ namespace Robust.Client.GameStates
|
||||
var outputData = new Dictionary<EntityUid, Dictionary<uint, ComponentState>>();
|
||||
|
||||
Debug.Assert(_players.LocalPlayer != null, "_players.LocalPlayer != null");
|
||||
var player = _players.LocalPlayer.Session;
|
||||
|
||||
var bus = _entityManager.EventBus;
|
||||
|
||||
@@ -390,7 +392,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
foreach (var (netId, component) in _entityManager.GetNetComponents(createdEntity))
|
||||
{
|
||||
var state = _entityManager.GetComponentState(bus, component, player);
|
||||
var state = _entityManager.GetComponentState(bus, component);
|
||||
|
||||
if(state.GetType() == typeof(ComponentState))
|
||||
continue;
|
||||
@@ -426,16 +428,22 @@ namespace Robust.Client.GameStates
|
||||
ReadOnlySpan<EntityState> nextEntStates)
|
||||
{
|
||||
var toApply = new Dictionary<IEntity, (EntityState?, EntityState?)>();
|
||||
var toInitialize = new List<Entity>();
|
||||
var toInitialize = new List<IEntity>();
|
||||
var created = new List<EntityUid>();
|
||||
var toHide = new List<EntityUid>();
|
||||
var toShow = new List<EntityUid>();
|
||||
|
||||
foreach (var es in curEntStates)
|
||||
{
|
||||
EntityUid uid;
|
||||
//Known entities
|
||||
if (_entities.TryGetEntity(es.Uid, out var entity))
|
||||
{
|
||||
// Logger.Debug($"[{IGameTiming.TickStampStatic}] MOD {es.Uid}");
|
||||
toApply.Add(entity, (es, null));
|
||||
if(_hiddenEntities.ContainsKey(es.Uid))
|
||||
toShow.Add(es.Uid);
|
||||
uid = es.Uid;
|
||||
}
|
||||
else //Unknown entities
|
||||
{
|
||||
@@ -445,11 +453,14 @@ namespace Robust.Client.GameStates
|
||||
throw new InvalidOperationException($"Server sent new entity state for {es.Uid} without metadata component!");
|
||||
}
|
||||
// Logger.Debug($"[{IGameTiming.TickStampStatic}] CREATE {es.Uid} {metaState.PrototypeId}");
|
||||
var newEntity = (Entity)_entities.CreateEntity(metaState.PrototypeId, es.Uid);
|
||||
var newEntity = _entities.CreateEntity(metaState.PrototypeId, es.Uid);
|
||||
toApply.Add(newEntity, (es, null));
|
||||
toInitialize.Add(newEntity);
|
||||
created.Add(newEntity.Uid);
|
||||
uid = newEntity.Uid;
|
||||
}
|
||||
if(es.Hide)
|
||||
toHide.Add(uid);
|
||||
}
|
||||
|
||||
foreach (var es in nextEntStates)
|
||||
@@ -471,7 +482,7 @@ namespace Robust.Client.GameStates
|
||||
foreach (var kvStates in toApply)
|
||||
{
|
||||
var ent = kvStates.Key;
|
||||
var entity = (Entity) ent;
|
||||
var entity = ent;
|
||||
HandleEntityState(entity, _entities.EventBus, kvStates.Value.Item1,
|
||||
kvStates.Value.Item2);
|
||||
}
|
||||
@@ -483,7 +494,7 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
HashSet<Entity> brokenEnts = new HashSet<Entity>();
|
||||
HashSet<IEntity> brokenEnts = new HashSet<IEntity>();
|
||||
#endif
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
@@ -523,13 +534,6 @@ namespace Robust.Client.GameStates
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if (brokenEnts.Contains(entity))
|
||||
continue;
|
||||
#endif
|
||||
}
|
||||
#if EXCEPTION_TOLERANCE
|
||||
foreach (var entity in brokenEnts)
|
||||
{
|
||||
@@ -537,6 +541,21 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var entityUid in toHide)
|
||||
{
|
||||
if(_entityManager.HasComponent<MapGridComponent>(entityUid)) continue;
|
||||
|
||||
var xform = _entityManager.GetComponent<TransformComponent>(entityUid);
|
||||
_hiddenEntities.Add(entityUid, xform.MapID);
|
||||
xform.ChangeMapId(MapId.Nullspace);
|
||||
}
|
||||
|
||||
foreach (var entityUid in toShow)
|
||||
{
|
||||
_entityManager.GetComponent<TransformComponent>(entityUid).ChangeMapId(_hiddenEntities[entityUid]);
|
||||
_hiddenEntities.Remove(entityUid);
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
|
||||
74
Robust.Client/Graphics/Audio/ClydeAudio.ALDisposeQueues.cs
Normal file
74
Robust.Client/Graphics/Audio/ClydeAudio.ALDisposeQueues.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit.Audio.OpenAL;
|
||||
using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenToolkit.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Log;
|
||||
using Vector2 = Robust.Shared.Maths.Vector2;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
// Used to track audio sources that were disposed in the finalizer thread,
|
||||
// so we need to properly send them off in the main thread.
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
|
||||
|
||||
private void _flushALDisposeQueues()
|
||||
{
|
||||
// Clear out finalized audio sources.
|
||||
while (_sourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
_openALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_audioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized buffered audio sources.
|
||||
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
_openALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_bufferedAudioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized audio buffers.
|
||||
while (_bufferDisposeQueue.TryDequeue(out var handle))
|
||||
{
|
||||
AL.DeleteBuffer((int) handle);
|
||||
_checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
|
||||
{
|
||||
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
|
||||
{
|
||||
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
private void DeleteAudioBufferOnMainThread(int bufferHandle)
|
||||
{
|
||||
_bufferDisposeQueue.Enqueue(bufferHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,436 +10,20 @@ using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenToolkit.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Log;
|
||||
using Vector2 = Robust.Shared.Maths.Vector2;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class Clyde
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
private ALDevice _openALDevice;
|
||||
private ALContext _openALContext;
|
||||
|
||||
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
|
||||
new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
|
||||
new();
|
||||
|
||||
private readonly HashSet<string> _alcDeviceExtensions = new();
|
||||
private readonly HashSet<string> _alContextExtensions = new();
|
||||
|
||||
// Used to track audio sources that were disposed in the finalizer thread,
|
||||
// so we need to properly send them off in the main thread.
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
|
||||
|
||||
// The base gain value for a listener, used to boost the default volume.
|
||||
private const float _baseGain = 2f;
|
||||
|
||||
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
|
||||
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
|
||||
|
||||
internal bool IsEfxSupported;
|
||||
|
||||
private ISawmill _openALSawmill = default!;
|
||||
|
||||
private void _initializeAudio()
|
||||
{
|
||||
_openALSawmill = Logger.GetSawmill("clyde.oal");
|
||||
|
||||
_audioOpenDevice();
|
||||
|
||||
// Create OpenAL context.
|
||||
_audioCreateContext();
|
||||
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
|
||||
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
|
||||
}
|
||||
|
||||
private void _audioCreateContext()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
|
||||
}
|
||||
|
||||
ALC.MakeContextCurrent(_openALContext);
|
||||
_checkAlcError(_openALDevice);
|
||||
_checkAlError();
|
||||
|
||||
// Load up AL context extensions.
|
||||
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alContextExtensions.Add(extension);
|
||||
}
|
||||
|
||||
_openALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
|
||||
_openALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
|
||||
_openALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
|
||||
}
|
||||
|
||||
private void _audioOpenDevice()
|
||||
{
|
||||
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
|
||||
|
||||
// Open device.
|
||||
if (!string.IsNullOrEmpty(preferredDevice))
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(preferredDevice);
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
_openALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
|
||||
preferredDevice, ALC.GetError(ALDevice.Null));
|
||||
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
|
||||
_checkAlcError(_openALDevice);
|
||||
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to open OpenAL device! {ALC.GetError(ALDevice.Null)}");
|
||||
}
|
||||
|
||||
// Load up ALC extensions.
|
||||
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alcDeviceExtensions.Add(extension);
|
||||
}
|
||||
}
|
||||
|
||||
private void _shutdownAudio()
|
||||
{
|
||||
foreach (var source in _audioSources.Values.ToArray())
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var source in _bufferedAudioSources.Values.ToArray())
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (_openALContext != ALContext.Null)
|
||||
{
|
||||
ALC.DestroyContext(_openALContext);
|
||||
}
|
||||
|
||||
if (_openALDevice != IntPtr.Zero)
|
||||
{
|
||||
ALC.CloseDevice(_openALDevice);
|
||||
}
|
||||
}
|
||||
|
||||
private void _updateAudio()
|
||||
{
|
||||
var eye = _eyeManager.CurrentEye;
|
||||
var (x, y) = eye.Position.Position;
|
||||
AL.Listener(ALListener3f.Position, x, y, -5);
|
||||
var rot2d = eye.Rotation.ToVec();
|
||||
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
|
||||
|
||||
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
|
||||
var (rotX, rotY) = eye.Rotation.ToVec();
|
||||
var at = new Vector3(0f, 0f, -1f);
|
||||
var up = new Vector3(rotY, rotX, 0f);
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
|
||||
// Clear out finalized audio sources.
|
||||
while (_sourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
_openALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_audioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized buffered audio sources.
|
||||
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
_openALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_bufferedAudioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized audio buffers.
|
||||
while (_bufferDisposeQueue.TryDequeue(out var handle))
|
||||
{
|
||||
AL.DeleteBuffer((int) handle);
|
||||
_checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
|
||||
{
|
||||
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
|
||||
}
|
||||
|
||||
public void SetAudioAttenuation(int value)
|
||||
{
|
||||
var attenuation = (Attenuation) value;
|
||||
|
||||
switch (attenuation)
|
||||
{
|
||||
case Attenuation.NoAttenuation:
|
||||
AL.DistanceModel(ALDistanceModel.None);
|
||||
break;
|
||||
case Attenuation.InverseDistance:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistance);
|
||||
break;
|
||||
case Attenuation.Default:
|
||||
case Attenuation.InverseDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
|
||||
break;
|
||||
case Attenuation.LinearDistance:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistance);
|
||||
break;
|
||||
case Attenuation.LinearDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
|
||||
break;
|
||||
case Attenuation.ExponentDistance:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistance);
|
||||
break;
|
||||
case Attenuation.ExponentDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
|
||||
}
|
||||
|
||||
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
|
||||
|
||||
_openALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
// TODO: This really shouldn't be indexing based on the ClydeHandle...
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value.Value].BufferHandle);
|
||||
|
||||
var audioSource = new AudioSource(this, source, stream);
|
||||
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
|
||||
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
|
||||
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
private void _checkAlcError(ALDevice device,
|
||||
[CallerMemberName] string callerMember = "",
|
||||
[CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = ALC.GetError(device);
|
||||
if (error != AlcError.NoError)
|
||||
{
|
||||
_openALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
private void _checkAlError([CallerMemberName] string callerMember = "",
|
||||
[CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = AL.GetError();
|
||||
if (error != ALError.NoError)
|
||||
{
|
||||
_openALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
var vorbis = _readOggVorbis(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
// NVorbis only supports loading into floats.
|
||||
// If this becomes a problem due to missing extension support (doubt it but ok),
|
||||
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
|
||||
if (vorbis.Channels == 1)
|
||||
{
|
||||
format = ALFormat.MonoFloat32Ext;
|
||||
}
|
||||
else if (vorbis.Channels == 2)
|
||||
{
|
||||
format = ALFormat.StereoFloat32Ext;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (float* ptr = vorbis.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
|
||||
(int) vorbis.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
|
||||
return new AudioStream(handle, length, (int) vorbis.Channels, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
var wav = _readWav(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
if (wav.BitsPerSample == 16)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono16;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo16;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else if (wav.BitsPerSample == 8)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono8;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo8;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* ptr = wav.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
|
||||
return new AudioStream(handle, length, wav.NumChannels, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var fmt = channels switch
|
||||
{
|
||||
1 => ALFormat.Mono16,
|
||||
2 => ALFormat.Stereo16,
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(channels), "Only stereo and mono is currently supported")
|
||||
};
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
_checkAlError();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (short* ptr = samples)
|
||||
{
|
||||
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
return new AudioStream(handle, length, channels, name);
|
||||
}
|
||||
|
||||
private sealed class LoadedAudioSample
|
||||
{
|
||||
public readonly int BufferHandle;
|
||||
|
||||
public LoadedAudioSample(int bufferHandle)
|
||||
{
|
||||
BufferHandle = bufferHandle;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
|
||||
{
|
||||
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
|
||||
{
|
||||
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
private void DeleteAudioBufferOnMainThread(int bufferHandle)
|
||||
{
|
||||
_bufferDisposeQueue.Enqueue(bufferHandle);
|
||||
}
|
||||
|
||||
private sealed class AudioSource : IClydeAudioSource
|
||||
{
|
||||
private int SourceHandle;
|
||||
private readonly Clyde _master;
|
||||
private readonly ClydeAudio _master;
|
||||
private readonly AudioStream _sourceStream;
|
||||
private int FilterHandle;
|
||||
#if DEBUG
|
||||
@@ -450,7 +34,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private bool IsEfxSupported => _master.IsEfxSupported;
|
||||
|
||||
public AudioSource(Clyde master, int sourceHandle, AudioStream sourceStream)
|
||||
public AudioSource(ClydeAudio master, int sourceHandle, AudioStream sourceStream)
|
||||
{
|
||||
_master = master;
|
||||
SourceHandle = sourceHandle;
|
||||
@@ -700,7 +284,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private int? SourceHandle = null;
|
||||
private int[] BufferHandles;
|
||||
private Dictionary<int, int> BufferMap = new();
|
||||
private readonly Clyde _master;
|
||||
private readonly ClydeAudio _master;
|
||||
private bool _mono = true;
|
||||
private bool _float = false;
|
||||
private int FilterHandle;
|
||||
@@ -711,7 +295,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private bool IsEfxSupported => _master.IsEfxSupported;
|
||||
|
||||
public BufferedAudioSource(Clyde master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
|
||||
public BufferedAudioSource(ClydeAudio master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
|
||||
{
|
||||
_master = master;
|
||||
SourceHandle = sourceHandle;
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class Clyde
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
private OggVorbisData _readOggVorbis(Stream stream)
|
||||
{
|
||||
@@ -3,9 +3,9 @@ using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class Clyde
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
/// <summary>
|
||||
/// Load up a WAVE file.
|
||||
50
Robust.Client/Graphics/Audio/ClydeAudio.IoC.cs
Normal file
50
Robust.Client/Graphics/Audio/ClydeAudio.IoC.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit.Audio.OpenAL;
|
||||
using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenToolkit.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Vector2 = Robust.Shared.Maths.Vector2;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
private Thread? _gameThread;
|
||||
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
_gameThread = Thread.CurrentThread;
|
||||
return _initializeAudio();
|
||||
}
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
_updateAudio();
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_shutdownAudio();
|
||||
}
|
||||
|
||||
private bool IsMainThread()
|
||||
{
|
||||
return Thread.CurrentThread == _gameThread;
|
||||
}
|
||||
}
|
||||
}
|
||||
399
Robust.Client/Graphics/Audio/ClydeAudio.cs
Normal file
399
Robust.Client/Graphics/Audio/ClydeAudio.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit.Audio.OpenAL;
|
||||
using OpenToolkit.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenToolkit.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Log;
|
||||
using Vector2 = Robust.Shared.Maths.Vector2;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio : IClydeAudio, IClydeAudioInternal
|
||||
{
|
||||
private ALDevice _openALDevice;
|
||||
private ALContext _openALContext;
|
||||
|
||||
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
|
||||
new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
|
||||
new();
|
||||
|
||||
private readonly HashSet<string> _alcDeviceExtensions = new();
|
||||
private readonly HashSet<string> _alContextExtensions = new();
|
||||
|
||||
// The base gain value for a listener, used to boost the default volume.
|
||||
private const float _baseGain = 2f;
|
||||
|
||||
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
|
||||
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
|
||||
|
||||
internal bool IsEfxSupported;
|
||||
|
||||
private ISawmill _openALSawmill = default!;
|
||||
|
||||
private bool _initializeAudio()
|
||||
{
|
||||
_openALSawmill = Logger.GetSawmill("clyde.oal");
|
||||
|
||||
if (!_audioOpenDevice())
|
||||
return false;
|
||||
|
||||
// Create OpenAL context.
|
||||
_audioCreateContext();
|
||||
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
|
||||
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void _audioCreateContext()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
|
||||
}
|
||||
|
||||
ALC.MakeContextCurrent(_openALContext);
|
||||
_checkAlcError(_openALDevice);
|
||||
_checkAlError();
|
||||
|
||||
// Load up AL context extensions.
|
||||
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alContextExtensions.Add(extension);
|
||||
}
|
||||
|
||||
_openALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
|
||||
_openALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
|
||||
_openALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
|
||||
}
|
||||
|
||||
private bool _audioOpenDevice()
|
||||
{
|
||||
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
|
||||
|
||||
// Open device.
|
||||
if (!string.IsNullOrEmpty(preferredDevice))
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(preferredDevice);
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
_openALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
|
||||
preferredDevice, ALC.GetError(ALDevice.Null));
|
||||
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
|
||||
_checkAlcError(_openALDevice);
|
||||
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
_openALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load up ALC extensions.
|
||||
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alcDeviceExtensions.Add(extension);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void _shutdownAudio()
|
||||
{
|
||||
foreach (var source in _audioSources.Values.ToArray())
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var source in _bufferedAudioSources.Values.ToArray())
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (_openALContext != ALContext.Null)
|
||||
{
|
||||
ALC.DestroyContext(_openALContext);
|
||||
}
|
||||
|
||||
if (_openALDevice != IntPtr.Zero)
|
||||
{
|
||||
ALC.CloseDevice(_openALDevice);
|
||||
}
|
||||
}
|
||||
|
||||
private void _updateAudio()
|
||||
{
|
||||
var eye = _eyeManager.CurrentEye;
|
||||
var (x, y) = eye.Position.Position;
|
||||
AL.Listener(ALListener3f.Position, x, y, -5);
|
||||
var rot2d = eye.Rotation.ToVec();
|
||||
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
|
||||
|
||||
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
|
||||
var (rotX, rotY) = eye.Rotation.ToVec();
|
||||
var at = new Vector3(0f, 0f, -1f);
|
||||
var up = new Vector3(rotY, rotX, 0f);
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
|
||||
_flushALDisposeQueues();
|
||||
}
|
||||
|
||||
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
|
||||
{
|
||||
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
|
||||
}
|
||||
|
||||
public void SetAudioAttenuation(int value)
|
||||
{
|
||||
var attenuation = (Attenuation) value;
|
||||
|
||||
switch (attenuation)
|
||||
{
|
||||
case Attenuation.NoAttenuation:
|
||||
AL.DistanceModel(ALDistanceModel.None);
|
||||
break;
|
||||
case Attenuation.InverseDistance:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistance);
|
||||
break;
|
||||
case Attenuation.Default:
|
||||
case Attenuation.InverseDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
|
||||
break;
|
||||
case Attenuation.LinearDistance:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistance);
|
||||
break;
|
||||
case Attenuation.LinearDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
|
||||
break;
|
||||
case Attenuation.ExponentDistance:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistance);
|
||||
break;
|
||||
case Attenuation.ExponentDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
|
||||
}
|
||||
|
||||
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
|
||||
|
||||
_openALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
// TODO: This really shouldn't be indexing based on the ClydeHandle...
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value.Value].BufferHandle);
|
||||
|
||||
var audioSource = new AudioSource(this, source, stream);
|
||||
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
|
||||
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
|
||||
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
private void _checkAlcError(ALDevice device,
|
||||
[CallerMemberName] string callerMember = "",
|
||||
[CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = ALC.GetError(device);
|
||||
if (error != AlcError.NoError)
|
||||
{
|
||||
_openALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
private void _checkAlError([CallerMemberName] string callerMember = "",
|
||||
[CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = AL.GetError();
|
||||
if (error != ALError.NoError)
|
||||
{
|
||||
_openALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
var vorbis = _readOggVorbis(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
// NVorbis only supports loading into floats.
|
||||
// If this becomes a problem due to missing extension support (doubt it but ok),
|
||||
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
|
||||
if (vorbis.Channels == 1)
|
||||
{
|
||||
format = ALFormat.MonoFloat32Ext;
|
||||
}
|
||||
else if (vorbis.Channels == 2)
|
||||
{
|
||||
format = ALFormat.StereoFloat32Ext;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (float* ptr = vorbis.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
|
||||
(int) vorbis.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
|
||||
return new AudioStream(handle, length, (int) vorbis.Channels, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
var wav = _readWav(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
if (wav.BitsPerSample == 16)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono16;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo16;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else if (wav.BitsPerSample == 8)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono8;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo8;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* ptr = wav.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
|
||||
return new AudioStream(handle, length, wav.NumChannels, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var fmt = channels switch
|
||||
{
|
||||
1 => ALFormat.Mono16,
|
||||
2 => ALFormat.Stereo16,
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(channels), "Only stereo and mono is currently supported")
|
||||
};
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
_checkAlError();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (short* ptr = samples)
|
||||
{
|
||||
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
return new AudioStream(handle, length, channels, name);
|
||||
}
|
||||
|
||||
private sealed class LoadedAudioSample
|
||||
{
|
||||
public readonly int BufferHandle;
|
||||
|
||||
public LoadedAudioSample(int bufferHandle)
|
||||
{
|
||||
BufferHandle = bufferHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Robust.Client/Graphics/Audio/ClydeAudioHeadless.cs
Normal file
70
Robust.Client/Graphics/Audio/ClydeAudioHeadless.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Hey look, it's ClydeAudio's evil twin brother!
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal sealed class ClydeAudioHeadless : IClydeAudio, IClydeAudioInternal
|
||||
{
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, channels, name);
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return DummyAudioSource.Instance;
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
|
||||
{
|
||||
return DummyBufferedAudioSource.Instance;
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Robust.Client/Graphics/Audio/DummyAudioSource.cs
Normal file
98
Robust.Client/Graphics/Audio/DummyAudioSource.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Hey look, it's ClydeAudio.AudioSource's evil twin brother!
|
||||
/// </summary>
|
||||
internal class DummyAudioSource : IClydeAudioSource
|
||||
{
|
||||
public static DummyAudioSource Instance { get; } = new();
|
||||
|
||||
public bool IsPlaying => default;
|
||||
public bool IsLooping { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void StartPlaying()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void StopPlaying()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public bool SetPosition(Vector2 position)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetGlobal()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolume(float decibels)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float scale)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetMaxDistance(float maxDistance)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetRolloffFactor(float rolloffFactor)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetReferenceDistance(float refDistance)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetOcclusion(float blocks)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetPlaybackPosition(float seconds)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Robust.Client/Graphics/Audio/DummyBufferedAudioSource.cs
Normal file
56
Robust.Client/Graphics/Audio/DummyBufferedAudioSource.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Hey look, it's ClydeAudio.BufferedAudioSource's evil twin brother!
|
||||
/// </summary>
|
||||
internal sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
|
||||
{
|
||||
public new static DummyBufferedAudioSource Instance { get; } = new();
|
||||
public int SampleRate { get; set; } = 0;
|
||||
|
||||
public void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void WriteBuffer(int handle, ReadOnlySpan<float> data)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void QueueBuffers(ReadOnlySpan<int> handles)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void EmptyBuffers()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void GetBuffersProcessed(Span<int> handles)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public int GetNumberOfBuffersProcessed()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Robust.Client/Graphics/Audio/FallbackProxyClydeAudio.cs
Normal file
34
Robust.Client/Graphics/Audio/FallbackProxyClydeAudio.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// For "start ss14 with no audio devices" Smugleaf
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal sealed class FallbackProxyClydeAudio : ProxyClydeAudio
|
||||
{
|
||||
public override bool InitializePostWindowing()
|
||||
{
|
||||
// Deliberate lack of base call here (see base implementation for comments as to why there even is a base)
|
||||
|
||||
ActualImplementation = new ClydeAudio();
|
||||
IoCManager.InjectDependencies(ActualImplementation);
|
||||
if (ActualImplementation.InitializePostWindowing())
|
||||
return true;
|
||||
|
||||
// If we get here, that failed, so use the fallback
|
||||
ActualImplementation = new ClydeAudioHeadless();
|
||||
IoCManager.InjectDependencies(ActualImplementation);
|
||||
return ActualImplementation.InitializePostWindowing();
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Robust.Client/Graphics/Audio/ProxyClydeAudio.cs
Normal file
72
Robust.Client/Graphics/Audio/ProxyClydeAudio.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// For "start ss14 with no audio devices" Smugleaf
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal abstract class ProxyClydeAudio : IClydeAudio, IClydeAudioInternal
|
||||
{
|
||||
protected IClydeAudioInternal ActualImplementation = default!;
|
||||
|
||||
public virtual bool InitializePostWindowing()
|
||||
{
|
||||
// This particular implementation exists to be overridden because removing this method causes C# to complain
|
||||
return ActualImplementation.InitializePostWindowing();
|
||||
}
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
ActualImplementation.FrameProcess(eventArgs);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
ActualImplementation.Shutdown();
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
return ActualImplementation.LoadAudioOggVorbis(stream, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
return ActualImplementation.LoadAudioWav(stream, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
return ActualImplementation.LoadAudioRaw(samples, channels, sampleRate, name);
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return ActualImplementation.CreateAudioSource(stream);
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
|
||||
{
|
||||
return ActualImplementation.CreateBufferedAudioSource(buffers, floatAudio);
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
ActualImplementation.SetMasterVolume(newVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
RuntimeInformation.ProcessArchitecture == Architecture.X64 &&
|
||||
Environment.GetEnvironmentVariable("ROBUST_INTEGRATED_GPU") != "1")
|
||||
{
|
||||
try
|
||||
{
|
||||
// We force load nvapi64.dll so nvidia gives us the dedicated GPU on optimus laptops.
|
||||
// This is 100x easier than nvidia's documented approach of NvOptimusEnablement,
|
||||
// and works while developing.
|
||||
NativeLibrary.Load("nvapi64.dll");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If this fails whatever.
|
||||
}
|
||||
// We force load nvapi64.dll so nvidia gives us the dedicated GPU on optimus laptops.
|
||||
// This is 100x easier than nvidia's documented approach of NvOptimusEnablement,
|
||||
// and works while developing.
|
||||
NativeLibrary.TryLoad("nvapi64.dll", out _);
|
||||
// If this fails whatever.
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
|
||||
@@ -74,7 +74,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case DEventWindowFocus(var args):
|
||||
OnWindowFocused?.Invoke(args);
|
||||
break;
|
||||
case DEventWindowResized(var args):
|
||||
case DEventWindowResized(var reg, var args):
|
||||
reg.Resized?.Invoke(args);
|
||||
OnWindowResized?.Invoke(args);
|
||||
break;
|
||||
}
|
||||
@@ -112,7 +113,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
reg.FramebufferSize,
|
||||
reg.Handle);
|
||||
|
||||
_eventDispatchQueue.Enqueue(new DEventWindowResized(eventArgs));
|
||||
_eventDispatchQueue.Enqueue(new DEventWindowResized(reg, eventArgs));
|
||||
}
|
||||
|
||||
private void SendWindowContentScaleChanged(WindowContentScaleEventArgs ev)
|
||||
@@ -151,7 +152,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private sealed record DEventWindowClosed(WindowReg Reg, WindowRequestClosedEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventWindowResized(WindowResizedEventArgs Args) : DEventBase;
|
||||
private sealed record DEventWindowResized(WindowReg Reg, WindowResizedEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventWindowContentScaleChanged(WindowContentScaleEventArgs Args) : DEventBase;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Advanced GL contexts currently disabled due to lack of testing etc.
|
||||
if (OperatingSystem.IsWindows() && _cfg.GetCVar(CVars.DisplayAngle))
|
||||
{
|
||||
/*
|
||||
if (_cfg.GetCVar(CVars.DisplayAngleCustomSwapChain))
|
||||
{
|
||||
_sawmillOgl.Debug("Trying custom swap chain ANGLE.");
|
||||
@@ -31,7 +30,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
if (_cfg.GetCVar(CVars.DisplayEgl))
|
||||
|
||||
@@ -927,10 +927,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
//
|
||||
|
||||
// Calculate delta positions from camera.
|
||||
var (dTlX, dTlY) = eyeTransform.Transform(tl);
|
||||
var (dTrX, dTrY) = eyeTransform.Transform(tr);
|
||||
var (dBlX, dBlY) = eyeTransform.Transform(bl);
|
||||
var (dBrX, dBrY) = eyeTransform.Transform(br);
|
||||
var dTl = eyeTransform.Transform(tl);
|
||||
var dTr = eyeTransform.Transform(tr);
|
||||
var dBl = eyeTransform.Transform(bl);
|
||||
var dBr = eyeTransform.Transform(br);
|
||||
|
||||
// Get which neighbors are occluding.
|
||||
var no = (occluder.Occluding & OccluderDir.North) != 0;
|
||||
@@ -939,10 +939,26 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var wo = (occluder.Occluding & OccluderDir.West) != 0;
|
||||
|
||||
// Do visibility tests for occluders (described above).
|
||||
var tlV = dTlX > 0 && !wo || dTlY < 0 && !no;
|
||||
var trV = dTrX < 0 && !eo || dTrY < 0 && !no;
|
||||
var blV = dBlX > 0 && !wo || dBlY > 0 && !so;
|
||||
var brV = dBrX < 0 && !eo || dBrY > 0 && !so;
|
||||
bool CheckFaceEyeVis(Vector2 a, Vector2 b)
|
||||
{
|
||||
// get normal
|
||||
var alongNormal = b - a;
|
||||
var normal = alongNormal.Rotated90DegreesAnticlockwiseWorld.Normalized;
|
||||
// determine which side of the plane the face is on
|
||||
// the plane is at the origin of this coordinate system, which is also the eye
|
||||
// the normal of the plane is that of the face
|
||||
// therefore, if the dot <= 0, the face is facing the camera
|
||||
// I don't like this, but rotated occluders started happening
|
||||
return Vector2.Dot(normal, a) <= 0;
|
||||
}
|
||||
var nV = ((!no) && CheckFaceEyeVis(dTl, dTr));
|
||||
var sV = ((!so) && CheckFaceEyeVis(dBr, dBl));
|
||||
var eV = ((!eo) && CheckFaceEyeVis(dTr, dBr));
|
||||
var wV = ((!wo) && CheckFaceEyeVis(dBl, dTl));
|
||||
var tlV = nV || wV;
|
||||
var trV = nV || eV;
|
||||
var blV = sV || wV;
|
||||
var brV = sV || eV;
|
||||
|
||||
// Handle faces, rules described above.
|
||||
// Note that "from above" it should be clockwise.
|
||||
|
||||
@@ -155,10 +155,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
isActuallySrgb = loadParams.Srgb;
|
||||
}
|
||||
else if (pixelType == typeof(Bgra32))
|
||||
{
|
||||
isActuallySrgb = loadParams.Srgb;
|
||||
}
|
||||
else if (pixelType == typeof(A8))
|
||||
{
|
||||
DebugTools.Assert(_hasGLTextureSwizzle);
|
||||
@@ -264,7 +260,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// 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),
|
||||
Bgra32 => (srgb && _hasGLSrgb ? PIF.Srgb8Alpha8 : PIF.Rgba8, PF.Bgra, PT.UnsignedByte),
|
||||
A8 or L8 => (PIF.R8, PF.Red, PT.UnsignedByte),
|
||||
_ => throw new NotSupportedException("Unsupported pixel type."),
|
||||
};
|
||||
@@ -445,7 +440,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
return default(T) switch
|
||||
{
|
||||
Rgba32 or Bgra32 => TexturePixelType.Rgba32,
|
||||
Rgba32 => TexturePixelType.Rgba32,
|
||||
L8 => TexturePixelType.L8,
|
||||
A8 => TexturePixelType.A8,
|
||||
_ => throw new NotSupportedException("Unsupported pixel type."),
|
||||
|
||||
@@ -218,7 +218,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return false;
|
||||
}
|
||||
|
||||
InitOpenGL();
|
||||
if (!_earlyGLInit)
|
||||
InitOpenGL();
|
||||
|
||||
_sawmillOgl.Debug("Setting viewport and rendering splash...");
|
||||
|
||||
@@ -458,6 +459,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public RenderWindow RenderTarget = default!;
|
||||
public Action<WindowRequestClosedEventArgs>? RequestClosed;
|
||||
public Action<WindowDestroyedEventArgs>? Closed;
|
||||
public Action<WindowResizedEventArgs>? Resized;
|
||||
}
|
||||
|
||||
private sealed class WindowHandle : IClydeWindowInternal
|
||||
@@ -523,6 +525,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
remove => Reg.Closed -= value;
|
||||
}
|
||||
|
||||
public event Action<WindowResizedEventArgs>? Resized
|
||||
{
|
||||
add => Reg.Resized += value;
|
||||
remove => Reg.Resized -= value;
|
||||
}
|
||||
|
||||
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// <summary>
|
||||
/// Responsible for most things rendering on OpenGL mode.
|
||||
/// </summary>
|
||||
internal sealed partial class Clyde : IClydeInternal, IClydeAudio, IPostInjectInit
|
||||
internal sealed partial class Clyde : IClydeInternal, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
@@ -66,6 +66,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private ISawmill _sawmillOgl = default!;
|
||||
|
||||
private IBindingsContext _glBindingsContext = default!;
|
||||
private bool _earlyGLInit;
|
||||
|
||||
public Clyde()
|
||||
{
|
||||
@@ -97,7 +98,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (!InitMainWindowAndRenderer())
|
||||
return false;
|
||||
|
||||
_initializeAudio();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -115,8 +115,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
_updateAudio();
|
||||
|
||||
_windowing?.FlushDispose();
|
||||
FlushShaderInstanceDispose();
|
||||
FlushRenderTargetDispose();
|
||||
@@ -509,7 +507,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
_glContext?.Shutdown();
|
||||
ShutdownWindowing();
|
||||
_shutdownAudio();
|
||||
}
|
||||
|
||||
private bool IsMainThread()
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// Hey look, it's Clyde's evil twin brother!
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal sealed class ClydeHeadless : IClydeInternal, IClydeAudio
|
||||
internal sealed class ClydeHeadless : IClydeInternal
|
||||
{
|
||||
// Would it make sense to report a fake resolution like 720p here so code doesn't break? idk.
|
||||
public IClydeWindow MainWindow { get; }
|
||||
@@ -236,34 +236,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, channels, name);
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return DummyAudioSource.Instance;
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
|
||||
{
|
||||
return DummyBufferedAudioSource.Instance;
|
||||
}
|
||||
|
||||
public Task<string> GetText()
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
@@ -274,11 +246,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
private class DummyCursor : ICursor
|
||||
{
|
||||
public void Dispose()
|
||||
@@ -627,6 +594,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public bool DisposeOnClose { get; set; }
|
||||
public event Action<WindowRequestClosedEventArgs>? RequestClosed { add { } remove { } }
|
||||
public event Action<WindowDestroyedEventArgs>? Destroyed;
|
||||
public event Action<WindowResizedEventArgs>? Resized { add { } remove { } }
|
||||
|
||||
public void MaximizeOnMonitor(IClydeMonitor monitor)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// Commented out because I can't be bothered to figure out trimming for TerraFX.
|
||||
/*
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -10,13 +8,18 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using TerraFX.Interop;
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static Robust.Client.Graphics.Clyde.Egl;
|
||||
using static TerraFX.Interop.D3D_DRIVER_TYPE;
|
||||
using static TerraFX.Interop.D3D_FEATURE_LEVEL;
|
||||
using static TerraFX.Interop.DXGI_FORMAT;
|
||||
using static TerraFX.Interop.DXGI_SWAP_EFFECT;
|
||||
using static TerraFX.Interop.Windows;
|
||||
using static TerraFX.Interop.DirectX.D3D_DRIVER_TYPE;
|
||||
using static TerraFX.Interop.DirectX.D3D_FEATURE_LEVEL;
|
||||
using static TerraFX.Interop.DirectX.DXGI_FORMAT;
|
||||
using static TerraFX.Interop.DirectX.DXGI_SWAP_EFFECT;
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
using static TerraFX.Interop.DirectX.DirectX;
|
||||
using static TerraFX.Interop.DirectX.D3D11;
|
||||
using static TerraFX.Interop.DirectX.DXGI;
|
||||
using GL = OpenToolkit.Graphics.OpenGL4.GL;
|
||||
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -77,7 +80,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
};
|
||||
_windowData[reg.Id] = data;
|
||||
|
||||
var hWnd = Clyde._windowing!.WindowGetWin32Window(reg)!.Value;
|
||||
var hWnd = (HWND) Clyde._windowing!.WindowGetWin32Window(reg)!.Value;
|
||||
|
||||
// todo: exception management.
|
||||
CreateSwapChain1(hWnd, data);
|
||||
@@ -98,7 +101,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
if (data.EglBackbuffer != null)
|
||||
{
|
||||
eglMakeCurrent(_eglDisplay, null, null, null);
|
||||
if (data.Reg.IsMainWindow)
|
||||
eglMakeCurrent(_eglDisplay, null, null, null);
|
||||
eglDestroySurface(_eglDisplay, data.EglBackbuffer);
|
||||
|
||||
data.EglBackbuffer = null;
|
||||
@@ -115,8 +119,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
fixed (ID3D11Texture2D** texPtr = &data.Backbuffer)
|
||||
{
|
||||
var iid = IID_ID3D11Texture2D;
|
||||
ThrowIfFailed("GetBuffer", data.SwapChain->GetBuffer(0, &iid, (void**) texPtr));
|
||||
ThrowIfFailed("GetBuffer", data.SwapChain->GetBuffer(0, __uuidof<ID3D11Texture2D>(), (void**) texPtr));
|
||||
}
|
||||
|
||||
var attributes = stackalloc int[]
|
||||
@@ -134,7 +137,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
attributes);
|
||||
}
|
||||
|
||||
private void CreateSwapChain1(nint hWnd, WindowData data)
|
||||
private void CreateSwapChain1(HWND hWnd, WindowData data)
|
||||
{
|
||||
var desc = new DXGI_SWAP_CHAIN_DESC
|
||||
{
|
||||
@@ -199,6 +202,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// and so that we can know _hasGLSrgb in window creation.
|
||||
eglMakeCurrent(_eglDisplay, null, null, _eglContext);
|
||||
Clyde.InitOpenGL();
|
||||
Clyde._earlyGLInit = true;
|
||||
}
|
||||
|
||||
private void TryInitializeCore()
|
||||
@@ -294,11 +298,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
try
|
||||
{
|
||||
var iid = IID_IDXGIFactory1;
|
||||
|
||||
fixed (IDXGIFactory1** ptr = &_factory)
|
||||
{
|
||||
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(&iid, (void**) ptr));
|
||||
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(__uuidof<IDXGIFactory1>(), (void**) ptr));
|
||||
}
|
||||
|
||||
// Try to find the correct adapter if specified.
|
||||
@@ -317,6 +319,38 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Logger.DebugS("clyde.ogl.angle", $"Found display adapter with name: {adapterName}");
|
||||
}
|
||||
|
||||
#pragma warning disable CA1416
|
||||
IDXGIFactory6* factory6;
|
||||
if (_adapter == null && _factory->QueryInterface(__uuidof<IDXGIFactory6>(), (void**) &factory6) == 0)
|
||||
{
|
||||
var gpuPref = (DXGI_GPU_PREFERENCE) Clyde._cfg.GetCVar(CVars.DisplayGpuPreference);
|
||||
IDXGIAdapter1* adapter;
|
||||
for (var adapterIndex = 0u;
|
||||
factory6->EnumAdapterByGpuPreference(
|
||||
adapterIndex,
|
||||
gpuPref,
|
||||
__uuidof<IDXGIAdapter1>(),
|
||||
(void**)&adapter) != DXGI_ERROR_NOT_FOUND;
|
||||
adapterIndex++)
|
||||
{
|
||||
/*
|
||||
DXGI_ADAPTER_DESC1 aDesc;
|
||||
ThrowIfFailed("GetDesc1", adapter->GetDesc1(&aDesc));
|
||||
|
||||
var aDescName = new ReadOnlySpan<char>(aDesc.Description, 128);
|
||||
|
||||
Logger.DebugS("clyde.ogl.angle", aDescName.ToString());
|
||||
|
||||
adapter->Release();
|
||||
*/
|
||||
_adapter = adapter;
|
||||
break;
|
||||
}
|
||||
|
||||
factory6->Release();
|
||||
}
|
||||
#pragma warning restore CA1416
|
||||
|
||||
Span<D3D_FEATURE_LEVEL> featureLevels = stackalloc D3D_FEATURE_LEVEL[]
|
||||
{
|
||||
// 11_0 can do GLES3
|
||||
@@ -335,7 +369,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
ThrowIfFailed("D3D11CreateDevice", D3D11CreateDevice(
|
||||
(IDXGIAdapter*) _adapter,
|
||||
_adapter == null ? D3D_DRIVER_TYPE_HARDWARE : D3D_DRIVER_TYPE_UNKNOWN,
|
||||
IntPtr.Zero,
|
||||
HMODULE.NULL,
|
||||
0,
|
||||
fl,
|
||||
(uint) featureLevels.Length,
|
||||
@@ -348,13 +382,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// Get adapter from the device.
|
||||
|
||||
iid = IID_IDXGIDevice1;
|
||||
ThrowIfFailed("QueryInterface", _device->QueryInterface(&iid, (void**) &dxgiDevice));
|
||||
ThrowIfFailed("QueryInterface", _device->QueryInterface(__uuidof<IDXGIDevice1>(), (void**) &dxgiDevice));
|
||||
|
||||
fixed (IDXGIAdapter1** ptrAdapter = &_adapter)
|
||||
{
|
||||
iid = IID_IDXGIAdapter1;
|
||||
ThrowIfFailed("GetParent", dxgiDevice->GetParent(&iid, (void**) ptrAdapter));
|
||||
ThrowIfFailed("GetParent", dxgiDevice->GetParent(__uuidof<IDXGIAdapter1>(), (void**) ptrAdapter));
|
||||
}
|
||||
|
||||
_deviceFl = _device->GetFeatureLevel();
|
||||
@@ -527,4 +559,3 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
/*
|
||||
using System;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Utility;
|
||||
using TerraFX.Interop;
|
||||
using static TerraFX.Interop.D3D_DRIVER_TYPE;
|
||||
using static TerraFX.Interop.D3D_FEATURE_LEVEL;
|
||||
using static TerraFX.Interop.DXGI_MEMORY_SEGMENT_GROUP;
|
||||
using static TerraFX.Interop.DXGI_SWAP_EFFECT;
|
||||
using static TerraFX.Interop.Windows;
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static TerraFX.Interop.DirectX.DXGI_MEMORY_SEGMENT_GROUP;
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
using static TerraFX.Interop.DirectX.DirectX;
|
||||
using static TerraFX.Interop.DirectX.DXGI;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
public sealed class VramCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "vram";
|
||||
public string Description => "Checks vram";
|
||||
public string Description => "Displays video memory usage statics by the game.";
|
||||
public string Help => "Usage: vram";
|
||||
|
||||
public unsafe void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
shell.WriteError("This command is only supported on Windows.");
|
||||
return;
|
||||
}
|
||||
|
||||
IDXGIFactory1* dxgiFactory;
|
||||
var iid = IID_IDXGIFactory1;
|
||||
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(&iid, (void**) &dxgiFactory));
|
||||
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(__uuidof<IDXGIFactory1>(), (void**) &dxgiFactory));
|
||||
|
||||
uint idx = 0;
|
||||
IDXGIAdapter* adapter;
|
||||
@@ -30,8 +34,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
DXGI_ADAPTER_DESC2 desc;
|
||||
IDXGIAdapter3* adapter3;
|
||||
iid = IID_IDXGIAdapter3;
|
||||
adapter->QueryInterface(&iid, (void**) &adapter3);
|
||||
adapter->QueryInterface(__uuidof<IDXGIAdapter3>(), (void**) &adapter3);
|
||||
adapter->Release();
|
||||
ThrowIfFailed("GetDesc", adapter3->GetDesc2(&desc));
|
||||
|
||||
var descString = new ReadOnlySpan<char>(desc.Description, 128).TrimEnd('\0');
|
||||
@@ -46,6 +50,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
shell.WriteLine($"Usage (non local): {ByteHelpers.FormatBytes((long) memInfo.CurrentUsage)}");
|
||||
|
||||
idx += 1;
|
||||
|
||||
adapter3->Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +64,3 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared;
|
||||
using System.Threading;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Input;
|
||||
@@ -8,6 +9,8 @@ using GlfwKey = OpenToolkit.GraphicsLibraryFramework.Keys;
|
||||
using GlfwButton = OpenToolkit.GraphicsLibraryFramework.MouseButton;
|
||||
using static Robust.Client.Input.Mouse;
|
||||
using static Robust.Client.Input.Keyboard;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
@@ -22,6 +25,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void InitKeyMap()
|
||||
{
|
||||
_printableKeyNameMap.Clear();
|
||||
// From GLFW's source code: this is the actual list of "printable" keys
|
||||
// that GetKeyName returns something for.
|
||||
CacheKey(Keys.KeyPadEqual);
|
||||
@@ -41,8 +45,18 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (rKey == Key.Unknown)
|
||||
return;
|
||||
|
||||
var name = GLFW.GetKeyName(key, 0);
|
||||
if (name != null)
|
||||
string name;
|
||||
|
||||
if (!_clyde._cfg.GetCVar(CVars.DisplayUSQWERTYHotkeys))
|
||||
{
|
||||
name = GLFW.GetKeyName(key, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
name = key.ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
_printableKeyNameMap.Add(rKey, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
@@ -268,7 +268,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
var (desc, errCode) = errorResult!.Value;
|
||||
return (null, $"[{errCode}]: {desc}");
|
||||
return (null, (string)$"[{errCode}]: {desc}");
|
||||
}
|
||||
|
||||
public void WindowDestroy(WindowReg window)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -16,6 +17,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
|
||||
private readonly Clyde _clyde;
|
||||
|
||||
@@ -39,6 +41,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
#if DEBUG
|
||||
_cfg.OnValueChanged(CVars.DisplayWin32Experience, b => _win32Experience = b, true);
|
||||
#endif
|
||||
_cfg.OnValueChanged(CVars.DisplayUSQWERTYHotkeys, ReInitKeyMap);
|
||||
|
||||
InitChannels();
|
||||
|
||||
@@ -60,6 +63,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (_glfwInitialized)
|
||||
{
|
||||
_sawmill.Debug("Terminating GLFW.");
|
||||
_cfg.UnsubValueChanged(CVars.DisplayUSQWERTYHotkeys, ReInitKeyMap);
|
||||
GLFW.Terminate();
|
||||
}
|
||||
}
|
||||
@@ -69,6 +73,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Not currently used
|
||||
}
|
||||
|
||||
private void ReInitKeyMap(bool onValueChanged)
|
||||
{
|
||||
InitKeyMap();
|
||||
_inputManager.InputModeChanged();
|
||||
}
|
||||
|
||||
private bool InitGlfw()
|
||||
{
|
||||
StoreCallbacks();
|
||||
|
||||
@@ -52,6 +52,11 @@ namespace Robust.Client.Graphics
|
||||
public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated quad,
|
||||
Color? modulate = null, UIBox2? subRegion = null);
|
||||
|
||||
private Box2 GetQuad(Texture texture, Vector2 position)
|
||||
{
|
||||
return Box2.FromDimensions(position, texture.Size / (float)Ppm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a full texture sprite to the world. The coordinate system is right handed.
|
||||
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
|
||||
@@ -67,7 +72,28 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
DrawTextureRect(texture, Box2.FromDimensions(position, texture.Size / (float) Ppm), modulate);
|
||||
DrawTextureRect(texture, GetQuad(texture, position), modulate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a full texture sprite to the world. The coordinate system is right handed.
|
||||
/// Make sure to set <see cref="DrawingHandleBase.SetTransform"/>
|
||||
/// to set the model matrix if needed.
|
||||
/// </summary>
|
||||
/// <param name="texture">Texture to draw.</param>
|
||||
/// <param name="position">The coordinates of the quad in object space (or world if the transform is identity.).</param>
|
||||
/// <param name="angle">The angle of the quad in object space.</param>
|
||||
/// <param name="modulate">A color to multiply the texture by when shading.</param>
|
||||
/// <remarks>
|
||||
/// The sprite will have it's local dimensions calculated so that it has <see cref="EyeManager.PixelsPerMeter"/> texels per meter in the world.
|
||||
/// </remarks>
|
||||
public void DrawTexture(Texture texture, Vector2 position, Angle angle, Color? modulate = null)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
var quad = GetQuad(texture, position);
|
||||
|
||||
DrawTextureRect(texture, new Box2Rotated(quad, angle, quad.Center), modulate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
16
Robust.Client/Graphics/IClydeAudioInternal.cs
Normal file
16
Robust.Client/Graphics/IClydeAudioInternal.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
internal interface IClydeAudioInternal : IClydeAudio
|
||||
{
|
||||
bool InitializePostWindowing();
|
||||
void FrameProcess(FrameEventArgs eventArgs);
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,12 @@ namespace Robust.Client.Graphics
|
||||
/// This means the window must not be used anymore (it is disposed).
|
||||
/// </summary>
|
||||
event Action<WindowDestroyedEventArgs> Destroyed;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the window has been definitively closed.
|
||||
/// This means the window must not be used anymore (it is disposed).
|
||||
/// </summary>
|
||||
event Action<WindowResizedEventArgs> Resized;
|
||||
}
|
||||
|
||||
public interface IClydeWindowInternal : IClydeWindow
|
||||
|
||||
@@ -111,6 +111,7 @@ namespace Robust.Client.Input
|
||||
|
||||
event Action<IKeyBinding> OnKeyBindingAdded;
|
||||
event Action<IKeyBinding> OnKeyBindingRemoved;
|
||||
event Action OnInputModeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the keybinds bound to a specific function.
|
||||
@@ -131,5 +132,7 @@ namespace Robust.Client.Input
|
||||
bool IsKeyFunctionModified(BoundKeyFunction function);
|
||||
|
||||
bool IsKeyDown(Keyboard.Key key);
|
||||
|
||||
void InputModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Robust.Client.Input
|
||||
{
|
||||
BoundKeyState State { get; }
|
||||
BoundKeyFunction Function { get; }
|
||||
string FunctionCommand { get; }
|
||||
KeyBindingType BindingType { get; }
|
||||
|
||||
Keyboard.Key BaseKey { get; }
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Input;
|
||||
@@ -43,6 +44,7 @@ namespace Robust.Client.Input
|
||||
[Dependency] private readonly IResourceManager _resourceMan = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _uiMgr = default!;
|
||||
[Dependency] private readonly IConsoleHost _console = default!;
|
||||
|
||||
private bool _currentlyFindingViewport;
|
||||
|
||||
@@ -76,7 +78,7 @@ namespace Robust.Client.Input
|
||||
|
||||
public IEnumerable<BoundKeyFunction> DownKeyFunctions => _bindings
|
||||
.Where(x => x.State == BoundKeyState.Down)
|
||||
.Select(x => x.Function)
|
||||
.Select(x => (BoundKeyFunction)x.Function)
|
||||
.ToList();
|
||||
|
||||
public virtual string GetKeyName(Key key)
|
||||
@@ -98,6 +100,7 @@ namespace Robust.Client.Input
|
||||
public event KeyEventAction? FirstChanceOnKeyEvent;
|
||||
public event Action<IKeyBinding>? OnKeyBindingAdded;
|
||||
public event Action<IKeyBinding>? OnKeyBindingRemoved;
|
||||
public event Action? OnInputModeChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
@@ -131,7 +134,7 @@ namespace Robust.Client.Input
|
||||
.SelectMany(p => p)
|
||||
.Select(p => new KeyBindingRegistration
|
||||
{
|
||||
Function = p.Function,
|
||||
Function = p.Function.FunctionName,
|
||||
BaseKey = p.BaseKey,
|
||||
Mod1 = p.Mod1,
|
||||
Mod2 = p.Mod2,
|
||||
@@ -213,7 +216,7 @@ namespace Robust.Client.Input
|
||||
foreach (var binding in _bindings)
|
||||
{
|
||||
// check if our binding is even in the active context
|
||||
if (!Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
|
||||
if (binding.BindingType != KeyBindingType.Command && !Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
|
||||
continue;
|
||||
|
||||
if (PackedMatchesPressedState(binding.PackedKeyCombo))
|
||||
@@ -359,6 +362,12 @@ namespace Robust.Client.Input
|
||||
|
||||
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false)
|
||||
{
|
||||
if (binding.BindingType == KeyBindingType.Command && state == BoundKeyState.Down)
|
||||
{
|
||||
_console.ExecuteCommand(binding.FunctionCommand);
|
||||
return true;
|
||||
}
|
||||
|
||||
// christ this crap *is* re-entrant thanks to PlacementManager and
|
||||
// I honestly have no idea what the best solution here is.
|
||||
// note from the future: context switches won't cause re-entrancy anymore because InputContextContainer defers context switches
|
||||
@@ -492,10 +501,10 @@ namespace Robust.Client.Input
|
||||
|
||||
foreach (var reg in baseKeyRegs)
|
||||
{
|
||||
if (!NetworkBindMap.FunctionExists(reg.Function.FunctionName))
|
||||
if (reg.Type != KeyBindingType.Command && !NetworkBindMap.FunctionExists(reg.Function.FunctionName))
|
||||
{
|
||||
Logger.ErrorS("input", "Key function in {0} does not exist: '{1}'", file,
|
||||
reg.Function.FunctionName);
|
||||
reg.Function);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -531,6 +540,17 @@ namespace Robust.Client.Input
|
||||
/// <inheritdoc />
|
||||
public IKeyBinding RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
|
||||
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
|
||||
{
|
||||
var binding = new KeyBinding(this, function.FunctionName, bindingType, baseKey, false, false, false,
|
||||
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
|
||||
|
||||
RegisterBinding(binding);
|
||||
|
||||
return binding;
|
||||
}
|
||||
|
||||
public IKeyBinding RegisterBinding(string function, KeyBindingType bindingType,
|
||||
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
|
||||
{
|
||||
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false, false,
|
||||
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
|
||||
@@ -542,7 +562,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,
|
||||
var binding = new KeyBinding(this, reg.Function.FunctionName, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
|
||||
reg.AllowSubCombs, reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
|
||||
|
||||
RegisterBinding(binding, markModified);
|
||||
@@ -569,6 +589,8 @@ namespace Robust.Client.Input
|
||||
OnKeyBindingRemoved?.Invoke(binding);
|
||||
}
|
||||
|
||||
public void InputModeChanged() => OnInputModeChanged?.Invoke();
|
||||
|
||||
private void RegisterBinding(KeyBinding binding, bool markModified = true)
|
||||
{
|
||||
// we sort larger combos first so they take priority over smaller (single key) combos,
|
||||
@@ -684,6 +706,7 @@ namespace Robust.Client.Input
|
||||
[ViewVariables] public BoundKeyState State { get; set; }
|
||||
public PackedKeyCombo PackedKeyCombo { get; }
|
||||
[ViewVariables] public BoundKeyFunction Function { get; }
|
||||
[ViewVariables] public string FunctionCommand => Function.FunctionName;
|
||||
[ViewVariables] public KeyBindingType BindingType { get; }
|
||||
|
||||
[ViewVariables] public Key BaseKey => PackedKeyCombo.BaseKey;
|
||||
@@ -711,7 +734,9 @@ namespace Robust.Client.Input
|
||||
|
||||
[ViewVariables] public int Priority { get; internal set; }
|
||||
|
||||
public KeyBinding(InputManager inputManager, BoundKeyFunction function,
|
||||
public KeyBinding(
|
||||
InputManager inputManager,
|
||||
string function,
|
||||
KeyBindingType bindingType,
|
||||
Key baseKey,
|
||||
bool canFocus, bool canRepeat, bool allowSubCombs, int priority, Key mod1 = Key.Unknown,
|
||||
@@ -771,7 +796,7 @@ namespace Robust.Client.Input
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendFormat("{0}: {1}", Function.FunctionName, BaseKey);
|
||||
sb.AppendFormat("{0}: {1}", Function, BaseKey);
|
||||
if (Mod1 != Key.Unknown)
|
||||
{
|
||||
sb.AppendFormat("+{0}", Mod1);
|
||||
@@ -868,6 +893,10 @@ namespace Robust.Client.Input
|
||||
Unknown = 0,
|
||||
State,
|
||||
Toggle,
|
||||
/// <summary>
|
||||
/// This keybind does not execute a real key function but instead causes a console command to be executed.
|
||||
/// </summary>
|
||||
Command,
|
||||
}
|
||||
|
||||
public enum CommandState : byte
|
||||
@@ -922,7 +951,7 @@ namespace Robust.Client.Input
|
||||
|
||||
var registration = new KeyBindingRegistration
|
||||
{
|
||||
Function = new BoundKeyFunction(inputCommand),
|
||||
Function = inputCommand,
|
||||
BaseKey = keyId,
|
||||
Type = keyMode
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>../bin/Client</OutputPath>
|
||||
<NoWarn>NU1701</NoWarn>
|
||||
<RobustILLink>true</RobustILLink>
|
||||
</PropertyGroup>
|
||||
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
|
||||
<ItemGroup>
|
||||
@@ -22,7 +23,7 @@
|
||||
<PackageReference Include="OpenToolkit.OpenAL" Version="4.0.0-pre9.1" />
|
||||
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" />
|
||||
<PackageReference Include="Robust.Natives" Version="0.1.0" />
|
||||
<!--<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-beta1" />-->
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-rc2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="3.8.0" />
|
||||
@@ -44,10 +45,16 @@
|
||||
|
||||
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<RobustLinkRoots Include="Robust.Client" />
|
||||
<RobustLinkAssemblies Include="TerraFX.Interop.Windows" />
|
||||
<RobustLinkAssemblies Include="OpenToolkit.Graphics" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\MSBuild\Robust.Engine.targets" />
|
||||
<PropertyGroup>
|
||||
<RobustToolsPath>../Tools</RobustToolsPath>
|
||||
</PropertyGroup>
|
||||
<Target Name="RobustAfterBuild" AfterTargets="Build" />
|
||||
<Import Project="..\MSBuild\XamlIL.targets" />
|
||||
<Import Project="..\MSBuild\Robust.Trimming.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace Robust.Client.UserInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent == UserInterfaceManager.RootControl)
|
||||
if (parent is UIRoot)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// <seealso cref="CheckBox"/>
|
||||
public abstract class BaseButton : Control
|
||||
{
|
||||
private bool _attemptingPress;
|
||||
private int _attemptingPress;
|
||||
private bool _beingHovered;
|
||||
private bool _disabled;
|
||||
private bool _pressed;
|
||||
@@ -151,11 +151,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
return DrawModeEnum.Disabled;
|
||||
}
|
||||
else if (Pressed || _attemptingPress)
|
||||
else if (Pressed || (_attemptingPress > 0 && IsHovered))
|
||||
{
|
||||
return DrawModeEnum.Pressed;
|
||||
}
|
||||
else if (IsHovered)
|
||||
else if (IsHovered || _attemptingPress > 0)
|
||||
{
|
||||
return DrawModeEnum.Hover;
|
||||
}
|
||||
@@ -210,7 +210,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var drawMode = DrawMode;
|
||||
if (Mode == ActionMode.Release)
|
||||
{
|
||||
_attemptingPress = true;
|
||||
UserInterfaceManager.ControlFocused = this;
|
||||
_attemptingPress += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -227,7 +228,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
else
|
||||
{
|
||||
_attemptingPress = true;
|
||||
UserInterfaceManager.ControlFocused = this;
|
||||
_attemptingPress += 1;
|
||||
OnPressed?.Invoke(buttonEventArgs);
|
||||
}
|
||||
}
|
||||
@@ -251,7 +253,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
OnButtonUp?.Invoke(buttonEventArgs);
|
||||
|
||||
var drawMode = DrawMode;
|
||||
if (Mode == ActionMode.Release && _attemptingPress && HasPoint((args.PointerLocation.Position - GlobalPixelPosition) / UIScale))
|
||||
if (Mode == ActionMode.Release && _attemptingPress > 0 && HasPoint((args.PointerLocation.Position - GlobalPixelPosition) / UIScale))
|
||||
{
|
||||
// Can't un press a radio button directly.
|
||||
if (Group == null || !Pressed)
|
||||
@@ -270,7 +272,22 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
_attemptingPress = false;
|
||||
if (_attemptingPress > 0)
|
||||
_attemptingPress -= 1;
|
||||
if (_attemptingPress <= 0 && UserInterfaceManager.ControlFocused == this)
|
||||
UserInterfaceManager.ControlFocused = null;
|
||||
if (drawMode != DrawMode)
|
||||
{
|
||||
DrawModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected internal override void ControlFocusExited()
|
||||
{
|
||||
base.ControlFocusExited();
|
||||
|
||||
var drawMode = DrawMode;
|
||||
_attemptingPress = 0;
|
||||
if (drawMode != DrawMode)
|
||||
{
|
||||
DrawModeChanged();
|
||||
|
||||
136
Robust.Client/UserInterface/Controls/Collapsible.cs
Normal file
136
Robust.Client/UserInterface/Controls/Collapsible.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
public class Collapsible : BoxContainer
|
||||
{
|
||||
public BaseButton? Heading { get; private set; }
|
||||
public Control? Body { get; private set; }
|
||||
|
||||
private bool _initialized = false;
|
||||
|
||||
private bool _bodyVisible;
|
||||
public bool BodyVisible
|
||||
{
|
||||
get => _bodyVisible;
|
||||
set
|
||||
{
|
||||
_bodyVisible = value;
|
||||
|
||||
if (Heading != null && Body != null)
|
||||
{
|
||||
Heading.Pressed = value;
|
||||
Body.Visible = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Collapsible()
|
||||
{}
|
||||
|
||||
public Collapsible(CollapsibleHeading header, CollapsibleBody body)
|
||||
{
|
||||
AddChild(header);
|
||||
AddChild(body);
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public Collapsible(string title, CollapsibleBody body)
|
||||
{
|
||||
AddChild(new CollapsibleHeading(title));
|
||||
AddChild(body);
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
if (!_initialized) Initialize();
|
||||
|
||||
base.Draw(handle);
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
var enumerator = Children.GetEnumerator();
|
||||
enumerator.MoveNext();
|
||||
|
||||
// downcast
|
||||
if (enumerator.Current is not BaseButton heading
|
||||
|| !heading.ToggleMode)
|
||||
throw new ArgumentException("No toggle button defined in Collapsible, or title is missing");
|
||||
|
||||
Heading = heading;
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
throw new ArgumentException("Not enough children in Collapsible");
|
||||
|
||||
Body = enumerator.Current;
|
||||
BodyVisible = _bodyVisible;
|
||||
Heading.Pressed = _bodyVisible;
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
throw new ArgumentException("Too many children in Collapsible");
|
||||
|
||||
Heading.OnToggled += args => BodyVisible = args.Pressed;
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public class CollapsibleHeading : ContainerButton
|
||||
{
|
||||
private TextureRect _chevron = new TextureRect
|
||||
{
|
||||
StyleClasses = { OptionButton.StyleClassOptionTriangle },
|
||||
Margin = new Thickness(2, 0),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
public bool ChevronVisible
|
||||
{
|
||||
get => _chevron.Visible;
|
||||
set => _chevron.Visible = value;
|
||||
}
|
||||
|
||||
public Thickness ChevronMargin
|
||||
{
|
||||
get => _chevron.Margin;
|
||||
set => _chevron.Margin = value;
|
||||
}
|
||||
|
||||
private Label _title = new();
|
||||
public string? Title
|
||||
{
|
||||
get => _title.Text;
|
||||
set => _title.Text = value;
|
||||
}
|
||||
|
||||
public CollapsibleHeading()
|
||||
{
|
||||
ToggleMode = true;
|
||||
var box = new BoxContainer();
|
||||
AddChild(box);
|
||||
box.AddChild(_chevron);
|
||||
_title = new Label();
|
||||
box.AddChild(_title);
|
||||
}
|
||||
|
||||
public CollapsibleHeading(string title) : this()
|
||||
{
|
||||
Title = title;
|
||||
}
|
||||
}
|
||||
|
||||
public class CollapsibleBody : Container
|
||||
{
|
||||
public CollapsibleBody()
|
||||
{
|
||||
this.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
ClydeWindow = _clyde.CreateWindow(parameters);
|
||||
ClydeWindow.RequestClosed += OnWindowRequestClosed;
|
||||
ClydeWindow.Destroyed += OnWindowDestroyed;
|
||||
ClydeWindow.Resized += OnWindowResized;
|
||||
|
||||
_root = UserInterfaceManager.CreateWindowRoot(ClydeWindow);
|
||||
_root.AddChild(this);
|
||||
@@ -167,6 +168,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
RealClosed();
|
||||
}
|
||||
|
||||
private void OnWindowResized(WindowResizedEventArgs obj)
|
||||
{
|
||||
SetSize = obj.NewSize;
|
||||
}
|
||||
|
||||
private void RealClosed()
|
||||
{
|
||||
Orphan();
|
||||
|
||||
@@ -153,6 +153,9 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
this.placementManager.PlacementChanged += OnPlacementCanceled;
|
||||
this.placementManager.DirectionChanged += OnDirectionChanged;
|
||||
UpdateDirectionLabel();
|
||||
|
||||
OnClose += OnWindowClosed;
|
||||
|
||||
SearchBar.GrabKeyboardFocus();
|
||||
}
|
||||
|
||||
@@ -162,7 +165,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
if (!disposing) return;
|
||||
|
||||
if(EraseButton.Pressed)
|
||||
if (EraseButton.Pressed)
|
||||
placementManager.Clear();
|
||||
|
||||
placementManager.PlacementChanged -= OnPlacementCanceled;
|
||||
@@ -171,6 +174,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
placementManager.Clear();
|
||||
BuildEntityList(args.Text);
|
||||
ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
|
||||
}
|
||||
@@ -196,13 +200,17 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
placementManager.Clear();
|
||||
SearchBar.Clear();
|
||||
BuildEntityList("");
|
||||
}
|
||||
|
||||
private void OnEraseButtonToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
placementManager.Clear();
|
||||
placementManager.ToggleEraser();
|
||||
// clearing will toggle the erase button off...
|
||||
args.Button.Pressed = args.Pressed;
|
||||
OverrideMenu.Disabled = args.Pressed;
|
||||
}
|
||||
|
||||
@@ -505,6 +513,16 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosed()
|
||||
{
|
||||
if (SelectedButton != null)
|
||||
{
|
||||
SelectedButton.ActualButton.Pressed = false;
|
||||
SelectedButton = null;
|
||||
}
|
||||
placementManager.Clear();
|
||||
}
|
||||
|
||||
private void OnPlacementCanceled(object? sender, EventArgs e)
|
||||
{
|
||||
if (SelectedButton != null)
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
internal sealed class FpsCounter : Label
|
||||
public sealed class FpsCounter : Label
|
||||
{
|
||||
private readonly IGameTiming _gameTiming;
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
public const string StyleClassWindowHeader = "windowHeader";
|
||||
public const string StyleClassWindowCloseButton = "windowCloseButton";
|
||||
|
||||
private string? _headerClass;
|
||||
private string? _titleClass;
|
||||
|
||||
public SS14Window()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -31,6 +34,42 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
XamlChildren = new SS14ContentCollection(this);
|
||||
}
|
||||
|
||||
public string? HeaderClass
|
||||
{
|
||||
get => _headerClass;
|
||||
set
|
||||
{
|
||||
if (_headerClass == value)
|
||||
return;
|
||||
|
||||
if (_headerClass != null)
|
||||
WindowHeader.RemoveStyleClass(_headerClass);
|
||||
|
||||
if (value != null)
|
||||
WindowHeader.AddStyleClass(value);
|
||||
|
||||
_headerClass = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string? TitleClass
|
||||
{
|
||||
get => _titleClass;
|
||||
set
|
||||
{
|
||||
if (_titleClass == value)
|
||||
return;
|
||||
|
||||
if (_titleClass != null)
|
||||
TitleLabel.RemoveStyleClass(_titleClass);
|
||||
|
||||
if (value != null)
|
||||
TitleLabel.AddStyleClass(value);
|
||||
|
||||
_titleClass = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Control Contents { get; private set; }
|
||||
|
||||
private const int DRAG_MARGIN_SIZE = 7;
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
_placementManager.PlacementChanged += OnPlacementCanceled;
|
||||
|
||||
OnClose += OnWindowClosed;
|
||||
|
||||
Title = "Place Tiles";
|
||||
SearchBar.GrabKeyboardFocus();
|
||||
|
||||
@@ -80,6 +82,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
TileList.ClearSelected();
|
||||
_placementManager.Clear();
|
||||
SearchBar.Clear();
|
||||
BuildTileList("");
|
||||
ClearButton.Disabled = true;
|
||||
@@ -87,6 +91,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
TileList.ClearSelected();
|
||||
_placementManager.Clear();
|
||||
BuildTileList(args.Text);
|
||||
ClearButton.Disabled = string.IsNullOrEmpty(args.Text);
|
||||
}
|
||||
@@ -120,6 +126,12 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosed()
|
||||
{
|
||||
TileList.ClearSelected();
|
||||
_placementManager.Clear();
|
||||
}
|
||||
|
||||
private void OnPlacementCanceled(object? sender, EventArgs e)
|
||||
{
|
||||
_clearingSelections = true;
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Robust.Client.UserInterface
|
||||
/// (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; }
|
||||
Control? ControlFocused { get; set; }
|
||||
|
||||
ViewportContainer MainViewport { get; }
|
||||
|
||||
@@ -53,8 +53,7 @@ namespace Robust.Client.UserInterface
|
||||
float DefaultUIScale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The "root" control to which all other controls are parented,
|
||||
/// potentially indirectly.
|
||||
/// The root control for the main game window.
|
||||
/// </summary>
|
||||
WindowRoot RootControl { get; }
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
@@ -14,11 +14,20 @@ namespace Robust.Client.UserInterface
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var reflection = IoCManager.Resolve<IReflectionManager>();
|
||||
var type = reflection.LooseGetType(args[0]);
|
||||
var types = reflection.GetAllChildren(typeof(State.State));
|
||||
|
||||
var stateMan = IoCManager.Resolve<IStateManager>();
|
||||
foreach (var tryType in types)
|
||||
{
|
||||
if (tryType.FullName!.EndsWith(args[0]))
|
||||
{
|
||||
var stateMan = IoCManager.Resolve<IStateManager>();
|
||||
stateMan.RequestStateChange(tryType);
|
||||
shell.WriteLine($"Switching to scene {tryType.FullName}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
stateMan.RequestStateChange(type);
|
||||
shell.WriteError($"No scene child class type ends with {args[0]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,19 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
[ViewVariables] public Control? KeyboardFocused { get; private set; }
|
||||
|
||||
[ViewVariables] public Control? ControlFocused { get; private set; }
|
||||
private Control? _controlFocused;
|
||||
[ViewVariables]
|
||||
public Control? ControlFocused
|
||||
{
|
||||
get => _controlFocused;
|
||||
set
|
||||
{
|
||||
if (_controlFocused == value)
|
||||
return;
|
||||
_controlFocused?.ControlFocusExited();
|
||||
_controlFocused = value;
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public ViewportContainer MainViewport { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer StateRoot { get; private set; } = default!;
|
||||
@@ -348,7 +360,6 @@ namespace Robust.Client.UserInterface
|
||||
RemoveModal(top);
|
||||
else
|
||||
{
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = top;
|
||||
hitData = null;
|
||||
return false; // prevent anything besides the top modal control from receiving input
|
||||
@@ -370,7 +381,6 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
var (control, rel) = hit.Value;
|
||||
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = control;
|
||||
|
||||
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
|
||||
@@ -384,7 +394,6 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public void HandleCanFocusUp()
|
||||
{
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
@@ -646,7 +655,6 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
|
||||
if (control != ControlFocused) return;
|
||||
ControlFocused?.ControlFocusExited();
|
||||
ControlFocused = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,9 +80,11 @@ namespace Robust.Server
|
||||
[Dependency] private readonly IWatchdogApi _watchdogApi = default!;
|
||||
[Dependency] private readonly IScriptHost _scriptHost = default!;
|
||||
[Dependency] private readonly IMetricsManager _metricsManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
|
||||
[Dependency] private readonly ILocalizationManagerInternal _loc = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _netCfgMan = default!;
|
||||
[Dependency] private readonly IServerConsoleHost _consoleHost = default!;
|
||||
|
||||
private readonly Stopwatch _uptimeStopwatch = new();
|
||||
|
||||
@@ -103,11 +105,12 @@ namespace Robust.Server
|
||||
/// <inheritdoc />
|
||||
public string ServerName => _config.GetCVar(CVars.GameHostName);
|
||||
|
||||
public bool ContentStart { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Restart()
|
||||
{
|
||||
Logger.InfoS("srv", "Restarting Server...");
|
||||
|
||||
// FIXME: This explodes very violently.
|
||||
Cleanup();
|
||||
Start(Options, _logHandlerFactory);
|
||||
}
|
||||
@@ -139,10 +142,6 @@ namespace Robust.Server
|
||||
public bool Start(ServerOptions options, Func<ILogHandler>? logHandlerFactory = null)
|
||||
{
|
||||
Options = options;
|
||||
var profilePath = Path.Join(Environment.CurrentDirectory, "AAAAAAAA");
|
||||
ProfileOptimization.SetProfileRoot(profilePath);
|
||||
ProfileOptimization.StartProfile("AAAAAAAAAA");
|
||||
|
||||
_config.Initialize(true);
|
||||
|
||||
if (Options.LoadConfigAndUserData)
|
||||
@@ -337,21 +336,12 @@ namespace Robust.Server
|
||||
prototypeManager.LoadDirectory(Options.PrototypeDirectory);
|
||||
prototypeManager.Resync();
|
||||
|
||||
IoCManager.Resolve<IServerConsoleHost>().Initialize();
|
||||
_consoleHost.Initialize();
|
||||
_entityManager.Startup();
|
||||
_mapManager.Startup();
|
||||
IoCManager.Resolve<IEntityLookup>().Startup();
|
||||
_stateManager.Initialize();
|
||||
|
||||
// sometime after content init
|
||||
{
|
||||
var reg = _entityManager.ComponentFactory.GetRegistration<TransformComponent>();
|
||||
if (!reg.NetID.HasValue)
|
||||
throw new InvalidOperationException("TransformComponent does not have a NetId.");
|
||||
|
||||
_stateManager.SetTransformNetId(reg.NetID.Value);
|
||||
}
|
||||
|
||||
_scriptHost.Initialize();
|
||||
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
|
||||
@@ -372,6 +362,8 @@ namespace Robust.Server
|
||||
|
||||
GC.Collect();
|
||||
|
||||
ProgramShared.RunExecCommands(_consoleHost, _commandLineArgs?.ExecCommands);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -423,12 +415,12 @@ namespace Robust.Server
|
||||
return false;
|
||||
}
|
||||
|
||||
LokiCredentials credentials;
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
LokiSinkConfiguration cfg = new()
|
||||
{
|
||||
credentials = new NoAuthCredentials(address);
|
||||
}
|
||||
else
|
||||
LokiUrl = address
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
@@ -436,13 +428,16 @@ namespace Robust.Server
|
||||
return false;
|
||||
}
|
||||
|
||||
credentials = new BasicAuthCredentials(address, username, password);
|
||||
cfg.LokiUsername = username;
|
||||
cfg.LokiPassword = password;
|
||||
}
|
||||
|
||||
cfg.LogLabelProvider = new LogLabelProvider(serverName);
|
||||
|
||||
Logger.DebugS("loki", "Loki enabled for server {ServerName} loki address {LokiAddress}.", serverName,
|
||||
address);
|
||||
|
||||
var handler = new LokiLogHandler(serverName, credentials);
|
||||
var handler = new LokiLogHandler(cfg);
|
||||
_log.RootSawmill.AddHandler(handler);
|
||||
return true;
|
||||
}
|
||||
@@ -507,8 +502,6 @@ namespace Robust.Server
|
||||
FinishMainLoop();
|
||||
}
|
||||
|
||||
public bool ContentStart { get; set; }
|
||||
|
||||
public void OverrideMainLoop(IGameLoop gameLoop)
|
||||
{
|
||||
_mainLoop = gameLoop;
|
||||
@@ -538,17 +531,18 @@ namespace Robust.Server
|
||||
}
|
||||
|
||||
// called right before main loop returns, do all saving/cleanup in here
|
||||
private void Cleanup()
|
||||
public void Cleanup()
|
||||
{
|
||||
_modLoader.Shutdown();
|
||||
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
|
||||
|
||||
_playerManager.Shutdown();
|
||||
|
||||
// shut down networking, kicking all players.
|
||||
_network.Shutdown($"Server shutting down: {_shutdownReason}");
|
||||
|
||||
// shutdown entities
|
||||
IoCManager.Resolve<IEntityLookup>().Shutdown();
|
||||
_entityManager.Shutdown();
|
||||
_entityManager.Cleanup();
|
||||
|
||||
if (_config.GetCVar(CVars.LogRuntimeLog))
|
||||
{
|
||||
@@ -569,6 +563,8 @@ namespace Robust.Server
|
||||
{
|
||||
WindowsTickPeriod.TimeEndPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
|
||||
}
|
||||
|
||||
_config.Shutdown();
|
||||
}
|
||||
|
||||
private void Input(FrameEventArgs args)
|
||||
|
||||
@@ -4,130 +4,138 @@ using Robust.Shared;
|
||||
using Robust.Shared.Utility;
|
||||
using C = System.Console;
|
||||
|
||||
namespace Robust.Server
|
||||
namespace Robust.Server;
|
||||
|
||||
internal sealed class CommandLineArgs
|
||||
{
|
||||
internal sealed class CommandLineArgs
|
||||
public MountOptions MountOptions { get; }
|
||||
public string? ConfigFile { get; }
|
||||
public string? DataDir { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> CVars { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
|
||||
public IReadOnlyList<string> ExecCommands { get; set; }
|
||||
|
||||
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
|
||||
// Also I don't like spending 100ms parsing command line args. Do you?
|
||||
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
|
||||
{
|
||||
public MountOptions MountOptions { get; }
|
||||
public string? ConfigFile { get; }
|
||||
public string? DataDir { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> CVars { get; }
|
||||
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
|
||||
parsed = null;
|
||||
string? configFile = null;
|
||||
string? dataDir = null;
|
||||
var cvars = new List<(string, string)>();
|
||||
var logLevels = new List<(string, string)>();
|
||||
var mountOptions = new MountOptions();
|
||||
var execCommands = new List<string>();
|
||||
|
||||
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
|
||||
// Also I don't like spending 100ms parsing command line args. Do you?
|
||||
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
|
||||
using var enumerator = args.GetEnumerator();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
parsed = null;
|
||||
string? configFile = null;
|
||||
string? dataDir = null;
|
||||
var cvars = new List<(string, string)>();
|
||||
var logLevels = new List<(string, string)>();
|
||||
var mountOptions = new MountOptions();
|
||||
|
||||
using var enumerator = args.GetEnumerator();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
var arg = enumerator.Current;
|
||||
if (arg == "--config-file")
|
||||
{
|
||||
var arg = enumerator.Current;
|
||||
if (arg == "--config-file")
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing config file.");
|
||||
return false;
|
||||
}
|
||||
|
||||
configFile = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--data-dir")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing data directory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
dataDir = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--cvar")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing cvar value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var cvar = enumerator.Current;
|
||||
DebugTools.AssertNotNull(cvar);
|
||||
var pos = cvar.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in cvar.");
|
||||
return false;
|
||||
}
|
||||
|
||||
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
|
||||
}
|
||||
else if (arg == "--help")
|
||||
{
|
||||
PrintHelp();
|
||||
C.WriteLine("Missing config file.");
|
||||
return false;
|
||||
}
|
||||
else if (arg == "--mount-zip")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.ZipMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--mount-dir")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.DirMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--loglevel")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing loglevel sawmill.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var loglevel = enumerator.Current;
|
||||
DebugTools.AssertNotNull(loglevel);
|
||||
var pos = loglevel.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in loglevel.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
|
||||
}
|
||||
else
|
||||
{
|
||||
C.WriteLine("Unknown argument: {0}", arg);
|
||||
}
|
||||
configFile = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--data-dir")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing data directory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
parsed = new CommandLineArgs(configFile, dataDir, cvars, logLevels, mountOptions);
|
||||
return true;
|
||||
dataDir = enumerator.Current;
|
||||
}
|
||||
else if (arg == "--cvar")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing cvar value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var cvar = enumerator.Current;
|
||||
DebugTools.AssertNotNull(cvar);
|
||||
var pos = cvar.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in cvar.");
|
||||
return false;
|
||||
}
|
||||
|
||||
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
|
||||
}
|
||||
else if (arg == "--help")
|
||||
{
|
||||
PrintHelp();
|
||||
return false;
|
||||
}
|
||||
else if (arg == "--mount-zip")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.ZipMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--mount-dir")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing mount path");
|
||||
return false;
|
||||
}
|
||||
|
||||
mountOptions.DirMounts.Add(enumerator.Current);
|
||||
}
|
||||
else if (arg == "--loglevel")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
C.WriteLine("Missing loglevel sawmill.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var loglevel = enumerator.Current;
|
||||
DebugTools.AssertNotNull(loglevel);
|
||||
var pos = loglevel.IndexOf('=');
|
||||
|
||||
if (pos == -1)
|
||||
{
|
||||
C.WriteLine("Expected = in loglevel.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
|
||||
}
|
||||
else if (arg.StartsWith("+"))
|
||||
{
|
||||
execCommands.Add(arg[1..]);
|
||||
}
|
||||
else
|
||||
{
|
||||
C.WriteLine("Unknown argument: {0}", arg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
C.WriteLine(@"
|
||||
parsed = new CommandLineArgs(configFile, dataDir, cvars, logLevels, mountOptions, execCommands);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
C.WriteLine(@"
|
||||
Usage: Robust.Server [options] [+command [+command]]
|
||||
|
||||
Options:
|
||||
--config-file Path to the config file to read from.
|
||||
--data-dir Path to the data directory to read/write from/to.
|
||||
@@ -136,16 +144,25 @@ Options:
|
||||
--mount-dir Resource directory to mount.
|
||||
--mount-zip Resource zip to mount.
|
||||
--help Display this help text and exit.
|
||||
");
|
||||
}
|
||||
|
||||
private CommandLineArgs(string? configFile, string? dataDir, IReadOnlyCollection<(string, string)> cVars, IReadOnlyCollection<(string, string)> logLevels, MountOptions mountOptions)
|
||||
{
|
||||
ConfigFile = configFile;
|
||||
DataDir = dataDir;
|
||||
CVars = cVars;
|
||||
LogLevels = logLevels;
|
||||
MountOptions = mountOptions;
|
||||
}
|
||||
+command: You can pass a set of commands, prefixed by +,
|
||||
to be executed in the console in order after the game has finished initializing.
|
||||
");
|
||||
}
|
||||
|
||||
private CommandLineArgs(
|
||||
string? configFile,
|
||||
string? dataDir,
|
||||
IReadOnlyCollection<(string, string)> cVars,
|
||||
IReadOnlyCollection<(string, string)> logLevels,
|
||||
MountOptions mountOptions,
|
||||
IReadOnlyList<string> execCommands)
|
||||
{
|
||||
ConfigFile = configFile;
|
||||
DataDir = dataDir;
|
||||
CVars = cVars;
|
||||
LogLevels = logLevels;
|
||||
MountOptions = mountOptions;
|
||||
ExecCommands = execCommands;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var players = IoCManager.Resolve<IPlayerManager>().GetAllPlayers();
|
||||
var players = IoCManager.Resolve<IPlayerManager>().ServerSessions;
|
||||
sb.AppendLine($"{"Player Name",20} {"Status",12} {"Playing Time",14} {"Ping",9} {"IP EndPoint",20}");
|
||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||
|
||||
@@ -188,7 +188,7 @@ namespace Robust.Server.Console.Commands
|
||||
if (args.Length < 1)
|
||||
{
|
||||
var player = shell.Player as IPlayerSession;
|
||||
var toKickPlayer = player ?? players.GetAllPlayers().FirstOrDefault();
|
||||
var toKickPlayer = player ?? players.ServerSessions.FirstOrDefault();
|
||||
if (toKickPlayer == null)
|
||||
{
|
||||
shell.WriteLine("You need to provide a player to kick.");
|
||||
|
||||
98
Robust.Server/Console/Commands/ScaleCommand.cs
Normal file
98
Robust.Server/Console/Commands/ScaleCommand.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
|
||||
namespace Robust.Server.Console.Commands;
|
||||
|
||||
public sealed class ScaleCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "scale";
|
||||
public string Description => "Increases or decreases an entity's size naively";
|
||||
public string Help => $"{Command} <entityUid> <float>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteError($"Insufficient number of args supplied: expected 2 and received {args.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var uid))
|
||||
{
|
||||
shell.WriteError($"Unable to find entity {args[0]}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!float.TryParse(args[1], out var scale))
|
||||
{
|
||||
shell.WriteError($"Invalid scale supplied of {args[0]}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (scale < 0f)
|
||||
{
|
||||
shell.WriteError($"Invalid scale supplied that is negative!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Event for content to use
|
||||
// We'll just set engine stuff here
|
||||
var @event = new ScaleEntityEvent();
|
||||
var entManager = IoCManager.Resolve<IEntityManager>();
|
||||
entManager.EventBus.RaiseLocalEvent(uid, ref @event);
|
||||
|
||||
if (entManager.TryGetComponent(uid, out SpriteComponent? spriteComponent))
|
||||
{
|
||||
spriteComponent.Scale *= scale;
|
||||
}
|
||||
|
||||
if (entManager.TryGetComponent(uid, out FixturesComponent? manager))
|
||||
{
|
||||
foreach (var (_, fixture) in manager.Fixtures)
|
||||
{
|
||||
// TODO: May be worthwhile to swap to density like box2d? Either way mass is unchanged for now.
|
||||
switch (fixture.Shape)
|
||||
{
|
||||
case EdgeShape edge:
|
||||
edge.Vertex0 *= scale;
|
||||
edge.Vertex1 *= scale;
|
||||
edge.Vertex2 *= scale;
|
||||
edge.Vertex3 *= scale;
|
||||
break;
|
||||
case PhysShapeCircle circle:
|
||||
circle.Position *= scale;
|
||||
circle.Radius *= scale;
|
||||
break;
|
||||
case PolygonShape poly:
|
||||
var verts = poly.Vertices;
|
||||
|
||||
for (var i = 0; i < verts.Length; i++)
|
||||
{
|
||||
verts[i] *= scale;
|
||||
}
|
||||
|
||||
poly.SetVertices(verts);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
EntitySystem.Get<FixtureSystem>().FixtureUpdate(manager);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ScaleEntityEvent
|
||||
{
|
||||
public readonly EntityUid Uid;
|
||||
|
||||
public ScaleEntityEvent(EntityUid uid)
|
||||
{
|
||||
Uid = uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ namespace Robust.Server.Console.Commands
|
||||
Hard = true
|
||||
};
|
||||
|
||||
var broadphase = EntitySystem.Get<SharedBroadphaseSystem>();
|
||||
var broadphase = EntitySystem.Get<FixtureSystem>();
|
||||
|
||||
broadphase.CreateFixture(ground, horizontalFixture);
|
||||
|
||||
@@ -194,7 +194,7 @@ namespace Robust.Server.Console.Commands
|
||||
Hard = true
|
||||
};
|
||||
|
||||
var broadphase = EntitySystem.Get<SharedBroadphaseSystem>();
|
||||
var broadphase = EntitySystem.Get<FixtureSystem>();
|
||||
broadphase.CreateFixture(ground, horizontalFixture);
|
||||
|
||||
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
|
||||
@@ -258,7 +258,7 @@ namespace Robust.Server.Console.Commands
|
||||
Hard = true
|
||||
};
|
||||
|
||||
var broadphase = EntitySystem.Get<SharedBroadphaseSystem>();
|
||||
var broadphase = EntitySystem.Get<FixtureSystem>();
|
||||
broadphase.CreateFixture(ground, horizontalFixture);
|
||||
|
||||
// Setup boxes
|
||||
@@ -296,7 +296,7 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
private void CreateTumbler(MapId mapId)
|
||||
{
|
||||
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
|
||||
var broadphaseSystem = EntitySystem.Get<FixtureSystem>();
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var groundUid = entityManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId)).Uid;
|
||||
|
||||
88
Robust.Server/Console/Commands/ViewSubscriberCommand.cs
Normal file
88
Robust.Server/Console/Commands/ViewSubscriberCommand.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
public sealed class AddViewSubscriberCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "addview";
|
||||
public string Description => $"Allows you to subscribe to an entity's view for debugging purposes";
|
||||
public string Help => $"{Command} <entityUid>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var session = shell.Player;
|
||||
|
||||
if (session is not IPlayerSession playerSession)
|
||||
{
|
||||
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError($"Only 1 arg valid for {Command}, received {args.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var uid))
|
||||
{
|
||||
shell.WriteError($"Unable to parse {args[0]} as a {nameof(EntityUid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var entManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (!entManager.EntityExists(uid))
|
||||
{
|
||||
shell.WriteError($"Unable to find entity {uid}");
|
||||
return;
|
||||
}
|
||||
|
||||
EntitySystem.Get<ViewSubscriberSystem>().AddViewSubscriber(uid, playerSession);
|
||||
}
|
||||
|
||||
public sealed class RemoveViewSubscriberCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "removeview";
|
||||
public string Description => $"Allows you to unsubscribe to an entity's view for debugging purposes";
|
||||
public string Help => $"{Command} <entityUid>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var session = shell.Player;
|
||||
|
||||
if (session is not IPlayerSession playerSession)
|
||||
{
|
||||
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError($"Only 1 arg valid for {Command}, received {args.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var uid))
|
||||
{
|
||||
shell.WriteError($"Unable to parse {args[0]} as a {nameof(EntityUid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var entManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (!entManager.EntityExists(uid))
|
||||
{
|
||||
shell.WriteError($"Unable to find entity {uid}");
|
||||
return;
|
||||
}
|
||||
|
||||
EntitySystem.Get<ViewSubscriberSystem>().RemoveViewSubscriber(uid, playerSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(SharedAppearanceComponent))]
|
||||
public sealed class AppearanceComponent : SharedAppearanceComponent
|
||||
{
|
||||
[ViewVariables]
|
||||
readonly Dictionary<object, object> data = new();
|
||||
|
||||
public override void SetData(string key, object value)
|
||||
{
|
||||
if (data.TryGetValue(key, out var existing) && existing.Equals(value))
|
||||
return;
|
||||
|
||||
data[key] = value;
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override void SetData(Enum key, object value)
|
||||
{
|
||||
if (data.TryGetValue(key, out var existing) && existing.Equals(value))
|
||||
return;
|
||||
|
||||
data[key] = value;
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override T GetData<T>(string key)
|
||||
{
|
||||
return (T)data[key];
|
||||
}
|
||||
|
||||
public override T GetData<T>(Enum key)
|
||||
{
|
||||
return (T)data[key];
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(Enum key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
public override bool TryGetData<T>(string key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
return TryGetData(key, out data);
|
||||
}
|
||||
|
||||
private bool TryGetData<T>(object key, [NotNullWhen(true)] out T data)
|
||||
{
|
||||
if (this.data.TryGetValue(key, out var dat))
|
||||
{
|
||||
data = (T)dat;
|
||||
return true;
|
||||
}
|
||||
|
||||
data = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
return new AppearanceComponentState(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// This is the server instance of <see cref="AppearanceComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(AppearanceComponent))]
|
||||
public sealed class ServerAppearanceComponent : AppearanceComponent { }
|
||||
@@ -85,7 +85,7 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new EyeComponentState(DrawFov, Zoom, Offset, Rotation, VisibilityMask);
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ namespace Robust.Server.GameObjects
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new SpriteComponentState(Visible, DrawDepth, Scale, Rotation, Offset, Color,
|
||||
BaseRSIPath, Layers, RenderOrder);
|
||||
|
||||
@@ -15,8 +15,6 @@ namespace Robust.Server.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private const int AudioDistanceRange = 25;
|
||||
|
||||
private uint _streamIndex;
|
||||
|
||||
private class AudioSourceServer : IPlayingAudioStream
|
||||
@@ -66,9 +64,6 @@ namespace Robust.Server.GameObjects
|
||||
return unchecked(_streamIndex++);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int DefaultSoundRange => AudioDistanceRange;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
@@ -97,7 +92,7 @@ namespace Robust.Server.GameObjects
|
||||
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
//TODO: Calculate this from PAS
|
||||
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
|
||||
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? DefaultSoundRange : audioParams.Value.MaxDistance;
|
||||
|
||||
if(!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
|
||||
return null;
|
||||
@@ -129,7 +124,7 @@ namespace Robust.Server.GameObjects
|
||||
public IPlayingAudioStream Play(Filter playerFilter, string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
//TODO: Calculate this from PAS
|
||||
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
|
||||
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? DefaultSoundRange : audioParams.Value.MaxDistance;
|
||||
|
||||
var id = CacheIdentifier();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -19,7 +20,7 @@ namespace Robust.Server.GameObjects
|
||||
/// <summary>
|
||||
/// Priority queue sorted by how soon the effect will die, we remove messages from the front of the queue during update until caught up
|
||||
/// </summary>
|
||||
private readonly PriorityQueue<EffectSystemMessage> _CurrentEffects = new(new EffectMessageComparer());
|
||||
private readonly PriorityQueue<EffectSystemMessage> _currentEffects = new(new EffectMessageComparer());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a particle effect and sends it to clients.
|
||||
@@ -28,11 +29,11 @@ namespace Robust.Server.GameObjects
|
||||
/// <param name="excludedSession">Session to be excluded for prediction</param>
|
||||
public void CreateParticle(EffectSystemMessage effect, IPlayerSession? excludedSession = null)
|
||||
{
|
||||
_CurrentEffects.Add(effect);
|
||||
_currentEffects.Add(effect);
|
||||
|
||||
//For now we will use this which sends to ALL clients
|
||||
//TODO: Client bubbling
|
||||
foreach (var player in _playerManager.GetAllPlayers())
|
||||
foreach (var player in _playerManager.ServerSessions)
|
||||
{
|
||||
if (player.Status != SessionStatus.InGame || player == excludedSession)
|
||||
continue;
|
||||
@@ -44,9 +45,9 @@ namespace Robust.Server.GameObjects
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
//Take elements from front of priority queue until they are old
|
||||
while (_CurrentEffects.Count != 0 && _CurrentEffects.Peek().DeathTime < _timing.CurTime)
|
||||
while (_currentEffects.Count != 0 && _currentEffects.Peek().DeathTime < _timing.CurTime)
|
||||
{
|
||||
_CurrentEffects.Take();
|
||||
_currentEffects.Take();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace Robust.Server.GameObjects
|
||||
RegisterClass<OccluderComponent>();
|
||||
RegisterClass<OccluderTreeComponent>();
|
||||
RegisterClass<SpriteComponent>();
|
||||
RegisterClass<AppearanceComponent>();
|
||||
RegisterClass<ServerUserInterfaceComponent>();
|
||||
RegisterClass<TimerComponent>();
|
||||
RegisterClass<MapSaveIdComponent>();
|
||||
|
||||
@@ -50,24 +50,24 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityLoad(IEntity entity, IEntityLoadContext? context)
|
||||
{
|
||||
LoadEntity((Entity) entity, context);
|
||||
LoadEntity(entity, context);
|
||||
}
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityInitialization(IEntity entity)
|
||||
{
|
||||
InitializeEntity((Entity) entity);
|
||||
InitializeEntity(entity);
|
||||
}
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityStartup(IEntity entity)
|
||||
{
|
||||
StartEntity((Entity) entity);
|
||||
StartEntity(entity);
|
||||
}
|
||||
|
||||
private protected override Entity CreateEntity(string? prototypeName, EntityUid? uid = null)
|
||||
private protected override IEntity CreateEntity(string? prototypeName, EntityUid? uid = null)
|
||||
{
|
||||
var entity = base.CreateEntity(prototypeName, uid);
|
||||
|
||||
if (prototypeName != null)
|
||||
if (!string.IsNullOrWhiteSpace(prototypeName))
|
||||
{
|
||||
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
|
||||
|
||||
@@ -87,6 +87,13 @@ namespace Robust.Server.GameObjects
|
||||
return entity;
|
||||
}
|
||||
|
||||
public override EntityStringRepresentation ToPrettyString(EntityUid uid)
|
||||
{
|
||||
TryGetComponent(uid, out ActorComponent? actor);
|
||||
|
||||
return base.ToPrettyString(uid) with { Session = actor?.PlayerSession };
|
||||
}
|
||||
|
||||
#region IEntityNetworkManager impl
|
||||
|
||||
public override IEntityNetworkManager EntityNetManager => this;
|
||||
|
||||
42
Robust.Server/GameStates/ChunkIndicesEnumerator.cs
Normal file
42
Robust.Server/GameStates/ChunkIndicesEnumerator.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
public struct ChunkIndicesEnumerator
|
||||
{
|
||||
private Vector2i _topLeft;
|
||||
private Vector2i _bottomRight;
|
||||
|
||||
private int _x;
|
||||
private int _y;
|
||||
|
||||
public ChunkIndicesEnumerator(Box2 viewBox, float chunkSize)
|
||||
{
|
||||
_topLeft = (viewBox.TopLeft / chunkSize).Floored();
|
||||
_bottomRight = (viewBox.BottomRight / chunkSize).Floored();
|
||||
|
||||
_x = _topLeft.X;
|
||||
_y = _bottomRight.Y;
|
||||
}
|
||||
|
||||
public bool MoveNext([NotNullWhen(true)] out Vector2i? chunkIndices)
|
||||
{
|
||||
if (_y > _topLeft.Y)
|
||||
{
|
||||
_x++;
|
||||
_y = _bottomRight.Y;
|
||||
}
|
||||
|
||||
if (_x > _bottomRight.X)
|
||||
{
|
||||
chunkIndices = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
chunkIndices = new Vector2i(_x, _y);
|
||||
|
||||
_y++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,5 @@ namespace Robust.Server.GameStates
|
||||
/// Create and dispatch game states to all connected sessions.
|
||||
/// </summary>
|
||||
void SendGameStateUpdate();
|
||||
|
||||
bool PvsEnabled { get; set; }
|
||||
float PvsRange { get; set; }
|
||||
void SetTransformNetId(ushort netId);
|
||||
}
|
||||
}
|
||||
|
||||
419
Robust.Server/GameStates/PVSCollection.cs
Normal file
419
Robust.Server/GameStates/PVSCollection.cs
Normal file
@@ -0,0 +1,419 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
public interface IPVSCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes all previous additions, removals and updates of indices.
|
||||
/// </summary>
|
||||
public void Process();
|
||||
public void AddPlayer(ICommonSession session);
|
||||
public void AddGrid(GridId gridId);
|
||||
public void AddMap(MapId mapId);
|
||||
|
||||
public void RemovePlayer(ICommonSession session);
|
||||
|
||||
public void RemoveGrid(GridId gridId);
|
||||
|
||||
public void RemoveMap(MapId mapId);
|
||||
|
||||
/// <summary>
|
||||
/// Remove all deletions up to a <see cref="GameTick"/>.
|
||||
/// </summary>
|
||||
/// <param name="tick">The <see cref="GameTick"/> before which all deletions should be removed.</param>
|
||||
public void CullDeletionHistoryUntil(GameTick tick);
|
||||
}
|
||||
|
||||
public class PVSCollection<TIndex, TElement> : IPVSCollection where TIndex : IComparable<TIndex>, IEquatable<TIndex>
|
||||
{
|
||||
[Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Vector2i GetChunkIndices(Vector2 coordinates)
|
||||
{
|
||||
return (coordinates / PVSSystem.ChunkSize).Floored();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delegate to retrieve elements
|
||||
/// </summary>
|
||||
private readonly Func<TIndex, TElement> _getElementDelegate;
|
||||
|
||||
/// <summary>
|
||||
/// Index of which <see cref="TIndex"/> are contained in which mapchunk, indexed by <see cref="Vector2i"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<MapId, Dictionary<Vector2i, HashSet<TIndex>>> _mapChunkContents = new();
|
||||
|
||||
/// <summary>
|
||||
/// Index of which <see cref="TIndex"/> are contained in which gridchunk, indexed by <see cref="Vector2i"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<GridId, Dictionary<Vector2i, HashSet<TIndex>>> _gridChunkContents = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="TIndex"/> that should always get sent.
|
||||
/// </summary>
|
||||
private readonly HashSet<TIndex> _globalOverrides = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="TIndex"/> that should always get sent.
|
||||
/// </summary>
|
||||
public IReadOnlySet<TIndex> GlobalOverrides => _globalOverrides;
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="TIndex"/> that should always get sent to a certain <see cref="ICommonSession"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _localOverrides = new();
|
||||
|
||||
/// <summary>
|
||||
/// Which <see cref="TIndex"/> where last seen/sent to a certain <see cref="ICommonSession"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _lastSeen = new();
|
||||
|
||||
/// <summary>
|
||||
/// History of deletion-tuples, containing the <see cref="GameTick"/> of the deletion, as well as the <see cref="TIndex"/> of the object which was deleted.
|
||||
/// </summary>
|
||||
private readonly List<(GameTick tick, TIndex index)> _deletionHistory = new();
|
||||
|
||||
/// <summary>
|
||||
/// An index containing the <see cref="IndexLocation"/>s of all <see cref="TIndex"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<TIndex, IndexLocation> _indexLocations = new();
|
||||
|
||||
/// <summary>
|
||||
/// Buffer of all locationchanges since the last process call
|
||||
/// </summary>
|
||||
private readonly Dictionary<TIndex, IndexLocation> _locationChangeBuffer = new();
|
||||
/// <summary>
|
||||
/// Buffer of all indexremovals since the last process call
|
||||
/// </summary>
|
||||
private readonly Dictionary<TIndex, GameTick> _removalBuffer = new();
|
||||
|
||||
public PVSCollection(Func<TIndex, TElement> getElementDelegate)
|
||||
{
|
||||
_getElementDelegate = getElementDelegate;
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public void Process()
|
||||
{
|
||||
var changedIndices = new HashSet<TIndex>(_locationChangeBuffer.Keys);
|
||||
|
||||
var changedChunkLocations = new HashSet<IndexLocation>();
|
||||
foreach (var (index, tick) in _removalBuffer)
|
||||
{
|
||||
//changes dont need to be computed if we are removing the index anyways
|
||||
if (changedIndices.Remove(index) && !_indexLocations.ContainsKey(index))
|
||||
{
|
||||
//this index wasnt added yet, so we can safely just skip the deletion
|
||||
continue;
|
||||
}
|
||||
|
||||
var location = RemoveIndexInternal(index);
|
||||
if(location is GridChunkLocation or MapChunkLocation)
|
||||
changedChunkLocations.Add(location);
|
||||
_deletionHistory.Add((tick, index));
|
||||
}
|
||||
|
||||
// remove empty chunk-subsets
|
||||
foreach (var chunkLocation in changedChunkLocations)
|
||||
{
|
||||
switch (chunkLocation)
|
||||
{
|
||||
case GridChunkLocation gridChunkLocation:
|
||||
if (_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Count == 0)
|
||||
_gridChunkContents[gridChunkLocation.GridId].Remove(gridChunkLocation.ChunkIndices);
|
||||
break;
|
||||
case MapChunkLocation mapChunkLocation:
|
||||
if (_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Count == 0)
|
||||
_mapChunkContents[mapChunkLocation.MapId].Remove(mapChunkLocation.ChunkIndices);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var index in changedIndices)
|
||||
{
|
||||
RemoveIndexInternal(index);
|
||||
|
||||
AddIndexInternal(index, _locationChangeBuffer[index]);
|
||||
}
|
||||
|
||||
_locationChangeBuffer.Clear();
|
||||
_removalBuffer.Clear();
|
||||
}
|
||||
|
||||
public bool TryGetChunk(MapId mapId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
|
||||
_mapChunkContents[mapId].TryGetValue(chunkIndices, out indices);
|
||||
|
||||
public bool TryGetChunk(GridId gridId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
|
||||
_gridChunkContents[gridId].TryGetValue(chunkIndices, out indices);
|
||||
|
||||
public IReadOnlySet<TIndex> GetElementsForSession(ICommonSession session) => _localOverrides[session];
|
||||
|
||||
private void AddIndexInternal(TIndex index, IndexLocation location)
|
||||
{
|
||||
switch (location)
|
||||
{
|
||||
case GlobalOverride _:
|
||||
_globalOverrides.Add(index);
|
||||
break;
|
||||
case GridChunkLocation gridChunkLocation:
|
||||
// might be gone due to grid-deletions
|
||||
if(!_gridChunkContents.ContainsKey(gridChunkLocation.GridId)) return;
|
||||
if(!_gridChunkContents[gridChunkLocation.GridId].ContainsKey(gridChunkLocation.ChunkIndices))
|
||||
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices] = new();
|
||||
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Add(index);
|
||||
break;
|
||||
case LocalOverride localOverride:
|
||||
// might be gone due to disconnects
|
||||
if(!_localOverrides.ContainsKey(localOverride.Session)) return;
|
||||
_localOverrides[localOverride.Session].Add(index);
|
||||
break;
|
||||
case MapChunkLocation mapChunkLocation:
|
||||
// might be gone due to map-deletions
|
||||
if(!_mapChunkContents.ContainsKey(mapChunkLocation.MapId)) return;
|
||||
if(!_mapChunkContents[mapChunkLocation.MapId].ContainsKey(mapChunkLocation.ChunkIndices))
|
||||
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices] = new();
|
||||
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Add(index);
|
||||
break;
|
||||
}
|
||||
|
||||
// we want this to throw if there is already an entry because if that happens we fucked up somewhere
|
||||
_indexLocations.Add(index, location);
|
||||
}
|
||||
|
||||
private IndexLocation? RemoveIndexInternal(TIndex index)
|
||||
{
|
||||
// the index might be gone due to disconnects/grid-/map-deletions
|
||||
if (!_indexLocations.TryGetValue(index, out var location))
|
||||
return null;
|
||||
// since we can find the index, we can assume the dicts will be there too & dont need to do any checks. gaming.
|
||||
switch (location)
|
||||
{
|
||||
case GlobalOverride _:
|
||||
_globalOverrides.Remove(index);
|
||||
break;
|
||||
case GridChunkLocation gridChunkLocation:
|
||||
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Remove(index);
|
||||
break;
|
||||
case LocalOverride localOverride:
|
||||
_localOverrides[localOverride.Session].Remove(index);
|
||||
break;
|
||||
case MapChunkLocation mapChunkLocation:
|
||||
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Remove(index);
|
||||
break;
|
||||
}
|
||||
|
||||
_indexLocations.Remove(index);
|
||||
return location;
|
||||
}
|
||||
|
||||
#region Init Functions
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddPlayer(ICommonSession session)
|
||||
{
|
||||
_localOverrides[session] = new();
|
||||
_lastSeen[session] = new();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddGrid(GridId gridId) => _gridChunkContents[gridId] = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddMap(MapId mapId) => _mapChunkContents[mapId] = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region ShutdownFunctions
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemovePlayer(ICommonSession session)
|
||||
{
|
||||
foreach (var index in _localOverrides[session])
|
||||
{
|
||||
_indexLocations.Remove(index);
|
||||
}
|
||||
_localOverrides.Remove(session);
|
||||
_lastSeen.Remove(session);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveGrid(GridId gridId)
|
||||
{
|
||||
foreach (var (_, indices) in _gridChunkContents[gridId])
|
||||
{
|
||||
foreach (var index in indices)
|
||||
{
|
||||
_indexLocations.Remove(index);
|
||||
}
|
||||
}
|
||||
_gridChunkContents.Remove(gridId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveMap(MapId mapId)
|
||||
{
|
||||
foreach (var (_, indices) in _mapChunkContents[mapId])
|
||||
{
|
||||
foreach (var index in indices)
|
||||
{
|
||||
_indexLocations.Remove(index);
|
||||
}
|
||||
}
|
||||
_mapChunkContents.Remove(mapId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeletionHistory & RemoveIndex
|
||||
|
||||
/// <summary>
|
||||
/// Registers a deletion of an <see cref="TIndex"/> on a <see cref="GameTick"/>. WARNING: this also clears the index out of the internal cache!!!
|
||||
/// </summary>
|
||||
/// <param name="tick">The <see cref="GameTick"/> at which the deletion took place.</param>
|
||||
/// <param name="index">The <see cref="TIndex"/> of the removed object.</param>
|
||||
public void RemoveIndex(GameTick tick, TIndex index)
|
||||
{
|
||||
_removalBuffer[index] = tick;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CullDeletionHistoryUntil(GameTick tick) => _deletionHistory.RemoveAll(hist => hist.tick < tick);
|
||||
|
||||
public List<TIndex> GetDeletedIndices(GameTick fromTick)
|
||||
{
|
||||
var list = new List<TIndex>();
|
||||
foreach (var (tick, id) in _deletionHistory)
|
||||
{
|
||||
if (tick >= fromTick) list.Add(id);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateIndex
|
||||
|
||||
private bool IsOverride(TIndex index)
|
||||
{
|
||||
if (_locationChangeBuffer.TryGetValue(index, out var change) &&
|
||||
change is GlobalOverride or LocalOverride) return true;
|
||||
|
||||
if (_indexLocations.TryGetValue(index, out var indexLoc) &&
|
||||
indexLoc is GlobalOverride or LocalOverride) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an <see cref="TIndex"/> to be sent to all players at all times.
|
||||
/// </summary>
|
||||
/// <param name="index">The <see cref="TIndex"/> to update.</param>
|
||||
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
|
||||
public void UpdateIndex(TIndex index, bool removeFromOverride = false)
|
||||
{
|
||||
if(!removeFromOverride && IsOverride(index))
|
||||
return;
|
||||
|
||||
RegisterUpdate(index, new GlobalOverride());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an <see cref="TIndex"/> to be sent to a specific <see cref="ICommonSession"/> at all times.
|
||||
/// </summary>
|
||||
/// <param name="index">The <see cref="TIndex"/> to update.</param>
|
||||
/// <param name="session">The <see cref="ICommonSession"/> receiving the object.</param>
|
||||
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
|
||||
public void UpdateIndex(TIndex index, ICommonSession session, bool removeFromOverride = false)
|
||||
{
|
||||
if(!removeFromOverride && IsOverride(index))
|
||||
return;
|
||||
|
||||
RegisterUpdate(index, new LocalOverride(session));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an <see cref="TIndex"/> with the location based on the provided <see cref="EntityCoordinates"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The <see cref="TIndex"/> to update.</param>
|
||||
/// <param name="coordinates">The <see cref="EntityCoordinates"/> to use when adding the <see cref="TIndex"/> to the internal cache.</param>
|
||||
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
|
||||
public void UpdateIndex(TIndex index, EntityCoordinates coordinates, bool removeFromOverride = false)
|
||||
{
|
||||
if(!removeFromOverride && IsOverride(index))
|
||||
return;
|
||||
|
||||
var gridId = coordinates.GetGridId(_entityManager);
|
||||
if (gridId != GridId.Invalid)
|
||||
{
|
||||
var gridIndices = GetChunkIndices(_mapManager.GetGrid(gridId).LocalToGrid(coordinates));
|
||||
UpdateIndex(index, gridId, gridIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
|
||||
return;
|
||||
}
|
||||
|
||||
var mapId = coordinates.GetMapId(_entityManager);
|
||||
var mapIndices = GetChunkIndices(coordinates.ToMapPos(_entityManager));
|
||||
UpdateIndex(index, mapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an <see cref="TIndex"/> using the provided <see cref="gridId"/> and <see cref="chunkIndices"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The <see cref="TIndex"/> to update.</param>
|
||||
/// <param name="gridId">The id of the grid.</param>
|
||||
/// <param name="chunkIndices">The indices of the chunk.</param>
|
||||
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
|
||||
public void UpdateIndex(TIndex index, GridId gridId, Vector2i chunkIndices, bool removeFromOverride = false)
|
||||
{
|
||||
if(!removeFromOverride && IsOverride(index))
|
||||
return;
|
||||
|
||||
RegisterUpdate(index, new GridChunkLocation(gridId, chunkIndices));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an <see cref="TIndex"/> using the provided <see cref="mapId"/> and <see cref="chunkIndices"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The <see cref="TIndex"/> to update.</param>
|
||||
/// <param name="mapId">The id of the map.</param>
|
||||
/// <param name="chunkIndices">The indices of the mapchunk.</param>
|
||||
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
|
||||
public void UpdateIndex(TIndex index, MapId mapId, Vector2i chunkIndices, bool removeFromOverride = false)
|
||||
{
|
||||
if(!removeFromOverride && IsOverride(index))
|
||||
return;
|
||||
|
||||
RegisterUpdate(index, new MapChunkLocation(mapId, chunkIndices));
|
||||
}
|
||||
|
||||
private void RegisterUpdate(TIndex index, IndexLocation location)
|
||||
{
|
||||
if(_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation == location) return;
|
||||
|
||||
_locationChangeBuffer[index] = location;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IndexLocations
|
||||
|
||||
private abstract record IndexLocation;
|
||||
private record MapChunkLocation(MapId MapId, Vector2i ChunkIndices) : IndexLocation;
|
||||
private record GridChunkLocation(GridId GridId, Vector2i ChunkIndices) : IndexLocation;
|
||||
private record GlobalOverride : IndexLocation;
|
||||
private record LocalOverride(ICommonSession Session) : IndexLocation;
|
||||
|
||||
#endregion
|
||||
}
|
||||
21
Robust.Server/GameStates/PVSEntityPacket.cs
Normal file
21
Robust.Server/GameStates/PVSEntityPacket.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
public struct PVSEntityPacket
|
||||
{
|
||||
public readonly TransformComponent TransformComponent;
|
||||
public readonly MetaDataComponent MetaDataComponent;
|
||||
public readonly VisibilityComponent? VisibilityComponent;
|
||||
public readonly ContainerManagerComponent? ContainerManagerComponent;
|
||||
|
||||
public PVSEntityPacket(IEntityManager entityManager, EntityUid uid)
|
||||
{
|
||||
TransformComponent = entityManager.GetComponent<TransformComponent>(uid);
|
||||
MetaDataComponent = entityManager.GetComponent<MetaDataComponent>(uid);
|
||||
entityManager.TryGetComponent(uid, out VisibilityComponent);
|
||||
entityManager.TryGetComponent(uid, out ContainerManagerComponent);
|
||||
}
|
||||
}
|
||||
8
Robust.Server/GameStates/PVSEntityVisiblity.cs
Normal file
8
Robust.Server/GameStates/PVSEntityVisiblity.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
public enum PVSEntityVisiblity : byte
|
||||
{
|
||||
Entered,
|
||||
StayedUnchanged,
|
||||
StayedChanged
|
||||
}
|
||||
89
Robust.Server/GameStates/PVSSystem.Dirty.cs
Normal file
89
Robust.Server/GameStates/PVSSystem.Dirty.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
/// <summary>
|
||||
/// Caching for dirty bodies
|
||||
/// </summary>
|
||||
internal partial class PVSSystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private const int DirtyBufferSize = 4;
|
||||
|
||||
/// <summary>
|
||||
/// if it's a new entity we need to GetEntityState from tick 0.
|
||||
/// </summary>
|
||||
private HashSet<EntityUid>[] _addEntities = new HashSet<EntityUid>[DirtyBufferSize];
|
||||
private HashSet<EntityUid>[] _dirtyEntities = new HashSet<EntityUid>[DirtyBufferSize];
|
||||
private int _currentIndex = 1;
|
||||
|
||||
private void InitializeDirty()
|
||||
{
|
||||
for (var i = 0; i < DirtyBufferSize; i++)
|
||||
{
|
||||
_addEntities[i] = new HashSet<EntityUid>(32);
|
||||
_dirtyEntities[i] = new HashSet<EntityUid>(32);
|
||||
}
|
||||
EntityManager.EntityAdded += OnEntityAdd;
|
||||
SubscribeLocalEvent<EntityDirtyEvent>(OnDirty);
|
||||
}
|
||||
|
||||
private void ShutdownDirty()
|
||||
{
|
||||
EntityManager.EntityAdded -= OnEntityAdd;
|
||||
}
|
||||
|
||||
private void OnEntityAdd(object? sender, EntityUid e)
|
||||
{
|
||||
DebugTools.Assert(_currentIndex == _gameTiming.CurTick.Value % DirtyBufferSize);
|
||||
_addEntities[_currentIndex].Add(e);
|
||||
}
|
||||
|
||||
private void OnDirty(ref EntityDirtyEvent ev)
|
||||
{
|
||||
if (_addEntities[_currentIndex].Contains(ev.Uid) ||
|
||||
EntityManager.GetComponent<MetaDataComponent>(ev.Uid).EntityLifeStage < EntityLifeStage.Initialized) return;
|
||||
|
||||
_dirtyEntities[_currentIndex].Add(ev.Uid);
|
||||
}
|
||||
|
||||
private void CleanupDirty(IEnumerable<IPlayerSession> sessions)
|
||||
{
|
||||
// Just in case we desync somehow
|
||||
_currentIndex = ((int) _gameTiming.CurTick.Value + 1) % DirtyBufferSize;
|
||||
_addEntities[_currentIndex].Clear();
|
||||
_dirtyEntities[_currentIndex].Clear();
|
||||
}
|
||||
|
||||
private bool TryGetTick(GameTick tick, [NotNullWhen(true)] out HashSet<EntityUid>? addEntities, [NotNullWhen(true)] out HashSet<EntityUid>? dirtyEntities)
|
||||
{
|
||||
var currentTick = _gameTiming.CurTick;
|
||||
if (currentTick.Value - tick.Value >= DirtyBufferSize)
|
||||
{
|
||||
addEntities = null;
|
||||
dirtyEntities = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = tick.Value % DirtyBufferSize;
|
||||
if (index > _dirtyEntities.Length - 1)
|
||||
{
|
||||
addEntities = null;
|
||||
dirtyEntities = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
addEntities = _addEntities[index];
|
||||
dirtyEntities = _dirtyEntities[index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
@@ -15,7 +16,6 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -42,23 +42,6 @@ namespace Robust.Server.GameStates
|
||||
|
||||
private ISawmill _logger = default!;
|
||||
|
||||
public bool PvsEnabled
|
||||
{
|
||||
get => _configurationManager.GetCVar(CVars.NetPVS);
|
||||
set => _configurationManager.SetCVar(CVars.NetPVS, value);
|
||||
}
|
||||
|
||||
public float PvsRange
|
||||
{
|
||||
get => _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
|
||||
set => _configurationManager.SetCVar(CVars.NetMaxUpdateRange, value);
|
||||
}
|
||||
|
||||
public void SetTransformNetId(ushort netId)
|
||||
{
|
||||
_pvs.SetTransformNetId(netId);
|
||||
}
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_logger = Logger.GetSawmill("PVS");
|
||||
@@ -121,15 +104,13 @@ namespace Robust.Server.GameStates
|
||||
{
|
||||
DebugTools.Assert(_networkManager.IsServer);
|
||||
|
||||
_pvs.ViewSize = PvsRange * 2;
|
||||
_pvs.CullingEnabled = PvsEnabled;
|
||||
|
||||
if (!_networkManager.IsConnected)
|
||||
{
|
||||
// Prevent deletions piling up if we have no clients.
|
||||
_entityManager.CullDeletionHistory(GameTick.MaxValue);
|
||||
_pvs.CullDeletionHistory(GameTick.MaxValue);
|
||||
_mapManager.CullDeletionHistory(GameTick.MaxValue);
|
||||
_pvs.Cleanup(_playerManager.ServerSessions);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,7 +171,9 @@ namespace Robust.Server.GameStates
|
||||
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
|
||||
}
|
||||
|
||||
Parallel.ForEach(_playerManager.GetAllPlayers(), session =>
|
||||
_pvs.ProcessCollections();
|
||||
|
||||
Parallel.ForEach(_playerManager.ServerSessions, session =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -202,6 +185,7 @@ namespace Robust.Server.GameStates
|
||||
}
|
||||
});
|
||||
|
||||
_pvs.Cleanup(_playerManager.ServerSessions);
|
||||
var oldestAck = new GameTick(oldestAckValue);
|
||||
|
||||
// keep the deletion history buffers clean
|
||||
@@ -213,78 +197,5 @@ namespace Robust.Server.GameStates
|
||||
_mapManager.CullDeletionHistory(oldestAck);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a network entity state for the given entity.
|
||||
/// </summary>
|
||||
/// <param name="entMan">EntityManager that contains the entity.</param>
|
||||
/// <param name="player">The player to generate this state for.</param>
|
||||
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
|
||||
/// <param name="fromTick">Only provide delta changes from this tick.</param>
|
||||
/// <returns>New entity State for the given entity.</returns>
|
||||
internal static EntityState GetEntityState(IServerEntityManager entMan, ICommonSession player, EntityUid entityUid, GameTick fromTick)
|
||||
{
|
||||
var bus = entMan.EventBus;
|
||||
var changed = new List<ComponentChange>();
|
||||
|
||||
foreach (var (netId, component) in entMan.GetNetComponents(entityUid))
|
||||
{
|
||||
DebugTools.Assert(component.Initialized);
|
||||
|
||||
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
|
||||
// "not different from entity creation".
|
||||
// i.e. when the client spawns the entity and loads the entity prototype,
|
||||
// the data it deserializes from the prototype SHOULD be equal
|
||||
// to what the component state / ComponentChange would send.
|
||||
// As such, we can avoid sending this data in this case since the client "already has it".
|
||||
|
||||
DebugTools.Assert(component.LastModifiedTick >= component.CreationTick);
|
||||
|
||||
if (component.CreationTick != GameTick.Zero && component.CreationTick >= fromTick && !component.Deleted)
|
||||
{
|
||||
ComponentState? state = null;
|
||||
if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
|
||||
state = entMan.GetComponentState(bus, component, player);
|
||||
|
||||
// Can't be null since it's returned by GetNetComponents
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
changed.Add(ComponentChange.Added(netId, state));
|
||||
}
|
||||
else if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
|
||||
{
|
||||
changed.Add(ComponentChange.Changed(netId, entMan.GetComponentState(bus, component, player)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var netId in entMan.GetDeletedComponents(entityUid, fromTick))
|
||||
{
|
||||
changed.Add(ComponentChange.Removed(netId));
|
||||
}
|
||||
|
||||
return new EntityState(entityUid, changed.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all entity states that have been modified after and including the provided tick.
|
||||
/// </summary>
|
||||
internal static List<EntityState>? GetAllEntityStates(IServerEntityManager entityMan, ICommonSession player, GameTick fromTick)
|
||||
{
|
||||
var stateEntities = new List<EntityState>();
|
||||
foreach (var entity in entityMan.GetEntities())
|
||||
{
|
||||
if (entity.Deleted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(entity.Initialized);
|
||||
|
||||
if (entity.LastModifiedTick >= fromTick)
|
||||
stateEntities.Add(GetEntityState(entityMan, player, entity.Uid, fromTick));
|
||||
}
|
||||
|
||||
// no point sending an empty collection
|
||||
return stateEntities.Count == 0 ? default : stateEntities;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ namespace Robust.Server.Log
|
||||
{
|
||||
private readonly SLogger _sLogger;
|
||||
|
||||
public LokiLogHandler(string serverName, LokiCredentials credentials)
|
||||
public LokiLogHandler(LokiSinkConfiguration configuration)
|
||||
{
|
||||
_sLogger = new LoggerConfiguration()
|
||||
.WriteTo.LokiHttp(credentials, new LogLabelProvider(serverName))
|
||||
.WriteTo.LokiHttp(() => configuration)
|
||||
.MinimumLevel.Debug()
|
||||
.CreateLogger();
|
||||
}
|
||||
@@ -37,24 +37,28 @@ namespace Robust.Server.Log
|
||||
{
|
||||
_sLogger.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LogLabelProvider : ILogLabelProvider
|
||||
public sealed class LogLabelProvider : ILogLabelProvider
|
||||
{
|
||||
private readonly string _serverName;
|
||||
|
||||
public LogLabelProvider(string serverName)
|
||||
{
|
||||
private readonly string _serverName;
|
||||
|
||||
public LogLabelProvider(string serverName)
|
||||
{
|
||||
_serverName = serverName;
|
||||
}
|
||||
|
||||
public IList<LokiLabel> GetLabels()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new LokiLabel("App", "Robust.Server"),
|
||||
new LokiLabel("Server", _serverName),
|
||||
};
|
||||
}
|
||||
_serverName = serverName;
|
||||
}
|
||||
|
||||
public IList<LokiLabel> GetLabels()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new LokiLabel("App", "Robust.Server"),
|
||||
new LokiLabel("Server", _serverName),
|
||||
};
|
||||
}
|
||||
|
||||
public IList<string> PropertiesAsLabels => new[] {"level"};
|
||||
public IList<string> PropertiesToAppend => Array.Empty<string>();
|
||||
public LokiFormatterStrategy FormatterStrategy => LokiFormatterStrategy.SpecificPropertiesAsLabelsAndRestAppended;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ namespace Robust.Server.Map
|
||||
|
||||
// TODO: Like MapManager injecting this is a PITA so need to work out an easy way to do it.
|
||||
// Maybe just add like a PostInject method that gets called way later?
|
||||
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
|
||||
var fixtureSystem = EntitySystem.Get<FixtureSystem>();
|
||||
|
||||
foreach (var fixture in chunk.Fixtures)
|
||||
{
|
||||
broadphaseSystem.DestroyFixture(body, fixture);
|
||||
fixtureSystem.DestroyFixture(body, fixture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Server.Physics;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -440,19 +441,21 @@ namespace Robust.Server.Maps
|
||||
{
|
||||
var entManager = IoCManager.Resolve<IEntityManager>();
|
||||
var gridFixtures = EntitySystem.Get<GridFixtureSystem>();
|
||||
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
|
||||
var fixtureSystem = EntitySystem.Get<FixtureSystem>();
|
||||
|
||||
foreach (var grid in Grids)
|
||||
{
|
||||
var gridInternal = (IMapGridInternal) grid;
|
||||
var body = entManager.EnsureComponent<PhysicsComponent>(entManager.GetEntity(grid.GridEntityId));
|
||||
var body = entManager.EnsureComponent<PhysicsComponent>(grid.GridEntityId);
|
||||
body.Broadphase = _mapManager.GetMapEntity(grid.ParentMapId).GetComponent<BroadphaseComponent>();
|
||||
var fixtures = entManager.EnsureComponent<FixturesComponent>(grid.GridEntityId);
|
||||
gridFixtures.ProcessGrid(gridInternal);
|
||||
|
||||
// Need to go through and double-check we don't have any hanging-on fixtures that
|
||||
// no longer apply (e.g. due to an update in GridFixtureSystem)
|
||||
var toRemove = new List<Fixture>();
|
||||
var toRemove = new RemQueue<Fixture>();
|
||||
|
||||
foreach (var fixture in body.Fixtures)
|
||||
foreach (var (_, fixture) in fixtures.Fixtures)
|
||||
{
|
||||
var found = false;
|
||||
|
||||
@@ -476,8 +479,10 @@ namespace Robust.Server.Maps
|
||||
|
||||
foreach (var fixture in toRemove)
|
||||
{
|
||||
broadphaseSystem.DestroyFixture(fixture);
|
||||
fixtureSystem.DestroyFixture(body, fixture, false, fixtures);
|
||||
}
|
||||
|
||||
fixtureSystem.FixtureUpdate(fixtures, body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,12 +504,14 @@ namespace Robust.Server.Maps
|
||||
|
||||
private void FixMapEntities()
|
||||
{
|
||||
var pvs = EntitySystem.Get<PVSSystem>();
|
||||
foreach (var entity in Entities)
|
||||
{
|
||||
if (entity.TryGetComponent(out IMapGridComponent? grid))
|
||||
{
|
||||
var castGrid = (MapGrid) grid.Grid;
|
||||
castGrid.GridEntityId = entity.Uid;
|
||||
pvs?.EntityPVSCollection.UpdateIndex(entity.Uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user