mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
250 Commits
reactjs-su
...
v0.19.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4787b63a8 | ||
|
|
6b305349c9 | ||
|
|
3c16779d68 | ||
|
|
38fcadf649 | ||
|
|
6cc50013be | ||
|
|
bb7c9ad7fa | ||
|
|
b53d838b14 | ||
|
|
e03831586e | ||
|
|
6acb8f8b1b | ||
|
|
29bf2cbbc4 | ||
|
|
4fc238864b | ||
|
|
cb8d2727f4 | ||
|
|
9ef433d3ed | ||
|
|
02de3cd437 | ||
|
|
e7211063e7 | ||
|
|
e32ee1a58e | ||
|
|
1eb5859f01 | ||
|
|
ff7c3dc45c | ||
|
|
655b5974a3 | ||
|
|
47d7c7cf6c | ||
|
|
1c39deae6c | ||
|
|
6c4dc7fc72 | ||
|
|
3c5618364c | ||
|
|
bf1cc8fd7c | ||
|
|
72d4d50788 | ||
|
|
6a86fcdeeb | ||
|
|
dad95ad73a | ||
|
|
77f915acf5 | ||
|
|
6fc1daf978 | ||
|
|
934f589778 | ||
|
|
305ea97d5b | ||
|
|
e3b26e80bf | ||
|
|
810ef119b0 | ||
|
|
393b27679d | ||
|
|
a592134caf | ||
|
|
2389df7c6f | ||
|
|
422202348e | ||
|
|
284e08ffd9 | ||
|
|
2ce3d04bba | ||
|
|
8e67bfe990 | ||
|
|
27dc9510a3 | ||
|
|
d4708cb144 | ||
|
|
986de32043 | ||
|
|
4e90c291b3 | ||
|
|
7775767687 | ||
|
|
5624476e2b | ||
|
|
9ef80864b7 | ||
|
|
c32ef136a1 | ||
|
|
b2ea0fe1e2 | ||
|
|
be86b12b14 | ||
|
|
8b8e499615 | ||
|
|
36b0d38b70 | ||
|
|
78ef6921da | ||
|
|
f6ab67e92c | ||
|
|
44ffdcf429 | ||
|
|
53f4d3fab6 | ||
|
|
7dcf543443 | ||
|
|
4e445ee313 | ||
|
|
c6be3e145a | ||
|
|
f90526be22 | ||
|
|
df09bc2cf0 | ||
|
|
a1affcfd3f | ||
|
|
a402f3a880 | ||
|
|
b0fe9fb1a4 | ||
|
|
5057c91dcd | ||
|
|
84a80db21f | ||
|
|
8657e5516b | ||
|
|
6b24da6990 | ||
|
|
0ee87bc771 | ||
|
|
6e35c89740 | ||
|
|
9d69b330b5 | ||
|
|
22b3f3148c | ||
|
|
b73b3007f2 | ||
|
|
37dd4b0893 | ||
|
|
07c5a38582 | ||
|
|
c866c6b59b | ||
|
|
c3e97cb97e | ||
|
|
52ffb97369 | ||
|
|
bad5657725 | ||
|
|
f8dc3c8a0e | ||
|
|
77493b5d14 | ||
|
|
b1ceafbe4f | ||
|
|
41e9d9b08b | ||
|
|
dc32549c75 | ||
|
|
17dd3af3c7 | ||
|
|
32bdc19fe9 | ||
|
|
70fbefbe62 | ||
|
|
64cd6ea9be | ||
|
|
aa92c2444d | ||
|
|
9b19626f1b | ||
|
|
2bbe5193ee | ||
|
|
d11318f082 | ||
|
|
237f948d99 | ||
|
|
f3449be1e9 | ||
|
|
b95ffda711 | ||
|
|
0932fa0058 | ||
|
|
da2e46b81b | ||
|
|
d4a1fcc9c6 | ||
|
|
c7e7f4f28f | ||
|
|
87e191b8d7 | ||
|
|
05b21fcfcc | ||
|
|
48e4ae87d8 | ||
|
|
8b090034f1 | ||
|
|
41c183ac09 | ||
|
|
4380b95451 | ||
|
|
90fc486a20 | ||
|
|
f254153962 | ||
|
|
a2538d1905 | ||
|
|
5d7a2879a7 | ||
|
|
986adf6494 | ||
|
|
15932fb9aa | ||
|
|
3ae4bb03f5 | ||
|
|
7f47478997 | ||
|
|
a2923450ea | ||
|
|
5366b9a35b | ||
|
|
5ca37c68e2 | ||
|
|
22aa274d03 | ||
|
|
54bbe55bb9 | ||
|
|
9706576e66 | ||
|
|
b2ccd65da4 | ||
|
|
c587cd2e12 | ||
|
|
ba23a989a9 | ||
|
|
c6c69668c8 | ||
|
|
b0a4593f44 | ||
|
|
2d6e699e2c | ||
|
|
4307509402 | ||
|
|
ed165b1e4c | ||
|
|
3503300a23 | ||
|
|
3ab3d255ed | ||
|
|
fd625e0b30 | ||
|
|
53af37cd56 | ||
|
|
6bf4945181 | ||
|
|
868320c513 | ||
|
|
577b836420 | ||
|
|
4a36b52657 | ||
|
|
8dc944d22d | ||
|
|
0a10170155 | ||
|
|
dfa2c222d1 | ||
|
|
e288af162c | ||
|
|
6280820c86 | ||
|
|
102b31f83d | ||
|
|
37ca9ca19a | ||
|
|
0c151ad456 | ||
|
|
55e3f749b4 | ||
|
|
16902d7fbc | ||
|
|
c37f53e083 | ||
|
|
4cc499afdf | ||
|
|
48895efad7 | ||
|
|
eb46b04c0e | ||
|
|
442de12b99 | ||
|
|
712c1f3559 | ||
|
|
5a5cfa1eba | ||
|
|
7988386bc3 | ||
|
|
76bfe2905d | ||
|
|
23893bcacd | ||
|
|
c1da159e8f | ||
|
|
13889acb37 | ||
|
|
ffb3de3f2c | ||
|
|
87cd45fc64 | ||
|
|
1cf914813c | ||
|
|
86d61f8d03 | ||
|
|
8f638fbf9e | ||
|
|
95ac134a07 | ||
|
|
4e65f9b2a1 | ||
|
|
d60bbe9fe9 | ||
|
|
dec1495e1e | ||
|
|
dc72c6fe22 | ||
|
|
141b1205c6 | ||
|
|
65f4a09ad5 | ||
|
|
3d1545c0b9 | ||
|
|
ec26dd622b | ||
|
|
0ab3131964 | ||
|
|
588a9e9f63 | ||
|
|
f2fa930edd | ||
|
|
ec47229a37 | ||
|
|
bf5d1d58a8 | ||
|
|
8b4da24ee7 | ||
|
|
3fba108d70 | ||
|
|
35029f0eed | ||
|
|
b66ab9d7c6 | ||
|
|
5e0b745ba9 | ||
|
|
45d906ba7e | ||
|
|
24b124fb17 | ||
|
|
7cb0978468 | ||
|
|
44cb135a1d | ||
|
|
146b673203 | ||
|
|
b0d23c5665 | ||
|
|
68f89c8958 | ||
|
|
1327d6bf25 | ||
|
|
237e37ff30 | ||
|
|
4d707c86cb | ||
|
|
c7027c6e00 | ||
|
|
81ec61bcc8 | ||
|
|
649178fa54 | ||
|
|
9ff46b9aad | ||
|
|
cdcbb60ca7 | ||
|
|
d8cdb2b312 | ||
|
|
fa1c1b8e6e | ||
|
|
2ded835602 | ||
|
|
a43f04818d | ||
|
|
278dc60119 | ||
|
|
8a202be0cf | ||
|
|
58940a3cd7 | ||
|
|
aa9721d146 | ||
|
|
79f114ad5b | ||
|
|
197a6ddd9d | ||
|
|
4a4fb15e06 | ||
|
|
e7e83ce6e8 | ||
|
|
a82b293452 | ||
|
|
bb483a3a38 | ||
|
|
2ca3d0e395 | ||
|
|
bda79b1e82 | ||
|
|
4f4b754e2d | ||
|
|
214cabac43 | ||
|
|
7b6229c222 | ||
|
|
1a14a75897 | ||
|
|
76b75fd9b3 | ||
|
|
b969fd22f7 | ||
|
|
8d27d091af | ||
|
|
1eb7393a60 | ||
|
|
4cf88507c2 | ||
|
|
3565d8b321 | ||
|
|
7094c29b2e | ||
|
|
63004b270f | ||
|
|
6714a99b38 | ||
|
|
4c3b8df1e7 | ||
|
|
4bb695121f | ||
|
|
09fd47c421 | ||
|
|
d201d9c688 | ||
|
|
fd1e25c584 | ||
|
|
6bb66ae70e | ||
|
|
cc82d6b1d9 | ||
|
|
956be749b6 | ||
|
|
6585a00608 | ||
|
|
c0525f710f | ||
|
|
d3672807d2 | ||
|
|
60f18d5f36 | ||
|
|
e72d3de256 | ||
|
|
ba9846b9c4 | ||
|
|
09586284dc | ||
|
|
a1ee4374b2 | ||
|
|
4de6f25f11 | ||
|
|
582d8a5587 | ||
|
|
ec53b04f99 | ||
|
|
950fc94408 | ||
|
|
58d12e6e09 | ||
|
|
94323005c4 | ||
|
|
4989842057 | ||
|
|
80172636a8 | ||
|
|
8491f7be24 |
38
.github/workflows/benchmarks.yml
vendored
38
.github/workflows/benchmarks.yml
vendored
@@ -1,11 +1,13 @@
|
||||
name: Benchmarks
|
||||
#on:
|
||||
# push
|
||||
#schedule:
|
||||
# - cron: '0 5 * * *'
|
||||
#push:
|
||||
# tags:
|
||||
# - 'v*'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
concurrency: benchmarks
|
||||
|
||||
env:
|
||||
ROBUST_BENCHMARKS_ENABLE_SQL: 1
|
||||
@@ -20,14 +22,14 @@ jobs:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Run benchmark
|
||||
run: cd Robust.Benchmarks && sudo dotnet run --filter '*' --configuration Release
|
||||
- name: Run script on centcomm
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: centcomm.spacestation14.io
|
||||
username: robust-benchmark-runner
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }}
|
||||
command_timeout: 100000m
|
||||
script: |
|
||||
wget https://raw.githubusercontent.com/space-wizards/RobustToolbox/${{ github.sha }}/Tools/run_benchmarks.py
|
||||
python3 run_benchmarks.py "${{ secrets.BENCHMARKS_WRITE_ADDRESS }}" "${{ secrets.BENCHMARKS_WRITE_PORT }}" "${{ secrets.BENCHMARKS_WRITE_USER }}" "${{ secrets.BENCHMARKS_WRITE_PASSWORD }}" "${{ github.sha }}"
|
||||
rm run_benchmarks.py
|
||||
|
||||
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -28,6 +28,6 @@ jobs:
|
||||
- name: Build
|
||||
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
|
||||
- name: Test Engine
|
||||
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -v n
|
||||
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
|
||||
|
||||
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -10,9 +10,6 @@
|
||||
[submodule "Robust.LoaderApi"]
|
||||
path = Robust.LoaderApi
|
||||
url = https://github.com/space-wizards/Robust.LoaderApi.git
|
||||
[submodule "ManagedHttpListener"]
|
||||
path = ManagedHttpListener
|
||||
url = https://github.com/space-wizards/ManagedHttpListener.git
|
||||
[submodule "cefglue"]
|
||||
path = cefglue
|
||||
url = https://github.com/space-wizards/cefglue.git
|
||||
|
||||
Submodule Lidgren.Network/Lidgren.Network updated: b723fc532e...1e7fb3c2b6
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<PropertyGroup><Version>0.8.86</Version></PropertyGroup>
|
||||
<PropertyGroup><Version>0.19.2.0</Version></PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<!-- Avoid MSBuild adding a None entry for XAML files because they'd show up TWICE in the project view. -->
|
||||
<DefaultItemExcludes>**/*.xaml</DefaultItemExcludes>
|
||||
<RobustUseExternalMSBuild>true</RobustUseExternalMSBuild>
|
||||
<RobustUseExternalMSBuild>false</RobustUseExternalMSBuild>
|
||||
<_RobustUseExternalMSBuild>$(RobustUseExternalMSBuild)</_RobustUseExternalMSBuild>
|
||||
<_RobustUseExternalMSBuild Condition="'$(_RobustForceInternalMSBuild)' == 'true'">false</_RobustUseExternalMSBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
Submodule ManagedHttpListener deleted from ae0539e66f
89
Resources/Locale/en-US/commands.ftl
Normal file
89
Resources/Locale/en-US/commands.ftl
Normal file
@@ -0,0 +1,89 @@
|
||||
### Localization for engine console commands
|
||||
|
||||
## 'help' command
|
||||
cmd-help-desc = Display general help or help text for a specific command
|
||||
cmd-help-help = Usage: help [command name]
|
||||
When no command name is provided, displays general-purpose help text. If a command name is provided, displays help text for that command.
|
||||
|
||||
cmd-help-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
|
||||
cmd-help-unknown = Unknown command: { $command }
|
||||
cmd-help-top = { $command } - { $description }
|
||||
cmd-help-invalid-args = Invalid amount of arguments.
|
||||
cmd-help-arg-cmdname = [command name]
|
||||
|
||||
## 'cvar' command
|
||||
cmd-cvar-desc = Gets or sets a CVar.
|
||||
cmd-cvar-help = Usage: cvar <name | ?> [value]
|
||||
If a value is passed, the value is parsed and stored as the new value of the CVar.
|
||||
If not, the current value of the CVar is displayed.
|
||||
Use 'cvar ?' to get a list of all registered CVars.
|
||||
|
||||
cmd-cvar-invalid-args = Must provide exactly one or two arguments.
|
||||
cmd-cvar-not-registered = CVar '{ $cvar }' is not registered. Use 'cvar ?' to get a list of all registered CVars.
|
||||
cmd-cvar-parse-error = Input value is in incorrect format for type { $type }
|
||||
cmd-cvar-compl-list = List available CVars
|
||||
cmd-cvar-arg-name = <name | ?>
|
||||
|
||||
## 'list' command
|
||||
cmd-list-desc = Lists available commands, with optional search filter
|
||||
cmd-list-help = Usage: list [filter]
|
||||
Lists all available commands. If an argument is provided, it will be used to filter commands by name.
|
||||
|
||||
cmd-list-heading = SIDE NAME DESC{"\u000A"}-------------------------{"\u000A"}
|
||||
|
||||
cmd-list-arg-filter = [filter]
|
||||
|
||||
## '>' command, aka remote exec
|
||||
cmd-remoteexec-desc = Executes server-side commands
|
||||
cmd-remoteexec-help = Usage: > <command> [arg] [arg] [arg...]
|
||||
Executes a command on the server. This is necessary if a command with the same name exists on the client, as simply running the command would run the client command first.
|
||||
|
||||
## 'gc' command
|
||||
cmd-gc-desc = Run the GC (Garbage Collector)
|
||||
cmd-gc-help = Usage: gc [generation]
|
||||
Uses GC.Collect() to execute the Garbage Collector.
|
||||
If an argument is provided, it is parsed as a GC generation number and GC.Collect(int) is used.
|
||||
Use the 'gfc' command to do an LOH-compacting full GC.
|
||||
cmd-gc-failed-parse = Failed to parse argument.
|
||||
cmd-gc-arg-generation = [generation]
|
||||
|
||||
## 'gcf' command
|
||||
cmd-gcf-desc = Run the GC, fully, compacting LOH and everything.
|
||||
cmd-gcf-help = Usage: gcf
|
||||
Does a full GC.Collect(2, GCCollectionMode.Forced, true, true) while also compacting LOH.
|
||||
This will probably lock up for hundreds of milliseconds, be warned.
|
||||
|
||||
## 'gc_mode' command
|
||||
cmd-gc_mode-desc = Change/Read the GC Latency mode
|
||||
cmd-gc_mode-help = Usage: gc_mode [type]
|
||||
If no argument is provided, returns the current GC latency mode.
|
||||
If an argument is passed, it is parsed as GCLatencyMode and set as the GC latency mode.
|
||||
|
||||
cmd-gc_mode-current = current gc latency mode: { $prevMode }
|
||||
cmd-gc_mode-possible = possible modes:
|
||||
cmd-gc_mode-option = - { $mode }
|
||||
cmd-gc_mode-unknown = unknown gc latency mode: { $arg }
|
||||
cmd-gc_mode-attempt = attempting gc latency mode change: { $prevMode } -> { $mode }
|
||||
cmd-gc_mode-result = resulting gc latency mode: { $mode }
|
||||
cmd-gc_mode-arg-type = [type]
|
||||
|
||||
## 'mem' command
|
||||
cmd-mem-desc = Prints managed memory info
|
||||
cmd-mem-help = Usage: mem
|
||||
|
||||
cmd-mem-report = Heap Size: { TOSTRING($heapSize, "N0") }
|
||||
Total Allocated: { TOSTRING($totalAllocated, "N0") }
|
||||
|
||||
## 'physics' command
|
||||
cmd-physics-overlay = {$overlay} is not a recognised overlay
|
||||
|
||||
## 'lsasm' command
|
||||
cmd-lsasm-desc = Lists loaded assemblies by load context
|
||||
cmd-lsasm-help = Usage: lsasm
|
||||
|
||||
## 'exec' command
|
||||
cmd-exec-desc = Executes a script file from the game's writeable user data
|
||||
cmd-exec-help = Usage: exec <fileName>
|
||||
Each line in the file is executed as a single command, unless it starts with a #
|
||||
|
||||
cmd-exec-arg-filename = <fileName>
|
||||
10
Resources/Locale/en-US/controls.ftl
Normal file
10
Resources/Locale/en-US/controls.ftl
Normal file
@@ -0,0 +1,10 @@
|
||||
color-selector-sliders-red = R
|
||||
color-selector-sliders-green = G
|
||||
color-selector-sliders-blue = B
|
||||
color-selector-sliders-hue = H
|
||||
color-selector-sliders-saturation = S
|
||||
color-selector-sliders-value = V
|
||||
color-selector-sliders-alpha = A
|
||||
|
||||
color-selector-sliders-rgb = RGB
|
||||
color-selector-sliders-hsv = HSV
|
||||
1
Resources/Locale/en-US/midi-commands.ftl
Normal file
1
Resources/Locale/en-US/midi-commands.ftl
Normal file
@@ -0,0 +1 @@
|
||||
midi-panic-command-description = Turns off every note for every active MIDI renderer.
|
||||
225
Robust.Benchmarks/EntityManager/ComponentIndexBenchmark.cs
Normal file
225
Robust.Benchmarks/EntityManager/ComponentIndexBenchmark.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Benchmarks.EntityManager;
|
||||
|
||||
public class ComponentIndexBenchmark
|
||||
{
|
||||
// Just a bunch of types to bloat the test lists.
|
||||
|
||||
private readonly CompIndexFetcher _compIndexFetcherDirect;
|
||||
private readonly IFetcher _compIndexFetcher;
|
||||
private readonly DictFetcher _dictFetcherDirect;
|
||||
private readonly IFetcher _dictFetcher;
|
||||
|
||||
|
||||
public ComponentIndexBenchmark()
|
||||
{
|
||||
_compIndexFetcherDirect = new CompIndexFetcher();
|
||||
_compIndexFetcher = _compIndexFetcherDirect;
|
||||
_dictFetcherDirect = new DictFetcher();
|
||||
_dictFetcher = _dictFetcherDirect;
|
||||
}
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var types = typeof(ComponentIndexBenchmark)
|
||||
.GetNestedTypes(BindingFlags.NonPublic)
|
||||
.Where(t => t.Name.StartsWith("TestType"))
|
||||
.ToArray();
|
||||
|
||||
_compIndexFetcher.Init(types);
|
||||
_dictFetcher.Init(types);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int BenchCompIndex() => _compIndexFetcher.Get<TestType50>();
|
||||
|
||||
[Benchmark]
|
||||
public int BenchDict() => _dictFetcher.Get<TestType50>();
|
||||
|
||||
[Benchmark]
|
||||
public int BenchCompIndexDirect() => _compIndexFetcherDirect.Get<TestType50>();
|
||||
|
||||
[Benchmark]
|
||||
public int BenchDictDirect() => _dictFetcherDirect.Get<TestType50>();
|
||||
|
||||
private static CompIdx ArrayIndexFor<T>() => CompArrayIndex<T>.Idx;
|
||||
|
||||
private static int _compIndexMaster = -1;
|
||||
|
||||
private static class CompArrayIndex<T>
|
||||
{
|
||||
// ReSharper disable once StaticMemberInGenericType
|
||||
public static readonly CompIdx Idx = new(Interlocked.Increment(ref _compIndexMaster));
|
||||
}
|
||||
|
||||
private static CompIdx GetCompIdIndex(Type type)
|
||||
{
|
||||
return (CompIdx)typeof(CompArrayIndex<>)
|
||||
.MakeGenericType(type)
|
||||
.GetField(nameof(CompArrayIndex<int>.Idx), BindingFlags.Static | BindingFlags.Public)!
|
||||
.GetValue(null)!;
|
||||
}
|
||||
|
||||
private interface IFetcher
|
||||
{
|
||||
void Init(Type[] types);
|
||||
|
||||
int Get<T>();
|
||||
}
|
||||
|
||||
private sealed class CompIndexFetcher : IFetcher
|
||||
{
|
||||
private int[] _values = Array.Empty<int>();
|
||||
|
||||
public void Init(Type[] types)
|
||||
{
|
||||
var max = types.Max(t => GetCompIdIndex(t).Value);
|
||||
|
||||
_values = new int[max + 1];
|
||||
|
||||
var i = 0;
|
||||
foreach (var type in types)
|
||||
{
|
||||
_values[GetCompIdIndex(type).Value] = i++;
|
||||
}
|
||||
}
|
||||
|
||||
public int Get<T>()
|
||||
{
|
||||
return _values[CompArrayIndex<T>.Idx.Value];
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DictFetcher : IFetcher
|
||||
{
|
||||
private readonly Dictionary<Type, int> _values = new();
|
||||
|
||||
public void Init(Type[] types)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var type in types)
|
||||
{
|
||||
_values[type] = i++;
|
||||
}
|
||||
}
|
||||
|
||||
public int Get<T>()
|
||||
{
|
||||
return _values[typeof(T)];
|
||||
}
|
||||
}
|
||||
|
||||
// Just a bunch of types to pad the size of the arrays and such.
|
||||
|
||||
// @formatter:off
|
||||
// ReSharper disable UnusedType.Local
|
||||
private sealed class TestType1{}
|
||||
private sealed class TestType2{}
|
||||
private sealed class TestType3{}
|
||||
private sealed class TestType4{}
|
||||
private sealed class TestType5{}
|
||||
private sealed class TestType6{}
|
||||
private sealed class TestType7{}
|
||||
private sealed class TestType8{}
|
||||
private sealed class TestType9{}
|
||||
private sealed class TestType10{}
|
||||
private sealed class TestType11{}
|
||||
private sealed class TestType12{}
|
||||
private sealed class TestType13{}
|
||||
private sealed class TestType14{}
|
||||
private sealed class TestType15{}
|
||||
private sealed class TestType16{}
|
||||
private sealed class TestType17{}
|
||||
private sealed class TestType18{}
|
||||
private sealed class TestType19{}
|
||||
private sealed class TestType20{}
|
||||
private sealed class TestType21{}
|
||||
private sealed class TestType22{}
|
||||
private sealed class TestType23{}
|
||||
private sealed class TestType24{}
|
||||
private sealed class TestType25{}
|
||||
private sealed class TestType26{}
|
||||
private sealed class TestType27{}
|
||||
private sealed class TestType28{}
|
||||
private sealed class TestType29{}
|
||||
private sealed class TestType30{}
|
||||
private sealed class TestType31{}
|
||||
private sealed class TestType32{}
|
||||
private sealed class TestType33{}
|
||||
private sealed class TestType34{}
|
||||
private sealed class TestType35{}
|
||||
private sealed class TestType36{}
|
||||
private sealed class TestType37{}
|
||||
private sealed class TestType38{}
|
||||
private sealed class TestType39{}
|
||||
private sealed class TestType40{}
|
||||
private sealed class TestType41{}
|
||||
private sealed class TestType42{}
|
||||
private sealed class TestType43{}
|
||||
private sealed class TestType44{}
|
||||
private sealed class TestType45{}
|
||||
private sealed class TestType46{}
|
||||
private sealed class TestType47{}
|
||||
private sealed class TestType48{}
|
||||
private sealed class TestType49{}
|
||||
private sealed class TestType50{}
|
||||
private sealed class TestType51{}
|
||||
private sealed class TestType52{}
|
||||
private sealed class TestType53{}
|
||||
private sealed class TestType54{}
|
||||
private sealed class TestType55{}
|
||||
private sealed class TestType56{}
|
||||
private sealed class TestType57{}
|
||||
private sealed class TestType58{}
|
||||
private sealed class TestType59{}
|
||||
private sealed class TestType60{}
|
||||
private sealed class TestType61{}
|
||||
private sealed class TestType62{}
|
||||
private sealed class TestType63{}
|
||||
private sealed class TestType64{}
|
||||
private sealed class TestType65{}
|
||||
private sealed class TestType66{}
|
||||
private sealed class TestType67{}
|
||||
private sealed class TestType68{}
|
||||
private sealed class TestType69{}
|
||||
private sealed class TestType70{}
|
||||
private sealed class TestType71{}
|
||||
private sealed class TestType72{}
|
||||
private sealed class TestType73{}
|
||||
private sealed class TestType74{}
|
||||
private sealed class TestType75{}
|
||||
private sealed class TestType76{}
|
||||
private sealed class TestType77{}
|
||||
private sealed class TestType78{}
|
||||
private sealed class TestType79{}
|
||||
private sealed class TestType80{}
|
||||
private sealed class TestType81{}
|
||||
private sealed class TestType82{}
|
||||
private sealed class TestType83{}
|
||||
private sealed class TestType84{}
|
||||
private sealed class TestType85{}
|
||||
private sealed class TestType86{}
|
||||
private sealed class TestType87{}
|
||||
private sealed class TestType88{}
|
||||
private sealed class TestType89{}
|
||||
private sealed class TestType90{}
|
||||
private sealed class TestType91{}
|
||||
private sealed class TestType92{}
|
||||
private sealed class TestType93{}
|
||||
private sealed class TestType94{}
|
||||
private sealed class TestType95{}
|
||||
private sealed class TestType96{}
|
||||
private sealed class TestType97{}
|
||||
private sealed class TestType98{}
|
||||
private sealed class TestType99{}
|
||||
// ReSharper restore UnusedType.Local
|
||||
// @formatter:on
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using BenchmarkDotNet.Exporters;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Mathematics;
|
||||
@@ -11,6 +12,9 @@ using BenchmarkDotNet.Reports;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Npgsql;
|
||||
using Npgsql.Internal;
|
||||
using Npgsql.Internal.TypeHandlers;
|
||||
using Npgsql.Internal.TypeHandling;
|
||||
|
||||
namespace Robust.Benchmarks.Exporters;
|
||||
|
||||
@@ -68,6 +72,13 @@ public sealed class SQLExporter : IExporter
|
||||
using var ctx = new BenchmarkContext(builder.Options);
|
||||
try
|
||||
{
|
||||
ctx.Database.OpenConnection();
|
||||
var con = (NpgsqlConnection) ctx.Database.GetDbConnection();
|
||||
con.TypeMapper.AddTypeResolverFactory(new JsonOverrideTypeHandlerResolverFactory(new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
||||
}));
|
||||
|
||||
ctx.Database.Migrate();
|
||||
ctx.BenchmarkRuns.Add(BenchmarkRun.FromSummary(summary, gitHash));
|
||||
ctx.SaveChanges();
|
||||
@@ -81,6 +92,45 @@ public sealed class SQLExporter : IExporter
|
||||
public string Name => "sql";
|
||||
}
|
||||
|
||||
// https://github.com/npgsql/efcore.pg/issues/1107#issuecomment-945126627
|
||||
class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
|
||||
{
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
public JsonOverrideTypeHandlerResolverFactory(JsonSerializerOptions options)
|
||||
=> _options = options;
|
||||
|
||||
public override TypeHandlerResolver Create(NpgsqlConnector connector)
|
||||
=> new JsonOverrideTypeHandlerResolver(connector, _options);
|
||||
|
||||
public override string? GetDataTypeNameByClrType(Type clrType)
|
||||
=> null;
|
||||
|
||||
public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName)
|
||||
=> null;
|
||||
|
||||
class JsonOverrideTypeHandlerResolver : TypeHandlerResolver
|
||||
{
|
||||
readonly JsonHandler _jsonbHandler;
|
||||
|
||||
internal JsonOverrideTypeHandlerResolver(NpgsqlConnector connector, JsonSerializerOptions options)
|
||||
=> _jsonbHandler ??= new JsonHandler(
|
||||
connector.DatabaseInfo.GetPostgresTypeByName("jsonb"),
|
||||
connector.TextEncoding,
|
||||
isJsonb: true,
|
||||
options);
|
||||
|
||||
public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName)
|
||||
=> typeName == "jsonb" ? _jsonbHandler : null;
|
||||
|
||||
public override NpgsqlTypeHandler? ResolveByClrType(Type type)
|
||||
// You can add any user-defined CLR types which you want mapped to jsonb
|
||||
=> type == typeof(JsonDocument) ? _jsonbHandler : null;
|
||||
|
||||
public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName)
|
||||
=> null; // Let the built-in resolver do this
|
||||
}
|
||||
}
|
||||
public sealed class DesignTimeContextFactoryPostgres : IDesignTimeDbContextFactory<BenchmarkContext>
|
||||
{
|
||||
public BenchmarkContext CreateDbContext(string[] args)
|
||||
@@ -101,12 +151,14 @@ public class BenchmarkContext : DbContext
|
||||
|
||||
public class BenchmarkRun
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public ulong Id { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string GitHash { get; set; } = string.Empty;
|
||||
|
||||
[Column(TypeName = "timestamptz")]
|
||||
public DateTime RunDate { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public BenchmarkRunReport[] Reports { get; set; } = Array.Empty<BenchmarkRunReport>();
|
||||
|
||||
|
||||
57
Robust.Benchmarks/Migrations/20220510131430_fix-pk.Designer.cs
generated
Normal file
57
Robust.Benchmarks/Migrations/20220510131430_fix-pk.Designer.cs
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Robust.Benchmarks.Exporters;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Robust.Benchmarks.Migrations
|
||||
{
|
||||
[DbContext(typeof(BenchmarkContext))]
|
||||
[Migration("20220510131430_fix-pk")]
|
||||
partial class fixpk
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Robust.Benchmarks.Exporters.BenchmarkRun", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("GitHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<BenchmarkRunReport[]>("Reports")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("timestamptz");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("BenchmarkRuns");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Robust.Benchmarks/Migrations/20220510131430_fix-pk.cs
Normal file
51
Robust.Benchmarks/Migrations/20220510131430_fix-pk.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Robust.Benchmarks.Migrations
|
||||
{
|
||||
public partial class fixpk : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "RunDate",
|
||||
table: "BenchmarkRuns",
|
||||
type: "timestamptz",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "Date");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "Id",
|
||||
table: "BenchmarkRuns",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "numeric(20,0)")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "RunDate",
|
||||
table: "BenchmarkRuns",
|
||||
type: "Date",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamptz");
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "Id",
|
||||
table: "BenchmarkRuns",
|
||||
type: "numeric(20,0)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer")
|
||||
.OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,11 @@ namespace Robust.Benchmarks.Migrations
|
||||
|
||||
modelBuilder.Entity("Robust.Benchmarks.Exporters.BenchmarkRun", b =>
|
||||
{
|
||||
b.Property<decimal>("Id")
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("numeric(20,0)");
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("GitHash")
|
||||
.IsRequired()
|
||||
@@ -41,7 +43,7 @@ namespace Robust.Benchmarks.Migrations
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("Date");
|
||||
.HasColumnType("timestamptz");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>../bin/Benchmarks</OutputPath>
|
||||
<OutputType>Exe</OutputType>
|
||||
<NoWarn>RA0003</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
|
||||
@@ -18,7 +19,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.0-rc.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\MSBuild\Robust.Engine.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Globalization;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Manager.Result;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
@@ -19,10 +18,10 @@ namespace Robust.Benchmarks.Serialization
|
||||
: new ErrorNode(node, $"Failed parsing int value: {node.Value}");
|
||||
}
|
||||
|
||||
public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node,
|
||||
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null)
|
||||
public int Read(ISerializationManager serializationManager, ValueDataNode node,
|
||||
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null, int _ = default)
|
||||
{
|
||||
return new DeserializedValue<int>(int.Parse(node.Value, CultureInfo.InvariantCulture));
|
||||
return int.Parse(node.Value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public DataNode Write(ISerializationManager serializationManager, int value, bool alwaysWrite = false,
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Robust.Benchmarks.Serialization.Copy
|
||||
|
||||
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
|
||||
|
||||
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
|
||||
Seed = SerializationManager.Read<SeedDataDefinition>(seedMapping);
|
||||
}
|
||||
|
||||
private const string String = "ABC";
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Robust.Benchmarks.Serialization.Definitions
|
||||
name: tobacco
|
||||
seedName: tobacco
|
||||
displayName: tobacco plant
|
||||
plantRsi: Objects/Specific/Hydroponics/tobacco.rsi
|
||||
productPrototypes:
|
||||
- LeavesTobacco
|
||||
harvestRepeat: Repeat
|
||||
@@ -36,7 +37,7 @@ namespace Robust.Benchmarks.Serialization.Definitions
|
||||
Max: 10
|
||||
PotencyDivisor: 10";
|
||||
|
||||
[DataField("id", required: true)] public string ID { get; set; } = default!;
|
||||
[IdDataFieldAttribute] public string ID { get; set; } = default!;
|
||||
|
||||
#region Tracking
|
||||
[DataField("name")] public string Name { get; set; } = string.Empty;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Benchmarks.Serialization.Definitions;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.Serialization.Manager.Result;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
@@ -42,32 +41,32 @@ namespace Robust.Benchmarks.Serialization.Read
|
||||
private ValueDataNode FlagThirtyOne { get; } = new("ThirtyOne");
|
||||
|
||||
[Benchmark]
|
||||
public string? ReadString()
|
||||
public string ReadString()
|
||||
{
|
||||
return SerializationManager.ReadValue<string>(StringNode);
|
||||
return SerializationManager.Read<string>(StringNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int? ReadInteger()
|
||||
public int ReadInteger()
|
||||
{
|
||||
return SerializationManager.ReadValue<int>(IntNode);
|
||||
return SerializationManager.Read<int>(IntNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DataDefinitionWithString? ReadDataDefinitionWithString()
|
||||
public DataDefinitionWithString ReadDataDefinitionWithString()
|
||||
{
|
||||
return SerializationManager.ReadValue<DataDefinitionWithString>(StringDataDefNode);
|
||||
return SerializationManager.Read<DataDefinitionWithString>(StringDataDefNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public SeedDataDefinition? ReadSeedDataDefinition()
|
||||
public SeedDataDefinition ReadSeedDataDefinition()
|
||||
{
|
||||
return SerializationManager.ReadValue<SeedDataDefinition>(SeedNode);
|
||||
return SerializationManager.Read<SeedDataDefinition>(SeedNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("flag")]
|
||||
public DeserializationResult ReadFlagZero()
|
||||
public object? ReadFlagZero()
|
||||
{
|
||||
return SerializationManager.ReadWithTypeSerializer(
|
||||
typeof(int),
|
||||
@@ -77,7 +76,7 @@ namespace Robust.Benchmarks.Serialization.Read
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("flag")]
|
||||
public DeserializationResult ReadThirtyOne()
|
||||
public object? ReadThirtyOne()
|
||||
{
|
||||
return SerializationManager.ReadWithTypeSerializer(
|
||||
typeof(int),
|
||||
@@ -87,7 +86,7 @@ namespace Robust.Benchmarks.Serialization.Read
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("customTypeSerializer")]
|
||||
public DeserializationResult ReadIntegerCustomSerializer()
|
||||
public object? ReadIntegerCustomSerializer()
|
||||
{
|
||||
return SerializationManager.ReadWithTypeSerializer(
|
||||
typeof(int),
|
||||
|
||||
@@ -46,84 +46,84 @@ namespace Robust.Benchmarks.Serialization
|
||||
[BenchmarkCategory("read")]
|
||||
public string[]? ReadEmptyString()
|
||||
{
|
||||
return SerializationManager.ReadValue<string[]>(EmptyNode);
|
||||
return SerializationManager.Read<string[]>(EmptyNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public string[]? ReadOneString()
|
||||
{
|
||||
return SerializationManager.ReadValue<string[]>(OneIntNode);
|
||||
return SerializationManager.Read<string[]>(OneIntNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public string[]? ReadTenStrings()
|
||||
{
|
||||
return SerializationManager.ReadValue<string[]>(TenIntsNode);
|
||||
return SerializationManager.Read<string[]>(TenIntsNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public int[]? ReadEmptyInt()
|
||||
{
|
||||
return SerializationManager.ReadValue<int[]>(EmptyNode);
|
||||
return SerializationManager.Read<int[]>(EmptyNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public int[]? ReadOneInt()
|
||||
{
|
||||
return SerializationManager.ReadValue<int[]>(OneIntNode);
|
||||
return SerializationManager.Read<int[]>(OneIntNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public int[]? ReadTenInts()
|
||||
{
|
||||
return SerializationManager.ReadValue<int[]>(TenIntsNode);
|
||||
return SerializationManager.Read<int[]>(TenIntsNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public DataDefinitionWithString[]? ReadEmptyStringDataDef()
|
||||
{
|
||||
return SerializationManager.ReadValue<DataDefinitionWithString[]>(EmptyNode);
|
||||
return SerializationManager.Read<DataDefinitionWithString[]>(EmptyNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public DataDefinitionWithString[]? ReadOneStringDataDef()
|
||||
{
|
||||
return SerializationManager.ReadValue<DataDefinitionWithString[]>(OneStringDefNode);
|
||||
return SerializationManager.Read<DataDefinitionWithString[]>(OneStringDefNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public DataDefinitionWithString[]? ReadTenStringDataDefs()
|
||||
{
|
||||
return SerializationManager.ReadValue<DataDefinitionWithString[]>(TenStringDefsNode);
|
||||
return SerializationManager.Read<DataDefinitionWithString[]>(TenStringDefsNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public SealedDataDefinitionWithString[]? ReadEmptySealedStringDataDef()
|
||||
{
|
||||
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(EmptyNode);
|
||||
return SerializationManager.Read<SealedDataDefinitionWithString[]>(EmptyNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public SealedDataDefinitionWithString[]? ReadOneSealedStringDataDef()
|
||||
{
|
||||
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(OneStringDefNode);
|
||||
return SerializationManager.Read<SealedDataDefinitionWithString[]>(OneStringDefNode);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("read")]
|
||||
public SealedDataDefinitionWithString[]? ReadTenSealedStringDataDefs()
|
||||
{
|
||||
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(TenStringDefsNode);
|
||||
return SerializationManager.Read<SealedDataDefinitionWithString[]>(TenStringDefsNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Robust.Benchmarks.Serialization.Write
|
||||
|
||||
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
|
||||
|
||||
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
|
||||
Seed = SerializationManager.Read<SeedDataDefinition>(seedMapping);
|
||||
}
|
||||
|
||||
private const string String = "ABC";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -101,7 +101,7 @@ namespace Robust.Client.Animations
|
||||
case double d:
|
||||
return MathHelper.Lerp(d, (double) b, t);
|
||||
case Angle angle:
|
||||
return (Angle) MathHelper.Lerp(angle, (Angle) b, t);
|
||||
return Angle.Lerp(angle, (Angle) b, t);
|
||||
case Color color:
|
||||
return Color.InterpolateBetween(color, (Color) b, t);
|
||||
case int i:
|
||||
|
||||
22
Robust.Client/Audio/Midi/Commands/MidiPanicCommand.cs
Normal file
22
Robust.Client/Audio/Midi/Commands/MidiPanicCommand.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Client.Audio.Midi.Commands;
|
||||
|
||||
public sealed class MidiPanicCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
|
||||
public string Command => "midipanic";
|
||||
public string Description => Loc.GetString("midi-panic-command-description");
|
||||
public string Help => $"{Command}";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
foreach (var renderer in _midiManager.Renderers)
|
||||
{
|
||||
renderer.StopAllNotes();
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Robust.Client/Audio/Midi/IMidiManager.cs
Normal file
62
Robust.Client/Audio/Midi/IMidiManager.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Generic;
|
||||
using NFluidsynth;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
public interface IMidiManager
|
||||
{
|
||||
/// <summary>
|
||||
/// A read-only list of all existing MIDI Renderers.
|
||||
/// </summary>
|
||||
IReadOnlyList<IMidiRenderer> Renderers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, MIDI support is available.
|
||||
/// </summary>
|
||||
bool IsAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Volume, in db.
|
||||
/// </summary>
|
||||
float Volume { get; set; }
|
||||
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This method tries to return a midi renderer ready to be used.
|
||||
/// You only need to set the <see cref="IMidiRenderer.MidiProgram"/> afterwards.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can fail if MIDI support is not available.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// <c>null</c> if MIDI support is not available.
|
||||
/// </returns>
|
||||
IMidiRenderer? GetNewRenderer(bool mono = true);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RobustMidiEvent"/> given a <see cref="MidiEvent"/> and a sequencer tick.
|
||||
/// </summary>
|
||||
RobustMidiEvent FromFluidEvent(MidiEvent midiEvent, uint tick);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="SequencerEvent"/> given a <see cref="RobustMidiEvent"/>.
|
||||
/// Be sure to dispose of the result after you've used it.
|
||||
/// </summary>
|
||||
SequencerEvent ToSequencerEvent(RobustMidiEvent midiEvent);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RobustMidiEvent"/> given a <see cref="SequencerEvent"/> and a sequencer tick.
|
||||
/// </summary>
|
||||
RobustMidiEvent FromSequencerEvent(SequencerEvent midiEvent, uint tick);
|
||||
|
||||
/// <summary>
|
||||
/// Method called every frame.
|
||||
/// Should be used to update positional audio.
|
||||
/// </summary>
|
||||
/// <param name="frameTime"></param>
|
||||
void FrameUpdate(float frameTime);
|
||||
|
||||
void Shutdown();
|
||||
}
|
||||
174
Robust.Client/Audio/Midi/IMidiRenderer.cs
Normal file
174
Robust.Client/Audio/Midi/IMidiRenderer.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
public enum MidiRendererStatus : byte
|
||||
{
|
||||
None,
|
||||
Input,
|
||||
File,
|
||||
}
|
||||
|
||||
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>
|
||||
bool LoopMidi { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This increases all note on velocities to 127.
|
||||
/// </summary>
|
||||
bool VolumeBoost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The midi program (instrument) the renderer is using.
|
||||
/// </summary>
|
||||
byte MidiProgram { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The instrument bank the renderer is using.
|
||||
/// </summary>
|
||||
byte MidiBank { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The soundfont currently selected by the renderer.
|
||||
/// </summary>
|
||||
uint MidiSoundfont { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current status of the renderer.
|
||||
/// "None" if the renderer isn't playing from input or a midi file.
|
||||
/// "Input" if the renderer is playing from midi input.
|
||||
/// "File" if the renderer is playing from a midi file.
|
||||
/// </summary>
|
||||
MidiRendererStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the sound will play in stereo or mono.
|
||||
/// </summary>
|
||||
bool Mono { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to drop messages on the percussion channel.
|
||||
/// </summary>
|
||||
bool DisablePercussionChannel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to drop messages for program change events.
|
||||
/// </summary>
|
||||
bool DisableProgramChangeEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of ticks possible for the MIDI player.
|
||||
/// </summary>
|
||||
int PlayerTotalTick { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets (seeks) the current tick of the MIDI player.
|
||||
/// </summary>
|
||||
int PlayerTick { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current tick of the sequencer.
|
||||
/// </summary>
|
||||
uint SequencerTick { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Time Scale of the sequencer in ticks per second. Default is 1000 for 1 tick per millisecond.
|
||||
/// </summary>
|
||||
double SequencerTimeScale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Start listening for midi input.
|
||||
/// </summary>
|
||||
bool OpenInput();
|
||||
|
||||
/// <summary>
|
||||
/// Start playing a midi file.
|
||||
/// </summary>
|
||||
/// <param name="buffer">Bytes of the midi file</param>
|
||||
bool OpenMidi(ReadOnlySpan<byte> buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Stops listening for midi input.
|
||||
/// </summary>
|
||||
bool CloseInput();
|
||||
|
||||
/// <summary>
|
||||
/// Stops playing midi files.
|
||||
/// </summary>
|
||||
bool CloseMidi();
|
||||
|
||||
/// <summary>
|
||||
/// Stops all notes being played currently.
|
||||
/// </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>
|
||||
void LoadSoundfont(string filename, bool resetPresets = false);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked whenever a new midi event is registered.
|
||||
/// </summary>
|
||||
event Action<RobustMidiEvent> OnMidiEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the midi player finishes playing a song.
|
||||
/// </summary>
|
||||
event Action OnMidiPlayerFinished;
|
||||
|
||||
/// <summary>
|
||||
/// The entity whose position will be used for positional audio.
|
||||
/// This is only used if <see cref="Mono"/> is set to True.
|
||||
/// </summary>
|
||||
EntityUid? TrackingEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The position that will be used for positional audio.
|
||||
/// This is only used if <see cref="Mono"/> is set to True
|
||||
/// and <see cref="TrackingEntity"/> is null.
|
||||
/// </summary>
|
||||
EntityCoordinates? TrackingCoordinates { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Send a midi event for the renderer to play.
|
||||
/// </summary>
|
||||
/// <param name="midiEvent">The midi event to be played</param>
|
||||
void SendMidiEvent(RobustMidiEvent midiEvent);
|
||||
|
||||
/// <summary>
|
||||
/// Schedule a MIDI event to be played at a later time.
|
||||
/// </summary>
|
||||
/// <param name="midiEvent">the midi event in question</param>
|
||||
/// <param name="time"></param>
|
||||
/// <param name="absolute"></param>
|
||||
void ScheduleMidiEvent(RobustMidiEvent midiEvent, uint time, bool absolute);
|
||||
|
||||
/// <summary>
|
||||
/// Actually disposes of this renderer. Do NOT use outside the MIDI thread.
|
||||
/// </summary>
|
||||
internal void InternalDispose();
|
||||
}
|
||||
155
Robust.Client/Audio/Midi/MidiManager.Conversion.cs
Normal file
155
Robust.Client/Audio/Midi/MidiManager.Conversion.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using NFluidsynth;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed partial class MidiManager
|
||||
{
|
||||
public RobustMidiEvent FromFluidEvent(MidiEvent midiEvent, uint tick)
|
||||
{
|
||||
var status = RobustMidiEvent.MakeStatus((byte) midiEvent.Channel, (byte) midiEvent.Type);
|
||||
|
||||
// Control is always the first data byte. Value is always the second data byte. Fluidsynth's API ain't great.
|
||||
var data1 = (byte) midiEvent.Control;
|
||||
var data2 = (byte) midiEvent.Value;
|
||||
|
||||
// PitchBend is handled specially.
|
||||
if (midiEvent.Type == (int) RobustMidiCommand.PitchBend)
|
||||
{
|
||||
// We pack pitch into both data values.
|
||||
var pitch = (ushort) midiEvent.Pitch;
|
||||
data1 = (byte) pitch;
|
||||
data2 = (byte) (pitch >> 8);
|
||||
}
|
||||
|
||||
return new RobustMidiEvent(status, data1, data2, tick);
|
||||
}
|
||||
|
||||
public SequencerEvent ToSequencerEvent(RobustMidiEvent midiEvent)
|
||||
{
|
||||
var sequencerEvent = new SequencerEvent();
|
||||
|
||||
switch (midiEvent.MidiCommand)
|
||||
{
|
||||
case RobustMidiCommand.NoteOff:
|
||||
sequencerEvent.NoteOff(midiEvent.Channel, midiEvent.Key);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.NoteOn:
|
||||
sequencerEvent.NoteOn(midiEvent.Channel, midiEvent.Key, midiEvent.Velocity);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.AfterTouch:
|
||||
sequencerEvent.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.ControlChange:
|
||||
sequencerEvent.ControlChange(midiEvent.Channel, midiEvent.Control, midiEvent.Value);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.ProgramChange:
|
||||
sequencerEvent.ProgramChange(midiEvent.Channel, midiEvent.Program);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.ChannelPressure:
|
||||
sequencerEvent.ChannelPressure(midiEvent.Channel, midiEvent.Value);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.PitchBend:
|
||||
sequencerEvent.PitchBend(midiEvent.Channel, midiEvent.Pitch);
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.SystemMessage:
|
||||
switch (midiEvent.Control)
|
||||
{
|
||||
case 0x0 when midiEvent.Status == 0xFF:
|
||||
sequencerEvent.SystemReset();
|
||||
break;
|
||||
|
||||
case 0x0B:
|
||||
sequencerEvent.AllNotesOff(midiEvent.Channel);
|
||||
break;
|
||||
|
||||
default:
|
||||
_midiSawmill.Warning($"Tried to convert unsupported event to sequencer event:\n{midiEvent}");
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
_midiSawmill.Warning($"Tried to convert unsupported event to sequencer event:\n{midiEvent}");
|
||||
break;
|
||||
}
|
||||
|
||||
return sequencerEvent;
|
||||
}
|
||||
|
||||
public RobustMidiEvent FromSequencerEvent(SequencerEvent midiEvent, uint tick)
|
||||
{
|
||||
byte channel = (byte) midiEvent.Channel;
|
||||
RobustMidiCommand command = 0x0;
|
||||
byte data1 = 0;
|
||||
byte data2 = 0;
|
||||
|
||||
switch (midiEvent.Type)
|
||||
{
|
||||
case FluidSequencerEventType.NoteOn:
|
||||
command = RobustMidiCommand.NoteOn;
|
||||
data1 = (byte) midiEvent.Key;
|
||||
data2 = (byte) midiEvent.Velocity;
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.NoteOff:
|
||||
command = RobustMidiCommand.NoteOff;
|
||||
data1 = (byte) midiEvent.Key;
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.PitchBend:
|
||||
command = RobustMidiCommand.PitchBend;
|
||||
// We pack pitch into both data values
|
||||
var pitch = (ushort) midiEvent.Pitch;
|
||||
data1 = (byte) pitch;
|
||||
data2 = (byte) (pitch >> 8);
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.ProgramChange:
|
||||
command = RobustMidiCommand.ProgramChange;
|
||||
data1 = (byte) midiEvent.Program;
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.KeyPressure:
|
||||
command = RobustMidiCommand.AfterTouch;
|
||||
data1 = (byte) midiEvent.Key;
|
||||
data2 = (byte) midiEvent.Value;
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.ControlChange:
|
||||
command = RobustMidiCommand.ControlChange;
|
||||
data1 = (byte) midiEvent.Control;
|
||||
data2 = (byte) midiEvent.Value;
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.ChannelPressure:
|
||||
command = RobustMidiCommand.ChannelPressure;
|
||||
data1 = (byte) midiEvent.Value;
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.AllNotesOff:
|
||||
command = RobustMidiCommand.SystemMessage;
|
||||
data1 = 0x0B;
|
||||
break;
|
||||
|
||||
case FluidSequencerEventType.SystemReset:
|
||||
command = RobustMidiCommand.SystemMessage;
|
||||
channel = 0x0F;
|
||||
break;
|
||||
|
||||
default:
|
||||
_midiSawmill.Error($"Unsupported Sequencer Event: {tick:D8}: {SequencerEventToString(midiEvent)}");
|
||||
break;
|
||||
}
|
||||
|
||||
return new RobustMidiEvent(RobustMidiEvent.MakeStatus(channel, (byte)command), data1, data2, tick);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using NFluidsynth;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -15,404 +16,440 @@ 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
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
public interface IMidiManager
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
|
||||
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
[Dependency] private readonly ILogManager _logger = default!;
|
||||
|
||||
private SharedPhysicsSystem _broadPhaseSystem = default!;
|
||||
|
||||
public IReadOnlyList<IMidiRenderer> Renderers
|
||||
{
|
||||
/// <summary>
|
||||
/// This method tries to return a midi renderer ready to be used.
|
||||
/// You only need to set the <see cref="IMidiRenderer.MidiProgram"/> afterwards.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can fail if MIDI support is not available.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// <c>null</c> if MIDI support is not available.
|
||||
/// </returns>
|
||||
IMidiRenderer? GetNewRenderer();
|
||||
|
||||
/// <summary>
|
||||
/// Method called every frame.
|
||||
/// Should be used to update positional audio.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
bool IsAvailable { get; }
|
||||
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
void Shutdown();
|
||||
get
|
||||
{
|
||||
lock (_renderers)
|
||||
{
|
||||
// Perform a copy. Sadly, we can't return a reference to the original list due to threading concerns.
|
||||
return _renderers.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MidiManager : IMidiManager
|
||||
[ViewVariables]
|
||||
public bool IsAvailable
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
|
||||
|
||||
private SharedPhysicsSystem _broadPhaseSystem = default!;
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsAvailable
|
||||
get
|
||||
{
|
||||
get
|
||||
{
|
||||
InitializeFluidsynth();
|
||||
InitializeFluidsynth();
|
||||
|
||||
return FluidsynthInitialized;
|
||||
}
|
||||
return FluidsynthInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
|
||||
// Not reliable until Fluidsynth is initialized!
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
if (MathHelper.CloseToPercent(_volume, value))
|
||||
return;
|
||||
|
||||
_cfgMan.SetCVar(CVars.MidiVolume, value);
|
||||
_volumeDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string[] LinuxSoundfonts =
|
||||
{
|
||||
"/usr/share/soundfonts/default.sf2",
|
||||
"/usr/share/soundfonts/default.dls",
|
||||
"/usr/share/soundfonts/FluidR3_GM.sf2",
|
||||
"/usr/share/soundfonts/freepats-general-midi.sf2",
|
||||
"/usr/share/sounds/sf2/default.sf2",
|
||||
"/usr/share/sounds/sf2/default.dls",
|
||||
"/usr/share/sounds/sf2/FluidR3_GM.sf2",
|
||||
"/usr/share/sounds/sf2/TimGM6mb.sf2",
|
||||
};
|
||||
|
||||
private const string WindowsSoundfont = @"C:\WINDOWS\system32\drivers\gm.dls";
|
||||
|
||||
private const string OsxSoundfont =
|
||||
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
|
||||
|
||||
private const string FallbackSoundfont = "/Midi/fallback.sf2";
|
||||
|
||||
private const float MaxDistanceForOcclusion = 1000;
|
||||
|
||||
private static ResourcePath CustomSoundfontDirectory = new ResourcePath("/soundfonts/");
|
||||
|
||||
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
|
||||
|
||||
private bool FluidsynthInitialized;
|
||||
private bool _failedInitialize;
|
||||
|
||||
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
private void InitializeFluidsynth()
|
||||
{
|
||||
if (FluidsynthInitialized || _failedInitialize) return;
|
||||
|
||||
_volume = _cfgMan.GetCVar(CVars.MidiVolume);
|
||||
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
|
||||
{
|
||||
_volume = value;
|
||||
_volumeDirty = true;
|
||||
}, true);
|
||||
|
||||
_midiSawmill = _logger.GetSawmill("midi");
|
||||
_midiSawmill.Level = LogLevel.Info;
|
||||
_sawmill = _logger.GetSawmill("midi.fluidsynth");
|
||||
_loggerDelegate = LoggerDelegate;
|
||||
|
||||
if (!_resourceManager.UserData.Exists(CustomSoundfontDirectory))
|
||||
{
|
||||
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
|
||||
}
|
||||
// not a directory, preserve the old file and create an actual directory
|
||||
else if (!_resourceManager.UserData.IsDir(CustomSoundfontDirectory))
|
||||
{
|
||||
_resourceManager.UserData.Rename(CustomSoundfontDirectory, CustomSoundfontDirectory.WithName(CustomSoundfontDirectory.Filename + ".old"));
|
||||
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
|
||||
}
|
||||
|
||||
[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;
|
||||
|
||||
// Not reliable until Fluidsynth is initialized!
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Volume
|
||||
try
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
if (MathHelper.CloseToPercent(_volume, value))
|
||||
return;
|
||||
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
|
||||
|
||||
_cfgMan.SetCVar(CVars.MidiVolume, value);
|
||||
_volumeDirty = true;
|
||||
}
|
||||
_settings = new Settings();
|
||||
_settings["synth.sample-rate"].DoubleValue = 44100;
|
||||
_settings["player.timing-source"].StringValue = "sample";
|
||||
_settings["synth.lock-memory"].IntValue = 0;
|
||||
_settings["synth.threadsafe-api"].IntValue = 1;
|
||||
_settings["synth.gain"].DoubleValue = 1.0d;
|
||||
_settings["synth.polyphony"].IntValue = 1024;
|
||||
_settings["synth.cpu-cores"].IntValue = 2;
|
||||
_settings["synth.midi-channels"].IntValue = 16;
|
||||
_settings["synth.overflow.age"].DoubleValue = 3000;
|
||||
_settings["audio.driver"].StringValue = "file";
|
||||
_settings["audio.periods"].IntValue = 8;
|
||||
_settings["audio.period-size"].IntValue = 4096;
|
||||
_settings["midi.autoconnect"].IntValue = 1;
|
||||
_settings["player.reset-synth"].IntValue = 0;
|
||||
_settings["synth.midi-bank-select"].StringValue = "gm";
|
||||
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_midiSawmill.Warning(
|
||||
"Failed to initialize fluidsynth due to exception, disabling MIDI support:\n{0}", e);
|
||||
_failedInitialize = true;
|
||||
return;
|
||||
}
|
||||
|
||||
private static readonly string[] LinuxSoundfonts =
|
||||
{
|
||||
"/usr/share/soundfonts/default.sf2",
|
||||
"/usr/share/soundfonts/FluidR3_GM.sf2",
|
||||
"/usr/share/soundfonts/freepats-general-midi.sf2",
|
||||
"/usr/share/sounds/sf2/default.sf2",
|
||||
"/usr/share/sounds/sf2/FluidR3_GM.sf2",
|
||||
"/usr/share/sounds/sf2/TimGM6mb.sf2",
|
||||
_midiThread = new Thread(ThreadUpdate);
|
||||
_midiThread.Start();
|
||||
|
||||
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
|
||||
FluidsynthInitialized = true;
|
||||
}
|
||||
|
||||
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
|
||||
{
|
||||
var rLevel = level switch {
|
||||
NFluidsynth.Logger.LogLevel.Panic => LogLevel.Error,
|
||||
NFluidsynth.Logger.LogLevel.Error => LogLevel.Error,
|
||||
NFluidsynth.Logger.LogLevel.Warning => LogLevel.Warning,
|
||||
NFluidsynth.Logger.LogLevel.Information => LogLevel.Info,
|
||||
NFluidsynth.Logger.LogLevel.Debug => LogLevel.Debug,
|
||||
_ => LogLevel.Debug
|
||||
};
|
||||
_sawmill.Log(rLevel, message);
|
||||
}
|
||||
|
||||
private const string WindowsSoundfont = @"C:\WINDOWS\system32\drivers\gm.dls";
|
||||
|
||||
private const string OsxSoundfont =
|
||||
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
|
||||
|
||||
private const string FallbackSoundfont = "/Midi/fallback.sf2";
|
||||
|
||||
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
|
||||
|
||||
private bool FluidsynthInitialized;
|
||||
private bool _failedInitialize;
|
||||
|
||||
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
private void InitializeFluidsynth()
|
||||
public IMidiRenderer? GetNewRenderer(bool mono = true)
|
||||
{
|
||||
if (!FluidsynthInitialized)
|
||||
{
|
||||
if (FluidsynthInitialized || _failedInitialize) return;
|
||||
InitializeFluidsynth();
|
||||
|
||||
_volume = _cfgMan.GetCVar(CVars.MidiVolume);
|
||||
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
|
||||
if (!FluidsynthInitialized) // init failed
|
||||
{
|
||||
_volume = value;
|
||||
_volumeDirty = true;
|
||||
}, true);
|
||||
|
||||
_midiSawmill = Logger.GetSawmill("midi");
|
||||
_sawmill = Logger.GetSawmill("midi.fluidsynth");
|
||||
_loggerDelegate = LoggerDelegate;
|
||||
|
||||
try
|
||||
{
|
||||
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
|
||||
|
||||
_settings = new Settings();
|
||||
_settings["synth.sample-rate"].DoubleValue = 44100;
|
||||
_settings["player.timing-source"].StringValue = "sample";
|
||||
_settings["synth.lock-memory"].IntValue = 0;
|
||||
_settings["synth.threadsafe-api"].IntValue = 1;
|
||||
_settings["synth.gain"].DoubleValue = 1.0d;
|
||||
_settings["synth.polyphony"].IntValue = 1024;
|
||||
_settings["synth.cpu-cores"].IntValue = 2;
|
||||
_settings["synth.overflow.age"].DoubleValue = 3000;
|
||||
_settings["audio.driver"].StringValue = "file";
|
||||
_settings["audio.periods"].IntValue = 8;
|
||||
_settings["audio.period-size"].IntValue = 4096;
|
||||
_settings["midi.autoconnect"].IntValue = 1;
|
||||
_settings["player.reset-synth"].IntValue = 0;
|
||||
_settings["synth.midi-bank-select"].StringValue = "gm";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.WarningS("midi",
|
||||
"Failed to initialize fluidsynth due to exception, disabling MIDI support:\n{0}", e);
|
||||
_failedInitialize = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_midiThread = new Thread(ThreadUpdate);
|
||||
_midiThread.Start();
|
||||
|
||||
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
|
||||
FluidsynthInitialized = true;
|
||||
}
|
||||
|
||||
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
|
||||
{
|
||||
var rLevel = level switch {
|
||||
NFluidsynth.Logger.LogLevel.Panic => LogLevel.Error,
|
||||
NFluidsynth.Logger.LogLevel.Error => LogLevel.Error,
|
||||
NFluidsynth.Logger.LogLevel.Warning => LogLevel.Warning,
|
||||
NFluidsynth.Logger.LogLevel.Information => LogLevel.Info,
|
||||
NFluidsynth.Logger.LogLevel.Debug => LogLevel.Debug,
|
||||
_ => LogLevel.Debug
|
||||
};
|
||||
_sawmill.Log(rLevel, message);
|
||||
}
|
||||
|
||||
public IMidiRenderer? GetNewRenderer()
|
||||
{
|
||||
if (!FluidsynthInitialized)
|
||||
{
|
||||
InitializeFluidsynth();
|
||||
|
||||
if (!FluidsynthInitialized) // init failed
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var soundfontLoader = SoundFontLoader.NewDefaultSoundFontLoader(_settings);
|
||||
|
||||
// Just making double sure these don't get GC'd.
|
||||
// They shouldn't, MidiRenderer keeps a ref, but making sure...
|
||||
var handle = GCHandle.Alloc(soundfontLoader);
|
||||
|
||||
try
|
||||
{
|
||||
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
|
||||
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader);
|
||||
|
||||
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls") continue;
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
renderer.LoadSoundfont(FallbackSoundfont);
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
foreach (var filepath in LinuxSoundfonts)
|
||||
{
|
||||
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
renderer.LoadSoundfont(filepath, true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
renderer.LoadSoundfont(OsxSoundfont, true);
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
renderer.LoadSoundfont(WindowsSoundfont, true);
|
||||
}
|
||||
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
_renderers.Add(renderer);
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void FrameUpdate(float frameTime)
|
||||
var soundfontLoader = SoundFontLoader.NewDefaultSoundFontLoader(_settings);
|
||||
|
||||
// Just making double sure these don't get GC'd.
|
||||
// They shouldn't, MidiRenderer keeps a ref, but making sure...
|
||||
var handle = GCHandle.Alloc(soundfontLoader);
|
||||
|
||||
try
|
||||
{
|
||||
if (!FluidsynthInitialized)
|
||||
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
|
||||
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
|
||||
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
renderer.LoadSoundfont(FallbackSoundfont);
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update positions of streams every frame.
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
continue;
|
||||
|
||||
if(_volumeDirty)
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
if (!renderer.Mono)
|
||||
foreach (var filepath in LinuxSoundfonts)
|
||||
{
|
||||
renderer.Source.SetGlobal();
|
||||
continue;
|
||||
}
|
||||
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
|
||||
|
||||
MapCoordinates? mapPos = null;
|
||||
var trackingEntity = renderer.TrackingEntity != null && !_entityManager.Deleted(renderer.TrackingEntity);
|
||||
if (trackingEntity)
|
||||
{
|
||||
renderer.TrackingCoordinates = _entityManager.GetComponent<TransformComponent>(renderer.TrackingEntity!.Value).Coordinates;
|
||||
}
|
||||
|
||||
if (renderer.TrackingCoordinates != null)
|
||||
{
|
||||
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
|
||||
if (mapPos != null && mapPos.Value.MapId == _eyeManager.CurrentMap)
|
||||
{
|
||||
var pos = mapPos.Value;
|
||||
|
||||
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length > 0)
|
||||
try
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
pos.Position,
|
||||
sourceRelative.Normalized,
|
||||
OcclusionCollisionMask),
|
||||
sourceRelative.Length,
|
||||
renderer.TrackingEntity);
|
||||
renderer.LoadSoundfont(filepath, true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
|
||||
if (!renderer.Source.SetPosition(pos.Position))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackingEntity)
|
||||
{
|
||||
renderer.Source.SetVelocity(renderer.TrackingEntity!.Value.GlobalLinearVelocity());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_volumeDirty = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main method for the thread rendering the midi audio.
|
||||
/// </summary>
|
||||
private void ThreadUpdate()
|
||||
{
|
||||
while (_alive)
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
lock (_renderers)
|
||||
{
|
||||
for (var i = 0; i < _renderers.Count; i++)
|
||||
{
|
||||
var renderer = _renderers[i];
|
||||
if (!renderer.Disposed)
|
||||
renderer.Render();
|
||||
else
|
||||
{
|
||||
renderer.InternalDispose();
|
||||
_renderers.Remove(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
renderer.LoadSoundfont(OsxSoundfont, true);
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
renderer.LoadSoundfont(WindowsSoundfont, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_alive = false;
|
||||
_midiThread?.Join();
|
||||
_settings?.Dispose();
|
||||
// Load content-specific custom soundfonts, which could override the system/fallback soundfont.
|
||||
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls") continue;
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
// Load every soundfont from the user data directory last, since those may override any other soundfont.
|
||||
_midiSawmill.Debug($"loading soundfonts from {CustomSoundfontDirectory.ToRelativePath().ToString()}/*");
|
||||
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath().ToString()}/*").Item1;
|
||||
foreach (var soundfont in enumerator)
|
||||
{
|
||||
if (soundfont.Extension != "sf2" && soundfont.Extension != "dls") continue;
|
||||
_midiSawmill.Debug($"loading soundfont {soundfont}");
|
||||
renderer.LoadSoundfont(soundfont.ToString());
|
||||
}
|
||||
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
renderer?.Dispose();
|
||||
}
|
||||
_renderers.Add(renderer);
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public void FrameUpdate(float frameTime)
|
||||
{
|
||||
if (!FluidsynthInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update positions of streams every frame.
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
continue;
|
||||
|
||||
if(_volumeDirty)
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
if (!renderer.Mono)
|
||||
{
|
||||
renderer.Source.SetGlobal();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (FluidsynthInitialized && !_failedInitialize)
|
||||
MapCoordinates? mapPos = null;
|
||||
var trackingEntity = renderer.TrackingEntity != null && !_entityManager.Deleted(renderer.TrackingEntity);
|
||||
if (trackingEntity)
|
||||
{
|
||||
NFluidsynth.Logger.SetLoggerMethod(null);
|
||||
renderer.TrackingCoordinates = _entityManager.GetComponent<TransformComponent>(renderer.TrackingEntity!.Value).Coordinates;
|
||||
}
|
||||
|
||||
if (renderer.TrackingCoordinates != null)
|
||||
{
|
||||
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
|
||||
if (mapPos != null && mapPos.Value.MapId == _eyeManager.CurrentMap)
|
||||
{
|
||||
var pos = mapPos.Value;
|
||||
|
||||
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),
|
||||
MathF.Min(sourceRelative.Length, MaxDistanceForOcclusion),
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
|
||||
if (!renderer.Source.SetPosition(pos.Position))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackingEntity)
|
||||
{
|
||||
renderer.Source.SetVelocity(renderer.TrackingEntity!.Value.GlobalLinearVelocity());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to load soundfonts.
|
||||
/// </summary>
|
||||
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
|
||||
_volumeDirty = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main method for the thread rendering the midi audio.
|
||||
/// </summary>
|
||||
private void ThreadUpdate()
|
||||
{
|
||||
while (_alive)
|
||||
{
|
||||
private readonly Dictionary<int, Stream> _openStreams = new();
|
||||
private int _nextStreamId = 1;
|
||||
|
||||
public override IntPtr Open(string filename)
|
||||
lock (_renderers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
for (var i = 0; i < _renderers.Count; i++)
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
var renderer = _renderers[i];
|
||||
if (!renderer.Disposed)
|
||||
renderer.Render();
|
||||
else
|
||||
{
|
||||
renderer.InternalDispose();
|
||||
_renderers.Remove(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stream? stream;
|
||||
var resourceCache = IoCManager.Resolve<IResourceCache>();
|
||||
var resourcePath = new ResourcePath(filename);
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (resourcePath.IsRooted && resourceCache.ContentFileExists(filename))
|
||||
public void Shutdown()
|
||||
{
|
||||
_alive = false;
|
||||
_midiThread?.Join();
|
||||
_settings?.Dispose();
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
renderer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (FluidsynthInitialized && !_failedInitialize)
|
||||
{
|
||||
NFluidsynth.Logger.SetLoggerMethod(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method to get a human-readable representation of a <see cref="SequencerEvent"/>.
|
||||
/// </summary>
|
||||
internal static string SequencerEventToString(SequencerEvent midiEvent)
|
||||
{
|
||||
// ReSharper disable once UseStringInterpolation
|
||||
return string.Format(
|
||||
"{0} chan:{1:D2} key:{2:D5} bank:{3:D2} ctrl:{4:D5} dur:{5:D5} pitch:{6:D5} prog:{7:D3} val:{8:D5} vel:{9:D5}",
|
||||
midiEvent.Type.ToString().PadLeft(22),
|
||||
midiEvent.Channel,
|
||||
midiEvent.Key,
|
||||
midiEvent.Bank,
|
||||
midiEvent.Control,
|
||||
midiEvent.Duration,
|
||||
midiEvent.Pitch,
|
||||
midiEvent.Program,
|
||||
midiEvent.Value,
|
||||
midiEvent.Velocity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to load soundfonts.
|
||||
/// </summary>
|
||||
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
|
||||
{
|
||||
private readonly Dictionary<int, Stream> _openStreams = new();
|
||||
private int _nextStreamId = 1;
|
||||
|
||||
public override IntPtr Open(string filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
Stream? stream;
|
||||
var resourceCache = IoCManager.Resolve<IResourceCache>();
|
||||
var resourcePath = new ResourcePath(filename);
|
||||
|
||||
if (resourcePath.IsRooted)
|
||||
{
|
||||
// is it in content?
|
||||
if (resourceCache.ContentFileExists(filename))
|
||||
{
|
||||
if (!resourceCache.TryContentFileRead(filename, out stream))
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
// is it in userdata?
|
||||
else if (resourceCache.UserData.Exists(resourcePath))
|
||||
{
|
||||
stream = resourceCache.UserData.OpenRead(resourcePath);
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
@@ -421,73 +458,81 @@ namespace Robust.Client.Audio.Midi
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var id = _nextStreamId++;
|
||||
|
||||
_openStreams.Add(id, stream);
|
||||
|
||||
return (IntPtr) id;
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
|
||||
{
|
||||
var length = (int) count;
|
||||
var span = new Span<byte>(buf.ToPointer(), length);
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
var id = _nextStreamId++;
|
||||
|
||||
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
|
||||
try
|
||||
_openStreams.Add(id, stream);
|
||||
|
||||
return (IntPtr) id;
|
||||
}
|
||||
|
||||
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
|
||||
{
|
||||
var length = (int) count;
|
||||
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).
|
||||
if (count < 1024)
|
||||
{
|
||||
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
|
||||
if (count < 1024)
|
||||
{
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
|
||||
stream.ReadExact(buffer);
|
||||
stream.ReadExact(buffer);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
stream.Seek(offset, origin);
|
||||
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override int Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) stream.Position;
|
||||
}
|
||||
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
stream.Seek(offset, origin);
|
||||
|
||||
stream.Dispose();
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override int Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
return (int) stream.Position;
|
||||
}
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
|
||||
stream.Dispose();
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -45,6 +46,8 @@ namespace Robust.Client
|
||||
|
||||
public string? LastDisconnectReason { get; private set; }
|
||||
|
||||
private (TimeSpan, GameTick) _timeBase;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
@@ -52,20 +55,34 @@ namespace Robust.Client
|
||||
_net.ConnectFailed += OnConnectFailed;
|
||||
_net.Disconnect += OnNetDisconnect;
|
||||
|
||||
_net.RegisterNetMessage<MsgSyncTimeBase>(
|
||||
SyncTimeBase,
|
||||
NetMessageAccept.Handshake | NetMessageAccept.Client);
|
||||
|
||||
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
|
||||
|
||||
_playMan.Initialize();
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void TickRateChanged(int tickrate)
|
||||
private void SyncTimeBase(MsgSyncTimeBase message)
|
||||
{
|
||||
Logger.DebugS("client", $"Synchronized time base: {message.Tick}: {message.Time}");
|
||||
|
||||
if (RunLevel >= ClientRunLevel.Connected)
|
||||
_timing.TimeBase = (message.Time, message.Tick);
|
||||
else
|
||||
_timeBase = (message.Time, message.Tick);
|
||||
}
|
||||
|
||||
private void TickRateChanged(int tickrate, in CVarChangeInfo info)
|
||||
{
|
||||
if (GameInfo != null)
|
||||
{
|
||||
GameInfo.TickRate = (byte) tickrate;
|
||||
}
|
||||
|
||||
_timing.TickRate = (byte) tickrate;
|
||||
_timing.SetTickRateAt((byte) tickrate, info.TickChanged);
|
||||
Logger.InfoS("client", $"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
|
||||
}
|
||||
|
||||
@@ -74,7 +91,7 @@ namespace Robust.Client
|
||||
{
|
||||
if (RunLevel == ClientRunLevel.Connecting)
|
||||
{
|
||||
_net.Shutdown("Client mashing that connect button.");
|
||||
_net.Reset("Client mashing that connect button.");
|
||||
Reset();
|
||||
}
|
||||
|
||||
@@ -213,7 +230,7 @@ namespace Robust.Client
|
||||
_entityManager.Startup();
|
||||
_mapManager.Startup();
|
||||
|
||||
_timing.ResetSimTime();
|
||||
_timing.ResetSimTime(_timeBase);
|
||||
_timing.Paused = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Profiling;
|
||||
using Robust.Client.Prototypes;
|
||||
using Robust.Client.Reflection;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -61,6 +62,7 @@ namespace Robust.Client
|
||||
IoCManager.Register<IResourceCache, ResourceCache>();
|
||||
IoCManager.Register<IResourceCacheInternal, ResourceCache>();
|
||||
IoCManager.Register<IClientNetManager, NetManager>();
|
||||
IoCManager.Register<EntityManager, ClientEntityManager>();
|
||||
IoCManager.Register<IClientEntityManager, ClientEntityManager>();
|
||||
IoCManager.Register<IClientEntityManagerInternal, ClientEntityManager>();
|
||||
IoCManager.Register<IEntityNetworkManager, ClientEntityManager>();
|
||||
@@ -75,6 +77,7 @@ namespace Robust.Client
|
||||
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
|
||||
IoCManager.Register<IMidiManager, MidiManager>();
|
||||
IoCManager.Register<IAuthManager, AuthManager>();
|
||||
IoCManager.Register<ProfViewManager>();
|
||||
IoCManager.Register<IPhysicsManager, PhysicsManager>();
|
||||
switch (mode)
|
||||
{
|
||||
|
||||
94
Robust.Client/Console/ClientConsoleHost.Completions.cs
Normal file
94
Robust.Client/Console/ClientConsoleHost.Completions.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Network.Messages;
|
||||
|
||||
namespace Robust.Client.Console;
|
||||
|
||||
internal sealed partial class ClientConsoleHost
|
||||
{
|
||||
private readonly Dictionary<int, PendingCompletion> _completionsPending = new();
|
||||
private int _completionSeq;
|
||||
|
||||
|
||||
public async Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel)
|
||||
{
|
||||
// Last element is the command currently being typed. May be empty.
|
||||
|
||||
// Logger.Debug($"Running completions: {string.Join(", ", args)}");
|
||||
|
||||
var delay = _cfg.GetCVar(CVars.ConCompletionDelay);
|
||||
if (delay > 0)
|
||||
await Task.Delay((int)(delay * 1000), cancel);
|
||||
|
||||
return await CalcCompletions(args, cancel);
|
||||
}
|
||||
|
||||
private Task<CompletionResult> CalcCompletions(List<string> args, CancellationToken cancel)
|
||||
{
|
||||
if (args.Count == 1)
|
||||
{
|
||||
// Typing out command name, handle this ourselves.
|
||||
var cmdOptions = CompletionResult.FromOptions(
|
||||
RegisteredCommands.Values.Select(c => new CompletionOption(c.Command, c.Description)));
|
||||
return Task.FromResult(cmdOptions);
|
||||
}
|
||||
|
||||
if (!RegisteredCommands.TryGetValue(args[0], out var cmd))
|
||||
return Task.FromResult(CompletionResult.Empty);
|
||||
|
||||
return cmd.GetCompletionAsync(LocalShell, args.ToArray()[1..], cancel).AsTask();
|
||||
}
|
||||
|
||||
private Task<CompletionResult> DoServerCompletions(List<string> args, CancellationToken cancel)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<CompletionResult>();
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel);
|
||||
var seq = _completionSeq++;
|
||||
|
||||
var pending = new PendingCompletion
|
||||
{
|
||||
Cts = cts,
|
||||
Tcs = tcs
|
||||
};
|
||||
|
||||
var msg = new MsgConCompletion
|
||||
{
|
||||
Args = args.ToArray(),
|
||||
Seq = seq
|
||||
};
|
||||
|
||||
cts.Token.Register(() =>
|
||||
{
|
||||
tcs.SetCanceled(cts.Token);
|
||||
cts.Dispose();
|
||||
_completionsPending.Remove(seq);
|
||||
}, true);
|
||||
|
||||
NetManager.ClientSendMessage(msg);
|
||||
|
||||
_completionsPending.Add(seq, pending);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private void ProcessCompletionResp(MsgConCompletionResp message)
|
||||
{
|
||||
if (!_completionsPending.TryGetValue(message.Seq, out var pending))
|
||||
return;
|
||||
|
||||
pending.Cts.Dispose();
|
||||
pending.Tcs.SetResult(message.Result);
|
||||
|
||||
_completionsPending.Remove(message.Seq);
|
||||
}
|
||||
|
||||
private struct PendingCompletion
|
||||
{
|
||||
public TaskCompletionSource<CompletionResult> Tcs;
|
||||
public CancellationTokenSource Cts;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Log;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
@@ -41,18 +46,23 @@ namespace Robust.Client.Console
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IClientConsoleHost" />
|
||||
internal sealed class ClientConsoleHost : ConsoleHost, IClientConsoleHost
|
||||
internal sealed partial class ClientConsoleHost : ConsoleHost, IClientConsoleHost, IConsoleHostInternal
|
||||
{
|
||||
[Dependency] private readonly IClientConGroupController _conGroup = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private bool _requestedCommands;
|
||||
|
||||
public ClientConsoleHost() : base(isServer: false) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
NetManager.RegisterNetMessage<MsgConCmdReg>(HandleConCmdReg);
|
||||
NetManager.RegisterNetMessage<MsgConCmdAck>(HandleConCmdAck);
|
||||
NetManager.RegisterNetMessage<MsgConCmd>(ProcessCommand);
|
||||
NetManager.RegisterNetMessage<MsgConCompletion>();
|
||||
NetManager.RegisterNetMessage<MsgConCompletionResp>(ProcessCompletionResp);
|
||||
|
||||
_requestedCommands = false;
|
||||
NetManager.Connected += OnNetworkConnected;
|
||||
@@ -88,6 +98,11 @@ namespace Robust.Client.Console
|
||||
OutputText(text, true, true);
|
||||
}
|
||||
|
||||
public bool IsCmdServer(IConsoleCommand cmd)
|
||||
{
|
||||
return cmd is ServerDummyCommand;
|
||||
}
|
||||
|
||||
public override event ConAnyCommandCallback? AnyCommandExecuted;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -108,8 +123,8 @@ namespace Robust.Client.Console
|
||||
|
||||
if (AvailableCommands.ContainsKey(commandName))
|
||||
{
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
#if !DEBUG
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
if (!_conGroup.CanCommand(commandName) && playerManager.LocalPlayer?.Session.Status > SessionStatus.Connecting)
|
||||
{
|
||||
WriteError(null, $"Insufficient perms for command: {commandName}");
|
||||
@@ -134,7 +149,7 @@ namespace Robust.Client.Console
|
||||
if (!NetManager.IsConnected) // we don't care about session on client
|
||||
return;
|
||||
|
||||
var msg = NetManager.CreateNetMessage<MsgConCmd>();
|
||||
var msg = new MsgConCmd();
|
||||
msg.Text = command;
|
||||
NetManager.ClientSendMessage(msg);
|
||||
}
|
||||
@@ -198,36 +213,69 @@ namespace Robust.Client.Console
|
||||
if (!NetManager.IsConnected)
|
||||
return;
|
||||
|
||||
var msg = NetManager.CreateNetMessage<MsgConCmdReg>();
|
||||
var msg = new MsgConCmdReg();
|
||||
NetManager.ClientSendMessage(msg);
|
||||
|
||||
_requestedCommands = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// These dummies are made purely so list and help can list server-side commands.
|
||||
/// </summary>
|
||||
[Reflect(false)]
|
||||
internal sealed class ServerDummyCommand : IConsoleCommand
|
||||
{
|
||||
internal ServerDummyCommand(string command, string help, string description)
|
||||
/// <summary>
|
||||
/// These dummies are made purely so list and help can list server-side commands.
|
||||
/// </summary>
|
||||
[Reflect(false)]
|
||||
private sealed class ServerDummyCommand : IConsoleCommand
|
||||
{
|
||||
Command = command;
|
||||
Help = help;
|
||||
Description = description;
|
||||
internal ServerDummyCommand(string command, string help, string description)
|
||||
{
|
||||
Command = command;
|
||||
Help = help;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
public string Command { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public string Help { get; }
|
||||
|
||||
// Always forward to server.
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
shell.RemoteExecuteCommand(argStr);
|
||||
}
|
||||
|
||||
public async ValueTask<CompletionResult> GetCompletionAsync(
|
||||
IConsoleShell shell,
|
||||
string[] args,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var host = (ClientConsoleHost)shell.ConsoleHost;
|
||||
var argsList = args.ToList();
|
||||
argsList.Insert(0, Command);
|
||||
|
||||
return await host.DoServerCompletions(argsList, cancel);
|
||||
}
|
||||
}
|
||||
|
||||
public string Command { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public string Help { get; }
|
||||
|
||||
// Always forward to server.
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
private sealed class RemoteExecCommand : IConsoleCommand
|
||||
{
|
||||
shell.RemoteExecuteCommand(argStr);
|
||||
public string Command => ">";
|
||||
public string Description => Loc.GetString("cmd-remoteexec-desc");
|
||||
public string Help => Loc.GetString("cmd-remoteexec-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
shell.RemoteExecuteCommand(argStr["> ".Length..]);
|
||||
}
|
||||
|
||||
public async ValueTask<CompletionResult> GetCompletionAsync(
|
||||
IConsoleShell shell,
|
||||
string[] args,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var host = (ClientConsoleHost)shell.ConsoleHost;
|
||||
return await host.DoServerCompletions(args.ToList(), cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
@@ -7,57 +5,6 @@ using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class CVarCommand : SharedCVarCommand, IConsoleCommand
|
||||
{
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 1 || args.Length > 2)
|
||||
{
|
||||
shell.WriteError("Must provide exactly one or two arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
var name = args[0];
|
||||
|
||||
if (name == "?")
|
||||
{
|
||||
var cvars = configManager.GetRegisteredCVars().OrderBy(c => c);
|
||||
shell.WriteLine(string.Join("\n", cvars));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configManager.IsCVarRegistered(name))
|
||||
{
|
||||
shell.WriteError($"CVar '{name}' is not registered. Use 'cvar ?' to get a list of all registered CVars.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length == 1)
|
||||
{
|
||||
// Read CVar
|
||||
var value = configManager.GetCVar<object>(name);
|
||||
shell.WriteLine(value.ToString() ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write CVar
|
||||
var value = args[1];
|
||||
var type = configManager.GetCVarType(name);
|
||||
try
|
||||
{
|
||||
var parsed = ParseObject(type, value);
|
||||
configManager.SetCVar(name, parsed);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
shell.WriteLine($"Input value is in incorrect format for type {type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class SaveConfig : IConsoleCommand
|
||||
{
|
||||
|
||||
@@ -739,103 +739,6 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gc";
|
||||
public string Description => "Run the GC.";
|
||||
public string Help => "gc [generation]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
GC.Collect();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (int.TryParse(args[0], out int result))
|
||||
GC.Collect(result);
|
||||
else
|
||||
shell.WriteError("Failed to parse argument.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcFullCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gcf";
|
||||
public string Description => "Run the GC, fully, compacting LOH and everything.";
|
||||
public string Help => "gcf";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect(2, GCCollectionMode.Forced, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcModeCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
public string Command => "gc_mode";
|
||||
|
||||
public string Description => "Change/Read the GC Latency mode.";
|
||||
|
||||
public string Help => "gc_mode\nSee current GC Latencymode\ngc_mode [type]\n Change GC Latency mode to [type]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var prevMode = GCSettings.LatencyMode;
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteLine($"current gc latency mode: {(int) prevMode} ({prevMode})");
|
||||
shell.WriteLine("possible modes:");
|
||||
foreach (int mode in (int[]) Enum.GetValues(typeof(GCLatencyMode)))
|
||||
{
|
||||
shell.WriteLine($" {mode}: {Enum.GetName(typeof(GCLatencyMode), mode)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GCLatencyMode mode;
|
||||
if (char.IsDigit(args[0][0]) && int.TryParse(args[0], out var modeNum))
|
||||
{
|
||||
mode = (GCLatencyMode) modeNum;
|
||||
}
|
||||
else if (!Enum.TryParse(args[0], true, out mode))
|
||||
{
|
||||
shell.WriteLine($"unknown gc latency mode: {args[0]}");
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine($"attempting gc latency mode change: {(int) prevMode} ({prevMode}) -> {(int) mode} ({mode})");
|
||||
GCSettings.LatencyMode = mode;
|
||||
shell.WriteLine($"resulting gc latency mode: {(int) GCSettings.LatencyMode} ({GCSettings.LatencyMode})");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class SerializeStatsCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
public string Command => "szr_stats";
|
||||
|
||||
public string Description => "Report serializer statistics.";
|
||||
|
||||
public string Help => "szr_stats";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
|
||||
shell.WriteLine($"serialized: {RobustSerializer.BytesSerialized} bytes, {RobustSerializer.ObjectsSerialized} objects");
|
||||
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectSerializedBytes} bytes, {RobustSerializer.LargestObjectSerializedType} objects");
|
||||
shell.WriteLine($"deserialized: {RobustSerializer.BytesDeserialized} bytes, {RobustSerializer.ObjectsDeserialized} objects");
|
||||
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectDeserializedBytes} bytes, {RobustSerializer.LargestObjectDeserializedType} objects");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class ChunkInfoCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "chunkinfo";
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
using System.Linq;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
sealed class HelpCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "help";
|
||||
public string Help => "When no arguments are provided, displays a generic help text. When an argument is passed, display the help text for the command with that name.";
|
||||
public string Description => "Display help text.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
shell.WriteLine("To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'.");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
string commandname = args[0];
|
||||
if (!shell.ConsoleHost.RegisteredCommands.ContainsKey(commandname))
|
||||
{
|
||||
if (!IoCManager.Resolve<IClientNetManager>().IsConnected)
|
||||
{
|
||||
// No server so nothing to respond with unknown command.
|
||||
shell.WriteError("Unknown command: " + commandname);
|
||||
return;
|
||||
}
|
||||
// TODO: Maybe have a server side help?
|
||||
return;
|
||||
}
|
||||
IConsoleCommand command = shell.ConsoleHost.RegisteredCommands[commandname];
|
||||
shell.WriteLine(string.Format("{0} - {1}", command.Command, command.Description));
|
||||
shell.WriteLine(command.Help);
|
||||
break;
|
||||
|
||||
default:
|
||||
shell.WriteError("Invalid amount of arguments.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ListCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "list";
|
||||
public string Help => "Usage: list [filter]\n" +
|
||||
"Lists all available commands, and their short descriptions.\n" +
|
||||
"If a filter is provided, " +
|
||||
"only commands that contain the given string in their name will be listed.";
|
||||
public string Description => "List all commands, optionally with a filter.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var filter = "";
|
||||
if (args.Length == 1)
|
||||
{
|
||||
filter = args[0];
|
||||
}
|
||||
|
||||
var conGroup = IoCManager.Resolve<IClientConGroupController>();
|
||||
foreach (var command in shell.ConsoleHost.RegisteredCommands.Values
|
||||
.Where(p => p.Command.Contains(filter) && (p is not ServerDummyCommand || conGroup.CanCommand(p.Command)))
|
||||
.OrderBy(c => c.Command))
|
||||
{
|
||||
shell.WriteLine(command.Command + ": " + command.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.Loader;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class ListAssembliesCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "lsasm";
|
||||
public string Description => "Lists loaded assemblies by load context.";
|
||||
public string Help => Command;
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
foreach (var context in AssemblyLoadContext.All)
|
||||
{
|
||||
shell.WriteLine($"{context.Name}:");
|
||||
foreach (var assembly in context.Assemblies.OrderBy(a => a.FullName))
|
||||
{
|
||||
shell.WriteLine($" {assembly.FullName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using Robust.Shared.Log;
|
||||
using System;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
sealed class LogSetLevelCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "loglevel";
|
||||
public string Description => "Changes the log level for a provided sawmill.";
|
||||
public string Help => "Usage: loglevel <sawmill> <level>"
|
||||
+ "\n sawmill: A label prefixing log messages. This is the one you're setting the level for."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
LogLevel? level;
|
||||
if (levelname == "null")
|
||||
{
|
||||
level = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
level = result;
|
||||
}
|
||||
Logger.GetSawmill(name).Level = level;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TestLog : IConsoleCommand
|
||||
{
|
||||
public string Command => "testlog";
|
||||
public string Description => "Writes a test log to a sawmill.";
|
||||
public string Help => "Usage: testlog <sawmill> <level> <message>"
|
||||
+ "\n sawmill: A label prefixing the logged message."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum."
|
||||
+ "\n message: The message to be logged. Wrap this in double quotes if you want to use spaces.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 3)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 3 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
var message = args[2]; // yes this doesn't support spaces idgaf.
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
var level = result;
|
||||
|
||||
Logger.LogS(level, name, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Robust.Client.Debugging;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
@@ -8,12 +9,13 @@ namespace Robust.Client.Console.Commands
|
||||
{
|
||||
public string Command => "physics";
|
||||
public string Description => $"Shows a debug physics overlay. The arg supplied specifies the overlay.";
|
||||
public string Help => $"{Command} <aabbs / contactnormals / contactpoints / joints / shapeinfo / shapes>";
|
||||
public string Help => $"{Command} <aabbs / com / contactnormals / contactpoints / joints / shapeinfo / shapes>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine($"Invalid number of args supplied");
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", ("properAmount", 1), ("currentAmount", args.Length)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,11 +45,27 @@ namespace Robust.Client.Console.Commands
|
||||
system.Flags ^= PhysicsDebugFlags.Shapes;
|
||||
break;
|
||||
default:
|
||||
shell.WriteLine($"{args[0]} is not a recognised overlay");
|
||||
shell.WriteError(Loc.GetString("cmd-physics-overlay", ("overlay", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length != 1) return CompletionResult.Empty;
|
||||
|
||||
return CompletionResult.FromOptions(new[]
|
||||
{
|
||||
"aabbs",
|
||||
"com",
|
||||
"contactnormals",
|
||||
"contactpoints",
|
||||
"joints",
|
||||
"shapeinfo",
|
||||
"shapes",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Robust.Client.Console.Commands
|
||||
// MsgStringTableEntries is registered as NetMessageAccept.Client so the server will immediately deny it.
|
||||
// And kick us.
|
||||
var net = IoCManager.Resolve<IClientNetManager>();
|
||||
var msg = net.CreateNetMessage<MsgStringTableEntries>();
|
||||
var msg = new MsgStringTableEntries();
|
||||
msg.Entries = new MsgStringTableEntries.Entry[0];
|
||||
net.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -15,5 +18,7 @@ namespace Robust.Client.Console
|
||||
event EventHandler<AddFormattedMessageArgs> AddFormatted;
|
||||
|
||||
void AddFormattedLine(FormattedMessage message);
|
||||
|
||||
Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Robust.Client.Console
|
||||
|
||||
RunButton.Disabled = true;
|
||||
|
||||
var msg = _client._netManager.CreateNetMessage<MsgScriptEval>();
|
||||
var msg = new MsgScriptEval();
|
||||
msg.ScriptSession = _session;
|
||||
msg.Code = _lastEnteredText = InputBar.Text;
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace Robust.Client.Console
|
||||
|
||||
protected override void Complete()
|
||||
{
|
||||
var msg = _client._netManager.CreateNetMessage<MsgScriptCompletion>();
|
||||
var msg = new MsgScriptCompletion();
|
||||
msg.ScriptSession = _session;
|
||||
msg.Code = InputBar.Text;
|
||||
msg.Cursor = InputBar.CursorPosition;
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Robust.Client.Console
|
||||
throw new InvalidOperationException("We do not have scripting permission.");
|
||||
}
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgScriptStart>();
|
||||
var msg = new MsgScriptStart();
|
||||
msg.ScriptSession = _nextSessionId++;
|
||||
_netManager.ClientSendMessage(msg);
|
||||
}
|
||||
@@ -74,7 +74,7 @@ namespace Robust.Client.Console
|
||||
{
|
||||
_activeConsoles.Remove(session);
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgScriptStop>();
|
||||
var msg = new MsgScriptStop();
|
||||
msg.ScriptSession = session;
|
||||
_netManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
@@ -88,6 +89,7 @@ namespace Robust.Client.Debugging
|
||||
EntityManager,
|
||||
IoCManager.Resolve<IEyeManager>(),
|
||||
IoCManager.Resolve<IInputManager>(),
|
||||
IoCManager.Resolve<IMapManager>(),
|
||||
IoCManager.Resolve<IResourceCache>(),
|
||||
this,
|
||||
Get<SharedPhysicsSystem>()));
|
||||
@@ -151,10 +153,12 @@ namespace Robust.Client.Debugging
|
||||
/// Shows the world point for each contact in the viewport.
|
||||
/// </summary>
|
||||
ContactPoints = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Shows the world normal for each contact in the viewport.
|
||||
/// </summary>
|
||||
ContactNormals = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Shows all physics shapes in the viewport.
|
||||
/// </summary>
|
||||
@@ -162,6 +166,10 @@ namespace Robust.Client.Debugging
|
||||
ShapeInfo = 1 << 3,
|
||||
Joints = 1 << 4,
|
||||
AABBs = 1 << 5,
|
||||
|
||||
/// <summary>
|
||||
/// Shows Center of Mass for all bodies in the viewport.
|
||||
/// </summary>
|
||||
COM = 1 << 6,
|
||||
}
|
||||
|
||||
@@ -170,6 +178,7 @@ namespace Robust.Client.Debugging
|
||||
private IEntityManager _entityManager = default!;
|
||||
private IEyeManager _eyeManager = default!;
|
||||
private IInputManager _inputManager = default!;
|
||||
private IMapManager _mapManager = default!;
|
||||
private DebugPhysicsSystem _debugPhysicsSystem = default!;
|
||||
private SharedPhysicsSystem _physicsSystem = default!;
|
||||
|
||||
@@ -181,11 +190,12 @@ namespace Robust.Client.Debugging
|
||||
|
||||
private HashSet<Joint> _drawnJoints = new();
|
||||
|
||||
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IResourceCache cache, DebugPhysicsSystem system, SharedPhysicsSystem physicsSystem)
|
||||
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IResourceCache cache, DebugPhysicsSystem system, SharedPhysicsSystem physicsSystem)
|
||||
{
|
||||
_entityManager = entityManager;
|
||||
_eyeManager = eyeManager;
|
||||
_inputManager = inputManager;
|
||||
_mapManager = mapManager;
|
||||
_debugPhysicsSystem = system;
|
||||
_physicsSystem = physicsSystem;
|
||||
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||
@@ -240,26 +250,21 @@ namespace Robust.Client.Debugging
|
||||
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.COM) != 0)
|
||||
{
|
||||
const float Alpha = 0.25f;
|
||||
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
|
||||
{
|
||||
Color color;
|
||||
const float Alpha = 0.25f;
|
||||
float size;
|
||||
|
||||
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner))
|
||||
{
|
||||
color = Color.Orange.WithAlpha(Alpha);
|
||||
size = 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
color = Color.Purple.WithAlpha(Alpha);
|
||||
size = 0.2f;
|
||||
}
|
||||
|
||||
var color = Color.Purple.WithAlpha(Alpha);
|
||||
var transform = physBody.GetTransform();
|
||||
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 0.2f, color);
|
||||
}
|
||||
|
||||
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), size, color);
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, viewBounds))
|
||||
{
|
||||
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid.GridEntityId);
|
||||
var color = Color.Orange.WithAlpha(Alpha);
|
||||
var transform = physBody.GetTransform();
|
||||
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 1f, color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
Robust.Client/GameController/GameController.Redialler.cs
Normal file
67
Robust.Client/GameController/GameController.Redialler.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
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
|
||||
{
|
||||
// A little paranoia goes a long way.
|
||||
// This is static on purpose - the whole process shouldn't redial more than once, ever.
|
||||
private static int _hasRedialled = 0;
|
||||
|
||||
public void Redial(string address, string? text = null)
|
||||
{
|
||||
// -- ATTENTION, YE WOULD-BE TRESPASSERS! --
|
||||
// This code is the least testable code ever (because it's in-engine code that requires the Launcher), so don't make it do too much.
|
||||
// This checks for some obvious requirements and then forwards to RedialApi which does the actual work.
|
||||
// Testing of RedialApi is doable in the SS14.Launcher project, this is why it has a fake RobustToolbox build.
|
||||
// -- THANK YE, NOW KINDLY MOVE ALONG! --
|
||||
|
||||
// We don't ever want to successfully redial more than once, and we want to shutdown once we've redialled.
|
||||
// Otherwise abuse could happen.
|
||||
DebugTools.AssertNotNull(_mainLoop);
|
||||
|
||||
Logger.Info($"Attempting redial of {address}: {text ?? "no reason given"}");
|
||||
|
||||
if (!_mainLoop!.Running)
|
||||
{
|
||||
throw new Exception("Attempted a redial during shutdown, this is not acceptable - redial first and if you succeed it'll shutdown anyway.");
|
||||
}
|
||||
|
||||
if (_loaderArgs == null)
|
||||
{
|
||||
throw new Exception("Attempted a redial when the game was not run from the launcher (_loaderArgs == null)");
|
||||
}
|
||||
|
||||
if (_loaderArgs!.RedialApi == null)
|
||||
{
|
||||
throw new Exception("Attempted a redial when redialling was not supported by the loader (Outdated launcher?)");
|
||||
}
|
||||
|
||||
if (Interlocked.Increment(ref _hasRedialled) != 1)
|
||||
{
|
||||
// Don't let it overflow or anything
|
||||
Interlocked.Decrement(ref _hasRedialled);
|
||||
throw new Exception("Attempted a redial after one already succeeded or while one is in progress, this is never acceptable");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_loaderArgs!.RedialApi!.Redial(new Uri(address), text ?? "");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Interlocked.Decrement(ref _hasRedialled);
|
||||
throw;
|
||||
}
|
||||
|
||||
Shutdown("Redial");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,16 @@ using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
@@ -66,6 +68,9 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IAuthManager _authManager = default!;
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IParallelManagerInternal _parallelMgr = default!;
|
||||
[Dependency] private readonly ProfManager _prof = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
@@ -91,7 +96,8 @@ namespace Robust.Client
|
||||
|
||||
_clyde.InitializePostWindowing();
|
||||
_clydeAudio.InitializePostWindowing();
|
||||
_clyde.SetWindowTitle(Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
|
||||
_clyde.SetWindowTitle(
|
||||
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
|
||||
|
||||
_taskManager.Initialize();
|
||||
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
@@ -102,7 +108,8 @@ namespace Robust.Client
|
||||
// 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(!ContentStart);
|
||||
_modLoader.SetEnableSandboxing(Options.Sandboxing);
|
||||
var disableSandbox = Environment.GetEnvironmentVariable("ROBUST_DISABLE_SANDBOX") == "1";
|
||||
_modLoader.SetEnableSandboxing(!disableSandbox && Options.Sandboxing);
|
||||
|
||||
var assemblyPrefix = Options.ContentModulePrefix ?? _resourceManifest!.AssemblyPrefix ?? "Content.";
|
||||
if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, assemblyPrefix))
|
||||
@@ -131,8 +138,9 @@ namespace Robust.Client
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDirectory(new ResourcePath("/EnginePrototypes/"));
|
||||
_prototypeManager.LoadDirectory(Options.PrototypeDirectory);
|
||||
_prototypeManager.Resync();
|
||||
_prototypeManager.ResolveResults();
|
||||
_entityManager.Initialize();
|
||||
_mapManager.Initialize();
|
||||
_gameStateManager.Initialize();
|
||||
@@ -157,7 +165,7 @@ namespace Robust.Client
|
||||
// Setup main loop
|
||||
if (_mainLoop == null)
|
||||
{
|
||||
_mainLoop = new GameLoop(_gameTiming)
|
||||
_mainLoop = new GameLoop(_gameTiming, _runtimeLog, _prof)
|
||||
{
|
||||
SleepMode = displayMode == DisplayMode.Headless ? SleepMode.Delay : SleepMode.None
|
||||
};
|
||||
@@ -197,7 +205,7 @@ namespace Robust.Client
|
||||
|
||||
_clyde.Ready();
|
||||
|
||||
if (!Options.DisableCommandLineConnect &&
|
||||
if (_resourceManifest!.AutoConnect &&
|
||||
(_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
|
||||
&& LaunchState.ConnectEndpoint != null)
|
||||
{
|
||||
@@ -213,7 +221,7 @@ namespace Robust.Client
|
||||
{
|
||||
// Parses /manifest.yml for game-specific settings that cannot be exclusively set up by content code.
|
||||
if (!_resourceCache.TryContentFileRead("/manifest.yml", out var stream))
|
||||
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null);
|
||||
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true);
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
using (stream)
|
||||
@@ -223,7 +231,7 @@ namespace Robust.Client
|
||||
}
|
||||
|
||||
if (yamlStream.Documents.Count == 0)
|
||||
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null);
|
||||
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true);
|
||||
|
||||
if (yamlStream.Documents.Count != 1 || yamlStream.Documents[0].RootNode is not YamlMappingNode mapping)
|
||||
{
|
||||
@@ -258,7 +266,11 @@ namespace Robust.Client
|
||||
if (mapping.TryGetNode("splashLogo", out var splashNode))
|
||||
splashLogo = splashNode.AsString();
|
||||
|
||||
return new ResourceManifestData(modules, assemblyPrefix, defaultWindowTitle, windowIconSet, splashLogo);
|
||||
bool autoConnect = true;
|
||||
if (mapping.TryGetNode("autoConnect", out var autoConnectNode))
|
||||
autoConnect = autoConnectNode.AsBool();
|
||||
|
||||
return new ResourceManifestData(modules, assemblyPrefix, defaultWindowTitle, windowIconSet, splashLogo, autoConnect);
|
||||
}
|
||||
|
||||
internal bool StartupSystemSplash(GameControllerOptions options, Func<ILogHandler>? logHandlerFactory)
|
||||
@@ -326,6 +338,12 @@ namespace Robust.Client
|
||||
|
||||
ProfileOptSetup.Setup(_configurationManager);
|
||||
|
||||
_parallelMgr.Initialize();
|
||||
_prof.Initialize();
|
||||
#if !FULL_RELEASE
|
||||
_configurationManager.OverrideDefault(CVars.ProfEnabled, true);
|
||||
#endif
|
||||
|
||||
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
@@ -446,52 +464,127 @@ namespace Robust.Client
|
||||
|
||||
private void Input(FrameEventArgs frameEventArgs)
|
||||
{
|
||||
_clyde.ProcessInput(frameEventArgs);
|
||||
_networkManager.ProcessPackets();
|
||||
_taskManager.ProcessPendingTasks(); // tasks like connect
|
||||
using (_prof.Group("Input Events"))
|
||||
{
|
||||
_clyde.ProcessInput(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Network"))
|
||||
{
|
||||
_networkManager.ProcessPackets();
|
||||
}
|
||||
|
||||
using (_prof.Group("Async"))
|
||||
{
|
||||
_taskManager.ProcessPendingTasks(); // tasks like connect
|
||||
}
|
||||
}
|
||||
|
||||
private void Tick(FrameEventArgs frameEventArgs)
|
||||
{
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
|
||||
_console.CommandBufferExecute();
|
||||
_timerManager.UpdateTimers(frameEventArgs);
|
||||
_taskManager.ProcessPendingTasks();
|
||||
using (_prof.Group("Content pre engine"))
|
||||
{
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Console"))
|
||||
{
|
||||
_console.CommandBufferExecute();
|
||||
}
|
||||
|
||||
using (_prof.Group("Timers"))
|
||||
{
|
||||
_timerManager.UpdateTimers(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Async"))
|
||||
{
|
||||
_taskManager.ProcessPendingTasks();
|
||||
}
|
||||
|
||||
// GameStateManager is in full control of the simulation update in multiplayer.
|
||||
if (_client.RunLevel == ClientRunLevel.InGame || _client.RunLevel == ClientRunLevel.Connected)
|
||||
{
|
||||
_gameStateManager.ApplyGameState();
|
||||
using (_prof.Group("Game state"))
|
||||
{
|
||||
_gameStateManager.ApplyGameState();
|
||||
}
|
||||
}
|
||||
|
||||
// In singleplayer, however, we're in full control instead.
|
||||
else if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
|
||||
{
|
||||
// The last real tick is the current tick! This way we won't be in "prediction" mode.
|
||||
_gameTiming.LastRealTick = _gameTiming.CurTick;
|
||||
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
|
||||
using (_prof.Group("Entity"))
|
||||
{
|
||||
// The last real tick is the current tick! This way we won't be in "prediction" mode.
|
||||
_gameTiming.LastRealTick = _gameTiming.CurTick;
|
||||
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
|
||||
}
|
||||
}
|
||||
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
|
||||
using (_prof.Group("Content post engine"))
|
||||
{
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update(FrameEventArgs frameEventArgs)
|
||||
{
|
||||
_webViewHook?.Update();
|
||||
_clydeAudio.FrameProcess(frameEventArgs);
|
||||
_clyde.FrameProcess(frameEventArgs);
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePreEngine, frameEventArgs);
|
||||
_stateManager.FrameUpdate(frameEventArgs);
|
||||
if (_webViewHook != null)
|
||||
{
|
||||
using (_prof.Group("WebView"))
|
||||
{
|
||||
_webViewHook?.Update();
|
||||
}
|
||||
}
|
||||
|
||||
using (_prof.Group("ClydeAudio"))
|
||||
{
|
||||
_clydeAudio.FrameProcess(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Clyde"))
|
||||
{
|
||||
_clyde.FrameProcess(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Content Pre Engine"))
|
||||
{
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePreEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("State"))
|
||||
{
|
||||
_stateManager.FrameUpdate(frameEventArgs);
|
||||
}
|
||||
|
||||
if (_client.RunLevel >= ClientRunLevel.Connected)
|
||||
{
|
||||
_placementManager.FrameUpdate(frameEventArgs);
|
||||
_entityManager.FrameUpdate(frameEventArgs.DeltaSeconds);
|
||||
using (_prof.Group("Placement"))
|
||||
{
|
||||
_placementManager.FrameUpdate(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Entity"))
|
||||
{
|
||||
_entityManager.FrameUpdate(frameEventArgs.DeltaSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
_overlayManager.FrameUpdate(frameEventArgs);
|
||||
_userInterfaceManager.FrameUpdate(frameEventArgs);
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
|
||||
using (_prof.Group("Overlay"))
|
||||
{
|
||||
_overlayManager.FrameUpdate(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("UI"))
|
||||
{
|
||||
_userInterfaceManager.FrameUpdate(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Content Post Engine"))
|
||||
{
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetupLogging(ILogManager logManager, Func<ILogHandler> logHandlerFactory)
|
||||
@@ -584,7 +677,8 @@ namespace Robust.Client
|
||||
string? AssemblyPrefix,
|
||||
string? DefaultWindowTitle,
|
||||
string? WindowIconSet,
|
||||
string? SplashLogo
|
||||
string? SplashLogo,
|
||||
bool AutoConnect
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +82,5 @@ namespace Robust.Client
|
||||
/// Whether to load config and user data.
|
||||
/// </summary>
|
||||
public bool LoadConfigAndUserData { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to disable command line args server auto-connecting.
|
||||
/// </summary>
|
||||
public bool DisableCommandLineConnect { get; init; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ namespace Robust.Client.GameObjects
|
||||
return base.CreateEntity(prototypeName, uid);
|
||||
}
|
||||
|
||||
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity)
|
||||
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta)
|
||||
{
|
||||
base.InitializeEntity(entity);
|
||||
base.InitializeEntity(entity, meta);
|
||||
}
|
||||
|
||||
void IClientEntityManagerInternal.StartEntity(EntityUid entity)
|
||||
@@ -86,7 +86,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
|
||||
{
|
||||
var msg = _networkManager.CreateNetMessage<MsgEntity>();
|
||||
var msg = new MsgEntity();
|
||||
msg.Type = EntityMessageType.SystemMessage;
|
||||
msg.SystemMessage = message;
|
||||
msg.SourceTick = _gameTiming.CurTick;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
@@ -32,11 +33,13 @@ namespace Robust.Client.GameObjects
|
||||
public const string LogCategory = "go.comp.icon";
|
||||
const string SerializationCache = "icon";
|
||||
|
||||
[Obsolete("Use SpriteSystem instead.")]
|
||||
private static IRsiStateLike TextureForConfig(IconComponent compData, IResourceCache resourceCache)
|
||||
{
|
||||
return compData.Icon?.Default ?? resourceCache.GetFallback<TextureResource>().Texture;
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem instead.")]
|
||||
public static IRsiStateLike? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
{
|
||||
if (!prototype.Components.TryGetValue("Icon", out var compData))
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedPointLightComponent))]
|
||||
public sealed class PointLightComponent : SharedPointLightComponent, ISerializationHooks
|
||||
public sealed class PointLightComponent : SharedPointLightComponent
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
|
||||
@@ -601,8 +601,10 @@ namespace Robust.Client.GameObjects
|
||||
private void RebuildBounds()
|
||||
{
|
||||
_bounds = new Box2();
|
||||
foreach (var layer in Layers.Where(layer => layer.Visible))
|
||||
foreach (var layer in Layers)
|
||||
{
|
||||
if (!layer.Visible) continue;
|
||||
|
||||
_bounds = _bounds.Union(layer.CalculateBoundingBox());
|
||||
}
|
||||
}
|
||||
@@ -1520,6 +1522,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem instead.")]
|
||||
internal static RSI.State GetFallbackState(IResourceCache cache)
|
||||
{
|
||||
var rsi = cache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
|
||||
@@ -1968,7 +1971,7 @@ namespace Robust.Client.GameObjects
|
||||
/// <inheritdoc/>
|
||||
public Box2 CalculateBoundingBox()
|
||||
{
|
||||
var textureSize = PixelSize / EyeManager.PixelsPerMeter;
|
||||
var textureSize = (Vector2) PixelSize / EyeManager.PixelsPerMeter;
|
||||
|
||||
// If the parent has locked rotation and we don't have any rotation,
|
||||
// we can take the quick path of just making a box the size of the texture.
|
||||
@@ -2174,7 +2177,7 @@ namespace Robust.Client.GameObjects
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy);
|
||||
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy, spriteComponent);
|
||||
|
||||
var anyTexture = false;
|
||||
foreach (var layer in spriteComponent.AllLayers)
|
||||
|
||||
@@ -75,6 +75,10 @@ namespace Robust.Client.GameObjects
|
||||
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[] {this, wrapped.UiKey});
|
||||
boundInterface.Open();
|
||||
_openInterfaces[wrapped.UiKey] = boundInterface;
|
||||
|
||||
var playerSession = _playerManager.LocalPlayer?.Session;
|
||||
if(playerSession != null)
|
||||
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIOpenedEvent(wrapped.UiKey, Owner, playerSession));
|
||||
}
|
||||
|
||||
internal void Close(object uiKey, bool remoteCall)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
@@ -94,17 +95,21 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
var spriteQuery = GetEntityQuery<SpriteComponent>();
|
||||
while (_queuedUpdates.TryDequeue(out var appearance))
|
||||
{
|
||||
if (appearance.Deleted)
|
||||
continue;
|
||||
|
||||
OnChangeData(appearance.Owner, appearance);
|
||||
// Sprite comp is allowed to be null, so that things like spriteless point-lights can use this system
|
||||
spriteQuery.TryGetComponent(appearance.Owner, out var sprite);
|
||||
|
||||
OnChangeData(appearance.Owner, sprite, appearance);
|
||||
UnmarkDirty(appearance);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnChangeData(EntityUid uid, ClientAppearanceComponent? appearanceComponent = null)
|
||||
public void OnChangeData(EntityUid uid, SpriteComponent? sprite, ClientAppearanceComponent? appearanceComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref appearanceComponent, false)) return;
|
||||
|
||||
@@ -112,6 +117,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
Component = appearanceComponent,
|
||||
AppearanceData = appearanceComponent.AppearanceData,
|
||||
Sprite = sprite,
|
||||
};
|
||||
|
||||
// Give it AppearanceData so we can still keep the friend attribute on the component.
|
||||
@@ -133,5 +139,6 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
public AppearanceComponent Component;
|
||||
public IReadOnlyDictionary<object, object> AppearanceData;
|
||||
public SpriteComponent? Sprite;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,75 +144,94 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
stream.Source.SetVolume(-10000000);
|
||||
}
|
||||
else
|
||||
else if (stream.Attenuation == Attenuation.NoAttenuation)
|
||||
{
|
||||
var sourceRelative = ourPos - pos.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
pos.Position,
|
||||
sourceRelative.Normalized,
|
||||
OcclusionCollisionMask),
|
||||
sourceRelative.Length,
|
||||
stream.TrackingEntity);
|
||||
}
|
||||
|
||||
var distance = MathF.Max(stream.ReferenceDistance, MathF.Min(sourceRelative.Length, stream.MaxDistance));
|
||||
float gain;
|
||||
|
||||
// Technically these are formulas for gain not decibels but EHHHHHHHH.
|
||||
switch (stream.Attenuation)
|
||||
{
|
||||
case Attenuation.Default:
|
||||
gain = 1f;
|
||||
break;
|
||||
// You thought I'd implement clamping per source? Hell no that's just for the overall OpenAL setting
|
||||
// I didn't even wanna implement this much for linear but figured it'd be cleaner.
|
||||
case Attenuation.InverseDistanceClamped:
|
||||
case Attenuation.InverseDistance:
|
||||
gain = stream.ReferenceDistance /
|
||||
(stream.ReferenceDistance + stream.RolloffFactor * (distance - stream.ReferenceDistance));
|
||||
|
||||
break;
|
||||
case Attenuation.LinearDistanceClamped:
|
||||
case Attenuation.LinearDistance:
|
||||
gain = 1f - stream.RolloffFactor * (distance - stream.ReferenceDistance) /
|
||||
(stream.MaxDistance - stream.ReferenceDistance);
|
||||
|
||||
break;
|
||||
case Attenuation.ExponentDistanceClamped:
|
||||
case Attenuation.ExponentDistance:
|
||||
gain = MathF.Pow((distance / stream.ReferenceDistance),
|
||||
(-stream.RolloffFactor));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"No implemented attenuation for {stream.Attenuation.ToString()}");
|
||||
}
|
||||
|
||||
var volume = MathF.Pow(10, stream.Volume / 10);
|
||||
var actualGain = MathF.Max(0f, volume * gain);
|
||||
|
||||
stream.Source.SetVolumeDirect(actualGain);
|
||||
stream.Source.SetOcclusion(occlusion);
|
||||
}
|
||||
|
||||
SetAudioPos(stream, stream.Attenuation != Attenuation.NoAttenuation ? pos.Position : ourPos);
|
||||
|
||||
void SetAudioPos(PlayingStream stream, Vector2 pos)
|
||||
{
|
||||
if (!stream.Source.SetPosition(pos))
|
||||
//TODO: OpenAL supports positional audio together with no attenuation, we should do too.
|
||||
if (!stream.Source.SetPosition(ourPos))
|
||||
{
|
||||
Logger.Warning("Interrupting positional audio, can't set position.");
|
||||
stream.Source.StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.TrackingEntity != default)
|
||||
else
|
||||
{
|
||||
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
|
||||
var sourceRelative = ourPos - pos.Position;
|
||||
// OpenAL uses MaxDistance to limit how much attenuation can *reduce* the gain,
|
||||
// and doesn't do any culling. We however cull based on MaxDistance, because
|
||||
// this is what all current code that uses MaxDistance expects and because
|
||||
// we don't need the OpenAL behaviour.
|
||||
if (sourceRelative.Length > stream.MaxDistance)
|
||||
{
|
||||
stream.Source.SetVolume(-10000000);
|
||||
}
|
||||
else
|
||||
{
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
pos.Position,
|
||||
sourceRelative.Normalized,
|
||||
OcclusionCollisionMask),
|
||||
sourceRelative.Length,
|
||||
stream.TrackingEntity);
|
||||
}
|
||||
|
||||
// OpenAL also limits the distance to <= AL_MAX_DISTANCE, but since we cull
|
||||
// sources that are further away than stream.MaxDistance, we don't do that.
|
||||
var distance = MathF.Max(stream.ReferenceDistance, sourceRelative.Length);
|
||||
float gain;
|
||||
|
||||
// Technically these are formulas for gain not decibels but EHHHHHHHH.
|
||||
switch (stream.Attenuation)
|
||||
{
|
||||
case Attenuation.Default:
|
||||
gain = 1f;
|
||||
break;
|
||||
// You thought I'd implement clamping per source? Hell no that's just for the overall OpenAL setting
|
||||
// I didn't even wanna implement this much for linear but figured it'd be cleaner.
|
||||
case Attenuation.InverseDistanceClamped:
|
||||
case Attenuation.InverseDistance:
|
||||
gain = stream.ReferenceDistance /
|
||||
(stream.ReferenceDistance + stream.RolloffFactor *
|
||||
(distance - stream.ReferenceDistance));
|
||||
|
||||
break;
|
||||
case Attenuation.LinearDistanceClamped:
|
||||
case Attenuation.LinearDistance:
|
||||
gain = 1f - stream.RolloffFactor * (distance - stream.ReferenceDistance) /
|
||||
(stream.MaxDistance - stream.ReferenceDistance);
|
||||
|
||||
break;
|
||||
case Attenuation.ExponentDistanceClamped:
|
||||
case Attenuation.ExponentDistance:
|
||||
gain = MathF.Pow((distance / stream.ReferenceDistance),
|
||||
(-stream.RolloffFactor));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(
|
||||
$"No implemented attenuation for {stream.Attenuation.ToString()}");
|
||||
}
|
||||
|
||||
var volume = MathF.Pow(10, stream.Volume / 10);
|
||||
var actualGain = MathF.Max(0f, volume * gain);
|
||||
|
||||
stream.Source.SetVolumeDirect(actualGain);
|
||||
stream.Source.SetOcclusion(occlusion);
|
||||
|
||||
if (!stream.Source.SetPosition(pos.Position))
|
||||
{
|
||||
Logger.Warning("Interrupting positional audio, can't set position.");
|
||||
stream.Source.StopPlaying();
|
||||
}
|
||||
|
||||
if (stream.TrackingEntity != default)
|
||||
{
|
||||
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +285,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
source.SetGlobal();
|
||||
source.StartPlaying();
|
||||
// These defaults differ from AudioParams.Default
|
||||
var playing = new PlayingStream
|
||||
{
|
||||
Source = source,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -33,9 +31,10 @@ namespace Robust.Client.GameObjects
|
||||
UpdatesAfter.Add(typeof(TransformSystem));
|
||||
UpdatesAfter.Add(typeof(PhysicsSystem));
|
||||
|
||||
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
|
||||
SubscribeLocalEvent<OccluderDirtyEvent>(OnOccluderDirty);
|
||||
|
||||
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(HandleAnchorChanged);
|
||||
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<ClientOccluderComponent, ReAnchorEvent>(OnReAnchor);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
@@ -62,51 +61,58 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
|
||||
private static void OnAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
component.AnchorStateChanged();
|
||||
}
|
||||
|
||||
private void HandleDirtyEvent(OccluderDirtyEvent ev)
|
||||
private void OnReAnchor(EntityUid uid, ClientOccluderComponent component, ref ReAnchorEvent args)
|
||||
{
|
||||
component.AnchorStateChanged();
|
||||
}
|
||||
|
||||
private void OnOccluderDirty(OccluderDirtyEvent ev)
|
||||
{
|
||||
var sender = ev.Sender;
|
||||
IMapGrid? grid;
|
||||
var occluderQuery = GetEntityQuery<ClientOccluderComponent>();
|
||||
|
||||
if (EntityManager.EntityExists(sender) &&
|
||||
EntityManager.TryGetComponent(sender, out ClientOccluderComponent? iconSmooth)
|
||||
&& iconSmooth.Initialized)
|
||||
occluderQuery.HasComponent(sender))
|
||||
{
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(sender);
|
||||
if (!_mapManager.TryGetGrid(xform.GridID, out var grid1))
|
||||
if (!_mapManager.TryGetGrid(xform.GridID, out grid))
|
||||
return;
|
||||
|
||||
var coords = xform.Coordinates;
|
||||
var localGrid = grid.TileIndicesFor(coords);
|
||||
|
||||
_dirtyEntities.Enqueue(sender);
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.North));
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.South));
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.East));
|
||||
AddValidEntities(grid1.GetInDir(coords, Direction.West));
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, 1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, -1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(1, 0)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(-1, 0)), occluderQuery);
|
||||
}
|
||||
|
||||
// Entity is no longer valid, update around the last position it was at.
|
||||
else if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out var grid))
|
||||
else if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out grid))
|
||||
{
|
||||
var pos = ev.LastPosition.Value.pos;
|
||||
|
||||
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)));
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)), occluderQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddValidEntities(IEnumerable<EntityUid> candidates)
|
||||
private void AddValidEntities(AnchoredEntitiesEnumerator enumerator, EntityQuery<ClientOccluderComponent> occluderQuery)
|
||||
{
|
||||
foreach (var entity in candidates)
|
||||
while (enumerator.MoveNext(out var entity))
|
||||
{
|
||||
if (EntityManager.HasComponent<ClientOccluderComponent>(entity))
|
||||
{
|
||||
_dirtyEntities.Enqueue(entity);
|
||||
}
|
||||
if (!occluderQuery.HasComponent(entity.Value)) continue;
|
||||
|
||||
_dirtyEntities.Enqueue(entity.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class DebugEntityLookupCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "togglelookup";
|
||||
public string Description => "Shows / hides entitylookup bounds via an overlay";
|
||||
public string Help => $"{Command}";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
EntitySystem.Get<DebugEntityLookupSystem>().Enabled ^= true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DebugEntityLookupSystem : EntitySystem
|
||||
{
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (_enabled == value) return;
|
||||
|
||||
_enabled = value;
|
||||
|
||||
if (_enabled)
|
||||
{
|
||||
IoCManager.Resolve<IOverlayManager>().AddOverlay(
|
||||
new EntityLookupOverlay(
|
||||
EntityManager,
|
||||
Get<EntityLookupSystem>()));
|
||||
}
|
||||
else
|
||||
{
|
||||
IoCManager.Resolve<IOverlayManager>().RemoveOverlay<EntityLookupOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enabled;
|
||||
}
|
||||
|
||||
public sealed class EntityLookupOverlay : Overlay
|
||||
{
|
||||
private IEntityManager _entityManager = default!;
|
||||
private EntityLookupSystem _lookup = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public EntityLookupOverlay(IEntityManager entManager, EntityLookupSystem lookup)
|
||||
{
|
||||
_entityManager = entManager;
|
||||
_lookup = lookup;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var lookup in _lookup.FindLookupsIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
var lookupXform = xformQuery.GetComponent(lookup.Owner);
|
||||
|
||||
var (_, rotation, matrix, invMatrix) = lookupXform.GetWorldPositionRotationMatrixWithInv();
|
||||
|
||||
worldHandle.SetTransform(matrix);
|
||||
|
||||
|
||||
var lookupAABB = invMatrix.TransformBox(args.WorldBounds);
|
||||
var ents = new List<EntityUid>();
|
||||
|
||||
// Gonna allocate a lot but debug overlay sooo
|
||||
lookup.Tree._b2Tree.FastQuery(ref lookupAABB, (ref EntityUid data) =>
|
||||
{
|
||||
ents.Add(data);
|
||||
});
|
||||
|
||||
foreach (var ent in ents)
|
||||
{
|
||||
if (_entityManager.Deleted(ent)) continue;
|
||||
var xform = xformQuery.GetComponent(ent);
|
||||
|
||||
//DebugTools.Assert(!ent.IsInContainer(_entityManager));
|
||||
var (entPos, entRot) = xform.GetWorldPositionRotation();
|
||||
|
||||
var lookupPos = invMatrix.Transform(entPos);
|
||||
var lookupRot = entRot - rotation;
|
||||
|
||||
var aabb = _lookup.GetAABB(ent, lookupPos, lookupRot, xform, xformQuery);
|
||||
|
||||
worldHandle.DrawRect(aabb, Color.Blue.WithAlpha(0.2f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ namespace Robust.Client.GameObjects
|
||||
worldHandle.SetTransform(worldMatrix);
|
||||
var transform = new Transform(Vector2.Zero, Angle.Zero);
|
||||
|
||||
gridInternal.GetMapChunks(viewport, out var chunkEnumerator);
|
||||
var chunkEnumerator = gridInternal.GetMapChunks(viewport);
|
||||
|
||||
while (chunkEnumerator.MoveNext(out var chunk))
|
||||
{
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class ScaleVisualsSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AppearanceChangeEvent>(OnChangeData);
|
||||
}
|
||||
|
||||
private void OnChangeData(ref AppearanceChangeEvent ev)
|
||||
{
|
||||
if (!ev.AppearanceData.TryGetValue(ScaleVisuals.Scale, out var scale) ||
|
||||
ev.Sprite == null) return;
|
||||
|
||||
var vecScale = (Vector2)scale;
|
||||
|
||||
// Set it directly because prediction may call this multiple times.
|
||||
ev.Sprite.Scale = vecScale;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -17,13 +18,13 @@ public sealed partial class SpriteSystem
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
[Pure]
|
||||
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
|
||||
|
||||
public Texture Frame0(SpriteSpecifier specifier)
|
||||
{
|
||||
return RsiStateLike(specifier).Default;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
|
||||
{
|
||||
switch (specifier)
|
||||
@@ -35,38 +36,71 @@ public sealed partial class SpriteSystem
|
||||
return GetState(rsi);
|
||||
|
||||
case SpriteSpecifier.EntityPrototype prototypeIcon:
|
||||
if (!_proto.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
|
||||
{
|
||||
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
|
||||
return SpriteComponent.GetFallbackState(_resourceCache);
|
||||
}
|
||||
|
||||
return SpriteComponent.GetPrototypeIcon(prototype, _resourceCache);
|
||||
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
|
||||
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
/// <summary>
|
||||
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
|
||||
/// This method caches the result based on the prototype identifier.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(string prototype)
|
||||
{
|
||||
var icon = IconComponent.GetPrototypeIcon(prototype, _resourceCache);
|
||||
if (icon != null) return icon;
|
||||
// Check if this prototype has been cached before, and if so return the result.
|
||||
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
|
||||
return cachedResult;
|
||||
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
|
||||
{
|
||||
return SpriteComponent.GetFallbackState(resourceCache);
|
||||
// The specified prototype doesn't exist, return the fallback "error" sprite.
|
||||
Logger.Error("Failed to load PrototypeIcon {0}", prototype);
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Generate the icon and cache it in case it's ever needed again.
|
||||
var result = GetPrototypeIcon(entityPrototype);
|
||||
_cachedPrototypeIcons[prototype] = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
|
||||
/// This method does NOT cache the result.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
|
||||
{
|
||||
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
|
||||
if (prototype.Components.TryGetValue("Icon", out var compData)
|
||||
&& compData is IconComponent {Icon: {} icon})
|
||||
{
|
||||
return icon.Default;
|
||||
}
|
||||
|
||||
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
{
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Finally, we use spawn a dummy entity to get its icon.
|
||||
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
|
||||
var result = spriteComponent.Icon ?? SpriteComponent.GetFallbackState(resourceCache);
|
||||
var result = spriteComponent.Icon ?? GetFallbackState();
|
||||
Del(dummy);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetFallbackState()
|
||||
{
|
||||
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
|
||||
{
|
||||
@@ -79,6 +113,20 @@ public sealed partial class SpriteSystem
|
||||
}
|
||||
|
||||
Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
|
||||
return SpriteComponent.GetFallbackState(_resourceCache);
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs protoReloaded)
|
||||
{
|
||||
// Check if any EntityPrototype has been changed.
|
||||
if (!protoReloaded.ByType.TryGetValue(typeof(EntityPrototype), out var changedSet))
|
||||
return;
|
||||
|
||||
// Remove all changed prototypes from the cache, if they're there.
|
||||
foreach (var (prototype, _) in changedSet.Modified)
|
||||
{
|
||||
// Let's be lazy and not regenerate them until something needs them again.
|
||||
_cachedPrototypeIcons.Remove(prototype);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,16 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_proto.PrototypesReloaded += OnPrototypesReloaded;
|
||||
SubscribeLocalEvent<SpriteUpdateInertEvent>(QueueUpdateInert);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_proto.PrototypesReloaded -= OnPrototypesReloaded;
|
||||
}
|
||||
|
||||
private void QueueUpdateInert(SpriteUpdateInertEvent ev)
|
||||
{
|
||||
_inertUpdateQueue.Enqueue(ev.Sprite);
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// Only lerp if parent didn't change.
|
||||
// E.g. entering lockers would do it.
|
||||
if (transform.LerpParent == transform.ParentUid)
|
||||
if (transform.LerpParent == transform.ParentUid &&
|
||||
transform.ParentUid.IsValid())
|
||||
{
|
||||
if (transform.LerpDestination != null)
|
||||
{
|
||||
@@ -71,7 +72,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
var lerpDest = transform.LerpAngle.Value;
|
||||
var lerpSource = transform.LerpSourceAngle;
|
||||
if (lerpDest.Theta - lerpSource.Theta < MaxInterpolationAngle)
|
||||
if (Math.Abs(Angle.ShortestDistance(lerpDest, lerpSource)) < MaxInterpolationAngle)
|
||||
{
|
||||
transform.LocalRotation = Angle.Lerp(lerpSource, lerpDest, step);
|
||||
// Setting LocalRotation clears LerpAngle so fix that.
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
EntityUid CreateEntity(string? prototypeName, EntityUid uid = default);
|
||||
|
||||
void InitializeEntity(EntityUid entity);
|
||||
void InitializeEntity(EntityUid entity, MetaDataComponent? meta = null);
|
||||
|
||||
void StartEntity(EntityUid entity);
|
||||
}
|
||||
|
||||
82
Robust.Client/GameStates/ClientDirtySystem.cs
Normal file
82
Robust.Client/GameStates/ClientDirtySystem.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameStates;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks dirty entities on the client for the purposes of gamestatemanager.
|
||||
/// </summary>
|
||||
internal sealed class ClientDirtySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private readonly Dictionary<GameTick, HashSet<EntityUid>> _dirtyEntities = new();
|
||||
|
||||
private ObjectPool<HashSet<EntityUid>> _dirtyPool =
|
||||
new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), 64);
|
||||
|
||||
// Keep it out of the pool because it's probably going to be a lot bigger.
|
||||
private HashSet<EntityUid> _dirty = new(256);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
EntityManager.EntityDirtied += OnEntityDirty;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
EntityManager.EntityDirtied -= OnEntityDirty;
|
||||
_dirtyEntities.Clear();
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
foreach (var (_, sets) in _dirtyEntities)
|
||||
{
|
||||
sets.Clear();
|
||||
_dirtyPool.Return(sets);
|
||||
}
|
||||
|
||||
_dirtyEntities.Clear();
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> GetDirtyEntities(GameTick currentTick)
|
||||
{
|
||||
_dirty.Clear();
|
||||
|
||||
// This is just to avoid collection being modified during iteration unfortunately.
|
||||
foreach (var (tick, dirty) in _dirtyEntities)
|
||||
{
|
||||
if (tick < currentTick) continue;
|
||||
foreach (var ent in dirty)
|
||||
{
|
||||
_dirty.Add(ent);
|
||||
}
|
||||
}
|
||||
|
||||
return _dirty;
|
||||
}
|
||||
|
||||
private void OnEntityDirty(EntityUid e)
|
||||
{
|
||||
if (e.IsClientSide()) return;
|
||||
|
||||
var tick = _timing.CurTick;
|
||||
if (!_dirtyEntities.TryGetValue(tick, out var ents))
|
||||
{
|
||||
ents = _dirtyPool.Get();
|
||||
_dirtyEntities[tick] = ents;
|
||||
}
|
||||
|
||||
ents.Add(e);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -54,6 +55,7 @@ namespace Robust.Client.GameStates
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly ProfManager _prof = default!;
|
||||
#if EXCEPTION_TOLERANCE
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
#endif
|
||||
@@ -224,18 +226,28 @@ namespace Robust.Client.GameStates
|
||||
ResetPredictedEntities(_timing.CurTick);
|
||||
}
|
||||
|
||||
if (!curState.Extrapolated)
|
||||
using (_prof.Group("FullRep"))
|
||||
{
|
||||
_processor.UpdateFullRep(curState);
|
||||
if (!curState.Extrapolated)
|
||||
{
|
||||
_processor.UpdateFullRep(curState);
|
||||
}
|
||||
}
|
||||
|
||||
// Store last tick we got from the GameStateProcessor.
|
||||
_lastProcessedTick = _timing.CurTick;
|
||||
|
||||
// apply current state
|
||||
var createdEntities = ApplyGameState(curState, nextState);
|
||||
List<EntityUid> createdEntities;
|
||||
using (_prof.Group("ApplyGameState"))
|
||||
{
|
||||
createdEntities = ApplyGameState(curState, nextState);
|
||||
}
|
||||
|
||||
MergeImplicitData(createdEntities);
|
||||
using (_prof.Group("MergeImplicitData"))
|
||||
{
|
||||
MergeImplicitData(createdEntities);
|
||||
}
|
||||
|
||||
if (_lastProcessedSeq < curState.LastProcessedInput)
|
||||
{
|
||||
@@ -270,6 +282,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
if (IsPredictionEnabled)
|
||||
{
|
||||
using var _p = _prof.Group("Prediction");
|
||||
using var _ = _timing.StartPastPredictionArea();
|
||||
|
||||
if (_pendingInputs.Count > 0)
|
||||
@@ -290,6 +303,8 @@ namespace Robust.Client.GameStates
|
||||
|
||||
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
|
||||
{
|
||||
var groupStart = _prof.WriteGroupStart();
|
||||
|
||||
var tick = new GameTick(t);
|
||||
_timing.CurTick = tick;
|
||||
|
||||
@@ -321,30 +336,40 @@ namespace Robust.Client.GameStates
|
||||
|
||||
if (t != targetTick)
|
||||
{
|
||||
// Don't run EntitySystemManager.TickUpdate if this is the target tick,
|
||||
// because the rest of the main loop will call into it with the target tick later,
|
||||
// and it won't be a past prediction.
|
||||
_entitySystemManager.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: false);
|
||||
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
|
||||
using (_prof.Group("Systems"))
|
||||
{
|
||||
// Don't run EntitySystemManager.TickUpdate if this is the target tick,
|
||||
// because the rest of the main loop will call into it with the target tick later,
|
||||
// and it won't be a past prediction.
|
||||
_entitySystemManager.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: false);
|
||||
}
|
||||
|
||||
using (_prof.Group("Event queue"))
|
||||
{
|
||||
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
|
||||
}
|
||||
}
|
||||
|
||||
_prof.WriteGroupEnd(groupStart, "Prediction tick", ProfData.Int64(t));
|
||||
}
|
||||
}
|
||||
|
||||
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
|
||||
using (_prof.Group("Tick"))
|
||||
{
|
||||
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetPredictedEntities(GameTick curTick)
|
||||
{
|
||||
foreach (var meta in _entityManager.EntityQuery<MetaDataComponent>(true))
|
||||
using var _ = _prof.Group("ResetPredictedEntities");
|
||||
|
||||
var countReset = 0;
|
||||
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
|
||||
var query = _entityManager.GetEntityQuery<MetaDataComponent>();
|
||||
|
||||
foreach (var entity in system.GetDirtyEntities(curTick))
|
||||
{
|
||||
var entity = meta.Owner;
|
||||
|
||||
// TODO: 99% there's an off-by-one here.
|
||||
if (entity.IsClientSide() || meta.EntityLastModifiedTick < curTick)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check log level first to avoid the string alloc.
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($"Entity {entity} was made dirty.");
|
||||
@@ -355,6 +380,8 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
}
|
||||
|
||||
countReset += 1;
|
||||
|
||||
// TODO: handle component deletions/creations.
|
||||
foreach (var (netId, comp) in _entityManager.GetNetComponents(entity))
|
||||
{
|
||||
@@ -372,8 +399,14 @@ namespace Robust.Client.GameStates
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
comp.HandleComponentState(compState, null);
|
||||
comp.LastModifiedTick = curTick;
|
||||
}
|
||||
query.GetComponent(entity).EntityLastModifiedTick = curTick;
|
||||
}
|
||||
|
||||
system.Reset();
|
||||
|
||||
_prof.WriteValue("Reset count", ProfData.Int32(countReset));
|
||||
}
|
||||
|
||||
private void MergeImplicitData(List<EntityUid> createdEntities)
|
||||
@@ -409,27 +442,47 @@ namespace Robust.Client.GameStates
|
||||
|
||||
private void AckGameState(GameTick sequence)
|
||||
{
|
||||
var msg = _network.CreateNetMessage<MsgStateAck>();
|
||||
var msg = new MsgStateAck();
|
||||
msg.Sequence = sequence;
|
||||
_network.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
private List<EntityUid> ApplyGameState(GameState curState, GameState? nextState)
|
||||
{
|
||||
_config.TickProcessMessages();
|
||||
_mapManager.ApplyGameStatePre(curState.MapData, curState.EntityStates.Span);
|
||||
var createdEntities = ApplyEntityStates(curState.EntityStates.Span, curState.EntityDeletions.Span,
|
||||
nextState != null ? nextState.EntityStates.Span : default);
|
||||
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
|
||||
using (_prof.Group("Config"))
|
||||
{
|
||||
_config.TickProcessMessages();
|
||||
}
|
||||
|
||||
using (_prof.Group("Map Pre"))
|
||||
{
|
||||
_mapManager.ApplyGameStatePre(curState.MapData, curState.EntityStates.Span);
|
||||
}
|
||||
|
||||
List<EntityUid> createdEntities;
|
||||
using (_prof.Group("Entity"))
|
||||
{
|
||||
createdEntities = ApplyEntityStates(curState.EntityStates.Span, curState.EntityDeletions.Span,
|
||||
nextState != null ? nextState.EntityStates.Span : default);
|
||||
}
|
||||
|
||||
using (_prof.Group("Player"))
|
||||
{
|
||||
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
|
||||
}
|
||||
|
||||
using (_prof.Group("Callback"))
|
||||
{
|
||||
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
|
||||
}
|
||||
|
||||
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
|
||||
return createdEntities;
|
||||
}
|
||||
|
||||
private List<EntityUid> ApplyEntityStates(ReadOnlySpan<EntityState> curEntStates, ReadOnlySpan<EntityUid> deletions,
|
||||
ReadOnlySpan<EntityState> nextEntStates)
|
||||
{
|
||||
var toApply = new Dictionary<EntityUid, (EntityState?, EntityState?)>();
|
||||
var toApply = new Dictionary<EntityUid, (EntityState?, EntityState?)>(curEntStates.Length);
|
||||
var toInitialize = new List<EntityUid>();
|
||||
var created = new List<EntityUid>();
|
||||
|
||||
@@ -537,6 +590,9 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
#endif
|
||||
|
||||
_prof.WriteValue("Created", ProfData.Int32(created.Count));
|
||||
_prof.WriteValue("Applied", ProfData.Int32(toApply.Count));
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -548,6 +604,8 @@ namespace Robust.Client.GameStates
|
||||
|
||||
if (curState != null)
|
||||
{
|
||||
compStateWork.EnsureCapacity(curState.ComponentChanges.Span.Length);
|
||||
|
||||
foreach (var compChange in curState.ComponentChanges.Span)
|
||||
{
|
||||
if (compChange.Deleted)
|
||||
@@ -580,6 +638,8 @@ namespace Robust.Client.GameStates
|
||||
|
||||
if (nextState != null)
|
||||
{
|
||||
compStateWork.EnsureCapacity(compStateWork.Count + nextState.ComponentChanges.Span.Length);
|
||||
|
||||
foreach (var compState in nextState.ComponentChanges.Span)
|
||||
{
|
||||
if (compStateWork.TryGetValue(compState.NetID, out var state))
|
||||
|
||||
@@ -2,6 +2,7 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -132,6 +133,12 @@ namespace Robust.Client.Graphics
|
||||
|
||||
public ScreenCoordinates MapToScreen(MapCoordinates point)
|
||||
{
|
||||
if (CurrentEye.Position.MapId != point.MapId)
|
||||
{
|
||||
Logger.Error($"Attempted to convert map coordinates ({point}) to screen coordinates with an eye on another map ({CurrentEye.Position.MapId})");
|
||||
return new(default, WindowId.Invalid);
|
||||
}
|
||||
|
||||
return new(WorldToScreen(point.Position), MainViewport.Window?.Id ?? default);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,20 +5,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private static readonly (string, uint)[] BaseShaderAttribLocations =
|
||||
{
|
||||
("aPos", 0),
|
||||
("tCoord", 1)
|
||||
("tCoord", 1),
|
||||
("modulate", 2)
|
||||
};
|
||||
|
||||
private const int UniIModUV = 0;
|
||||
private const int UniIModelMatrix = 1;
|
||||
private const int UniIModulate = 2;
|
||||
private const int UniITexturePixelSize = 3;
|
||||
private const int UniIMainTexture = 4;
|
||||
private const int UniILightTexture = 5;
|
||||
private const int UniCount = 6;
|
||||
private const int UniITexturePixelSize = 2;
|
||||
private const int UniIMainTexture = 3;
|
||||
private const int UniILightTexture = 4;
|
||||
private const int UniCount = 5;
|
||||
|
||||
private const string UniModUV = "modifyUV";
|
||||
private const string UniModelMatrix = "modelMatrix";
|
||||
private const string UniModulate = "modulate";
|
||||
private const string UniTexturePixelSize = "TEXTURE_PIXEL_SIZE";
|
||||
private const string UniMainTexture = "TEXTURE";
|
||||
private const string UniLightTexture = "lightMap";
|
||||
|
||||
@@ -37,7 +37,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
gridProgram.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
|
||||
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
|
||||
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
|
||||
gridProgram.SetUniform(UniIModulate, Color.White);
|
||||
|
||||
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
|
||||
{
|
||||
@@ -50,7 +49,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(grid.GridEntityId);
|
||||
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
|
||||
grid.GetMapChunks(worldBounds, out var enumerator);
|
||||
var enumerator = grid.GetMapChunks(worldBounds);
|
||||
|
||||
while (enumerator.MoveNext(out var chunk))
|
||||
{
|
||||
@@ -115,10 +114,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var gy = y + cScaled.Y;
|
||||
|
||||
var vIdx = i * 4;
|
||||
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom);
|
||||
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom);
|
||||
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top);
|
||||
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top);
|
||||
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top, Color.White);
|
||||
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top, Color.White);
|
||||
var nIdx = i * GetQuadBatchIndexCount();
|
||||
var tIdx = (ushort)(i * 4);
|
||||
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
|
||||
@@ -136,13 +135,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
datum.TileCount = i;
|
||||
}
|
||||
|
||||
private MapChunkData _initChunkBuffers(IMapGrid grid, MapChunk chunk)
|
||||
private unsafe MapChunkData _initChunkBuffers(IMapGrid grid, MapChunk chunk)
|
||||
{
|
||||
var vao = GenVertexArray();
|
||||
BindVertexArray(vao);
|
||||
CheckGlError();
|
||||
|
||||
var vboSize = _verticesPerChunk(chunk) * Vertex2D.SizeOf;
|
||||
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
|
||||
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
|
||||
|
||||
var vbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
|
||||
@@ -151,12 +150,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
eboSize, $"Grid {grid.Index} chunk {chunk.Indices} EBO");
|
||||
|
||||
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Index} chunk {chunk.Indices} VAO");
|
||||
// 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);
|
||||
SetupVAOLayout();
|
||||
CheckGlError();
|
||||
|
||||
// Assign VBO and EBO to VAO.
|
||||
|
||||
@@ -13,7 +13,8 @@ using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Profiling;
|
||||
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
@@ -97,6 +98,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
ClearFramebuffer(_userInterfaceManager.GetMainClearColor());
|
||||
|
||||
using (DebugGroup("UI"))
|
||||
using (_prof.Group("UI"))
|
||||
{
|
||||
_userInterfaceManager.Render(_renderHandle);
|
||||
FlushRenderQueue();
|
||||
@@ -104,29 +106,46 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
TakeScreenshot(ScreenshotType.Final);
|
||||
|
||||
// And finally, swap those buffers!
|
||||
SwapAllBuffers();
|
||||
using (_prof.Group("Swap buffers"))
|
||||
{
|
||||
// And finally, swap those buffers!
|
||||
SwapAllBuffers();
|
||||
}
|
||||
|
||||
using (_prof.Group("Stats"))
|
||||
{
|
||||
_prof.WriteValue("GL Draw Calls", ProfData.Int32(_debugStats.LastGLDrawCalls));
|
||||
_prof.WriteValue("Clyde Draw Calls", ProfData.Int32(_debugStats.LastClydeDrawCalls));
|
||||
_prof.WriteValue("Batches", ProfData.Int32(_debugStats.LastBatches));
|
||||
_prof.WriteValue("Max Batch Verts", ProfData.Int32(_debugStats.LargestBatchVertices));
|
||||
_prof.WriteValue("Max Batch Idxes", ProfData.Int32(_debugStats.LargestBatchIndices));
|
||||
_prof.WriteValue("Lights", ProfData.Int32(_debugStats.TotalLights));
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderSingleOverlay(Overlay overlay, Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
|
||||
{
|
||||
if (overlay.RequestScreenTexture)
|
||||
{
|
||||
FlushRenderQueue();
|
||||
UpdateOverlayScreenTexture(space, vp.RenderTarget);
|
||||
}
|
||||
|
||||
if (overlay.OverwriteTargetFrameBuffer())
|
||||
{
|
||||
ClearFramebuffer(default);
|
||||
}
|
||||
|
||||
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox, worldBounds);
|
||||
}
|
||||
|
||||
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
|
||||
{
|
||||
using (DebugGroup($"Overlays: {space}"))
|
||||
{
|
||||
var list = GetOverlaysForSpace(space);
|
||||
foreach (var overlay in list)
|
||||
foreach (var overlay in GetOverlaysForSpace(space))
|
||||
{
|
||||
if (overlay.RequestScreenTexture)
|
||||
{
|
||||
FlushRenderQueue();
|
||||
UpdateOverlayScreenTexture(space, vp.RenderTarget);
|
||||
}
|
||||
|
||||
if (overlay.OverwriteTargetFrameBuffer())
|
||||
{
|
||||
ClearFramebuffer(default);
|
||||
}
|
||||
|
||||
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox, worldBounds);
|
||||
RenderSingleOverlay(overlay, vp, space, worldBox, worldBounds);
|
||||
}
|
||||
|
||||
FlushRenderQueue();
|
||||
@@ -140,11 +159,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
OverlaySpace space,
|
||||
in UIBox2i bounds)
|
||||
{
|
||||
using var _ = _prof.Group($"Overlays SS {space}");
|
||||
|
||||
var list = GetOverlaysForSpace(space);
|
||||
|
||||
var worldAABB = CalcWorldAABB(vp);
|
||||
var worldBounds = CalcWorldBounds(vp);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, worldAABB, worldBounds);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, vp.Eye!.Position.MapId, worldAABB, worldBounds);
|
||||
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
@@ -193,6 +214,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (lastFrameSize != texture.Size)
|
||||
{
|
||||
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
|
||||
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
|
||||
(int)OGLTextureWrapMode.MirroredRepeat);
|
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
|
||||
(int)OGLTextureWrapMode.MirroredRepeat);
|
||||
|
||||
GL.TexImage2D(TextureTarget.Texture2D, 0,
|
||||
_hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, texture.Size.X,
|
||||
texture.Size.Y, 0,
|
||||
@@ -213,7 +240,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
|
||||
{
|
||||
var mapId = eye.Position.MapId;
|
||||
@@ -228,17 +254,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
ProcessSpriteEntities(mapId, viewport, eye, worldBounds, _drawingSpriteList);
|
||||
|
||||
var worldOverlays = new List<Overlay>();
|
||||
|
||||
foreach (var overlay in _overlayManager.AllOverlays)
|
||||
{
|
||||
if ((overlay.Space & OverlaySpace.WorldSpace) != 0)
|
||||
{
|
||||
worldOverlays.Add(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
worldOverlays.Sort(OverlayComparer.Instance);
|
||||
var worldOverlays = GetOverlaysForSpace(OverlaySpace.WorldSpaceEntities);
|
||||
|
||||
// We use a separate list for indexing so that the sort is faster.
|
||||
var indexList = ArrayPool<int>.Shared.Rent(_drawingSpriteList.Count);
|
||||
@@ -249,41 +265,34 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
var overlayIndex = 0;
|
||||
RenderTexture? entityPostRenderTarget = null;
|
||||
Array.Sort(indexList, 0, _drawingSpriteList.Count, new SpriteDrawingOrderComparer(_drawingSpriteList));
|
||||
|
||||
bool flushed = false;
|
||||
for (var i = 0; i < _drawingSpriteList.Count; i++)
|
||||
{
|
||||
ref var entry = ref _drawingSpriteList[indexList[i]];
|
||||
var flushed = false;
|
||||
|
||||
for (var j = overlayIndex; j < worldOverlays.Count; j++)
|
||||
{
|
||||
overlayIndex = j;
|
||||
var overlay = worldOverlays[j];
|
||||
|
||||
if (overlay.ZIndex <= entry.sprite.DrawDepth)
|
||||
if (overlay.ZIndex > entry.sprite.DrawDepth)
|
||||
{
|
||||
if (!flushed)
|
||||
{
|
||||
FlushRenderQueue();
|
||||
flushed = true;
|
||||
}
|
||||
|
||||
overlay.ClydeRender(
|
||||
_renderHandle,
|
||||
OverlaySpace.WorldSpace,
|
||||
null,
|
||||
viewport,
|
||||
new UIBox2i((0, 0), viewport.Size),
|
||||
worldAABB,
|
||||
worldBounds);
|
||||
overlayIndex = j;
|
||||
continue;
|
||||
flushed = false;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
if (!flushed)
|
||||
{
|
||||
FlushRenderQueue();
|
||||
flushed = true;
|
||||
}
|
||||
|
||||
RenderSingleOverlay(overlay, viewport, OverlaySpace.WorldSpaceEntities, worldAABB, worldBounds);
|
||||
}
|
||||
|
||||
RenderTexture? entityPostRenderTarget = null;
|
||||
Vector2i roundedPos = default;
|
||||
if (entry.sprite.PostShader != null)
|
||||
{
|
||||
@@ -301,10 +310,27 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// check that sprite size is valid
|
||||
if (screenSpriteSize.X > 0 && screenSpriteSize.Y > 0)
|
||||
{
|
||||
// create new render texture with correct sprite size
|
||||
entityPostRenderTarget = CreateRenderTarget(screenSpriteSize,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
|
||||
name: nameof(entityPostRenderTarget));
|
||||
// This is really bare-bones render target re-use logic. One problem is that if it ever draws a
|
||||
// single large entity in a frame, the render target may be way to big for every subsequent
|
||||
// entity. But the vast majority of sprites are currently all 32x32, so it doesn't matter all that
|
||||
// much.
|
||||
//
|
||||
// Also, if there are many differenty sizes, and they all happen to be drawn in order of
|
||||
// increasing size, then this will still generate a whole bunch of render targets. So maybe
|
||||
// iterate once _drawingSpriteList, check sprite sizes, and decide what render targets to create
|
||||
// based off of that?
|
||||
//
|
||||
// TODO PERFORMANCE better renderTarget re-use / caching.
|
||||
|
||||
if (entityPostRenderTarget == null
|
||||
|| entityPostRenderTarget.Size.X < screenSpriteSize.X
|
||||
|| entityPostRenderTarget.Size.Y < screenSpriteSize.Y)
|
||||
{
|
||||
entityPostRenderTarget = CreateRenderTarget(screenSpriteSize,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
|
||||
name: nameof(entityPostRenderTarget));
|
||||
}
|
||||
|
||||
_renderHandle.UseRenderTarget(entityPostRenderTarget);
|
||||
_renderHandle.Clear(new Color());
|
||||
|
||||
@@ -343,13 +369,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
_renderHandle.SetProjView(oldProj, oldView);
|
||||
_renderHandle.UseShader(null);
|
||||
|
||||
// TODO: cache this properly across frames.
|
||||
entityPostRenderTarget.DisposeDeferred();
|
||||
}
|
||||
}
|
||||
|
||||
// draw remainder of overlays
|
||||
for (var j = overlayIndex; j < worldOverlays.Count; j++)
|
||||
{
|
||||
if (!flushed)
|
||||
{
|
||||
FlushRenderQueue();
|
||||
flushed = true;
|
||||
}
|
||||
|
||||
RenderSingleOverlay(worldOverlays[j], viewport, OverlaySpace.WorldSpaceEntities, worldAABB, worldBounds);
|
||||
}
|
||||
|
||||
ArrayPool<int>.Shared.Return(indexList);
|
||||
entityPostRenderTarget?.DisposeDeferred();
|
||||
|
||||
_drawingSpriteList.Clear();
|
||||
FlushRenderQueue();
|
||||
@@ -450,6 +486,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return;
|
||||
}
|
||||
|
||||
using var _ = _prof.Group("Viewport");
|
||||
|
||||
RenderInRenderTarget(viewport.RenderTarget, () =>
|
||||
{
|
||||
using var _ = DebugGroup($"Viewport: {viewport.Name}");
|
||||
@@ -471,24 +509,33 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (_eyeManager.CurrentMap != MapId.Nullspace)
|
||||
{
|
||||
using (DebugGroup("Lights"))
|
||||
using (_prof.Group("Lights"))
|
||||
{
|
||||
DrawLightsAndFov(viewport, worldBounds, worldAABB, eye);
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldAABB, worldBounds);
|
||||
using (_prof.Group("Overlays WSBW"))
|
||||
{
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldAABB, worldBounds);
|
||||
}
|
||||
|
||||
using (DebugGroup("Grids"))
|
||||
using (_prof.Group("Grids"))
|
||||
{
|
||||
_drawGrids(viewport, worldBounds, eye);
|
||||
}
|
||||
|
||||
// We will also render worldspace overlays here so we can do them under / above entities as necessary
|
||||
using (DebugGroup("Entities"))
|
||||
using (_prof.Group("Entities"))
|
||||
{
|
||||
DrawEntities(viewport, worldBounds, worldAABB, eye);
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
|
||||
using (_prof.Group("Overlays WSBFOV"))
|
||||
{
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
|
||||
}
|
||||
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
|
||||
{
|
||||
@@ -521,7 +568,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
UIBox2.FromDimensions(Vector2.Zero, viewport.Size), new Color(1, 1, 1, 0.5f));
|
||||
}
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB, worldBounds);
|
||||
using (_prof.Group("Overlays WS"))
|
||||
{
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB, worldBounds);
|
||||
}
|
||||
|
||||
_currentViewport = oldVp;
|
||||
}, viewport.ClearColor);
|
||||
|
||||
@@ -272,7 +272,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadGLProc<T>(string name, out T field) where T : Delegate
|
||||
private nint LoadGLProc(string name)
|
||||
{
|
||||
var proc = _glBindingsContext.GetProcAddress(name);
|
||||
if (proc == IntPtr.Zero || proc == new IntPtr(1) || proc == new IntPtr(2))
|
||||
@@ -280,7 +280,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
throw new InvalidOperationException($"Unable to load GL function '{name}'!");
|
||||
}
|
||||
|
||||
field = Marshal.GetDelegateForFunctionPointer<T>(proc);
|
||||
return proc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using JetBrains.Annotations;
|
||||
@@ -8,45 +9,64 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Contains various layout/rendering structs used inside Clyde.
|
||||
internal partial class Clyde
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets up VAO layout for Vertex2D for base and raw shader types.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe void SetupVAOLayout()
|
||||
{
|
||||
// Vertex Coords
|
||||
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 0);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
// Texture Coords.
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(1);
|
||||
// Colour Modulation.
|
||||
GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(2);
|
||||
}
|
||||
|
||||
// NOTE: This is:
|
||||
// + Directly cast from DrawVertexUV2DColor!!!
|
||||
// + GLContextWindow does it's own thing with this for winblit, be careful!
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[PublicAPI]
|
||||
private readonly struct Vertex2D
|
||||
{
|
||||
public static readonly int SizeOf;
|
||||
|
||||
public readonly Vector2 Position;
|
||||
public readonly Vector2 TextureCoordinates;
|
||||
|
||||
static Vertex2D()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
SizeOf = sizeof(Vertex2D);
|
||||
}
|
||||
}
|
||||
// Note that this color is in linear space.
|
||||
public readonly Color Modulate;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(Vector2 position, Vector2 textureCoordinates)
|
||||
public Vertex2D(Vector2 position, Vector2 textureCoordinates, Color modulate)
|
||||
{
|
||||
Position = position;
|
||||
TextureCoordinates = textureCoordinates;
|
||||
Modulate = modulate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(float x, float y, float u, float v)
|
||||
: this(new Vector2(x, y), new Vector2(u, v))
|
||||
public Vertex2D(float x, float y, float u, float v, float r, float g, float b, float a)
|
||||
: this(new Vector2(x, y), new Vector2(u, v), new Color(r, g, b, a))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(Vector2 position, float u, float v)
|
||||
: this(position, new Vector2(u, v))
|
||||
public Vertex2D(float x, float y, float u, float v, Color modulate)
|
||||
: this(new Vector2(x, y), new Vector2(u, v), modulate)
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(Vector2 position, float u, float v, Color modulate)
|
||||
: this(position, new Vector2(u, v), modulate)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Vertex2D: {Position}, {TextureCoordinates}";
|
||||
return $"Vertex2D: {Position}, {TextureCoordinates}, {Modulate}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// For depth calculation of lighting shadows.
|
||||
private RenderTexture _shadowRenderTarget = default!;
|
||||
|
||||
// Used because otherwise a MaxLightsPerScene change callback getting hit on startup causes interesting issues (read: bugs)
|
||||
private bool _shadowRenderTargetCanInitializeSafely = false;
|
||||
|
||||
@@ -91,7 +92,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private ClydeTexture FovTexture => _fovRenderTarget.Texture;
|
||||
private ClydeTexture ShadowTexture => _shadowRenderTarget.Texture;
|
||||
|
||||
private (PointLightComponent light, Vector2 pos, float distanceSquared)[] _lightsToRenderList = new (PointLightComponent light, Vector2 pos, float distanceSquared)[LightsToRenderListSize];
|
||||
private (PointLightComponent light, Vector2 pos, float distanceSquared)[] _lightsToRenderList =
|
||||
new (PointLightComponent light, Vector2 pos, float distanceSquared)[LightsToRenderListSize];
|
||||
|
||||
private unsafe void InitLighting()
|
||||
{
|
||||
@@ -151,7 +153,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// FOV FBO.
|
||||
_fovRenderTarget = CreateRenderTarget((FovMapSize, 2),
|
||||
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
|
||||
new RenderTargetFormatParameters(
|
||||
_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
|
||||
new TextureSampleParameters { WrapMode = TextureWrapMode.Repeat },
|
||||
nameof(_fovRenderTarget));
|
||||
|
||||
@@ -175,7 +178,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var depthVert = ReadEmbeddedShader("shadow-depth.vert");
|
||||
var depthFrag = ReadEmbeddedShader("shadow-depth.frag");
|
||||
|
||||
(string, uint)[] attribLocations = {
|
||||
(string, uint)[] attribLocations =
|
||||
{
|
||||
("aPos", 0),
|
||||
("subVertex", 1)
|
||||
};
|
||||
@@ -208,6 +212,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private void DrawFov(Viewport viewport, IEye eye)
|
||||
{
|
||||
using var _ = DebugGroup(nameof(DrawFov));
|
||||
using var _p = _prof.Group("DrawFov");
|
||||
|
||||
PrepareDepthDraw(RtToLoaded(_fovRenderTarget));
|
||||
|
||||
@@ -294,6 +299,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
GL.ClearColor(1, 1, 1, 1);
|
||||
}
|
||||
|
||||
CheckGlError();
|
||||
GL.Clear(ClearBufferMask.DepthBufferBit | ClearBufferMask.ColorBufferBit);
|
||||
CheckGlError();
|
||||
@@ -336,7 +342,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return;
|
||||
}
|
||||
|
||||
var (lights, count, expandedBounds) = GetLightsToRender(mapId, worldBounds, worldAABB);
|
||||
(PointLightComponent light, Vector2 pos, float distanceSquared)[] lights;
|
||||
int count;
|
||||
Box2 expandedBounds;
|
||||
using (_prof.Group("LightsToRender"))
|
||||
{
|
||||
(lights, count, expandedBounds) = GetLightsToRender(mapId, worldBounds, worldAABB);
|
||||
}
|
||||
|
||||
eye.GetViewMatrixNoOffset(out var eyeTransform, eye.Scale);
|
||||
|
||||
UpdateOcclusionGeometry(mapId, expandedBounds, eyeTransform);
|
||||
@@ -352,6 +365,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
using (DebugGroup("Draw shadow depth"))
|
||||
using (_prof.Group("Draw shadow depth"))
|
||||
{
|
||||
PrepareDepthDraw(RtToLoaded(_shadowRenderTarget));
|
||||
GL.CullFace(CullFaceMode.Back);
|
||||
@@ -389,7 +403,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
ApplyLightingFovToBuffer(viewport, eye);
|
||||
|
||||
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle].Program;
|
||||
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle]
|
||||
.Program;
|
||||
lightShader.Use();
|
||||
|
||||
SetupGlobalUniformsImmediate(lightShader, ShadowTexture);
|
||||
@@ -411,78 +426,82 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var lastSoftness = float.NaN;
|
||||
Texture? lastMask = null;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
using (_prof.Group("Draw Lights"))
|
||||
{
|
||||
var (component, lightPos, _) = lights[i];
|
||||
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(component.Owner);
|
||||
|
||||
Texture? mask = null;
|
||||
var rotation = Angle.Zero;
|
||||
if (component.Mask != null)
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
mask = component.Mask;
|
||||
rotation = component.Rotation;
|
||||
var (component, lightPos, _) = lights[i];
|
||||
|
||||
if (component.MaskAutoRotate)
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(component.Owner);
|
||||
|
||||
Texture? mask = null;
|
||||
var rotation = Angle.Zero;
|
||||
if (component.Mask != null)
|
||||
{
|
||||
rotation += transform.WorldRotation;
|
||||
mask = component.Mask;
|
||||
rotation = component.Rotation;
|
||||
|
||||
if (component.MaskAutoRotate)
|
||||
{
|
||||
rotation += transform.WorldRotation;
|
||||
}
|
||||
}
|
||||
|
||||
var maskTexture = mask ?? Texture.White;
|
||||
if (lastMask != maskTexture)
|
||||
{
|
||||
SetTexture(TextureUnit.Texture0, maskTexture);
|
||||
lastMask = maskTexture;
|
||||
lightShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseToPercent(lastRange, component.Radius))
|
||||
{
|
||||
lastRange = component.Radius;
|
||||
lightShader.SetUniformMaybe("lightRange", lastRange);
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseToPercent(lastPower, component.Energy))
|
||||
{
|
||||
lastPower = component.Energy;
|
||||
lightShader.SetUniformMaybe("lightPower", lastPower);
|
||||
}
|
||||
|
||||
if (lastColor != component.Color)
|
||||
{
|
||||
lastColor = component.Color;
|
||||
lightShader.SetUniformMaybe("lightColor", lastColor);
|
||||
}
|
||||
|
||||
if (_enableSoftShadows && !MathHelper.CloseToPercent(lastSoftness, component.Softness))
|
||||
{
|
||||
lastSoftness = component.Softness;
|
||||
lightShader.SetUniformMaybe("lightSoftness", lastSoftness);
|
||||
}
|
||||
|
||||
lightShader.SetUniformMaybe("lightCenter", lightPos);
|
||||
lightShader.SetUniformMaybe("lightIndex",
|
||||
component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1);
|
||||
|
||||
var offset = new Vector2(component.Radius, component.Radius);
|
||||
|
||||
Matrix3 matrix;
|
||||
if (mask == null)
|
||||
{
|
||||
matrix = Matrix3.Identity;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only apply rotation if a mask is said, because else it doesn't matter.
|
||||
matrix = Matrix3.CreateRotation(rotation);
|
||||
}
|
||||
|
||||
(matrix.R0C2, matrix.R1C2) = lightPos;
|
||||
|
||||
_drawQuad(-offset, offset, matrix, lightShader);
|
||||
|
||||
_debugStats.TotalLights += 1;
|
||||
}
|
||||
|
||||
var maskTexture = mask ?? Texture.White;
|
||||
if (lastMask != maskTexture)
|
||||
{
|
||||
SetTexture(TextureUnit.Texture0, maskTexture);
|
||||
lastMask = maskTexture;
|
||||
lightShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseToPercent(lastRange, component.Radius))
|
||||
{
|
||||
lastRange = component.Radius;
|
||||
lightShader.SetUniformMaybe("lightRange", lastRange);
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseToPercent(lastPower, component.Energy))
|
||||
{
|
||||
lastPower = component.Energy;
|
||||
lightShader.SetUniformMaybe("lightPower", lastPower);
|
||||
}
|
||||
|
||||
if (lastColor != component.Color)
|
||||
{
|
||||
lastColor = component.Color;
|
||||
lightShader.SetUniformMaybe("lightColor", lastColor);
|
||||
}
|
||||
|
||||
if (_enableSoftShadows && !MathHelper.CloseToPercent(lastSoftness, component.Softness))
|
||||
{
|
||||
lastSoftness = component.Softness;
|
||||
lightShader.SetUniformMaybe("lightSoftness", lastSoftness);
|
||||
}
|
||||
|
||||
lightShader.SetUniformMaybe("lightCenter", lightPos);
|
||||
lightShader.SetUniformMaybe("lightIndex", component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1);
|
||||
|
||||
var offset = new Vector2(component.Radius, component.Radius);
|
||||
|
||||
Matrix3 matrix;
|
||||
if (mask == null)
|
||||
{
|
||||
matrix = Matrix3.Identity;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only apply rotation if a mask is said, because else it doesn't matter.
|
||||
matrix = Matrix3.CreateRotation(rotation);
|
||||
}
|
||||
|
||||
(matrix.R0C2, matrix.R1C2) = lightPos;
|
||||
|
||||
_drawQuad(-offset, offset, matrix, lightShader);
|
||||
|
||||
_debugStats.TotalLights += 1;
|
||||
}
|
||||
|
||||
ResetBlendFunc();
|
||||
@@ -494,9 +513,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (_cfg.GetCVar(CVars.DisplayBlurLight))
|
||||
BlurLights(viewport, eye);
|
||||
|
||||
BlurOntoWalls(viewport, eye);
|
||||
using (_prof.Group("BlurOntoWalls"))
|
||||
{
|
||||
BlurOntoWalls(viewport, eye);
|
||||
}
|
||||
|
||||
MergeWallLayer(viewport);
|
||||
using (_prof.Group("MergeWallLayer"))
|
||||
{
|
||||
MergeWallLayer(viewport);
|
||||
}
|
||||
|
||||
BindRenderTargetFull(viewport.RenderTarget);
|
||||
GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y);
|
||||
@@ -507,7 +532,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_lightingReady = true;
|
||||
}
|
||||
|
||||
private ((PointLightComponent light, Vector2 pos, float distanceSquared)[] lights, int count, Box2 expandedBounds)
|
||||
private ((PointLightComponent light, Vector2 pos, float distanceSquared)[] lights, int count, Box2
|
||||
expandedBounds)
|
||||
GetLightsToRender(MapId map, in Box2Rotated worldBounds, in Box2 worldAABB)
|
||||
{
|
||||
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
|
||||
@@ -521,36 +547,39 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(worldBounds);
|
||||
|
||||
comp.LightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldAABB, int count, int shadowCastingCount) state, in PointLightComponent light) =>
|
||||
{
|
||||
if (state.count >= LightsToRenderListSize)
|
||||
comp.LightTree.QueryAabb(ref state,
|
||||
(ref (Clyde clyde, Box2 worldAABB, int count, int shadowCastingCount) state,
|
||||
in PointLightComponent light) =>
|
||||
{
|
||||
// There are too many lights to fit in the static memory.
|
||||
return false;
|
||||
}
|
||||
if (state.count >= LightsToRenderListSize)
|
||||
{
|
||||
// There are too many lights to fit in the static memory.
|
||||
return false;
|
||||
}
|
||||
|
||||
var transform = xforms.GetComponent(light.Owner);
|
||||
var transform = xforms.GetComponent(light.Owner);
|
||||
|
||||
if (float.IsNaN(transform.LocalPosition.X) || float.IsNaN(transform.LocalPosition.Y)) return true;
|
||||
if (float.IsNaN(transform.LocalPosition.X) || float.IsNaN(transform.LocalPosition.Y))
|
||||
return true;
|
||||
|
||||
var lightPos = transform.WorldMatrix.Transform(light.Offset);
|
||||
var lightPos = transform.WorldMatrix.Transform(light.Offset);
|
||||
|
||||
var circle = new Circle(lightPos, light.Radius);
|
||||
var circle = new Circle(lightPos, light.Radius);
|
||||
|
||||
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
|
||||
if (!circle.Intersects(state.worldAABB))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the light is a shadow casting light, keep a separate track of that
|
||||
if (light.CastShadows) state.shadowCastingCount++;
|
||||
|
||||
float distanceSquared = (state.worldAABB.Center - lightPos).LengthSquared;
|
||||
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
|
||||
|
||||
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
|
||||
if (!circle.Intersects(state.worldAABB))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the light is a shadow casting light, keep a separate track of that
|
||||
if (light.CastShadows) state.shadowCastingCount++;
|
||||
|
||||
float distanceSquared = (state.worldAABB.Center - lightPos).LengthSquared;
|
||||
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
|
||||
|
||||
return true;
|
||||
}, bounds);
|
||||
}, bounds);
|
||||
}
|
||||
|
||||
if (state.shadowCastingCount > _maxLightsPerScene)
|
||||
@@ -560,18 +589,20 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// First, partition the array based on whether the lights are shadow casting or not
|
||||
// (non shadow casting lights should be the first partition, shadow casting lights the second)
|
||||
Array.Sort(_lightsToRenderList, 0, state.count, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
|
||||
{
|
||||
if (x.light.CastShadows && !y.light.CastShadows) return 1;
|
||||
else if (!x.light.CastShadows && y.light.CastShadows) return -1;
|
||||
else return 0;
|
||||
}));
|
||||
Array.Sort(_lightsToRenderList, 0, state.count,
|
||||
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
|
||||
{
|
||||
if (x.light.CastShadows && !y.light.CastShadows) return 1;
|
||||
else if (!x.light.CastShadows && y.light.CastShadows) return -1;
|
||||
else return 0;
|
||||
}));
|
||||
|
||||
// Next, sort just the shadow casting lights by distance.
|
||||
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
|
||||
{
|
||||
return x.distanceSquared.CompareTo(y.distanceSquared);
|
||||
}));
|
||||
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount,
|
||||
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
|
||||
{
|
||||
return x.distanceSquared.CompareTo(y.distanceSquared);
|
||||
}));
|
||||
|
||||
// Then effectively delete the furthest lights, by setting the end of the array to exclude N
|
||||
// number of shadow casting lights (where N is the number above the max number per scene.)
|
||||
@@ -755,7 +786,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
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.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);
|
||||
|
||||
@@ -857,6 +889,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void UpdateOcclusionGeometry(MapId map, Box2 expandedBounds, Matrix3 eyeTransform)
|
||||
{
|
||||
using var _ = _prof.Group("UpdateOcclusionGeometry");
|
||||
using var _p = DebugGroup(nameof(UpdateOcclusionGeometry));
|
||||
|
||||
// This method generates two sets of occlusion geometry:
|
||||
// 3D geometry used during depth projection.
|
||||
// 2D mask geometry used to apply wall bleed.
|
||||
@@ -865,8 +900,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
const int maxOccluders = 2048;
|
||||
|
||||
using var _ = DebugGroup(nameof(UpdateOcclusionGeometry));
|
||||
|
||||
// 16 = 4 vertices * 4 directions
|
||||
var arrayBuffer = ArrayPool<Vector4>.Shared.Rent(maxOccluders * 4 * 4);
|
||||
// multiplied by 2 (it's a vector2 of bytes)
|
||||
@@ -971,6 +1004,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// I don't like this, but rotated occluders started happening
|
||||
return Vector2.Dot(normal, a) <= 0;
|
||||
}
|
||||
|
||||
var nV = ((!no) && CheckFaceEyeVis(dTl, dTr));
|
||||
var sV = ((!so) && CheckFaceEyeVis(dBr, dBl));
|
||||
var eV = ((!eo) && CheckFaceEyeVis(dTr, dBr));
|
||||
@@ -999,6 +1033,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// height
|
||||
arrayVIBuffer[avi++] = (byte)(((vi & 2) != 0) ? 0 : 255);
|
||||
}
|
||||
|
||||
QuadBatchIndexWrite(indexBuffer, ref ii, (ushort)aiBase);
|
||||
}
|
||||
|
||||
@@ -1074,7 +1109,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var lightMapSize = GetLightMapSize(viewport.Size);
|
||||
var lightMapSizeQuart = GetLightMapSize(viewport.Size, true);
|
||||
var lightMapColorFormat = _hasGLFloatFramebuffers ? RenderTargetColorFormat.R11FG11FB10F : RenderTargetColorFormat.Rgba8;
|
||||
var lightMapColorFormat = _hasGLFloatFramebuffers
|
||||
? RenderTargetColorFormat.R11FG11FB10F
|
||||
: RenderTargetColorFormat.Rgba8;
|
||||
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
|
||||
|
||||
viewport.LightRenderTarget?.Dispose();
|
||||
@@ -1147,9 +1184,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
DeleteRenderTexture(_shadowRenderTarget.Handle);
|
||||
}
|
||||
|
||||
// Shadow FBO.
|
||||
_shadowRenderTarget = CreateRenderTarget((ShadowMapSize, _maxLightsPerScene),
|
||||
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
|
||||
new RenderTargetFormatParameters(
|
||||
_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
|
||||
new TextureSampleParameters { WrapMode = TextureWrapMode.Repeat, Filter = true },
|
||||
nameof(_shadowRenderTarget));
|
||||
}
|
||||
|
||||
@@ -208,60 +208,34 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_clyde.DrawClear(color);
|
||||
}
|
||||
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan<Vector2> vertices,
|
||||
Color color)
|
||||
{
|
||||
// TODO: Maybe don't stackalloc if the data is too large.
|
||||
Span<DrawVertexUV2D> drawVertices = stackalloc DrawVertexUV2D[vertices.Length];
|
||||
PadVertices(vertices, drawVertices);
|
||||
|
||||
DrawPrimitives(primitiveTopology, Texture.White, drawVertices, color);
|
||||
}
|
||||
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<Vector2> vertices, Color color)
|
||||
{
|
||||
// TODO: Maybe don't stackalloc if the data is too large.
|
||||
Span<DrawVertexUV2D> drawVertices = stackalloc DrawVertexUV2D[vertices.Length];
|
||||
PadVertices(vertices, drawVertices);
|
||||
|
||||
DrawPrimitives(primitiveTopology, Texture.White, indices, drawVertices, color);
|
||||
}
|
||||
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<DrawVertexUV2D> vertices, Color color)
|
||||
ReadOnlySpan<DrawVertexUV2DColor> vertices)
|
||||
{
|
||||
if (!(texture is ClydeTexture clydeTexture))
|
||||
{
|
||||
throw new ArgumentException("Texture must be a basic texture.");
|
||||
}
|
||||
|
||||
var castSpan = MemoryMarshal.Cast<DrawVertexUV2D, Vertex2D>(vertices);
|
||||
var castSpan = MemoryMarshal.Cast<DrawVertexUV2DColor, Vertex2D>(vertices);
|
||||
|
||||
_clyde.DrawPrimitives(primitiveTopology, clydeTexture.TextureId, castSpan, color);
|
||||
_clyde.DrawPrimitives(primitiveTopology, clydeTexture.TextureId, castSpan);
|
||||
}
|
||||
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<DrawVertexUV2D> vertices, Color color)
|
||||
ReadOnlySpan<DrawVertexUV2DColor> vertices)
|
||||
{
|
||||
if (!(texture is ClydeTexture clydeTexture))
|
||||
{
|
||||
throw new ArgumentException("Texture must be a basic texture.");
|
||||
}
|
||||
|
||||
var castSpan = MemoryMarshal.Cast<DrawVertexUV2D, Vertex2D>(vertices);
|
||||
var castSpan = MemoryMarshal.Cast<DrawVertexUV2DColor, Vertex2D>(vertices);
|
||||
|
||||
_clyde.DrawPrimitives(primitiveTopology, clydeTexture.TextureId, indices, castSpan, color);
|
||||
_clyde.DrawPrimitives(primitiveTopology, clydeTexture.TextureId, indices, castSpan);
|
||||
}
|
||||
|
||||
private void PadVertices(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2D> output)
|
||||
{
|
||||
for (var i = 0; i < output.Length; i++)
|
||||
{
|
||||
output[i] = new DrawVertexUV2D(input[i], (0.5f, 0.5f));
|
||||
}
|
||||
}
|
||||
// ---- (end) ----
|
||||
|
||||
private sealed class DrawingHandleScreenImpl : DrawingHandleScreen
|
||||
{
|
||||
@@ -282,38 +256,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_renderHandle.UseShader(shader);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology,
|
||||
ReadOnlySpan<Vector2> vertices,
|
||||
Color color)
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<DrawVertexUV2DColor> vertices)
|
||||
{
|
||||
var realColor = color * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, vertices, realColor);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology,
|
||||
ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<Vector2> vertices, Color color)
|
||||
{
|
||||
var realColor = color * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, indices, vertices, realColor);
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, vertices);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<DrawVertexUV2D> vertices, Color? color = null)
|
||||
ReadOnlySpan<ushort> indices, ReadOnlySpan<DrawVertexUV2DColor> vertices)
|
||||
{
|
||||
var realColor = (color ?? Color.White) * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, vertices, realColor);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<ushort> indices, ReadOnlySpan<DrawVertexUV2D> vertices, Color? color = null)
|
||||
{
|
||||
var realColor = (color ?? Color.White) * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, indices, vertices, realColor);
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, indices, vertices);
|
||||
}
|
||||
|
||||
public override void DrawLine(Vector2 from, Vector2 to, Color color)
|
||||
@@ -379,7 +331,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
int divisions = Math.Max(16,(int)(radius * 16));
|
||||
float arcLength = MathF.PI * 2 / divisions;
|
||||
|
||||
Span<Vector2> filledTriangle = stackalloc Vector2[3];
|
||||
var colorReal = color * Modulate;
|
||||
|
||||
if (filled)
|
||||
{
|
||||
// Unfilled (using _renderHandle.DrawLine) does the linear conversion internally.
|
||||
// Filled meanwhile uses DrawPrimitives, so has to do it here.
|
||||
colorReal = Color.FromSrgb(color);
|
||||
}
|
||||
|
||||
Span<DrawVertexUV2DColor> filledTriangle = stackalloc DrawVertexUV2DColor[3];
|
||||
|
||||
// Draws a "circle", but its just a polygon with a bunch of sides
|
||||
// this is the GL_LINES version, not GL_LINE_STRIP
|
||||
@@ -389,14 +350,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var endPos = new Vector2(MathF.Cos(arcLength * (i+1)) * radius, MathF.Sin(arcLength * (i + 1)) * radius);
|
||||
|
||||
if(!filled)
|
||||
_renderHandle.DrawLine(startPos, endPos, color);
|
||||
_renderHandle.DrawLine(startPos, endPos, colorReal);
|
||||
else
|
||||
{
|
||||
filledTriangle[0] = startPos + position;
|
||||
filledTriangle[1] = endPos + position;
|
||||
filledTriangle[2] = Vector2.Zero + position;
|
||||
filledTriangle[0] = new DrawVertexUV2DColor(startPos + position, colorReal);
|
||||
filledTriangle[1] = new DrawVertexUV2DColor(endPos + position, colorReal);
|
||||
filledTriangle[2] = new DrawVertexUV2DColor(position, colorReal);
|
||||
|
||||
_renderHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, filledTriangle, color);
|
||||
_renderHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, Texture.White, filledTriangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -477,38 +438,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
quad.TopLeft, quad.TopRight, color, in subRegion);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology,
|
||||
ReadOnlySpan<Vector2> vertices,
|
||||
Color color)
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<DrawVertexUV2DColor> vertices)
|
||||
{
|
||||
var realColor = color * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, vertices, realColor);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology,
|
||||
ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<Vector2> vertices, Color color)
|
||||
{
|
||||
var realColor = color * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, indices, vertices, realColor);
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, vertices);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<DrawVertexUV2D> vertices, Color? color = null)
|
||||
ReadOnlySpan<ushort> indices, ReadOnlySpan<DrawVertexUV2DColor> vertices)
|
||||
{
|
||||
var realColor = (color ?? Color.White) * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, vertices, realColor);
|
||||
}
|
||||
|
||||
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<ushort> indices, ReadOnlySpan<DrawVertexUV2D> vertices, Color? color = null)
|
||||
{
|
||||
var realColor = (color ?? Color.White) * Modulate;
|
||||
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, indices, vertices, realColor);
|
||||
_renderHandle.DrawPrimitives(primitiveTopology, texture, indices, vertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Reset ModUV to ensure it's identity and doesn't touch anything.
|
||||
program.SetUniformMaybe(UniIModUV, new Vector4(0, 0, 1, 1));
|
||||
|
||||
program.SetUniformMaybe(UniIModulate, command.Modulate);
|
||||
program.SetUniformMaybe(UniITexturePixelSize, Vector2.One / loadedTexture.Size);
|
||||
|
||||
var primitiveType = MapPrimitiveType(command.PrimitiveType);
|
||||
@@ -415,9 +414,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case float f:
|
||||
program.SetUniform(name, f);
|
||||
break;
|
||||
case float[] fArr:
|
||||
program.SetUniform(name, fArr);
|
||||
break;
|
||||
case Vector2 vector2:
|
||||
program.SetUniform(name, vector2);
|
||||
break;
|
||||
case Vector2[] vector2Arr:
|
||||
program.SetUniform(name, vector2Arr);
|
||||
break;
|
||||
case Vector3 vector3:
|
||||
program.SetUniform(name, vector3);
|
||||
break;
|
||||
@@ -523,13 +528,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// <param name="br">Bottom right vertex of the quad in object space.</param>
|
||||
/// <param name="tl">Top left vertex of the quad in object space.</param>
|
||||
/// <param name="tr">Top right vertex of the quad in object space.</param>
|
||||
/// <param name="modulate">A color to multiply the texture by when shading.</param>
|
||||
/// <param name="modulate">A color to multiply the texture by when shading. Non-linear.</param>
|
||||
/// <param name="texCoords">The four corners of the texture coordinates, matching the four vertices.</param>
|
||||
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);
|
||||
EnsureBatchState(texture, true, GetQuadBatchPrimitiveType(), _queuedShader);
|
||||
|
||||
bl = _currentMatrixModel.Transform(bl);
|
||||
br = _currentMatrixModel.Transform(br);
|
||||
@@ -538,10 +543,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// TODO: split batch if necessary.
|
||||
var vIdx = BatchVertexIndex;
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight);
|
||||
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight);
|
||||
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft);
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, modulate);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, modulate);
|
||||
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, modulate);
|
||||
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, modulate);
|
||||
BatchVertexIndex += 4;
|
||||
QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx);
|
||||
|
||||
@@ -549,7 +554,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle textureId,
|
||||
ReadOnlySpan<ushort> indices, ReadOnlySpan<Vertex2D> vertices, in Color color)
|
||||
ReadOnlySpan<ushort> indices, ReadOnlySpan<Vertex2D> vertices)
|
||||
{
|
||||
FinishBatch();
|
||||
_batchMetaData = null;
|
||||
@@ -579,7 +584,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
command.DrawBatch.Indexed = true;
|
||||
command.DrawBatch.StartIndex = BatchIndexIndex;
|
||||
command.DrawBatch.PrimitiveType = MapDrawToBatchPrimitiveType(primitiveTopology);
|
||||
command.DrawBatch.Modulate = color;
|
||||
command.DrawBatch.TextureId = textureId;
|
||||
command.DrawBatch.ShaderInstance = _queuedShader;
|
||||
|
||||
@@ -592,7 +596,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle textureId,
|
||||
in ReadOnlySpan<Vertex2D> vertices, in Color color)
|
||||
in ReadOnlySpan<Vertex2D> vertices)
|
||||
{
|
||||
FinishBatch();
|
||||
_batchMetaData = null;
|
||||
@@ -606,7 +610,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
command.DrawBatch.Indexed = false;
|
||||
command.DrawBatch.StartIndex = BatchVertexIndex;
|
||||
command.DrawBatch.PrimitiveType = MapDrawToBatchPrimitiveType(primitiveTopology);
|
||||
command.DrawBatch.Modulate = color;
|
||||
command.DrawBatch.TextureId = textureId;
|
||||
command.DrawBatch.ShaderInstance = _queuedShader;
|
||||
|
||||
@@ -635,15 +638,15 @@ 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);
|
||||
EnsureBatchState(_stockTextureWhite.TextureId, false, BatchPrimitiveType.LineList, _queuedShader);
|
||||
|
||||
a = _currentMatrixModel.Transform(a);
|
||||
b = _currentMatrixModel.Transform(b);
|
||||
|
||||
// TODO: split batch if necessary.
|
||||
var vIdx = BatchVertexIndex;
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(a, Vector2.Zero);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(b, Vector2.Zero);
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(a, Vector2.Zero, color);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(b, Vector2.Zero, color);
|
||||
BatchVertexIndex += 2;
|
||||
|
||||
_debugStats.LastClydeDrawCalls += 1;
|
||||
@@ -710,14 +713,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// Ensures that batching metadata matches the current batch.
|
||||
/// If not, the current batch is finished and a new one is started.
|
||||
/// </summary>
|
||||
private void EnsureBatchState(ClydeHandle textureId, in Color color, bool indexed,
|
||||
private void EnsureBatchState(ClydeHandle textureId, bool indexed,
|
||||
BatchPrimitiveType primitiveType, ClydeHandle shaderInstance)
|
||||
{
|
||||
if (_batchMetaData.HasValue)
|
||||
{
|
||||
var metaData = _batchMetaData.Value;
|
||||
if (metaData.TextureId == textureId &&
|
||||
StrictColorEquality(metaData.Color, color) &&
|
||||
indexed == metaData.Indexed &&
|
||||
metaData.PrimitiveType == primitiveType &&
|
||||
metaData.ShaderInstance == shaderInstance)
|
||||
@@ -731,7 +733,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
// ... and start another.
|
||||
_batchMetaData = new BatchMetaData(textureId, color, indexed, primitiveType,
|
||||
_batchMetaData = new BatchMetaData(textureId, indexed, primitiveType,
|
||||
indexed ? BatchIndexIndex : BatchVertexIndex, shaderInstance);
|
||||
|
||||
/*
|
||||
@@ -763,7 +765,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
command.DrawBatch.Indexed = indexed;
|
||||
command.DrawBatch.StartIndex = metaData.StartIndex;
|
||||
command.DrawBatch.PrimitiveType = metaData.PrimitiveType;
|
||||
command.DrawBatch.Modulate = metaData.Color;
|
||||
command.DrawBatch.TextureId = metaData.TextureId;
|
||||
command.DrawBatch.ShaderInstance = metaData.ShaderInstance;
|
||||
|
||||
@@ -892,7 +893,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
public ClydeHandle TextureId;
|
||||
public ClydeHandle ShaderInstance;
|
||||
public Color Modulate;
|
||||
|
||||
public int StartIndex;
|
||||
public int Count;
|
||||
@@ -964,17 +964,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private readonly struct BatchMetaData
|
||||
{
|
||||
public readonly ClydeHandle TextureId;
|
||||
public readonly Color Color;
|
||||
public readonly bool Indexed;
|
||||
public readonly BatchPrimitiveType PrimitiveType;
|
||||
public readonly int StartIndex;
|
||||
public readonly ClydeHandle ShaderInstance;
|
||||
|
||||
public BatchMetaData(ClydeHandle textureId, in Color color, bool indexed, BatchPrimitiveType primitiveType,
|
||||
public BatchMetaData(ClydeHandle textureId, bool indexed, BatchPrimitiveType primitiveType,
|
||||
int startIndex, ClydeHandle shaderInstance)
|
||||
{
|
||||
TextureId = textureId;
|
||||
Color = color;
|
||||
Indexed = indexed;
|
||||
PrimitiveType = primitiveType;
|
||||
StartIndex = startIndex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -162,6 +162,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
versionHeader += "#extension GL_OES_standard_derivatives : enable\n";
|
||||
}
|
||||
|
||||
versionHeader += "#define NO_ARRAY_PRECISION\n";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -395,12 +397,24 @@ namespace Robust.Client.Graphics.Clyde
|
||||
data.Parameters[name] = value;
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, float[] value)
|
||||
{
|
||||
var data = Parent._shaderInstances[Handle];
|
||||
data.Parameters[name] = value;
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Vector2 value)
|
||||
{
|
||||
var data = Parent._shaderInstances[Handle];
|
||||
data.Parameters[name] = value;
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Vector2[] value)
|
||||
{
|
||||
var data = Parent._shaderInstances[Handle];
|
||||
data.Parameters[name] = value;
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Vector3 value)
|
||||
{
|
||||
var data = Parent._shaderInstances[Handle];
|
||||
|
||||
@@ -95,6 +95,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private bool InitWindowing()
|
||||
{
|
||||
if (OperatingSystem.IsWindows() && _cfg.GetCVar(CVars.DisplayAngleEs3On10_0))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ANGLE_FEATURE_OVERRIDES_ENABLED", "allowES3OnFL10_0");
|
||||
}
|
||||
|
||||
var iconPath = _cfg.GetCVar(CVars.DisplayWindowIconSet);
|
||||
if (!string.IsNullOrWhiteSpace(iconPath))
|
||||
_windowIconPath = new ResourcePath(iconPath);
|
||||
|
||||
@@ -14,6 +14,8 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Timing;
|
||||
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
|
||||
|
||||
@@ -35,6 +37,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ProfManager _prof = default!;
|
||||
|
||||
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
|
||||
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
|
||||
@@ -273,10 +276,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
Span<Vertex2D> quadVertices = stackalloc[]
|
||||
{
|
||||
new Vertex2D(1, 0, 1, 1),
|
||||
new Vertex2D(0, 0, 0, 1),
|
||||
new Vertex2D(1, 1, 1, 0),
|
||||
new Vertex2D(0, 1, 0, 0)
|
||||
new Vertex2D(1, 0, 1, 1, Color.White),
|
||||
new Vertex2D(0, 0, 0, 1, Color.White),
|
||||
new Vertex2D(1, 1, 1, 0, Color.White),
|
||||
new Vertex2D(0, 1, 0, 0, Color.White)
|
||||
};
|
||||
|
||||
QuadVBO = new GLBuffer<Vertex2D>(this, BufferTarget.ArrayBuffer, BufferUsageHint.StaticDraw,
|
||||
@@ -292,10 +295,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
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),
|
||||
new Vertex2D(-1, 1, 0, 1, Color.White),
|
||||
new Vertex2D(-1, -1, 0, 0, Color.White),
|
||||
new Vertex2D(1, 1, 1, 1, Color.White),
|
||||
new Vertex2D(1, -1, 1, 0, Color.White),
|
||||
};
|
||||
|
||||
WindowVBO = new GLBuffer<Vertex2D>(
|
||||
@@ -311,17 +314,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Batch rendering
|
||||
{
|
||||
BatchVBO = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
|
||||
Vertex2D.SizeOf * BatchVertexData.Length, nameof(BatchVBO));
|
||||
sizeof(Vertex2D) * BatchVertexData.Length, nameof(BatchVBO));
|
||||
|
||||
BatchVAO = new GLHandle(GenVertexArray());
|
||||
BindVertexArray(BatchVAO.Handle);
|
||||
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, BatchVAO, nameof(BatchVAO));
|
||||
// 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);
|
||||
SetupVAOLayout();
|
||||
|
||||
CheckGlError();
|
||||
|
||||
@@ -345,18 +343,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
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);
|
||||
SetupVAOLayout();
|
||||
|
||||
return vao;
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void SetupDebugCallback()
|
||||
private unsafe void SetupDebugCallback()
|
||||
{
|
||||
if (!_hasGLKhrDebug)
|
||||
{
|
||||
@@ -367,19 +360,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.Enable(EnableCap.DebugOutput);
|
||||
GL.Enable(EnableCap.DebugOutputSynchronous);
|
||||
|
||||
GCHandle.Alloc(_debugMessageCallbackInstance);
|
||||
_debugMessageCallbackInstance ??= DebugMessageCallback;
|
||||
|
||||
// OpenTK seemed to have trouble marshalling the delegate so do it manually.
|
||||
|
||||
var procName = _isGLKhrDebugESExtension ? "glDebugMessageCallbackKHR" : "glDebugMessageCallback";
|
||||
LoadGLProc(procName, out DebugMessageCallbackDelegate proc);
|
||||
_debugMessageCallbackInstance = DebugMessageCallback;
|
||||
var glDebugMessageCallback = (delegate* unmanaged[Stdcall] <nint, nint, void>) LoadGLProc(procName);
|
||||
var funcPtr = Marshal.GetFunctionPointerForDelegate(_debugMessageCallbackInstance);
|
||||
proc(funcPtr, new IntPtr(0x3005));
|
||||
glDebugMessageCallback(funcPtr, new IntPtr(0x3005));
|
||||
}
|
||||
|
||||
private delegate void DebugMessageCallbackDelegate(IntPtr funcPtr, IntPtr userParam);
|
||||
|
||||
private void DebugMessageCallback(DebugSource source, DebugType type, int id, DebugSeverity severity,
|
||||
int length, IntPtr message, IntPtr userParam)
|
||||
{
|
||||
|
||||
@@ -412,10 +412,18 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, float[] value)
|
||||
{
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Vector2 value)
|
||||
{
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Vector2[] value)
|
||||
{
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Vector3 value)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -374,6 +374,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
};
|
||||
}
|
||||
|
||||
if (Clyde._cfg.GetCVar(CVars.DisplayAngleForce10_0))
|
||||
{
|
||||
featureLevels = stackalloc D3D_FEATURE_LEVEL[]
|
||||
{
|
||||
D3D_FEATURE_LEVEL_10_0,
|
||||
};
|
||||
}
|
||||
|
||||
fixed (ID3D11Device** device = &_device)
|
||||
fixed (D3D_FEATURE_LEVEL* fl = &featureLevels[0])
|
||||
{
|
||||
|
||||
@@ -195,11 +195,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void BlitThreadDoSecondaryWindowBlit(WindowData window)
|
||||
{
|
||||
var rt = window.RenderTexture!;
|
||||
|
||||
if (Clyde._hasGLFenceSync)
|
||||
{
|
||||
// 0xFFFFFFFFFFFFFFFFUL is GL_TIMEOUT_IGNORED
|
||||
var rt = window.Reg.RenderTarget;
|
||||
var sync = rt.LastGLSync;
|
||||
GL.WaitSync(sync, WaitSyncFlags.None, unchecked((long) 0xFFFFFFFFFFFFFFFFUL));
|
||||
Clyde.CheckGlError();
|
||||
@@ -217,11 +216,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Clyde._windowing!.WindowSwapBuffers(window.Reg);
|
||||
}
|
||||
|
||||
private void BlitThreadInit(WindowData reg)
|
||||
private unsafe void BlitThreadInit(WindowData reg)
|
||||
{
|
||||
Clyde._windowing!.GLMakeContextCurrent(reg.Reg);
|
||||
Clyde._windowing.GLSwapInterval(0);
|
||||
|
||||
Clyde.SetupDebugCallback();
|
||||
|
||||
if (!Clyde._isGLES)
|
||||
GL.Enable(EnableCap.FramebufferSrgb);
|
||||
|
||||
@@ -229,10 +230,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.BindVertexArray(vao);
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, Clyde.WindowVBO.ObjectHandle);
|
||||
// Vertex Coords
|
||||
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
|
||||
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 0);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
// Texture Coords.
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(1);
|
||||
|
||||
var program = Clyde._compileProgram(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
@@ -178,9 +178,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case UniIModUV:
|
||||
name = UniModUV;
|
||||
break;
|
||||
case UniIModulate:
|
||||
name = UniModulate;
|
||||
break;
|
||||
case UniILightTexture:
|
||||
name = UniLightTexture;
|
||||
break;
|
||||
@@ -234,6 +231,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.Uniform1(uniformId, single);
|
||||
}
|
||||
|
||||
public void SetUniform(string uniformName, float[] singles)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
GL.Uniform1(uniformId, singles.Length, singles);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
|
||||
public void SetUniform(int uniformName, float[] singles)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
GL.Uniform1(uniformId, singles.Length, singles);
|
||||
}
|
||||
|
||||
public void SetUniform(string uniformName, in Matrix3 matrix)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
@@ -375,6 +385,31 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUniform(string uniformName, Vector2[] vector)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
SetUniformDirect(uniformId, vector);
|
||||
}
|
||||
|
||||
public void SetUniform(int uniformName, Vector2[] vector)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
SetUniformDirect(uniformId, vector);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetUniformDirect(int slot, Vector2[] vectors)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (Vector2* ptr = &vectors[0])
|
||||
{
|
||||
GL.Uniform2(slot, vectors.Length, (float*)ptr);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUniformTexture(string uniformName, TextureUnit textureUnit)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
varying highp vec2 UV;
|
||||
varying highp vec2 Pos;
|
||||
varying highp vec4 VtxModulate;
|
||||
|
||||
uniform sampler2D lightMap;
|
||||
uniform highp vec4 modulate;
|
||||
|
||||
// [SHADER_HEADER_CODE]
|
||||
|
||||
@@ -16,5 +16,5 @@ void main()
|
||||
|
||||
lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;
|
||||
|
||||
gl_FragColor = zAdjustResult(COLOR * modulate * vec4(lightSample, 1.0));
|
||||
gl_FragColor = zAdjustResult(COLOR * VtxModulate * vec4(lightSample, 1.0));
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
/*layout (location = 0)*/ attribute vec2 aPos;
|
||||
// Texture coordinates.
|
||||
/*layout (location = 1)*/ attribute vec2 tCoord;
|
||||
// Colour modulation.
|
||||
/*layout (location = 2)*/ attribute vec4 modulate;
|
||||
|
||||
varying vec2 UV;
|
||||
varying vec2 Pos;
|
||||
varying vec4 VtxModulate;
|
||||
|
||||
// Maybe we should merge these CPU side.
|
||||
// idk yet.
|
||||
@@ -33,4 +36,5 @@ void main()
|
||||
gl_Position = vec4(VERTEX, 0.0, 1.0);
|
||||
Pos = (VERTEX + 1.0) / 2.0;
|
||||
UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
|
||||
VtxModulate = zFromSrgb(modulate);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
varying highp vec2 UV;
|
||||
|
||||
uniform sampler2D lightMap;
|
||||
uniform highp vec4 modulate;
|
||||
|
||||
// [SHADER_HEADER_CODE]
|
||||
|
||||
@@ -13,5 +12,7 @@ void main()
|
||||
|
||||
// [SHADER_CODE]
|
||||
|
||||
// NOTE: You may want to add modulation here. Problem: Game doesn't like that.
|
||||
// In particular, walls disappear.
|
||||
gl_FragColor = zAdjustResult(COLOR);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
/*layout (location = 0)*/ attribute vec2 aPos;
|
||||
// Texture coordinates.
|
||||
/*layout (location = 1)*/ attribute vec2 tCoord;
|
||||
// Colour modulation.
|
||||
/*layout (location = 2)*/ attribute vec4 modulate;
|
||||
|
||||
varying vec2 UV;
|
||||
|
||||
|
||||
@@ -19,6 +19,16 @@ out highp vec4 colourOutput;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef NO_ARRAY_PRECISION
|
||||
#define ARRAY_LOWP lowp
|
||||
#define ARRAY_MEDIUMP mediump
|
||||
#define ARRAY_HIGHP highp
|
||||
#else
|
||||
#define ARRAY_LOWP lowp
|
||||
#define ARRAY_MEDIUMP mediump
|
||||
#define ARRAY_HIGHP highp
|
||||
#endif
|
||||
|
||||
// -- shadow depth --
|
||||
|
||||
// If float textures are supported, puts the values in the R/G fields.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
@@ -12,6 +14,15 @@ namespace Robust.Client.Graphics
|
||||
//private protected IRenderHandle _renderHandle;
|
||||
private protected readonly int _handleId;
|
||||
public bool Disposed { get; private set; }
|
||||
/// <summary>
|
||||
/// Drawing commands that do NOT receive per-vertex modulation get modulated by this.
|
||||
/// Specifically, *DrawPrimitives w/ DrawVertexUV2DColor IS NOT AFFECTED BY THIS*.
|
||||
/// The only code that should ever be setting this is UserInterfaceManager.
|
||||
/// It's absolutely evil statefulness.
|
||||
/// I understand it's existence and operation.
|
||||
/// I understand that removing it would require rewriting all the UI controls everywhere.
|
||||
/// I still wish it a prolonged death - it's a performance nightmare. - 20kdc
|
||||
/// </summary>
|
||||
public Color Modulate { get; set; } = Color.White;
|
||||
|
||||
public void Dispose()
|
||||
@@ -37,14 +48,25 @@ namespace Robust.Client.Graphics
|
||||
|
||||
public abstract void UseShader(ShaderInstance? shader);
|
||||
|
||||
// ---- DrawPrimitives: Vector2 API ----
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary geometry primitives with a flat color.
|
||||
/// </summary>
|
||||
/// <param name="primitiveTopology">The topology of the primitives to draw.</param>
|
||||
/// <param name="vertices">The set of vertices to render.</param>
|
||||
/// <param name="color">The color to draw with.</param>
|
||||
public abstract void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan<Vector2> vertices,
|
||||
Color color);
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan<Vector2> vertices,
|
||||
Color color)
|
||||
{
|
||||
var realColor = color * Modulate;
|
||||
|
||||
// TODO: Maybe don't stackalloc if the data is too large.
|
||||
Span<DrawVertexUV2DColor> drawVertices = stackalloc DrawVertexUV2DColor[vertices.Length];
|
||||
PadVerticesV2(vertices, drawVertices, realColor);
|
||||
|
||||
DrawPrimitives(primitiveTopology, Texture.White, drawVertices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary indexed geometry primitives with a flat color.
|
||||
@@ -53,8 +75,28 @@ namespace Robust.Client.Graphics
|
||||
/// <param name="indices">The indices into <paramref name="vertices"/> to render.</param>
|
||||
/// <param name="vertices">The set of vertices to render.</param>
|
||||
/// <param name="color">The color to draw with.</param>
|
||||
public abstract void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<Vector2> vertices, Color color);
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<Vector2> vertices, Color color)
|
||||
{
|
||||
var realColor = color * Modulate;
|
||||
|
||||
// TODO: Maybe don't stackalloc if the data is too large.
|
||||
Span<DrawVertexUV2DColor> drawVertices = stackalloc DrawVertexUV2DColor[vertices.Length];
|
||||
PadVerticesV2(vertices, drawVertices, realColor);
|
||||
|
||||
DrawPrimitives(primitiveTopology, Texture.White, indices, drawVertices);
|
||||
}
|
||||
|
||||
private void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
|
||||
{
|
||||
Color colorLinear = Color.FromSrgb(color);
|
||||
for (var i = 0; i < output.Length; i++)
|
||||
{
|
||||
output[i] = new DrawVertexUV2DColor(input[i], (0.5f, 0.5f), colorLinear);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- DrawPrimitives: DrawVertexUV2D API ----
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary geometry primitives with a texture.
|
||||
@@ -63,20 +105,70 @@ namespace Robust.Client.Graphics
|
||||
/// <param name="texture">The texture to render with.</param>
|
||||
/// <param name="vertices">The set of vertices to render.</param>
|
||||
/// <param name="color">The color to draw with.</param>
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture, ReadOnlySpan<DrawVertexUV2D> vertices,
|
||||
Color? color = null)
|
||||
{
|
||||
var realColor = (color ?? Color.White) * Modulate;
|
||||
|
||||
// TODO: Maybe don't stackalloc if the data is too large.
|
||||
Span<DrawVertexUV2DColor> drawVertices = stackalloc DrawVertexUV2DColor[vertices.Length];
|
||||
PadVerticesUV(vertices, drawVertices, realColor);
|
||||
|
||||
DrawPrimitives(primitiveTopology, texture, drawVertices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary geometry primitives with a texture.
|
||||
/// </summary>
|
||||
/// <param name="primitiveTopology">The topology of the primitives to draw.</param>
|
||||
/// <param name="texture">The texture to render with.</param>
|
||||
/// <param name="vertices">The set of vertices to render.</param>
|
||||
/// <param name="indices">The indices into <paramref name="vertices"/> to render.</param>
|
||||
/// <param name="color">The color to draw with.</param>
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture, ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<DrawVertexUV2D> vertices, Color? color = null)
|
||||
{
|
||||
var realColor = (color ?? Color.White) * Modulate;
|
||||
|
||||
// TODO: Maybe don't stackalloc if the data is too large.
|
||||
Span<DrawVertexUV2DColor> drawVertices = stackalloc DrawVertexUV2DColor[vertices.Length];
|
||||
PadVerticesUV(vertices, drawVertices, realColor);
|
||||
|
||||
DrawPrimitives(primitiveTopology, texture, indices, drawVertices);
|
||||
}
|
||||
|
||||
private void PadVerticesUV(ReadOnlySpan<DrawVertexUV2D> input, Span<DrawVertexUV2DColor> output, Color color)
|
||||
{
|
||||
Color colorLinear = Color.FromSrgb(color);
|
||||
for (var i = 0; i < output.Length; i++)
|
||||
{
|
||||
output[i] = new DrawVertexUV2DColor(input[i], colorLinear);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- End wrappers ----
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary geometry primitives with a texture.
|
||||
/// Be aware that this ignores the Modulate property! Apply it yourself if necessary.
|
||||
/// </summary>
|
||||
/// <param name="primitiveTopology">The topology of the primitives to draw.</param>
|
||||
/// <param name="texture">The texture to render with.</param>
|
||||
/// <param name="vertices">The set of vertices to render.</param>
|
||||
public abstract void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<DrawVertexUV2D> vertices, Color? color = null);
|
||||
ReadOnlySpan<DrawVertexUV2DColor> vertices);
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary geometry primitives with a flat color.
|
||||
/// Be aware that this ignores the Modulate property! Apply it yourself if necessary.
|
||||
/// </summary>
|
||||
/// <param name="primitiveTopology">The topology of the primitives to draw.</param>
|
||||
/// <param name="texture">The texture to render with.</param>
|
||||
/// <param name="indices">The indices into <paramref name="vertices"/> to render.</param>
|
||||
/// <param name="vertices">The set of vertices to render.</param>
|
||||
/// <param name="color">The color to draw with.</param>
|
||||
public abstract void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
|
||||
ReadOnlySpan<ushort> indices,
|
||||
ReadOnlySpan<DrawVertexUV2D> vertices, Color? color = null);
|
||||
ReadOnlySpan<DrawVertexUV2DColor> vertices);
|
||||
|
||||
[DebuggerStepThrough]
|
||||
protected void CheckDisposed()
|
||||
@@ -108,4 +200,51 @@ namespace Robust.Client.Graphics
|
||||
UV = uv;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2D Vertex that contains position and UV coordinates, and a modulation colour (Linear!!!)
|
||||
/// NOTE: This is directly cast into Clyde Vertex2D!!!!
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DrawVertexUV2DColor
|
||||
{
|
||||
public Vector2 Position;
|
||||
public Vector2 UV;
|
||||
/// <summary>
|
||||
/// Modulation colour for this vertex.
|
||||
/// Note that this color is in linear space.
|
||||
/// </summary>
|
||||
public Color Color;
|
||||
|
||||
/// <param name="position">The location.</param>
|
||||
/// <param name="uv">The texture coordinate.</param>
|
||||
/// <param name="col">Modulation colour (In linear space, use Color.FromSrgb if needed)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DrawVertexUV2DColor(Vector2 position, Vector2 uv, Color col)
|
||||
{
|
||||
Position = position;
|
||||
UV = uv;
|
||||
Color = col;
|
||||
}
|
||||
|
||||
/// <param name="position">The location.</param>
|
||||
/// <param name="col">Modulation colour (In linear space, use Color.FromSrgb if needed)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DrawVertexUV2DColor(Vector2 position, Color col)
|
||||
{
|
||||
Position = position;
|
||||
UV = new Vector2(0.5f, 0.5f);
|
||||
Color = col;
|
||||
}
|
||||
|
||||
/// <param name="b">The existing position/UV pair.</param>
|
||||
/// <param name="col">Modulation colour (In linear space, use Color.FromSrgb if needed)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DrawVertexUV2DColor(DrawVertexUV2D b, Color col)
|
||||
{
|
||||
Position = b.Position;
|
||||
UV = b.UV;
|
||||
Color = col;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,28 +56,7 @@ namespace Robust.Client.Graphics
|
||||
/// <param name="str">The text to draw.</param>
|
||||
/// <param name="color">The color of text to draw.</param>
|
||||
public Vector2 DrawString(Font font, Vector2 pos, string str, Color color)
|
||||
{
|
||||
var advanceTotal = Vector2.Zero;
|
||||
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
|
||||
var lineHeight = font.GetLineHeight(1);
|
||||
|
||||
foreach (var rune in str.EnumerateRunes())
|
||||
{
|
||||
if (rune == new Rune('\n'))
|
||||
{
|
||||
baseLine.X = pos.X;
|
||||
baseLine.Y += lineHeight;
|
||||
advanceTotal.Y += lineHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
var advance = font.DrawChar(this, rune, baseLine, 1, color);
|
||||
advanceTotal.X += advance;
|
||||
baseLine += new Vector2(advance, 0);
|
||||
}
|
||||
|
||||
return advanceTotal;
|
||||
}
|
||||
=> DrawString(font, pos, str, 1, color);
|
||||
|
||||
/// <summary>
|
||||
/// Draw a simple string to the screen at the specified position.
|
||||
@@ -94,6 +73,30 @@ namespace Robust.Client.Graphics
|
||||
public Vector2 DrawString(Font font, Vector2 pos, string str)
|
||||
=> DrawString(font, pos, str, Color.White);
|
||||
|
||||
public Vector2 DrawString(Font font, Vector2 pos, ReadOnlySpan<char> str, float scale, Color color)
|
||||
{
|
||||
var advanceTotal = Vector2.Zero;
|
||||
var baseLine = new Vector2(pos.X, font.GetAscent(scale) + pos.Y);
|
||||
var lineHeight = font.GetLineHeight(scale);
|
||||
|
||||
foreach (var rune in str.EnumerateRunes())
|
||||
{
|
||||
if (rune == new Rune('\n'))
|
||||
{
|
||||
baseLine.X = pos.X;
|
||||
baseLine.Y += lineHeight;
|
||||
advanceTotal.Y += lineHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
var advance = font.DrawChar(this, rune, baseLine, scale, color);
|
||||
advanceTotal.X += advance;
|
||||
baseLine += new Vector2(advance, 0);
|
||||
}
|
||||
|
||||
return advanceTotal;
|
||||
}
|
||||
|
||||
public abstract void DrawEntity(EntityUid entity, Vector2 position, Vector2 scale, Direction? overrideDirection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Robust.Client.Graphics
|
||||
handle = renderHandle.DrawingHandleWorld;
|
||||
}
|
||||
|
||||
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, worldBox, worldBounds);
|
||||
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, vp.Eye!.Position.MapId, worldBox, worldBounds);
|
||||
|
||||
Draw(args);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
@@ -38,6 +39,11 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
public readonly UIBox2i ViewportBounds;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="MapId"/> of the viewport's eye.
|
||||
/// </summary>
|
||||
public readonly MapId MapId;
|
||||
|
||||
/// <summary>
|
||||
/// AABB enclosing the area visible in the viewport.
|
||||
/// </summary>
|
||||
@@ -57,6 +63,7 @@ namespace Robust.Client.Graphics
|
||||
IClydeViewport viewport,
|
||||
DrawingHandleBase drawingHandle,
|
||||
in UIBox2i viewportBounds,
|
||||
in MapId mapId,
|
||||
in Box2 worldAabb,
|
||||
in Box2Rotated worldBounds)
|
||||
{
|
||||
@@ -65,6 +72,7 @@ namespace Robust.Client.Graphics
|
||||
Viewport = viewport;
|
||||
DrawingHandle = drawingHandle;
|
||||
ViewportBounds = viewportBounds;
|
||||
MapId = mapId;
|
||||
WorldAABB = worldAabb;
|
||||
WorldBounds = worldBounds;
|
||||
}
|
||||
|
||||
@@ -135,36 +135,42 @@ namespace Robust.Client.Graphics
|
||||
|
||||
internal sealed class ShaderDataTypeFull
|
||||
{
|
||||
public ShaderDataTypeFull(ShaderDataType type, ShaderPrecisionQualifier prec)
|
||||
public ShaderDataTypeFull(ShaderDataType type, ShaderPrecisionQualifier prec, int? count = null)
|
||||
{
|
||||
Type = type;
|
||||
Precision = prec;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public ShaderDataType Type { get; }
|
||||
public ShaderPrecisionQualifier Precision { get; }
|
||||
|
||||
public int? Count;
|
||||
|
||||
public bool IsArray => Count != null;
|
||||
|
||||
public string GetNativeType()
|
||||
{
|
||||
if (Precision == ShaderPrecisionQualifier.Low)
|
||||
string? precision = Precision switch
|
||||
{
|
||||
return "lowp " + Type.GetNativeType();
|
||||
}
|
||||
else if (Precision == ShaderPrecisionQualifier.Medium)
|
||||
{
|
||||
return "mediump " + Type.GetNativeType();
|
||||
}
|
||||
else if (Precision == ShaderPrecisionQualifier.High)
|
||||
{
|
||||
return "highp " + Type.GetNativeType();
|
||||
}
|
||||
return Type.GetNativeType();
|
||||
ShaderPrecisionQualifier.Low => "ARRAY_LOWP ",
|
||||
ShaderPrecisionQualifier.Medium => "ARRAY_MEDIUMP ",
|
||||
ShaderPrecisionQualifier.High => "ARRAY_HIGHP ",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return IsArray ? $"{precision}{Type.GetNativeType()}[{Count}]" : $"{precision}{Type.GetNativeType()}";
|
||||
}
|
||||
|
||||
public bool TypePrecisionConsistent()
|
||||
{
|
||||
return Type.TypeHasPrecision() == (Precision != ShaderPrecisionQualifier.None);
|
||||
}
|
||||
|
||||
public bool TypeCountConsistent()
|
||||
{
|
||||
return Count == null || Type.TypeSupportsArrays();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ShaderEnumExt
|
||||
@@ -195,6 +201,7 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
return
|
||||
(type == ShaderDataType.Float) ||
|
||||
(type == ShaderDataType.Int) ||
|
||||
(type == ShaderDataType.Vec2) ||
|
||||
(type == ShaderDataType.Vec3) ||
|
||||
(type == ShaderDataType.Vec4) ||
|
||||
@@ -203,6 +210,14 @@ namespace Robust.Client.Graphics
|
||||
(type == ShaderDataType.Mat4);
|
||||
}
|
||||
|
||||
public static bool TypeSupportsArrays(this ShaderDataType type)
|
||||
{
|
||||
// TODO: add support for int, and vec3/4 arrays
|
||||
return
|
||||
(type == ShaderDataType.Float) ||
|
||||
(type == ShaderDataType.Vec2);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||
private static readonly Dictionary<ShaderDataType, string> _nativeTypes = new()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
@@ -119,6 +119,13 @@ namespace Robust.Client.Graphics
|
||||
SetParameterImpl(name, value);
|
||||
}
|
||||
|
||||
public void SetParameter(string name, float[] value)
|
||||
{
|
||||
EnsureAlive();
|
||||
EnsureMutable();
|
||||
SetParameterImpl(name, value);
|
||||
}
|
||||
|
||||
public void SetParameter(string name, Vector2 value)
|
||||
{
|
||||
EnsureAlive();
|
||||
@@ -126,6 +133,13 @@ namespace Robust.Client.Graphics
|
||||
SetParameterImpl(name, value);
|
||||
}
|
||||
|
||||
public void SetParameter(string name, Vector2[] value)
|
||||
{
|
||||
EnsureAlive();
|
||||
EnsureMutable();
|
||||
SetParameterImpl(name, value);
|
||||
}
|
||||
|
||||
public void SetParameter(string name, Vector3 value)
|
||||
{
|
||||
EnsureAlive();
|
||||
@@ -244,7 +258,9 @@ namespace Robust.Client.Graphics
|
||||
private protected abstract ShaderInstance DuplicateImpl();
|
||||
|
||||
private protected abstract void SetParameterImpl(string name, float value);
|
||||
private protected abstract void SetParameterImpl(string name, float[] value);
|
||||
private protected abstract void SetParameterImpl(string name, Vector2 value);
|
||||
private protected abstract void SetParameterImpl(string name, Vector2[] value);
|
||||
private protected abstract void SetParameterImpl(string name, Vector3 value);
|
||||
private protected abstract void SetParameterImpl(string name, Vector4 value);
|
||||
private protected abstract void SetParameterImpl(string name, Color value);
|
||||
|
||||
@@ -504,17 +504,33 @@ namespace Robust.Client.Graphics
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_shaderTypeMap.TryGetValue(wordType.Word, out var ret))
|
||||
if (!_shaderTypeMap.TryGetValue(wordType.Word, out var ret))
|
||||
throw new ShaderParseException("Expected type", wordType.Position);
|
||||
|
||||
var result = new ShaderDataTypeFull(ret, precision);
|
||||
if (!result.TypePrecisionConsistent())
|
||||
{
|
||||
var result = new ShaderDataTypeFull(ret, precision);
|
||||
if (!result.TypePrecisionConsistent())
|
||||
{
|
||||
throw new ShaderParseException($"Type {ret} cannot accept precision {precision}", wordType.Position);
|
||||
}
|
||||
return result;
|
||||
throw new ShaderParseException($"Type {ret} cannot accept precision {precision}", wordType.Position);
|
||||
}
|
||||
|
||||
throw new ShaderParseException("Expected type or precision", wordType.Position);
|
||||
// is this type meant to be an array?
|
||||
if (_peekToken() is TokenSymbol bracketOpen && bracketOpen.Symbol == Symbols.BracketOpen)
|
||||
{
|
||||
_takeToken();
|
||||
if (_takeToken() is not TokenNumber number || !int.TryParse(number.Number, out var count))
|
||||
throw new ShaderParseException($"Failed to parse array length", bracketOpen.Position);
|
||||
|
||||
if (_takeToken() is not TokenSymbol bracketClose || bracketClose.Symbol != Symbols.BracketClosed)
|
||||
throw new ShaderParseException($"Array length definition missing closing bracket", number.Position);
|
||||
|
||||
result.Count = count;
|
||||
|
||||
// are arrays supported by this type?
|
||||
if (!result.TypeCountConsistent())
|
||||
throw new ShaderParseException($"Type {ret} does not support arrays", wordType.Position);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Robust.Client.Graphics
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
[IdDataFieldAttribute]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
private ShaderKind Kind;
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
namespace Robust.Client
|
||||
{
|
||||
public interface IGameController
|
||||
{
|
||||
InitialLaunchState LaunchState { get; }
|
||||
namespace Robust.Client;
|
||||
|
||||
public interface IGameController
|
||||
{
|
||||
InitialLaunchState LaunchState { get; }
|
||||
|
||||
void Shutdown(string? reason=null);
|
||||
|
||||
/// <summary>
|
||||
/// Try to cause the launcher to either reconnect to the same server or connect to a new server.
|
||||
/// *The engine will shutdown on success.*
|
||||
/// Will throw an exception if contacting the launcher failed (success indicates it is now the launcher's responsibility).
|
||||
/// To redial the same server, retrieve the server's address from `LaunchState.Ss14Address`.
|
||||
/// </summary>
|
||||
/// <param name="address">The server address, such as "ss14://localhost:1212/".</param>
|
||||
/// <param name="text">Informational text on the cause of the reconnect. Empty or null gives a default reason.</param>
|
||||
void Redial(string address, string? text = null);
|
||||
}
|
||||
|
||||
void Shutdown(string? reason=null);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ namespace Robust.Client.Input
|
||||
common.AddFunction(EngineKeyFunctions.TextScrollToBottom);
|
||||
common.AddFunction(EngineKeyFunctions.TextDelete);
|
||||
common.AddFunction(EngineKeyFunctions.TextTabComplete);
|
||||
common.AddFunction(EngineKeyFunctions.TextCompleteNext);
|
||||
common.AddFunction(EngineKeyFunctions.TextCompletePrev);
|
||||
|
||||
var editor = contexts.New("editor", common);
|
||||
editor.AddFunction(EngineKeyFunctions.EditorLinePlace);
|
||||
|
||||
@@ -497,7 +497,7 @@ namespace Robust.Client.Input
|
||||
|
||||
if (robustMapping.TryGet("binds", out var BaseKeyRegsNode))
|
||||
{
|
||||
var baseKeyRegs = serializationManager.ReadValueOrThrow<KeyBindingRegistration[]>(BaseKeyRegsNode);
|
||||
var baseKeyRegs = serializationManager.Read<KeyBindingRegistration[]>(BaseKeyRegsNode);
|
||||
|
||||
foreach (var reg in baseKeyRegs)
|
||||
{
|
||||
@@ -526,7 +526,7 @@ namespace Robust.Client.Input
|
||||
|
||||
if (userData && robustMapping.TryGet("leaveEmpty", out var node))
|
||||
{
|
||||
var leaveEmpty = serializationManager.ReadValueOrThrow<BoundKeyFunction[]>(node);
|
||||
var leaveEmpty = serializationManager.Read<BoundKeyFunction[]>(node);
|
||||
|
||||
if (leaveEmpty.Length > 0)
|
||||
{
|
||||
|
||||
137
Robust.Client/Physics/GridFixtureSystem.cs
Normal file
137
Robust.Client/Physics/GridFixtureSystem.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Physics
|
||||
{
|
||||
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
|
||||
{
|
||||
public bool EnableDebug
|
||||
{
|
||||
get => _enableDebug;
|
||||
set
|
||||
{
|
||||
if (_enableDebug == value) return;
|
||||
|
||||
Sawmill.Info($"Set grid fixture debug to {value}");
|
||||
_enableDebug = value;
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if (_enableDebug)
|
||||
{
|
||||
var overlay = new GridSplitNodeOverlay(EntityManager, IoCManager.Resolve<IMapManager>(), this);
|
||||
overlayManager.AddOverlay(overlay);
|
||||
RaiseNetworkEvent(new RequestGridNodesMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
overlayManager.RemoveOverlay<GridSplitNodeOverlay>();
|
||||
RaiseNetworkEvent(new StopGridNodesMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enableDebug = false;
|
||||
private readonly Dictionary<EntityUid, Dictionary<Vector2i, List<List<Vector2i>>>> _nodes = new();
|
||||
private readonly Dictionary<EntityUid, List<(Vector2, Vector2)>> _connections = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<ChunkSplitDebugMessage>(OnDebugMessage);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_nodes.Clear();
|
||||
_connections.Clear();
|
||||
}
|
||||
|
||||
private void OnDebugMessage(ChunkSplitDebugMessage ev)
|
||||
{
|
||||
Sawmill.Info($"Received grid fixture debug data");
|
||||
if (!_enableDebug) return;
|
||||
|
||||
_nodes[ev.Grid] = ev.Nodes;
|
||||
_connections[ev.Grid] = ev.Connections;
|
||||
}
|
||||
|
||||
private sealed class GridSplitNodeOverlay : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private IEntityManager _entManager;
|
||||
private IMapManager _mapManager;
|
||||
private GridFixtureSystem _system;
|
||||
|
||||
public GridSplitNodeOverlay(IEntityManager entManager, IMapManager mapManager, GridFixtureSystem system)
|
||||
{
|
||||
_entManager = entManager;
|
||||
_mapManager = mapManager;
|
||||
_system = system;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var iGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
// May not have received nodes yet.
|
||||
if (!_system._nodes.TryGetValue(iGrid.GridEntityId, out var nodes)) continue;
|
||||
|
||||
var gridXform = xformQuery.GetComponent(iGrid.GridEntityId);
|
||||
worldHandle.SetTransform(gridXform.WorldMatrix);
|
||||
var grid = (MapGrid)iGrid;
|
||||
var chunkEnumerator = grid.GetMapChunks(args.WorldBounds);
|
||||
|
||||
while (chunkEnumerator.MoveNext(out var chunk))
|
||||
{
|
||||
if (!nodes.TryGetValue(chunk.Indices, out var chunkNodes)) continue;
|
||||
|
||||
for (var i = 0; i < chunkNodes.Count; i++)
|
||||
{
|
||||
var group = chunkNodes[i];
|
||||
var offset = chunk.Indices * chunk.ChunkSize;
|
||||
var color = GetColor(chunk, i);
|
||||
|
||||
foreach (var index in group)
|
||||
{
|
||||
worldHandle.DrawRect(new Box2(offset + index, offset + index + 1).Enlarged(-0.1f), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var connections = _system._connections[iGrid.GridEntityId];
|
||||
|
||||
foreach (var (start, end) in connections)
|
||||
{
|
||||
worldHandle.DrawLine(start, end, Color.Aquamarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetColor(MapChunk chunk, int index)
|
||||
{
|
||||
// Just want something that doesn't give similar indices at 0,0 but is also deterministic.
|
||||
// Add an offset to yIndex so we at least have some colour that isn't grey at 0,0
|
||||
var actualIndex = chunk.Indices.X * 20 + (chunk.Indices.Y + 20) * 35 + index * 50;
|
||||
|
||||
var red = (byte) (actualIndex % 255);
|
||||
var green = (byte) (actualIndex * 20 % 255);
|
||||
var blue = (byte) (actualIndex * 30 % 255);
|
||||
|
||||
return new Color(red, green, blue, 85);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Robust.Client/Physics/GridSplitVisualsCommand.cs
Normal file
18
Robust.Client/Physics/GridSplitVisualsCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Physics;
|
||||
|
||||
public sealed class GridSplitVisualCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => SharedGridFixtureSystem.ShowGridNodesCommand;
|
||||
public string Description => "Shows the nodes for grid split purposes";
|
||||
public string Help => $"{Command}";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GridFixtureSystem>();
|
||||
system.EnableDebug ^= true;
|
||||
shell.WriteLine($"Toggled gridsplit node visuals to {system.EnableDebug}");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user