mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee330d0ae9 | ||
|
|
6c44dd9665 | ||
|
|
c81413b0b4 | ||
|
|
88b3a557da | ||
|
|
572eb01290 | ||
|
|
9dab74c9d5 | ||
|
|
e1cb1e1b9c | ||
|
|
a23da702b1 | ||
|
|
ae9c2423ff | ||
|
|
a6dae8e30a | ||
|
|
96c0a4ae1f | ||
|
|
c26ebcbc78 | ||
|
|
8334050411 | ||
|
|
cc67edfc2c | ||
|
|
943ea9e6c8 | ||
|
|
3aa5cefe03 | ||
|
|
c5b34bab69 | ||
|
|
e4f24ec125 | ||
|
|
250971ade7 | ||
|
|
718adf9740 | ||
|
|
5d63aa8c95 | ||
|
|
17af3612a5 | ||
|
|
d2ecf6b9b1 | ||
|
|
2a1eda0d38 | ||
|
|
f0180abeb0 | ||
|
|
720f1b1d05 | ||
|
|
ae45a96753 | ||
|
|
74257c72ee | ||
|
|
cea088f4b4 | ||
|
|
88678e7d58 | ||
|
|
d222e25d22 | ||
|
|
f0d7fbb6f2 | ||
|
|
adec0e71ec | ||
|
|
86d9067d62 | ||
|
|
b989c9dbee | ||
|
|
3101ec1985 | ||
|
|
9ec2da1777 | ||
|
|
0acd28e8f4 | ||
|
|
c340f50ee5 | ||
|
|
2b589215aa | ||
|
|
490a567ad4 | ||
|
|
32f0c49484 | ||
|
|
61113d2434 | ||
|
|
6ba1baa88c | ||
|
|
07867acb9a | ||
|
|
3e28b083b9 | ||
|
|
68d9e13edf | ||
|
|
a0c0a722c9 | ||
|
|
bf4aead1e8 | ||
|
|
39f0d2e974 | ||
|
|
b20ae98f21 | ||
|
|
7899780543 | ||
|
|
0c9e322b3e | ||
|
|
6005208285 | ||
|
|
2b15831349 | ||
|
|
1b2450d1cb | ||
|
|
5f31036ab2 | ||
|
|
8efffc471d | ||
|
|
eb9e842027 | ||
|
|
f9cd9ac12a | ||
|
|
8fd98c75a9 | ||
|
|
7c008e857d | ||
|
|
4de2e35e66 | ||
|
|
d4467acf93 | ||
|
|
f3babcc39f | ||
|
|
f491bb5571 | ||
|
|
b201f10c76 | ||
|
|
a9208c0d29 | ||
|
|
b8cc01d872 | ||
|
|
2d827890e9 | ||
|
|
f86d6ccd3c | ||
|
|
967b76483a | ||
|
|
ef2c0ad8cf | ||
|
|
9ae1352030 | ||
|
|
d8d9b271cc | ||
|
|
122acc5fd5 | ||
|
|
32dea84196 | ||
|
|
91d58dbca4 | ||
|
|
c54b1572f5 | ||
|
|
1fa979c0f6 | ||
|
|
760599171d | ||
|
|
10d295d535 | ||
|
|
0047c5000f | ||
|
|
810a6d190f | ||
|
|
197227dcf6 | ||
|
|
fa23ec8fc6 | ||
|
|
6506171ea0 | ||
|
|
8bd1e72e9f | ||
|
|
4ce6629ace | ||
|
|
f9ef605903 | ||
|
|
c6b74e998f | ||
|
|
c4946b8466 | ||
|
|
ffa908bf27 | ||
|
|
0d37ff3f20 | ||
|
|
7aecdcf70a | ||
|
|
70f82d6db8 | ||
|
|
20b7870739 | ||
|
|
172639baea | ||
|
|
6038483b1e | ||
|
|
39d98d591c | ||
|
|
01c2fc0730 | ||
|
|
1884bb0067 | ||
|
|
1c368bbaa8 | ||
|
|
d16078a35f | ||
|
|
4dd04207ac | ||
|
|
02af42da30 | ||
|
|
2c75c8b36d | ||
|
|
013e6f7ce4 | ||
|
|
cbd7b62ad7 | ||
|
|
c1396f1c50 | ||
|
|
3ec9e7a734 | ||
|
|
3a1e6e84b1 | ||
|
|
7224419f77 | ||
|
|
056e4de0c1 | ||
|
|
aa90f22e23 | ||
|
|
071234095d | ||
|
|
5b06391159 | ||
|
|
8edd44086b | ||
|
|
ccf212e9cb | ||
|
|
493011d1f9 | ||
|
|
40e193df33 | ||
|
|
5068294d38 | ||
|
|
24054b5e2f | ||
|
|
17869c16cd | ||
|
|
d8aad89c2f | ||
|
|
2a349eb023 | ||
|
|
47ad07b3d2 | ||
|
|
aacf6522b4 | ||
|
|
c73d27b9ae | ||
|
|
f068b30a7c | ||
|
|
5400dddcfc |
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
||||
7
Avalonia.Base/Avalonia.Base.csproj
Normal file
7
Avalonia.Base/Avalonia.Base.csproj
Normal file
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
2
Avalonia.Base/README.md
Normal file
2
Avalonia.Base/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
See `Robust.Client/UserInterface/XAML/RiderNotes.md` for why this project exists.
|
||||
We are not actually using Avalonia (yet).
|
||||
@@ -4,5 +4,6 @@
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<LangVersion>9</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -14,12 +14,6 @@
|
||||
<PackageReference Condition="'$(TargetFramework)' == 'net472'" Include="System.Memory" Version="4.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="OpenToolkit.GraphicsLibraryFramework.dll.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<configuration>
|
||||
<!-- I actually have no idea whether this works on FreeBSD but it can't hurt to set it as such. -->
|
||||
<dllmap os="linux,freebsd" dll="glfw3.dll" target="glfw.so.3" />
|
||||
<dllmap os="osx" dll="glfw3.dll" target="glfw.3.dylib" />
|
||||
</configuration>
|
||||
25
README.md
25
README.md
@@ -1,25 +1,16 @@
|
||||

|
||||
|
||||
[](https://ci.appveyor.com/project/Silvertorch5/space-station-14/branch/master)
|
||||
[](https://sonarcloud.io/dashboard?id=ss14)
|
||||
Robust Toolbox is an engine primarily being developed for [Space Station 14](https://github.com/space-wizards/space-station-14), although we're working on making it usable for both [singleplayer](https://github.com/space-wizards/RobustToolboxTemplateSingleplayer) and [multiplayer](https://github.com/space-wizards/RobustToolboxTemplate) projects.
|
||||
|
||||
Robust Toolbox is a client/server backend for [Space Station 14](https://github.com/space-wizards/space-station-14).
|
||||
Use the [content repo](https://github.com/space-wizards/space-station-14) for actual development, even if you're modifying the engine itself.
|
||||
|
||||
### *This repository is the *engine* section of SS14. This is the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/space-wizards/space-station-14). Use said repo for actual development, even if you're modifying the engine itself. Think of Robust Toolbox as BYOND in the context of Spacestation 13.*
|
||||
## Project Links
|
||||
|
||||
## Getting in Touch
|
||||
[Website](https://spacestation14.io/) | [Discord](https://discord.gg/t2jac3p) | [Forum](https://forum.spacestation14.io/) | [Steam](https://store.steampowered.com/app/1255460/Space_Station_14/) | [Standalone Download](https://spacestation14.io/about/nightlies/)
|
||||
|
||||
* Website: [spacestation14.io](https://spacestation14.io/)
|
||||
* Discord: [Invite Link](https://discord.gg/t2jac3p)
|
||||
* IRC: `irc.rizon.net#spacebus`
|
||||
* Code Analysis: [Sonar Cloud](https://sonarcloud.io/dashboard?id=ss14)
|
||||
* Automatic Content Builds: [builds.spacestation14.io](https://builds.spacestation14.io/jenkins/)
|
||||
## Documentation/Wiki
|
||||
|
||||
The IRC is setup to relay back and forth to the Discord server so [IRC nerds](https://xkcd.com/1782/) will not be left out.
|
||||
|
||||
## Documentation
|
||||
|
||||
We have various technical documentation articles on the [HackMD Wiki](https://hackmd.io/@ss14/docs/%2F%40ss14%2Ftechnical-documentation-overview).
|
||||
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.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -27,8 +18,8 @@ We are happy to accept contributions from anybody. Get in Discord or IRC if you
|
||||
|
||||
## Building
|
||||
|
||||
**In practice, you usually don't build this repository directly.**
|
||||
This repository is the **engine** part of SS14. It's the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/space-wizards/space-station-14). Think of Robust Toolbox as BYOND in the context of Spacestation 13.
|
||||
|
||||
## Legal Info
|
||||
|
||||
See `legal.md` for licenses and copyright.
|
||||
See [legal.md](https://github.com/space-wizards/RobustToolbox/blob/master/legal.md) for licenses and copyright.
|
||||
|
||||
1
Resources/Locale/en-US/console.ftl
Normal file
1
Resources/Locale/en-US/console.ftl
Normal file
@@ -0,0 +1 @@
|
||||
console-line-edit-placeholder = Command Here
|
||||
54
Resources/Locale/en-US/input.ftl
Normal file
54
Resources/Locale/en-US/input.ftl
Normal file
@@ -0,0 +1,54 @@
|
||||
input-key-Escape = Escape
|
||||
input-key-Control = Control
|
||||
input-key-Shift = Shift
|
||||
input-key-Alt = Alt
|
||||
input-key-Menu = Menu
|
||||
input-key-F1 = F1
|
||||
input-key-F2 = F2
|
||||
input-key-F3 = F3
|
||||
input-key-F4 = F4
|
||||
input-key-F5 = F5
|
||||
input-key-F6 = F6
|
||||
input-key-F7 = F7
|
||||
input-key-F8 = F8
|
||||
input-key-F9 = F9
|
||||
input-key-F10 = F10
|
||||
input-key-F11 = F11
|
||||
input-key-F12 = F12
|
||||
input-key-F13 = F13
|
||||
input-key-F14 = F14
|
||||
input-key-F15 = F15
|
||||
input-key-Pause = Pause
|
||||
input-key-Left = Left
|
||||
input-key-Up = Up
|
||||
input-key-Down = Down
|
||||
input-key-Right = Right
|
||||
input-key-Space = Space
|
||||
input-key-Return = Return
|
||||
input-key-NumpadEnter = Num Enter
|
||||
input-key-BackSpace = Backspace
|
||||
input-key-Tab = Tab
|
||||
input-key-PageUp = Page Up
|
||||
input-key-PageDown = Page Down
|
||||
input-key-End = End
|
||||
input-key-Home = Home
|
||||
input-key-Insert = Insert
|
||||
input-key-Delete = Delete
|
||||
input-key-MouseLeft = Mouse Left
|
||||
input-key-MouseRight = Mouse Right
|
||||
input-key-MouseMiddle = Mouse Middle
|
||||
input-key-MouseButton4 = Mouse 4
|
||||
input-key-MouseButton5 = Mouse 5
|
||||
input-key-MouseButton6 = Mouse 6
|
||||
input-key-MouseButton7 = Mouse 7
|
||||
input-key-MouseButton8 = Mouse 8
|
||||
input-key-MouseButton9 = Mouse 9
|
||||
|
||||
input-key-LSystem-win = Left Win
|
||||
input-key-RSystem-win = Right Win
|
||||
input-key-LSystem-mac = Left Cmd
|
||||
input-key-RSystem-mac = Right Cmd
|
||||
input-key-LSystem-linux = Left Meta
|
||||
input-key-RSystem-linux = Right Meta
|
||||
|
||||
input-key-unknown = <unknown key>
|
||||
@@ -1,40 +0,0 @@
|
||||
- type: entity
|
||||
id: debugRotation1
|
||||
name: dbg_rotation1
|
||||
components:
|
||||
- type: Clickable
|
||||
- type: InteractionOutline
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
visible: true
|
||||
sprite: debugRotation.rsi
|
||||
state: direction1
|
||||
placement:
|
||||
mode: AlignTileAny
|
||||
|
||||
- type: entity
|
||||
id: debugRotation4
|
||||
name: dbg_rotation4
|
||||
components:
|
||||
- type: Clickable
|
||||
- type: InteractionOutline
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
visible: true
|
||||
sprite: debugRotation.rsi
|
||||
state: direction4
|
||||
placement:
|
||||
mode: AlignTileAny
|
||||
|
||||
- type: entity
|
||||
id: debugRotationTex
|
||||
name: dbg_rotationTex
|
||||
components:
|
||||
- type: Clickable
|
||||
- type: InteractionOutline
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
visible: true
|
||||
texture: debugRotation.rsi/direction1.png
|
||||
placement:
|
||||
mode: AlignTileAny
|
||||
@@ -1,4 +0,0 @@
|
||||
- type: entity
|
||||
name: blank entity
|
||||
id: BlankEntity
|
||||
abstract: true
|
||||
@@ -1,5 +1,11 @@
|
||||
#include "/Shaders/Internal/light_shared.swsl"
|
||||
|
||||
highp vec4 calcGaussianWeights(highp float sigma, highp vec4 offset)
|
||||
{
|
||||
highp vec4 eExp = offset * offset / (2.0 * sigma * sigma);
|
||||
return exp(-eExp) / (sigma * sqrt(2.0 * PI));
|
||||
}
|
||||
|
||||
highp float createOcclusion(highp vec2 diff)
|
||||
{
|
||||
// Calculate vector perpendicular to light vector.
|
||||
@@ -8,23 +14,57 @@ highp float createOcclusion(highp vec2 diff)
|
||||
|
||||
highp float ourDist = length(diff);
|
||||
|
||||
highp vec2 occlDist = occludeDepth(diff, shadowMap, lightIndex);
|
||||
// Sample 7 points on a line perpendicular to the light source.
|
||||
// Depending on the closest point, we change the gaussian weights down below
|
||||
// to change the "narrowness" of the samples.
|
||||
perpendicular *= lightSoftness * 1.5;
|
||||
|
||||
// Get all the samples we need.
|
||||
highp vec2 sample1 = occludeDepth(diff, shadowMap, lightIndex);
|
||||
highp vec2 sample2 = occludeDepth(diff + perpendicular, shadowMap, lightIndex);
|
||||
highp vec2 sample3 = occludeDepth(diff - perpendicular, shadowMap, lightIndex);
|
||||
highp vec2 sample4 = occludeDepth(diff + perpendicular * 2.0, shadowMap, lightIndex);
|
||||
highp vec2 sample5 = occludeDepth(diff - perpendicular * 2.0, shadowMap, lightIndex);
|
||||
highp vec2 sample6 = occludeDepth(diff + perpendicular * 3.0, shadowMap, lightIndex);
|
||||
highp vec2 sample7 = occludeDepth(diff - perpendicular * 3.0, shadowMap, lightIndex);
|
||||
|
||||
highp float mindist =
|
||||
min(sample1.x,
|
||||
min(sample2.x,
|
||||
min(sample3.x,
|
||||
min(sample4.x,
|
||||
min(sample5.x,
|
||||
min(sample6.x,
|
||||
sample7.x))))));
|
||||
|
||||
mindist = max(0.001, mindist);
|
||||
|
||||
// Change soft shadow size based on distance from primary occluder.
|
||||
highp float distRatio = (ourDist - occlDist.x) / occlDist.x / 2.0;
|
||||
highp float distRatio = (ourDist - mindist);
|
||||
|
||||
perpendicular *= distRatio * lightSoftness;
|
||||
// Sigma can never be zero so make sure to clamp.
|
||||
// TODO: Scaling the dist ratio here in a more sane way might make shadows look better buuuut I'm lazy.
|
||||
// Shadows look pretty nice already.
|
||||
highp float sigma = max(0.001, distRatio * 0.75);
|
||||
highp vec4 weights = calcGaussianWeights(sigma, vec4(0.0, 1.0, 2.0, 3.0));
|
||||
|
||||
// Totally not hacky PCF on top of VSM.
|
||||
highp float occlusion = smoothstep(0.1, 1.0, ChebyshevUpperBound(occlDist, ourDist));
|
||||
// Calculation of gaussian weights here is broken because it doesn't add up to 1.
|
||||
// Fixing this is hard and if I had to guess too expensive for GPU shaders.
|
||||
// So instead we add up the total weights and scale the result with that,
|
||||
// so that we still end up with 0-1.
|
||||
highp float totalWeigths = weights.x + weights.y * 2.0 + weights.z * 2.0 + weights.w * 2.0;
|
||||
|
||||
occlusion += shadowContrib(diff + perpendicular);
|
||||
occlusion += shadowContrib(diff - perpendicular);
|
||||
occlusion += shadowContrib(diff + perpendicular * 2.0);
|
||||
occlusion += shadowContrib(diff - perpendicular * 2.0);
|
||||
occlusion += shadowContrib(diff + perpendicular * 3.0);
|
||||
occlusion += shadowContrib(diff - perpendicular * 3.0);
|
||||
highp float occlusion = 0.0;
|
||||
|
||||
return occlusion / 7.0;
|
||||
// Calculate actual occlusion with new weights.
|
||||
occlusion += ChebyshevUpperBound(sample1, ourDist) * weights.x;
|
||||
occlusion += ChebyshevUpperBound(sample2, ourDist) * weights.y;
|
||||
occlusion += ChebyshevUpperBound(sample3, ourDist) * weights.y;
|
||||
occlusion += ChebyshevUpperBound(sample4, ourDist) * weights.z;
|
||||
occlusion += ChebyshevUpperBound(sample5, ourDist) * weights.z;
|
||||
occlusion += ChebyshevUpperBound(sample6, ourDist) * weights.w;
|
||||
occlusion += ChebyshevUpperBound(sample7, ourDist) * weights.w;
|
||||
|
||||
return occlusion / totalWeigths;
|
||||
}
|
||||
|
||||
|
||||
12
Robust.Benchmarks/Program.cs
Normal file
12
Robust.Benchmarks/Program.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace Robust.Benchmarks
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Robust.Benchmarks/Robust.Benchmarks.csproj
Normal file
18
Robust.Benchmarks/Robust.Benchmarks.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>../bin/Benchmarks</OutputPath>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
|
||||
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\MSBuild\Robust.Engine.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Benchmarks.Serialization.Definitions;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Benchmarks.Serialization.Copy
|
||||
{
|
||||
public class SerializationCopyBenchmark : SerializationBenchmark
|
||||
{
|
||||
public SerializationCopyBenchmark()
|
||||
{
|
||||
InitializeSerialization();
|
||||
|
||||
DataDefinitionWithString = new DataDefinitionWithString {StringField = "ABC"};
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(new StringReader(SeedDataDefinition.Prototype));
|
||||
|
||||
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
|
||||
|
||||
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
|
||||
}
|
||||
|
||||
private const string String = "ABC";
|
||||
|
||||
private const int Integer = 1;
|
||||
|
||||
private DataDefinitionWithString DataDefinitionWithString { get; }
|
||||
|
||||
private SeedDataDefinition Seed { get; }
|
||||
|
||||
[Benchmark]
|
||||
public string? CreateCopyString()
|
||||
{
|
||||
return SerializationManager.CreateCopy(String);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int? CreateCopyInteger()
|
||||
{
|
||||
return SerializationManager.CreateCopy(Integer);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DataDefinitionWithString? CreateCopyDataDefinitionWithString()
|
||||
{
|
||||
return SerializationManager.CreateCopy(DataDefinitionWithString);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public SeedDataDefinition? CreateCopySeedDataDefinition()
|
||||
{
|
||||
return SerializationManager.CreateCopy(Seed);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public SeedDataDefinition BaselineCreateCopySeedDataDefinition()
|
||||
{
|
||||
// ReSharper disable once UseObjectOrCollectionInitializer
|
||||
var copy = new SeedDataDefinition();
|
||||
|
||||
copy.ID = Seed.ID;
|
||||
copy.Name = Seed.Name;
|
||||
copy.SeedName = Seed.SeedName;
|
||||
copy.SeedNoun = Seed.SeedNoun;
|
||||
copy.DisplayName = Seed.DisplayName;
|
||||
copy.RoundStart = Seed.RoundStart;
|
||||
copy.Mysterious = Seed.Mysterious;
|
||||
copy.Immutable = Seed.Immutable;
|
||||
|
||||
copy.ProductPrototypes = Seed.ProductPrototypes.ToList();
|
||||
copy.Chemicals = Seed.Chemicals.ToDictionary(p => p.Key, p => p.Value);
|
||||
copy.ConsumeGasses = Seed.ConsumeGasses.ToDictionary(p => p.Key, p => p.Value);
|
||||
copy.ExudeGasses = Seed.ExudeGasses.ToDictionary(p => p.Key, p => p.Value);
|
||||
|
||||
copy.NutrientConsumption = Seed.NutrientConsumption;
|
||||
copy.WaterConsumption = Seed.WaterConsumption;
|
||||
copy.IdealHeat = Seed.IdealHeat;
|
||||
copy.HeatTolerance = Seed.HeatTolerance;
|
||||
copy.IdealLight = Seed.IdealLight;
|
||||
copy.LightTolerance = Seed.LightTolerance;
|
||||
copy.ToxinsTolerance = Seed.ToxinsTolerance;
|
||||
copy.LowPressureTolerance = Seed.LowPressureTolerance;
|
||||
copy.HighPressureTolerance = Seed.HighPressureTolerance;
|
||||
copy.PestTolerance = Seed.PestTolerance;
|
||||
copy.WeedTolerance = Seed.WeedTolerance;
|
||||
|
||||
copy.Endurance = Seed.Endurance;
|
||||
copy.Yield = Seed.Yield;
|
||||
copy.Lifespan = Seed.Lifespan;
|
||||
copy.Maturation = Seed.Maturation;
|
||||
copy.Production = Seed.Production;
|
||||
copy.GrowthStages = Seed.GrowthStages;
|
||||
copy.HarvestRepeat = Seed.HarvestRepeat;
|
||||
copy.Potency = Seed.Potency;
|
||||
copy.Ligneous = Seed.Ligneous;
|
||||
|
||||
copy.PlantRsi = Seed.PlantRsi == null
|
||||
? null!
|
||||
: new ResourcePath(Seed.PlantRsi.ToString(), Seed.PlantRsi.Separator);
|
||||
copy.PlantIconState = Seed.PlantIconState;
|
||||
copy.Bioluminescent = Seed.Bioluminescent;
|
||||
copy.BioluminescentColor = Seed.BioluminescentColor;
|
||||
copy.SplatPrototype = Seed.SplatPrototype;
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Benchmarks.Serialization.Definitions
|
||||
{
|
||||
[DataDefinition]
|
||||
public class DataDefinitionWithString
|
||||
{
|
||||
[field: DataField("string")]
|
||||
public string StringField { get; init; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Benchmarks.Serialization.Definitions
|
||||
{
|
||||
/// <summary>
|
||||
/// Arbitrarily large data definition for benchmarks.
|
||||
/// Taken from content.
|
||||
/// </summary>
|
||||
[Prototype("seed")]
|
||||
public class SeedDataDefinition : IPrototype
|
||||
{
|
||||
public const string Prototype = @"
|
||||
- type: seed
|
||||
id: tobacco
|
||||
name: tobacco
|
||||
seedName: tobacco
|
||||
displayName: tobacco plant
|
||||
productPrototypes:
|
||||
- LeavesTobacco
|
||||
harvestRepeat: Repeat
|
||||
lifespan: 75
|
||||
maturation: 5
|
||||
production: 5
|
||||
yield: 2
|
||||
potency: 20
|
||||
growthStages: 3
|
||||
idealLight: 9
|
||||
idealHeat: 298
|
||||
chemicals:
|
||||
chem.Nicotine:
|
||||
Min: 1
|
||||
Max: 10
|
||||
PotencyDivisor: 10";
|
||||
|
||||
[DataField("id", required: true)] public string ID { get; set; } = default!;
|
||||
|
||||
#region Tracking
|
||||
[DataField("name")] public string Name { get; set; } = string.Empty;
|
||||
[DataField("seedName")] public string SeedName { get; set; } = string.Empty;
|
||||
[DataField("seedNoun")] public string SeedNoun { get; set; } = "seeds";
|
||||
[DataField("displayName")] public string DisplayName { get; set; } = string.Empty;
|
||||
[DataField("roundStart")] public bool RoundStart { get; set; } = true;
|
||||
[DataField("mysterious")] public bool Mysterious { get; set; }
|
||||
[DataField("immutable")] public bool Immutable { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
[DataField("productPrototypes")]
|
||||
public List<string> ProductPrototypes { get; set; } = new();
|
||||
[DataField("chemicals")]
|
||||
public Dictionary<string, SeedChemQuantity> Chemicals { get; set; } = new();
|
||||
[DataField("consumeGasses")]
|
||||
public Dictionary<Gas, float> ConsumeGasses { get; set; } = new();
|
||||
[DataField("exudeGasses")]
|
||||
public Dictionary<Gas, float> ExudeGasses { get; set; } = new();
|
||||
#endregion
|
||||
|
||||
#region Tolerances
|
||||
[DataField("nutrientConsumption")] public float NutrientConsumption { get; set; } = 0.25f;
|
||||
[DataField("waterConsumption")] public float WaterConsumption { get; set; } = 3f;
|
||||
[DataField("idealHeat")] public float IdealHeat { get; set; } = 293f;
|
||||
[DataField("heatTolerance")] public float HeatTolerance { get; set; } = 20f;
|
||||
[DataField("idealLight")] public float IdealLight { get; set; } = 7f;
|
||||
[DataField("lightTolerance")] public float LightTolerance { get; set; } = 5f;
|
||||
[DataField("toxinsTolerance")] public float ToxinsTolerance { get; set; } = 4f;
|
||||
[DataField("lowPressureTolerance")] public float LowPressureTolerance { get; set; } = 25f;
|
||||
[DataField("highPressureTolerance")] public float HighPressureTolerance { get; set; } = 200f;
|
||||
[DataField("pestTolerance")] public float PestTolerance { get; set; } = 5f;
|
||||
[DataField("weedTolerance")] public float WeedTolerance { get; set; } = 5f;
|
||||
#endregion
|
||||
|
||||
#region General traits
|
||||
[DataField("endurance")] public float Endurance { get; set; } = 100f;
|
||||
[DataField("yield")] public int Yield { get; set; }
|
||||
[DataField("lifespan")] public float Lifespan { get; set; }
|
||||
[DataField("maturation")] public float Maturation { get; set; }
|
||||
[DataField("production")] public float Production { get; set; }
|
||||
[DataField("growthStages")] public int GrowthStages { get; set; } = 6;
|
||||
[DataField("harvestRepeat")] public HarvestType HarvestRepeat { get; set; } = HarvestType.NoRepeat;
|
||||
[DataField("potency")] public float Potency { get; set; } = 1f;
|
||||
[DataField("ligneous")] public bool Ligneous { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Cosmetics
|
||||
[DataField("plantRsi", required: true)] public ResourcePath PlantRsi { get; set; } = default!;
|
||||
[DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
|
||||
[DataField("bioluminescent")] public bool Bioluminescent { get; set; }
|
||||
[DataField("bioluminescentColor")] public Color BioluminescentColor { get; set; } = Color.White;
|
||||
[DataField("splatPrototype")] public string? SplatPrototype { get; set; }
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum HarvestType
|
||||
{
|
||||
NoRepeat,
|
||||
Repeat
|
||||
}
|
||||
|
||||
public enum Gas
|
||||
{
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
public struct SeedChemQuantity
|
||||
{
|
||||
[DataField("Min")]
|
||||
public int Min;
|
||||
|
||||
[DataField("Max")]
|
||||
public int Max;
|
||||
|
||||
[DataField("PotencyDivisor")]
|
||||
public int PotencyDivisor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
|
||||
namespace Robust.Benchmarks.Serialization.Initialize
|
||||
{
|
||||
public class SerializationInitializeBenchmark : SerializationBenchmark
|
||||
{
|
||||
[IterationCleanup]
|
||||
public void IterationCleanup()
|
||||
{
|
||||
SerializationManager.Shutdown();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ISerializationManager Initialize()
|
||||
{
|
||||
InitializeSerialization();
|
||||
return SerializationManager;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.IO;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Benchmarks.Serialization.Definitions;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Benchmarks.Serialization.Read
|
||||
{
|
||||
public class SerializationReadBenchmark : SerializationBenchmark
|
||||
{
|
||||
public SerializationReadBenchmark()
|
||||
{
|
||||
InitializeSerialization();
|
||||
|
||||
StringDataDefNode = new MappingDataNode();
|
||||
StringDataDefNode.Add(new ValueDataNode("string"), new ValueDataNode("ABC"));
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(new StringReader(SeedDataDefinition.Prototype));
|
||||
|
||||
SeedNode = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
|
||||
}
|
||||
|
||||
private ValueDataNode StringNode { get; } = new("ABC");
|
||||
|
||||
private ValueDataNode IntNode { get; } = new("1");
|
||||
|
||||
private MappingDataNode StringDataDefNode { get; }
|
||||
|
||||
private MappingDataNode SeedNode { get; }
|
||||
|
||||
[Benchmark]
|
||||
public string? ReadString()
|
||||
{
|
||||
return SerializationManager.ReadValue<string>(StringNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int? ReadInteger()
|
||||
{
|
||||
return SerializationManager.ReadValue<int>(IntNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DataDefinitionWithString? ReadDataDefinitionWithString()
|
||||
{
|
||||
return SerializationManager.ReadValue<DataDefinitionWithString>(StringDataDefNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public SeedDataDefinition? ReadSeedDataDefinition()
|
||||
{
|
||||
return SerializationManager.ReadValue<SeedDataDefinition>(SeedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Robust.Benchmarks/Serialization/SerializationBenchmark.cs
Normal file
43
Robust.Benchmarks/Serialization/SerializationBenchmark.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Robust.Server;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
|
||||
namespace Robust.Benchmarks.Serialization
|
||||
{
|
||||
public abstract class SerializationBenchmark
|
||||
{
|
||||
public SerializationBenchmark()
|
||||
{
|
||||
IoCManager.InitThread();
|
||||
ServerIoC.RegisterIoC();
|
||||
IoCManager.BuildGraph();
|
||||
|
||||
var assemblies = new[]
|
||||
{
|
||||
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Shared"),
|
||||
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Server"),
|
||||
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Benchmarks")
|
||||
};
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
IoCManager.Resolve<IConfigurationManagerInternal>().LoadCVarsFromAssembly(assembly);
|
||||
}
|
||||
|
||||
IoCManager.Resolve<IReflectionManager>().LoadAssemblies(assemblies);
|
||||
|
||||
SerializationManager = IoCManager.Resolve<ISerializationManager>();
|
||||
}
|
||||
|
||||
protected ISerializationManager SerializationManager { get; }
|
||||
|
||||
public void InitializeSerialization()
|
||||
{
|
||||
SerializationManager.Initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Benchmarks.Serialization.Definitions;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Benchmarks.Serialization.Write
|
||||
{
|
||||
public class SerializationWriteBenchmark : SerializationBenchmark
|
||||
{
|
||||
public SerializationWriteBenchmark()
|
||||
{
|
||||
InitializeSerialization();
|
||||
|
||||
DataDefinitionWithString = new DataDefinitionWithString {StringField = "ABC"};
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(new StringReader(SeedDataDefinition.Prototype));
|
||||
|
||||
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
|
||||
|
||||
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
|
||||
}
|
||||
|
||||
private const string String = "ABC";
|
||||
|
||||
private const int Integer = 1;
|
||||
|
||||
private DataDefinitionWithString DataDefinitionWithString { get; }
|
||||
|
||||
private SeedDataDefinition Seed { get; }
|
||||
|
||||
[Benchmark]
|
||||
public DataNode WriteString()
|
||||
{
|
||||
return SerializationManager.WriteValue(String);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DataNode WriteInteger()
|
||||
{
|
||||
return SerializationManager.WriteValue(Integer);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DataNode WriteDataDefinitionWithString()
|
||||
{
|
||||
return SerializationManager.WriteValue(DataDefinitionWithString);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DataNode WriteSeedDataDefinition()
|
||||
{
|
||||
return SerializationManager.WriteValue(Seed);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DataNode BaselineWriteSeedDataDefinition()
|
||||
{
|
||||
var mapping = new MappingDataNode();
|
||||
|
||||
mapping.Add("id", Seed.ID);
|
||||
mapping.Add("name", Seed.Name);
|
||||
mapping.Add("seedName", Seed.SeedName);
|
||||
mapping.Add("displayName", Seed.DisplayName);
|
||||
mapping.Add("productPrototypes", Seed.ProductPrototypes);
|
||||
mapping.Add("harvestRepeat", Seed.HarvestRepeat.ToString());
|
||||
mapping.Add("lifespan", Seed.Lifespan.ToString(CultureInfo.InvariantCulture));
|
||||
mapping.Add("maturation", Seed.Maturation.ToString(CultureInfo.InvariantCulture));
|
||||
mapping.Add("production", Seed.Production.ToString(CultureInfo.InvariantCulture));
|
||||
mapping.Add("yield", Seed.Yield.ToString(CultureInfo.InvariantCulture));
|
||||
mapping.Add("potency", Seed.Potency.ToString(CultureInfo.InvariantCulture));
|
||||
mapping.Add("growthStages", Seed.GrowthStages.ToString(CultureInfo.InvariantCulture));
|
||||
mapping.Add("idealLight", Seed.IdealLight.ToString(CultureInfo.InvariantCulture));
|
||||
mapping.Add("idealHeat", Seed.IdealHeat.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
var chemicals = new MappingDataNode();
|
||||
foreach (var (name, quantity) in Seed.Chemicals)
|
||||
{
|
||||
chemicals.Add(name, new MappingDataNode
|
||||
{
|
||||
["Min"] = new ValueDataNode(quantity.Min.ToString(CultureInfo.InvariantCulture)),
|
||||
["Max"] = new ValueDataNode(quantity.Max.ToString(CultureInfo.InvariantCulture)),
|
||||
["PotencyDivisor"] = new ValueDataNode(quantity.PotencyDivisor.ToString(CultureInfo.InvariantCulture))
|
||||
});
|
||||
}
|
||||
|
||||
mapping.Add("chemicals", chemicals);
|
||||
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,6 @@ namespace {nameSpace}
|
||||
DiagnosticSeverity.Error,
|
||||
true),
|
||||
Location.None));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Broadphase;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Logger = Robust.Shared.Log.Logger;
|
||||
|
||||
namespace Robust.Client.Audio.Midi
|
||||
@@ -32,24 +34,6 @@ namespace Robust.Client.Audio.Midi
|
||||
/// </returns>
|
||||
IMidiRenderer? GetNewRenderer();
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Checks whether the file at the given path is a valid midi file or not.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We add this here so content doesn't need to reference NFluidsynth.
|
||||
/// </remarks>
|
||||
bool IsMidiFile(string filename);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the file at the given path is a valid midi file or not.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We add this here so content doesn't need to reference NFluidsynth.
|
||||
/// </remarks>
|
||||
bool IsSoundfontFile(string filename);
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Method called every frame.
|
||||
/// Should be used to update positional audio.
|
||||
@@ -57,6 +41,11 @@ namespace Robust.Client.Audio.Midi
|
||||
/// <param name="frameTime"></param>
|
||||
void FrameUpdate(float frameTime);
|
||||
|
||||
/// <summary>
|
||||
/// Volume, in db.
|
||||
/// </summary>
|
||||
float Volume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, MIDI support is available.
|
||||
/// </summary>
|
||||
@@ -75,6 +64,7 @@ namespace Robust.Client.Audio.Midi
|
||||
|
||||
private SharedBroadPhaseSystem _broadPhaseSystem = default!;
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsAvailable
|
||||
{
|
||||
get
|
||||
@@ -85,12 +75,29 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<MidiRenderer> _renderers = new();
|
||||
[ViewVariables]
|
||||
private readonly List<IMidiRenderer> _renderers = new();
|
||||
|
||||
private bool _alive = true;
|
||||
private Settings? _settings;
|
||||
private Thread? _midiThread;
|
||||
private ISawmill _midiSawmill = default!;
|
||||
private float _volume = 0f;
|
||||
private bool _volumeDirty = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
if (MathHelper.CloseTo(_volume, value))
|
||||
return;
|
||||
|
||||
_volume = value;
|
||||
_volumeDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string[] LinuxSoundfonts =
|
||||
{
|
||||
@@ -117,6 +124,7 @@ namespace Robust.Client.Audio.Midi
|
||||
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
private void InitializeFluidsynth()
|
||||
@@ -175,18 +183,6 @@ namespace Robust.Client.Audio.Midi
|
||||
_sawmill.Log(rLevel, message);
|
||||
}
|
||||
|
||||
/*
|
||||
public bool IsMidiFile(string filename)
|
||||
{
|
||||
return SoundFont.IsMidiFile(filename);
|
||||
}
|
||||
|
||||
public bool IsSoundfontFile(string filename)
|
||||
{
|
||||
return SoundFont.IsSoundFont(filename);
|
||||
}
|
||||
*/
|
||||
|
||||
public IMidiRenderer? GetNewRenderer()
|
||||
{
|
||||
if (!FluidsynthInitialized)
|
||||
@@ -250,7 +246,9 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
_renderers.Add(renderer);
|
||||
}
|
||||
|
||||
return renderer;
|
||||
}
|
||||
@@ -268,74 +266,79 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
|
||||
// Update positions of streams every frame.
|
||||
lock (_renderers)
|
||||
foreach (var renderer in _renderers)
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
continue;
|
||||
|
||||
if(_volumeDirty)
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
if (!renderer.Mono)
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
continue;
|
||||
|
||||
if (!renderer.Mono)
|
||||
{
|
||||
renderer.Source.SetGlobal();
|
||||
continue;
|
||||
}
|
||||
|
||||
MapCoordinates? mapPos = null;
|
||||
if (renderer.TrackingCoordinates != null)
|
||||
{
|
||||
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
else if (renderer.TrackingEntity != null)
|
||||
{
|
||||
mapPos = renderer.TrackingEntity.Transform.MapPosition;
|
||||
}
|
||||
|
||||
if (mapPos != null)
|
||||
{
|
||||
var pos = mapPos.Value;
|
||||
if (pos.MapId != _eyeManager.CurrentMap)
|
||||
{
|
||||
renderer.Source.SetVolume(-10000000);
|
||||
}
|
||||
else
|
||||
{
|
||||
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
pos.Position,
|
||||
sourceRelative.Normalized,
|
||||
OcclusionCollisionMask),
|
||||
sourceRelative.Length,
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
}
|
||||
|
||||
if (renderer.Source.SetPosition(pos.Position))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (renderer.TrackingEntity != null)
|
||||
{
|
||||
renderer.Source.SetVelocity(renderer.TrackingEntity.GlobalLinearVelocity());
|
||||
}
|
||||
|
||||
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
|
||||
{
|
||||
// just duck out instead of move to NaN
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
_midiSawmill?.Warning("Interrupting positional audio, can't set position.");
|
||||
renderer.Source.StopPlaying();
|
||||
}
|
||||
renderer.Source.SetGlobal();
|
||||
continue;
|
||||
}
|
||||
|
||||
MapCoordinates? mapPos = null;
|
||||
if (renderer.TrackingCoordinates != null)
|
||||
{
|
||||
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
else if (renderer.TrackingEntity != null)
|
||||
{
|
||||
mapPos = renderer.TrackingEntity.Transform.MapPosition;
|
||||
}
|
||||
|
||||
if (mapPos != null)
|
||||
{
|
||||
var pos = mapPos.Value;
|
||||
if (pos.MapId != _eyeManager.CurrentMap)
|
||||
{
|
||||
renderer.Source.SetVolume(-10000000);
|
||||
}
|
||||
else
|
||||
{
|
||||
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
pos.Position,
|
||||
sourceRelative.Normalized,
|
||||
OcclusionCollisionMask),
|
||||
sourceRelative.Length,
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
}
|
||||
|
||||
if (renderer.Source.SetPosition(pos.Position))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (renderer.TrackingEntity != null)
|
||||
{
|
||||
renderer.Source.SetVelocity(renderer.TrackingEntity.GlobalLinearVelocity());
|
||||
}
|
||||
|
||||
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
|
||||
{
|
||||
// just duck out instead of move to NaN
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
_midiSawmill?.Warning("Interrupting positional audio, can't set position.");
|
||||
renderer.Source.StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
_volumeDirty = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -346,6 +349,7 @@ namespace Robust.Client.Audio.Midi
|
||||
while (_alive)
|
||||
{
|
||||
lock (_renderers)
|
||||
{
|
||||
for (var i = 0; i < _renderers.Count; i++)
|
||||
{
|
||||
var renderer = _renderers[i];
|
||||
@@ -353,10 +357,11 @@ namespace Robust.Client.Audio.Midi
|
||||
renderer.Render();
|
||||
else
|
||||
{
|
||||
((IMidiRenderer)renderer).InternalDispose();
|
||||
renderer.InternalDispose();
|
||||
_renderers.Remove(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
@@ -367,9 +372,13 @@ namespace Robust.Client.Audio.Midi
|
||||
_alive = false;
|
||||
_midiThread?.Join();
|
||||
_settings?.Dispose();
|
||||
foreach (var renderer in _renderers)
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
renderer?.Dispose();
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
renderer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (FluidsynthInitialized && !_failedInitialize)
|
||||
@@ -424,6 +433,7 @@ namespace Robust.Client.Audio.Midi
|
||||
var span = new Span<byte>(buf.ToPointer(), length);
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
|
||||
try
|
||||
{
|
||||
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
|
||||
@@ -447,6 +457,7 @@ namespace Robust.Client.Audio.Midi
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -468,10 +479,12 @@ namespace Robust.Client.Audio.Midi
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
|
||||
stream.Dispose();
|
||||
_openStreams.Remove((int) sfHandle);
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using MidiEvent = NFluidsynth.MidiEvent;
|
||||
|
||||
namespace Robust.Client.Audio.Midi
|
||||
@@ -21,6 +22,17 @@ namespace Robust.Client.Audio.Midi
|
||||
|
||||
public interface IMidiRenderer : IDisposable
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The buffered audio source of this renderer.
|
||||
/// </summary>
|
||||
internal IClydeBufferedAudioSource Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this renderer has been disposed or not.
|
||||
/// </summary>
|
||||
bool Disposed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This controls whether the midi file being played will loop or not.
|
||||
/// </summary>
|
||||
@@ -110,6 +122,11 @@ namespace Robust.Client.Audio.Midi
|
||||
/// </summary>
|
||||
void StopAllNotes();
|
||||
|
||||
/// <summary>
|
||||
/// Render and play MIDI to the audio source.
|
||||
/// </summary>
|
||||
internal void Render();
|
||||
|
||||
/// <summary>
|
||||
/// Loads a new soundfont into the renderer.
|
||||
/// </summary>
|
||||
@@ -159,7 +176,7 @@ namespace Robust.Client.Audio.Midi
|
||||
internal void InternalDispose();
|
||||
}
|
||||
|
||||
public class MidiRenderer : IMidiRenderer
|
||||
internal class MidiRenderer : IMidiRenderer
|
||||
{
|
||||
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
@@ -188,8 +205,12 @@ namespace Robust.Client.Audio.Midi
|
||||
private readonly object _playerStateLock = new();
|
||||
private SequencerClientId _synthRegister;
|
||||
public IClydeBufferedAudioSource Source { get; set; }
|
||||
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
|
||||
|
||||
[ViewVariables]
|
||||
public bool Disposed { get; private set; } = false;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public byte MidiProgram
|
||||
{
|
||||
get => _midiProgram;
|
||||
@@ -203,6 +224,7 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public byte MidiBank
|
||||
{
|
||||
get => _midiBank;
|
||||
@@ -216,6 +238,7 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public uint MidiSoundfont
|
||||
{
|
||||
get => _midiSoundfont;
|
||||
@@ -229,10 +252,16 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool DisablePercussionChannel { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool DisableProgramChangeEvent { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PlayerTick
|
||||
{
|
||||
get => _player?.CurrentTick ?? 0;
|
||||
@@ -243,12 +272,19 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public uint SequencerTick => _sequencer?.Tick ?? 0;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public double SequencerTimeScale => _sequencer?.TimeScale ?? 0;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Mono { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public MidiRendererStatus Status { get; private set; } = MidiRendererStatus.None;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool LoopMidi
|
||||
{
|
||||
get => _loopMidi;
|
||||
@@ -260,10 +296,11 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public IEntity? TrackingEntity { get; set; } = null;
|
||||
public EntityCoordinates? TrackingCoordinates { get; set; } = null;
|
||||
|
||||
internal bool Free { get; set; } = false;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public EntityCoordinates? TrackingCoordinates { get; set; } = null;
|
||||
|
||||
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono = true)
|
||||
{
|
||||
@@ -294,7 +331,11 @@ namespace Robust.Client.Audio.Midi
|
||||
Status = MidiRendererStatus.Input;
|
||||
StopAllNotes();
|
||||
|
||||
_driver = new MidiDriver(_settings, MidiDriverEventHandler);
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
_driver = new MidiDriver(_settings, MidiDriverEventHandler);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -332,8 +373,13 @@ namespace Robust.Client.Audio.Midi
|
||||
{
|
||||
if (Status != MidiRendererStatus.Input) return false;
|
||||
Status = MidiRendererStatus.None;
|
||||
_driver?.Dispose();
|
||||
_driver = null;
|
||||
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
_driver?.Dispose();
|
||||
_driver = null;
|
||||
}
|
||||
|
||||
StopAllNotes();
|
||||
return true;
|
||||
}
|
||||
@@ -357,7 +403,8 @@ namespace Robust.Client.Audio.Midi
|
||||
|
||||
public void StopAllNotes()
|
||||
{
|
||||
_synth.AllNotesOff(-1);
|
||||
lock(_playerStateLock)
|
||||
_synth.AllNotesOff(-1);
|
||||
}
|
||||
|
||||
public void LoadSoundfont(string filename, bool resetPresets = false)
|
||||
@@ -372,7 +419,12 @@ namespace Robust.Client.Audio.Midi
|
||||
public event Action<Shared.Audio.Midi.MidiEvent>? OnMidiEvent;
|
||||
public event Action? OnMidiPlayerFinished;
|
||||
|
||||
internal void Render(int length = SampleRate / 250)
|
||||
void IMidiRenderer.Render()
|
||||
{
|
||||
Render();
|
||||
}
|
||||
|
||||
private void Render(int length = SampleRate / 250)
|
||||
{
|
||||
if (Disposed) return;
|
||||
|
||||
@@ -452,6 +504,7 @@ namespace Robust.Client.Audio.Midi
|
||||
var timestamp = SequencerTick;
|
||||
var midiEv = (Shared.Audio.Midi.MidiEvent) midiEvent;
|
||||
midiEv.Tick = timestamp;
|
||||
midiEvent.Dispose();
|
||||
SendMidiEvent(midiEv);
|
||||
return 0;
|
||||
}
|
||||
@@ -462,6 +515,7 @@ namespace Robust.Client.Audio.Midi
|
||||
var timestamp = SequencerTick;
|
||||
var midiEv = (Shared.Audio.Midi.MidiEvent) midiEvent;
|
||||
midiEv.Tick = timestamp;
|
||||
midiEvent.Dispose();
|
||||
SendMidiEvent(midiEv);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -24,6 +26,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IPlayerManager _playMan = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityLookup _entityLookup = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IDiscordRichPresence _discord = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
@@ -96,6 +99,25 @@ namespace Robust.Client
|
||||
_net.ClientDisconnect(reason);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StartSinglePlayer()
|
||||
{
|
||||
DebugTools.Assert(RunLevel < ClientRunLevel.Connecting);
|
||||
DebugTools.Assert(!_net.IsConnected);
|
||||
_playMan.Startup();
|
||||
_playMan.LocalPlayer!.Name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
|
||||
OnRunLevelChanged(ClientRunLevel.SinglePlayerGame);
|
||||
GameStartedSetup();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopSinglePlayer()
|
||||
{
|
||||
DebugTools.Assert(RunLevel == ClientRunLevel.SinglePlayerGame);
|
||||
DebugTools.Assert(!_net.IsConnected);
|
||||
GameStoppedReset();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<RunLevelChangedEventArgs>? RunLevelChanged;
|
||||
|
||||
@@ -132,7 +154,7 @@ namespace Robust.Client
|
||||
var userId = _net.ServerChannel.UserId;
|
||||
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString());
|
||||
// start up player management
|
||||
_playMan.Startup(_net.ServerChannel!);
|
||||
_playMan.Startup();
|
||||
|
||||
_playMan.LocalPlayer!.UserId = userId;
|
||||
_playMan.LocalPlayer.Name = userName;
|
||||
@@ -145,16 +167,13 @@ namespace Robust.Client
|
||||
/// receiving states when they join the lobby.
|
||||
/// </summary>
|
||||
/// <param name="session">Session of the player.</param>
|
||||
private void OnPlayerJoinedServer(IPlayerSession session)
|
||||
private void OnPlayerJoinedServer(ICommonSession session)
|
||||
{
|
||||
DebugTools.Assert(RunLevel < ClientRunLevel.Connected);
|
||||
OnRunLevelChanged(ClientRunLevel.Connected);
|
||||
|
||||
_entityManager.Startup();
|
||||
_mapManager.Startup();
|
||||
GameStartedSetup();
|
||||
|
||||
_timing.ResetSimTime();
|
||||
_timing.Paused = false;
|
||||
PlayerJoinedServer?.Invoke(this, new PlayerEventArgs(session));
|
||||
}
|
||||
|
||||
@@ -162,7 +181,7 @@ namespace Robust.Client
|
||||
/// Player is joining the game
|
||||
/// </summary>
|
||||
/// <param name="session">Session of the player.</param>
|
||||
private void OnPlayerJoinedGame(IPlayerSession session)
|
||||
private void OnPlayerJoinedGame(ICommonSession session)
|
||||
{
|
||||
DebugTools.Assert(RunLevel >= ClientRunLevel.Connected);
|
||||
OnRunLevelChanged(ClientRunLevel.InGame);
|
||||
@@ -189,10 +208,25 @@ namespace Robust.Client
|
||||
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
|
||||
|
||||
LastDisconnectReason = args.Reason;
|
||||
GameStoppedReset();
|
||||
}
|
||||
|
||||
private void GameStartedSetup()
|
||||
{
|
||||
_entityManager.Startup();
|
||||
_mapManager.Startup();
|
||||
_entityLookup.Startup();
|
||||
|
||||
_timing.ResetSimTime();
|
||||
_timing.Paused = false;
|
||||
}
|
||||
|
||||
private void GameStoppedReset()
|
||||
{
|
||||
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
|
||||
_gameStates.Reset();
|
||||
_playMan.Shutdown();
|
||||
_entityLookup.Shutdown();
|
||||
_entityManager.Shutdown();
|
||||
_mapManager.Shutdown();
|
||||
_discord.ClearPresence();
|
||||
@@ -249,6 +283,11 @@ namespace Robust.Client
|
||||
/// The client is now in the game, moving around.
|
||||
/// </summary>
|
||||
InGame,
|
||||
|
||||
/// <summary>
|
||||
/// The client is now in singleplayer mode, in-game.
|
||||
/// </summary>
|
||||
SinglePlayerGame,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -259,12 +298,12 @@ namespace Robust.Client
|
||||
/// <summary>
|
||||
/// The session that triggered the event.
|
||||
/// </summary>
|
||||
private IPlayerSession? Session { get; }
|
||||
private ICommonSession? Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the class.
|
||||
/// </summary>
|
||||
public PlayerEventArgs(IPlayerSession? session)
|
||||
public PlayerEventArgs(ICommonSession? session)
|
||||
{
|
||||
Session = session;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using Robust.Client.Prototypes;
|
||||
using Robust.Client.Reflection;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
@@ -27,6 +28,7 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client
|
||||
{
|
||||
@@ -36,11 +38,18 @@ namespace Robust.Client
|
||||
{
|
||||
SharedIoC.RegisterIoC();
|
||||
|
||||
IoCManager.Register<IGameTiming, ClientGameTiming>();
|
||||
IoCManager.Register<IClientGameTiming, ClientGameTiming>();
|
||||
IoCManager.Register<IPrototypeManager, ClientPrototypeManager>();
|
||||
IoCManager.Register<IMapManager, ClientMapManager>();
|
||||
IoCManager.Register<IMapManagerInternal, ClientMapManager>();
|
||||
IoCManager.Register<IClientMapManager, ClientMapManager>();
|
||||
IoCManager.Register<IEntityManager, ClientEntityManager>();
|
||||
IoCManager.Register<IEntityLookup, EntityLookup>();
|
||||
IoCManager.Register<IComponentFactory, ClientComponentFactory>();
|
||||
IoCManager.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
|
||||
IoCManager.Register<IClydeTileDefinitionManager, ClydeTileDefinitionManager>();
|
||||
IoCManager.Register<GameController, GameController>();
|
||||
IoCManager.Register<IGameController, GameController>();
|
||||
IoCManager.Register<IGameControllerInternal, GameController>();
|
||||
IoCManager.Register<IReflectionManager, ClientReflectionManager>();
|
||||
@@ -50,7 +59,8 @@ namespace Robust.Client
|
||||
IoCManager.Register<IResourceCacheInternal, ResourceCache>();
|
||||
IoCManager.Register<IClientNetManager, NetManager>();
|
||||
IoCManager.Register<IClientEntityManager, ClientEntityManager>();
|
||||
IoCManager.Register<IEntityNetworkManager, ClientEntityNetworkManager>();
|
||||
IoCManager.Register<IClientEntityManagerInternal, ClientEntityManager>();
|
||||
IoCManager.Register<IEntityNetworkManager, ClientEntityManager>();
|
||||
IoCManager.Register<IClientGameStateManager, ClientGameStateManager>();
|
||||
IoCManager.Register<IBaseClient, BaseClient>();
|
||||
IoCManager.Register<IPlayerManager, PlayerManager>();
|
||||
|
||||
@@ -280,12 +280,12 @@ namespace Robust.Client.Console.Commands
|
||||
internal class SnapGridGetCell : IConsoleCommand
|
||||
{
|
||||
public string Command => "sggcell";
|
||||
public string Help => "sggcell <gridID> <vector2i> [offset]\nThat vector2i param is in the form x<int>,y<int>.";
|
||||
public string Help => "sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.";
|
||||
public string Description => "Lists entities on a snap grid cell.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2 && args.Length != 3)
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
@@ -293,7 +293,6 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
string gridId = args[0];
|
||||
string indices = args[1];
|
||||
string offset = args.Length == 3 ? args[2] : "Center";
|
||||
|
||||
if (!int.TryParse(args[0], out var id))
|
||||
{
|
||||
@@ -307,29 +306,17 @@ namespace Robust.Client.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
SnapGridOffset selectedOffset;
|
||||
if (Enum.IsDefined(typeof(SnapGridOffset), offset))
|
||||
{
|
||||
selectedOffset = (SnapGridOffset)Enum.Parse(typeof(SnapGridOffset), offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteError("given offset type is not defined");
|
||||
return;
|
||||
}
|
||||
|
||||
var mapMan = IoCManager.Resolve<IMapManager>();
|
||||
|
||||
if (mapMan.GridExists(new GridId(int.Parse(gridId, CultureInfo.InvariantCulture))))
|
||||
{
|
||||
foreach (var entity in
|
||||
mapMan.GetGrid(new GridId(int.Parse(gridId, CultureInfo.InvariantCulture))).GetSnapGridCell(
|
||||
mapMan.GetGrid(new GridId(int.Parse(gridId, CultureInfo.InvariantCulture))).GetAnchoredEntities(
|
||||
new Vector2i(
|
||||
int.Parse(indices.Split(',')[0], CultureInfo.InvariantCulture),
|
||||
int.Parse(indices.Split(',')[1], CultureInfo.InvariantCulture)),
|
||||
selectedOffset))
|
||||
int.Parse(indices.Split(',')[1], CultureInfo.InvariantCulture))))
|
||||
{
|
||||
shell.WriteLine(entity.Owner.Uid.ToString());
|
||||
shell.WriteLine(entity.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -678,10 +665,10 @@ namespace Robust.Client.Console.Commands
|
||||
public string Description => "Gets the system clipboard";
|
||||
public string Help => "getclipboard";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var mgr = IoCManager.Resolve<IClipboardManager>();
|
||||
shell.WriteLine(mgr.GetText());
|
||||
shell.WriteLine(await mgr.GetText());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1097,15 +1084,8 @@ namespace Robust.Client.Console.Commands
|
||||
var key = (Keyboard.Key) parsed!;
|
||||
|
||||
var name = clyde.GetKeyName(key);
|
||||
var scanCode = clyde.GetKeyScanCode(key);
|
||||
var nameScanCode = clyde.GetKeyNameScanCode(scanCode);
|
||||
|
||||
shell.WriteLine($"name: '{name}' scan code: '{scanCode}' name via scan code: '{nameScanCode}'");
|
||||
}
|
||||
else if (int.TryParse(args[0], out var scanCode))
|
||||
{
|
||||
var nameScanCode = clyde.GetKeyNameScanCode(scanCode);
|
||||
shell.WriteLine($"name via scan code: '{nameScanCode}'");
|
||||
shell.WriteLine($"name: '{name}' ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,34 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class MonitorInfoCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "monitorinfo";
|
||||
public string Description => "";
|
||||
public string Help => "Usage: monitorinfo <id>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 1)
|
||||
{
|
||||
shell.WriteError("Expected one argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
var monitor = clyde.EnumerateMonitors().Single(c => c.Id == int.Parse(args[0]));
|
||||
|
||||
shell.WriteLine($"{monitor.Id}: {monitor.Name}");
|
||||
shell.WriteLine($"Video modes:");
|
||||
|
||||
foreach (var mode in monitor.VideoModes)
|
||||
{
|
||||
shell.WriteLine($" {mode.Width}x{mode.Height} {mode.RefreshRate} Hz {mode.RedBits}/{mode.GreenBits}/{mode.BlueBits}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class SetMonitorCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
class QuitCommand : IConsoleCommand
|
||||
class HardQuitCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "quit";
|
||||
public string Command => "hardquit";
|
||||
public string Description => "Kills the game client instantly.";
|
||||
public string Help => "Kills the game client instantly, leaving no traces. No telling the server goodbye";
|
||||
|
||||
@@ -14,4 +15,16 @@ namespace Robust.Client.Console.Commands
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
class QuitCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "quit";
|
||||
public string Description => "Shuts down the game client gracefully.";
|
||||
public string Help => "Properly shuts down the game client, notifying the connected server and such.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
IoCManager.Resolve<IGameController>().Shutdown("quit command used");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ namespace Robust.Client.Console
|
||||
|
||||
var console = new ScriptConsoleServer(this, session);
|
||||
_activeConsoles.Add(session, console);
|
||||
console.Open();
|
||||
// FIXME: When this is Open(), resizing the window will cause its position to get NaN'd.
|
||||
console.OpenCentered();
|
||||
}
|
||||
|
||||
private void ReceiveScriptResponse(MsgScriptResponse message)
|
||||
|
||||
@@ -5,10 +5,15 @@ namespace Robust.Client
|
||||
public static void Start(string[] args)
|
||||
{
|
||||
#if FULL_RELEASE
|
||||
throw new System.InvalidOperationException("ContentStart is not available on a full release.");
|
||||
throw new System.InvalidOperationException("ContentStart.Start is not available on a full release.");
|
||||
#else
|
||||
GameController.Start(args, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void StartLibrary(string[] args, GameControllerOptions options)
|
||||
{
|
||||
GameController.Start(args, true, null, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Broadphase;
|
||||
using Robust.Shared.Physics.Dynamics.Joints;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.Debugging
|
||||
@@ -19,11 +21,10 @@ namespace Robust.Client.Debugging
|
||||
public class DebugDrawing : IDebugDrawing
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IComponentManager _componentManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPhysicsManager _physicsManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
|
||||
private bool _debugColliders;
|
||||
@@ -44,8 +45,8 @@ namespace Robust.Client.Debugging
|
||||
|
||||
if (value && !_overlayManager.HasOverlay<PhysicsOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new PhysicsOverlay(_componentManager, _eyeManager,
|
||||
_prototypeManager, _inputManager, _physicsManager));
|
||||
_overlayManager.AddOverlay(new PhysicsOverlay(_eyeManager,
|
||||
_prototypeManager, _inputManager, _mapManager));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -80,10 +81,9 @@ namespace Robust.Client.Debugging
|
||||
|
||||
private class PhysicsOverlay : Overlay
|
||||
{
|
||||
private readonly IComponentManager _componentManager;
|
||||
private readonly IEyeManager _eyeManager;
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly IInputManager _inputManager;
|
||||
private readonly IPhysicsManager _physicsManager;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
|
||||
private readonly ShaderInstance _shader;
|
||||
@@ -93,12 +93,11 @@ namespace Robust.Client.Debugging
|
||||
private List<IPhysBody> _hoverBodies = new();
|
||||
|
||||
|
||||
public PhysicsOverlay(IComponentManager compMan, IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IPhysicsManager physicsManager)
|
||||
public PhysicsOverlay(IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IMapManager mapManager)
|
||||
{
|
||||
_componentManager = compMan;
|
||||
_eyeManager = eyeMan;
|
||||
_inputManager = inputManager;
|
||||
_physicsManager = physicsManager;
|
||||
_mapManager = mapManager;
|
||||
|
||||
_shader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
|
||||
var cache = IoCManager.Resolve<IResourceCache>();
|
||||
@@ -106,22 +105,23 @@ namespace Robust.Client.Debugging
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
switch (currentSpace)
|
||||
switch (args.Space)
|
||||
{
|
||||
case OverlaySpace.ScreenSpace:
|
||||
DrawScreen((DrawingHandleScreen) handle);
|
||||
DrawScreen(args);
|
||||
break;
|
||||
case OverlaySpace.WorldSpace:
|
||||
DrawWorld((DrawingHandleWorld) handle);
|
||||
DrawWorld(args);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void DrawScreen(DrawingHandleScreen screenHandle)
|
||||
private void DrawScreen(in OverlayDrawArgs args)
|
||||
{
|
||||
var screenHandle = args.ScreenHandle;
|
||||
var lineHeight = _font.GetLineHeight(1f);
|
||||
Vector2 drawPos = _hoverStartScreen + new Vector2(20, 0) + new Vector2(0, -(_hoverBodies.Count * 4 * lineHeight / 2f));
|
||||
int row = 0;
|
||||
@@ -134,7 +134,7 @@ namespace Robust.Client.Debugging
|
||||
row++;
|
||||
}
|
||||
|
||||
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Entity}");
|
||||
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
|
||||
row++;
|
||||
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
|
||||
row++;
|
||||
@@ -146,15 +146,16 @@ namespace Robust.Client.Debugging
|
||||
|
||||
}
|
||||
|
||||
private void DrawWorld(DrawingHandleWorld worldHandle)
|
||||
private void DrawWorld(in OverlayDrawArgs args)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
worldHandle.UseShader(_shader);
|
||||
var drawing = new PhysDrawingAdapter(worldHandle);
|
||||
|
||||
_hoverBodies.Clear();
|
||||
var mouseScreenPos = _inputManager.MouseScreenPosition;
|
||||
var mouseWorldPos = _eyeManager.ScreenToMap(mouseScreenPos).Position;
|
||||
_hoverStartScreen = mouseScreenPos;
|
||||
_hoverStartScreen = mouseScreenPos.Position;
|
||||
|
||||
var viewport = _eyeManager.GetWorldViewport();
|
||||
|
||||
@@ -163,20 +164,29 @@ namespace Robust.Client.Debugging
|
||||
var mapId = _eyeManager.CurrentMap;
|
||||
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
|
||||
var colorEdge = Color.Red.WithAlpha(0.33f);
|
||||
var drawnJoints = new HashSet<Joint>();
|
||||
|
||||
foreach (var physBody in EntitySystem.Get<SharedBroadPhaseSystem>().GetCollidingEntities(mapId, viewport))
|
||||
{
|
||||
// all entities have a TransformComponent
|
||||
var transform = physBody.Entity.Transform;
|
||||
var transform = physBody.Owner.Transform;
|
||||
|
||||
var worldBox = physBody.GetWorldAABB();
|
||||
var worldBox = physBody.GetWorldAABB(_mapManager);
|
||||
if (worldBox.IsEmpty()) continue;
|
||||
|
||||
foreach (var fixture in physBody.Fixtures)
|
||||
{
|
||||
var shape = fixture.Shape;
|
||||
var sleepPercent = physBody.Awake ? physBody.SleepTime / sleepThreshold : 1.0f;
|
||||
shape.DebugDraw(drawing, transform.WorldMatrix, in viewport, sleepPercent);
|
||||
shape.DebugDraw(drawing, transform.WorldMatrix, in viewport, sleepPercent);
|
||||
}
|
||||
|
||||
foreach (var joint in physBody.Joints)
|
||||
{
|
||||
if (drawnJoints.Contains(joint)) continue;
|
||||
drawnJoints.Add(joint);
|
||||
|
||||
joint.DebugDraw(drawing, in viewport);
|
||||
}
|
||||
|
||||
if (worldBox.Contains(mouseWorldPos))
|
||||
@@ -271,11 +281,11 @@ namespace Robust.Client.Debugging
|
||||
_eyeManager = eyeManager;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
const float stubLength = 0.25f;
|
||||
|
||||
var worldHandle = (DrawingHandleWorld) handle;
|
||||
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
|
||||
foreach (var entity in _entityManager.GetEntities())
|
||||
{
|
||||
var transform = entity.Transform;
|
||||
|
||||
@@ -87,8 +87,9 @@ namespace Robust.Client.Debugging
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var handle = args.WorldHandle;
|
||||
foreach (var ray in _owner.raysWithLifeTime)
|
||||
{
|
||||
handle.DrawLine(
|
||||
|
||||
@@ -125,11 +125,11 @@ namespace Robust.Client.Debugging
|
||||
_physics = system;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (_physics.Flags == PhysicsDebugFlags.None) return;
|
||||
|
||||
var worldHandle = (DrawingHandleWorld) handle;
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
if ((_physics.Flags & PhysicsDebugFlags.Shapes) != 0)
|
||||
{
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Robust.LoaderApi;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client
|
||||
{
|
||||
internal partial class GameController
|
||||
{
|
||||
private IGameLoop _mainLoop = default!;
|
||||
private IGameLoop? _mainLoop;
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
|
||||
private static bool _hasStarted;
|
||||
|
||||
private Thread? _gameThread;
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Start(args);
|
||||
}
|
||||
|
||||
public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null)
|
||||
public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null, GameControllerOptions? options = null)
|
||||
{
|
||||
if (_hasStarted)
|
||||
{
|
||||
@@ -30,11 +36,11 @@ namespace Robust.Client
|
||||
|
||||
if (CommandLineArgs.TryParse(args, out var parsed))
|
||||
{
|
||||
ParsedMain(parsed, contentStart, loaderArgs);
|
||||
ParsedMain(parsed, contentStart, loaderArgs, options);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs)
|
||||
private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs, GameControllerOptions? options)
|
||||
{
|
||||
IoCManager.InitThread();
|
||||
|
||||
@@ -42,23 +48,18 @@ namespace Robust.Client
|
||||
|
||||
InitIoC(mode);
|
||||
|
||||
var gc = (GameController) IoCManager.Resolve<IGameController>();
|
||||
var gc = IoCManager.Resolve<GameController>();
|
||||
gc.SetCommandLineArgs(args);
|
||||
gc._loaderArgs = loaderArgs;
|
||||
if(options != null)
|
||||
gc.Options = options;
|
||||
|
||||
// When the game is ran with the startup executable being content,
|
||||
// we have to disable the separate load context.
|
||||
// Otherwise the content assemblies will be loaded twice which causes *many* fun bugs.
|
||||
gc._disableAssemblyLoadContext = contentStart;
|
||||
if (!gc.Startup())
|
||||
{
|
||||
Logger.Fatal("Failed to start game controller!");
|
||||
return;
|
||||
}
|
||||
gc.MainLoop(mode);
|
||||
gc.ContentStart = contentStart;
|
||||
|
||||
Logger.Debug("Goodbye");
|
||||
IoCManager.Clear();
|
||||
gc.Run(mode);
|
||||
}
|
||||
|
||||
public void OverrideMainLoop(IGameLoop gameLoop)
|
||||
@@ -66,52 +67,68 @@ namespace Robust.Client
|
||||
_mainLoop = gameLoop;
|
||||
}
|
||||
|
||||
public void MainLoop(DisplayMode mode)
|
||||
public void Run(DisplayMode mode, Func<ILogHandler>? logHandlerFactory = null)
|
||||
{
|
||||
if (_mainLoop == null)
|
||||
if (!StartupSystemSplash(logHandlerFactory))
|
||||
{
|
||||
_mainLoop = new GameLoop(_gameTiming)
|
||||
{
|
||||
SleepMode = mode == DisplayMode.Headless ? SleepMode.Delay : SleepMode.None
|
||||
};
|
||||
Logger.Fatal("Failed to start game controller!");
|
||||
return;
|
||||
}
|
||||
|
||||
_mainLoop.Tick += (sender, args) =>
|
||||
if (_clyde.SeparateWindowThread)
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
{
|
||||
Tick(args);
|
||||
}
|
||||
};
|
||||
var stackSize = _configurationManager.GetCVar(CVars.SysGameThreadStackSize);
|
||||
var priority = (ThreadPriority) _configurationManager.GetCVar(CVars.SysGameThreadPriority);
|
||||
|
||||
_mainLoop.Render += (sender, args) =>
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
_gameThread = new Thread(() => GameThreadMain(mode), stackSize)
|
||||
{
|
||||
_gameTiming.CurFrame++;
|
||||
_clyde.Render();
|
||||
}
|
||||
};
|
||||
_mainLoop.Input += (sender, args) =>
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
{
|
||||
Input(args);
|
||||
}
|
||||
};
|
||||
IsBackground = false,
|
||||
Priority = priority,
|
||||
Name = "Game thread",
|
||||
};
|
||||
|
||||
_mainLoop.Update += (sender, args) =>
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
{
|
||||
Update(args);
|
||||
}
|
||||
};
|
||||
_gameThread.Start();
|
||||
|
||||
// set GameLoop.Running to false to return from this function.
|
||||
_mainLoop.Run();
|
||||
// Will block until game exit
|
||||
_clyde.EnterWindowLoop();
|
||||
|
||||
if (_gameThread.IsAlive)
|
||||
{
|
||||
Logger.Debug("Window loop exited; waiting for game thread to exit");
|
||||
_gameThread.Join();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ContinueStartupAndLoop(mode);
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
|
||||
Logger.Debug("Goodbye");
|
||||
IoCManager.Clear();
|
||||
}
|
||||
|
||||
private void GameThreadMain(DisplayMode mode)
|
||||
{
|
||||
IoCManager.InitThread(_dependencyCollection);
|
||||
|
||||
ContinueStartupAndLoop(mode);
|
||||
|
||||
// Game thread exited, make sure window thread unblocks to finish shutdown.
|
||||
_clyde.TerminateWindowLoop();
|
||||
}
|
||||
|
||||
private void ContinueStartupAndLoop(DisplayMode mode)
|
||||
{
|
||||
if (!StartupContinue(mode))
|
||||
{
|
||||
Logger.Fatal("Failed to start game controller!");
|
||||
return;
|
||||
}
|
||||
|
||||
DebugTools.AssertNotNull(_mainLoop);
|
||||
_mainLoop!.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Audio.Midi;
|
||||
@@ -49,6 +49,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
[Dependency] private readonly ITimerManager _timerManager = default!;
|
||||
[Dependency] private readonly IClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityLookup _lookup = default!;
|
||||
[Dependency] private readonly IPlacementManager _placementManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IOverlayManagerInternal _overlayManager = default!;
|
||||
@@ -60,17 +61,18 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
|
||||
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
|
||||
[Dependency] private readonly IScriptClient _scriptClient = default!;
|
||||
[Dependency] private readonly IComponentManager _componentManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
|
||||
[Dependency] private readonly IAuthManager _authManager = default!;
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
private CommandLineArgs? _commandLineArgs;
|
||||
private bool _disableAssemblyLoadContext;
|
||||
|
||||
// Arguments for loader-load. Not used otherwise.
|
||||
private IMainArgs? _loaderArgs;
|
||||
|
||||
public bool ContentStart { get; set; } = false;
|
||||
public GameControllerOptions Options { get; private set; } = new();
|
||||
public InitialLaunchState LaunchState { get; private set; } = default!;
|
||||
|
||||
public bool LoadConfigAndUserData { get; set; } = true;
|
||||
@@ -80,17 +82,20 @@ namespace Robust.Client
|
||||
_commandLineArgs = args;
|
||||
}
|
||||
|
||||
public bool Startup(Func<ILogHandler>? logHandlerFactory = null)
|
||||
private bool StartupContinue(DisplayMode displayMode)
|
||||
{
|
||||
if (!StartupSystemSplash(logHandlerFactory))
|
||||
return false;
|
||||
_clyde.InitializePostWindowing();
|
||||
_clyde.SetWindowTitle(Options.DefaultWindowTitle);
|
||||
|
||||
_taskManager.Initialize();
|
||||
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
|
||||
// Disable load context usage on content start.
|
||||
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
|
||||
_modLoader.SetUseLoadContext(!_disableAssemblyLoadContext);
|
||||
_modLoader.SetEnableSandboxing(true);
|
||||
_modLoader.SetUseLoadContext(!ContentStart);
|
||||
_modLoader.SetEnableSandboxing(false && Options.Sandboxing);
|
||||
|
||||
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content."))
|
||||
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), Options.ContentModulePrefix))
|
||||
{
|
||||
Logger.Fatal("Errors while loading content assemblies.");
|
||||
return false;
|
||||
@@ -109,13 +114,14 @@ namespace Robust.Client
|
||||
|
||||
_resourceCache.PreloadTextures();
|
||||
_userInterfaceManager.Initialize();
|
||||
_eyeManager.Initialize();
|
||||
_networkManager.Initialize(false);
|
||||
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
|
||||
_prototypeManager.LoadDirectory(Options.PrototypeDirectory);
|
||||
_prototypeManager.Resync();
|
||||
_mapManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
@@ -137,9 +143,51 @@ namespace Robust.Client
|
||||
|
||||
GC.Collect();
|
||||
|
||||
// Setup main loop
|
||||
if (_mainLoop == null)
|
||||
{
|
||||
_mainLoop = new GameLoop(_gameTiming)
|
||||
{
|
||||
SleepMode = displayMode == DisplayMode.Headless ? SleepMode.Delay : SleepMode.None
|
||||
};
|
||||
}
|
||||
|
||||
_mainLoop.Tick += (sender, args) =>
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
{
|
||||
Tick(args);
|
||||
}
|
||||
};
|
||||
|
||||
_mainLoop.Render += (sender, args) =>
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
{
|
||||
_gameTiming.CurFrame++;
|
||||
_clyde.Render();
|
||||
}
|
||||
};
|
||||
_mainLoop.Input += (sender, args) =>
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
{
|
||||
Input(args);
|
||||
}
|
||||
};
|
||||
|
||||
_mainLoop.Update += (sender, args) =>
|
||||
{
|
||||
if (_mainLoop.Running)
|
||||
{
|
||||
Update(args);
|
||||
}
|
||||
};
|
||||
|
||||
_clyde.Ready();
|
||||
|
||||
if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
|
||||
if (!Options.DisableCommandLineConnect &&
|
||||
(_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
|
||||
&& LaunchState.ConnectEndpoint != null)
|
||||
{
|
||||
_client.ConnectToServer(LaunchState.ConnectEndpoint);
|
||||
@@ -154,8 +202,6 @@ namespace Robust.Client
|
||||
|
||||
SetupLogging(_logManager, logHandlerFactory ?? (() => new ConsoleLogHandler()));
|
||||
|
||||
_taskManager.Initialize();
|
||||
|
||||
// Figure out user data directory.
|
||||
var userDataDir = GetUserDataDir();
|
||||
|
||||
@@ -168,7 +214,7 @@ namespace Robust.Client
|
||||
|
||||
if (LoadConfigAndUserData)
|
||||
{
|
||||
var configFile = Path.Combine(userDataDir, "client_config.toml");
|
||||
var configFile = Path.Combine(userDataDir, Options.ConfigFileName);
|
||||
if (File.Exists(configFile))
|
||||
{
|
||||
// Load config from user data if available.
|
||||
@@ -192,7 +238,12 @@ namespace Robust.Client
|
||||
|
||||
_resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null);
|
||||
|
||||
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null);
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
|
||||
|
||||
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
|
||||
_loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
|
||||
|
||||
if (_loaderArgs != null)
|
||||
{
|
||||
_stringSerializer.EnableCaching = false;
|
||||
@@ -205,18 +256,16 @@ namespace Robust.Client
|
||||
_clyde.KeyUp += KeyUp;
|
||||
_clyde.KeyDown += KeyDown;
|
||||
_clyde.MouseWheel += MouseWheel;
|
||||
_clyde.CloseWindow += Shutdown;
|
||||
_clyde.CloseWindow += args =>
|
||||
{
|
||||
if (args.Window == _clyde.MainWindow)
|
||||
{
|
||||
Shutdown("Main window closed");
|
||||
}
|
||||
};
|
||||
|
||||
// Bring display up as soon as resources are mounted.
|
||||
if (!_clyde.Initialize())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_clyde.SetWindowTitle("Space Station 14");
|
||||
|
||||
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
return true;
|
||||
return _clyde.InitializePreWindowing();
|
||||
}
|
||||
|
||||
private Stream? VerifierExtraLoadHandler(string arg)
|
||||
@@ -262,8 +311,10 @@ namespace Robust.Client
|
||||
|
||||
public void Shutdown(string? reason = null)
|
||||
{
|
||||
DebugTools.AssertNotNull(_mainLoop);
|
||||
|
||||
// Already got shut down I assume,
|
||||
if (!_mainLoop.Running)
|
||||
if (!_mainLoop!.Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -293,12 +344,19 @@ namespace Robust.Client
|
||||
_timerManager.UpdateTimers(frameEventArgs);
|
||||
_taskManager.ProcessPendingTasks();
|
||||
|
||||
// GameStateManager is in full control of the simulation update.
|
||||
if (_client.RunLevel >= ClientRunLevel.Connected)
|
||||
// GameStateManager is in full control of the simulation update in multiplayer.
|
||||
if (_client.RunLevel == ClientRunLevel.InGame || _client.RunLevel == ClientRunLevel.Connected)
|
||||
{
|
||||
_gameStateManager.ApplyGameState();
|
||||
}
|
||||
|
||||
// In singleplayer, however, we're in full control instead.
|
||||
else if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
|
||||
{
|
||||
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds);
|
||||
_lookup.Update();
|
||||
}
|
||||
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
@@ -319,11 +377,6 @@ namespace Robust.Client
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal static void SetupLogging(ILogManager logManager, Func<ILogHandler> logHandlerFactory)
|
||||
{
|
||||
logManager.RootSawmill.AddHandler(logHandlerFactory());
|
||||
@@ -400,6 +453,7 @@ namespace Robust.Client
|
||||
{
|
||||
_networkManager.Shutdown("Client shutting down");
|
||||
_midiManager.Shutdown();
|
||||
IoCManager.Resolve<IEntityLookup>().Shutdown();
|
||||
_entityManager.Shutdown();
|
||||
_clyde.Shutdown();
|
||||
}
|
||||
60
Robust.Client/GameControllerOptions.cs
Normal file
60
Robust.Client/GameControllerOptions.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client
|
||||
{
|
||||
public class GameControllerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether content sandboxing will be enabled & enforced.
|
||||
/// </summary>
|
||||
public bool Sandboxing { get; init; } = true;
|
||||
|
||||
// TODO: Expose mounting methods to games using Robust as a library.
|
||||
/// <summary>
|
||||
/// Lists of mount options to mount.
|
||||
/// </summary>
|
||||
public MountOptions MountOptions { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Name the userdata directory will have.
|
||||
/// </summary>
|
||||
public string UserDataDirectoryName { get; init; } = "Space Station 14";
|
||||
|
||||
/// <summary>
|
||||
/// Name of the configuration file in the user data directory.
|
||||
/// </summary>
|
||||
public string ConfigFileName { get; init; } = "client_config.toml";
|
||||
|
||||
// TODO: Define engine branding from json file in resources.
|
||||
/// <summary>
|
||||
/// Default window title.
|
||||
/// </summary>
|
||||
public string DefaultWindowTitle { get; init; } = "Space Station 14";
|
||||
|
||||
/// <summary>
|
||||
/// Assemblies with this prefix will be loaded.
|
||||
/// </summary>
|
||||
public string ContentModulePrefix { get; init; } = "Content.";
|
||||
|
||||
/// <summary>
|
||||
/// Name of the content build directory, for game pack mounting purposes.
|
||||
/// </summary>
|
||||
public string ContentBuildDirectory { get; init; } = "Content.Client";
|
||||
|
||||
/// <summary>
|
||||
/// Directory to load all prototypes from.
|
||||
/// </summary>
|
||||
public ResourcePath PrototypeDirectory { get; init; } = new(@"/Prototypes/");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to disable mounting the "Resources/" folder on FULL_RELEASE.
|
||||
/// </summary>
|
||||
public bool ResourceMountDisabled { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to disable command line args server auto-connecting.
|
||||
/// </summary>
|
||||
public bool DisableCommandLineConnect { get; init; } = false;
|
||||
}
|
||||
}
|
||||
@@ -9,58 +9,30 @@ namespace Robust.Client.GameObjects
|
||||
public ClientComponentFactory()
|
||||
{
|
||||
// Required for the engine to work
|
||||
Register<MetaDataComponent>();
|
||||
RegisterReference<MetaDataComponent, IMetaDataComponent>();
|
||||
|
||||
// Required for the engine to work
|
||||
Register<TransformComponent>();
|
||||
RegisterReference<TransformComponent, ITransformComponent>();
|
||||
|
||||
Register<MapComponent>();
|
||||
RegisterReference<MapComponent, IMapComponent>();
|
||||
|
||||
Register<MapGridComponent>();
|
||||
RegisterReference<MapGridComponent, IMapGridComponent>();
|
||||
|
||||
Register<PhysicsComponent>();
|
||||
RegisterReference<PhysicsComponent, IPhysBody>();
|
||||
|
||||
Register<CollisionWakeComponent>();
|
||||
|
||||
Register<ContainerManagerComponent>();
|
||||
RegisterReference<ContainerManagerComponent, IContainerManager>();
|
||||
|
||||
RegisterIgnore("KeyBindingInput");
|
||||
|
||||
Register<InputComponent>();
|
||||
|
||||
Register<SpriteComponent>();
|
||||
RegisterReference<SpriteComponent, SharedSpriteComponent>();
|
||||
RegisterReference<SpriteComponent, ISpriteComponent>();
|
||||
|
||||
Register<ClientOccluderComponent>();
|
||||
RegisterReference<ClientOccluderComponent, OccluderComponent>();
|
||||
|
||||
Register<EyeComponent>();
|
||||
RegisterReference<EyeComponent, SharedEyeComponent>();
|
||||
|
||||
Register<AppearanceComponent>();
|
||||
RegisterReference<AppearanceComponent, SharedAppearanceComponent>();
|
||||
|
||||
Register<AppearanceTestComponent>();
|
||||
Register<SnapGridComponent>();
|
||||
|
||||
Register<ClientUserInterfaceComponent>();
|
||||
RegisterReference<ClientUserInterfaceComponent, SharedUserInterfaceComponent>();
|
||||
|
||||
Register<AnimationPlayerComponent>();
|
||||
|
||||
Register<TimerComponent>();
|
||||
RegisterClass<MetaDataComponent>();
|
||||
RegisterClass<TransformComponent>();
|
||||
RegisterClass<MapComponent>();
|
||||
RegisterClass<MapGridComponent>();
|
||||
RegisterClass<PhysicsComponent>();
|
||||
RegisterClass<CollisionWakeComponent>();
|
||||
RegisterClass<ClientUserInterfaceComponent>();
|
||||
RegisterClass<ContainerManagerComponent>();
|
||||
RegisterClass<InputComponent>();
|
||||
RegisterClass<SpriteComponent>();
|
||||
RegisterClass<ClientOccluderComponent>();
|
||||
RegisterClass<EyeComponent>();
|
||||
RegisterClass<AppearanceComponent>();
|
||||
RegisterClass<AppearanceTestComponent>();
|
||||
RegisterClass<SnapGridComponent>();
|
||||
RegisterClass<AnimationPlayerComponent>();
|
||||
RegisterClass<TimerComponent>();
|
||||
|
||||
#if DEBUG
|
||||
Register<DebugExceptionOnAddComponent>();
|
||||
Register<DebugExceptionInitializeComponent>();
|
||||
Register<DebugExceptionStartupComponent>();
|
||||
RegisterClass<DebugExceptionOnAddComponent>();
|
||||
RegisterClass<DebugExceptionInitializeComponent>();
|
||||
RegisterClass<DebugExceptionStartupComponent>();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Prometheus;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -13,297 +19,153 @@ namespace Robust.Client.GameObjects
|
||||
/// <summary>
|
||||
/// Manager for entities -- controls things like template loading and instantiation
|
||||
/// </summary>
|
||||
public sealed class ClientEntityManager : EntityManager, IClientEntityManager
|
||||
public sealed class ClientEntityManager : EntityManager, IClientEntityManagerInternal
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IComponentFactory _compFactory = default!;
|
||||
#if EXCEPTION_TOLERANCE
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
#endif
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private int _nextClientEntityUid = EntityUid.ClientUid + 1;
|
||||
protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1;
|
||||
|
||||
public override void Startup()
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Startup();
|
||||
SetupNetworking();
|
||||
ReceivedComponentMessage += (_, compMsg) => DispatchComponentMessage(compMsg);
|
||||
ReceivedSystemMessage += (_, systemMsg) => EventBus.RaiseEvent(EventSource.Network, systemMsg);
|
||||
|
||||
if (Started)
|
||||
{
|
||||
throw new InvalidOperationException("Startup() called multiple times");
|
||||
}
|
||||
|
||||
EntitySystemManager.Initialize();
|
||||
Started = true;
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
public List<EntityUid> ApplyEntityStates(EntityState[]? curEntStates, IEnumerable<EntityUid>? deletions,
|
||||
EntityState[]? nextEntStates)
|
||||
IEntity IClientEntityManagerInternal.CreateEntity(string? prototypeName, EntityUid? uid)
|
||||
{
|
||||
var toApply = new Dictionary<IEntity, (EntityState?, EntityState?)>();
|
||||
var toInitialize = new List<Entity>();
|
||||
var created = new List<EntityUid>();
|
||||
deletions ??= new EntityUid[0];
|
||||
return base.CreateEntity(prototypeName, uid);
|
||||
}
|
||||
|
||||
if (curEntStates != null && curEntStates.Length != 0)
|
||||
void IClientEntityManagerInternal.InitializeEntity(IEntity entity)
|
||||
{
|
||||
EntityManager.InitializeEntity((Entity)entity);
|
||||
}
|
||||
|
||||
void IClientEntityManagerInternal.StartEntity(IEntity entity)
|
||||
{
|
||||
base.StartEntity((Entity)entity);
|
||||
}
|
||||
|
||||
#region IEntityNetworkManager impl
|
||||
|
||||
public override IEntityNetworkManager EntityNetManager => this;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<NetworkComponentMessage>? ReceivedComponentMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<object>? ReceivedSystemMessage;
|
||||
|
||||
private readonly PriorityQueue<(uint seq, MsgEntity msg)> _queue = new(new MessageTickComparer());
|
||||
private uint _incomingMsgSequence = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetupNetworking()
|
||||
{
|
||||
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
|
||||
}
|
||||
|
||||
public override void TickUpdate(float frameTime, Histogram? histogram)
|
||||
{
|
||||
using (histogram?.WithLabels("EntityNet").NewTimer())
|
||||
{
|
||||
foreach (var es in curEntStates)
|
||||
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
|
||||
{
|
||||
//Known entities
|
||||
if (Entities.TryGetValue(es.Uid, out var entity))
|
||||
{
|
||||
toApply.Add(entity, (es, null));
|
||||
}
|
||||
else //Unknown entities
|
||||
{
|
||||
var metaState = (MetaDataComponentState?) es.ComponentStates
|
||||
?.FirstOrDefault(c => c.NetID == NetIDs.META_DATA);
|
||||
if (metaState == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Server sent new entity state for {es.Uid} without metadata component!");
|
||||
}
|
||||
var newEntity = CreateEntity(metaState.PrototypeId, es.Uid);
|
||||
toApply.Add(newEntity, (es, null));
|
||||
toInitialize.Add(newEntity);
|
||||
created.Add(newEntity.Uid);
|
||||
}
|
||||
var (_, msg) = _queue.Take();
|
||||
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
|
||||
DispatchMsgEntity(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextEntStates != null && nextEntStates.Length != 0)
|
||||
{
|
||||
foreach (var es in nextEntStates)
|
||||
{
|
||||
if (Entities.TryGetValue(es.Uid, out var entity))
|
||||
{
|
||||
if (toApply.TryGetValue(entity, out var state))
|
||||
{
|
||||
toApply[entity] = (state.Item1, es);
|
||||
}
|
||||
else
|
||||
{
|
||||
toApply[entity] = (null, es);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure this is done after all entities have been instantiated.
|
||||
foreach (var kvStates in toApply)
|
||||
{
|
||||
var ent = kvStates.Key;
|
||||
var entity = (Entity) ent;
|
||||
HandleEntityState(entity.EntityManager.ComponentManager, entity, kvStates.Value.Item1,
|
||||
kvStates.Value.Item2);
|
||||
}
|
||||
|
||||
foreach (var kvp in toApply)
|
||||
{
|
||||
UpdateEntityTree(kvp.Key);
|
||||
}
|
||||
|
||||
foreach (var id in deletions)
|
||||
{
|
||||
DeleteEntity(id);
|
||||
}
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
HashSet<Entity> brokenEnts = new HashSet<Entity>();
|
||||
#endif
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
InitializeEntity(entity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("state", $"Server entity threw in Init: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
|
||||
brokenEnts.Add(entity);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if(brokenEnts.Contains(entity))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
#endif
|
||||
StartEntity(entity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("state", $"Server entity threw in Start: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
|
||||
brokenEnts.Add(entity);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if(brokenEnts.Contains(entity))
|
||||
continue;
|
||||
#endif
|
||||
UpdateEntityTree(entity);
|
||||
}
|
||||
#if EXCEPTION_TOLERANCE
|
||||
foreach (var entity in brokenEnts)
|
||||
{
|
||||
entity.Delete();
|
||||
}
|
||||
#endif
|
||||
|
||||
return created;
|
||||
base.TickUpdate(frameTime, histogram);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity CreateEntityUninitialized(string? prototypeName)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message)
|
||||
{
|
||||
return CreateEntity(prototypeName);
|
||||
SendSystemNetworkMessage(message, default(uint));
|
||||
}
|
||||
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
|
||||
{
|
||||
var msg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
msg.Type = EntityMessageType.SystemMessage;
|
||||
msg.SystemMessage = message;
|
||||
msg.SourceTick = _gameTiming.CurTick;
|
||||
msg.Sequence = sequence;
|
||||
|
||||
_networkManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
|
||||
{
|
||||
var newEntity = CreateEntity(prototypeName, GenerateEntityUid());
|
||||
|
||||
if (TryGetEntity(coordinates.EntityId, out var entity))
|
||||
{
|
||||
newEntity.Transform.AttachParent(entity);
|
||||
newEntity.Transform.Coordinates = coordinates;
|
||||
}
|
||||
|
||||
return newEntity;
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates)
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public void SendComponentNetworkMessage(INetChannel? channel, IEntity entity, IComponent component, ComponentMessage message)
|
||||
{
|
||||
var newEntity = CreateEntity(prototypeName, GenerateEntityUid());
|
||||
newEntity.Transform.AttachParent(_mapManager.GetMapEntity(coordinates.MapId));
|
||||
newEntity.Transform.WorldPosition = coordinates.Position;
|
||||
return newEntity;
|
||||
if (!component.NetID.HasValue)
|
||||
throw new ArgumentException($"Component {component.Name} does not have a NetID.", nameof(component));
|
||||
|
||||
var msg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
msg.Type = EntityMessageType.ComponentMessage;
|
||||
msg.EntityUid = entity.Uid;
|
||||
msg.NetId = component.NetID.Value;
|
||||
msg.ComponentMessage = message;
|
||||
msg.SourceTick = _gameTiming.CurTick;
|
||||
|
||||
_networkManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity SpawnEntity(string? protoName, EntityCoordinates coordinates)
|
||||
private void HandleEntityNetworkMessage(MsgEntity message)
|
||||
{
|
||||
var newEnt = CreateEntityUninitialized(protoName, coordinates);
|
||||
InitializeAndStartEntity((Entity) newEnt);
|
||||
UpdateEntityTree(newEnt);
|
||||
return newEnt;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEntity SpawnEntity(string? protoName, MapCoordinates coordinates)
|
||||
{
|
||||
var entity = CreateEntityUninitialized(protoName, coordinates);
|
||||
InitializeAndStartEntity((Entity) entity);
|
||||
UpdateEntityTree(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
protected override EntityUid GenerateEntityUid()
|
||||
{
|
||||
return new(_nextClientEntityUid++);
|
||||
}
|
||||
|
||||
private void HandleEntityState(IComponentManager compMan, IEntity entity, EntityState? curState,
|
||||
EntityState? nextState)
|
||||
{
|
||||
var compStateWork = new Dictionary<uint, (ComponentState? curState, ComponentState? nextState)>();
|
||||
var entityUid = entity.Uid;
|
||||
|
||||
if (curState?.ComponentChanges != null)
|
||||
if (message.SourceTick <= _gameStateManager.CurServerTick)
|
||||
{
|
||||
foreach (var compChange in curState.ComponentChanges)
|
||||
{
|
||||
if (compChange.Deleted)
|
||||
{
|
||||
if (compMan.TryGetComponent(entityUid, compChange.NetID, out var comp))
|
||||
{
|
||||
compMan.RemoveComponent(entityUid, comp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (compMan.HasComponent(entityUid, compChange.NetID))
|
||||
continue;
|
||||
|
||||
var newComp = (Component) _compFactory.GetComponent(compChange.ComponentName!);
|
||||
newComp.Owner = entity;
|
||||
compMan.AddComponent(entity, newComp, true);
|
||||
}
|
||||
}
|
||||
DispatchMsgEntity(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (curState?.ComponentStates != null)
|
||||
{
|
||||
foreach (var compState in curState.ComponentStates)
|
||||
{
|
||||
compStateWork[compState.NetID] = (compState, null);
|
||||
}
|
||||
}
|
||||
// MsgEntity is sent with ReliableOrdered so Lidgren guarantees ordering of incoming messages.
|
||||
// We still need to store a sequence input number to ensure ordering remains consistent in
|
||||
// the priority queue.
|
||||
_queue.Add((++_incomingMsgSequence, message));
|
||||
}
|
||||
|
||||
if (nextState?.ComponentStates != null)
|
||||
private void DispatchMsgEntity(MsgEntity message)
|
||||
{
|
||||
switch (message.Type)
|
||||
{
|
||||
foreach (var compState in nextState.ComponentStates)
|
||||
{
|
||||
if (compStateWork.TryGetValue(compState.NetID, out var state))
|
||||
{
|
||||
compStateWork[compState.NetID] = (state.curState, compState);
|
||||
}
|
||||
else
|
||||
{
|
||||
compStateWork[compState.NetID] = (null, compState);
|
||||
}
|
||||
}
|
||||
}
|
||||
case EntityMessageType.ComponentMessage:
|
||||
ReceivedComponentMessage?.Invoke(this, new NetworkComponentMessage(message));
|
||||
return;
|
||||
|
||||
foreach (var (netId, (cur, next)) in compStateWork)
|
||||
{
|
||||
if (compMan.TryGetComponent(entityUid, netId, out var component))
|
||||
{
|
||||
try
|
||||
{
|
||||
component.HandleComponentState(cur, next);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var wrapper = new ComponentStateApplyException(
|
||||
$"Failed to apply comp state: entity={component.Owner}, comp={component.Name}", e);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
_runtimeLog.LogException(wrapper, "Component state apply");
|
||||
#else
|
||||
throw wrapper;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The component can be null here due to interp.
|
||||
// Because the NEXT state will have a new component, but this one doesn't yet.
|
||||
// That's fine though.
|
||||
if (cur == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var eUid = entityUid;
|
||||
var eRegisteredNetUidName = _compFactory.GetRegistration(netId).Name;
|
||||
DebugTools.Assert(
|
||||
$"Component does not exist for state: entUid={eUid}, expectedNetId={netId}, expectedName={eRegisteredNetUidName}");
|
||||
}
|
||||
case EntityMessageType.SystemMessage:
|
||||
ReceivedSystemMessage?.Invoke(this, message.SystemMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MessageTickComparer : IComparer<(uint seq, MsgEntity msg)>
|
||||
{
|
||||
public int Compare((uint seq, MsgEntity msg) x, (uint seq, MsgEntity msg) y)
|
||||
{
|
||||
var cmp = y.msg.SourceTick.CompareTo(x.msg.SourceTick);
|
||||
if (cmp != 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
|
||||
return y.seq.CompareTo(x.seq);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// The client implementation of the Entity Network Manager.
|
||||
/// </summary>
|
||||
public class ClientEntityNetworkManager : IEntityNetworkManager
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<NetworkComponentMessage>? ReceivedComponentMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<object>? ReceivedSystemMessage;
|
||||
|
||||
private readonly PriorityQueue<(uint seq, MsgEntity msg)> _queue = new(new MessageTickComparer());
|
||||
private uint _incomingMsgSequence = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetupNetworking()
|
||||
{
|
||||
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
|
||||
}
|
||||
|
||||
public void TickUpdate()
|
||||
{
|
||||
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
|
||||
{
|
||||
var (_, msg) = _queue.Take();
|
||||
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
|
||||
DispatchMsgEntity(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message)
|
||||
{
|
||||
SendSystemNetworkMessage(message, default(uint));
|
||||
}
|
||||
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
|
||||
{
|
||||
var msg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
msg.Type = EntityMessageType.SystemMessage;
|
||||
msg.SystemMessage = message;
|
||||
msg.SourceTick = _gameTiming.CurTick;
|
||||
msg.Sequence = sequence;
|
||||
|
||||
_networkManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendComponentNetworkMessage(INetChannel? channel, IEntity entity, IComponent component, ComponentMessage message)
|
||||
{
|
||||
if (!component.NetID.HasValue)
|
||||
throw new ArgumentException($"Component {component.Name} does not have a NetID.", nameof(component));
|
||||
|
||||
var msg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
msg.Type = EntityMessageType.ComponentMessage;
|
||||
msg.EntityUid = entity.Uid;
|
||||
msg.NetId = component.NetID.Value;
|
||||
msg.ComponentMessage = message;
|
||||
msg.SourceTick = _gameTiming.CurTick;
|
||||
|
||||
_networkManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
private void HandleEntityNetworkMessage(MsgEntity message)
|
||||
{
|
||||
if (message.SourceTick <= _gameStateManager.CurServerTick)
|
||||
{
|
||||
DispatchMsgEntity(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// MsgEntity is sent with ReliableOrdered so Lidgren guarantees ordering of incoming messages.
|
||||
// We still need to store a sequence input number to ensure ordering remains consistent in
|
||||
// the priority queue.
|
||||
_queue.Add((++_incomingMsgSequence, message));
|
||||
}
|
||||
|
||||
private void DispatchMsgEntity(MsgEntity message)
|
||||
{
|
||||
switch (message.Type)
|
||||
{
|
||||
case EntityMessageType.ComponentMessage:
|
||||
ReceivedComponentMessage?.Invoke(this, new NetworkComponentMessage(message));
|
||||
return;
|
||||
|
||||
case EntityMessageType.SystemMessage:
|
||||
ReceivedSystemMessage?.Invoke(this, message.SystemMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MessageTickComparer : IComparer<(uint seq, MsgEntity msg)>
|
||||
{
|
||||
public int Compare((uint seq, MsgEntity msg) x, (uint seq, MsgEntity msg) y)
|
||||
{
|
||||
var cmp = y.msg.SourceTick.CompareTo(x.msg.SourceTick);
|
||||
if (cmp != 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
|
||||
return y.seq.CompareTo(x.seq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(SharedAppearanceComponent))]
|
||||
public sealed class AppearanceComponent : SharedAppearanceComponent
|
||||
{
|
||||
[ViewVariables]
|
||||
@@ -94,8 +95,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
EntitySystem.Get<AppearanceSystem>()
|
||||
.EnqueueAppearanceUpdate(this);
|
||||
EntitySystem.Get<AppearanceSystem>().EnqueueUpdate(this);
|
||||
_appearanceDirty = true;
|
||||
}
|
||||
|
||||
@@ -115,21 +115,6 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
|
||||
internal class SpriteLayerToggle : AppearanceVisualizer
|
||||
{
|
||||
public const string NAME = "sprite_layer_toggle";
|
||||
|
||||
public readonly object Key;
|
||||
public readonly int SpriteLayer;
|
||||
|
||||
public SpriteLayerToggle(object key, int spriteLayer)
|
||||
{
|
||||
Key = key;
|
||||
SpriteLayer = spriteLayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(SharedEyeComponent))]
|
||||
public class EyeComponent : SharedEyeComponent
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
@@ -25,7 +26,7 @@ namespace Robust.Client.GameObjects
|
||||
[DataField("drawFov")]
|
||||
private bool _setDrawFovOnInitialize = true;
|
||||
[DataField("zoom")]
|
||||
private Vector2 _setZoomOnInitialize = Vector2.One/2f;
|
||||
private Vector2 _setZoomOnInitialize = Vector2.One;
|
||||
private Vector2 _offset = Vector2.Zero;
|
||||
|
||||
public IEye? Eye => _eye;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(OccluderComponent))]
|
||||
internal sealed class ClientOccluderComponent : OccluderComponent
|
||||
{
|
||||
internal SnapGridComponent? SnapGrid { get; private set; }
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
[ViewVariables] private (GridId, Vector2i) _lastPosition;
|
||||
[ViewVariables] internal OccluderDir Occluding { get; private set; }
|
||||
@@ -29,39 +32,36 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
if (Owner.TryGetComponent(out SnapGridComponent? snap))
|
||||
if (Owner.Transform.Anchored)
|
||||
{
|
||||
SnapGrid = snap;
|
||||
SnapGrid.OnPositionChanged += SnapGridOnPositionChanged;
|
||||
|
||||
SnapGridOnPositionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SnapGridOnPositionChanged()
|
||||
public void SnapGridOnPositionChanged()
|
||||
{
|
||||
SendDirty();
|
||||
_lastPosition = (Owner.Transform.GridID, SnapGrid!.Position);
|
||||
|
||||
if(!Owner.Transform.Anchored)
|
||||
return;
|
||||
|
||||
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
|
||||
_lastPosition = (Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates));
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
if (SnapGrid != null)
|
||||
{
|
||||
SnapGrid.OnPositionChanged -= SnapGridOnPositionChanged;
|
||||
}
|
||||
|
||||
SendDirty();
|
||||
}
|
||||
|
||||
private void SendDirty()
|
||||
{
|
||||
if (SnapGrid != null)
|
||||
if (Owner.Transform.Anchored)
|
||||
{
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
|
||||
new OccluderDirtyEvent(Owner, _lastPosition, SnapGrid.Offset));
|
||||
new OccluderDirtyEvent(Owner, _lastPosition));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,16 +69,18 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
Occluding = OccluderDir.None;
|
||||
|
||||
if (Deleted || SnapGrid == null)
|
||||
if (Deleted || !Owner.Transform.Anchored)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
void CheckDir(Direction dir, OccluderDir oclDir)
|
||||
{
|
||||
foreach (var neighbor in SnapGrid.GetInDir(dir))
|
||||
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
|
||||
var position = Owner.Transform.Coordinates;
|
||||
foreach (var neighbor in grid.GetInDir(position, dir))
|
||||
{
|
||||
if (neighbor.TryGetComponent(out ClientOccluderComponent? comp) && comp.Enabled)
|
||||
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out ClientOccluderComponent? comp) && comp.Enabled)
|
||||
{
|
||||
Occluding |= oclDir;
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Animations;
|
||||
@@ -167,7 +168,7 @@ namespace Robust.Client.GameObjects
|
||||
set
|
||||
{
|
||||
_radius = MathF.Max(value, 0.01f); // setting radius to 0 causes exceptions, so just use a value close enough to zero that it's unnoticeable.
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedMessage(this));
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedEvent(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +180,18 @@ namespace Robust.Client.GameObjects
|
||||
Mask = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What MapId we are intersecting for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
|
||||
|
||||
/// <summary>
|
||||
/// What grids we're on for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal List<GridId> IntersectingGrids = new();
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
if (_maskPath != null)
|
||||
@@ -230,7 +243,7 @@ namespace Robust.Client.GameObjects
|
||||
if (map != MapId.Nullspace)
|
||||
{
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
|
||||
new RenderTreeRemoveLightMessage(this, map));
|
||||
new RenderTreeRemoveLightEvent(this, map));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,11 +261,11 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public struct PointLightRadiusChangedMessage
|
||||
public class PointLightRadiusChangedEvent : EntityEventArgs
|
||||
{
|
||||
public PointLightComponent PointLightComponent { get; }
|
||||
|
||||
public PointLightRadiusChangedMessage(PointLightComponent pointLightComponent)
|
||||
public PointLightRadiusChangedEvent(PointLightComponent pointLightComponent)
|
||||
{
|
||||
PointLightComponent = pointLightComponent;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(SharedSpriteComponent))]
|
||||
[ComponentReference(typeof(ISpriteComponent))]
|
||||
public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent,
|
||||
IComponentDebug, ISerializationHooks
|
||||
{
|
||||
@@ -123,6 +125,18 @@ namespace Robust.Client.GameObjects
|
||||
[DataField("directional")]
|
||||
private bool _directional = true;
|
||||
|
||||
/// <summary>
|
||||
/// What MapId we are intersecting for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
|
||||
|
||||
/// <summary>
|
||||
/// What grids we're on for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal List<GridId> IntersectingGrids { get; } = new();
|
||||
|
||||
[DataField("layerDatums")]
|
||||
private List<PrototypeLayerData> LayerDatums
|
||||
{
|
||||
@@ -848,8 +862,8 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId,
|
||||
Environment.StackTrace);
|
||||
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI {1}. Trace:\n{2}", stateId,
|
||||
actualRsi.Path, Environment.StackTrace);
|
||||
theLayer.Texture = null;
|
||||
}
|
||||
}
|
||||
@@ -1353,7 +1367,7 @@ namespace Robust.Client.GameObjects
|
||||
if (map != MapId.Nullspace)
|
||||
{
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
|
||||
new RenderTreeRemoveSpriteMessage(this, map));
|
||||
new RenderTreeRemoveSpriteEvent(this, map));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1576,9 +1590,10 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendFormat(
|
||||
"vis/depth/scl/rot/ofs/col/diral/dir: {0}/{1}/{2}/{3}/{4}/{5}/{6}/{7}\n",
|
||||
"vis/depth/scl/rot/ofs/col/norot/override/dir: {0}/{1}/{2}/{3}/{4}/{5}/{6}/{8}/{7}\n",
|
||||
Visible, DrawDepth, Scale, Rotation, Offset,
|
||||
Color, Directional, GetDir(RSI.State.DirectionType.Dir8, Owner.Transform.WorldRotation)
|
||||
Color, NoRotation, GetDir(RSI.State.DirectionType.Dir8, Owner.Transform.WorldRotation),
|
||||
DirectionOverride
|
||||
);
|
||||
|
||||
foreach (var layer in Layers)
|
||||
@@ -2176,6 +2191,10 @@ namespace Robust.Client.GameObjects
|
||||
return null;
|
||||
}
|
||||
|
||||
public void QueueDelete()
|
||||
{
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
}
|
||||
@@ -2190,10 +2209,12 @@ namespace Robust.Client.GameObjects
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public void SendMessage(IComponent? owner, ComponentMessage message)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public void SendNetworkMessage(IComponent owner, ComponentMessage message, INetChannel? channel = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(SharedUserInterfaceComponent))]
|
||||
public class ClientUserInterfaceComponent : SharedUserInterfaceComponent, ISerializationHooks
|
||||
{
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
@@ -23,6 +23,9 @@ namespace Robust.Client.GameObjects
|
||||
[DataField("interfaces", readOnly: true)]
|
||||
private List<PrototypeData> _interfaceData = new();
|
||||
|
||||
[ViewVariables]
|
||||
public IEnumerable<BoundUserInterface> Interfaces => _openInterfaces.Values;
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
_interfaces.Clear();
|
||||
@@ -33,48 +36,40 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel,
|
||||
ICommonSession? session = null)
|
||||
internal void MessageReceived(BoundUIWrapMessage msg)
|
||||
{
|
||||
base.HandleNetworkMessage(message, netChannel, session);
|
||||
|
||||
switch (message)
|
||||
switch (msg.Message)
|
||||
{
|
||||
case BoundInterfaceMessageWrapMessage wrapped:
|
||||
// Double nested switches who needs readability anyways.
|
||||
switch (wrapped.Message)
|
||||
case OpenBoundInterfaceMessage _:
|
||||
if (_openInterfaces.ContainsKey(msg.UiKey))
|
||||
{
|
||||
case OpenBoundInterfaceMessage _:
|
||||
if (_openInterfaces.ContainsKey(wrapped.UiKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
OpenInterface(wrapped);
|
||||
break;
|
||||
OpenInterface(msg);
|
||||
break;
|
||||
|
||||
case CloseBoundInterfaceMessage _:
|
||||
Close(wrapped.UiKey, true);
|
||||
break;
|
||||
case CloseBoundInterfaceMessage _:
|
||||
Close(msg.UiKey, true);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (_openInterfaces.TryGetValue(wrapped.UiKey, out var bi))
|
||||
{
|
||||
bi.InternalReceiveMessage(wrapped.Message);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (_openInterfaces.TryGetValue(msg.UiKey, out var bi))
|
||||
{
|
||||
bi.InternalReceiveMessage(msg.Message);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenInterface(BoundInterfaceMessageWrapMessage wrapped)
|
||||
private void OpenInterface(BoundUIWrapMessage wrapped)
|
||||
{
|
||||
var data = _interfaces[wrapped.UiKey];
|
||||
// TODO: This type should be cached, but I'm too lazy.
|
||||
var type = _reflectionManager.LooseGetType(data.ClientType);
|
||||
var boundInterface = (BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[]{this, wrapped.UiKey});
|
||||
var boundInterface =
|
||||
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[] {this, wrapped.UiKey});
|
||||
boundInterface.Open();
|
||||
_openInterfaces[wrapped.UiKey] = boundInterface;
|
||||
}
|
||||
@@ -86,7 +81,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
if(!remoteCall)
|
||||
if (!remoteCall)
|
||||
SendMessage(new CloseBoundInterfaceMessage(), uiKey);
|
||||
_openInterfaces.Remove(uiKey);
|
||||
boundUserInterface.Dispose();
|
||||
@@ -94,7 +89,8 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
internal void SendMessage(BoundUserInterfaceMessage message, object uiKey)
|
||||
{
|
||||
SendNetworkMessage(new BoundInterfaceMessageWrapMessage(message, uiKey));
|
||||
EntitySystem.Get<UserInterfaceSystem>()
|
||||
.Send(new BoundUIWrapMessage(Owner.Uid, message, uiKey));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Robust.Client.GameObjects
|
||||
DebugTools.AssertNotNull(localPlayer);
|
||||
|
||||
var sequence = IoCManager.Resolve<IClientGameStateManager>().SystemMessageDispatched(msg);
|
||||
entityManager.EntityNetManager.SendSystemNetworkMessage(msg, sequence);
|
||||
entityManager.EntityNetManager?.SendSystemNetworkMessage(msg, sequence);
|
||||
|
||||
var eventArgs = new EntitySessionEventArgs(localPlayer!.Session);
|
||||
|
||||
|
||||
@@ -1,51 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class AppearanceSystem : EntitySystem
|
||||
{
|
||||
private readonly Queue<AppearanceComponent> _updatesQueued = new();
|
||||
private readonly Queue<AppearanceComponent> _queuedUpdates = new();
|
||||
|
||||
public void EnqueueUpdate(AppearanceComponent component)
|
||||
{
|
||||
_queuedUpdates.Enqueue(component);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
while (_updatesQueued.TryDequeue(out var appearance))
|
||||
while (_queuedUpdates.TryDequeue(out var appearance))
|
||||
{
|
||||
UpdateComponent(appearance);
|
||||
if (appearance.Deleted)
|
||||
return;
|
||||
|
||||
foreach (var visualizer in appearance.Visualizers)
|
||||
{
|
||||
visualizer.OnChangeData(appearance);
|
||||
}
|
||||
|
||||
appearance.UnmarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateComponent(AppearanceComponent component)
|
||||
{
|
||||
if (component.Deleted)
|
||||
return;
|
||||
|
||||
foreach (var visualizer in component.Visualizers)
|
||||
{
|
||||
switch (visualizer)
|
||||
{
|
||||
case AppearanceComponent.SpriteLayerToggle spriteLayerToggle:
|
||||
UpdateSpriteLayerToggle(component, spriteLayerToggle);
|
||||
break;
|
||||
|
||||
default:
|
||||
visualizer.OnChangeData(component);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateSpriteLayerToggle(AppearanceComponent component, AppearanceComponent.SpriteLayerToggle toggle)
|
||||
{
|
||||
component.TryGetData(toggle.Key, out bool visible);
|
||||
var sprite = component.Owner.GetComponent<SpriteComponent>();
|
||||
sprite.LayerSetVisible(toggle.SpriteLayer, visible);
|
||||
}
|
||||
|
||||
public void EnqueueAppearanceUpdate(AppearanceComponent component)
|
||||
{
|
||||
_updatesQueued.Enqueue(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -20,10 +21,11 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private readonly Queue<IEntity> _dirtyEntities = new();
|
||||
private readonly Queue<EntityUid> _dirtyEntities = new();
|
||||
|
||||
private uint _updateGeneration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -32,6 +34,8 @@ namespace Robust.Client.GameObjects
|
||||
UpdatesAfter.Add(typeof(PhysicsSystem));
|
||||
|
||||
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
|
||||
|
||||
SubscribeLocalEvent<ClientOccluderComponent, SnapGridPositionChangedEvent>(HandleSnapGridMove);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
@@ -47,8 +51,8 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
while (_dirtyEntities.TryDequeue(out var entity))
|
||||
{
|
||||
if (!entity.Deleted
|
||||
&& entity.TryGetComponent(out ClientOccluderComponent? occluder)
|
||||
if (EntityManager.EntityExists(entity)
|
||||
&& ComponentManager.TryGetComponent(entity, out ClientOccluderComponent? occluder)
|
||||
&& occluder.UpdateGeneration != _updateGeneration)
|
||||
{
|
||||
occluder.Update();
|
||||
@@ -58,6 +62,11 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleSnapGridMove(EntityUid uid, ClientOccluderComponent component, SnapGridPositionChangedEvent args)
|
||||
{
|
||||
component.SnapGridOnPositionChanged();
|
||||
}
|
||||
|
||||
private void HandleDirtyEvent(OccluderDirtyEvent ev)
|
||||
{
|
||||
var sender = ev.Sender;
|
||||
@@ -65,13 +74,14 @@ namespace Robust.Client.GameObjects
|
||||
sender.TryGetComponent(out ClientOccluderComponent? iconSmooth)
|
||||
&& iconSmooth.Running)
|
||||
{
|
||||
var snapGrid = sender.GetComponent<SnapGridComponent>();
|
||||
var grid1 = _mapManager.GetGrid(sender.Transform.GridID);
|
||||
var coords = sender.Transform.Coordinates;
|
||||
|
||||
_dirtyEntities.Enqueue(sender);
|
||||
AddValidEntities(snapGrid.GetInDir(Direction.North));
|
||||
AddValidEntities(snapGrid.GetInDir(Direction.South));
|
||||
AddValidEntities(snapGrid.GetInDir(Direction.East));
|
||||
AddValidEntities(snapGrid.GetInDir(Direction.West));
|
||||
_dirtyEntities.Enqueue(sender.Uid);
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.North));
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.South));
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.East));
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.West));
|
||||
}
|
||||
|
||||
// Entity is no longer valid, update around the last position it was at.
|
||||
@@ -79,28 +89,23 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
var pos = ev.LastPosition.Value.pos;
|
||||
|
||||
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(1, 0), ev.Offset));
|
||||
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(-1, 0), ev.Offset));
|
||||
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(0, 1), ev.Offset));
|
||||
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(0, -1), ev.Offset));
|
||||
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(1, 0)));
|
||||
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, 0)));
|
||||
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(0, 1)));
|
||||
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(0, -1)));
|
||||
}
|
||||
}
|
||||
|
||||
private void AddValidEntities(IEnumerable<IEntity> candidates)
|
||||
private void AddValidEntities(IEnumerable<EntityUid> candidates)
|
||||
{
|
||||
foreach (var entity in candidates)
|
||||
{
|
||||
if (entity.HasComponent<ClientOccluderComponent>())
|
||||
if (ComponentManager.HasComponent<ClientOccluderComponent>(entity))
|
||||
{
|
||||
_dirtyEntities.Enqueue(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddValidEntities(IEnumerable<IComponent> candidates)
|
||||
{
|
||||
AddValidEntities(candidates.Select(c => c.Owner));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -108,15 +113,13 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
internal sealed class OccluderDirtyEvent : EntityEventArgs
|
||||
{
|
||||
public OccluderDirtyEvent(IEntity sender, (GridId grid, Vector2i pos)? lastPosition, SnapGridOffset offset)
|
||||
public OccluderDirtyEvent(IEntity sender, (GridId grid, Vector2i pos)? lastPosition)
|
||||
{
|
||||
LastPosition = lastPosition;
|
||||
Offset = offset;
|
||||
Sender = sender;
|
||||
}
|
||||
|
||||
public (GridId grid, Vector2i pos)? LastPosition { get; }
|
||||
public SnapGridOffset Offset { get; }
|
||||
public IEntity Sender { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,10 @@ namespace Robust.Client.GameObjects
|
||||
//Create effect from creation message
|
||||
var effect = new Effect(message, resourceCache, _mapManager, _entityManager);
|
||||
effect.Deathtime = gameTiming.CurTime + message.LifeTime;
|
||||
if (effect.AttachedEntityUid != null)
|
||||
if (effect.AttachedEntityUid != null
|
||||
&& _entityManager.TryGetEntity(effect.AttachedEntityUid.Value, out var attachedEntity))
|
||||
{
|
||||
effect.AttachedEntity = _entityManager.GetEntity(effect.AttachedEntityUid.Value);
|
||||
effect.AttachedEntity = attachedEntity;
|
||||
}
|
||||
|
||||
_Effects.Add(effect);
|
||||
@@ -346,11 +347,11 @@ namespace Robust.Client.GameObjects
|
||||
_entityManager = entityManager;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var map = _owner.eyeManager.CurrentMap;
|
||||
|
||||
var worldHandle = (DrawingHandleWorld) handle;
|
||||
var worldHandle = args.WorldHandle;
|
||||
ShaderInstance? currentShader = null;
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace Robust.Client.GameObjects
|
||||
private void DispatchInputCommand(FullInputCmdMessage message)
|
||||
{
|
||||
_stateManager.InputCommandDispatched(message);
|
||||
EntityNetworkManager.SendSystemNetworkMessage(message, message.InputSequence);
|
||||
EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, message.InputSequence);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -6,6 +8,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -47,14 +50,148 @@ namespace Robust.Client.GameObjects
|
||||
_mapManager.OnGridCreated += MapManagerOnGridCreated;
|
||||
_mapManager.OnGridRemoved += MapManagerOnGridRemoved;
|
||||
|
||||
SubscribeLocalEvent<EntMapIdChangedMessage>(EntMapIdChanged);
|
||||
SubscribeLocalEvent<MoveEvent>(EntMoved);
|
||||
SubscribeLocalEvent<EntParentChangedMessage>(EntParentChanged);
|
||||
SubscribeLocalEvent<PointLightRadiusChangedMessage>(PointLightRadiusChanged);
|
||||
SubscribeLocalEvent<RenderTreeRemoveSpriteMessage>(RemoveSprite);
|
||||
SubscribeLocalEvent<RenderTreeRemoveLightMessage>(RemoveLight);
|
||||
SubscribeLocalEvent<SpriteComponent, EntMapIdChangedMessage>(SpriteMapChanged);
|
||||
SubscribeLocalEvent<SpriteComponent, MoveEvent>(SpriteMoved);
|
||||
SubscribeLocalEvent<SpriteComponent, EntParentChangedMessage>(SpriteParentChanged);
|
||||
SubscribeLocalEvent<SpriteComponent, RenderTreeRemoveSpriteEvent>(RemoveSprite);
|
||||
|
||||
SubscribeLocalEvent<PointLightComponent, EntMapIdChangedMessage>(LightMapChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, MoveEvent>(LightMoved);
|
||||
SubscribeLocalEvent<PointLightComponent, EntParentChangedMessage>(LightParentChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, PointLightRadiusChangedEvent>(PointLightRadiusChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, RenderTreeRemoveLightEvent>(RemoveLight);
|
||||
}
|
||||
|
||||
// For the RemoveX methods
|
||||
// If the Transform is removed BEFORE the Sprite/Light,
|
||||
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
|
||||
// Otherwise these will still have their past MapId and that's all we need..
|
||||
|
||||
#region SpriteHandlers
|
||||
private void SpriteMapChanged(EntityUid uid, SpriteComponent component, EntMapIdChangedMessage args)
|
||||
{
|
||||
QueueSpriteUpdate(component);
|
||||
}
|
||||
|
||||
private void SpriteMoved(EntityUid uid, SpriteComponent component, MoveEvent args)
|
||||
{
|
||||
QueueSpriteUpdate(component);
|
||||
}
|
||||
|
||||
private void SpriteParentChanged(EntityUid uid, SpriteComponent component, EntParentChangedMessage args)
|
||||
{
|
||||
QueueSpriteUpdate(component);
|
||||
}
|
||||
|
||||
private void RemoveSprite(EntityUid uid, SpriteComponent component, RenderTreeRemoveSpriteEvent args)
|
||||
{
|
||||
ClearSprite(component);
|
||||
}
|
||||
|
||||
private void ClearSprite(SpriteComponent component)
|
||||
{
|
||||
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
|
||||
{
|
||||
foreach (var gridId in component.IntersectingGrids)
|
||||
{
|
||||
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
|
||||
tree.SpriteTree.Remove(component);
|
||||
}
|
||||
}
|
||||
|
||||
component.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
private void QueueSpriteUpdate(SpriteComponent component)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
|
||||
component.TreeUpdateQueued = true;
|
||||
_spriteQueue.Add(component);
|
||||
|
||||
foreach (var child in component.Owner.Transform.Children)
|
||||
{
|
||||
QueueSpriteUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueSpriteUpdate(IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out SpriteComponent? spriteComponent)) return;
|
||||
QueueSpriteUpdate(spriteComponent);
|
||||
|
||||
foreach (var child in entity.Transform.Children)
|
||||
{
|
||||
QueueSpriteUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LightHandlers
|
||||
private void LightMapChanged(EntityUid uid, PointLightComponent component, EntMapIdChangedMessage args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void LightMoved(EntityUid uid, PointLightComponent component, MoveEvent args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void LightParentChanged(EntityUid uid, PointLightComponent component, EntParentChangedMessage args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void PointLightRadiusChanged(EntityUid uid, PointLightComponent component, PointLightRadiusChangedEvent args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void RemoveLight(EntityUid uid, PointLightComponent component, RenderTreeRemoveLightEvent args)
|
||||
{
|
||||
ClearLight(component);
|
||||
}
|
||||
|
||||
private void ClearLight(PointLightComponent component)
|
||||
{
|
||||
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
|
||||
{
|
||||
foreach (var gridId in component.IntersectingGrids)
|
||||
{
|
||||
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
|
||||
tree.LightTree.Remove(component);
|
||||
}
|
||||
}
|
||||
|
||||
component.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
private void QueueLightUpdate(PointLightComponent component)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
|
||||
component.TreeUpdateQueued = true;
|
||||
_lightQueue.Add(component);
|
||||
|
||||
foreach (var child in component.Owner.Transform.Children)
|
||||
{
|
||||
QueueLightUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueLightUpdate(IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out PointLightComponent? lightComponent)) return;
|
||||
QueueLightUpdate(lightComponent);
|
||||
|
||||
foreach (var child in entity.Transform.Children)
|
||||
{
|
||||
QueueLightUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
@@ -62,149 +199,27 @@ namespace Robust.Client.GameObjects
|
||||
_mapManager.MapDestroyed -= MapManagerOnMapDestroyed;
|
||||
_mapManager.OnGridCreated -= MapManagerOnGridCreated;
|
||||
_mapManager.OnGridRemoved -= MapManagerOnGridRemoved;
|
||||
|
||||
UnsubscribeLocalEvent<EntMapIdChangedMessage>();
|
||||
UnsubscribeLocalEvent<MoveEvent>();
|
||||
UnsubscribeLocalEvent<EntParentChangedMessage>();
|
||||
UnsubscribeLocalEvent<PointLightRadiusChangedMessage>();
|
||||
UnsubscribeLocalEvent<RenderTreeRemoveSpriteMessage>();
|
||||
UnsubscribeLocalEvent<RenderTreeRemoveLightMessage>();
|
||||
}
|
||||
|
||||
// For these next 2 methods (the Remove* ones):
|
||||
// If the Transform is removed BEFORE the Sprite/Light,
|
||||
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
|
||||
// Otherwise these will still have their past MapId and that's all we need..
|
||||
private void RemoveLight(RenderTreeRemoveLightMessage ev)
|
||||
{
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.LightAabbFunc(ev.Light), true))
|
||||
{
|
||||
_gridTrees[ev.Map][gridId].LightTree.Remove(ev.Light);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveSprite(RenderTreeRemoveSpriteMessage ev)
|
||||
{
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.SpriteAabbFunc(ev.Sprite), true))
|
||||
{
|
||||
_gridTrees[ev.Map][gridId].SpriteTree.Remove(ev.Sprite);
|
||||
}
|
||||
}
|
||||
|
||||
private void PointLightRadiusChanged(PointLightRadiusChangedMessage ev)
|
||||
{
|
||||
QueueUpdateLight(ev.PointLightComponent);
|
||||
}
|
||||
|
||||
private void EntParentChanged(EntParentChangedMessage ev)
|
||||
{
|
||||
UpdateEntity(ev.Entity);
|
||||
}
|
||||
|
||||
private void EntMoved(MoveEvent ev)
|
||||
{
|
||||
UpdateEntity(ev.Sender);
|
||||
}
|
||||
|
||||
private void UpdateEntity(IEntity entity)
|
||||
{
|
||||
if (entity.TryGetComponent(out SpriteComponent? spriteComponent))
|
||||
{
|
||||
if (!spriteComponent.TreeUpdateQueued)
|
||||
{
|
||||
spriteComponent.TreeUpdateQueued = true;
|
||||
|
||||
_spriteQueue.Add(spriteComponent);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out PointLightComponent? light))
|
||||
{
|
||||
QueueUpdateLight(light);
|
||||
}
|
||||
|
||||
foreach (var child in entity.Transform.ChildEntityUids)
|
||||
{
|
||||
UpdateEntity(EntityManager.GetEntity(child));
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueUpdateLight(PointLightComponent light)
|
||||
{
|
||||
if (!light.TreeUpdateQueued)
|
||||
{
|
||||
light.TreeUpdateQueued = true;
|
||||
|
||||
_lightQueue.Add(light);
|
||||
}
|
||||
}
|
||||
|
||||
private void EntMapIdChanged(EntMapIdChangedMessage ev)
|
||||
{
|
||||
// Nullspace is a valid map ID for stuff to have but we also aren't gonna bother indexing it.
|
||||
// So that's why there's a GetValueOrDefault.
|
||||
var oldMapTrees = _gridTrees.GetValueOrDefault(ev.OldMapId);
|
||||
|
||||
// TODO: MMMM probably a better way to do this.
|
||||
if (ev.Entity.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
if (oldMapTrees != null)
|
||||
{
|
||||
foreach (var (_, gridTree) in oldMapTrees)
|
||||
{
|
||||
gridTree.SpriteTree.Remove(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = MapTrees.SpriteAabbFunc(sprite);
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
|
||||
{
|
||||
Box2 gridBounds;
|
||||
|
||||
if (gridId == GridId.Invalid)
|
||||
{
|
||||
gridBounds = bounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
gridBounds = bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
}
|
||||
_gridTrees[ev.Entity.Transform.MapID][gridId].SpriteTree.AddOrUpdate(sprite, gridBounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.Entity.TryGetComponent(out PointLightComponent? light))
|
||||
{
|
||||
if (oldMapTrees != null)
|
||||
{
|
||||
foreach (var (_, gridTree) in oldMapTrees)
|
||||
{
|
||||
gridTree.LightTree.Remove(light);
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = MapTrees.LightAabbFunc(light);
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
|
||||
{
|
||||
Box2 gridBounds;
|
||||
|
||||
if (gridId == GridId.Invalid)
|
||||
{
|
||||
gridBounds = bounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
gridBounds = bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
}
|
||||
_gridTrees[ev.Entity.Transform.MapID][gridId].LightTree.AddOrUpdate(light, gridBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MapManagerOnMapDestroyed(object? sender, MapEventArgs e)
|
||||
{
|
||||
foreach (var (_, gridTree) in _gridTrees[e.Map])
|
||||
{
|
||||
foreach (var comp in gridTree.LightTree)
|
||||
{
|
||||
comp.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
foreach (var comp in gridTree.SpriteTree)
|
||||
{
|
||||
comp.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
// Just in case?
|
||||
gridTree.LightTree.Clear();
|
||||
gridTree.SpriteTree.Clear();
|
||||
}
|
||||
|
||||
_gridTrees.Remove(e.Map);
|
||||
}
|
||||
|
||||
@@ -228,47 +243,109 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void MapManagerOnGridRemoved(MapId mapId, GridId gridId)
|
||||
{
|
||||
var gridTree = _gridTrees[mapId][gridId];
|
||||
|
||||
foreach (var sprite in gridTree.SpriteTree)
|
||||
{
|
||||
sprite.IntersectingGrids.Remove(gridId);
|
||||
}
|
||||
|
||||
foreach (var light in gridTree.LightTree)
|
||||
{
|
||||
light.IntersectingGrids.Remove(gridId);
|
||||
}
|
||||
|
||||
// Clear in case
|
||||
gridTree.LightTree.Clear();
|
||||
gridTree.SpriteTree.Clear();
|
||||
_gridTrees[mapId].Remove(gridId);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
foreach (var queuedUpdateSprite in _spriteQueue)
|
||||
foreach (var sprite in _spriteQueue)
|
||||
{
|
||||
var map = queuedUpdateSprite.Owner.Transform.MapID;
|
||||
if (map == MapId.Nullspace)
|
||||
var mapId = sprite.Owner.Transform.MapID;
|
||||
|
||||
// If we're on a new map then clear the old one.
|
||||
if (sprite.IntersectingMapId != mapId)
|
||||
{
|
||||
continue;
|
||||
ClearSprite(sprite);
|
||||
}
|
||||
|
||||
var mapTree = _gridTrees[map];
|
||||
sprite.IntersectingMapId = mapId;
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
|
||||
MapTrees.SpriteAabbFunc(queuedUpdateSprite), true))
|
||||
if (mapId == MapId.Nullspace) continue;
|
||||
|
||||
var mapTree = _gridTrees[mapId];
|
||||
var aabb = MapTrees.SpriteAabbFunc(sprite);
|
||||
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
|
||||
|
||||
// Remove from old
|
||||
foreach (var gridId in sprite.IntersectingGrids)
|
||||
{
|
||||
mapTree[gridId].SpriteTree.AddOrUpdate(queuedUpdateSprite);
|
||||
if (intersectingGrids.Contains(gridId)) continue;
|
||||
mapTree[gridId].SpriteTree.Remove(sprite);
|
||||
}
|
||||
|
||||
queuedUpdateSprite.TreeUpdateQueued = false;
|
||||
// Rebuild in the update below
|
||||
sprite.IntersectingGrids.Clear();
|
||||
|
||||
// Update / add to new
|
||||
foreach (var gridId in intersectingGrids)
|
||||
{
|
||||
var translated = aabb.Translated(gridId == GridId.Invalid
|
||||
? Vector2.Zero
|
||||
: -_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
mapTree[gridId].SpriteTree.AddOrUpdate(sprite, translated);
|
||||
|
||||
sprite.IntersectingGrids.Add(gridId);
|
||||
}
|
||||
|
||||
sprite.TreeUpdateQueued = false;
|
||||
}
|
||||
|
||||
foreach (var queuedUpdateLight in _lightQueue)
|
||||
foreach (var light in _lightQueue)
|
||||
{
|
||||
var map = queuedUpdateLight.Owner.Transform.MapID;
|
||||
if (map == MapId.Nullspace)
|
||||
var mapId = light.Owner.Transform.MapID;
|
||||
|
||||
// If we're on a new map then clear the old one.
|
||||
if (light.IntersectingMapId != mapId)
|
||||
{
|
||||
continue;
|
||||
ClearLight(light);
|
||||
}
|
||||
|
||||
var mapTree = _gridTrees[map];
|
||||
light.IntersectingMapId = mapId;
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
|
||||
MapTrees.LightAabbFunc(queuedUpdateLight), true))
|
||||
if (mapId == MapId.Nullspace) continue;
|
||||
|
||||
var mapTree = _gridTrees[mapId];
|
||||
var aabb = MapTrees.LightAabbFunc(light);
|
||||
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
|
||||
|
||||
// Remove from old
|
||||
foreach (var gridId in intersectingGrids)
|
||||
{
|
||||
mapTree[gridId].LightTree.AddOrUpdate(queuedUpdateLight);
|
||||
if (intersectingGrids.Contains(gridId)) continue;
|
||||
mapTree[gridId].LightTree.Remove(light);
|
||||
}
|
||||
|
||||
queuedUpdateLight.TreeUpdateQueued = false;
|
||||
// Rebuild in the update below
|
||||
light.IntersectingGrids.Clear();
|
||||
|
||||
// Update / add to new
|
||||
foreach (var gridId in intersectingGrids)
|
||||
{
|
||||
var translated = aabb.Translated(gridId == GridId.Invalid
|
||||
? Vector2.Zero
|
||||
: -_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
mapTree[gridId].LightTree.AddOrUpdate(light, translated);
|
||||
light.IntersectingGrids.Add(gridId);
|
||||
}
|
||||
|
||||
light.TreeUpdateQueued = false;
|
||||
}
|
||||
|
||||
_spriteQueue.Clear();
|
||||
@@ -303,9 +380,9 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
internal struct RenderTreeRemoveSpriteMessage
|
||||
internal class RenderTreeRemoveSpriteEvent : EntityEventArgs
|
||||
{
|
||||
public RenderTreeRemoveSpriteMessage(SpriteComponent sprite, MapId map)
|
||||
public RenderTreeRemoveSpriteEvent(SpriteComponent sprite, MapId map)
|
||||
{
|
||||
Sprite = sprite;
|
||||
Map = map;
|
||||
@@ -315,9 +392,9 @@ namespace Robust.Client.GameObjects
|
||||
public MapId Map { get; }
|
||||
}
|
||||
|
||||
internal struct RenderTreeRemoveLightMessage
|
||||
internal class RenderTreeRemoveLightEvent : EntityEventArgs
|
||||
{
|
||||
public RenderTreeRemoveLightMessage(PointLightComponent light, MapId map)
|
||||
public RenderTreeRemoveLightEvent(PointLightComponent light, MapId map)
|
||||
{
|
||||
Light = light;
|
||||
Map = map;
|
||||
|
||||
@@ -16,11 +16,17 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private RenderingTreeSystem _treeSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_treeSystem = Get<RenderingTreeSystem>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
var renderTreeSystem = EntitySystemManager.GetEntitySystem<RenderingTreeSystem>();
|
||||
|
||||
// So we could calculate the correct size of the entities based on the contents of their sprite...
|
||||
// Or we can just assume that no entity is larger than 10x10 and get a stupid easy check.
|
||||
var pvsBounds = _eyeManager.GetWorldViewport().Enlarged(5);
|
||||
@@ -33,18 +39,9 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(currentMap, pvsBounds, true))
|
||||
{
|
||||
Box2 gridBounds;
|
||||
var gridBounds = gridId == GridId.Invalid ? pvsBounds : pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
if (gridId == GridId.Invalid)
|
||||
{
|
||||
gridBounds = pvsBounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
gridBounds = pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
}
|
||||
|
||||
var mapTree = renderTreeSystem.GetSpriteTreeForMap(currentMap, gridId);
|
||||
var mapTree = _treeSystem.GetSpriteTreeForMap(currentMap, gridId);
|
||||
|
||||
mapTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class UserInterfaceSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<BoundUIWrapMessage>(MessageReceived);
|
||||
SubscribeLocalEvent<ClientUserInterfaceComponent, ComponentShutdown>(OnUserInterfaceShutdown);
|
||||
}
|
||||
|
||||
private void OnUserInterfaceShutdown(EntityUid uid, ClientUserInterfaceComponent component, ComponentShutdown args)
|
||||
{
|
||||
foreach (var bui in component.Interfaces)
|
||||
{
|
||||
bui.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void MessageReceived(BoundUIWrapMessage ev)
|
||||
{
|
||||
var cmp = ComponentManager.GetComponent<ClientUserInterfaceComponent>(ev.Entity);
|
||||
|
||||
cmp.MessageReceived(ev);
|
||||
}
|
||||
|
||||
internal void Send(BoundUIWrapMessage msg)
|
||||
{
|
||||
RaiseNetworkEvent(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,8 @@ using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
public interface IClientEntityManager : IEntityManager
|
||||
public interface IClientEntityManager : IEntityManager, IEntityNetworkManager
|
||||
{
|
||||
/// <returns>The list of new entities created.</returns>
|
||||
List<EntityUid> ApplyEntityStates(EntityState[]? curEntStates, IEnumerable<EntityUid>? deletions,
|
||||
EntityState[]? nextEntStates);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
15
Robust.Client/GameObjects/IClientEntityManagerInternal.cs
Normal file
15
Robust.Client/GameObjects/IClientEntityManagerInternal.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
internal interface IClientEntityManagerInternal : IClientEntityManager
|
||||
{
|
||||
// These methods are used by the Game State Manager.
|
||||
|
||||
IEntity CreateEntity(string? prototypeName, EntityUid? uid = null);
|
||||
|
||||
void InitializeEntity(IEntity entity);
|
||||
|
||||
void StartEntity(IEntity entity);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public class PlayerAttachedMsg : ComponentMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public class PlayerDetachedMsg : ComponentMessage
|
||||
{
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Log;
|
||||
@@ -31,16 +35,21 @@ namespace Robust.Client.GameStates
|
||||
_pendingSystemMessages
|
||||
= new();
|
||||
|
||||
[Dependency] private readonly IClientEntityManager _entities = default!;
|
||||
[Dependency] private readonly IComponentFactory _compFactory = default!;
|
||||
[Dependency] private readonly IClientEntityManagerInternal _entities = default!;
|
||||
[Dependency] private readonly IEntityLookup _lookup = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
[Dependency] private readonly IClientNetManager _network = default!;
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IClientMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IComponentManager _componentManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
#if EXCEPTION_TOLERANCE
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
#endif
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MinBufferSize => _processor.MinBufferSize;
|
||||
@@ -303,6 +312,8 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
|
||||
|
||||
_lookup.Update();
|
||||
}
|
||||
|
||||
private void ResetPredictedEntities(GameTick curTick)
|
||||
@@ -383,7 +394,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
_config.TickProcessMessages();
|
||||
_mapManager.ApplyGameStatePre(curState.MapData);
|
||||
var createdEntities = _entities.ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
|
||||
var createdEntities = ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
|
||||
nextState?.EntityStates);
|
||||
_players.ApplyPlayerStates(curState.PlayerStates);
|
||||
_mapManager.ApplyGameStatePost(curState.MapData);
|
||||
@@ -391,6 +402,218 @@ namespace Robust.Client.GameStates
|
||||
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
|
||||
return createdEntities;
|
||||
}
|
||||
|
||||
private List<EntityUid> ApplyEntityStates(EntityState[]? curEntStates, IEnumerable<EntityUid>? deletions,
|
||||
EntityState[]? nextEntStates)
|
||||
{
|
||||
var toApply = new Dictionary<IEntity, (EntityState?, EntityState?)>();
|
||||
var toInitialize = new List<Entity>();
|
||||
var created = new List<EntityUid>();
|
||||
deletions ??= new EntityUid[0];
|
||||
|
||||
if (curEntStates != null && curEntStates.Length != 0)
|
||||
{
|
||||
foreach (var es in curEntStates)
|
||||
{
|
||||
//Known entities
|
||||
if (_entities.TryGetEntity(es.Uid, out var entity))
|
||||
{
|
||||
toApply.Add(entity, (es, null));
|
||||
}
|
||||
else //Unknown entities
|
||||
{
|
||||
var metaState = (MetaDataComponentState?) es.ComponentStates
|
||||
?.FirstOrDefault(c => c.NetID == NetIDs.META_DATA);
|
||||
if (metaState == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Server sent new entity state for {es.Uid} without metadata component!");
|
||||
}
|
||||
var newEntity = (Entity)_entities.CreateEntity(metaState.PrototypeId, es.Uid);
|
||||
toApply.Add(newEntity, (es, null));
|
||||
toInitialize.Add(newEntity);
|
||||
created.Add(newEntity.Uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextEntStates != null && nextEntStates.Length != 0)
|
||||
{
|
||||
foreach (var es in nextEntStates)
|
||||
{
|
||||
if (_entities.TryGetEntity(es.Uid, out var entity))
|
||||
{
|
||||
if (toApply.TryGetValue(entity, out var state))
|
||||
{
|
||||
toApply[entity] = (state.Item1, es);
|
||||
}
|
||||
else
|
||||
{
|
||||
toApply[entity] = (null, es);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure this is done after all entities have been instantiated.
|
||||
foreach (var kvStates in toApply)
|
||||
{
|
||||
var ent = kvStates.Key;
|
||||
var entity = (Entity) ent;
|
||||
HandleEntityState(entity.EntityManager.ComponentManager, entity, kvStates.Value.Item1,
|
||||
kvStates.Value.Item2);
|
||||
}
|
||||
|
||||
foreach (var id in deletions)
|
||||
{
|
||||
_entities.DeleteEntity(id);
|
||||
}
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
HashSet<Entity> brokenEnts = new HashSet<Entity>();
|
||||
#endif
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
_entities.InitializeEntity(entity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("state", $"Server entity threw in Init: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
|
||||
brokenEnts.Add(entity);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if (brokenEnts.Contains(entity))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
#endif
|
||||
_entities.StartEntity(entity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("state", $"Server entity threw in Start: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
|
||||
brokenEnts.Add(entity);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (var entity in toInitialize)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if (brokenEnts.Contains(entity))
|
||||
continue;
|
||||
#endif
|
||||
}
|
||||
#if EXCEPTION_TOLERANCE
|
||||
foreach (var entity in brokenEnts)
|
||||
{
|
||||
entity.Delete();
|
||||
}
|
||||
#endif
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
private void HandleEntityState(IComponentManager compMan, IEntity entity, EntityState? curState,
|
||||
EntityState? nextState)
|
||||
{
|
||||
var compStateWork = new Dictionary<uint, (ComponentState? curState, ComponentState? nextState)>();
|
||||
var entityUid = entity.Uid;
|
||||
|
||||
if (curState?.ComponentChanges != null)
|
||||
{
|
||||
foreach (var compChange in curState.ComponentChanges)
|
||||
{
|
||||
if (compChange.Deleted)
|
||||
{
|
||||
if (compMan.TryGetComponent(entityUid, compChange.NetID, out var comp))
|
||||
{
|
||||
compMan.RemoveComponent(entityUid, comp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (compMan.HasComponent(entityUid, compChange.NetID))
|
||||
continue;
|
||||
|
||||
var newComp = (Component) _compFactory.GetComponent(compChange.ComponentName!);
|
||||
newComp.Owner = entity;
|
||||
compMan.AddComponent(entity, newComp, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (curState?.ComponentStates != null)
|
||||
{
|
||||
foreach (var compState in curState.ComponentStates)
|
||||
{
|
||||
compStateWork[compState.NetID] = (compState, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextState?.ComponentStates != null)
|
||||
{
|
||||
foreach (var compState in nextState.ComponentStates)
|
||||
{
|
||||
if (compStateWork.TryGetValue(compState.NetID, out var state))
|
||||
{
|
||||
compStateWork[compState.NetID] = (state.curState, compState);
|
||||
}
|
||||
else
|
||||
{
|
||||
compStateWork[compState.NetID] = (null, compState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (netId, (cur, next)) in compStateWork)
|
||||
{
|
||||
if (compMan.TryGetComponent(entityUid, netId, out var component))
|
||||
{
|
||||
try
|
||||
{
|
||||
component.HandleComponentState(cur, next);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var wrapper = new ComponentStateApplyException(
|
||||
$"Failed to apply comp state: entity={component.Owner}, comp={component.Name}", e);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
_runtimeLog.LogException(wrapper, "Component state apply");
|
||||
#else
|
||||
throw wrapper;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The component can be null here due to interp.
|
||||
// Because the NEXT state will have a new component, but this one doesn't yet.
|
||||
// That's fine though.
|
||||
if (cur == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var eUid = entityUid;
|
||||
var eRegisteredNetUidName = _compFactory.GetRegistration(netId).Name;
|
||||
DebugTools.Assert(
|
||||
$"Component does not exist for state: entUid={eUid}, expectedNetId={netId}, expectedName={eRegisteredNetUidName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class GameStateAppliedArgs : EventArgs
|
||||
|
||||
@@ -127,42 +127,41 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (!_netManager.IsConnected)
|
||||
return;
|
||||
|
||||
switch (currentSpace)
|
||||
switch (args.Space)
|
||||
{
|
||||
case OverlaySpace.ScreenSpace:
|
||||
DrawScreen(handle);
|
||||
DrawScreen(args);
|
||||
break;
|
||||
case OverlaySpace.WorldSpace:
|
||||
DrawWorld(handle);
|
||||
DrawWorld(args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWorld(DrawingHandleBase handle)
|
||||
private void DrawWorld(in OverlayDrawArgs args)
|
||||
{
|
||||
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
|
||||
|
||||
if(!pvsEnabled)
|
||||
return;
|
||||
|
||||
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
var pvsCenter = _eyeManager.CurrentEye.Position;
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize, pvsSize));
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange * 2, pvsRange * 2));
|
||||
|
||||
var worldHandle = (DrawingHandleWorld)handle;
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
worldHandle.DrawRect(pvsBox, Color.Red, false);
|
||||
}
|
||||
|
||||
private void DrawScreen(DrawingHandleBase handle)
|
||||
private void DrawScreen(in OverlayDrawArgs args)
|
||||
{
|
||||
// remember, 0,0 is top left of ui with +X right and +Y down
|
||||
var screenHandle = (DrawingHandleScreen) handle;
|
||||
var screenHandle = args.ScreenHandle;
|
||||
|
||||
for (int i = 0; i < _netEnts.Count; i++)
|
||||
{
|
||||
|
||||
@@ -153,7 +153,7 @@ namespace Robust.Client.GameStates
|
||||
_history.RemoveRange(0, over);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
// remember, 0,0 is top left of ui with +X right and +Y down
|
||||
|
||||
@@ -161,6 +161,7 @@ namespace Robust.Client.GameStates
|
||||
var width = HistorySize;
|
||||
var height = 500;
|
||||
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
|
||||
var handle = args.ScreenHandle;
|
||||
|
||||
// bottom payload line
|
||||
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
|
||||
@@ -183,7 +184,7 @@ namespace Robust.Client.GameStates
|
||||
// Draw size if above average
|
||||
if (drawSizeThreshold * 1.5 < state.Payload)
|
||||
{
|
||||
DrawString((DrawingHandleScreen) handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
|
||||
DrawString(handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
|
||||
}
|
||||
|
||||
// second tick marks
|
||||
@@ -223,14 +224,14 @@ namespace Robust.Client.GameStates
|
||||
handle.DrawLine(new Vector2(leftMargin, midYoff), new Vector2(leftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
|
||||
|
||||
// payload text
|
||||
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, warnYoff), "56K");
|
||||
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, midYoff), "33.6K");
|
||||
DrawString(handle, _font, new Vector2(leftMargin + width, warnYoff), "56K");
|
||||
DrawString(handle, _font, new Vector2(leftMargin + width, midYoff), "33.6K");
|
||||
|
||||
// interp text info
|
||||
if(lastLagY != -1)
|
||||
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
|
||||
DrawString(handle, _font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
|
||||
|
||||
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
|
||||
DrawString(handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
@@ -306,9 +307,9 @@ namespace Robust.Client.GameStates
|
||||
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var overlayMan = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
|
||||
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
|
||||
{
|
||||
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
|
||||
|
||||
@@ -26,8 +26,9 @@ namespace Robust.Client.GameStates
|
||||
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var handle = args.DrawingHandle;
|
||||
handle.UseShader(_shader);
|
||||
var worldHandle = (DrawingHandleWorld) handle;
|
||||
var viewport = _eyeManager.GetWorldViewport();
|
||||
|
||||
@@ -50,9 +50,9 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void GetViewMatrix(out Matrix3 viewMatrix)
|
||||
public void GetViewMatrix(out Matrix3 viewMatrix, Vector2 renderScale)
|
||||
{
|
||||
var scaleMat = Matrix3.CreateScale(_scale.X, _scale.Y);
|
||||
var scaleMat = Matrix3.CreateScale(_scale.X * renderScale.X, _scale.Y * renderScale.Y);
|
||||
var rotMat = Matrix3.CreateRotation(_rotation);
|
||||
var transMat = Matrix3.CreateTranslation(-_coords.Position);
|
||||
|
||||
@@ -60,9 +60,9 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void GetViewMatrixInv(out Matrix3 viewMatrixInv)
|
||||
public void GetViewMatrixInv(out Matrix3 viewMatrixInv, Vector2 renderScale)
|
||||
{
|
||||
GetViewMatrix(out var viewMatrix);
|
||||
GetViewMatrix(out var viewMatrix, renderScale);
|
||||
viewMatrixInv = Matrix3.Invert(viewMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -19,6 +21,7 @@ namespace Robust.Client.Graphics
|
||||
|
||||
[Dependency] private readonly IClyde _displayManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
|
||||
// We default to this when we get set to a null eye.
|
||||
private readonly FixedEye _defaultEye = new();
|
||||
@@ -32,11 +35,18 @@ namespace Robust.Client.Graphics
|
||||
set => _currentEye = value;
|
||||
}
|
||||
|
||||
public IViewportControl MainViewport { get; set; } = default!;
|
||||
|
||||
public void ClearCurrentEye()
|
||||
{
|
||||
_currentEye = _defaultEye;
|
||||
}
|
||||
|
||||
void IEyeManager.Initialize()
|
||||
{
|
||||
MainViewport = _uiManager.MainViewport;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MapId CurrentMap => CurrentEye.Position.MapId;
|
||||
|
||||
@@ -49,7 +59,7 @@ namespace Robust.Client.Graphics
|
||||
var topRight = ScreenToMap(new Vector2(vpSize.X, 0));
|
||||
var bottomRight = ScreenToMap(vpSize);
|
||||
var bottomLeft = ScreenToMap(new Vector2(0, vpSize.Y));
|
||||
|
||||
|
||||
var left = MathHelper.Min(topLeft.X, topRight.X, bottomRight.X, bottomLeft.X);
|
||||
var bottom = MathHelper.Min(topLeft.Y, topRight.Y, bottomRight.Y, bottomLeft.Y);
|
||||
var right = MathHelper.Max(topLeft.X, topRight.X, bottomRight.X, bottomLeft.X);
|
||||
@@ -61,16 +71,7 @@ namespace Robust.Client.Graphics
|
||||
/// <inheritdoc />
|
||||
public Vector2 WorldToScreen(Vector2 point)
|
||||
{
|
||||
var newPoint = point;
|
||||
|
||||
CurrentEye.GetViewMatrix(out var viewMatrix);
|
||||
newPoint = viewMatrix * newPoint;
|
||||
|
||||
// (inlined version of UiProjMatrix)
|
||||
newPoint *= new Vector2(1, -1) * PixelsPerMeter;
|
||||
newPoint += _displayManager.ScreenSize / 2f;
|
||||
|
||||
return newPoint;
|
||||
return MainViewport.WorldToScreen(point);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -103,29 +104,23 @@ namespace Robust.Client.Graphics
|
||||
|
||||
public ScreenCoordinates MapToScreen(MapCoordinates point)
|
||||
{
|
||||
return new(WorldToScreen(point.Position));
|
||||
return new(WorldToScreen(point.Position), MainViewport.Window?.Id ?? default);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MapCoordinates ScreenToMap(ScreenCoordinates point)
|
||||
{
|
||||
return ScreenToMap(point.Position);
|
||||
var (pos, window) = point;
|
||||
if (window != MainViewport.Window?.Id)
|
||||
return default;
|
||||
|
||||
return MainViewport.ScreenToMap(pos);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MapCoordinates ScreenToMap(Vector2 point)
|
||||
{
|
||||
var newPoint = point;
|
||||
|
||||
// (inlined version of UiProjMatrix^-1)
|
||||
newPoint -= _displayManager.ScreenSize / 2f;
|
||||
newPoint *= new Vector2(1, -1) / PixelsPerMeter;
|
||||
|
||||
// view matrix
|
||||
CurrentEye.GetViewMatrixInv(out var viewMatrixInv);
|
||||
newPoint = viewMatrixInv * newPoint;
|
||||
|
||||
return new MapCoordinates(newPoint, CurrentMap);
|
||||
return MainViewport.ScreenToMap(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,15 @@ namespace Robust.Client.Graphics
|
||||
/// world space to camera space.
|
||||
/// </summary>
|
||||
/// <param name="viewMatrix">View matrix for this camera.</param>
|
||||
void GetViewMatrix(out Matrix3 viewMatrix);
|
||||
/// <param name="renderScale"></param>
|
||||
void GetViewMatrix(out Matrix3 viewMatrix, Vector2 renderScale);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the inverted view matrix for this eye, used to convert a point from
|
||||
/// camera space to world space.
|
||||
/// </summary>
|
||||
/// <param name="viewMatrixInv">Inverted view matrix for this camera.</param>
|
||||
void GetViewMatrixInv(out Matrix3 viewMatrixInv);
|
||||
/// <param name="renderScale"></param>
|
||||
void GetViewMatrixInv(out Matrix3 viewMatrixInv, Vector2 renderScale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
@@ -17,6 +18,8 @@ namespace Robust.Client.Graphics
|
||||
/// </remarks>
|
||||
IEye CurrentEye { get; set; }
|
||||
|
||||
IViewportControl MainViewport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the map on which the current eye is "placed".
|
||||
/// </summary>
|
||||
@@ -72,5 +75,6 @@ namespace Robust.Client.Graphics
|
||||
MapCoordinates ScreenToMap(Vector2 point);
|
||||
|
||||
void ClearCurrentEye();
|
||||
void Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
ConfigurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
|
||||
}
|
||||
|
||||
private void _audioCreateContext()
|
||||
@@ -81,7 +81,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void _audioOpenDevice()
|
||||
{
|
||||
var preferredDevice = ConfigurationManager.GetCVar(CVars.AudioDevice);
|
||||
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
|
||||
|
||||
// Open device.
|
||||
if (!string.IsNullOrEmpty(preferredDevice))
|
||||
@@ -183,6 +183,32 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate)
|
||||
{
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (short* ptr = samples)
|
||||
{
|
||||
AL.BufferData(
|
||||
buffer,
|
||||
channels == 1 ? ALFormat.Mono16 : ALFormat.Stereo16,
|
||||
(IntPtr) ptr,
|
||||
samples.Length * 2,
|
||||
sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
var length = TimeSpan.FromSeconds(samples.Length / channels / (double) sampleRate);
|
||||
return new AudioStream(handle, length, channels);
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
|
||||
@@ -439,6 +465,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float decibels)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
|
||||
_gain = decibels;
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetOcclusion(float blocks)
|
||||
{
|
||||
_checkDisposed();
|
||||
@@ -747,6 +788,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float masterVolumeDecay)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
|
||||
_gain = masterVolumeDecay;
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
_checkDisposed();
|
||||
@@ -770,7 +826,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
if (SourceHandle == null) return;
|
||||
|
||||
if (!disposing || Thread.CurrentThread != _master._mainThread)
|
||||
if (!disposing || Thread.CurrentThread != _master._gameThread)
|
||||
{
|
||||
// We can't run this code inside another thread so tell Clyde to clear it up later.
|
||||
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal sealed partial class Clyde
|
||||
{
|
||||
// These are actually Cursor* but we can't do that because no pointer generic arguments.
|
||||
// Need a queue to dispose cursors since the GLFW methods aren't allowed from non-main thread (finalizers).
|
||||
// And they also aren't re-entrant.
|
||||
private readonly ConcurrentQueue<IntPtr> _cursorDisposeQueue = new();
|
||||
|
||||
private readonly Dictionary<StandardCursorShape, CursorImpl> _standardCursors =
|
||||
new();
|
||||
|
||||
// Keep current active cursor around so it doesn't get garbage collected.
|
||||
private CursorImpl? _currentCursor;
|
||||
|
||||
public ICursor GetStandardCursor(StandardCursorShape shape)
|
||||
{
|
||||
return _standardCursors[shape];
|
||||
}
|
||||
|
||||
public unsafe ICursor CreateCursor(Image<Rgba32> image, Vector2i hotSpot)
|
||||
{
|
||||
fixed (Rgba32* pixPtr = image.GetPixelSpan())
|
||||
{
|
||||
var gImg = new GlfwImage(image.Width, image.Height, (byte*) pixPtr);
|
||||
var (hotX, hotY) = hotSpot;
|
||||
var ptr = GLFW.CreateCursor(gImg, hotX, hotY);
|
||||
|
||||
return new CursorImpl(this, ptr, false);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void SetCursor(ICursor? cursor)
|
||||
{
|
||||
if (_currentCursor == cursor)
|
||||
{
|
||||
// Nothing has to be done!
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursor == null)
|
||||
{
|
||||
_currentCursor = null;
|
||||
GLFW.SetCursor(_glfwWindow, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(cursor is CursorImpl impl) || impl.Owner != this)
|
||||
{
|
||||
throw new ArgumentException("Cursor is not created by this clyde instance.");
|
||||
}
|
||||
|
||||
if (impl.Cursor == null)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(cursor));
|
||||
}
|
||||
|
||||
_currentCursor = impl;
|
||||
GLFW.SetCursor(_glfwWindow, impl.Cursor);
|
||||
}
|
||||
|
||||
private unsafe void FlushCursorDispose()
|
||||
{
|
||||
while (_cursorDisposeQueue.TryDequeue(out var cursor))
|
||||
{
|
||||
var ptr = (Cursor*) cursor;
|
||||
|
||||
if (_currentCursor != null && ptr == _currentCursor.Cursor)
|
||||
{
|
||||
// Currently active cursor getting disposed.
|
||||
_currentCursor = null;
|
||||
}
|
||||
|
||||
GLFW.DestroyCursor(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitCursors()
|
||||
{
|
||||
unsafe void AddStandardCursor(StandardCursorShape standardShape, CursorShape shape)
|
||||
{
|
||||
var ptr = GLFW.CreateStandardCursor(shape);
|
||||
|
||||
var impl = new CursorImpl(this, ptr, true);
|
||||
|
||||
_standardCursors.Add(standardShape, impl);
|
||||
}
|
||||
|
||||
AddStandardCursor(StandardCursorShape.Arrow, CursorShape.Arrow);
|
||||
AddStandardCursor(StandardCursorShape.IBeam, CursorShape.IBeam);
|
||||
AddStandardCursor(StandardCursorShape.Crosshair, CursorShape.Crosshair);
|
||||
AddStandardCursor(StandardCursorShape.Hand, CursorShape.Hand);
|
||||
AddStandardCursor(StandardCursorShape.HResize, CursorShape.HResize);
|
||||
AddStandardCursor(StandardCursorShape.VResize, CursorShape.VResize);
|
||||
}
|
||||
|
||||
private sealed class CursorImpl : ICursor
|
||||
{
|
||||
private readonly bool _standard;
|
||||
public Clyde Owner { get; }
|
||||
public unsafe Cursor* Cursor { get; private set; }
|
||||
|
||||
public unsafe CursorImpl(Clyde clyde, Cursor* pointer, bool standard)
|
||||
{
|
||||
_standard = standard;
|
||||
Owner = clyde;
|
||||
Cursor = pointer;
|
||||
}
|
||||
|
||||
~CursorImpl()
|
||||
{
|
||||
DisposeImpl();
|
||||
}
|
||||
|
||||
private unsafe void DisposeImpl()
|
||||
{
|
||||
Owner._cursorDisposeQueue.Enqueue((IntPtr) Cursor);
|
||||
Cursor = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_standard)
|
||||
{
|
||||
throw new InvalidOperationException("Can't dispose standard cursor shape.");
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
DisposeImpl();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
172
Robust.Client/Graphics/Clyde/Clyde.Events.cs
Normal file
172
Robust.Client/Graphics/Clyde/Clyde.Events.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
// Makes switching easier.
|
||||
#if EXCEPTION_TOLERANCE
|
||||
#define EXCEPTION_TOLERANCE_LOCAL
|
||||
using System;
|
||||
using Robust.Shared.Log;
|
||||
#endif
|
||||
|
||||
using System.Collections.Generic;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal sealed partial class Clyde
|
||||
{
|
||||
// To avoid re-entrancy bollocks we need ANOTHER queue here to actually dispatch our raw input events.
|
||||
// Yes, on top of the two queues inside the GLFW impl.
|
||||
// Because the GLFW-impl queue has to get flushed to avoid deadlocks on window creation
|
||||
// which is ALSO where key events get raised from in a re-entrant fashion. Yay.
|
||||
private readonly Queue<DEventBase> _eventDispatchQueue = new();
|
||||
|
||||
private void DispatchEvents()
|
||||
{
|
||||
while (_eventDispatchQueue.TryDequeue(out var ev))
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE_LOCAL
|
||||
try
|
||||
#endif
|
||||
{
|
||||
DispatchSingleEvent(ev);
|
||||
}
|
||||
#if EXCEPTION_TOLERANCE_LOCAL
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("clyde.win", $"Error dispatching window event {ev.GetType().Name}:\n{e}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchSingleEvent(DEventBase ev)
|
||||
{
|
||||
switch (ev)
|
||||
{
|
||||
case DEventKeyDown(var args):
|
||||
KeyDown?.Invoke(args);
|
||||
break;
|
||||
case DEventKeyUp(var args):
|
||||
KeyUp?.Invoke(args);
|
||||
break;
|
||||
case DEventMouseMove(var args):
|
||||
MouseMove?.Invoke(args);
|
||||
break;
|
||||
case DEventMouseEnterLeave(var args):
|
||||
MouseEnterLeave?.Invoke(args);
|
||||
break;
|
||||
case DEventScroll(var args):
|
||||
MouseWheel?.Invoke(args);
|
||||
break;
|
||||
case DEventText(var args):
|
||||
TextEntered?.Invoke(args);
|
||||
break;
|
||||
case DEventWindowClosed(var reg, var args):
|
||||
reg.Closed?.Invoke(args);
|
||||
CloseWindow?.Invoke(args);
|
||||
|
||||
if (reg.DisposeOnClose && !reg.IsMainWindow)
|
||||
DoDestroyWindow(reg);
|
||||
break;
|
||||
case DEventWindowContentScaleChanged(var args):
|
||||
OnWindowScaleChanged?.Invoke(args);
|
||||
break;
|
||||
case DEventWindowFocus(var args):
|
||||
OnWindowFocused?.Invoke(args);
|
||||
break;
|
||||
case DEventWindowResized(var args):
|
||||
OnWindowResized?.Invoke(args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SendKeyUp(KeyEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventKeyUp(ev));
|
||||
}
|
||||
|
||||
private void SendKeyDown(KeyEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventKeyDown(ev));
|
||||
}
|
||||
|
||||
private void SendScroll(MouseWheelEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventScroll(ev));
|
||||
}
|
||||
|
||||
private void SendCloseWindow(WindowReg windowReg, WindowClosedEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventWindowClosed(windowReg, ev));
|
||||
}
|
||||
|
||||
private void SendWindowResized(WindowReg reg, Vector2i oldSize)
|
||||
{
|
||||
if (reg.IsMainWindow)
|
||||
{
|
||||
UpdateMainWindowLoadedRtSize();
|
||||
GL.Viewport(0, 0, reg.FramebufferSize.X, reg.FramebufferSize.Y);
|
||||
CheckGlError();
|
||||
}
|
||||
else
|
||||
{
|
||||
reg.RenderTexture!.Dispose();
|
||||
CreateWindowRenderTexture(reg);
|
||||
}
|
||||
|
||||
var eventArgs = new WindowResizedEventArgs(
|
||||
oldSize,
|
||||
reg.FramebufferSize,
|
||||
reg.Handle);
|
||||
|
||||
_eventDispatchQueue.Enqueue(new DEventWindowResized(eventArgs));
|
||||
}
|
||||
|
||||
private void SendWindowContentScaleChanged(WindowContentScaleEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventWindowContentScaleChanged(ev));
|
||||
}
|
||||
|
||||
private void SendWindowFocus(WindowFocusedEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventWindowFocus(ev));
|
||||
}
|
||||
|
||||
private void SendText(TextEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventText(ev));
|
||||
}
|
||||
|
||||
private void SendMouseMove(MouseMoveEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventMouseMove(ev));
|
||||
}
|
||||
|
||||
private void SendMouseEnterLeave(MouseEnterLeaveEventArgs ev)
|
||||
{
|
||||
_eventDispatchQueue.Enqueue(new DEventMouseEnterLeave(ev));
|
||||
}
|
||||
|
||||
// D stands for Dispatch
|
||||
private abstract record DEventBase;
|
||||
|
||||
private sealed record DEventKeyUp(KeyEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventKeyDown(KeyEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventScroll(MouseWheelEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventWindowClosed(WindowReg Reg, WindowClosedEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventWindowResized(WindowResizedEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventWindowContentScaleChanged(WindowContentScaleEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventWindowFocus(WindowFocusedEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventText(TextEventArgs Args) : DEventBase;
|
||||
|
||||
private sealed record DEventMouseMove(MouseMoveEventArgs Args) : DEventBase;
|
||||
private sealed record DEventMouseEnterLeave(MouseEnterLeaveEventArgs Args) : DEventBase;
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var extensions = GetGLExtensions();
|
||||
|
||||
Logger.DebugS("clyde.ogl", "OpenGL capabilities:");
|
||||
_sawmillOgl.Debug("OpenGL capabilities:");
|
||||
|
||||
if (!_isGLES)
|
||||
{
|
||||
@@ -81,7 +81,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// We're ES <3.2, KHR_debug is extension and needs KHR suffixes.
|
||||
_isGLKhrDebugESExtension = true;
|
||||
Logger.DebugS("clyde.ogl", " khr_debug is ES extension!");
|
||||
_sawmillOgl.Debug(" khr_debug is ES extension!");
|
||||
}
|
||||
|
||||
CheckGLCap(ref _hasGLVertexArrayObject, "vertex_array_object", (3, 0));
|
||||
@@ -104,7 +104,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// This is 3.2 or extensions
|
||||
_hasGLFloatFramebuffers = !_isGLES;
|
||||
|
||||
Logger.DebugS("clyde.ogl", $" GLES: {_isGLES}");
|
||||
_sawmillOgl.Debug($" GLES: {_isGLES}");
|
||||
|
||||
void CheckGLCap(ref bool cap, string capName, (int major, int minor)? versionMin = null,
|
||||
params string[] exts)
|
||||
@@ -115,15 +115,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var prev = cap;
|
||||
var cVarName = $"display.ogl_block_{capName}";
|
||||
var block = ConfigurationManager.GetCVar<bool>(cVarName);
|
||||
var block = _cfg.GetCVar<bool>(cVarName);
|
||||
|
||||
if (block)
|
||||
{
|
||||
cap = false;
|
||||
Logger.DebugS("clyde.ogl", $" {cVarName} SET, BLOCKING {capName} (was: {prev})");
|
||||
_sawmillOgl.Debug($" {cVarName} SET, BLOCKING {capName} (was: {prev})");
|
||||
}
|
||||
|
||||
Logger.DebugS("clyde.ogl", $" {capName}: {cap}");
|
||||
_sawmillOgl.Debug($" {capName}: {cap}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
foreach (var cvar in cvars)
|
||||
{
|
||||
ConfigurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
|
||||
_cfg.RegisterCVar($"display.ogl_block_{cvar}", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,14 +168,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
extensionsText += extension;
|
||||
extensions.Add(extension);
|
||||
}
|
||||
Logger.DebugS("clyde.ogl", "OpenGL Extensions: {0}", extensionsText);
|
||||
_sawmillOgl.Debug("OpenGL Extensions: {0}", extensionsText);
|
||||
return extensions;
|
||||
}
|
||||
else
|
||||
{
|
||||
// GLES uses the (old?) API
|
||||
var extensions = GL.GetString(StringName.Extensions);
|
||||
Logger.DebugS("clyde.ogl", "OpenGL Extensions: {0}", extensions);
|
||||
_sawmillOgl.Debug("OpenGL Extensions: {0}", extensions);
|
||||
return new HashSet<string>(extensions.Split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -24,18 +25,28 @@ namespace Robust.Client.Graphics.Clyde
|
||||
=
|
||||
new();
|
||||
|
||||
public void Render()
|
||||
public unsafe void Render()
|
||||
{
|
||||
CheckTransferringScreenshots();
|
||||
|
||||
var allMinimized = true;
|
||||
foreach (var windowReg in _windowing!.AllWindows)
|
||||
{
|
||||
if (!windowReg.IsMinimized)
|
||||
{
|
||||
allMinimized = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var size = ScreenSize;
|
||||
if (size.X == 0 || size.Y == 0 || _isMinimized)
|
||||
if (size.X == 0 || size.Y == 0 || allMinimized)
|
||||
{
|
||||
ClearFramebuffer(Color.Black);
|
||||
|
||||
// We have to keep running swapbuffers here
|
||||
// or else the user's PC will turn into a heater!!
|
||||
SwapBuffers();
|
||||
SwapMainBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +62,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
ClearFramebuffer(Color.Black);
|
||||
|
||||
// Update shared UBOs.
|
||||
_updateUniformConstants(_framebufferSize);
|
||||
_updateUniformConstants(_windowing.MainWindow!.FramebufferSize);
|
||||
|
||||
{
|
||||
CalcScreenMatrices(ScreenSize, out var proj, out var view);
|
||||
@@ -63,103 +74,133 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
DrawSplash(_renderHandle);
|
||||
FlushRenderQueue();
|
||||
SwapBuffers();
|
||||
SwapMainBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
RenderOverlays(OverlaySpace.ScreenSpaceBelowWorld);
|
||||
|
||||
_mainViewport.Eye = _eyeManager.CurrentEye;
|
||||
RenderViewport(_mainViewport); //Worldspace overlays are rendered here.
|
||||
foreach (var weak in _viewports.Values)
|
||||
{
|
||||
var handle = _renderHandle.DrawingHandleScreen;
|
||||
var tex = _mainViewport.RenderTarget.Texture;
|
||||
|
||||
handle.DrawTexture(tex, (0, 0));
|
||||
FlushRenderQueue();
|
||||
if (weak.TryGetTarget(out var viewport) && viewport.AutomaticRender)
|
||||
RenderViewport(viewport);
|
||||
}
|
||||
|
||||
TakeScreenshot(ScreenshotType.BeforeUI);
|
||||
|
||||
RenderOverlays(OverlaySpace.ScreenSpace);
|
||||
|
||||
using (DebugGroup("UI"))
|
||||
{
|
||||
_userInterfaceManager.Render(_renderHandle);
|
||||
FlushRenderQueue();
|
||||
}
|
||||
|
||||
TakeScreenshot(ScreenshotType.AfterUI);
|
||||
TakeScreenshot(ScreenshotType.Final);
|
||||
|
||||
BlitSecondaryWindows();
|
||||
|
||||
// And finally, swap those buffers!
|
||||
SwapBuffers();
|
||||
SwapMainBuffers();
|
||||
}
|
||||
|
||||
private void RenderOverlays(OverlaySpace space)
|
||||
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox)
|
||||
{
|
||||
using (DebugGroup($"Overlays: {space}"))
|
||||
{
|
||||
var list = new List<Overlay>();
|
||||
|
||||
foreach (var overlay in _overlayManager.AllOverlays)
|
||||
var list = GetOverlaysForSpace(space);
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
if ((overlay.Space & space) != 0)
|
||||
if (overlay.RequestScreenTexture)
|
||||
{
|
||||
list.Add(overlay);
|
||||
}
|
||||
}
|
||||
FlushRenderQueue();
|
||||
list.Sort(OverlayComparer.Instance);
|
||||
foreach (var overlay in list) {
|
||||
if (overlay.RequestScreenTexture) {
|
||||
FlushRenderQueue();
|
||||
UpdateOverlayScreenTexture(space, _mainViewport.RenderTarget);
|
||||
UpdateOverlayScreenTexture(space, vp.RenderTarget);
|
||||
}
|
||||
if (overlay.OverwriteTargetFrameBuffer()) {
|
||||
|
||||
if (overlay.OverwriteTargetFrameBuffer())
|
||||
{
|
||||
ClearFramebuffer(default);
|
||||
}
|
||||
overlay.ClydeRender(_renderHandle, space);
|
||||
FlushRenderQueue();
|
||||
|
||||
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderOverlaysDirect(
|
||||
Viewport vp,
|
||||
IViewportControl vpControl,
|
||||
DrawingHandleBase handle,
|
||||
OverlaySpace space,
|
||||
in UIBox2i bounds)
|
||||
{
|
||||
var list = GetOverlaysForSpace(space);
|
||||
|
||||
var worldBounds = CalcWorldBounds(vp);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, worldBounds);
|
||||
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
overlay.Draw(args);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Overlay> GetOverlaysForSpace(OverlaySpace space)
|
||||
{
|
||||
var list = new List<Overlay>();
|
||||
|
||||
foreach (var overlay in _overlayManager.AllOverlays)
|
||||
{
|
||||
if ((overlay.Space & space) != 0)
|
||||
{
|
||||
list.Add(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
list.Sort(OverlayComparer.Instance);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private ClydeTexture? ScreenBufferTexture;
|
||||
private GLHandle screenBufferHandle;
|
||||
private Vector2 lastFrameSize;
|
||||
|
||||
/// <summary>
|
||||
/// Sends SCREEN_TEXTURE to all overlays in the given OverlaySpace that request it.
|
||||
/// </summary>
|
||||
private bool UpdateOverlayScreenTexture(OverlaySpace space, RenderTexture texture) {
|
||||
private bool UpdateOverlayScreenTexture(OverlaySpace space, RenderTexture texture)
|
||||
{
|
||||
//This currently does NOT consider viewports and just grabs the current screen framebuffer. This will need to be improved upon in the future.
|
||||
List<Overlay> oTargets = new List<Overlay>();
|
||||
foreach (var overlay in _overlayManager.AllOverlays) {
|
||||
if (overlay.RequestScreenTexture && overlay.Space == space) {
|
||||
foreach (var overlay in _overlayManager.AllOverlays)
|
||||
{
|
||||
if (overlay.RequestScreenTexture && overlay.Space == space)
|
||||
{
|
||||
oTargets.Add(overlay);
|
||||
}
|
||||
}
|
||||
if (oTargets.Count > 0 && ScreenBufferTexture != null) {
|
||||
if (lastFrameSize != _framebufferSize) {
|
||||
|
||||
if (oTargets.Count > 0 && ScreenBufferTexture != null)
|
||||
{
|
||||
if (lastFrameSize != texture.Size)
|
||||
{
|
||||
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
|
||||
GL.TexImage2D(TextureTarget.Texture2D, 0, _hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, _framebufferSize.X, _framebufferSize.Y, 0,
|
||||
GL.TexImage2D(TextureTarget.Texture2D, 0,
|
||||
_hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, texture.Size.X,
|
||||
texture.Size.Y, 0,
|
||||
PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
|
||||
}
|
||||
lastFrameSize = _framebufferSize;
|
||||
|
||||
lastFrameSize = texture.Size;
|
||||
CopyRenderTextureToTexture(texture, ScreenBufferTexture);
|
||||
foreach (Overlay overlay in oTargets) {
|
||||
foreach (Overlay overlay in oTargets)
|
||||
{
|
||||
overlay.ScreenTexture = ScreenBufferTexture;
|
||||
}
|
||||
|
||||
oTargets.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void DrawEntities(Viewport viewport, Box2 worldBounds)
|
||||
{
|
||||
if (_eyeManager.CurrentMap == MapId.Nullspace || !_mapManager.HasMapEntity(_eyeManager.CurrentMap))
|
||||
@@ -167,7 +208,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return;
|
||||
}
|
||||
|
||||
RenderOverlays(OverlaySpace.WorldSpaceBelowEntities);
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldBounds);
|
||||
|
||||
var screenSize = viewport.Size;
|
||||
|
||||
@@ -218,7 +259,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
flushed = true;
|
||||
}
|
||||
|
||||
overlay.ClydeRender(_renderHandle, OverlaySpace.WorldSpace);
|
||||
overlay.ClydeRender(
|
||||
_renderHandle,
|
||||
OverlaySpace.WorldSpace,
|
||||
null,
|
||||
viewport,
|
||||
new UIBox2i((0, 0), viewport.Size),
|
||||
worldBounds);
|
||||
overlayIndex = j;
|
||||
continue;
|
||||
}
|
||||
@@ -237,13 +284,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var spriteRT = spriteBB.TopRight;
|
||||
|
||||
// finally we can calculate screen bounding in pixels
|
||||
var screenLB = _eyeManager.WorldToScreen(spriteLB);
|
||||
var screenRT = _eyeManager.WorldToScreen(spriteRT);
|
||||
var screenLB = viewport.WorldToLocal(spriteLB);
|
||||
var screenRT = viewport.WorldToLocal(spriteRT);
|
||||
|
||||
// we need to scale RT a for effects like emission or highlight
|
||||
// scale can be passed with PostShader as variable in future
|
||||
var postShadeScale = 1.25f;
|
||||
var screenSpriteSize = (Vector2i)((screenRT - screenLB) * postShadeScale).Rounded();
|
||||
var screenSpriteSize = (Vector2i) ((screenRT - screenLB) * postShadeScale).Rounded();
|
||||
screenSpriteSize.Y = -screenSpriteSize.Y;
|
||||
|
||||
// I'm not 100% sure why it works, but without it post-shader
|
||||
@@ -268,8 +315,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// which is necessary for light application,
|
||||
// but it's ACTUALLY drawing into the center of the render target.
|
||||
var spritePos = spriteBB.Center;
|
||||
var screenPos = _eyeManager.WorldToScreen(spritePos);
|
||||
var (roundedX, roundedY) = roundedPos = (Vector2i)screenPos;
|
||||
var screenPos = viewport.WorldToLocal(spritePos);
|
||||
var (roundedX, roundedY) = roundedPos = (Vector2i) screenPos;
|
||||
var flippedPos = new Vector2i(roundedX, screenSize.Y - roundedY);
|
||||
flippedPos -= entityPostRenderTarget.Size / 2;
|
||||
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
|
||||
@@ -303,6 +350,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
_renderHandle.SetProjView(oldProj, oldView);
|
||||
_renderHandle.UseShader(null);
|
||||
|
||||
// TODO: cache this properly across frames.
|
||||
entityPostRenderTarget.DisposeDeferred();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,13 +415,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
handle.DrawingHandleScreen.DrawTexture(texture, (ScreenSize - texture.Size) / 2);
|
||||
}
|
||||
|
||||
private void RenderViewport(Viewport viewport)
|
||||
private void RenderInRenderTarget(RenderTargetBase rt, Action a)
|
||||
{
|
||||
if (viewport.Eye == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: for the love of god all this state pushing/popping needs to be cleaned up.
|
||||
|
||||
var oldTransform = _currentMatrixModel;
|
||||
@@ -380,28 +425,54 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Have to flush the render queue so that all commands finish rendering to the previous framebuffer.
|
||||
FlushRenderQueue();
|
||||
|
||||
var eye = viewport.Eye;
|
||||
|
||||
var oldVp = _currentViewport;
|
||||
_currentViewport = viewport;
|
||||
|
||||
var state = PushRenderStateFull();
|
||||
|
||||
{
|
||||
// Actual code that isn't just pushing/popping renderer state so we can return safely.
|
||||
|
||||
var rt = _currentViewport.RenderTarget;
|
||||
BindRenderTargetFull(RtToLoaded(rt));
|
||||
ClearFramebuffer(default);
|
||||
SetViewportImmediate(Box2i.FromDimensions(Vector2i.Zero, rt.Size));
|
||||
_updateUniformConstants(viewport.Size);
|
||||
_updateUniformConstants(rt.Size);
|
||||
CalcScreenMatrices(rt.Size, out var proj, out var view);
|
||||
SetProjViewFull(proj, view);
|
||||
|
||||
CalcWorldMatrices(rt.Size, eye, out var proj, out var view);
|
||||
// Smugleaf moment
|
||||
a();
|
||||
|
||||
FlushRenderQueue();
|
||||
}
|
||||
|
||||
FenceRenderTarget(rt);
|
||||
|
||||
PopRenderStateFull(state);
|
||||
_updateUniformConstants(_currentRenderTarget.Size);
|
||||
|
||||
SetScissorFull(oldScissor);
|
||||
_currentMatrixModel = oldTransform;
|
||||
}
|
||||
|
||||
private void RenderViewport(Viewport viewport)
|
||||
{
|
||||
if (viewport.Eye == null || viewport.Eye.Position.MapId == MapId.Nullspace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RenderInRenderTarget(viewport.RenderTarget, () =>
|
||||
{
|
||||
using var _ = DebugGroup($"Viewport: {viewport.Name}");
|
||||
|
||||
var oldVp = _currentViewport;
|
||||
|
||||
_currentViewport = viewport;
|
||||
var eye = viewport.Eye;
|
||||
|
||||
// Actual code that isn't just pushing/popping renderer state so we can return safely.
|
||||
|
||||
CalcWorldMatrices(viewport.RenderTarget.Size, viewport.RenderScale, eye, out var proj, out var view);
|
||||
SetProjViewFull(proj, view);
|
||||
|
||||
// Calculate world-space AABB for camera, to cull off-screen things.
|
||||
var worldBounds = Box2.CenteredAround(eye.Position.Position,
|
||||
_framebufferSize / (float) EyeManager.PixelsPerMeter * eye.Zoom);
|
||||
var worldBounds = CalcWorldBounds(viewport);
|
||||
|
||||
if (_eyeManager.CurrentMap != MapId.Nullspace)
|
||||
{
|
||||
@@ -410,6 +481,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
DrawLightsAndFov(viewport, worldBounds, eye);
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldBounds);
|
||||
FlushRenderQueue();
|
||||
|
||||
using (DebugGroup("Grids"))
|
||||
{
|
||||
_drawGrids(worldBounds);
|
||||
@@ -421,18 +495,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
DrawEntities(viewport, worldBounds);
|
||||
}
|
||||
|
||||
RenderOverlays(OverlaySpace.WorldSpaceBelowFOV);
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldBounds);
|
||||
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
|
||||
{
|
||||
GL.Clear(ClearBufferMask.StencilBufferBit);
|
||||
GL.Enable(EnableCap.StencilTest);
|
||||
GL.StencilOp(OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Replace);
|
||||
GL.StencilFunc(StencilFunction.Always, 1, 0xFF);
|
||||
GL.StencilMask(0xFF);
|
||||
ApplyFovToBuffer(viewport, eye);
|
||||
GL.StencilMask(0x00);
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +515,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// So there are distortions from incorrect projection.
|
||||
_renderHandle.UseShader(_fovDebugShaderInstance);
|
||||
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3.Identity);
|
||||
var pos = UIBox2.FromDimensions(ScreenSize / 2 - (200, 200), (400, 400));
|
||||
var pos = UIBox2.FromDimensions(viewport.Size / 2 - (200, 200), (400, 400));
|
||||
_renderHandle.DrawingHandleScreen.DrawTextureRect(FovTexture, pos);
|
||||
}
|
||||
|
||||
@@ -458,24 +525,25 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3.Identity);
|
||||
_renderHandle.DrawingHandleScreen.DrawTextureRect(
|
||||
viewport.WallBleedIntermediateRenderTarget2.Texture,
|
||||
UIBox2.FromDimensions(Vector2.Zero, ScreenSize), new Color(1, 1, 1, 0.5f));
|
||||
UIBox2.FromDimensions(Vector2.Zero, viewport.Size), new Color(1, 1, 1, 0.5f));
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldBounds);
|
||||
FlushRenderQueue();
|
||||
|
||||
RenderOverlays(OverlaySpace.WorldSpace);
|
||||
_currentViewport = oldVp;
|
||||
});
|
||||
}
|
||||
|
||||
GL.StencilFunc(StencilFunction.Notequal, 1, 0xFF);
|
||||
GL.Disable(EnableCap.DepthTest);
|
||||
RenderOverlays(OverlaySpace.WorldSpaceFOVStencil);
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
}
|
||||
private static Box2 CalcWorldBounds(Viewport viewport)
|
||||
{
|
||||
var eye = viewport.Eye;
|
||||
if (eye == null)
|
||||
return default;
|
||||
|
||||
PopRenderStateFull(state);
|
||||
_updateUniformConstants(oldVp?.Size ?? _framebufferSize);
|
||||
|
||||
SetScissorFull(oldScissor);
|
||||
_currentMatrixModel = oldTransform;
|
||||
_currentViewport = oldVp;
|
||||
// TODO: This seems completely unfit by lacking things like rotation handling.
|
||||
return Box2.CenteredAround(eye.Position.Position,
|
||||
viewport.Size / viewport.RenderScale / EyeManager.PixelsPerMeter * eye.Zoom);
|
||||
}
|
||||
|
||||
private sealed class OverlayComparer : IComparer<Overlay>
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
GL.BindTexture(TextureTarget.Texture2D, _loadedTextures[target.TextureId].OpenGLObject.Handle);
|
||||
CheckGlError();
|
||||
GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, 0, 0, _framebufferSize.X, _framebufferSize.Y);
|
||||
GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, 0, 0, source.Size.X, source.Size.Y);
|
||||
CheckGlError();
|
||||
|
||||
if (pause && store != null) {
|
||||
@@ -160,12 +160,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void CheckGlErrorInternal(string? path, int line)
|
||||
private void CheckGlErrorInternal(string? path, int line)
|
||||
{
|
||||
var err = GL.GetError();
|
||||
if (err != ErrorCode.NoError)
|
||||
{
|
||||
Logger.ErrorS("clyde.ogl", $"OpenGL error: {err} at {path}:{line}\n{Environment.StackTrace}");
|
||||
_sawmillOgl.Error($"OpenGL error: {err} at {path}:{line}\n{Environment.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void LoadGLProc<T>(string name, out T field) where T : Delegate
|
||||
{
|
||||
var proc = _graphicsContext.GetProcAddress(name);
|
||||
var proc = _windowing!.GraphicsBindingContext.GetProcAddress(name);
|
||||
if (proc == IntPtr.Zero || proc == new IntPtr(1) || proc == new IntPtr(2))
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load GL function '{name}'!");
|
||||
|
||||
@@ -604,7 +604,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// Have to scale the blurring radius based on viewport size and camera zoom.
|
||||
const float refCameraHeight = 14;
|
||||
var cameraSize = eye.Zoom.Y * viewport.Size.Y / EyeManager.PixelsPerMeter;
|
||||
var cameraSize = eye.Zoom.Y * viewport.Size.Y * (1 / viewport.RenderScale.Y) / EyeManager.PixelsPerMeter;
|
||||
// 7e-3f is just a magic factor that makes it look ok.
|
||||
var factor = 7e-3f * (refCameraHeight / cameraSize);
|
||||
|
||||
@@ -672,6 +672,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void ApplyFovToBuffer(Viewport viewport, IEye eye)
|
||||
{
|
||||
GL.Clear(ClearBufferMask.StencilBufferBit);
|
||||
GL.Enable(EnableCap.StencilTest);
|
||||
GL.StencilOp(OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Replace);
|
||||
GL.StencilFunc(StencilFunction.Always, 1, 0xFF);
|
||||
GL.StencilMask(0xFF);
|
||||
|
||||
// Applies FOV to the final framebuffer.
|
||||
|
||||
var fovShader = _loadedShaders[_fovShaderHandle].Program;
|
||||
@@ -685,6 +691,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
fovShader.SetUniformMaybe("center", eye.Position.Position);
|
||||
|
||||
DrawBlit(viewport, fovShader);
|
||||
|
||||
GL.StencilMask(0x00);
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
_isStencilling = false;
|
||||
}
|
||||
|
||||
private void ApplyLightingFovToBuffer(Viewport viewport, IEye eye)
|
||||
@@ -1012,13 +1022,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return (w, h);
|
||||
}
|
||||
|
||||
protected override void LightmapDividerChanged(int newValue)
|
||||
private void LightmapDividerChanged(int newValue)
|
||||
{
|
||||
_lightmapDivider = newValue;
|
||||
RegenAllLightRts();
|
||||
}
|
||||
|
||||
protected override void MaxLightsPerSceneChanged(int newValue)
|
||||
private void MaxLightsPerSceneChanged(int newValue)
|
||||
{
|
||||
_maxLightsPerScene = newValue;
|
||||
|
||||
@@ -1037,7 +1047,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
nameof(_shadowRenderTarget));
|
||||
}
|
||||
|
||||
protected override void SoftShadowsChanged(bool newValue)
|
||||
private void SoftShadowsChanged(bool newValue)
|
||||
{
|
||||
_enableSoftShadows = newValue;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return clydeTexture;
|
||||
}
|
||||
|
||||
public void RenderInRenderTarget(IRenderTarget target, Action a)
|
||||
{
|
||||
_clyde.RenderInRenderTarget((RenderTargetBase) target, a);
|
||||
}
|
||||
|
||||
public void SetScissor(UIBox2i? scissorBox)
|
||||
{
|
||||
_clyde.DrawSetScissor(scissorBox);
|
||||
|
||||
@@ -6,6 +6,7 @@ using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Log;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
// ReSharper disable once IdentifierTypo
|
||||
using RTCF = Robust.Client.Graphics.RenderTargetColorFormat;
|
||||
@@ -23,9 +24,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private readonly ConcurrentQueue<ClydeHandle> _renderTargetDisposeQueue
|
||||
= new();
|
||||
|
||||
IRenderWindow IClyde.MainWindowRenderTarget => _mainWindowRenderTarget;
|
||||
// Initialized in Clyde's constructor
|
||||
private readonly RenderWindow _mainWindowRenderTarget;
|
||||
private readonly RenderMainWindow _mainMainWindowRenderMainTarget;
|
||||
|
||||
// This is always kept up-to-date, except in CreateRenderTarget (because it restores the old value)
|
||||
// It is used for SRGB emulation.
|
||||
@@ -97,7 +97,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case RTCF.RG32F:
|
||||
case RTCF.R11FG11FB10F:
|
||||
case RTCF.Rgba16F:
|
||||
Logger.WarningS("clyde.ogl", "The framebuffer {0} [{1}] is trying to be floating-point when that's not supported. Forcing Rgba8.", name == null ? "[unnamed]" : name, size);
|
||||
_sawmillOgl.Warning("The framebuffer {0} [{1}] is trying to be floating-point when that's not supported. Forcing Rgba8.", name == null ? "[unnamed]" : name, size);
|
||||
colorFormat = RTCF.Rgba8;
|
||||
break;
|
||||
}
|
||||
@@ -191,7 +191,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
FramebufferHandle = fbo,
|
||||
Size = size,
|
||||
TextureHandle = textureObject.TextureId,
|
||||
MemoryPressure = pressure
|
||||
MemoryPressure = pressure,
|
||||
ColorFormat = format.ColorFormat
|
||||
};
|
||||
|
||||
//GC.AddMemoryPressure(pressure);
|
||||
@@ -266,10 +267,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWindowLoadedRtSize()
|
||||
private void UpdateMainWindowLoadedRtSize()
|
||||
{
|
||||
var loadedRt = RtToLoaded(_mainWindowRenderTarget);
|
||||
loadedRt.Size = _framebufferSize;
|
||||
var loadedRt = RtToLoaded(_mainMainWindowRenderMainTarget);
|
||||
loadedRt.Size = _windowing!.MainWindow!.FramebufferSize;
|
||||
}
|
||||
|
||||
private sealed class LoadedRenderTarget
|
||||
@@ -278,6 +279,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public Vector2i Size;
|
||||
public bool IsSrgb;
|
||||
|
||||
public RTCF ColorFormat;
|
||||
|
||||
// Remaining properties only apply if the render target is NOT a window.
|
||||
// Handle to the framebuffer object.
|
||||
public GLHandle FramebufferHandle;
|
||||
@@ -295,6 +298,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
protected readonly Clyde Clyde;
|
||||
private bool _disposed;
|
||||
|
||||
public bool MakeGLFence;
|
||||
public nint LastGLSync;
|
||||
|
||||
protected RenderTargetBase(Clyde clyde, ClydeHandle handle)
|
||||
{
|
||||
Clyde = clyde;
|
||||
@@ -302,6 +308,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
public abstract Vector2i Size { get; }
|
||||
|
||||
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
Clyde.CopyRenderTargetPixels(Handle, subRegion, callback);
|
||||
}
|
||||
|
||||
public ClydeHandle Handle { get; }
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
@@ -320,6 +332,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void DisposeDeferred()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
DisposeDeferredImpl();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void DisposeDeferredImpl()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
~RenderTargetBase()
|
||||
{
|
||||
Dispose(false);
|
||||
@@ -347,16 +376,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
else
|
||||
{
|
||||
Clyde._renderTargetDisposeQueue.Enqueue(Handle);
|
||||
DisposeDeferredImpl();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeDeferredImpl()
|
||||
{
|
||||
Clyde._renderTargetDisposeQueue.Enqueue(Handle);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RenderWindow : RenderTargetBase, IRenderWindow
|
||||
private sealed class RenderMainWindow : RenderTargetBase
|
||||
{
|
||||
public override Vector2i Size => Clyde._framebufferSize;
|
||||
public override Vector2i Size => Clyde._windowing!.MainWindow!.FramebufferSize;
|
||||
|
||||
public RenderWindow(Clyde clyde, ClydeHandle handle) : base(clyde, handle)
|
||||
public RenderMainWindow(Clyde clyde, ClydeHandle handle) : base(clyde, handle)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
|
||||
|
||||
@@ -120,9 +117,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
view = Matrix3.Identity;
|
||||
}
|
||||
|
||||
private static void CalcWorldMatrices(in Vector2i screenSize, IEye eye, out Matrix3 proj, out Matrix3 view)
|
||||
private static void CalcWorldMatrices(in Vector2i screenSize, in Vector2 renderScale, IEye eye,
|
||||
out Matrix3 proj, out Matrix3 view)
|
||||
{
|
||||
eye.GetViewMatrix(out view);
|
||||
eye.GetViewMatrix(out view, renderScale);
|
||||
|
||||
CalcWorldProjMatrix(screenSize, out proj);
|
||||
}
|
||||
@@ -266,8 +264,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void _drawQuad(Vector2 a, Vector2 b, in Matrix3 modelMatrix, GLShaderProgram program)
|
||||
{
|
||||
BindVertexArray(QuadVAO.Handle);
|
||||
DrawQuadWithVao(QuadVAO, a, b, modelMatrix, program);
|
||||
}
|
||||
|
||||
private void DrawQuadWithVao(GLHandle vao, Vector2 a, Vector2 b, in Matrix3 modelMatrix,
|
||||
GLShaderProgram program)
|
||||
{
|
||||
BindVertexArray(vao.Handle);
|
||||
CheckGlError();
|
||||
|
||||
var rectTransform = Matrix3.Identity;
|
||||
(rectTransform.R0C0, rectTransform.R1C1) = b - a;
|
||||
(rectTransform.R0C2, rectTransform.R1C2) = a;
|
||||
@@ -283,6 +288,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// Flushes the render handle, processing and re-pooling all the command lists.
|
||||
/// </summary>
|
||||
private void FlushRenderQueue()
|
||||
{
|
||||
FlushBatchQueue();
|
||||
|
||||
// Reset renderer state.
|
||||
_currentMatrixModel = Matrix3.Identity;
|
||||
_queuedShader = _defaultShader.Handle;
|
||||
SetScissorFull(null);
|
||||
}
|
||||
|
||||
private void FlushBatchQueue()
|
||||
{
|
||||
// Finish any batches that may have been WiP.
|
||||
BreakBatch();
|
||||
@@ -308,11 +323,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
ProcessRenderCommands();
|
||||
_queuedRenderCommands.Clear();
|
||||
|
||||
// Reset renderer state.
|
||||
_currentMatrixModel = Matrix3.Identity;
|
||||
_queuedShader = _defaultShader.Handle;
|
||||
SetScissorFull(null);
|
||||
}
|
||||
|
||||
private void SetScissorFull(UIBox2i? state)
|
||||
@@ -418,8 +428,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
//It's important to start at Texture6 here since DrawCommandBatch uses Texture0 and Texture1 immediately after calling this
|
||||
//function! If passing in textures as uniforms ever stops working it might be since someone made it use all the way up to Texture6 too.
|
||||
//Might change this in the future?
|
||||
TextureUnit cTarget = TextureUnit.Texture6+textureUnitVal;
|
||||
SetTexture(cTarget, ((ClydeTexture)clydeTexture).TextureId);
|
||||
TextureUnit cTarget = TextureUnit.Texture6 + textureUnitVal;
|
||||
SetTexture(cTarget, ((ClydeTexture) clydeTexture).TextureId);
|
||||
program.SetUniformTexture(name, cTarget);
|
||||
textureUnitVal++;
|
||||
break;
|
||||
@@ -500,6 +510,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr, in Color modulate,
|
||||
in Box2 texCoords)
|
||||
{
|
||||
EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount());
|
||||
EnsureBatchState(texture, in modulate, true, GetQuadBatchPrimitiveType(), _queuedShader);
|
||||
|
||||
bl = _currentMatrixModel.Transform(bl);
|
||||
@@ -525,6 +536,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
FinishBatch();
|
||||
_batchMetaData = null;
|
||||
|
||||
EnsureBatchSpaceAvailable(vertices.Length, indices.Length);
|
||||
|
||||
vertices.CopyTo(BatchVertexData.AsSpan(BatchVertexIndex));
|
||||
|
||||
// We are weaving this into the batch buffers for performance (and simplicity).
|
||||
@@ -566,6 +579,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
FinishBatch();
|
||||
_batchMetaData = null;
|
||||
|
||||
EnsureBatchSpaceAvailable(vertices.Length, 0);
|
||||
|
||||
vertices.CopyTo(BatchVertexData.AsSpan(BatchVertexIndex));
|
||||
|
||||
ref var command = ref AllocRenderCommand(RenderCommandType.DrawBatch);
|
||||
@@ -601,6 +616,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void DrawLine(Vector2 a, Vector2 b, Color color)
|
||||
{
|
||||
EnsureBatchSpaceAvailable(2, 0);
|
||||
EnsureBatchState(_stockTextureWhite.TextureId, color, false, BatchPrimitiveType.LineList, _queuedShader);
|
||||
|
||||
a = _currentMatrixModel.Transform(a);
|
||||
@@ -615,6 +631,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_debugStats.LastClydeDrawCalls += 1;
|
||||
}
|
||||
|
||||
private void EnsureBatchSpaceAvailable(int vtx, int idx)
|
||||
{
|
||||
if (BatchVertexIndex + vtx >= BatchVertexData.Length || BatchIndexIndex + idx > BatchIndexData.Length)
|
||||
{
|
||||
FlushBatchQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSetScissor(UIBox2i? scissorBox)
|
||||
{
|
||||
BreakBatch();
|
||||
@@ -773,116 +797,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_batchMetaData = null;
|
||||
}
|
||||
|
||||
private unsafe void TakeScreenshot(ScreenshotType type)
|
||||
{
|
||||
if (_queuedScreenshots.Count == 0 || _queuedScreenshots.All(p => p.type != type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var delegates = _queuedScreenshots.Where(p => p.type == type).ToList();
|
||||
|
||||
_queuedScreenshots.RemoveAll(p => p.type == type);
|
||||
|
||||
GL.PixelStore(PixelStoreParameter.PackAlignment, 1);
|
||||
CheckGlError();
|
||||
|
||||
var bufferLength = ScreenSize.X * ScreenSize.Y;
|
||||
if (!(_hasGLFenceSync && HasGLAnyMapBuffer && _hasGLPixelBufferObjects))
|
||||
{
|
||||
Logger.DebugS("clyde.ogl", "Necessary features for async screenshots not available, falling back to blocking path.");
|
||||
|
||||
// We need these 3 features to be able to do asynchronous screenshots, if we don't have them,
|
||||
// we'll have to fall back to a crappy synchronous stalling method of glReadPixels().
|
||||
|
||||
var buffer = new Rgba32[bufferLength];
|
||||
fixed (Rgba32* ptr = buffer)
|
||||
{
|
||||
var bufSize = sizeof(Rgba32) * bufferLength;
|
||||
GL.ReadnPixels(0, 0, ScreenSize.X, ScreenSize.Y, PixelFormat.Rgba, PixelType.UnsignedByte, bufSize,
|
||||
(IntPtr) ptr);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
var (w, h) = ScreenSize;
|
||||
|
||||
var image = new Image<Rgb24>(w, h);
|
||||
var imageSpan = image.GetPixelSpan();
|
||||
|
||||
FlipCopyScreenshot(buffer, imageSpan, w, h);
|
||||
|
||||
RunCallback(image);
|
||||
return;
|
||||
}
|
||||
|
||||
GL.GenBuffers(1, out uint pbo);
|
||||
CheckGlError();
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
|
||||
CheckGlError();
|
||||
GL.BufferData(BufferTarget.PixelPackBuffer, bufferLength * sizeof(Rgba32), IntPtr.Zero,
|
||||
BufferUsageHint.StreamRead);
|
||||
CheckGlError();
|
||||
GL.ReadPixels(0, 0, ScreenSize.X, ScreenSize.Y, PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
|
||||
CheckGlError();
|
||||
var fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
|
||||
CheckGlError();
|
||||
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
|
||||
CheckGlError();
|
||||
|
||||
_transferringScreenshots.Add((pbo, fence, ScreenSize, RunCallback));
|
||||
|
||||
void RunCallback(Image<Rgb24> image) => delegates.ForEach(p => p.callback(image));
|
||||
}
|
||||
|
||||
private unsafe void CheckTransferringScreenshots()
|
||||
{
|
||||
if (_transferringScreenshots.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var screenshot in _transferringScreenshots.ToList())
|
||||
{
|
||||
var (pbo, fence, (width, height), callback) = screenshot;
|
||||
|
||||
int status;
|
||||
GL.GetSync(fence, SyncParameterName.SyncStatus, sizeof(int), null, &status);
|
||||
CheckGlError();
|
||||
|
||||
if (status == (int) All.Signaled)
|
||||
{
|
||||
var bufLen = width * height;
|
||||
var bufSize = sizeof(Rgba32) * bufLen;
|
||||
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
|
||||
CheckGlError();
|
||||
var ptr = MapFullBuffer(BufferTarget.PixelPackBuffer, bufSize, BufferAccess.ReadOnly,
|
||||
BufferAccessMask.MapReadBit);
|
||||
|
||||
var packSpan = new ReadOnlySpan<Rgba32>((void*) ptr, width * height);
|
||||
|
||||
var image = new Image<Rgb24>(width, height);
|
||||
var imageSpan = image.GetPixelSpan();
|
||||
|
||||
FlipCopyScreenshot(packSpan, imageSpan, width, height);
|
||||
|
||||
UnmapBuffer(BufferTarget.PixelPackBuffer);
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
|
||||
CheckGlError();
|
||||
GL.DeleteBuffer(pbo);
|
||||
CheckGlError();
|
||||
GL.DeleteSync(fence);
|
||||
CheckGlError();
|
||||
|
||||
_transferringScreenshots.Remove(screenshot);
|
||||
|
||||
// TODO: Don't do unnecessary copy here.
|
||||
callback(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FullStoredRendererState PushRenderStateFull()
|
||||
{
|
||||
return new FullStoredRendererState(_currentMatrixProj, _currentMatrixView, _currentRenderTarget);
|
||||
@@ -913,7 +827,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_lightingReady = false;
|
||||
_currentMatrixModel = Matrix3.Identity;
|
||||
SetScissorFull(null);
|
||||
BindRenderTargetFull(_mainWindowRenderTarget);
|
||||
BindRenderTargetFull(_mainMainWindowRenderMainTarget);
|
||||
_batchMetaData = null;
|
||||
_queuedShader = _defaultShader.Handle;
|
||||
}
|
||||
@@ -927,6 +841,109 @@ namespace Robust.Client.Graphics.Clyde
|
||||
BlendingFactorDest.OneMinusSrcAlpha);
|
||||
}
|
||||
|
||||
private void BlitSecondaryWindows()
|
||||
{
|
||||
// Only got main window.
|
||||
if (_windowing!.AllWindows.Count == 1)
|
||||
return;
|
||||
|
||||
if (!_hasGLFenceSync && _cfg.GetCVar(CVars.DisplayForceSyncWindows))
|
||||
{
|
||||
GL.Finish();
|
||||
}
|
||||
|
||||
if (EffectiveThreadWindowBlit)
|
||||
{
|
||||
foreach (var window in _windowing.AllWindows)
|
||||
{
|
||||
if (window.IsMainWindow)
|
||||
continue;
|
||||
|
||||
window.BlitDoneEvent!.Reset();
|
||||
window.BlitStartEvent!.Set();
|
||||
window.BlitDoneEvent.Wait();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var window in _windowing.AllWindows)
|
||||
{
|
||||
if (window.IsMainWindow)
|
||||
continue;
|
||||
|
||||
_windowing.GLMakeContextCurrent(window);
|
||||
BlitThreadDoSecondaryWindowBlit(window);
|
||||
}
|
||||
|
||||
_windowing.GLMakeContextCurrent(_windowing.MainWindow!);
|
||||
}
|
||||
}
|
||||
|
||||
private void BlitThreadDoSecondaryWindowBlit(WindowReg window)
|
||||
{
|
||||
var rt = window.RenderTexture!;
|
||||
|
||||
if (_hasGLFenceSync)
|
||||
{
|
||||
// 0xFFFFFFFFFFFFFFFFUL is GL_TIMEOUT_IGNORED
|
||||
var sync = rt!.LastGLSync;
|
||||
GL.WaitSync(sync, WaitSyncFlags.None, unchecked((long) 0xFFFFFFFFFFFFFFFFUL));
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
GL.Viewport(0, 0, window.FramebufferSize.X, window.FramebufferSize.Y);
|
||||
CheckGlError();
|
||||
|
||||
SetTexture(TextureUnit.Texture0, window.RenderTexture!.Texture);
|
||||
|
||||
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
|
||||
CheckGlError();
|
||||
|
||||
window.BlitDoneEvent?.Set();
|
||||
_windowing!.WindowSwapBuffers(window);
|
||||
}
|
||||
|
||||
private void BlitThreadInit(WindowReg reg)
|
||||
{
|
||||
_windowing!.GLMakeContextCurrent(reg);
|
||||
_windowing.GLSwapInterval(0);
|
||||
|
||||
if (!_isGLES)
|
||||
GL.Enable(EnableCap.FramebufferSrgb);
|
||||
|
||||
var vao = GL.GenVertexArray();
|
||||
GL.BindVertexArray(vao);
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, WindowVBO.ObjectHandle);
|
||||
// Vertex Coords
|
||||
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
// Texture Coords.
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(1);
|
||||
|
||||
var program = _compileProgram(_winBlitShaderVert, _winBlitShaderFrag, new (string, uint)[]
|
||||
{
|
||||
("aPos", 0),
|
||||
("tCoord", 1),
|
||||
}, includeLib: false);
|
||||
|
||||
GL.UseProgram(program.Handle);
|
||||
var loc = GL.GetUniformLocation(program.Handle, "tex");
|
||||
SetTexture(TextureUnit.Texture0, reg.RenderTexture!.Texture);
|
||||
GL.Uniform1(loc, 0);
|
||||
}
|
||||
|
||||
private void FenceRenderTarget(RenderTargetBase rt)
|
||||
{
|
||||
if (!_hasGLFenceSync || !rt.MakeGLFence)
|
||||
return;
|
||||
|
||||
if (rt.LastGLSync != 0)
|
||||
GL.DeleteSync(rt.LastGLSync);
|
||||
|
||||
rt.LastGLSync = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct RenderCommand
|
||||
{
|
||||
|
||||
223
Robust.Client/Graphics/Clyde/Clyde.Screenshots.cs
Normal file
223
Robust.Client/Graphics/Clyde/Clyde.Screenshots.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
|
||||
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// Contains primary screenshot and pixel-copying logic.
|
||||
|
||||
internal sealed partial class Clyde
|
||||
{
|
||||
// Full-framebuffer screenshots undergo the following sequence of events:
|
||||
// 1. Screenshots are queued by content or whatever.
|
||||
// 2. When the rendering code reaches the screenshot type,
|
||||
// we instruct the GPU driver to copy the framebuffer and asynchronously transfer it to host memory.
|
||||
// 3. Transfer finished asynchronously, we invoke the callback.
|
||||
//
|
||||
// On RAW GLES2, we cannot do this asynchronously due to lacking GL features,
|
||||
// and the game will stutter as a result. This is sadly unavoidable.
|
||||
//
|
||||
// For CopyPixels on render targets, the copy and transfer is started immediately when the function is called.
|
||||
|
||||
private readonly List<QueuedScreenshot> _queuedScreenshots = new();
|
||||
private readonly List<TransferringPixelCopy> _transferringPixelCopies = new();
|
||||
|
||||
public void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback, UIBox2i? subRegion = null)
|
||||
{
|
||||
_queuedScreenshots.Add(new QueuedScreenshot(type, callback, subRegion));
|
||||
}
|
||||
|
||||
private void TakeScreenshot(ScreenshotType type)
|
||||
{
|
||||
if (_queuedScreenshots.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GL.PixelStore(PixelStoreParameter.PackAlignment, 1);
|
||||
CheckGlError();
|
||||
|
||||
for (var i = 0; i < _queuedScreenshots.Count; i++)
|
||||
{
|
||||
var (qType, callback, subRegion) = _queuedScreenshots[i];
|
||||
if (qType != type)
|
||||
continue;
|
||||
|
||||
DoCopyPixels(ScreenSize, subRegion, callback);
|
||||
_queuedScreenshots.RemoveSwap(i--);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyRenderTargetPixels<T>(
|
||||
ClydeHandle renderTarget,
|
||||
UIBox2i? subRegion,
|
||||
CopyPixelsDelegate<T> callback)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var loaded = _renderTargets[renderTarget];
|
||||
|
||||
var original = GL.GetInteger(GetPName.ReadFramebufferBinding);
|
||||
|
||||
GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, loaded.FramebufferHandle.Handle);
|
||||
|
||||
DoCopyPixels(loaded.Size, subRegion, callback);
|
||||
|
||||
GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, original);
|
||||
}
|
||||
|
||||
private unsafe void DoCopyPixels<T>(
|
||||
Vector2i fbSize,
|
||||
UIBox2i? subRegion,
|
||||
CopyPixelsDelegate<T> callback)
|
||||
where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var (pf, pt) = default(T) switch
|
||||
{
|
||||
Rgba32 => (PF.Rgba, PT.UnsignedByte),
|
||||
Rgb24 => (PF.Rgb, PT.UnsignedByte),
|
||||
_ => throw new ArgumentException("Unsupported pixel type.")
|
||||
};
|
||||
|
||||
var size = ClydeBase.ClampSubRegion(fbSize, subRegion);
|
||||
|
||||
var bufferLength = size.X * size.Y;
|
||||
if (!(_hasGLFenceSync && HasGLAnyMapBuffer && _hasGLPixelBufferObjects))
|
||||
{
|
||||
_sawmillOgl.Debug("clyde.ogl",
|
||||
"Necessary features for async screenshots not available, falling back to blocking path.");
|
||||
|
||||
// We need these 3 features to be able to do asynchronous screenshots, if we don't have them,
|
||||
// we'll have to fall back to a crappy synchronous stalling method of glReadnPixels().
|
||||
|
||||
var buffer = new T[bufferLength];
|
||||
fixed (T* ptr = buffer)
|
||||
{
|
||||
var bufSize = sizeof(T) * bufferLength;
|
||||
GL.ReadnPixels(
|
||||
0, 0,
|
||||
size.X, size.Y,
|
||||
pf, pt,
|
||||
bufSize,
|
||||
(nint) ptr);
|
||||
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
var image = new Image<T>(size.X, size.Y);
|
||||
var imageSpan = image.GetPixelSpan();
|
||||
|
||||
FlipCopy(buffer, imageSpan, size.X, size.Y);
|
||||
|
||||
callback(image);
|
||||
return;
|
||||
}
|
||||
|
||||
GL.GenBuffers(1, out uint pbo);
|
||||
CheckGlError();
|
||||
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
|
||||
CheckGlError();
|
||||
|
||||
GL.BufferData(
|
||||
BufferTarget.PixelPackBuffer,
|
||||
bufferLength * sizeof(Rgba32), IntPtr.Zero,
|
||||
BufferUsageHint.StreamRead);
|
||||
CheckGlError();
|
||||
|
||||
GL.ReadPixels(0, 0, size.X, size.Y, pf, pt, IntPtr.Zero);
|
||||
CheckGlError();
|
||||
|
||||
var fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
|
||||
CheckGlError();
|
||||
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
|
||||
CheckGlError();
|
||||
|
||||
var transferring = new TransferringPixelCopy(pbo, fence, size, FinishPixelTransfer<T>, callback);
|
||||
_transferringPixelCopies.Add(transferring);
|
||||
}
|
||||
|
||||
private unsafe void CheckTransferringScreenshots()
|
||||
{
|
||||
if (_transferringPixelCopies.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _transferringPixelCopies.Count; i++)
|
||||
{
|
||||
var transferring = _transferringPixelCopies[i];
|
||||
|
||||
// Check if transfer done (sync signalled)
|
||||
int status;
|
||||
GL.GetSync(transferring.Sync, SyncParameterName.SyncStatus, sizeof(int), null, &status);
|
||||
CheckGlError();
|
||||
|
||||
if (status != (int) All.Signaled)
|
||||
continue;
|
||||
|
||||
transferring.TransferContinue(transferring);
|
||||
_transferringPixelCopies.RemoveSwap(i--);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void FinishPixelTransfer<T>(TransferringPixelCopy transferring) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var (pbo, fence, (width, height), _, callback) = transferring;
|
||||
|
||||
var bufLen = width * height;
|
||||
var bufSize = sizeof(T) * bufLen;
|
||||
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
|
||||
CheckGlError();
|
||||
|
||||
var ptr = MapFullBuffer(
|
||||
BufferTarget.PixelPackBuffer,
|
||||
bufSize,
|
||||
BufferAccess.ReadOnly,
|
||||
BufferAccessMask.MapReadBit);
|
||||
|
||||
var packSpan = new ReadOnlySpan<T>(ptr, width * height);
|
||||
|
||||
var image = new Image<T>(width, height);
|
||||
var imageSpan = image.GetPixelSpan();
|
||||
|
||||
FlipCopy(packSpan, imageSpan, width, height);
|
||||
|
||||
UnmapBuffer(BufferTarget.PixelPackBuffer);
|
||||
|
||||
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
|
||||
CheckGlError();
|
||||
|
||||
GL.DeleteBuffer(pbo);
|
||||
CheckGlError();
|
||||
|
||||
GL.DeleteSync(fence);
|
||||
CheckGlError();
|
||||
|
||||
var castCallback = (CopyPixelsDelegate<T>) callback;
|
||||
castCallback(image);
|
||||
}
|
||||
|
||||
private sealed record QueuedScreenshot(
|
||||
ScreenshotType Type,
|
||||
CopyPixelsDelegate<Rgb24> Callback,
|
||||
UIBox2i? SubRegion);
|
||||
|
||||
private sealed record TransferringPixelCopy(
|
||||
uint Pbo,
|
||||
nint Sync,
|
||||
Vector2i Size,
|
||||
// Funny callback dance to handle the generics.
|
||||
Action<TransferringPixelCopy> TransferContinue,
|
||||
Delegate Callback);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private string _shaderWrapCodeRawFrag = default!;
|
||||
private string _shaderWrapCodeRawVert = default!;
|
||||
|
||||
private string _winBlitShaderVert = default!;
|
||||
private string _winBlitShaderFrag = default!;
|
||||
|
||||
private readonly Dictionary<ClydeHandle, LoadedShader> _loadedShaders =
|
||||
new();
|
||||
|
||||
@@ -117,6 +120,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_shaderWrapCodeRawVert = ReadEmbeddedShader("base-raw.vert");
|
||||
_shaderWrapCodeRawFrag = ReadEmbeddedShader("base-raw.frag");
|
||||
|
||||
_winBlitShaderVert = ReadEmbeddedShader("winblit.vert");
|
||||
_winBlitShaderFrag = ReadEmbeddedShader("winblit.frag");
|
||||
|
||||
var defaultLoadedShader = _resourceCache
|
||||
.GetResource<ShaderSourceResource>("/Shaders/Internal/default-sprite.swsl").ClydeHandle;
|
||||
|
||||
@@ -135,7 +141,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
private GLShaderProgram _compileProgram(string vertexSource, string fragmentSource,
|
||||
(string, uint)[] attribLocations, string? name = null)
|
||||
(string, uint)[] attribLocations, string? name = null, bool includeLib=true)
|
||||
{
|
||||
GLShader? vertexShader = null;
|
||||
GLShader? fragmentShader = null;
|
||||
@@ -172,8 +178,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
versionHeader += "#define HAS_UNIFORM_BUFFERS\n";
|
||||
}
|
||||
|
||||
vertexSource = versionHeader + "#define VERTEX_SHADER\n" + _shaderLibrary + vertexSource;
|
||||
fragmentSource = versionHeader + "#define FRAGMENT_SHADER\n" + _shaderLibrary + fragmentSource;
|
||||
var lib = includeLib ? _shaderLibrary : "";
|
||||
vertexSource = versionHeader + "#define VERTEX_SHADER\n" + lib + vertexSource;
|
||||
fragmentSource = versionHeader + "#define FRAGMENT_SHADER\n" + lib + fragmentSource;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
TextureLoadParameters? loadParams = null)
|
||||
{
|
||||
DebugTools.Assert(_mainThread == Thread.CurrentThread);
|
||||
DebugTools.Assert(_gameThread == Thread.CurrentThread);
|
||||
|
||||
// Load using Rgba32.
|
||||
using var image = Image.Load<Rgba32>(stream);
|
||||
@@ -40,7 +40,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
|
||||
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
DebugTools.Assert(_mainThread == Thread.CurrentThread);
|
||||
DebugTools.Assert(_gameThread == Thread.CurrentThread);
|
||||
|
||||
var actualParams = loadParams ?? TextureLoadParameters.Default;
|
||||
var pixelType = typeof(T);
|
||||
@@ -452,24 +452,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlipCopyScreenshot(ReadOnlySpan<Rgba32> srcSpan, Span<Rgb24> dstSpan, int w, int h)
|
||||
{
|
||||
var dr = h - 1;
|
||||
for (var r = 0; r < h; r++, dr--)
|
||||
{
|
||||
var si = r * w;
|
||||
var di = dr * w;
|
||||
var srcRow = srcSpan[si..(si + w)];
|
||||
var dstRow = dstSpan[di..(di + w)];
|
||||
|
||||
for (var x = 0; x < w; x++)
|
||||
{
|
||||
var src = srcRow[x];
|
||||
dstRow[x] = new Rgb24(src.R, src.G, src.B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Image<Rgba32> ApplyA8Swizzle(Image<A8> source)
|
||||
{
|
||||
var newImage = new Image<Rgba32>(source.Width, source.Height);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -10,7 +13,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private readonly Dictionary<ClydeHandle, WeakReference<Viewport>> _viewports =
|
||||
new();
|
||||
|
||||
private Viewport CreateViewport(Vector2i size, string? name = null)
|
||||
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
|
||||
{
|
||||
var handle = AllocRid();
|
||||
var viewport = new Viewport(handle, name, this)
|
||||
@@ -18,6 +21,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Size = size,
|
||||
RenderTarget = CreateRenderTarget(size,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
|
||||
sampleParameters: sampleParameters,
|
||||
name: $"{name}-MainRenderTarget")
|
||||
};
|
||||
|
||||
@@ -28,9 +32,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return viewport;
|
||||
}
|
||||
|
||||
IClydeViewport IClyde.CreateViewport(Vector2i size, string? name)
|
||||
IClydeViewport IClyde.CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters, string? name)
|
||||
{
|
||||
return CreateViewport(size, name);
|
||||
return CreateViewport(size, sampleParameters, name);
|
||||
}
|
||||
|
||||
private static Vector2 ScreenToMap(Vector2 point, Viewport vp)
|
||||
@@ -45,7 +49,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
point *= new Vector2(1, -1) / EyeManager.PixelsPerMeter;
|
||||
|
||||
// view matrix
|
||||
vp.Eye.GetViewMatrixInv(out var viewMatrixInv);
|
||||
vp.Eye.GetViewMatrixInv(out var viewMatrixInv, vp.RenderScale);
|
||||
point = viewMatrixInv * point;
|
||||
|
||||
return point;
|
||||
@@ -106,12 +110,66 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
public Vector2i Size { get; set; }
|
||||
public Vector2 RenderScale { get; set; } = Vector2.One;
|
||||
public bool AutomaticRender { get; set; }
|
||||
|
||||
void IClydeViewport.Render()
|
||||
{
|
||||
_clyde.RenderViewport(this);
|
||||
}
|
||||
|
||||
public MapCoordinates LocalToWorld(Vector2 point)
|
||||
{
|
||||
if (Eye == null)
|
||||
return default;
|
||||
|
||||
var newPoint = point;
|
||||
|
||||
// (inlined version of UiProjMatrix^-1)
|
||||
newPoint -= Size / 2f;
|
||||
newPoint *= new Vector2(1, -1) / EyeManager.PixelsPerMeter;
|
||||
|
||||
// view matrix
|
||||
Eye.GetViewMatrixInv(out var viewMatrixInv, RenderScale);
|
||||
newPoint = viewMatrixInv * newPoint;
|
||||
|
||||
return new MapCoordinates(newPoint, Eye.Position.MapId);
|
||||
}
|
||||
|
||||
public Vector2 WorldToLocal(Vector2 point)
|
||||
{
|
||||
if (Eye == null)
|
||||
return default;
|
||||
|
||||
var eye = (IEye) Eye;
|
||||
var newPoint = point;
|
||||
|
||||
eye.GetViewMatrix(out var viewMatrix, RenderScale);
|
||||
newPoint = viewMatrix * newPoint;
|
||||
|
||||
// (inlined version of UiProjMatrix)
|
||||
newPoint *= new Vector2(1, -1) * EyeManager.PixelsPerMeter;
|
||||
newPoint += Size / 2f;
|
||||
|
||||
return newPoint;
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysBelow(
|
||||
DrawingHandleScreen handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpaceBelowWorld, viewportBounds);
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysAbove(
|
||||
DrawingHandleScreen handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RenderTarget.Dispose();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -23,7 +21,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// <summary>
|
||||
/// Responsible for most things rendering on OpenGL mode.
|
||||
/// </summary>
|
||||
internal sealed partial class Clyde : ClydeBase, IClydeInternal, IClydeAudio, IPostInjectInit
|
||||
internal sealed partial class Clyde : IClydeInternal, IClydeAudio, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
@@ -35,6 +33,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
|
||||
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
|
||||
@@ -47,7 +46,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private GLBuffer QuadVBO = default!;
|
||||
private GLHandle QuadVAO;
|
||||
|
||||
private Viewport _mainViewport = default!;
|
||||
// VBO to blit to the window
|
||||
// VAO is per-window and not stored (not necessary!)
|
||||
private GLBuffer WindowVBO = default!;
|
||||
|
||||
private bool _drawingSplash = true;
|
||||
|
||||
@@ -59,18 +60,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private bool _checkGLErrors;
|
||||
|
||||
private readonly List<(ScreenshotType type, Action<Image<Rgb24>> callback)> _queuedScreenshots
|
||||
= new();
|
||||
private Thread? _gameThread;
|
||||
|
||||
private readonly List<(uint pbo, IntPtr sync, Vector2i size, Action<Image<Rgb24>> callback)>
|
||||
_transferringScreenshots
|
||||
= new();
|
||||
private ISawmill _sawmillOgl = default!;
|
||||
|
||||
public Clyde()
|
||||
{
|
||||
// Init main window render target.
|
||||
var windowRid = AllocRid();
|
||||
var window = new RenderWindow(this, windowRid);
|
||||
var window = new RenderMainWindow(this, windowRid);
|
||||
var loadedData = new LoadedRenderTarget
|
||||
{
|
||||
IsWindow = true,
|
||||
@@ -78,33 +76,54 @@ namespace Robust.Client.Graphics.Clyde
|
||||
};
|
||||
_renderTargets.Add(windowRid, loadedData);
|
||||
|
||||
_mainWindowRenderTarget = window;
|
||||
_mainMainWindowRenderMainTarget = window;
|
||||
_currentRenderTarget = RtToLoaded(window);
|
||||
_currentBoundRenderTarget = _currentRenderTarget;
|
||||
}
|
||||
|
||||
public override bool Initialize()
|
||||
public bool InitializePreWindowing()
|
||||
{
|
||||
base.Initialize();
|
||||
_sawmillOgl = Logger.GetSawmill("clyde.ogl");
|
||||
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
|
||||
_cfg.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
|
||||
// I can't be bothered to tear down and set these threads up in a cvar change handler.
|
||||
_threadWindowBlit = _cfg.GetCVar(CVars.DisplayThreadWindowBlit);
|
||||
|
||||
if (!InitWindowing())
|
||||
{
|
||||
return InitWindowing();
|
||||
}
|
||||
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
_gameThread = Thread.CurrentThread;
|
||||
if (!InitMainWindowAndRenderer())
|
||||
return false;
|
||||
}
|
||||
|
||||
_initializeAudio();
|
||||
ReloadConfig();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SeparateWindowThread => true;
|
||||
|
||||
public void EnterWindowLoop()
|
||||
{
|
||||
_windowing!.EnterWindowLoop();
|
||||
}
|
||||
|
||||
public void TerminateWindowLoop()
|
||||
{
|
||||
_windowing!.TerminateWindowLoop();
|
||||
}
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
_updateAudio();
|
||||
|
||||
FlushCursorDispose();
|
||||
_windowing?.FlushDispose();
|
||||
FlushShaderInstanceDispose();
|
||||
FlushRenderTargetDispose();
|
||||
FlushTextureDispose();
|
||||
@@ -121,21 +140,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public IClydeDebugInfo DebugInfo { get; private set; } = default!;
|
||||
public IClydeDebugStats DebugStats => _debugStats;
|
||||
|
||||
protected override void ReadConfig()
|
||||
{
|
||||
base.ReadConfig();
|
||||
_lightmapDivider = ConfigurationManager.GetCVar(CVars.DisplayLightMapDivider);
|
||||
_maxLightsPerScene = ConfigurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
|
||||
_enableSoftShadows = ConfigurationManager.GetCVar(CVars.DisplaySoftShadows);
|
||||
}
|
||||
|
||||
protected override void ReloadConfig()
|
||||
{
|
||||
base.ReloadConfig();
|
||||
|
||||
RegenAllLightRts();
|
||||
}
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_mapManager.TileChanged += _updateTileMapOnUpdate;
|
||||
@@ -148,15 +152,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
RegisterBlockCVars();
|
||||
}
|
||||
|
||||
public override event Action<WindowResizedEventArgs>? OnWindowResized;
|
||||
|
||||
public override event Action<WindowFocusedEventArgs>? OnWindowFocused;
|
||||
|
||||
public void Screenshot(ScreenshotType type, Action<Image<Rgb24>> callback)
|
||||
{
|
||||
_queuedScreenshots.Add((type, callback));
|
||||
}
|
||||
|
||||
private void InitOpenGL()
|
||||
{
|
||||
var vendor = GL.GetString(StringName.Vendor);
|
||||
@@ -165,16 +160,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var major = GL.GetInteger(GetPName.MajorVersion);
|
||||
var minor = GL.GetInteger(GetPName.MinorVersion);
|
||||
|
||||
Logger.DebugS("clyde.ogl", "OpenGL Vendor: {0}", vendor);
|
||||
Logger.DebugS("clyde.ogl", "OpenGL Renderer: {0}", renderer);
|
||||
Logger.DebugS("clyde.ogl", "OpenGL Version: {0}", version);
|
||||
_sawmillOgl.Debug("OpenGL Vendor: {0}", vendor);
|
||||
_sawmillOgl.Debug("OpenGL Renderer: {0}", renderer);
|
||||
_sawmillOgl.Debug("OpenGL Version: {0}", version);
|
||||
|
||||
var overrideVersion = ParseGLOverrideVersion();
|
||||
|
||||
if (overrideVersion != null)
|
||||
{
|
||||
(major, minor) = overrideVersion.Value;
|
||||
Logger.DebugS("clyde.ogl", "OVERRIDING detected GL version to: {0}.{1}", major, minor);
|
||||
_sawmillOgl.Debug("OVERRIDING detected GL version to: {0}.{1}", major, minor);
|
||||
}
|
||||
|
||||
DetectOpenGLFeatures(major, minor);
|
||||
@@ -201,7 +196,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
if (!HasGLAnyVertexArrayObjects)
|
||||
{
|
||||
Logger.WarningS("clyde.ogl", "NO VERTEX ARRAY OBJECTS! Things will probably go terribly, terribly wrong (no fallback path yet)");
|
||||
_sawmillOgl.Warning("NO VERTEX ARRAY OBJECTS! Things will probably go terribly, terribly wrong (no fallback path yet)");
|
||||
}
|
||||
|
||||
ResetBlendFunc();
|
||||
@@ -211,23 +206,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Primitive Restart's presence or lack thereof changes the amount of required memory.
|
||||
InitRenderingBatchBuffers();
|
||||
|
||||
Logger.DebugS("clyde.ogl", "Loading stock textures...");
|
||||
_sawmillOgl.Debug("Loading stock textures...");
|
||||
|
||||
LoadStockTextures();
|
||||
|
||||
Logger.DebugS("clyde.ogl", "Loading stock shaders...");
|
||||
_sawmillOgl.Debug("Loading stock shaders...");
|
||||
|
||||
LoadStockShaders();
|
||||
|
||||
Logger.DebugS("clyde.ogl", "Creating various GL objects...");
|
||||
_sawmillOgl.Debug("Creating various GL objects...");
|
||||
|
||||
CreateMiscGLObjects();
|
||||
|
||||
Logger.DebugS("clyde.ogl", "Setting up RenderHandle...");
|
||||
_sawmillOgl.Debug("Setting up RenderHandle...");
|
||||
|
||||
_renderHandle = new RenderHandle(this);
|
||||
|
||||
Logger.DebugS("clyde.ogl", "Setting viewport and rendering splash...");
|
||||
_sawmillOgl.Debug("Setting viewport and rendering splash...");
|
||||
|
||||
GL.Viewport(0, 0, ScreenSize.X, ScreenSize.Y);
|
||||
CheckGlError();
|
||||
@@ -238,7 +233,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private (int major, int minor)? ParseGLOverrideVersion()
|
||||
{
|
||||
var overrideGLVersion = ConfigurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
|
||||
var overrideGLVersion = _cfg.GetCVar(CVars.DisplayOGLOverrideVersion);
|
||||
if (string.IsNullOrEmpty(overrideGLVersion))
|
||||
{
|
||||
return null;
|
||||
@@ -247,14 +242,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var split = overrideGLVersion.Split(".");
|
||||
if (split.Length != 2)
|
||||
{
|
||||
Logger.WarningS("clyde.ogl", "display.ogl_override_version is in invalid format");
|
||||
_sawmillOgl.Warning("display.ogl_override_version is in invalid format");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!int.TryParse(split[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var major)
|
||||
|| !int.TryParse(split[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var minor))
|
||||
{
|
||||
Logger.WarningS("clyde.ogl", "display.ogl_override_version is in invalid format");
|
||||
_sawmillOgl.Warning("display.ogl_override_version is in invalid format");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -277,15 +272,27 @@ namespace Robust.Client.Graphics.Clyde
|
||||
quadVertices,
|
||||
nameof(QuadVBO));
|
||||
|
||||
QuadVAO = new GLHandle(GenVertexArray());
|
||||
BindVertexArray(QuadVAO.Handle);
|
||||
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, QuadVAO, nameof(QuadVAO));
|
||||
// Vertex Coords
|
||||
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
// Texture Coords.
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(1);
|
||||
QuadVAO = MakeQuadVao();
|
||||
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
// Window VBO
|
||||
{
|
||||
Span<Vertex2D> winVertices = stackalloc[]
|
||||
{
|
||||
new Vertex2D(-1, 1, 0, 1),
|
||||
new Vertex2D(-1, -1, 0, 0),
|
||||
new Vertex2D(1, 1, 1, 1),
|
||||
new Vertex2D(1, -1, 1, 0),
|
||||
};
|
||||
|
||||
WindowVBO = new GLBuffer<Vertex2D>(
|
||||
this,
|
||||
BufferTarget.ArrayBuffer,
|
||||
BufferUsageHint.StaticDraw,
|
||||
winVertices,
|
||||
nameof(WindowVBO));
|
||||
|
||||
CheckGlError();
|
||||
}
|
||||
@@ -314,23 +321,26 @@ namespace Robust.Client.Graphics.Clyde
|
||||
ProjViewUBO = new GLUniformBuffer<ProjViewMatrices>(this, BindingIndexProjView, nameof(ProjViewUBO));
|
||||
UniformConstantsUBO = new GLUniformBuffer<UniformConstants>(this, BindingIndexUniformConstants, nameof(UniformConstantsUBO));
|
||||
|
||||
CreateMainViewport();
|
||||
|
||||
screenBufferHandle = new GLHandle(GL.GenTexture());
|
||||
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
|
||||
ApplySampleParameters(TextureSampleParameters.Default);
|
||||
ScreenBufferTexture = GenTexture(screenBufferHandle, _framebufferSize, true, null, TexturePixelType.Rgba32);
|
||||
ScreenBufferTexture = GenTexture(screenBufferHandle, _windowing!.MainWindow!.FramebufferSize, true, null, TexturePixelType.Rgba32);
|
||||
}
|
||||
|
||||
private void CreateMainViewport()
|
||||
private GLHandle MakeQuadVao()
|
||||
{
|
||||
var (w, h) = _framebufferSize;
|
||||
var vao = new GLHandle(GenVertexArray());
|
||||
BindVertexArray(vao.Handle);
|
||||
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, nameof(QuadVAO));
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, QuadVBO.ObjectHandle);
|
||||
// Vertex Coords
|
||||
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
// Texture Coords.
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(1);
|
||||
|
||||
// Ensure viewport size is always even to avoid artifacts.
|
||||
if (w % 2 == 1) w += 1;
|
||||
if (h % 2 == 1) h += 1;
|
||||
|
||||
_mainViewport = CreateViewport((w, h), nameof(_mainViewport));
|
||||
return vao;
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
@@ -338,7 +348,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
if (!_hasGLKhrDebug)
|
||||
{
|
||||
Logger.DebugS("clyde.ogl", "KHR_debug not present, OpenGL debug logging not enabled.");
|
||||
_sawmillOgl.Debug("KHR_debug not present, OpenGL debug logging not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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;
|
||||
@@ -16,13 +19,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// Hey look, it's Clyde's evil twin brother!
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal sealed class ClydeHeadless : ClydeBase, IClydeInternal, IClydeAudio
|
||||
internal sealed class ClydeHeadless : IClydeInternal, IClydeAudio
|
||||
{
|
||||
// Would it make sense to report a fake resolution like 720p here so code doesn't break? idk.
|
||||
public IRenderWindow MainWindowRenderTarget { get; }
|
||||
public override Vector2i ScreenSize { get; } = (1280, 720);
|
||||
public IClydeWindow MainWindow { get; }
|
||||
public Vector2i ScreenSize => (1280, 720);
|
||||
public IEnumerable<IClydeWindow> AllWindows => _windows;
|
||||
public Vector2 DefaultWindowScale => (1, 1);
|
||||
public override bool IsFocused => true;
|
||||
public bool IsFocused => true;
|
||||
private readonly List<IClydeWindow> _windows = new();
|
||||
private int _nextWindowId = 2;
|
||||
|
||||
public ShaderInstance InstanceShader(ClydeHandle handle)
|
||||
{
|
||||
@@ -31,19 +37,25 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public ClydeHeadless()
|
||||
{
|
||||
MainWindowRenderTarget = new DummyRenderWindow(this);
|
||||
var mainRt = new DummyRenderWindow(this);
|
||||
var window = new DummyWindow(mainRt) {Id = new WindowId(1)};
|
||||
|
||||
_windows.Add(window);
|
||||
MainWindow = window;
|
||||
}
|
||||
|
||||
public Vector2 MouseScreenPosition => ScreenSize / 2;
|
||||
public ScreenCoordinates MouseScreenPosition => default;
|
||||
public IClydeDebugInfo DebugInfo { get; } = new DummyDebugInfo();
|
||||
public IClydeDebugStats DebugStats { get; } = new DummyDebugStats();
|
||||
|
||||
public event Action<TextEventArgs>? TextEntered;
|
||||
public event Action<MouseMoveEventArgs>? MouseMove;
|
||||
public event Action<KeyEventArgs>? KeyUp;
|
||||
public event Action<KeyEventArgs>? KeyDown;
|
||||
public event Action<MouseWheelEventArgs>? MouseWheel;
|
||||
public event Action<string>? CloseWindow;
|
||||
public event Action<TextEventArgs>? TextEntered { add { } remove { } }
|
||||
public event Action<MouseMoveEventArgs>? MouseMove { add { } remove { } }
|
||||
public event Action<MouseEnterLeaveEventArgs>? MouseEnterLeave { add { } remove { } }
|
||||
public event Action<KeyEventArgs>? KeyUp { add { } remove { } }
|
||||
public event Action<KeyEventArgs>? KeyDown { add { } remove { } }
|
||||
public event Action<MouseWheelEventArgs>? MouseWheel { add { } remove { } }
|
||||
public event Action<WindowClosedEventArgs>? CloseWindow { add { } remove { } }
|
||||
public event Action<WindowDestroyedEventArgs>? DestroyWindow { add { } remove { } }
|
||||
|
||||
public Texture GetStockTexture(ClydeStockTexture stockTexture)
|
||||
{
|
||||
@@ -53,8 +65,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public ClydeDebugLayers DebugLayers { get; set; }
|
||||
|
||||
public string GetKeyName(Keyboard.Key key) => string.Empty;
|
||||
public string GetKeyNameScanCode(int scanCode) => string.Empty;
|
||||
public int GetKeyScanCode(Keyboard.Key key) => default;
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
@@ -66,7 +76,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void SetWindowTitle(string title)
|
||||
public void SetWindowTitle(string title)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
@@ -81,25 +91,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public override bool Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override event Action<WindowResizedEventArgs> OnWindowResized
|
||||
public event Action<WindowResizedEventArgs> OnWindowResized
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public override event Action<WindowFocusedEventArgs> OnWindowFocused
|
||||
public event Action<WindowFocusedEventArgs> OnWindowFocused
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action OnWindowScaleChanged
|
||||
public event Action<WindowContentScaleEventArgs> OnWindowScaleChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
@@ -120,6 +124,28 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public bool SeparateWindowThread => false;
|
||||
|
||||
public bool InitializePreWindowing()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void TerminateWindowLoop()
|
||||
{
|
||||
throw new InvalidOperationException("ClydeHeadless does not use windowing threads");
|
||||
}
|
||||
|
||||
public void EnterWindowLoop()
|
||||
{
|
||||
throw new InvalidOperationException("ClydeHeadless does not use windowing threads");
|
||||
}
|
||||
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
TextureLoadParameters? loadParams = null)
|
||||
{
|
||||
@@ -165,14 +191,17 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void Screenshot(ScreenshotType type, Action<Image<Rgb24>> callback)
|
||||
public void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback, UIBox2i? subRegion = null)
|
||||
{
|
||||
callback(new Image<Rgb24>(ScreenSize.X, ScreenSize.Y));
|
||||
// Immediately call callback with an empty buffer.
|
||||
var (x, y) = ClydeBase.ClampSubRegion(ScreenSize, subRegion);
|
||||
callback(new Image<Rgb24>(x, y));
|
||||
}
|
||||
|
||||
public IClydeViewport CreateViewport(Vector2i size, string? name = null)
|
||||
public IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters,
|
||||
string? name = null)
|
||||
{
|
||||
return new Viewport();
|
||||
return new Viewport(size);
|
||||
}
|
||||
|
||||
public IEnumerable<IClydeMonitor> EnumerateMonitors()
|
||||
@@ -181,6 +210,17 @@ namespace Robust.Client.Graphics.Clyde
|
||||
yield break;
|
||||
}
|
||||
|
||||
public Task<IClydeWindow> CreateWindow(WindowCreateParameters parameters)
|
||||
{
|
||||
var window = new DummyWindow(CreateRenderTarget((123, 123), default))
|
||||
{
|
||||
Id = new WindowId(_nextWindowId++)
|
||||
};
|
||||
_windows.Add(window);
|
||||
|
||||
return Task.FromResult<IClydeWindow>(window);
|
||||
}
|
||||
|
||||
public ClydeHandle LoadShader(ParsedShader shader, string? name = null)
|
||||
{
|
||||
return default;
|
||||
@@ -208,6 +248,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return DummyAudioSource.Instance;
|
||||
@@ -218,9 +263,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return DummyBufferedAudioSource.Instance;
|
||||
}
|
||||
|
||||
public string GetText()
|
||||
public Task<string> GetText()
|
||||
{
|
||||
return string.Empty;
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
@@ -297,6 +342,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float masterVolumeDecay)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
|
||||
@@ -432,18 +482,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
public Vector2i Size { get; }
|
||||
public Texture Texture { get; }
|
||||
|
||||
public void Delete()
|
||||
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var (x, y) = ClydeBase.ClampSubRegion(Size, subRegion);
|
||||
callback(new Image<T>(x, y));
|
||||
}
|
||||
|
||||
public Texture Texture { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyRenderWindow : IRenderWindow
|
||||
private sealed class DummyRenderWindow : IRenderTarget
|
||||
{
|
||||
private readonly ClydeHeadless _clyde;
|
||||
|
||||
@@ -454,6 +507,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public Vector2i Size => _clyde.ScreenSize;
|
||||
|
||||
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var (x, y) = ClydeBase.ClampSubRegion(Size, subRegion);
|
||||
callback(new Image<T>(x, y));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
@@ -479,6 +538,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private sealed class Viewport : IClydeViewport
|
||||
{
|
||||
public Viewport(Vector2i size)
|
||||
{
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
@@ -488,9 +552,67 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public IEye? Eye { get; set; }
|
||||
public Vector2i Size { get; }
|
||||
public Vector2 RenderScale { get; set; }
|
||||
public bool AutomaticRender { get; set; }
|
||||
|
||||
public void Render()
|
||||
{
|
||||
// Nada
|
||||
}
|
||||
|
||||
public MapCoordinates LocalToWorld(Vector2 point)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
public Vector2 WorldToLocal(Vector2 point)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysBelow(
|
||||
DrawingHandleScreen handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
// Nada
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysAbove(
|
||||
DrawingHandleScreen handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
// Nada
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyWindow : IClydeWindow
|
||||
{
|
||||
public DummyWindow(IRenderTarget renderTarget)
|
||||
{
|
||||
RenderTarget = renderTarget;
|
||||
}
|
||||
|
||||
public Vector2i Size { get; } = default;
|
||||
public bool IsDisposed { get; private set; }
|
||||
public WindowId Id { get; set; }
|
||||
public IRenderTarget RenderTarget { get; }
|
||||
public string Title { get; set; } = "";
|
||||
public bool IsFocused => false;
|
||||
public bool IsMinimized => false;
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public Vector2 ContentScale => Vector2.One;
|
||||
public bool DisposeOnClose { get; set; }
|
||||
public event Action<WindowClosedEventArgs>? Closed { add { } remove { } }
|
||||
|
||||
public void MaximizeOnMonitor(IClydeMonitor monitor)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
private readonly sbyte?[] _uniformIntCache = new sbyte?[UniCount];
|
||||
private readonly Dictionary<string, int> _uniformCache = new();
|
||||
private uint _handle = 0;
|
||||
public uint Handle = 0;
|
||||
private GLShader? _fragmentShader;
|
||||
private GLShader? _vertexShader;
|
||||
public string? Name { get; }
|
||||
@@ -49,22 +49,22 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public void Link((string, uint)[] attribLocations)
|
||||
{
|
||||
ClearCaches();
|
||||
_handle = (uint) GL.CreateProgram();
|
||||
Handle = (uint) GL.CreateProgram();
|
||||
_clyde.CheckGlError();
|
||||
if (Name != null)
|
||||
{
|
||||
_clyde.ObjectLabelMaybe(ObjectLabelIdentifier.Program, _handle, Name);
|
||||
_clyde.ObjectLabelMaybe(ObjectLabelIdentifier.Program, Handle, Name);
|
||||
}
|
||||
|
||||
if (_vertexShader != null)
|
||||
{
|
||||
GL.AttachShader(_handle, _vertexShader.ObjectHandle);
|
||||
GL.AttachShader(Handle, _vertexShader.ObjectHandle);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
|
||||
if (_fragmentShader != null)
|
||||
{
|
||||
GL.AttachShader(_handle, _fragmentShader.ObjectHandle);
|
||||
GL.AttachShader(Handle, _fragmentShader.ObjectHandle);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
|
||||
@@ -74,45 +74,50 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// So we have to manually do it here.
|
||||
// Ugh.
|
||||
|
||||
GL.BindAttribLocation(_handle, loc, varName);
|
||||
GL.BindAttribLocation(Handle, loc, varName);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
|
||||
GL.LinkProgram(_handle);
|
||||
GL.LinkProgram(Handle);
|
||||
_clyde.CheckGlError();
|
||||
|
||||
GL.GetProgram(_handle, GetProgramParameterName.LinkStatus, out var compiled);
|
||||
GL.GetProgram(Handle, GetProgramParameterName.LinkStatus, out var compiled);
|
||||
_clyde.CheckGlError();
|
||||
if (compiled != 1)
|
||||
{
|
||||
throw new ShaderCompilationException(GL.GetProgramInfoLog((int) _handle));
|
||||
throw new ShaderCompilationException(GL.GetProgramInfoLog((int) Handle));
|
||||
}
|
||||
}
|
||||
|
||||
public void Use()
|
||||
{
|
||||
DebugTools.Assert(_handle != 0);
|
||||
|
||||
if (_clyde._currentProgram == this)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ForceUse();
|
||||
}
|
||||
|
||||
public void ForceUse()
|
||||
{
|
||||
DebugTools.Assert(Handle != 0);
|
||||
|
||||
_clyde._currentProgram = this;
|
||||
GL.UseProgram(_handle);
|
||||
GL.UseProgram(Handle);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
if (_handle == 0)
|
||||
if (Handle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GL.DeleteProgram(_handle);
|
||||
GL.DeleteProgram(Handle);
|
||||
_clyde.CheckGlError();
|
||||
_handle = 0;
|
||||
Handle = 0;
|
||||
}
|
||||
|
||||
public int GetUniform(string name)
|
||||
@@ -137,14 +142,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public bool TryGetUniform(string name, out int index)
|
||||
{
|
||||
DebugTools.Assert(_handle != 0);
|
||||
DebugTools.Assert(Handle != 0);
|
||||
|
||||
if (_uniformCache.TryGetValue(name, out index))
|
||||
{
|
||||
return index != -1;
|
||||
}
|
||||
|
||||
index = GL.GetUniformLocation(_handle, name);
|
||||
index = GL.GetUniformLocation(Handle, name);
|
||||
_clyde.CheckGlError();
|
||||
_uniformCache.Add(name, index);
|
||||
return index != -1;
|
||||
@@ -152,7 +157,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public bool TryGetUniform(int id, out int index)
|
||||
{
|
||||
DebugTools.Assert(_handle != 0);
|
||||
DebugTools.Assert(Handle != 0);
|
||||
DebugTools.Assert(id < UniCount);
|
||||
|
||||
var value = _uniformIntCache[id];
|
||||
@@ -192,7 +197,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
index = GL.GetUniformLocation(_handle, name);
|
||||
index = GL.GetUniformLocation(Handle, name);
|
||||
_clyde.CheckGlError();
|
||||
_uniformIntCache[id] = (sbyte)index;
|
||||
return index != -1;
|
||||
@@ -203,9 +208,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public void BindBlock(string blockName, uint blockBinding)
|
||||
{
|
||||
var index = (uint) GL.GetUniformBlockIndex(_handle, blockName);
|
||||
var index = (uint) GL.GetUniformBlockIndex(Handle, blockName);
|
||||
_clyde.CheckGlError();
|
||||
GL.UniformBlockBinding(_handle, index, blockBinding);
|
||||
GL.UniformBlockBinding(Handle, index, blockBinding);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private class GLUniformBuffer<T> where T : unmanaged, IAppliableUniformSet
|
||||
{
|
||||
private readonly Clyde _clyde;
|
||||
private readonly int _index;
|
||||
|
||||
/// <summary>
|
||||
/// GPU Buffer (only used when uniform buffers are available)
|
||||
@@ -37,13 +38,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public GLUniformBuffer(Clyde clyde, int index, string? name = null)
|
||||
{
|
||||
_clyde = clyde;
|
||||
_index = index;
|
||||
if (_clyde._hasGLUniformBuffers)
|
||||
{
|
||||
_implUBO = new GLBuffer(_clyde, BufferTarget.UniformBuffer, BufferUsageHint.StreamDraw, name);
|
||||
unsafe {
|
||||
_implUBO.Reallocate(sizeof(T));
|
||||
}
|
||||
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, index, (int) _implUBO.ObjectHandle);
|
||||
|
||||
Rebind();
|
||||
}
|
||||
}
|
||||
|
||||
public void Rebind()
|
||||
{
|
||||
if (_implUBO != null)
|
||||
{
|
||||
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, _index, (int) _implUBO!.ObjectHandle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
Robust.Client/Graphics/Clyde/Shaders/winblit.frag
Normal file
16
Robust.Client/Graphics/Clyde/Shaders/winblit.frag
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef HAS_VARYING_ATTRIBUTE
|
||||
#define texture2D texture
|
||||
#define varying in
|
||||
#define attribute in
|
||||
#define gl_FragColor colourOutput
|
||||
out highp vec4 colourOutput;
|
||||
#endif
|
||||
|
||||
varying highp vec2 UV;
|
||||
|
||||
uniform sampler2D tex;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_FragColor = texture2D(tex, UV);
|
||||
}
|
||||
20
Robust.Client/Graphics/Clyde/Shaders/winblit.vert
Normal file
20
Robust.Client/Graphics/Clyde/Shaders/winblit.vert
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef HAS_VARYING_ATTRIBUTE
|
||||
#define texture2D texture
|
||||
#define varying out
|
||||
#define attribute in
|
||||
#endif
|
||||
|
||||
|
||||
// Vertex position.
|
||||
/*layout (location = 0)*/ attribute vec2 aPos;
|
||||
// Texture coordinates.
|
||||
/*layout (location = 1)*/ attribute vec2 tCoord;
|
||||
|
||||
varying vec2 UV;
|
||||
|
||||
void main()
|
||||
{
|
||||
UV = tCoord;
|
||||
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
173
Robust.Client/Graphics/Clyde/Windowing/Glfw.Cursors.cs
Normal file
173
Robust.Client/Graphics/Clyde/Windowing/Glfw.Cursors.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed unsafe partial class GlfwWindowingImpl
|
||||
{
|
||||
private readonly Dictionary<ClydeHandle, WinThreadCursorReg> _winThreadCursors = new();
|
||||
private readonly Dictionary<StandardCursorShape, CursorImpl> _standardCursors = new();
|
||||
|
||||
public ICursor CursorGetStandard(StandardCursorShape shape)
|
||||
{
|
||||
return _standardCursors[shape];
|
||||
}
|
||||
|
||||
public ICursor CursorCreate(Image<Rgba32> image, Vector2i hotSpot)
|
||||
{
|
||||
var cloneImg = new Image<Rgba32>(image.Width, image.Height);
|
||||
image.GetPixelSpan().CopyTo(cloneImg.GetPixelSpan());
|
||||
|
||||
var id = _clyde.AllocRid();
|
||||
SendCmd(new CmdCursorCreate(cloneImg, hotSpot, id));
|
||||
|
||||
return new CursorImpl(this, id, false);
|
||||
}
|
||||
|
||||
private void WinThreadCursorCreate(CmdCursorCreate cmd)
|
||||
{
|
||||
var (img, (hotX, hotY), id) = cmd;
|
||||
|
||||
fixed (Rgba32* pixPtr = img.GetPixelSpan())
|
||||
{
|
||||
var gImg = new GlfwImage(img.Width, img.Height, (byte*) pixPtr);
|
||||
var ptr = GLFW.CreateCursor(gImg, hotX, hotY);
|
||||
|
||||
_winThreadCursors.Add(id, new WinThreadCursorReg {Ptr = ptr});
|
||||
}
|
||||
|
||||
img.Dispose();
|
||||
}
|
||||
|
||||
public void CursorSet(WindowReg window, ICursor? cursor)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (GlfwWindowReg) window;
|
||||
|
||||
if (reg.Cursor == cursor)
|
||||
{
|
||||
// Nothing has to be done!
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursor == null)
|
||||
{
|
||||
reg.Cursor = null;
|
||||
SendCmd(new CmdWinCursorSet((nint) reg.GlfwWindow, default));
|
||||
return;
|
||||
}
|
||||
|
||||
var impl = (CursorImpl) cursor;
|
||||
DebugTools.Assert(impl.Owner == this);
|
||||
|
||||
if (impl.Id == default)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(cursor));
|
||||
}
|
||||
|
||||
reg.Cursor = impl;
|
||||
SendCmd(new CmdWinCursorSet((nint) reg.GlfwWindow, impl.Id));
|
||||
}
|
||||
|
||||
private void WinThreadWinCursorSet(CmdWinCursorSet cmd)
|
||||
{
|
||||
var window = (Window*) cmd.Window;
|
||||
Cursor* ptr = null;
|
||||
if (cmd.Cursor != default)
|
||||
ptr = _winThreadCursors[cmd.Cursor].Ptr;
|
||||
|
||||
if (_win32Experience)
|
||||
{
|
||||
// Based on a true story.
|
||||
Thread.Sleep(15);
|
||||
}
|
||||
|
||||
GLFW.SetCursor(window, ptr);
|
||||
}
|
||||
|
||||
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
|
||||
{
|
||||
var cursorReg = _winThreadCursors[cmd.Cursor];
|
||||
|
||||
GLFW.DestroyCursor(cursorReg.Ptr);
|
||||
|
||||
_winThreadCursors.Remove(cmd.Cursor);
|
||||
}
|
||||
|
||||
private void InitCursors()
|
||||
{
|
||||
// Gets ran on window thread don't worry about it.
|
||||
|
||||
void AddStandardCursor(StandardCursorShape standardShape, CursorShape shape)
|
||||
{
|
||||
var id = _clyde.AllocRid();
|
||||
var ptr = GLFW.CreateStandardCursor(shape);
|
||||
|
||||
var impl = new CursorImpl(this, id, true);
|
||||
|
||||
_standardCursors.Add(standardShape, impl);
|
||||
_winThreadCursors.Add(id, new WinThreadCursorReg {Ptr = ptr});
|
||||
}
|
||||
|
||||
AddStandardCursor(StandardCursorShape.Arrow, CursorShape.Arrow);
|
||||
AddStandardCursor(StandardCursorShape.IBeam, CursorShape.IBeam);
|
||||
AddStandardCursor(StandardCursorShape.Crosshair, CursorShape.Crosshair);
|
||||
AddStandardCursor(StandardCursorShape.Hand, CursorShape.Hand);
|
||||
AddStandardCursor(StandardCursorShape.HResize, CursorShape.HResize);
|
||||
AddStandardCursor(StandardCursorShape.VResize, CursorShape.VResize);
|
||||
}
|
||||
|
||||
private sealed class CursorImpl : ICursor
|
||||
{
|
||||
private readonly bool _standard;
|
||||
public GlfwWindowingImpl Owner { get; }
|
||||
public ClydeHandle Id { get; private set; }
|
||||
|
||||
public CursorImpl(GlfwWindowingImpl clyde, ClydeHandle id, bool standard)
|
||||
{
|
||||
_standard = standard;
|
||||
Owner = clyde;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
~CursorImpl()
|
||||
{
|
||||
DisposeImpl();
|
||||
}
|
||||
|
||||
private void DisposeImpl()
|
||||
{
|
||||
Owner.SendCmd(new CmdCursorDestroy(Id));
|
||||
Id = default;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_standard)
|
||||
{
|
||||
throw new InvalidOperationException("Can't dispose standard cursor shape.");
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
DisposeImpl();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WinThreadCursorReg
|
||||
{
|
||||
public Cursor* Ptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
256
Robust.Client/Graphics/Clyde/Windowing/Glfw.Events.cs
Normal file
256
Robust.Client/Graphics/Clyde/Windowing/Glfw.Events.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
partial class Clyde
|
||||
{
|
||||
private partial class GlfwWindowingImpl
|
||||
{
|
||||
public void ProcessEvents(bool single=false)
|
||||
{
|
||||
while (_eventReader.TryRead(out var ev))
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessEvent(ev);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error(
|
||||
"clyde.win",
|
||||
$"Caught exception in windowing event ({ev.GetType()}):\n{e}");
|
||||
}
|
||||
|
||||
if (single)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Block waiting on the windowing -> game thread channel.
|
||||
// I swear to god do not use this unless you know what you are doing.
|
||||
private void WaitEvents()
|
||||
{
|
||||
_eventReader.WaitToReadAsync().AsTask().Wait();
|
||||
}
|
||||
|
||||
private void ProcessEvent(EventBase evb)
|
||||
{
|
||||
switch (evb)
|
||||
{
|
||||
case EventMouseButton mb:
|
||||
ProcessEventMouseButton(mb);
|
||||
break;
|
||||
case EventCursorPos cp:
|
||||
ProcessEventCursorPos(cp);
|
||||
break;
|
||||
case EventCursorEnter ev:
|
||||
ProcessEventCursorEnter(ev);
|
||||
break;
|
||||
case EventScroll s:
|
||||
ProcessEventScroll(s);
|
||||
break;
|
||||
case EventKey k:
|
||||
ProcessEventKey(k);
|
||||
break;
|
||||
case EventChar c:
|
||||
ProcessEventChar(c);
|
||||
break;
|
||||
case EventMonitorSetup ms:
|
||||
ProcessSetupMonitor(ms);
|
||||
break;
|
||||
case EventMonitorDestroy md:
|
||||
ProcessEventDestroyMonitor(md);
|
||||
break;
|
||||
case EventWindowCreate wCreate:
|
||||
FinishWindowCreate(wCreate);
|
||||
break;
|
||||
case EventWindowClose wc:
|
||||
ProcessEventWindowClose(wc);
|
||||
break;
|
||||
case EventWindowFocus wf:
|
||||
ProcessEventWindowFocus(wf);
|
||||
break;
|
||||
case EventWindowSize ws:
|
||||
ProcessEventWindowSize(ws);
|
||||
break;
|
||||
case EventWindowPos wp:
|
||||
ProcessEventWindowPos(wp);
|
||||
break;
|
||||
case EventWindowIconify wi:
|
||||
ProcessEventWindowIconify(wi);
|
||||
break;
|
||||
case EventWindowContentScale cs:
|
||||
ProcessEventWindowContentScale(cs);
|
||||
break;
|
||||
default:
|
||||
_sawmill.Error($"Unknown GLFW event type: {evb.GetType()}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessEventChar(EventChar ev)
|
||||
{
|
||||
_clyde.SendText(new TextEventArgs(ev.CodePoint));
|
||||
}
|
||||
|
||||
private void ProcessEventCursorPos(EventCursorPos ev)
|
||||
{
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
var newPos = ((float) ev.XPos, (float) ev.YPos) * windowReg.PixelRatio;
|
||||
var delta = newPos - windowReg.LastMousePos;
|
||||
windowReg.LastMousePos = newPos;
|
||||
|
||||
_clyde._currentHoveredWindow = windowReg;
|
||||
|
||||
_clyde.SendMouseMove(new MouseMoveEventArgs(delta, new ScreenCoordinates(newPos, windowReg.Id)));
|
||||
}
|
||||
|
||||
private void ProcessEventCursorEnter(EventCursorEnter ev)
|
||||
{
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
if (ev.Entered)
|
||||
{
|
||||
_clyde._currentHoveredWindow = windowReg;
|
||||
}
|
||||
else if (_clyde._currentHoveredWindow == windowReg)
|
||||
{
|
||||
_clyde._currentHoveredWindow = null;
|
||||
}
|
||||
|
||||
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(windowReg.Handle, ev.Entered));
|
||||
}
|
||||
|
||||
private void ProcessEventKey(EventKey ev)
|
||||
{
|
||||
EmitKeyEvent(ConvertGlfwKey(ev.Key), ev.Action, ev.Mods);
|
||||
}
|
||||
|
||||
private void EmitKeyEvent(Keyboard.Key key, InputAction action, KeyModifiers mods)
|
||||
{
|
||||
var shift = (mods & KeyModifiers.Shift) != 0;
|
||||
var alt = (mods & KeyModifiers.Alt) != 0;
|
||||
var control = (mods & KeyModifiers.Control) != 0;
|
||||
var system = (mods & KeyModifiers.Super) != 0;
|
||||
|
||||
var ev = new KeyEventArgs(
|
||||
key,
|
||||
action == InputAction.Repeat,
|
||||
alt, control, shift, system);
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case InputAction.Release:
|
||||
_clyde.SendKeyUp(ev);
|
||||
break;
|
||||
case InputAction.Press:
|
||||
case InputAction.Repeat:
|
||||
_clyde.SendKeyDown(ev);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(action), action, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessEventMouseButton(EventMouseButton ev)
|
||||
{
|
||||
EmitKeyEvent(Mouse.MouseButtonToKey(ConvertGlfwButton(ev.Button)), ev.Action, ev.Mods);
|
||||
}
|
||||
|
||||
private void ProcessEventScroll(EventScroll ev)
|
||||
{
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
var eventArgs = new MouseWheelEventArgs(
|
||||
((float) ev.XOffset, (float) ev.YOffset),
|
||||
new ScreenCoordinates(windowReg.LastMousePos, windowReg.Id));
|
||||
_clyde.SendScroll(eventArgs);
|
||||
}
|
||||
|
||||
private void ProcessEventWindowClose(EventWindowClose ev)
|
||||
{
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
_clyde.SendCloseWindow(windowReg, new WindowClosedEventArgs(windowReg.Handle));
|
||||
}
|
||||
|
||||
private void ProcessEventWindowSize(EventWindowSize ev)
|
||||
{
|
||||
var window = ev.Window;
|
||||
var width = ev.Width;
|
||||
var height = ev.Height;
|
||||
var fbW = ev.FramebufferWidth;
|
||||
var fbH = ev.FramebufferHeight;
|
||||
|
||||
var windowReg = FindWindow(window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
var oldSize = windowReg.FramebufferSize;
|
||||
windowReg.FramebufferSize = (fbW, fbH);
|
||||
windowReg.WindowSize = (width, height);
|
||||
|
||||
if (fbW == 0 || fbH == 0 || width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
windowReg.PixelRatio = windowReg.FramebufferSize / windowReg.WindowSize;
|
||||
|
||||
_clyde.SendWindowResized(windowReg, oldSize);
|
||||
}
|
||||
|
||||
private void ProcessEventWindowPos(EventWindowPos ev)
|
||||
{
|
||||
var window = ev.Window;
|
||||
var x = ev.X;
|
||||
var y = ev.Y;
|
||||
|
||||
var windowReg = FindWindow(window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
windowReg.WindowPos = (x, y);
|
||||
}
|
||||
|
||||
private void ProcessEventWindowContentScale(EventWindowContentScale ev)
|
||||
{
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
windowReg.WindowScale = (ev.XScale, ev.YScale);
|
||||
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
|
||||
}
|
||||
|
||||
private void ProcessEventWindowIconify(EventWindowIconify ev)
|
||||
{
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
windowReg.IsMinimized = ev.Iconified;
|
||||
}
|
||||
|
||||
private void ProcessEventWindowFocus(EventWindowFocus ev)
|
||||
{
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
windowReg.IsFocused = ev.Focused;
|
||||
_clyde.SendWindowFocus(new WindowFocusedEventArgs(ev.Focused, windowReg.Handle));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
225
Robust.Client/Graphics/Clyde/Windowing/Glfw.Keys.cs
Normal file
225
Robust.Client/Graphics/Clyde/Windowing/Glfw.Keys.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Localization;
|
||||
using GlfwKey = OpenToolkit.GraphicsLibraryFramework.Keys;
|
||||
using GlfwButton = OpenToolkit.GraphicsLibraryFramework.MouseButton;
|
||||
using static Robust.Client.Input.Mouse;
|
||||
using static Robust.Client.Input.Keyboard;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class GlfwWindowingImpl
|
||||
{
|
||||
// TODO: to avoid having to ask the windowing thread, key names are cached.
|
||||
// This means they don't update correctly if the user switches keyboard mode. RIP.
|
||||
|
||||
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
|
||||
|
||||
private void InitKeyMap()
|
||||
{
|
||||
// From GLFW's source code: this is the actual list of "printable" keys
|
||||
// that GetKeyName returns something for.
|
||||
CacheKey(Keys.KeyPadEqual);
|
||||
for (var k = Keys.KeyPad0; k <= Keys.KeyPadAdd; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
for (var k = Keys.Apostrophe; k <= Keys.World2; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
void CacheKey(GlfwKey key)
|
||||
{
|
||||
var rKey = ConvertGlfwKey(key);
|
||||
if (rKey == Key.Unknown)
|
||||
return;
|
||||
|
||||
var name = GLFW.GetKeyName(key, 0);
|
||||
_printableKeyNameMap.Add(rKey, name);
|
||||
}
|
||||
}
|
||||
|
||||
public string KeyGetName(Keyboard.Key key)
|
||||
{
|
||||
if (_printableKeyNameMap.TryGetValue(key, out var name))
|
||||
{
|
||||
var textInfo = Thread.CurrentThread.CurrentCulture.TextInfo;
|
||||
return textInfo.ToTitleCase(name);
|
||||
}
|
||||
|
||||
name = Keyboard.GetSpecialKeyName(key, _loc);
|
||||
if (name != null)
|
||||
{
|
||||
return Loc.GetString(name);
|
||||
}
|
||||
|
||||
return Loc.GetString("<unknown key>");
|
||||
}
|
||||
|
||||
public static Button ConvertGlfwButton(GlfwButton button)
|
||||
{
|
||||
return MouseButtonMap[button];
|
||||
}
|
||||
|
||||
private static readonly Dictionary<GlfwButton, Button> MouseButtonMap = new()
|
||||
{
|
||||
{GlfwButton.Left, Button.Left},
|
||||
{GlfwButton.Middle, Button.Middle},
|
||||
{GlfwButton.Right, Button.Right},
|
||||
{GlfwButton.Button4, Button.Button4},
|
||||
{GlfwButton.Button5, Button.Button5},
|
||||
{GlfwButton.Button6, Button.Button6},
|
||||
{GlfwButton.Button7, Button.Button7},
|
||||
{GlfwButton.Button8, Button.Button8},
|
||||
};
|
||||
|
||||
private static readonly Dictionary<GlfwKey, Key> KeyMap;
|
||||
private static readonly Dictionary<Key, GlfwKey> KeyMapReverse;
|
||||
|
||||
|
||||
internal static Key ConvertGlfwKey(GlfwKey key)
|
||||
{
|
||||
if (KeyMap.TryGetValue(key, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return Key.Unknown;
|
||||
}
|
||||
|
||||
internal static GlfwKey ConvertGlfwKeyReverse(Key key)
|
||||
{
|
||||
if (KeyMapReverse.TryGetValue(key, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return GlfwKey.Unknown;
|
||||
}
|
||||
|
||||
static GlfwWindowingImpl()
|
||||
{
|
||||
KeyMap = new Dictionary<GlfwKey, Key>
|
||||
{
|
||||
{GlfwKey.A, Key.A},
|
||||
{GlfwKey.B, Key.B},
|
||||
{GlfwKey.C, Key.C},
|
||||
{GlfwKey.D, Key.D},
|
||||
{GlfwKey.E, Key.E},
|
||||
{GlfwKey.F, Key.F},
|
||||
{GlfwKey.G, Key.G},
|
||||
{GlfwKey.H, Key.H},
|
||||
{GlfwKey.I, Key.I},
|
||||
{GlfwKey.J, Key.J},
|
||||
{GlfwKey.K, Key.K},
|
||||
{GlfwKey.L, Key.L},
|
||||
{GlfwKey.M, Key.M},
|
||||
{GlfwKey.N, Key.N},
|
||||
{GlfwKey.O, Key.O},
|
||||
{GlfwKey.P, Key.P},
|
||||
{GlfwKey.Q, Key.Q},
|
||||
{GlfwKey.R, Key.R},
|
||||
{GlfwKey.S, Key.S},
|
||||
{GlfwKey.T, Key.T},
|
||||
{GlfwKey.U, Key.U},
|
||||
{GlfwKey.V, Key.V},
|
||||
{GlfwKey.W, Key.W},
|
||||
{GlfwKey.X, Key.X},
|
||||
{GlfwKey.Y, Key.Y},
|
||||
{GlfwKey.Z, Key.Z},
|
||||
{GlfwKey.D0, Key.Num0},
|
||||
{GlfwKey.D1, Key.Num1},
|
||||
{GlfwKey.D2, Key.Num2},
|
||||
{GlfwKey.D3, Key.Num3},
|
||||
{GlfwKey.D4, Key.Num4},
|
||||
{GlfwKey.D5, Key.Num5},
|
||||
{GlfwKey.D6, Key.Num6},
|
||||
{GlfwKey.D7, Key.Num7},
|
||||
{GlfwKey.D8, Key.Num8},
|
||||
{GlfwKey.D9, Key.Num9},
|
||||
{GlfwKey.KeyPad0, Key.NumpadNum0},
|
||||
{GlfwKey.KeyPad1, Key.NumpadNum1},
|
||||
{GlfwKey.KeyPad2, Key.NumpadNum2},
|
||||
{GlfwKey.KeyPad3, Key.NumpadNum3},
|
||||
{GlfwKey.KeyPad4, Key.NumpadNum4},
|
||||
{GlfwKey.KeyPad5, Key.NumpadNum5},
|
||||
{GlfwKey.KeyPad6, Key.NumpadNum6},
|
||||
{GlfwKey.KeyPad7, Key.NumpadNum7},
|
||||
{GlfwKey.KeyPad8, Key.NumpadNum8},
|
||||
{GlfwKey.KeyPad9, Key.NumpadNum9},
|
||||
{GlfwKey.Escape, Key.Escape},
|
||||
{GlfwKey.LeftControl, Key.Control},
|
||||
{GlfwKey.RightControl, Key.Control},
|
||||
{GlfwKey.RightShift, Key.Shift},
|
||||
{GlfwKey.LeftShift, Key.Shift},
|
||||
{GlfwKey.LeftAlt, Key.Alt},
|
||||
{GlfwKey.RightAlt, Key.Alt},
|
||||
{GlfwKey.LeftSuper, Key.LSystem},
|
||||
{GlfwKey.RightSuper, Key.RSystem},
|
||||
{GlfwKey.Menu, Key.Menu},
|
||||
{GlfwKey.LeftBracket, Key.LBracket},
|
||||
{GlfwKey.RightBracket, Key.RBracket},
|
||||
{GlfwKey.Semicolon, Key.SemiColon},
|
||||
{GlfwKey.Comma, Key.Comma},
|
||||
{GlfwKey.Period, Key.Period},
|
||||
{GlfwKey.Apostrophe, Key.Apostrophe},
|
||||
{GlfwKey.Slash, Key.Slash},
|
||||
{GlfwKey.Backslash, Key.BackSlash},
|
||||
{GlfwKey.GraveAccent, Key.Tilde},
|
||||
{GlfwKey.Equal, Key.Equal},
|
||||
{GlfwKey.Space, Key.Space},
|
||||
{GlfwKey.Enter, Key.Return},
|
||||
{GlfwKey.KeyPadEnter, Key.NumpadEnter},
|
||||
{GlfwKey.Backspace, Key.BackSpace},
|
||||
{GlfwKey.Tab, Key.Tab},
|
||||
{GlfwKey.PageUp, Key.PageUp},
|
||||
{GlfwKey.PageDown, Key.PageDown},
|
||||
{GlfwKey.End, Key.End},
|
||||
{GlfwKey.Home, Key.Home},
|
||||
{GlfwKey.Insert, Key.Insert},
|
||||
{GlfwKey.Delete, Key.Delete},
|
||||
{GlfwKey.Minus, Key.Minus},
|
||||
{GlfwKey.KeyPadAdd, Key.NumpadAdd},
|
||||
{GlfwKey.KeyPadSubtract, Key.NumpadSubtract},
|
||||
{GlfwKey.KeyPadDivide, Key.NumpadDivide},
|
||||
{GlfwKey.KeyPadMultiply, Key.NumpadMultiply},
|
||||
{GlfwKey.KeyPadDecimal, Key.NumpadDecimal},
|
||||
{GlfwKey.Left, Key.Left},
|
||||
{GlfwKey.Right, Key.Right},
|
||||
{GlfwKey.Up, Key.Up},
|
||||
{GlfwKey.Down, Key.Down},
|
||||
{GlfwKey.F1, Key.F1},
|
||||
{GlfwKey.F2, Key.F2},
|
||||
{GlfwKey.F3, Key.F3},
|
||||
{GlfwKey.F4, Key.F4},
|
||||
{GlfwKey.F5, Key.F5},
|
||||
{GlfwKey.F6, Key.F6},
|
||||
{GlfwKey.F7, Key.F7},
|
||||
{GlfwKey.F8, Key.F8},
|
||||
{GlfwKey.F9, Key.F9},
|
||||
{GlfwKey.F10, Key.F10},
|
||||
{GlfwKey.F11, Key.F11},
|
||||
{GlfwKey.F12, Key.F12},
|
||||
{GlfwKey.F13, Key.F13},
|
||||
{GlfwKey.F14, Key.F14},
|
||||
{GlfwKey.F15, Key.F15},
|
||||
{GlfwKey.Pause, Key.Pause},
|
||||
};
|
||||
|
||||
KeyMapReverse = new Dictionary<Key, GlfwKey>();
|
||||
|
||||
foreach (var (key, value) in KeyMap)
|
||||
{
|
||||
KeyMapReverse[value] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Robust.Client/Graphics/Clyde/Windowing/Glfw.Monitors.cs
Normal file
133
Robust.Client/Graphics/Clyde/Windowing/Glfw.Monitors.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Shared.Utility;
|
||||
using GlfwVideoMode = OpenToolkit.GraphicsLibraryFramework.VideoMode;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed unsafe partial class GlfwWindowingImpl
|
||||
{
|
||||
// TODO: GLFW doesn't have any events for complex monitor config changes,
|
||||
// so we need some way to reload stuff if e.g. the primary monitor changes.
|
||||
// Still better than SDL2 though which doesn't acknowledge monitor changes at all.
|
||||
|
||||
// Monitors are created at GLFW's will,
|
||||
// so we need to make SURE monitors keep existing while operating on them.
|
||||
// because, you know, async. Don't want a use-after-free.
|
||||
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
|
||||
|
||||
// Can't use ClydeHandle because it's 64 bit.
|
||||
private int _nextMonitorId = 1;
|
||||
private int _primaryMonitorId;
|
||||
private readonly Dictionary<int, GlfwMonitorReg> _monitors = new();
|
||||
|
||||
private void InitMonitors()
|
||||
{
|
||||
var monitors = GLFW.GetMonitorsRaw(out var count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
WinThreadSetupMonitor(monitors[i]);
|
||||
}
|
||||
|
||||
var primaryMonitor = GLFW.GetPrimaryMonitor();
|
||||
var up = GLFW.GetMonitorUserPointer(primaryMonitor);
|
||||
_primaryMonitorId = (int) up;
|
||||
|
||||
ProcessEvents();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WinThreadSetupMonitor(Monitor* monitor)
|
||||
{
|
||||
var id = _nextMonitorId++;
|
||||
|
||||
DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null,
|
||||
"GLFW window already has user pointer??");
|
||||
|
||||
var name = GLFW.GetMonitorName(monitor);
|
||||
var videoMode = GLFW.GetVideoMode(monitor);
|
||||
var modesPtr = GLFW.GetVideoModesRaw(monitor, out var modeCount);
|
||||
var modes = new VideoMode[modeCount];
|
||||
for (var i = 0; i < modes.Length; i++)
|
||||
{
|
||||
modes[i] = ConvertVideoMode(modesPtr[i]);
|
||||
}
|
||||
|
||||
GLFW.SetMonitorUserPointer(monitor, (void*) id);
|
||||
|
||||
_winThreadMonitors.Add(id, new WinThreadMonitorReg {Ptr = monitor});
|
||||
|
||||
SendEvent(new EventMonitorSetup(id, name, ConvertVideoMode(*videoMode), modes));
|
||||
}
|
||||
|
||||
private static VideoMode ConvertVideoMode(in GlfwVideoMode mode)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Width = (ushort) mode.Width,
|
||||
Height = (ushort) mode.Height,
|
||||
RedBits = (byte) mode.RedBits,
|
||||
RefreshRate = (ushort) mode.RefreshRate,
|
||||
GreenBits = (byte) mode.GreenBits,
|
||||
BlueBits = (byte) mode.BlueBits,
|
||||
};
|
||||
}
|
||||
|
||||
private void ProcessSetupMonitor(EventMonitorSetup ev)
|
||||
{
|
||||
var impl = new MonitorHandle(
|
||||
ev.Id,
|
||||
ev.Name,
|
||||
(ev.CurrentMode.Width, ev.CurrentMode.Height),
|
||||
ev.CurrentMode.RefreshRate,
|
||||
ev.AllModes);
|
||||
|
||||
_clyde._monitorHandles.Add(impl);
|
||||
_monitors[ev.Id] = new GlfwMonitorReg
|
||||
{
|
||||
Id = ev.Id,
|
||||
Handle = impl
|
||||
};
|
||||
}
|
||||
|
||||
private void WinThreadDestroyMonitor(Monitor* monitor)
|
||||
{
|
||||
var ptr = (int) GLFW.GetMonitorUserPointer(monitor);
|
||||
|
||||
if (ptr == 0)
|
||||
{
|
||||
var name = GLFW.GetMonitorName(monitor);
|
||||
_sawmill.Warning($"Monitor '{name}' had no user pointer set??");
|
||||
return;
|
||||
}
|
||||
|
||||
_winThreadMonitors.Remove(ptr);
|
||||
|
||||
GLFW.SetMonitorUserPointer(monitor, null);
|
||||
|
||||
SendEvent(new EventMonitorDestroy(ptr));
|
||||
}
|
||||
|
||||
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
|
||||
{
|
||||
var reg = _monitors[ev.Id];
|
||||
_monitors.Remove(ev.Id);
|
||||
_clyde._monitorHandles.Remove(reg.Handle);
|
||||
}
|
||||
|
||||
private sealed class GlfwMonitorReg : MonitorReg
|
||||
{
|
||||
public int Id;
|
||||
}
|
||||
|
||||
private sealed class WinThreadMonitorReg
|
||||
{
|
||||
public Monitor* Ptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
223
Robust.Client/Graphics/Clyde/Windowing/Glfw.RawEvents.cs
Normal file
223
Robust.Client/Graphics/Clyde/Windowing/Glfw.RawEvents.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using System.Threading.Tasks;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
partial class Clyde
|
||||
{
|
||||
private unsafe partial class GlfwWindowingImpl
|
||||
{
|
||||
// Keep delegates around to prevent GC issues.
|
||||
private GLFWCallbacks.ErrorCallback? _errorCallback;
|
||||
private GLFWCallbacks.MonitorCallback? _monitorCallback;
|
||||
private GLFWCallbacks.CharCallback? _charCallback;
|
||||
private GLFWCallbacks.CursorPosCallback? _cursorPosCallback;
|
||||
private GLFWCallbacks.CursorEnterCallback? _cursorEnterCallback;
|
||||
private GLFWCallbacks.KeyCallback? _keyCallback;
|
||||
private GLFWCallbacks.MouseButtonCallback? _mouseButtonCallback;
|
||||
private GLFWCallbacks.ScrollCallback? _scrollCallback;
|
||||
private GLFWCallbacks.WindowCloseCallback? _windowCloseCallback;
|
||||
private GLFWCallbacks.WindowPosCallback? _windowPosCallback;
|
||||
private GLFWCallbacks.WindowSizeCallback? _windowSizeCallback;
|
||||
private GLFWCallbacks.WindowContentScaleCallback? _windowContentScaleCallback;
|
||||
private GLFWCallbacks.WindowIconifyCallback? _windowIconifyCallback;
|
||||
private GLFWCallbacks.WindowFocusCallback? _windowFocusCallback;
|
||||
|
||||
private void StoreCallbacks()
|
||||
{
|
||||
_errorCallback = OnGlfwError;
|
||||
_monitorCallback = OnGlfwMonitor;
|
||||
_charCallback = OnGlfwChar;
|
||||
_cursorPosCallback = OnGlfwCursorPos;
|
||||
_cursorEnterCallback = OnGlfwCursorEnter;
|
||||
_keyCallback = OnGlfwKey;
|
||||
_mouseButtonCallback = OnGlfwMouseButton;
|
||||
_scrollCallback = OnGlfwScroll;
|
||||
_windowCloseCallback = OnGlfwWindowClose;
|
||||
_windowSizeCallback = OnGlfwWindowSize;
|
||||
_windowPosCallback = OnGlfwWindowPos;
|
||||
_windowContentScaleCallback = OnGlfwWindowContentScale;
|
||||
_windowIconifyCallback = OnGlfwWindowIconify;
|
||||
_windowFocusCallback = OnGlfwWindowFocus;
|
||||
}
|
||||
|
||||
private void SetupGlobalCallbacks()
|
||||
{
|
||||
GLFW.SetMonitorCallback(_monitorCallback);
|
||||
}
|
||||
|
||||
private void OnGlfwMonitor(Monitor* monitor, ConnectedState state)
|
||||
{
|
||||
if (state == ConnectedState.Connected)
|
||||
WinThreadSetupMonitor(monitor);
|
||||
else
|
||||
WinThreadDestroyMonitor(monitor);
|
||||
}
|
||||
|
||||
private void OnGlfwChar(Window* window, uint codepoint)
|
||||
{
|
||||
SendEvent(new EventChar((nint) window, codepoint));
|
||||
}
|
||||
|
||||
private void OnGlfwCursorPos(Window* window, double x, double y)
|
||||
{
|
||||
// System.Console.WriteLine($"{(nint)window:X16}: {x},{y}");
|
||||
SendEvent(new EventCursorPos((nint) window, x, y));
|
||||
}
|
||||
|
||||
private void OnGlfwCursorEnter(Window* window, bool entered)
|
||||
{
|
||||
// System.Console.WriteLine($"{(nint)window:X16}: {entered}");
|
||||
SendEvent(new EventCursorEnter((nint) window, entered));
|
||||
}
|
||||
|
||||
private void OnGlfwKey(Window* window, Keys key, int scanCode, InputAction action, KeyModifiers mods)
|
||||
{
|
||||
SendEvent(new EventKey((nint) window, key, scanCode, action, mods));
|
||||
}
|
||||
|
||||
private void OnGlfwMouseButton(Window* window, MouseButton button, InputAction action, KeyModifiers mods)
|
||||
{
|
||||
SendEvent(new EventMouseButton((nint) window, button, action, mods));
|
||||
}
|
||||
|
||||
private void OnGlfwScroll(Window* window, double offsetX, double offsetY)
|
||||
{
|
||||
SendEvent(new EventScroll((nint) window, offsetX, offsetY));
|
||||
}
|
||||
|
||||
private void OnGlfwWindowClose(Window* window)
|
||||
{
|
||||
SendEvent(new EventWindowClose((nint) window));
|
||||
}
|
||||
|
||||
private void OnGlfwWindowSize(Window* window, int width, int height)
|
||||
{
|
||||
GLFW.GetFramebufferSize(window, out var fbW, out var fbH);
|
||||
SendEvent(new EventWindowSize((nint) window, width, height, fbW, fbH));
|
||||
}
|
||||
|
||||
private void OnGlfwWindowPos(Window* window, int x, int y)
|
||||
{
|
||||
SendEvent(new EventWindowPos((nint) window, x, y));
|
||||
}
|
||||
|
||||
private void OnGlfwWindowContentScale(Window* window, float xScale, float yScale)
|
||||
{
|
||||
SendEvent(new EventWindowContentScale((nint) window, xScale, yScale));
|
||||
}
|
||||
|
||||
private void OnGlfwWindowIconify(Window* window, bool iconified)
|
||||
{
|
||||
SendEvent(new EventWindowIconify((nint) window, iconified));
|
||||
}
|
||||
|
||||
private void OnGlfwWindowFocus(Window* window, bool focused)
|
||||
{
|
||||
SendEvent(new EventWindowFocus((nint) window, focused));
|
||||
}
|
||||
|
||||
// NOTE: events do not correspond 1:1 to GLFW events
|
||||
// This is because they need to pack all the data required
|
||||
// for the game-thread event handling.
|
||||
|
||||
private abstract record EventBase;
|
||||
|
||||
private record EventMouseButton(
|
||||
nint Window,
|
||||
MouseButton Button,
|
||||
InputAction Action,
|
||||
KeyModifiers Mods
|
||||
) : EventBase;
|
||||
|
||||
private record EventCursorPos(
|
||||
nint Window,
|
||||
double XPos,
|
||||
double YPos
|
||||
) : EventBase;
|
||||
|
||||
private record EventCursorEnter(
|
||||
nint Window,
|
||||
bool Entered
|
||||
) : EventBase;
|
||||
|
||||
private record EventScroll(
|
||||
nint Window,
|
||||
double XOffset,
|
||||
double YOffset
|
||||
) : EventBase;
|
||||
|
||||
private record EventKey(
|
||||
nint Window,
|
||||
Keys Key,
|
||||
int ScanCode,
|
||||
InputAction Action,
|
||||
KeyModifiers Mods
|
||||
) : EventBase;
|
||||
|
||||
private record EventChar
|
||||
(
|
||||
nint Window,
|
||||
uint CodePoint
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowClose
|
||||
(
|
||||
nint Window
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowCreate(
|
||||
GlfwWindowCreateResult Result,
|
||||
TaskCompletionSource<GlfwWindowCreateResult> Tcs
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowSize
|
||||
(
|
||||
nint Window,
|
||||
int Width,
|
||||
int Height,
|
||||
int FramebufferWidth,
|
||||
int FramebufferHeight
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowPos
|
||||
(
|
||||
nint Window,
|
||||
int X,
|
||||
int Y
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowContentScale
|
||||
(
|
||||
nint Window,
|
||||
float XScale,
|
||||
float YScale
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowIconify
|
||||
(
|
||||
nint Window,
|
||||
bool Iconified
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowFocus
|
||||
(
|
||||
nint Window,
|
||||
bool Focused
|
||||
) : EventBase;
|
||||
|
||||
private record EventMonitorSetup
|
||||
(
|
||||
int Id,
|
||||
string Name,
|
||||
VideoMode CurrentMode,
|
||||
VideoMode[] AllModes
|
||||
) : EventBase;
|
||||
|
||||
private record EventMonitorDestroy
|
||||
(
|
||||
int Id
|
||||
) : EventBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
250
Robust.Client/Graphics/Clyde/Windowing/Glfw.WindowThread.cs
Normal file
250
Robust.Client/Graphics/Clyde/Windowing/Glfw.WindowThread.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class GlfwWindowingImpl
|
||||
{
|
||||
// glfwPostEmptyEvent is broken on macOS and crashes when not called from the main thread
|
||||
// (despite what the docs claim, and yes this makes it useless).
|
||||
// Because of this, we just forego it and use glfwWaitEventsTimeout on macOS instead.
|
||||
private static readonly bool IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
|
||||
private bool _windowingRunning;
|
||||
private ChannelWriter<CmdBase> _cmdWriter = default!;
|
||||
private ChannelReader<CmdBase> _cmdReader = default!;
|
||||
|
||||
private ChannelReader<EventBase> _eventReader = default!;
|
||||
private ChannelWriter<EventBase> _eventWriter = default!;
|
||||
|
||||
//
|
||||
// Let it be forever recorded that I started work on windowing thread separation
|
||||
// because win32 SetCursor was taking 15ms spinwaiting inside the kernel.
|
||||
//
|
||||
|
||||
//
|
||||
// To avoid stutters and solve some other problems like smooth window resizing,
|
||||
// we (by default) use a separate thread for windowing.
|
||||
//
|
||||
// Types like WindowReg are considered to be part of the "game" thread
|
||||
// and should **NOT** be directly updated/accessed from the windowing thread.
|
||||
//
|
||||
// Got that?
|
||||
//
|
||||
|
||||
//
|
||||
// The windowing -> game channel is bounded so that the OS properly detects the game as locked
|
||||
// up when it actually locks up. The other way around is not bounded to avoid deadlocks.
|
||||
// This also means that all operations like clipboard reading, window creation, etc....
|
||||
// have to be asynchronous.
|
||||
//
|
||||
|
||||
public void EnterWindowLoop()
|
||||
{
|
||||
_windowingRunning = true;
|
||||
|
||||
while (_windowingRunning)
|
||||
{
|
||||
if (IsMacOS)
|
||||
GLFW.WaitEventsTimeout(0.008);
|
||||
else
|
||||
GLFW.WaitEvents();
|
||||
|
||||
while (_cmdReader.TryRead(out var cmd))
|
||||
{
|
||||
ProcessGlfwCmd(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessGlfwCmd(CmdBase cmdb)
|
||||
{
|
||||
switch (cmdb)
|
||||
{
|
||||
case CmdTerminate:
|
||||
_windowingRunning = false;
|
||||
break;
|
||||
|
||||
case CmdWinSetTitle cmd:
|
||||
WinThreadWinSetTitle(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetMonitor cmd:
|
||||
WinThreadWinSetMonitor(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetVisible cmd:
|
||||
WinThreadWinSetVisible(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinRequestAttention cmd:
|
||||
WinThreadWinRequestAttention(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetFullscreen cmd:
|
||||
WinThreadWinSetFullscreen(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinCreate cmd:
|
||||
WinThreadWinCreate(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinDestroy cmd:
|
||||
WinThreadWinDestroy(cmd);
|
||||
break;
|
||||
|
||||
case CmdSetClipboard cmd:
|
||||
WinThreadSetClipboard(cmd);
|
||||
break;
|
||||
|
||||
case CmdGetClipboard cmd:
|
||||
WinThreadGetClipboard(cmd);
|
||||
break;
|
||||
|
||||
case CmdCursorCreate cmd:
|
||||
WinThreadCursorCreate(cmd);
|
||||
break;
|
||||
|
||||
case CmdCursorDestroy cmd:
|
||||
WinThreadCursorDestroy(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinCursorSet cmd:
|
||||
WinThreadWinCursorSet(cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void TerminateWindowLoop()
|
||||
{
|
||||
SendCmd(new CmdTerminate());
|
||||
}
|
||||
|
||||
private void InitChannels()
|
||||
{
|
||||
var cmdChannel = Channel.CreateUnbounded<CmdBase>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true,
|
||||
// Finalizers can write to this in some cases.
|
||||
SingleWriter = false
|
||||
});
|
||||
|
||||
_cmdReader = cmdChannel.Reader;
|
||||
_cmdWriter = cmdChannel.Writer;
|
||||
|
||||
var bufferSize = _cfg.GetCVar(CVars.DisplayInputBufferSize);
|
||||
var eventChannel = Channel.CreateBounded<EventBase>(new BoundedChannelOptions(bufferSize)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = true,
|
||||
SingleWriter = true
|
||||
});
|
||||
|
||||
_eventReader = eventChannel.Reader;
|
||||
_eventWriter = eventChannel.Writer;
|
||||
}
|
||||
|
||||
private void SendCmd(CmdBase cmd)
|
||||
{
|
||||
_cmdWriter.TryWrite(cmd);
|
||||
|
||||
// Post empty event to unstuck WaitEvents if necessary.
|
||||
if (!IsMacOS)
|
||||
GLFW.PostEmptyEvent();
|
||||
}
|
||||
|
||||
private void SendEvent(EventBase ev)
|
||||
{
|
||||
var task = _eventWriter.WriteAsync(ev);
|
||||
|
||||
if (!task.IsCompletedSuccessfully)
|
||||
{
|
||||
task.AsTask().Wait();
|
||||
}
|
||||
}
|
||||
|
||||
private abstract record CmdBase;
|
||||
|
||||
private sealed record CmdTerminate : CmdBase;
|
||||
|
||||
private sealed record CmdWinSetTitle(
|
||||
nint Window,
|
||||
string Title
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinSetMonitor(
|
||||
nint Window,
|
||||
int MonitorId,
|
||||
int X, int Y,
|
||||
int W, int H,
|
||||
int RefreshRate
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinMaximize(
|
||||
nint Window
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinSetFullscreen(
|
||||
nint Window
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinSetVisible(
|
||||
nint Window,
|
||||
bool Visible
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinRequestAttention(
|
||||
nint Window
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinCreate(
|
||||
Renderer Renderer,
|
||||
WindowCreateParameters Parameters,
|
||||
nint ShareWindow,
|
||||
TaskCompletionSource<GlfwWindowCreateResult> Tcs
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinDestroy(
|
||||
nint Window
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record GlfwWindowCreateResult(
|
||||
GlfwWindowReg? Reg,
|
||||
(string Desc, ErrorCode Code)? Error
|
||||
);
|
||||
|
||||
private sealed record CmdSetClipboard(
|
||||
nint Window,
|
||||
string Text
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdGetClipboard(
|
||||
nint Window,
|
||||
TaskCompletionSource<string> Tcs
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinCursorSet(
|
||||
nint Window,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdCursorCreate(
|
||||
Image<Rgba32> Bytes,
|
||||
Vector2i Hotspot,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdCursorDestroy(
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
687
Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs
Normal file
687
Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs
Normal file
@@ -0,0 +1,687 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using OpenToolkit;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image;
|
||||
using Monitor = OpenToolkit.GraphicsLibraryFramework.Monitor;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal partial class Clyde
|
||||
{
|
||||
// Wait for it.
|
||||
private sealed partial class GlfwWindowingImpl
|
||||
{
|
||||
private readonly List<GlfwWindowReg> _windows = new();
|
||||
|
||||
public IReadOnlyList<WindowReg> AllWindows => _windows;
|
||||
public IBindingsContext GraphicsBindingContext => _mainGraphicsContext;
|
||||
|
||||
public WindowReg? MainWindow => _mainWindow;
|
||||
private GlfwWindowReg? _mainWindow;
|
||||
private GlfwBindingsContext _mainGraphicsContext = default!;
|
||||
private int _nextWindowId = 1;
|
||||
|
||||
public async Task<WindowHandle> WindowCreate(WindowCreateParameters parameters)
|
||||
{
|
||||
// tfw await not allowed in unsafe contexts
|
||||
|
||||
// GL APIs don't take kindly to making a new window without unbinding the main context. Great.
|
||||
// Leaving code for async path in, in case it works on like GLX.
|
||||
var unbindContextAndBlock = true;
|
||||
|
||||
DebugTools.AssertNotNull(_mainWindow);
|
||||
|
||||
Task<GlfwWindowCreateResult> task;
|
||||
unsafe
|
||||
{
|
||||
if (unbindContextAndBlock)
|
||||
GLFW.MakeContextCurrent(null);
|
||||
|
||||
task = SharedWindowCreate(
|
||||
_clyde._chosenRenderer,
|
||||
parameters,
|
||||
_mainWindow!.GlfwWindow);
|
||||
}
|
||||
|
||||
if (unbindContextAndBlock)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
// Block the main thread (to avoid stuff like texture uploads being problematic).
|
||||
WaitWindowCreate(task);
|
||||
|
||||
if (unbindContextAndBlock)
|
||||
GLFW.MakeContextCurrent(_mainWindow.GlfwWindow);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await task;
|
||||
}
|
||||
|
||||
var (reg, error) = await task;
|
||||
|
||||
if (reg == null)
|
||||
{
|
||||
var (desc, errCode) = error!.Value;
|
||||
throw new GlfwException($"{errCode}: {desc}");
|
||||
}
|
||||
|
||||
_clyde.CreateWindowRenderTexture(reg);
|
||||
_clyde.InitWindowBlitThread(reg);
|
||||
|
||||
unsafe
|
||||
{
|
||||
GLFW.MakeContextCurrent(_mainWindow.GlfwWindow);
|
||||
}
|
||||
|
||||
return reg.Handle;
|
||||
}
|
||||
}
|
||||
|
||||
// Yes, you read that right.
|
||||
private sealed unsafe partial class GlfwWindowingImpl
|
||||
{
|
||||
public bool TryInitMainWindow(Renderer renderer, [NotNullWhen(false)] out string? error)
|
||||
{
|
||||
var width = _cfg.GetCVar(CVars.DisplayWidth);
|
||||
var height = _cfg.GetCVar(CVars.DisplayHeight);
|
||||
var prevWidth = width;
|
||||
var prevHeight = height;
|
||||
|
||||
IClydeMonitor? monitor = null;
|
||||
var fullscreen = false;
|
||||
|
||||
if (_clyde._windowMode == WindowMode.Fullscreen)
|
||||
{
|
||||
monitor = _monitors[_primaryMonitorId].Handle;
|
||||
width = monitor.Size.X;
|
||||
height = monitor.Size.Y;
|
||||
fullscreen = true;
|
||||
}
|
||||
|
||||
var parameters = new WindowCreateParameters
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
Monitor = monitor,
|
||||
Fullscreen = fullscreen
|
||||
};
|
||||
|
||||
var windowTask = SharedWindowCreate(renderer, parameters, null);
|
||||
WaitWindowCreate(windowTask);
|
||||
|
||||
var (reg, err) = windowTask.Result;
|
||||
if (reg == null)
|
||||
{
|
||||
var (desc, code) = err!.Value;
|
||||
error = $"[{code}] {desc}";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
DebugTools.Assert(reg.Id == WindowId.Main);
|
||||
|
||||
_mainWindow = reg;
|
||||
reg.IsMainWindow = true;
|
||||
|
||||
if (fullscreen)
|
||||
{
|
||||
reg.PrevWindowSize = (prevWidth, prevHeight);
|
||||
reg.PrevWindowPos = (50, 50);
|
||||
}
|
||||
|
||||
UpdateVSync();
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void WaitWindowCreate(Task<GlfwWindowCreateResult> windowTask)
|
||||
{
|
||||
while (!windowTask.IsCompleted)
|
||||
{
|
||||
// Keep processing events until the window task gives either an error or success.
|
||||
WaitEvents();
|
||||
ProcessEvents(single: true);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<GlfwWindowCreateResult> SharedWindowCreate(
|
||||
Renderer renderer,
|
||||
WindowCreateParameters parameters,
|
||||
Window* share)
|
||||
{
|
||||
// Yes we ping-pong this TCS through the window thread and back, deal with it.
|
||||
var tcs = new TaskCompletionSource<GlfwWindowCreateResult>();
|
||||
SendCmd(new CmdWinCreate(
|
||||
renderer,
|
||||
parameters,
|
||||
(nint) share,
|
||||
tcs));
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private void FinishWindowCreate(EventWindowCreate ev)
|
||||
{
|
||||
var (res, tcs) = ev;
|
||||
var reg = res.Reg;
|
||||
|
||||
if (reg != null)
|
||||
{
|
||||
_windows.Add(reg);
|
||||
_clyde._windowHandles.Add(reg.Handle);
|
||||
}
|
||||
|
||||
tcs.TrySetResult(res);
|
||||
}
|
||||
|
||||
private void WinThreadWinCreate(CmdWinCreate cmd)
|
||||
{
|
||||
var (renderer, parameters, share, tcs) = cmd;
|
||||
|
||||
var window = CreateGlfwWindowForRenderer(renderer, parameters, (Window*) share);
|
||||
|
||||
if (window == null)
|
||||
{
|
||||
var err = GLFW.GetError(out var desc);
|
||||
|
||||
SendEvent(new EventWindowCreate(new GlfwWindowCreateResult(null, (desc, err)), tcs));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't invoke the TCS directly from the windowing thread because:
|
||||
// * it'd hit the synchronization context,
|
||||
// which would make (blocking) main window init more annoying.
|
||||
// * it'd not be synchronized to other incoming window events correctly which might be icky.
|
||||
// So we send the TCS back to the game thread
|
||||
// which processes events in the correct order and has better control of stuff during init.
|
||||
var reg = WinThreadSetupWindow(window);
|
||||
|
||||
SendEvent(new EventWindowCreate(new GlfwWindowCreateResult(reg, null), tcs));
|
||||
}
|
||||
|
||||
private void WinThreadWinDestroy(CmdWinDestroy cmd)
|
||||
{
|
||||
GLFW.DestroyWindow((Window*) cmd.Window);
|
||||
}
|
||||
|
||||
public void WindowSetTitle(WindowReg window, string title)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
if (title == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(title));
|
||||
}
|
||||
|
||||
var reg = (GlfwWindowReg) window;
|
||||
|
||||
SendCmd(new CmdWinSetTitle((nint) reg.GlfwWindow, title));
|
||||
}
|
||||
|
||||
private void WinThreadWinSetTitle(CmdWinSetTitle cmd)
|
||||
{
|
||||
GLFW.SetWindowTitle((Window*) cmd.Window, cmd.Title);
|
||||
}
|
||||
|
||||
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var winReg = (GlfwWindowReg) window;
|
||||
|
||||
var monitorImpl = (MonitorHandle) monitor;
|
||||
|
||||
SendCmd(new CmdWinSetMonitor(
|
||||
(nint) winReg.GlfwWindow,
|
||||
monitorImpl.Id,
|
||||
0, 0,
|
||||
monitorImpl.Size.X, monitorImpl.Size.Y,
|
||||
monitorImpl.RefreshRate));
|
||||
}
|
||||
|
||||
private void WinThreadWinSetMonitor(CmdWinSetMonitor cmd)
|
||||
{
|
||||
Monitor* monitorPtr;
|
||||
if (cmd.MonitorId == 0)
|
||||
{
|
||||
monitorPtr = null;
|
||||
}
|
||||
else if (_winThreadMonitors.TryGetValue(cmd.MonitorId, out var monitorReg))
|
||||
{
|
||||
monitorPtr = monitorReg.Ptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GLFW.SetWindowMonitor(
|
||||
(Window*) cmd.Window,
|
||||
monitorPtr,
|
||||
cmd.X, cmd.Y,
|
||||
cmd.W, cmd.H,
|
||||
cmd.RefreshRate
|
||||
);
|
||||
}
|
||||
|
||||
public void WindowSetVisible(WindowReg window, bool visible)
|
||||
{
|
||||
var reg = (GlfwWindowReg) window;
|
||||
reg.IsVisible = visible;
|
||||
|
||||
SendCmd(new CmdWinSetVisible((nint) reg.GlfwWindow, visible));
|
||||
}
|
||||
|
||||
private void WinThreadWinSetVisible(CmdWinSetVisible cmd)
|
||||
{
|
||||
var win = (Window*) cmd.Window;
|
||||
|
||||
if (cmd.Visible)
|
||||
{
|
||||
GLFW.ShowWindow(win);
|
||||
}
|
||||
else
|
||||
{
|
||||
GLFW.HideWindow(win);
|
||||
}
|
||||
}
|
||||
|
||||
public void WindowRequestAttention(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (GlfwWindowReg) window;
|
||||
|
||||
SendCmd(new CmdWinRequestAttention((nint) reg.GlfwWindow));
|
||||
}
|
||||
|
||||
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
|
||||
{
|
||||
var win = (Window*) cmd.Window;
|
||||
|
||||
GLFW.RequestWindowAttention(win);
|
||||
}
|
||||
|
||||
public void WindowSwapBuffers(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (GlfwWindowReg) window;
|
||||
|
||||
GLFW.SwapBuffers(reg.GlfwWindow);
|
||||
}
|
||||
|
||||
public void UpdateVSync()
|
||||
{
|
||||
if (_mainWindow == null)
|
||||
return;
|
||||
|
||||
GLFW.MakeContextCurrent(_mainWindow!.GlfwWindow);
|
||||
GLFW.SwapInterval(_clyde._vSync ? 1 : 0);
|
||||
}
|
||||
|
||||
public void UpdateMainWindowMode()
|
||||
{
|
||||
if (_mainWindow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var win = _mainWindow;
|
||||
if (_clyde._windowMode == WindowMode.Fullscreen)
|
||||
{
|
||||
_mainWindow.PrevWindowSize = win.WindowSize;
|
||||
_mainWindow.PrevWindowPos = win.PrevWindowPos;
|
||||
|
||||
SendCmd(new CmdWinSetFullscreen((nint) _mainWindow.GlfwWindow));
|
||||
}
|
||||
else
|
||||
{
|
||||
SendCmd(new CmdWinSetMonitor(
|
||||
(nint) _mainWindow.GlfwWindow,
|
||||
0,
|
||||
_mainWindow.PrevWindowPos.X, _mainWindow.PrevWindowPos.Y,
|
||||
_mainWindow.PrevWindowSize.X, _mainWindow.PrevWindowSize.Y,
|
||||
0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void WinThreadWinSetFullscreen(CmdWinSetFullscreen cmd)
|
||||
{
|
||||
var ptr = (Window*) cmd.Window;
|
||||
GLFW.GetWindowSize(ptr, out var w, out var h);
|
||||
GLFW.GetWindowPos(ptr, out var x, out var y);
|
||||
|
||||
var monitor = MonitorForWindow(ptr);
|
||||
var mode = GLFW.GetVideoMode(monitor);
|
||||
|
||||
GLFW.SetWindowMonitor(
|
||||
ptr,
|
||||
monitor,
|
||||
0, 0,
|
||||
mode->Width, mode->Height,
|
||||
mode->RefreshRate);
|
||||
}
|
||||
|
||||
// glfwGetWindowMonitor only works for fullscreen windows.
|
||||
// Picks the monitor with the top-left corner of the window.
|
||||
private Monitor* MonitorForWindow(Window* window)
|
||||
{
|
||||
GLFW.GetWindowPos(window, out var winPosX, out var winPosY);
|
||||
var monitors = GLFW.GetMonitorsRaw(out var count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var monitor = monitors[i];
|
||||
GLFW.GetMonitorPos(monitor, out var monPosX, out var monPosY);
|
||||
var videoMode = GLFW.GetVideoMode(monitor);
|
||||
|
||||
var box = Box2i.FromDimensions(monPosX, monPosY, videoMode->Width, videoMode->Height);
|
||||
if (box.Contains(winPosX, winPosY))
|
||||
return monitor;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return GLFW.GetPrimaryMonitor();
|
||||
}
|
||||
|
||||
public uint? WindowGetX11Id(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (GlfwWindowReg) window;
|
||||
try
|
||||
{
|
||||
return GLFW.GetX11Window(reg.GlfwWindow);
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void WindowDestroy(WindowReg window)
|
||||
{
|
||||
var reg = (GlfwWindowReg) window;
|
||||
if (reg.IsDisposed)
|
||||
return;
|
||||
|
||||
reg.IsDisposed = true;
|
||||
|
||||
SendCmd(new CmdWinDestroy((nint) reg.GlfwWindow));
|
||||
|
||||
_windows.Remove(reg);
|
||||
_clyde._windowHandles.Remove(reg.Handle);
|
||||
|
||||
_clyde.DestroyWindow?.Invoke(new WindowDestroyedEventArgs(window.Handle));
|
||||
}
|
||||
|
||||
private Window* CreateGlfwWindowForRenderer(
|
||||
Renderer r,
|
||||
WindowCreateParameters parameters,
|
||||
Window* contextShare)
|
||||
{
|
||||
#if DEBUG
|
||||
GLFW.WindowHint(WindowHintBool.OpenGLDebugContext, true);
|
||||
#endif
|
||||
GLFW.WindowHint(WindowHintString.X11ClassName, "SS14");
|
||||
GLFW.WindowHint(WindowHintString.X11InstanceName, "SS14");
|
||||
|
||||
if (r == Renderer.OpenGL33)
|
||||
{
|
||||
GLFW.WindowHint(WindowHintInt.ContextVersionMajor, 3);
|
||||
GLFW.WindowHint(WindowHintInt.ContextVersionMinor, 3);
|
||||
GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, true);
|
||||
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi);
|
||||
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, ContextApi.NativeContextApi);
|
||||
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core);
|
||||
GLFW.WindowHint(WindowHintBool.SrgbCapable, true);
|
||||
}
|
||||
else if (r == Renderer.OpenGL31)
|
||||
{
|
||||
GLFW.WindowHint(WindowHintInt.ContextVersionMajor, 3);
|
||||
GLFW.WindowHint(WindowHintInt.ContextVersionMinor, 1);
|
||||
GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, false);
|
||||
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi);
|
||||
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, ContextApi.NativeContextApi);
|
||||
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
|
||||
GLFW.WindowHint(WindowHintBool.SrgbCapable, true);
|
||||
}
|
||||
else if (r == Renderer.OpenGLES2)
|
||||
{
|
||||
GLFW.WindowHint(WindowHintInt.ContextVersionMajor, 2);
|
||||
GLFW.WindowHint(WindowHintInt.ContextVersionMinor, 0);
|
||||
GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, true);
|
||||
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlEsApi);
|
||||
// GLES2 is initialized through EGL to allow ANGLE usage.
|
||||
// (It may be an idea to make this a configuration cvar)
|
||||
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, ContextApi.EglContextApi);
|
||||
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
|
||||
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
|
||||
}
|
||||
|
||||
|
||||
Monitor* monitor = null;
|
||||
if (parameters.Monitor != null &&
|
||||
_winThreadMonitors.TryGetValue(parameters.Monitor.Id, out var monitorReg))
|
||||
{
|
||||
monitor = monitorReg.Ptr;
|
||||
}
|
||||
|
||||
GLFW.WindowHint(WindowHintBool.Visible, false);
|
||||
|
||||
var window = GLFW.CreateWindow(
|
||||
parameters.Width, parameters.Height,
|
||||
parameters.Title,
|
||||
parameters.Fullscreen ? monitor : null,
|
||||
contextShare);
|
||||
|
||||
// Check if window failed to create.
|
||||
if (window == null)
|
||||
return null;
|
||||
|
||||
if (parameters.Maximized)
|
||||
{
|
||||
GLFW.GetMonitorPos(monitor, out var x, out var y);
|
||||
GLFW.SetWindowPos(window, x, y);
|
||||
GLFW.MaximizeWindow(window);
|
||||
}
|
||||
|
||||
if (parameters.Visible)
|
||||
{
|
||||
GLFW.ShowWindow(window);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
private GlfwWindowReg WinThreadSetupWindow(Window* window)
|
||||
{
|
||||
var reg = new GlfwWindowReg
|
||||
{
|
||||
GlfwWindow = window,
|
||||
Id = new WindowId(_nextWindowId++)
|
||||
};
|
||||
var handle = new WindowHandle(_clyde, reg);
|
||||
reg.Handle = handle;
|
||||
|
||||
LoadWindowIcon(window);
|
||||
|
||||
GLFW.SetCharCallback(window, _charCallback);
|
||||
GLFW.SetKeyCallback(window, _keyCallback);
|
||||
GLFW.SetWindowCloseCallback(window, _windowCloseCallback);
|
||||
GLFW.SetCursorPosCallback(window, _cursorPosCallback);
|
||||
GLFW.SetCursorEnterCallback(window, _cursorEnterCallback);
|
||||
GLFW.SetWindowSizeCallback(window, _windowSizeCallback);
|
||||
GLFW.SetWindowPosCallback(window, _windowPosCallback);
|
||||
GLFW.SetScrollCallback(window, _scrollCallback);
|
||||
GLFW.SetMouseButtonCallback(window, _mouseButtonCallback);
|
||||
GLFW.SetWindowContentScaleCallback(window, _windowContentScaleCallback);
|
||||
GLFW.SetWindowIconifyCallback(window, _windowIconifyCallback);
|
||||
GLFW.SetWindowFocusCallback(window, _windowFocusCallback);
|
||||
|
||||
GLFW.GetFramebufferSize(window, out var fbW, out var fbH);
|
||||
reg.FramebufferSize = (fbW, fbH);
|
||||
|
||||
GLFW.GetWindowContentScale(window, out var scaleX, out var scaleY);
|
||||
reg.WindowScale = (scaleX, scaleY);
|
||||
|
||||
GLFW.GetWindowSize(window, out var w, out var h);
|
||||
reg.PrevWindowSize = reg.WindowSize = (w, h);
|
||||
|
||||
GLFW.GetWindowPos(window, out var x, out var y);
|
||||
reg.PrevWindowPos = (x, y);
|
||||
|
||||
reg.PixelRatio = reg.FramebufferSize / reg.WindowSize;
|
||||
|
||||
return reg;
|
||||
}
|
||||
|
||||
private WindowReg? FindWindow(nint window) => FindWindow((Window*) window);
|
||||
|
||||
private WindowReg? FindWindow(Window* window)
|
||||
{
|
||||
foreach (var windowReg in _windows)
|
||||
{
|
||||
if (windowReg.GlfwWindow == window)
|
||||
{
|
||||
return windowReg;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public int KeyGetScanCode(Keyboard.Key key)
|
||||
{
|
||||
return GLFW.GetKeyScancode(ConvertGlfwKeyReverse(key));
|
||||
}
|
||||
|
||||
public string KeyGetNameScanCode(int scanCode)
|
||||
{
|
||||
return GLFW.GetKeyName(Keys.Unknown, scanCode);
|
||||
}
|
||||
|
||||
public Task<string> ClipboardGetText()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
SendCmd(new CmdGetClipboard((nint) _mainWindow!.GlfwWindow, tcs));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private void WinThreadGetClipboard(CmdGetClipboard cmd)
|
||||
{
|
||||
var clipboard = GLFW.GetClipboardString((Window*) cmd.Window);
|
||||
// Don't have to care about synchronization I don't think so just fire this immediately.
|
||||
cmd.Tcs.TrySetResult(clipboard);
|
||||
}
|
||||
|
||||
public void ClipboardSetText(string text)
|
||||
{
|
||||
SendCmd(new CmdSetClipboard((nint) _mainWindow!.GlfwWindow, text));
|
||||
}
|
||||
|
||||
private void WinThreadSetClipboard(CmdSetClipboard cmd)
|
||||
{
|
||||
GLFW.SetClipboardString((Window*) cmd.Window, cmd.Text);
|
||||
}
|
||||
|
||||
public void LoadWindowIcon(Window* window)
|
||||
{
|
||||
var icons = _clyde.LoadWindowIcons().ToArray();
|
||||
|
||||
// Turn each image into a byte[] so we can actually pin their contents.
|
||||
// Wish I knew a clean way to do this without allocations.
|
||||
var images = icons
|
||||
.Select(i => (MemoryMarshal.Cast<Rgba32, byte>(i.GetPixelSpan()).ToArray(), i.Width, i.Height))
|
||||
.ToList();
|
||||
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<GCHandle> handles = stackalloc GCHandle[images.Count];
|
||||
Span<GlfwImage> glfwImages = stackalloc GlfwImage[images.Count];
|
||||
|
||||
for (var i = 0; i < images.Count; i++)
|
||||
{
|
||||
var image = images[i];
|
||||
handles[i] = GCHandle.Alloc(image.Item1, GCHandleType.Pinned);
|
||||
var addrOfPinnedObject = (byte*) handles[i].AddrOfPinnedObject();
|
||||
glfwImages[i] = new GlfwImage(image.Width, image.Height, addrOfPinnedObject);
|
||||
}
|
||||
|
||||
GLFW.SetWindowIcon(window, glfwImages);
|
||||
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public void GLInitMainContext(bool gles)
|
||||
{
|
||||
_mainGraphicsContext = new GlfwBindingsContext();
|
||||
GL.LoadBindings(_mainGraphicsContext);
|
||||
|
||||
if (gles)
|
||||
{
|
||||
// On GLES we use some OES and KHR functions so make sure to initialize them.
|
||||
OpenToolkit.Graphics.ES20.GL.LoadBindings(_mainGraphicsContext);
|
||||
}
|
||||
}
|
||||
|
||||
public void GLMakeContextCurrent(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (GlfwWindowReg) window;
|
||||
|
||||
GLFW.MakeContextCurrent(reg.GlfwWindow);
|
||||
}
|
||||
|
||||
public void GLSwapInterval(int interval)
|
||||
{
|
||||
GLFW.SwapInterval(interval);
|
||||
}
|
||||
|
||||
private void CheckWindowDisposed(WindowReg reg)
|
||||
{
|
||||
if (reg.IsDisposed)
|
||||
throw new ObjectDisposedException("Window disposed");
|
||||
}
|
||||
|
||||
private sealed class GlfwWindowReg : WindowReg
|
||||
{
|
||||
public Window* GlfwWindow;
|
||||
|
||||
// Kept around to avoid it being GCd.
|
||||
public CursorImpl? Cursor;
|
||||
}
|
||||
|
||||
private class GlfwBindingsContext : IBindingsContext
|
||||
{
|
||||
public IntPtr GetProcAddress(string procName)
|
||||
{
|
||||
return GLFW.GetProcAddress(procName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
Robust.Client/Graphics/Clyde/Windowing/Glfw.cs
Normal file
119
Robust.Client/Graphics/Clyde/Windowing/Glfw.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using OpenToolkit.GraphicsLibraryFramework;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class GlfwWindowingImpl : IWindowingImpl
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private readonly Clyde _clyde;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly ISawmill _sawmillGlfw;
|
||||
|
||||
private bool _glfwInitialized;
|
||||
private bool _win32Experience;
|
||||
|
||||
public GlfwWindowingImpl(Clyde clyde)
|
||||
{
|
||||
_clyde = clyde;
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sawmill = _logManager.GetSawmill("clyde.win");
|
||||
_sawmillGlfw = _logManager.GetSawmill("clyde.win.glfw");
|
||||
}
|
||||
|
||||
public bool Init()
|
||||
{
|
||||
#if DEBUG
|
||||
_cfg.OnValueChanged(CVars.DisplayWin32Experience, b => _win32Experience = b, true);
|
||||
#endif
|
||||
|
||||
InitChannels();
|
||||
|
||||
if (!InitGlfw())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SetupGlobalCallbacks();
|
||||
InitMonitors();
|
||||
InitCursors();
|
||||
InitKeyMap();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
if (_glfwInitialized)
|
||||
{
|
||||
_sawmill.Debug("Terminating GLFW.");
|
||||
GLFW.Terminate();
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushDispose()
|
||||
{
|
||||
// Not currently used
|
||||
}
|
||||
|
||||
private bool InitGlfw()
|
||||
{
|
||||
StoreCallbacks();
|
||||
|
||||
GLFW.SetErrorCallback(_errorCallback);
|
||||
if (!GLFW.Init())
|
||||
{
|
||||
var err = GLFW.GetError(out var desc);
|
||||
_sawmill.Fatal($"Failed to initialize GLFW! [{err}] {desc}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_glfwInitialized = true;
|
||||
var version = GLFW.GetVersionString();
|
||||
_sawmill.Debug("GLFW initialized, version: {0}.", version);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnGlfwError(ErrorCode code, string description)
|
||||
{
|
||||
_sawmillGlfw.Error("GLFW Error: [{0}] {1}", code, description);
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GlfwException : Exception
|
||||
{
|
||||
public GlfwException()
|
||||
{
|
||||
}
|
||||
|
||||
public GlfwException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public GlfwException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
|
||||
protected GlfwException(
|
||||
SerializationInfo info,
|
||||
StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Robust.Client/Graphics/Clyde/Windowing/IWindowingImpl.cs
Normal file
58
Robust.Client/Graphics/Clyde/Windowing/IWindowingImpl.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using OpenToolkit;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
partial class Clyde
|
||||
{
|
||||
private interface IWindowingImpl
|
||||
{
|
||||
WindowReg? MainWindow { get; }
|
||||
IReadOnlyList<WindowReg> AllWindows { get; }
|
||||
IBindingsContext GraphicsBindingContext { get; }
|
||||
|
||||
// Lifecycle stuff
|
||||
bool Init();
|
||||
bool TryInitMainWindow(Renderer renderer, [NotNullWhen(false)] out string? error);
|
||||
void Shutdown();
|
||||
|
||||
void EnterWindowLoop();
|
||||
void TerminateWindowLoop();
|
||||
|
||||
void ProcessEvents(bool single=false);
|
||||
void FlushDispose();
|
||||
|
||||
ICursor CursorGetStandard(StandardCursorShape shape);
|
||||
ICursor CursorCreate(Image<Rgba32> image, Vector2i hotSpot);
|
||||
void CursorSet(WindowReg window, ICursor? cursor);
|
||||
|
||||
void WindowSetTitle(WindowReg window, string title);
|
||||
void WindowSetMonitor(WindowReg window, IClydeMonitor monitor);
|
||||
void WindowSetVisible(WindowReg window, bool visible);
|
||||
void WindowRequestAttention(WindowReg window);
|
||||
void WindowSwapBuffers(WindowReg window);
|
||||
uint? WindowGetX11Id(WindowReg window);
|
||||
Task<WindowHandle> WindowCreate(WindowCreateParameters parameters);
|
||||
void WindowDestroy(WindowReg reg);
|
||||
|
||||
string KeyGetName(Keyboard.Key key);
|
||||
|
||||
Task<string> ClipboardGetText();
|
||||
void ClipboardSetText(string text);
|
||||
|
||||
void UpdateVSync();
|
||||
void UpdateMainWindowMode();
|
||||
|
||||
// OpenGL-related stuff.
|
||||
void GLMakeContextCurrent(WindowReg reg);
|
||||
void GLSwapInterval(int interval);
|
||||
void GLInitMainContext(bool gles);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
using System;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
@@ -13,77 +9,14 @@ namespace Robust.Client.Graphics
|
||||
// Maybe add borderless? Not sure how good Godot's default fullscreen is with alt tabbing.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages the game window, resolutions, fullscreen mode, VSync, etc...
|
||||
/// </summary>
|
||||
internal abstract class ClydeBase
|
||||
// Remember when this was called DisplayManager?
|
||||
internal static class ClydeBase
|
||||
{
|
||||
[Dependency] protected readonly IConfigurationManager ConfigurationManager = default!;
|
||||
|
||||
protected WindowMode WindowMode { get; private set; } = WindowMode.Windowed;
|
||||
protected bool VSync { get; private set; } = true;
|
||||
|
||||
public abstract Vector2i ScreenSize { get; }
|
||||
public abstract bool IsFocused { get; }
|
||||
|
||||
public abstract void SetWindowTitle(string title);
|
||||
|
||||
public virtual bool Initialize()
|
||||
{
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
|
||||
ConfigurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void ReloadConfig()
|
||||
{
|
||||
ReadConfig();
|
||||
}
|
||||
|
||||
public abstract event Action<WindowResizedEventArgs> OnWindowResized;
|
||||
|
||||
public abstract event Action<WindowFocusedEventArgs> OnWindowFocused;
|
||||
|
||||
protected virtual void ReadConfig()
|
||||
{
|
||||
WindowMode = (WindowMode) ConfigurationManager.GetCVar(CVars.DisplayWindowMode);
|
||||
VSync = ConfigurationManager.GetCVar(CVars.DisplayVSync);
|
||||
}
|
||||
|
||||
private void _vSyncChanged(bool newValue)
|
||||
{
|
||||
VSync = newValue;
|
||||
VSyncChanged();
|
||||
}
|
||||
|
||||
protected virtual void VSyncChanged()
|
||||
{
|
||||
}
|
||||
|
||||
private void _windowModeChanged(int newValue)
|
||||
{
|
||||
WindowMode = (WindowMode) newValue;
|
||||
WindowModeChanged();
|
||||
}
|
||||
|
||||
protected virtual void WindowModeChanged()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void LightmapDividerChanged(int newValue)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void MaxLightsPerSceneChanged(int newValue)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void SoftShadowsChanged(bool newValue)
|
||||
internal static Vector2i ClampSubRegion(Vector2i size, UIBox2i? subRegionSpecified)
|
||||
{
|
||||
return subRegionSpecified == null
|
||||
? size
|
||||
: UIBox2i.FromDimensions(Vector2i.Zero, size).Intersection(subRegionSpecified.Value)?.Size ?? default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,17 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
public Thickness Padding
|
||||
{
|
||||
set
|
||||
{
|
||||
PaddingLeft = value.Left;
|
||||
PaddingTop = value.Top;
|
||||
PaddingRight = value.Right;
|
||||
PaddingBottom = value.Bottom;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw this style box to the screen at the specified coordinates.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,14 +8,19 @@ using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
public delegate void CopyPixelsDelegate<T>(Image<T> pixels) where T : unmanaged, IPixel<T>;
|
||||
|
||||
public interface IClyde
|
||||
{
|
||||
IRenderWindow MainWindowRenderTarget { get; }
|
||||
IClydeWindow MainWindow { get; }
|
||||
IRenderTarget MainWindowRenderTarget => MainWindow.RenderTarget;
|
||||
|
||||
Vector2i ScreenSize { get; }
|
||||
|
||||
bool IsFocused { get; }
|
||||
|
||||
IEnumerable<IClydeWindow> AllWindows { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default scale ratio for window contents, given to us by the OS.
|
||||
/// </summary>
|
||||
@@ -33,7 +38,7 @@ namespace Robust.Client.Graphics
|
||||
|
||||
event Action<WindowFocusedEventArgs> OnWindowFocused;
|
||||
|
||||
event Action OnWindowScaleChanged;
|
||||
event Action<WindowContentScaleEventArgs> OnWindowScaleChanged;
|
||||
|
||||
OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
|
||||
TextureLoadParameters? loadParams = null);
|
||||
@@ -96,9 +101,18 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
/// <param name="type">What kind of screenshot to take</param>
|
||||
/// <param name="callback">The callback to run when the screenshot has been made.</param>
|
||||
void Screenshot(ScreenshotType type, Action<Image<Rgb24>> callback);
|
||||
/// <param name="subRegion">
|
||||
/// The subregion of the framebuffer to copy.
|
||||
/// If null, the whole framebuffer is copied.
|
||||
/// </param>
|
||||
/// <seealso cref="ScreenshotAsync"/>
|
||||
/// <seealso cref="IRenderTarget.CopyPixelsToMemory{T}"/>
|
||||
void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback, UIBox2i? subRegion = null);
|
||||
|
||||
Task<Image<Rgb24>> ScreenshotAsync(ScreenshotType type)
|
||||
/// <summary>
|
||||
/// Async version of <see cref="Screenshot"/>.
|
||||
/// </summary>
|
||||
Task<Image<Rgb24>> ScreenshotAsync(ScreenshotType type, UIBox2i? subRegion = null)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Image<Rgb24>>();
|
||||
|
||||
@@ -107,10 +121,15 @@ namespace Robust.Client.Graphics
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
IClydeViewport CreateViewport(Vector2i size, string? name = null);
|
||||
IClydeViewport CreateViewport(Vector2i size, string? name = null)
|
||||
{
|
||||
return CreateViewport(size, default, name);
|
||||
}
|
||||
|
||||
IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters, string? name = null);
|
||||
|
||||
IEnumerable<IClydeMonitor> EnumerateMonitors();
|
||||
}
|
||||
|
||||
// TODO: Maybe implement IDisposable for render targets. I got lazy and didn't.
|
||||
Task<IClydeWindow> CreateWindow(WindowCreateParameters parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Robust.Client.Graphics
|
||||
// AUDIO SYSTEM DOWN BELOW.
|
||||
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
|
||||
AudioStream LoadAudioWav(Stream stream, string? name = null);
|
||||
AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate);
|
||||
|
||||
void SetMasterVolume(float newVolume);
|
||||
|
||||
|
||||
@@ -21,5 +21,6 @@ namespace Robust.Client.Graphics
|
||||
void SetOcclusion(float blocks);
|
||||
void SetPlaybackPosition(float seconds);
|
||||
void SetVelocity(Vector2 velocity);
|
||||
void SetVolumeDirect(float masterVolumeDecay);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user