mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
361 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15eded3da8 | ||
|
|
b4165e8661 | ||
|
|
ad339b5bfd | ||
|
|
e1197af8ce | ||
|
|
102cadf3a6 | ||
|
|
e7723b61bc | ||
|
|
a9d17337a3 | ||
|
|
74622bac83 | ||
|
|
a3047b1687 | ||
|
|
3a55118143 | ||
|
|
3c5fbc648a | ||
|
|
f9c39bce0b | ||
|
|
0e8c803c0f | ||
|
|
6bb7b88c69 | ||
|
|
9e0fc7017c | ||
|
|
76317b7ab3 | ||
|
|
d5f4d4bf2f | ||
|
|
156d1a6b14 | ||
|
|
3fa456fd44 | ||
|
|
bf9bb46154 | ||
|
|
fb3da0b53c | ||
|
|
5c635c09b4 | ||
|
|
fad539212d | ||
|
|
7275302639 | ||
|
|
5057ff97a3 | ||
|
|
754d5a1fbb | ||
|
|
7cee5b67a7 | ||
|
|
21729e7e48 | ||
|
|
394d1e6cc2 | ||
|
|
f73d7f7285 | ||
|
|
e505cfffd8 | ||
|
|
d0fe3591ef | ||
|
|
1868f32457 | ||
|
|
ca82767b07 | ||
|
|
76024330a7 | ||
|
|
21e8107eb1 | ||
|
|
bcaa97a79b | ||
|
|
19727f6a25 | ||
|
|
2102b96323 | ||
|
|
59b3ffda4f | ||
|
|
aae929966c | ||
|
|
0cf842cacc | ||
|
|
96885c5b53 | ||
|
|
d45ce7742e | ||
|
|
50e27fd204 | ||
|
|
af98933173 | ||
|
|
e357dada65 | ||
|
|
19c48862e2 | ||
|
|
448ce94b35 | ||
|
|
3681b7f0d5 | ||
|
|
7b3c883653 | ||
|
|
c81004ddb4 | ||
|
|
f844011348 | ||
|
|
0094040d68 | ||
|
|
dfb5369664 | ||
|
|
2462c906b3 | ||
|
|
8cbc05840f | ||
|
|
510846321d | ||
|
|
ac60567583 | ||
|
|
7592997f4e | ||
|
|
c68b3dccb7 | ||
|
|
da2a2ce4ff | ||
|
|
538418ea93 | ||
|
|
2bf284bce8 | ||
|
|
b51cb06d53 | ||
|
|
bab6c29fbe | ||
|
|
9502c86a65 | ||
|
|
359811f71e | ||
|
|
9de5840017 | ||
|
|
45af00096f | ||
|
|
2f0283edb7 | ||
|
|
d142393221 | ||
|
|
ac147dc2a1 | ||
|
|
16af5cff9c | ||
|
|
7725dbff78 | ||
|
|
f2f8824678 | ||
|
|
f2e9ed0b73 | ||
|
|
9e6d0aa44a | ||
|
|
1758fced75 | ||
|
|
31e2a3efe4 | ||
|
|
371db7db2f | ||
|
|
bb23bc6acc | ||
|
|
270326e188 | ||
|
|
7d27835543 | ||
|
|
cfb88b2e8e | ||
|
|
8a83787d58 | ||
|
|
6dd9f9e0f5 | ||
|
|
7f3445b1c6 | ||
|
|
5c15f26f09 | ||
|
|
b8fbe5e465 | ||
|
|
32049e34f2 | ||
|
|
26b09283f3 | ||
|
|
6c6360e50a | ||
|
|
9877323195 | ||
|
|
b6f52f4c27 | ||
|
|
c1789cbbaf | ||
|
|
f44f5b5a98 | ||
|
|
f4faa1ad3d | ||
|
|
97d03c6954 | ||
|
|
8accbc700a | ||
|
|
9f013534b3 | ||
|
|
0b8febf6a6 | ||
|
|
188985ecc2 | ||
|
|
42434d1f49 | ||
|
|
87492cb0c3 | ||
|
|
517ae7f1bd | ||
|
|
73357f022b | ||
|
|
012faa0a16 | ||
|
|
7a8abb3db7 | ||
|
|
d6ce7e950b | ||
|
|
e39b249070 | ||
|
|
5b889936be | ||
|
|
712809195d | ||
|
|
64b5d6e323 | ||
|
|
baa607532d | ||
|
|
ff064dd859 | ||
|
|
8305bffcac | ||
|
|
17a8972052 | ||
|
|
56e03eae3e | ||
|
|
1f948e17c4 | ||
|
|
efaed42b24 | ||
|
|
be2e31ff9d | ||
|
|
a891cacae5 | ||
|
|
e9e0117402 | ||
|
|
24114d87e6 | ||
|
|
7cd78f3f4e | ||
|
|
ca64aae7f0 | ||
|
|
be0fb4250c | ||
|
|
eba58cb893 | ||
|
|
ced6d5a8b0 | ||
|
|
495671576e | ||
|
|
0611674915 | ||
|
|
e6ab61fe42 | ||
|
|
5232e61d38 | ||
|
|
a3e90aa04e | ||
|
|
7a719d9f61 | ||
|
|
61385b7c21 | ||
|
|
2503f9c4e0 | ||
|
|
eb092e90ef | ||
|
|
eb556c8728 | ||
|
|
2e398ded08 | ||
|
|
75cf15caa2 | ||
|
|
8d7d6a26ba | ||
|
|
d593ffbb47 | ||
|
|
15b377dbd6 | ||
|
|
d212479689 | ||
|
|
2d3379d7f4 | ||
|
|
7b171b2212 | ||
|
|
7e8a5e199f | ||
|
|
ae74a5d7d4 | ||
|
|
9446ab76f9 | ||
|
|
bb5cb10d57 | ||
|
|
d6ccaee6d4 | ||
|
|
ce8982e371 | ||
|
|
371cd7ddf1 | ||
|
|
6573531d5d | ||
|
|
b1e1f27aa3 | ||
|
|
80ce583454 | ||
|
|
2b4a428f9f | ||
|
|
41ee330828 | ||
|
|
f82452c855 | ||
|
|
785e2f84f6 | ||
|
|
2bdc1d77ca | ||
|
|
51205beb56 | ||
|
|
ef325b4780 | ||
|
|
ede337a869 | ||
|
|
d416344aef | ||
|
|
fb98eb1a0c | ||
|
|
ed9a0b4812 | ||
|
|
e7c6151310 | ||
|
|
005c2e784a | ||
|
|
1009dd3ea0 | ||
|
|
87b82160b0 | ||
|
|
fc318c9ecd | ||
|
|
ac957ca7fc | ||
|
|
1bdd82b0bf | ||
|
|
0150c5e6ff | ||
|
|
b2cc90d00f | ||
|
|
1f8b89e92f | ||
|
|
33d394295e | ||
|
|
4934a9c5a5 | ||
|
|
91ebc3eb02 | ||
|
|
bc84590a33 | ||
|
|
e3944dc6fb | ||
|
|
6a77f4c27b | ||
|
|
6246ae412e | ||
|
|
ac86accc20 | ||
|
|
19ff7f25ca | ||
|
|
3f83733a03 | ||
|
|
438fed2f0e | ||
|
|
816a535a92 | ||
|
|
01df42aa8f | ||
|
|
a200d73ef9 | ||
|
|
8d30735ffb | ||
|
|
2686150f9d | ||
|
|
d720e9393b | ||
|
|
f5b1c26bec | ||
|
|
3204002c72 | ||
|
|
4c79d0c6d0 | ||
|
|
e0d38fb8bd | ||
|
|
250f6ca7db | ||
|
|
773365c185 | ||
|
|
9a1e6af586 | ||
|
|
dc96318379 | ||
|
|
31a3f145de | ||
|
|
331e1fcc81 | ||
|
|
dc7a51e582 | ||
|
|
bfe8e687da | ||
|
|
04b6d60d76 | ||
|
|
5bc5bfd58a | ||
|
|
56899b4e64 | ||
|
|
a23915e0dd | ||
|
|
726d91c5e8 | ||
|
|
d8a8783680 | ||
|
|
8839dd9a3b | ||
|
|
0296d9635c | ||
|
|
f24d9751d4 | ||
|
|
58ac82ae55 | ||
|
|
b9130bf236 | ||
|
|
a2cd33afe5 | ||
|
|
1772651049 | ||
|
|
826fa4d131 | ||
|
|
0b712ae86c | ||
|
|
22528fc484 | ||
|
|
20a411e6ce | ||
|
|
6f9ed8a242 | ||
|
|
790f4c1309 | ||
|
|
0b62cb6445 | ||
|
|
9d0f4d8a08 | ||
|
|
5069b0ccf9 | ||
|
|
12cfdb2175 | ||
|
|
b5e079815d | ||
|
|
ca3a3279c5 | ||
|
|
003752a161 | ||
|
|
55e51cba9c | ||
|
|
2cd829f4f6 | ||
|
|
43138669ec | ||
|
|
3fe30bc00f | ||
|
|
3ccbdeac6a | ||
|
|
9eb9c91da6 | ||
|
|
28d2b47a2c | ||
|
|
049ffa05e4 | ||
|
|
2f36a0a5fc | ||
|
|
41d03db59d | ||
|
|
68df887a65 | ||
|
|
e0bbcd7b08 | ||
|
|
525815427e | ||
|
|
70224ac100 | ||
|
|
dabb090dc2 | ||
|
|
ace8334a3e | ||
|
|
d8e70b4d52 | ||
|
|
2fca0e03ee | ||
|
|
b6980964b6 | ||
|
|
34d02256fd | ||
|
|
34637fb430 | ||
|
|
d905ef2a50 | ||
|
|
c3f7ef1b5c | ||
|
|
ced2a5c6cd | ||
|
|
9ec927543f | ||
|
|
215fc8c229 | ||
|
|
f82ff9e581 | ||
|
|
906f4598a2 | ||
|
|
0eb3c37bd8 | ||
|
|
9cc7cf80ba | ||
|
|
7ba02b5ca6 | ||
|
|
3ca7121f5b | ||
|
|
d3b31c1d58 | ||
|
|
92b5bb4660 | ||
|
|
33caf9c1ba | ||
|
|
58da8a6001 | ||
|
|
962f5dc650 | ||
|
|
c324562513 | ||
|
|
f5a2a710f0 | ||
|
|
357283e2bc | ||
|
|
5991bfa106 | ||
|
|
4160b120e0 | ||
|
|
98a1fa1fba | ||
|
|
fb08451849 | ||
|
|
ebea0d7572 | ||
|
|
eb6f28cce0 | ||
|
|
a1d02d7c55 | ||
|
|
777ab85cff | ||
|
|
d33a8465b0 | ||
|
|
6572fdb404 | ||
|
|
6273b1b80d | ||
|
|
a09a60efe9 | ||
|
|
d3339964ee | ||
|
|
3ffef625ec | ||
|
|
4fd9b2bc3b | ||
|
|
adc5051841 | ||
|
|
2733435218 | ||
|
|
24b0165ec9 | ||
|
|
7b9aa09b18 | ||
|
|
7bee6f6fc1 | ||
|
|
ff75495894 | ||
|
|
4cb51af733 | ||
|
|
89c1e90646 | ||
|
|
b6cadfedd5 | ||
|
|
9f57b705d7 | ||
|
|
68be9712ad | ||
|
|
aaa446254c | ||
|
|
5e2d2ab317 | ||
|
|
20ae63fbbd | ||
|
|
a92c0cbef4 | ||
|
|
95649a2dd0 | ||
|
|
861807f8b4 | ||
|
|
bd73f1c05a | ||
|
|
7dce51e2cf | ||
|
|
d9b0f3a227 | ||
|
|
05766a2eaa | ||
|
|
a761fbc09e | ||
|
|
f69440b3f2 | ||
|
|
b459d2ce21 | ||
|
|
202182e3d4 | ||
|
|
96cb52e5d2 | ||
|
|
82e0c0baeb | ||
|
|
54d6552164 | ||
|
|
c21b6c993c | ||
|
|
2fe4a8b859 | ||
|
|
8325966dbb | ||
|
|
2459a9d688 | ||
|
|
2cd2d1edd6 | ||
|
|
b982350851 | ||
|
|
4a50bc2154 | ||
|
|
4c85e205b9 | ||
|
|
0b447d9d82 | ||
|
|
ceb205ad52 | ||
|
|
a48ff3dbf1 | ||
|
|
2b85fa88c1 | ||
|
|
19564a421b | ||
|
|
b3f0e467ee | ||
|
|
216292c849 | ||
|
|
68753d15e0 | ||
|
|
2a357051ae | ||
|
|
58e0b62145 | ||
|
|
14cc273997 | ||
|
|
93f4428635 | ||
|
|
164bf68aca | ||
|
|
773b87672b | ||
|
|
eecf834039 | ||
|
|
325fe46aa3 | ||
|
|
2f6c29ab43 | ||
|
|
aab1a2dba9 | ||
|
|
f36fbd9c83 | ||
|
|
126c863f45 | ||
|
|
618a8491bf | ||
|
|
2743b64a2b | ||
|
|
28cc91934c | ||
|
|
eadfcd4c09 | ||
|
|
7871b0010e | ||
|
|
3da04ed17e | ||
|
|
170d192791 | ||
|
|
dcd9939554 | ||
|
|
98ef58eca6 | ||
|
|
ab1e99a0df | ||
|
|
499c236798 | ||
|
|
8dc2345ceb | ||
|
|
9b04270178 | ||
|
|
d75dbc901f | ||
|
|
19a3e82848 | ||
|
|
911abf2693 |
@@ -10,5 +10,8 @@ charset = utf-8
|
||||
[*.{csproj,xml,yml,dll.config,targets,props}]
|
||||
indent_size = 2
|
||||
|
||||
[nuget.config]
|
||||
indent_size = 2
|
||||
|
||||
[*.gdsl]
|
||||
indent_style = tab
|
||||
|
||||
2
.github/workflows/build-docfx.yml
vendored
2
.github/workflows/build-docfx.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
2
.github/workflows/publish-client.yml
vendored
2
.github/workflows/publish-client.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Package client
|
||||
run: Tools/package_client_build.py -p windows mac linux
|
||||
|
||||
2
.github/workflows/test-content.yml
vendored
2
.github/workflows/test-content.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 8.0.x
|
||||
- name: Disable submodule autoupdate
|
||||
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
|
||||
|
||||
|
||||
61
Directory.Packages.props
Normal file
61
Directory.Packages.props
Normal file
@@ -0,0 +1,61 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageVersion Include="ILReader.Core" Version="1.0.0.4" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.0" />
|
||||
<PackageVersion Include="Linguini.Bundle" Version="0.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1"/>
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1"/>
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeCoverage" Version="17.8.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.DotNet.RemoteExecutor" Version="8.0.0-beta.24059.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.ILVerification" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.70" />
|
||||
<PackageVersion Include="NUnit" Version="4.0.1" />
|
||||
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageVersion Include="Nett" Version="0.15.0" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
|
||||
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
|
||||
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
|
||||
<PackageVersion Include="Pidgin" Version="3.2.2" />
|
||||
<PackageVersion Include="Robust.Natives" Version="0.1.1" />
|
||||
<PackageVersion Include="Robust.Natives.Cef" Version="120.1.9" />
|
||||
<PackageVersion Include="Robust.Shared.AuthLib" Version="0.1.2" />
|
||||
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7" />
|
||||
<PackageVersion Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.7" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.2" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.5" />
|
||||
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
|
||||
<PackageVersion Include="VorbisPizza" Version="1.3.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// So I wanted to mess with NetIncomingMessage and NetOutgoingMessage from tests.
|
||||
// Now.. the instructors are internal...
|
||||
// Unless...
|
||||
// I mean we have this project here from the weird way we're compiling Lidgren.
|
||||
// I could just put this in here... it wouldn't touch the main Lidgren repo at all...
|
||||
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
|
||||
|
||||
Submodule Lidgren.Network/Lidgren.Network updated: f19cea8010...45f89ca263
@@ -10,9 +10,14 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="CursedHorrorsBeyondOurWildestImagination.cs" />
|
||||
|
||||
<Compile Include="Lidgren.Network\Lidgren.Network\**\*.cs">
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Compile>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
<!-- Engine-specific properties. Content should not use this file. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<LangVersion>11</LangVersion>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<!-- Import this at the end of any project files in Robust and Content. -->
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="Robust.Custom.targets" Condition="Exists('Robust.Custom.targets')"/>
|
||||
@@ -27,5 +28,5 @@
|
||||
<Import Project="Robust.Analyzers.targets" Condition="'$(SkipRobustAnalyzer)' != 'true'" />
|
||||
|
||||
<!-- serialization generator -->
|
||||
<Import Project="Robust.Serialization.Generator.targets" />
|
||||
<Import Project="Robust.Serialization.Generator.targets" Condition="'$(SkipRobustAnalyzer)' != 'true'" />
|
||||
</Project>
|
||||
|
||||
Submodule NetSerializer updated: 7224829e87...7f51deaeca
@@ -61,18 +61,5 @@ namespace OpenToolkit.GraphicsLibraryFramework
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GLFWException"/> class with the specified context
|
||||
/// and the serialization information.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="SerializationInfo"/> associated with this exception.</param>
|
||||
/// <param name="context">
|
||||
/// A <see cref="StreamingContext"/> that represents the context of this exception.
|
||||
/// </param>
|
||||
protected GLFWException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
987
RELEASE-NOTES.md
987
RELEASE-NOTES.md
@@ -54,6 +54,993 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 210.0.2
|
||||
|
||||
|
||||
## 210.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Revert changes to `TextureButton` that broke style property handling.
|
||||
|
||||
|
||||
## 210.0.0
|
||||
|
||||
### New features
|
||||
|
||||
* Controls can now hook before, after, and during rendering of their children.
|
||||
* IRenderHandle is now a public API, with the caveat that it's properties and methods are unstable.
|
||||
* ButtonGroup now exposes what buttons it contains, alongside which is currently pressed.
|
||||
* OptionButton has additional styleclasses, and has a hook for modifying it's internal buttons.
|
||||
* PanelContainer.GetStyleBox() is now protected rather than private.
|
||||
* TextureButton now uses a TextureRect instead of custom drawing code.
|
||||
* TextureRect has additional style properties exposed.
|
||||
* A new property, TextureSizeTarget, was added, which allows specifying a size in virtual pixels that the control should attempt to draw at.
|
||||
* Stretch mode is now a style property.
|
||||
* Scale is now a style property.
|
||||
* Avalonia.Metadata.XmlnsDefinitionAttribute is now permitted by the sandbox.
|
||||
* Add MaxDimension property to Box2 to return the higher of the Width or Height.
|
||||
* Add GetLocalPosition to convert ScreenCoordinates to coordinates relative to the control. Ignores window.
|
||||
* Add GlobalRect and GlobalPixelRect for controls to get their UIBox2i in screen terms.
|
||||
* Add dotted line drawing to DrawingHandleScreen.
|
||||
* You can use `Subs.CVar()` from an entity systems to subscribe to CVar changes. This is more convenient than `IConfigurationManager.OnValueChanged` as it automatically unsubscribes on system shutdown.
|
||||
* There is now a built-in type serializer for `DateTime`, so you can put `DateTime`s in your data fields.
|
||||
* `System.Text.Unicode.UnicodeRange` and `UnicodeRanges` are now available in the sandbox.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* UI drawing now properly accounts for a control's draw routine potentially mangling the current matrix.
|
||||
* UI roots now properly update when the global stylesheet is changed. They previously only did so if they had a dedicated stylesheet (which is the one case where they would be unaffected by a global sheet update.
|
||||
|
||||
|
||||
## 209.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix missed import from 209.0.0.
|
||||
|
||||
|
||||
## 209.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `replay.max_compressed_size` and `replay.max_uncompressed_size` CVars are now `long`.
|
||||
* Remove obsolete CoordinatesExtension for ToEntityCoordinates from GridUid / Vector2i.
|
||||
|
||||
### New features
|
||||
|
||||
* Add GetEntitiesOnMap / GetChildEntities to EntityLookupSystem to return components on the specified map and components with the specified parent respectively.
|
||||
* Add MaxDimension property to Box2 to return the higher of the Width or Height.
|
||||
* Add GetLocalPosition to convert ScreenCoordinates to coordinates relative to the control. Ignores window.
|
||||
* Add GlobalRect and GlobalPixelRect for controls to get their UIBox2i in screen terms.
|
||||
* Add dotted line drawing to DrawingHandleScreen.
|
||||
* `IConfigurationManager.LoadDefaultsFromTomlStream` properly does type conversions. This fixes scenarios like loading of `long` CVars.
|
||||
* Add helper methods for TileRef / Vector2i to SharedMapSystem for ToCenterCoordinates (tile center EntityCoordinates) and ToCoordinates (tile origin to EntityCoordinates).
|
||||
* Copy some of the coordinates extensions to SharedTransformSystem.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed integer overflows in replay max size calculation.
|
||||
* Explicitly capped `replay.replay_tick_batchSize` internally to avoid high values causing allocation failures.
|
||||
|
||||
### Other
|
||||
|
||||
* Important MIDI performance improvements.
|
||||
|
||||
|
||||
## 208.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Metadata flags are no longer serialized as they get rebuilt on entity startup.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Log failing to load user keybinds and handle the exception.
|
||||
|
||||
|
||||
## 207.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add the ability to merge grids via GridFixtureSystem.
|
||||
|
||||
|
||||
## 207.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Update EntityLookup internally so non-approximate queries use the GJK solver and are much more accurate. This also means the approximate flag matters much more if you don't need narrowphase checks.
|
||||
* Add shape versions of queries for both EntityLookup and MapManager.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix PVS full state updates not clearing session entities and causing exceptions.
|
||||
|
||||
### Other
|
||||
|
||||
* Integration tests now run `NetMessage`s through serialization rather than passing the objects between client and server. This causes tests that missed `[NetSerializer]` attributes on any objects that need them to fail.
|
||||
|
||||
### Internal
|
||||
|
||||
* Remove a lot of duplicate code internally from EntityLookup and MapManager.
|
||||
|
||||
|
||||
## 206.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* tpto will teleport you to physics-center instead of transform center instead.
|
||||
* Rename local EntityLookup methods to reflect they take local AABBs and not world AABBs.
|
||||
|
||||
### New features
|
||||
|
||||
* Add some additional EntityLookup methods for local queries.
|
||||
* Add support to PrototypeManager for parsing specific files / directories as abstract.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix tpto short-circuiting if one of the listed entities isn't found.
|
||||
* Fix tpto not allowing grids as targets.
|
||||
|
||||
### Other
|
||||
|
||||
* Reduce MIDI source update rate from 10hz to 4hz.
|
||||
|
||||
### Internal
|
||||
|
||||
* Remove some duplicate internal code in EntityLookupSystem.
|
||||
* Skip serialization sourcegen in GLFW and Lidgren.
|
||||
|
||||
|
||||
## 205.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The unused `Robust.Physics` project has been deleted.
|
||||
* The project now uses [Central Package Management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management).
|
||||
* (Almost) all the NuGet packages have been updated. This causes many problems. I am so sorry.
|
||||
* Cleaned up some unused packages as well.
|
||||
|
||||
|
||||
## 204.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* New `EntitySystem` subscription helper for working with Bound User Interface events. You can find them by doing `Subs.BuiEvents<>()` in a system.
|
||||
* The `EntityManager.Subscriptions` type (for building helper extension methods) now uses
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Avoid loading assemblies from content `/Assemblies` if Robust ships its own copy. This avoid duplicate or weird mismatching version issues.
|
||||
|
||||
### Other
|
||||
|
||||
* Removed glibc version check warning.
|
||||
|
||||
|
||||
## 204.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Make EntityManager abstract and make IEntityManager.EntityNetManager not nullable.
|
||||
* Make VVAccess.ReadWrite default for all Datafields instead of VVAccess.ReadOnly
|
||||
|
||||
### New features
|
||||
|
||||
* `TextEdit.OnTextChanged`
|
||||
* Add Pick and PickAndTake versions for System.Random for ICollections.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `IClipboardManager.GetText()` returning null in some cases.
|
||||
* Fix possible NRE in server-side console command completion code.
|
||||
* Fix possible NRE on DebugConsole logs.
|
||||
* Fix exception when VVing non-networked components.
|
||||
|
||||
### Other
|
||||
|
||||
* Remove "Do not use from content" from IComponent.
|
||||
|
||||
|
||||
## 203.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `IComponentFactory.RegisterIgnore()` no longer supports overwriting existing registrations, components should get ignored before they are registered.
|
||||
* Event bus subscriptions are now locked after `IEntityManager` has started, instead of after the first component gets added. Any event subscriptions now need to happen before startup (but after init).
|
||||
* Event bus subscriptions must now be locked before raising any events.
|
||||
* Delete FodyWeavers.xsd as it hasn't been used for a long time.
|
||||
* Remove physics sleep cancelling as it was, in hindsight, a bad idea.
|
||||
|
||||
### New features
|
||||
|
||||
* `RobustUnitTest` now has a `ExtraComponents` field for automatically registering additional components.
|
||||
* `IComponentFactory.RegisterIgnore()` now accepts more than one string.
|
||||
* Added `IComponentFactory.RegisterTypes` for simultaneously registering multiple components.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Clamp volume calculations for audio rather than throwing.
|
||||
|
||||
|
||||
## 202.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Reverted some map/grid initialisation changes that might've been causing broadphase/physics errors.
|
||||
* Fixed PVS sometimes sending entities without first sending their children.
|
||||
* Fixed a container state handling bug caused by containers not removing expected entities when shutting down.
|
||||
* Fixed a `EnsureEntity<T>` state handling bug caused by improper handling of entity deletions.
|
||||
* Fixed a bad NetSyncEnabled debug assert.
|
||||
|
||||
|
||||
## 202.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add GetLocalEntitiesIntersecting overload that takes in a griduid and a Vector2i tile.
|
||||
|
||||
|
||||
## 202.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Various entity manager methods now have a new `where T : IComponent` constraint.
|
||||
* The `IComponentFactory.ComponentAdded` event has been renamed to `ComponentsAdded` and now provides an array of component registrations.
|
||||
* `IComponentFactory.RegisterIgnore()` no longer supports overwriting existing registrations, components should get ignored before they are registered.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `IComponentFactory.GetAllRegistrations()`
|
||||
* Add IComponentState interface support for component states so structs can be used in lieu of classes.
|
||||
|
||||
|
||||
## 201.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The `zCircleGradient` shader function arguments have changed. It now requires a pixel-size to ensure that the gradient is properly entered.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed some PVS null reference errors.
|
||||
|
||||
|
||||
## 200.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* MappingDataNode is now ordered.
|
||||
* Make invalid AutoNetworkedFields compiler errors.
|
||||
|
||||
### New features
|
||||
|
||||
* `OSWindowStyles.NoTitleBar` (supported only on Linux X11 for now).
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Avoid calling DirtyEntity when a component's last modified tick is not current.
|
||||
* Fix `tpgrid` allowing moving grids to nullspace.
|
||||
|
||||
### Other
|
||||
|
||||
* `OSWindowStyles.NoTitleOptions` is now supported on Linux X11.
|
||||
|
||||
|
||||
## 199.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Various `IEntityManager` C# events now use `Entity<MetadataComponent>` instead of `EntityUid`
|
||||
* Entity visibility masks now use a ushort instead of an integer.
|
||||
* Run grid traversal on entity spawn.
|
||||
|
||||
### New features
|
||||
|
||||
* Added two new `IEntityManager` C# events that get raiseed before and after deleting ("flushing") all entities.
|
||||
* Added a new `DeleteEntity()` override that takes in the entity's metadata and transform components.
|
||||
* Add better audio logs.
|
||||
* Expand z-library shader.
|
||||
* Add a Box2i union for Vector2i and add a Contains variant that assumes the Vector2i is a tile and not a point.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Try to prevent some NREs in PVS.
|
||||
* More PVS fixes and cleanup.
|
||||
|
||||
|
||||
## 198.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* `IClydeViewport` now provides access to the light render target.
|
||||
* Added a style-class to the `MenuBar` popup control.
|
||||
* Added `NextGaussian()` extension method for `System.Random`.
|
||||
* Added per-session variant of `PvsOverrideSystem.AddForceSend()`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Stopped the client from logging errors when attempting to delete invalid entities.
|
||||
|
||||
### Other
|
||||
|
||||
* The `DevWindow` UI inspector has been improved a bit and it now groups properties by their defining type.
|
||||
|
||||
|
||||
## 198.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix preprocessor flag for FULL_RELEASE preventing building.
|
||||
|
||||
|
||||
## 198.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Disable DefaultMagicAczProvider for FULL_RELEASE as it's only meant for debugging.
|
||||
|
||||
### New features
|
||||
|
||||
* Automatic UI scale is disabled by default for non-main windows. If desired, it can be re-enabled per window by changing `WindowRoot.DisableAutoScaling`.
|
||||
* Add UI click and hover sound support via IUserInterfaceManager.SetClickSound / .SetHoverSound
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix GetEntitiesIntersecting for map entities without grids.
|
||||
|
||||
### Other
|
||||
|
||||
* Print more diagnostics on server startup.
|
||||
|
||||
|
||||
## 197.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* ACZ improvements: `IStatusHost.InvalidateAcz()` and `IStatusHost.SetFullHybridAczProvider()`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixes a PVS bug that happens when grids moved across maps.
|
||||
* Fixes sprite animations not working properly
|
||||
|
||||
|
||||
## 197.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* PvsOverrideSystem has been reworked:
|
||||
* Session and global overrides now default to always being recursive (i.e., sending all children).
|
||||
* Session & global overrides will always respect a client's PVS budgets.
|
||||
* Entities with an override will now still be sent in the same way as other entities if they are within a player's view. If you want to prevent them from being sent, you need to use visibility masks.
|
||||
* Entities can have more than one kind of override (i.e., multiple sessions).
|
||||
|
||||
### New features
|
||||
|
||||
* Added a `PvsSize ` field to `EyeComponent`, which can be used to modify the PVS range of an eye.
|
||||
* Added a new `NetLowLodRange` cvar for reducing the number of distant entities that get sent to a player. If a PVS chunk is beyond this range (but still within PVS range), then only high-priority entities on that chunk will get sent.
|
||||
* Added a new metadata flag for tagging an entity as a "high prority" entity that should get sent even on distant chunks. This only works for entities that are directly attached to a grid or map. This is currently used by lights & occluders.
|
||||
|
||||
### Other
|
||||
|
||||
* PVS has been reworked again, and should hopefully be noticeable faster.
|
||||
* PVS now prioritizes sending chunks that are closer to a player's eyes.
|
||||
|
||||
|
||||
## 196.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Dirtying a non-networked component will now fail a debug assert.
|
||||
* The `IInvocationContext` interface for toolshed commands now requires a UserId field. The session field should be cleared if a player disconnects.
|
||||
|
||||
### New features
|
||||
|
||||
* `LocalizationManager` now supports multiple fallback cultures
|
||||
* SpriteView now supports using a `NetEntity` to select an entity to draw.
|
||||
* Added methods for simultaneously dirtying several components on the same entity.
|
||||
* Animated sprite layers now have a "Cycle" option that will reverse an animation when it finishes.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed a recursion/stack-overflow in `GridTraversalSystem`
|
||||
* Ensure `Robust.Client.WebView` processes get shut down if game process exits uncleanly.
|
||||
* Fixed Toolshed commands not properly functioning after disconnecting and reconnecting.
|
||||
|
||||
### Other
|
||||
|
||||
* Console command completions no longer suggest toolshed commands for non-toolshed commands.
|
||||
|
||||
|
||||
|
||||
## 195.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixes playing audio using audio streams
|
||||
* Fixes placement manager exceptions when placing self deleting / spawner entities
|
||||
* Fixed `IPrototypeManager.EnumeratePrototypes<T>` throwing an exception when there are no instances.
|
||||
|
||||
|
||||
## 195.0.0
|
||||
|
||||
### New features
|
||||
|
||||
* Generic versions of `DebugTools.AssertEquals()` functions.
|
||||
* `[Prototype]` now does not need to have a name specified, the name is inferred from the class name.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixes a physics bug that could cause deleted entities to remain on the physics map.
|
||||
* Fixes a bug in entity lookup code that could cause clients to get stuck in an infinite loop.
|
||||
|
||||
### Other
|
||||
|
||||
* `Robust.Client.WebView` has been brought alive again.
|
||||
* The addition of physics joints is no longer deferred to the next tick.
|
||||
* Grid traversal is no longer deferred to the next tick.
|
||||
* Integration tests now fail when console commands log errors.
|
||||
|
||||
|
||||
## 194.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* `IAudioManager` has APIs to directly load `AudioStream`s from data streams.
|
||||
* `AudioSystem` has new `Play*` methods.
|
||||
* `EntityCoordinates.TryDelta()`
|
||||
* `EntityLookupSystem.GetEntitiesInRange()` untyped hashset overload has `flags` parameter.
|
||||
|
||||
|
||||
## 194.0.2
|
||||
|
||||
### Internal
|
||||
|
||||
* Added some null-checks to PVS to try reduce the error spam.
|
||||
|
||||
|
||||
## 194.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `Control.SetPositionInParent` failing to move an entity to the last position.
|
||||
* Fixed audio occlusion not working.
|
||||
|
||||
### Internal
|
||||
|
||||
* Added some logs for grid/map deletion and movement to debug some map loading issues.
|
||||
* Refactored some parts of PVS. It should be slightly faster, though the game may be unstable for a bit.
|
||||
|
||||
## 194.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* MoveEvent is no longer raised broadcast, subscribe to the SharedTransformSystem.OnGlobalMoveEvent C# event instead
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed the game sometimes freezing while trying to load specific audio files.
|
||||
|
||||
|
||||
## 193.2.0
|
||||
|
||||
### Other
|
||||
|
||||
* Added more PVS error logs
|
||||
|
||||
|
||||
## 193.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed an exception when building in FULL_RELEASE
|
||||
|
||||
|
||||
## 193.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added FrozenDictionary and FrozenHashSet to sandbox whitelist
|
||||
* Added yaml type serializers for FrozenDictionary and FrozenHashSet
|
||||
* Added `IPrototypeManager.GetInstances<T>()`
|
||||
* `IPrototypeManager` now also raises `PrototypesReloadedEventArgs` as a system event.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Might fix some PVS bugs added in the last version.
|
||||
|
||||
### Internal
|
||||
|
||||
* Various static dictionaries have been converted into FrozenDictionary.
|
||||
|
||||
|
||||
## 193.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The `TransformChildrenEnumerator`'s out values are now non-nullable
|
||||
|
||||
### New features
|
||||
|
||||
* Added `IPrototypeManager.TryGetInstances()`, which returns a dictionary of prototype instances for a given prototype kind/type.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `BaseAudioSource.SetAuxiliary()` throwing errors on non-EFX systems
|
||||
|
||||
### Internal
|
||||
|
||||
|
||||
* The internals of PVS system have been reworked to reduce the number of dictionary lookups.
|
||||
* `RobustMappedStringSerializer` now uses frozen dictionaries
|
||||
* `IPrototypeManager` now uses frozen dictionaries
|
||||
|
||||
|
||||
## 192.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `EntitySystem.TryGetEntity` is now `protected`.
|
||||
|
||||
### Internal
|
||||
|
||||
* PVS message ack processing now happens asynchronously
|
||||
* Dependency collections now use a `FrozenDictionary`
|
||||
|
||||
|
||||
## 191.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
.* Fix sandbox being broken thanks to .NET 8.
|
||||
|
||||
|
||||
## 191.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Robust now uses **.NET 8**. Nyoom.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* `IResourceCache.TryGetResource<T>` won't silently eat all exceptions anymore.
|
||||
|
||||
|
||||
## 190.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Revert broadphase job to prevent OOM from logs.
|
||||
|
||||
|
||||
## 190.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add OnGrabbed / OnReleased to slider controls.
|
||||
* Add Rotation method for matrices and also make the precision slightly better when angles are passed in by taking double-precision not single-precision floats.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix some grid setting asserts when adding gridcomponent to existing maps.
|
||||
|
||||
|
||||
## 190.0.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add color gradients to sliders.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix HSV / HSL producing black colors on 360 hue.
|
||||
* Stop terminating entities from prematurely detaching to nullspace.
|
||||
* Ensure shader parameters update when swapping instances.
|
||||
|
||||
### Other
|
||||
|
||||
* Add more verbose logging to OpenAL errors.
|
||||
|
||||
### Internal
|
||||
|
||||
* Change NetSyncEnabled to an assert and fix instances where it slips through to PVS.
|
||||
|
||||
|
||||
## 189.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Use the base AudioParams for networking not the z-offset adjusted ones.
|
||||
* Modulate SpriteView sprites by the control's color modulation.
|
||||
|
||||
### New features
|
||||
|
||||
* Improve YAML linter error messages for parent nodes.
|
||||
* ExpandPvsEvent will also be raised directed to the session's attached entity.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Client clientside entity error spam.
|
||||
|
||||
### Internal
|
||||
|
||||
* Set priorGain to 0 where no EFX is supported for audio rather than 0.5.
|
||||
* Try to hotfix MIDI lock contention more via a semaphore.
|
||||
|
||||
|
||||
## 188.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Return null buffered audio if there's an exception and use the dummy instance internally.
|
||||
* Use entity name then suffix for entity spawn window ordering.
|
||||
* Change MidiManager volume to gain.
|
||||
* Remove EntityQuery from the MapVelocity API.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Potentially fix some audio issues by setting gain to half where EFX not found and the prior gain was 0.
|
||||
* Log errors upon trying to spawn audio attached to deleted entities instead of trying to spawn them and erroring later.
|
||||
* Fixed predicted audio spawns not applying the adjusted audio params.
|
||||
* Fix GetDimensions for the screenhandle where the text is only a single line.
|
||||
|
||||
|
||||
## 187.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added a cancellable bool to physics sleeping events where we may wish to cancel it.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix corrupted physics awake state leading to client mispredicts.
|
||||
|
||||
|
||||
## 187.1.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Hotfix contact nullrefs if they're modified during manifold generation.
|
||||
|
||||
|
||||
## 187.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Revert physics solver job to fix crashes until box2d v3 rolls around.
|
||||
* Don't RegenerateContacts if the body isn't collidable to avoid putting non-collidable proxies on the movebuffer.
|
||||
|
||||
|
||||
## 187.1.0
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Apply default audio params to all audio sources not just non-buffered ones.
|
||||
* Avoid re-allocating broadphase job every tick and maybe fix a rare nullref for it.
|
||||
|
||||
|
||||
## 187.0.0
|
||||
|
||||
### New features
|
||||
|
||||
* Improved error message for network failing to initialize.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix not being able to add multiple PVS session overrides in a single tick without overwriting each one. This should fix issues with audio filters.
|
||||
|
||||
### Other
|
||||
|
||||
* Changed toolshed initialisation logs to verbose.
|
||||
|
||||
|
||||
## 186.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add public method to get PVS session overrides for a specific session.
|
||||
|
||||
### Internal
|
||||
|
||||
* Add temporary audio debugging.
|
||||
|
||||
|
||||
## 186.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Global audio is now stored on its own map to avoid contamination issues with nullspace.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix MIDIs playing cross-map
|
||||
* Only dispose audio on game closure and don't stop playing if it's disposed elsewhere i.e. MIDIs.
|
||||
|
||||
|
||||
## 185.2.0
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Bandaid deleted MIDI source entities spamming velocity error logs.
|
||||
|
||||
### Other
|
||||
|
||||
* Reverted MIDI audio not updating every frame due to lock contention with the MIDI renderer for now.
|
||||
|
||||
|
||||
## 185.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix Z-Offset for audio not being applied on initialization.
|
||||
|
||||
### Internal
|
||||
|
||||
* Flag some internal queries as approximate to avoid unnecessary AABB checks. Some of these are already covered off with TestOverlap calls and the rest will need updating to do so in a future update.
|
||||
|
||||
|
||||
## 185.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Audio listener's velocity is set using the attached entity's velocity rather than ignored.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix imprecision on audio position
|
||||
|
||||
|
||||
## 185.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Added a flag for grid-based audio rather than implicitly doing it.
|
||||
|
||||
### New features
|
||||
|
||||
* Added IRobustJob and IParallelRobustJob (which splits out into IRobustJob). These can be passed to ParallelManager for work to be run on the threadpool without relying upon Task.Run / Parallel.For which can allocate significantly more. It also has conveniences such as being able to specify batch sizing via the interface implementation.
|
||||
|
||||
|
||||
## 184.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add API to get gain / volume for a provided value on SharedAudioSystem.
|
||||
* Make GetOcclusion public for AudioSystem.
|
||||
* Add SharedAudioSystem.SetGain to complement SharedAudioSystem.SetVolume
|
||||
|
||||
|
||||
## 184.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Update MIDI position and occlusion every frame instead of at set intervals.
|
||||
* Fix global audio not being global.
|
||||
|
||||
|
||||
## 184.0.0
|
||||
|
||||
### Internal
|
||||
|
||||
* Add RobustMemoryManager with RecyclableIOMemoryStream to significantly reduce MsgState allocations until better memory management is implemented.
|
||||
|
||||
|
||||
## 183.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Audio rework has been re-merged now that the issues with packaging on server have been rectified (thanks PJB!)
|
||||
* Reverted Arch pending further performance work on making TryGetComponent competitive with live.
|
||||
|
||||
|
||||
## 182.1.1
|
||||
|
||||
### Internal
|
||||
|
||||
* Remove AggressiveInlining from Arch for debugging.
|
||||
|
||||
|
||||
## 182.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add IRobustRandom.SetSeed
|
||||
|
||||
### Other
|
||||
|
||||
* Add Arch.TrimExcess() back to remove excess archetypes on map load / EntityManager flush.
|
||||
|
||||
|
||||
## 182.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Add EntityUid's generation / version to the hashcode.
|
||||
|
||||
|
||||
## 181.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix exceptions from having too many lights on screen and causing the game to go black.
|
||||
* Fix components having events raised in ClientGameStateManager before fully set and causing nullable reference exceptions.
|
||||
* Replace tile intersection IEnumerables with TileEnumerator internally. Also made it public for external callers that wish to avoid IEnumerable.
|
||||
|
||||
|
||||
## 181.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix the non-generic HasComp and add a test for good measure.
|
||||
|
||||
|
||||
## 181.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- Arch is merged refactoring how components are stored on engine. There's minimal changes on the API end to facilitate component nullability with much internal refactoring.
|
||||
|
||||
|
||||
## 180.2.1
|
||||
|
||||
|
||||
## 180.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add EnsureEntity variants that take in collections.
|
||||
* Add more MapSystem helper methods.
|
||||
|
||||
### Internal
|
||||
|
||||
* Cache some more PVS data to avoid re-allocating every tick.
|
||||
|
||||
|
||||
## 180.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add the map name to lsmap.
|
||||
* Add net.pool_size to CVars to control the message data pool size in Lidgren and to also toggle pooling.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix physics contraints causing enormous heap allocations.
|
||||
* Fix potential error when writing a runtime log.
|
||||
* Fix shape lookups for non-hard fixtures in EntityLookupSystem from 180.0.0
|
||||
|
||||
|
||||
## 180.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Removed some obsolete methods from EntityLookupSystem.
|
||||
|
||||
### New features
|
||||
|
||||
* PhysicsSystem.TryGetNearest now supports chain shapes.
|
||||
* Add IPhysShape methods to EntityLookupSystem rather than relying on AABB checks.
|
||||
* Add some more helper methods to SharedTransformSystem.
|
||||
* Add GetOrNew dictionary extension that also returns a bool on whether the key existed.
|
||||
* Add a GetAnchoredEntities overload that takes in a list.
|
||||
|
||||
### Other
|
||||
|
||||
* Use NetEntities for the F3 debug panel to align with command usage.
|
||||
|
||||
|
||||
## 179.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* EyeComponent.Eye is no longer nullable
|
||||
|
||||
### New features
|
||||
|
||||
* Light rendering can now be enabled or disable per eye.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Deserializing old maps with empty grid chunks should now just ignore those chunks.
|
||||
|
||||
### Other
|
||||
|
||||
* UnknownPrototypeException now also tells you the prototype kind instead of just the unkown ID.
|
||||
* Adding or removing networked components while resetting predicted entities now results in a more informative exception.
|
||||
|
||||
|
||||
## 178.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Most methods in ActorSystem have been moved to ISharedPlayerManager.
|
||||
* Several actor/player related components and events have been moved to shared.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `NetListAsArray<T>.Value` to the sandbox whitelist
|
||||
|
||||
|
||||
## 177.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Removed toInsertXform and added containerXform in SharedContainerSystem.CanInsert.
|
||||
* Removed EntityQuery parameters from SharedContainerSystem.IsEntityOrParentInContainer.
|
||||
* Changed the signature of ContainsEntity in SharedTransformSystem to use Entity<T>.
|
||||
* Removed one obsoleted SharedTransformSystem.AnchorEntity method.
|
||||
* Changed signature of SharedTransformSystem.SetCoordinates to use Entity<T>.
|
||||
|
||||
### New features
|
||||
|
||||
* Added more Entity<T> query methods.
|
||||
* Added BeforeApplyState event to replay playback.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed inverted GetAllMapGrids map id check.
|
||||
* Fixed transform test warnings.
|
||||
* Fixed PlacementManager warnings.
|
||||
* Fixed reparenting bug for entities that are being deleted.
|
||||
|
||||
### Other
|
||||
|
||||
* Changed VerticalAlignment of RichTextLabel to Center to be consistent with Label.
|
||||
* Changed PVS error log to be a warning instead.
|
||||
* Marked insert and remove container methods as obsolete, added container system methods to replace them.
|
||||
* Marked TransformComponent.MapPosition as obsolete, added GetMapCoordinates system method to replace it.
|
||||
|
||||
### Internal
|
||||
|
||||
* Moved TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi methods from UserInterfaceSystem to SharedUserInterfaceSystem.
|
||||
|
||||
|
||||
## 176.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Reverted audio rework temporarily until packaging is fixed.
|
||||
* Changes to Robust.Packaging to facilitate Content.Packaging ports from the python packaging scripts.
|
||||
|
||||
### New features
|
||||
|
||||
* Add a cvar for max game state buffer size.
|
||||
* Add an overload for GetEntitiesInRange that takes in a set.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix PVS initial list capacity always being 0.
|
||||
* Fix replay lerp error spam.
|
||||
|
||||
|
||||
## 175.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Removed static SoundSystem.Play methods.
|
||||
* Moved IPlayingAudioStream onto AudioComponent and entities instead of an abstract stream.
|
||||
* IResourceCache is in shared and IClientResourceCache is the client version to use for textures.
|
||||
* Default audio attenuation changed from InverseDistanceClamped to LinearDistanceClamped.
|
||||
* Removed per-source audio attenuation.
|
||||
|
||||
### New features
|
||||
|
||||
* Add preliminary support for EFX Reverb presets + auxiliary slots; these are also entities.
|
||||
* Audio on grid entities is now attached to the grid.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* If an audio entity comes into PVS range its track will start at the relevant offset and not the beginning.
|
||||
* Z-Axis offset is considered for ReferenceDistance / MaxDistance for audio.
|
||||
* Audio will now pause if the attached entity is paused.
|
||||
|
||||
### Other
|
||||
|
||||
* Changed audio Z-Axis offset from -5m to -1m.
|
||||
|
||||
|
||||
## 174.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
5
Resources/EnginePrototypes/Audio/audio_entities.yml
Normal file
5
Resources/EnginePrototypes/Audio/audio_entities.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
- type: entity
|
||||
id: Audio
|
||||
name: Audio
|
||||
description: Audio entity used by engine
|
||||
save: false
|
||||
3076
Resources/EnginePrototypes/Audio/audio_presets.yml
Normal file
3076
Resources/EnginePrototypes/Audio/audio_presets.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,3 +12,8 @@
|
||||
id: bgra
|
||||
kind: source
|
||||
path: "/Shaders/Internal/bgra.swsl"
|
||||
|
||||
- type: shader
|
||||
id: ColorPicker
|
||||
kind: source
|
||||
path: "/Shaders/color_picker.swsl"
|
||||
|
||||
@@ -9,6 +9,7 @@ cmd-parse-failure-float = {$arg} is not a valid float.
|
||||
cmd-parse-failure-bool = {$arg} is not a valid bool.
|
||||
cmd-parse-failure-uid = {$arg} is not a valid entity UID.
|
||||
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
|
||||
cmd-parse-failure-grid = {$arg} is not a valid grid.
|
||||
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
|
||||
|
||||
cmd-error-file-not-found = Could not find file: {$file}.
|
||||
@@ -561,3 +562,7 @@ cmd-vfs_ls-hint-path = <path>
|
||||
|
||||
cmd-reloadtiletextures-desc = Reloads the tile texture atlas to allow hot reloading tile sprites
|
||||
cmd-reloadtiletextures-help = Usage: reloadtiletextures
|
||||
|
||||
cmd-audio_length-desc = Shows the length of an audio file
|
||||
cmd-audio_length-help = Usage: audio_length { cmd-audio_length-arg-file-name }
|
||||
cmd-audio_length-arg-file-name = <file name>
|
||||
|
||||
8
Resources/Locale/en-US/physics/grid_merging.ftl
Normal file
8
Resources/Locale/en-US/physics/grid_merging.ftl
Normal file
@@ -0,0 +1,8 @@
|
||||
cmd-merge_grids-desc = Combines 2 grids into 1 grid
|
||||
cmd-merge_grids-help = merge_grids <gridUid1> <gridUid2> <offsetX> <offsetY> [angle]
|
||||
|
||||
cmd-merge_grids-hintA = Grid A
|
||||
cmd-merge_grids-hintB = Grid B
|
||||
cmd-merge_grids-xOffset = X offset
|
||||
cmd-merge_grids-yOffset = Y offset
|
||||
cmd-merge_grids-angle = [Angle]
|
||||
46
Resources/Shaders/color_picker.swsl
Normal file
46
Resources/Shaders/color_picker.swsl
Normal file
@@ -0,0 +1,46 @@
|
||||
// Simple shader for creating a box with colours varying along the x and y axes.
|
||||
|
||||
uniform highp vec2 size;
|
||||
uniform highp vec2 offset;
|
||||
|
||||
uniform highp vec4 xAxis;
|
||||
uniform highp vec4 yAxis;
|
||||
uniform highp vec4 baseColor;
|
||||
|
||||
uniform bool hsv;
|
||||
|
||||
void fragment()
|
||||
{
|
||||
// Calculate local uv coordinates.
|
||||
// I.e., if using this shader to draw a box to the screen, (0,0) is the bottom left of the box.
|
||||
|
||||
highp float yCoords = 1.0/SCREEN_PIXEL_SIZE.y - FRAGCOORD.y;
|
||||
highp vec2 uv = vec2(FRAGCOORD.x - offset.x, yCoords - offset.y);
|
||||
uv /= size;
|
||||
uv.y = 1.0 - uv.y;
|
||||
|
||||
highp vec4 modulate = baseColor + uv.x * xAxis + uv.y * yAxis;
|
||||
|
||||
if (hsv)
|
||||
{
|
||||
modulate.xyz = hsv2rgb(modulate.xyz);
|
||||
}
|
||||
|
||||
// The UV used for the texture lookup is the TEXTURE UV coordinate, which is different from the coordinates computed above.
|
||||
COLOR = zTexture(UV) * modulate;
|
||||
}
|
||||
|
||||
|
||||
// hsv to RGB conversion taken from www.shadertoy.com/view/MsS3Wc
|
||||
|
||||
// The MIT License
|
||||
// Copyright © 2014 Inigo Quilez
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// https://www.youtube.com/c/InigoQuilez
|
||||
// https://iquilezles.org
|
||||
|
||||
highp vec3 hsv2rgb( in highp vec3 c )
|
||||
{
|
||||
highp vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 );
|
||||
return c.z * mix( vec3(1.0), rgb, c.y);
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets"/>
|
||||
<Import Project="..\MSBuild\Robust.Engine.props"/>
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1"/>
|
||||
<PackageReference Include="NUnit" Version="3.13.2"/>
|
||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.15.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageVersion Update="NUnit" Version="3.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces"/>
|
||||
<PackageReference Include="NUnit"/>
|
||||
<PackageReference Include="NUnit3TestAdapter"/>
|
||||
<PackageReference Include="NUnit.Analyzers"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Robust.Analyzers\Robust.Analyzers.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,19 +3,20 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>10</LangVersion>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.0.1" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for NotNullableFlagAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for FriendAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" />
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using BenchmarkDotNet.Analysers;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Diagnosers;
|
||||
using BenchmarkDotNet.EventProcessors;
|
||||
using BenchmarkDotNet.Exporters;
|
||||
using BenchmarkDotNet.Filters;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Order;
|
||||
using BenchmarkDotNet.Reports;
|
||||
using BenchmarkDotNet.Running;
|
||||
using BenchmarkDotNet.Validators;
|
||||
using Robust.Benchmarks.Exporters;
|
||||
|
||||
@@ -44,10 +47,16 @@ public sealed class DefaultSQLConfig : IConfig
|
||||
|
||||
public IEnumerable<BenchmarkLogicalGroupRule> GetLogicalGroupRules() => DefaultConfig.Instance.GetLogicalGroupRules();
|
||||
|
||||
public IEnumerable<EventProcessor> GetEventProcessors() => DefaultConfig.Instance.GetEventProcessors();
|
||||
|
||||
public IEnumerable<IColumnHidingRule> GetColumnHidingRules() => DefaultConfig.Instance.GetColumnHidingRules();
|
||||
public IOrderer Orderer => DefaultConfig.Instance.Orderer!;
|
||||
public ICategoryDiscoverer? CategoryDiscoverer => DefaultConfig.Instance.CategoryDiscoverer;
|
||||
public SummaryStyle SummaryStyle => DefaultConfig.Instance.SummaryStyle;
|
||||
public ConfigUnionRule UnionRule => DefaultConfig.Instance.UnionRule;
|
||||
public string ArtifactsPath => DefaultConfig.Instance.ArtifactsPath;
|
||||
public CultureInfo CultureInfo => DefaultConfig.Instance.CultureInfo!;
|
||||
public ConfigOptions Options => DefaultConfig.Instance.Options;
|
||||
public TimeSpan BuildTimeout => DefaultConfig.Instance.BuildTimeout;
|
||||
public IReadOnlyList<Conclusion> ConfigAnalysisConclusion => DefaultConfig.Instance.ConfigAnalysisConclusion;
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
<ProjectReference Include="..\Robust.UnitTesting\Robust.UnitTesting.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameStates;
|
||||
@@ -11,7 +12,9 @@ using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.UnitTesting.Server;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Robust.Benchmarks.Transform;
|
||||
|
||||
@@ -19,114 +22,177 @@ namespace Robust.Benchmarks.Transform;
|
||||
/// This benchmark tests various transform/move related functions with an entity that has many children.
|
||||
/// </summary>
|
||||
[Virtual, MemoryDiagnoser]
|
||||
public class RecursiveMoveBenchmark
|
||||
public class RecursiveMoveBenchmark : RobustIntegrationTest
|
||||
{
|
||||
private ISimulation _simulation = default!;
|
||||
private IEntityManager _entMan = default!;
|
||||
private SharedTransformSystem _transform = default!;
|
||||
private ContainerSystem _container = default!;
|
||||
private PvsSystem _pvs = default!;
|
||||
private EntityCoordinates _mapCoords;
|
||||
private EntityCoordinates _gridCoords;
|
||||
private EntityCoordinates _gridCoords2;
|
||||
private EntityUid _ent;
|
||||
private EntityUid _child;
|
||||
private TransformComponent _childXform = default!;
|
||||
private EntityQuery<TransformComponent> _query;
|
||||
private ICommonSession[] _players = default!;
|
||||
private PvsSession _session = default!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
_simulation = RobustServerSimulation
|
||||
.NewSimulation()
|
||||
.InitializeInstance();
|
||||
ProgramShared.PathOffset = "../../../../";
|
||||
var server = StartServer();
|
||||
var client = StartClient();
|
||||
|
||||
if (!_simulation.Resolve<IConfigurationManager>().GetCVar(CVars.NetPVS))
|
||||
throw new InvalidOperationException("PVS must be enabled");
|
||||
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();
|
||||
|
||||
var mapMan = server.ResolveDependency<IMapManager>();
|
||||
_entMan = server.ResolveDependency<IEntityManager>();
|
||||
var confMan = server.ResolveDependency<IConfigurationManager>();
|
||||
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
|
||||
|
||||
_entMan = _simulation.Resolve<IEntityManager>();
|
||||
_transform = _entMan.System<SharedTransformSystem>();
|
||||
_container = _entMan.System<ContainerSystem>();
|
||||
_pvs = _entMan.System<PvsSystem>();
|
||||
_query = _entMan.GetEntityQuery<TransformComponent>();
|
||||
|
||||
// Create map & grid
|
||||
var mapMan = _simulation.Resolve<IMapManager>();
|
||||
var mapSys = _entMan.System<SharedMapSystem>();
|
||||
var mapId = mapMan.CreateMap();
|
||||
var map = mapMan.GetMapEntityId(mapId);
|
||||
var gridComp = mapMan.CreateGridEntity(mapId);
|
||||
var grid = gridComp.Owner;
|
||||
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
|
||||
_mapCoords = new EntityCoordinates(map, 100, 100);
|
||||
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
|
||||
|
||||
// Next, we will spawn our test entity. This entity will have a complex transform/container hierarchy.
|
||||
// This is intended to be representative of a typical SS14 player entity, with organs. clothing, and a full backpack.
|
||||
_ent = _entMan.Spawn();
|
||||
var netMan = client.ResolveDependency<IClientNetManager>();
|
||||
client.SetConnectTarget(server);
|
||||
client.Post(() => netMan.ClientConnect(null!, 0, null!));
|
||||
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
|
||||
|
||||
// Quick check that SetCoordinates actually changes the parent as expected
|
||||
// I.e., ensure that grid-traversal code doesn't just dump the entity on the map.
|
||||
_transform.SetCoordinates(_ent, _gridCoords);
|
||||
if (_query.GetComponent(_ent).ParentUid != _gridCoords.EntityId)
|
||||
throw new Exception("Grid traversal error.");
|
||||
|
||||
_transform.SetCoordinates(_ent, _mapCoords);
|
||||
if (_query.GetComponent(_ent).ParentUid != _mapCoords.EntityId)
|
||||
throw new Exception("Grid traversal error.");
|
||||
|
||||
// Add 5 direct children in slots to represent clothing.
|
||||
for (var i = 0; i < 5; i++)
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var id = $"inventory{i}";
|
||||
_container.EnsureContainer<ContainerSlot>(_ent, id);
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, id, out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
server.WaitRunTicks(1).Wait();
|
||||
client.WaitRunTicks(1).Wait();
|
||||
}
|
||||
|
||||
// body parts
|
||||
_container.EnsureContainer<Container>(_ent, "body");
|
||||
for (var i = 0; i < 5; i++)
|
||||
// Ensure client & server ticks are synced.
|
||||
// Client runs 1 tick ahead
|
||||
{
|
||||
// Simple organ
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
var sTick = (int)server.Timing.CurTick.Value;
|
||||
var cTick = (int)client.Timing.CurTick.Value;
|
||||
var delta = cTick - sTick;
|
||||
|
||||
// body part that has another body part / limb
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out var limb))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
if (delta > 1)
|
||||
server.WaitRunTicks(delta - 1).Wait();
|
||||
else if (delta < 1)
|
||||
client.WaitRunTicks(1 - delta).Wait();
|
||||
|
||||
_container.EnsureContainer<ContainerSlot>(limb.Value, "limb");
|
||||
if (!_entMan.TrySpawnInContainer(null, limb.Value, "limb", out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
sTick = (int)server.Timing.CurTick.Value;
|
||||
cTick = (int)client.Timing.CurTick.Value;
|
||||
delta = cTick - sTick;
|
||||
if (delta != 1)
|
||||
throw new Exception("Failed setup");
|
||||
}
|
||||
|
||||
// Backpack
|
||||
_container.EnsureContainer<ContainerSlot>(_ent, "inventory-backpack");
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, "inventory-backpack", out var backpack))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
|
||||
// Misc backpack contents.
|
||||
var backpackStorage = _container.EnsureContainer<Container>(backpack.Value, "storage");
|
||||
for (var i = 0; i < 10; i++)
|
||||
// Set up map and spawn player
|
||||
server.WaitPost(() =>
|
||||
{
|
||||
if (!_entMan.TrySpawnInContainer(null, backpack.Value, "storage", out _))
|
||||
var mapId = mapMan.CreateMap();
|
||||
var map = mapMan.GetMapEntityId(mapId);
|
||||
var gridComp = mapMan.CreateGridEntity(mapId);
|
||||
var grid = gridComp.Owner;
|
||||
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
|
||||
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
|
||||
_gridCoords2 = new EntityCoordinates(grid, .5f, .6f);
|
||||
_mapCoords = new EntityCoordinates(map, 100, 100);
|
||||
|
||||
var playerUid = _entMan.SpawnEntity(null, _mapCoords);
|
||||
|
||||
// Attach player.
|
||||
var session = sPlayerMan.Sessions.First();
|
||||
server.PlayerMan.SetAttachedEntity(session, playerUid);
|
||||
sPlayerMan.JoinGame(session);
|
||||
|
||||
// Next, we will spawn our test entity. This entity will have a complex transform/container hierarchy.
|
||||
// This is intended to be representative of a typical SS14 player entity, with organs. clothing, and a full backpack.
|
||||
_ent = _entMan.Spawn();
|
||||
|
||||
// Quick check that SetCoordinates actually changes the parent as expected
|
||||
// I.e., ensure that grid-traversal code doesn't just dump the entity on the map.
|
||||
_transform.SetCoordinates(_ent, _gridCoords);
|
||||
if (_query.GetComponent(_ent).ParentUid != _gridCoords.EntityId)
|
||||
throw new Exception("Grid traversal error.");
|
||||
|
||||
_transform.SetCoordinates(_ent, _mapCoords);
|
||||
if (_query.GetComponent(_ent).ParentUid != _mapCoords.EntityId)
|
||||
throw new Exception("Grid traversal error.");
|
||||
|
||||
// Add 5 direct children in slots to represent clothing.
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var id = $"inventory{i}";
|
||||
_container.EnsureContainer<ContainerSlot>(_ent, id);
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, id, out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
}
|
||||
|
||||
// body parts
|
||||
_container.EnsureContainer<Container>(_ent, "body");
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
// Simple organ
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
|
||||
// body part that has another body part / limb
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, "body", out var limb))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
|
||||
_container.EnsureContainer<ContainerSlot>(limb.Value, "limb");
|
||||
if (!_entMan.TrySpawnInContainer(null, limb.Value, "limb", out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
}
|
||||
|
||||
// Backpack
|
||||
_container.EnsureContainer<ContainerSlot>(_ent, "inventory-backpack");
|
||||
if (!_entMan.TrySpawnInContainer(null, _ent, "inventory-backpack", out var backpack))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
|
||||
// Misc backpack contents.
|
||||
var backpackStorage = _container.EnsureContainer<Container>(backpack.Value, "storage");
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
if (!_entMan.TrySpawnInContainer(null, backpack.Value, "storage", out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
}
|
||||
|
||||
// Emergency box inside of the backpack
|
||||
var box = backpackStorage.ContainedEntities.First();
|
||||
var boxContainer = _container.EnsureContainer<Container>(box, "storage");
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
if (!_entMan.TrySpawnInContainer(null, box, "storage", out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
}
|
||||
|
||||
// Deepest child.
|
||||
_child = boxContainer.ContainedEntities.First();
|
||||
_childXform = _query.GetComponent(_child);
|
||||
|
||||
_players = new[] {session};
|
||||
_session = _pvs.PlayerData[session];
|
||||
}).Wait();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
server.WaitRunTicks(1).Wait();
|
||||
client.WaitRunTicks(1).Wait();
|
||||
}
|
||||
|
||||
// Emergency box inside of the backpack
|
||||
var box = backpackStorage.ContainedEntities.First();
|
||||
var boxContainer = _container.EnsureContainer<Container>(box, "storage");
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
if (!_entMan.TrySpawnInContainer(null, box, "storage", out _))
|
||||
throw new Exception($"Failed to setup entity");
|
||||
}
|
||||
PvsTick();
|
||||
PvsTick();
|
||||
}
|
||||
|
||||
// Deepest child.
|
||||
_child = boxContainer.ContainedEntities.First();
|
||||
_childXform = _query.GetComponent(_child);
|
||||
|
||||
_pvs.ProcessCollections();
|
||||
private void PvsTick()
|
||||
{
|
||||
_session.ClearState();
|
||||
_pvs.CacheSessionData(_players);
|
||||
_pvs.GetVisibleChunks();
|
||||
_pvs.ProcessVisibleChunksSequential();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -140,6 +206,13 @@ public class RecursiveMoveBenchmark
|
||||
_transform.SetCoordinates(_ent, _mapCoords);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void MoveEntityASmidge()
|
||||
{
|
||||
_transform.SetCoordinates(_ent, _gridCoords);
|
||||
_transform.SetCoordinates(_ent, _gridCoords2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="MoveEntity"/>, but also processes queued PVS chunk updates.
|
||||
/// </summary>
|
||||
@@ -147,9 +220,18 @@ public class RecursiveMoveBenchmark
|
||||
public void MoveAndUpdateChunks()
|
||||
{
|
||||
_transform.SetCoordinates(_ent, _gridCoords);
|
||||
_pvs.ProcessCollections();
|
||||
PvsTick();
|
||||
_transform.SetCoordinates(_ent, _mapCoords);
|
||||
_pvs.ProcessCollections();
|
||||
PvsTick();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void MoveASmidgeAndUpdateChunk()
|
||||
{
|
||||
_transform.SetCoordinates(_ent, _gridCoords);
|
||||
PvsTick();
|
||||
_transform.SetCoordinates(_ent, _gridCoords2);
|
||||
PvsTick();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" Version="17.0.0" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
|
||||
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
|
||||
<PackageReference Include="Pidgin" Version="2.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
85
Robust.Client.WebView/Cef/BaseRobustCefClient.cs
Normal file
85
Robust.Client.WebView/Cef/BaseRobustCefClient.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef;
|
||||
|
||||
/// <summary>
|
||||
/// Shared functionality for all <see cref="CefClient"/> implementations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Locks down a bunch of CEF functionality we absolutely do not need content to do right now.
|
||||
/// </remarks>
|
||||
internal abstract class BaseRobustCefClient : CefClient
|
||||
{
|
||||
private readonly RobustCefPrintHandler _printHandler = new();
|
||||
private readonly RobustCefPermissionHandler _permissionHandler = new();
|
||||
private readonly RobustCefDialogHandler _dialogHandler = new();
|
||||
private readonly RobustCefDragHandler _dragHandler = new();
|
||||
|
||||
protected override CefPrintHandler GetPrintHandler() => _printHandler;
|
||||
protected override CefPermissionHandler GetPermissionHandler() => _permissionHandler;
|
||||
protected override CefDialogHandler GetDialogHandler() => _dialogHandler;
|
||||
protected override CefDragHandler GetDragHandler() => _dragHandler;
|
||||
|
||||
private sealed class RobustCefPrintHandler : CefPrintHandler
|
||||
{
|
||||
protected override void OnPrintSettings(CefBrowser browser, CefPrintSettings settings, bool getDefaults)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnPrintDialog(CefBrowser browser, bool hasSelection, CefPrintDialogCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool OnPrintJob(CefBrowser browser, string documentName, string pdfFilePath, CefPrintJobCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnPrintReset(CefBrowser browser)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RobustCefPermissionHandler : CefPermissionHandler
|
||||
{
|
||||
protected override bool OnRequestMediaAccessPermission(
|
||||
CefBrowser browser,
|
||||
CefFrame frame,
|
||||
string requestingOrigin,
|
||||
CefMediaAccessPermissionTypes requestedPermissions,
|
||||
CefMediaAccessCallback callback)
|
||||
{
|
||||
callback.Cancel();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RobustCefDialogHandler : CefDialogHandler
|
||||
{
|
||||
protected override bool OnFileDialog(
|
||||
CefBrowser browser,
|
||||
CefFileDialogMode mode,
|
||||
string title,
|
||||
string defaultFilePath,
|
||||
string[] acceptFilters,
|
||||
CefFileDialogCallback callback)
|
||||
{
|
||||
callback.Cancel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RobustCefDragHandler : CefDragHandler
|
||||
{
|
||||
protected override bool OnDragEnter(CefBrowser browser, CefDragData dragData, CefDragOperationsMask mask)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDraggableRegionsChanged(CefBrowser browser, CefFrame frame, CefDraggableRegion[] regions)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
@@ -19,6 +22,8 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
var mainArgs = new CefMainArgs(argv);
|
||||
|
||||
StartWatchThread();
|
||||
|
||||
// This will block executing until the subprocess is shut down.
|
||||
var code = CefRuntime.ExecuteProcess(mainArgs, new RobustCefApp(null), IntPtr.Zero);
|
||||
|
||||
@@ -29,5 +34,44 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private static void StartWatchThread()
|
||||
{
|
||||
//
|
||||
// CEF has this nasty habit of not shutting down all its processes if the parent crashes.
|
||||
// Great!
|
||||
//
|
||||
// We use a separate thread in each CEF child process to watch the main PID.
|
||||
// If it exits, we kill ourselves after a couple seconds.
|
||||
//
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_ID") is not { } parentIdString)
|
||||
return;
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_MODULE") is not { } parentModuleString)
|
||||
return;
|
||||
|
||||
if (!int.TryParse(parentIdString, CultureInfo.InvariantCulture, out var parentId))
|
||||
return;
|
||||
|
||||
var process = Process.GetProcessById(parentId);
|
||||
if ((process.MainModule?.FileName ?? "") != parentModuleString)
|
||||
{
|
||||
process.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
new Thread(() => WatchThread(process)) { Name = "CEF Watch Thread", IsBackground = true }
|
||||
.Start();
|
||||
}
|
||||
|
||||
private static void WatchThread(Process p)
|
||||
{
|
||||
p.WaitForExit();
|
||||
|
||||
Thread.Sleep(3000);
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +76,9 @@ namespace Robust.Client.WebView.Cef
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override unsafe bool Read(IntPtr dataOut, int bytesToRead, out int bytesRead, CefResourceReadCallback callback)
|
||||
protected override bool Read(Span<byte> response, out int bytesRead, CefResourceReadCallback callback)
|
||||
{
|
||||
var byteSpan = new Span<byte>((void*) dataOut, bytesToRead);
|
||||
|
||||
bytesRead = _stream.Read(byteSpan);
|
||||
|
||||
bytesRead = _stream.Read(response);
|
||||
return bytesRead != 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,12 @@ namespace Robust.Client.WebView.Cef
|
||||
// Disable zygote on Linux.
|
||||
commandLine.AppendSwitch("--no-zygote");
|
||||
|
||||
// Work around https://bitbucket.org/chromiumembedded/cef/issues/3213/ozone-egl-initialization-does-not-have
|
||||
// Work around https://github.com/chromiumembedded/cef/issues/3213
|
||||
// Desktop GL force makes Chromium not try to load its own ANGLE/Swiftshader so load paths aren't problematic.
|
||||
if (OperatingSystem.IsLinux())
|
||||
commandLine.AppendSwitch("--use-gl", "desktop");
|
||||
// UPDATE: That bug got fixed and now this workaround breaks CEF.
|
||||
// Keeping all this comment history in case I ever wanan remember what the `--use-gl` flag is.
|
||||
//if (OperatingSystem.IsLinux())
|
||||
// commandLine.AppendSwitch("--use-gl", "desktop");
|
||||
|
||||
// commandLine.AppendSwitch("--single-process");
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using Xilium.CefGlue;
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
// Simple CEF client.
|
||||
internal sealed class RobustCefClient : CefClient
|
||||
internal sealed class RobustCefClient : BaseRobustCefClient
|
||||
{
|
||||
private readonly CefRenderHandler _renderHandler;
|
||||
private readonly CefRequestHandler _requestHandler;
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace Robust.Client.WebView.Cef
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WindowCefClient : CefClient
|
||||
private sealed class WindowCefClient : BaseRobustCefClient
|
||||
{
|
||||
private readonly CefLifeSpanHandler _lifeSpanHandler;
|
||||
private readonly CefRequestHandler _requestHandler;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
@@ -85,6 +86,10 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
_app = new RobustCefApp(_sawmill);
|
||||
|
||||
var process = Process.GetCurrentProcess();
|
||||
Environment.SetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_ID", process.Id.ToString());
|
||||
Environment.SetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_MODULE", process.MainModule?.FileName ?? "");
|
||||
|
||||
// So these arguments look like nonsense, but it turns out CEF is just *like that*.
|
||||
// The first argument is literally nonsense, but it needs to be there as otherwise the second argument doesn't apply
|
||||
// The second argument turns off CEF's bullshit error handling, which breaks dotnet's error handling.
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
|
||||
<PackageReference Include="Robust.Natives.Cef" Version="102.0.9" />
|
||||
<PackageReference Include="JetBrains.Annotations" />
|
||||
<PackageReference Include="Robust.Natives.Cef" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
28
Robust.Client.WebView/TestBrowseWindow.cs
Normal file
28
Robust.Client.WebView/TestBrowseWindow.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Client.WebView;
|
||||
|
||||
internal sealed class TestBrowseWindow : DefaultWindow
|
||||
{
|
||||
protected override Vector2 ContentsMinimumSize => new Vector2(640, 480);
|
||||
|
||||
public TestBrowseWindow()
|
||||
{
|
||||
var wv = new WebViewControl();
|
||||
wv.Url = "https://spacestation14.io";
|
||||
|
||||
Contents.AddChild(wv);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TestBrowseWindowCommand : LocalizedCommands
|
||||
{
|
||||
public override string Command => "test_browse_window";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
new TestBrowseWindow().Open();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Robust.Client.WebView.Headless;
|
||||
using Robust.Client.WebViewHook;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
[assembly: WebViewManagerImpl(typeof(WebViewManager))]
|
||||
@@ -22,6 +23,9 @@ namespace Robust.Client.WebView
|
||||
var cfg = dependencies.Resolve<IConfigurationManagerInternal>();
|
||||
cfg.LoadCVarsFromAssembly(typeof(WebViewManager).Assembly);
|
||||
|
||||
var refl = dependencies.Resolve<IReflectionManager>();
|
||||
refl.LoadAssemblies(typeof(WebViewManager).Assembly);
|
||||
|
||||
dependencies.RegisterInstance<IWebViewManager>(this);
|
||||
dependencies.RegisterInstance<IWebViewManagerInternal>(this);
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
@@ -37,7 +39,12 @@ namespace Robust.Client.Animations
|
||||
|
||||
var keyFrame = KeyFrames[keyFrameIndex];
|
||||
|
||||
SoundSystem.Play(keyFrame.Resource, Filter.Local(), entity, keyFrame.AudioParamsFunc.Invoke());
|
||||
var audioParams = keyFrame.AudioParamsFunc.Invoke();
|
||||
var audio = new SoundPathSpecifier(keyFrame.Resource)
|
||||
{
|
||||
Params = audioParams
|
||||
};
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
|
||||
}
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
|
||||
58
Robust.Client/Audio/AudioManager.ALDisposeQueues.cs
Normal file
58
Robust.Client/Audio/AudioManager.ALDisposeQueues.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Concurrent;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
internal partial class AudioManager
|
||||
{
|
||||
// Used to track audio sources that were disposed in the finalizer thread,
|
||||
// so we need to properly send them off in the main thread.
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
|
||||
|
||||
public void FlushALDisposeQueues()
|
||||
{
|
||||
// Clear out finalized audio sources.
|
||||
while (_sourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_audioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized buffered audio sources.
|
||||
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_bufferedAudioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized audio buffers.
|
||||
while (_bufferDisposeQueue.TryDequeue(out var handle))
|
||||
{
|
||||
AL.DeleteBuffer(handle);
|
||||
_checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
internal void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
|
||||
{
|
||||
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
internal void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
|
||||
{
|
||||
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
internal void DeleteAudioBufferOnMainThread(int bufferHandle)
|
||||
{
|
||||
_bufferDisposeQueue.Enqueue(bufferHandle);
|
||||
}
|
||||
}
|
||||
374
Robust.Client/Audio/AudioManager.Public.cs
Normal file
374
Robust.Client/Audio/AudioManager.Public.cs
Normal file
@@ -0,0 +1,374 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.AudioLoading;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
internal partial class AudioManager
|
||||
{
|
||||
private float _zOffset;
|
||||
|
||||
public void SetZOffset(float offset)
|
||||
{
|
||||
_zOffset = offset;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance)
|
||||
{
|
||||
switch (_attenuation)
|
||||
{
|
||||
case Attenuation.LinearDistance:
|
||||
return 1 - rolloffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance);
|
||||
case Attenuation.LinearDistanceClamped:
|
||||
distance = MathF.Max(referenceDistance, MathF.Min(distance, maxDistance));
|
||||
return 1 - rolloffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance);
|
||||
default:
|
||||
// TODO: If you see this you can implement
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializePostWindowing()
|
||||
{
|
||||
_gameThread = Thread.CurrentThread;
|
||||
InitializeAudio();
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
DisposeAllAudio();
|
||||
|
||||
if (_openALContext != ALContext.Null)
|
||||
{
|
||||
ALC.MakeContextCurrent(ALContext.Null);
|
||||
|
||||
ALC.DestroyContext(_openALContext);
|
||||
}
|
||||
|
||||
if (_openALDevice != IntPtr.Zero)
|
||||
{
|
||||
ALC.CloseDevice(_openALDevice);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
AL.Listener(ALListener3f.Velocity, velocity.X, velocity.Y, 0f);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetPosition(Vector2 position)
|
||||
{
|
||||
AL.Listener(ALListener3f.Position, position.X, position.Y, _zOffset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetRotation(Angle angle)
|
||||
{
|
||||
var vec = angle.ToVec();
|
||||
|
||||
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
|
||||
var at = new OpenTK.Mathematics.Vector3(0f, 0f, -1f);
|
||||
var up = new OpenTK.Mathematics.Vector3(vec.Y, vec.X, 0f);
|
||||
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, vec.X, vec.Y, 0});
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
var vorbis = AudioLoaderOgg.LoadAudioData(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
// NVorbis only supports loading into floats.
|
||||
// If this becomes a problem due to missing extension support (doubt it but ok),
|
||||
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
|
||||
if (vorbis.Channels == 1)
|
||||
{
|
||||
format = ALFormat.Mono16;
|
||||
}
|
||||
else if (vorbis.Channels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo16;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (short* ptr = vorbis.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(short),
|
||||
(int) vorbis.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
|
||||
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
var wav = AudioLoaderWav.LoadAudioData(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
if (wav.BitsPerSample == 16)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono16;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo16;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else if (wav.BitsPerSample == 8)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono8;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo8;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* ptr = wav.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
|
||||
return new AudioStream(handle, length, wav.NumChannels, name);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var fmt = channels switch
|
||||
{
|
||||
1 => ALFormat.Mono16,
|
||||
2 => ALFormat.Stereo16,
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(channels), "Only stereo and mono is currently supported")
|
||||
};
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
_checkAlError();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (short* ptr = samples)
|
||||
{
|
||||
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
return new AudioStream(handle, length, channels, name);
|
||||
}
|
||||
|
||||
public void SetMasterGain(float newGain)
|
||||
{
|
||||
if (newGain < 0f)
|
||||
{
|
||||
OpenALSawmill.Error("Tried to set master gain below 0, clamping to 0");
|
||||
AL.Listener(ALListenerf.Gain, 0f);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
#region Platform hack for MacOS
|
||||
// HACK/BUG: Apple's OpenAL implementation has a bug where values of 0f for listener gain don't actually
|
||||
// HACK/BUG: prevent sound playback. Workaround is to cap the minimum gain at a value just above 0.
|
||||
if (OperatingSystem.IsMacOS() && newGain == 0f)
|
||||
{
|
||||
OpenALSawmill.Verbose("Not setting gain to 0 because Apple can't write an OpenAL implementation");
|
||||
AL.Listener(ALListenerf.Gain, float.Epsilon);
|
||||
return;
|
||||
}
|
||||
#endregion Platform hack for MacOS
|
||||
|
||||
AL.Listener(ALListenerf.Gain, newGain);
|
||||
}
|
||||
|
||||
public void SetAttenuation(Attenuation attenuation)
|
||||
{
|
||||
switch (attenuation)
|
||||
{
|
||||
case Attenuation.NoAttenuation:
|
||||
AL.DistanceModel(ALDistanceModel.None);
|
||||
break;
|
||||
case Attenuation.InverseDistance:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistance);
|
||||
break;
|
||||
case Attenuation.InverseDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
|
||||
break;
|
||||
case Attenuation.LinearDistance:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistance);
|
||||
break;
|
||||
case Attenuation.LinearDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
|
||||
break;
|
||||
case Attenuation.ExponentDistance:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistance);
|
||||
break;
|
||||
case Attenuation.ExponentDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
|
||||
}
|
||||
|
||||
_attenuation = attenuation;
|
||||
OpenALSawmill.Info($"Set audio attenuation to {attenuation.ToString()}");
|
||||
}
|
||||
|
||||
internal void RemoveAudioSource(int handle)
|
||||
{
|
||||
_audioSources.Remove(handle);
|
||||
}
|
||||
|
||||
internal void RemoveBufferedAudioSource(int handle)
|
||||
{
|
||||
_bufferedAudioSources.Remove(handle);
|
||||
}
|
||||
|
||||
public IAudioSource? CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
|
||||
if (!AL.IsSource(source))
|
||||
{
|
||||
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
// TODO: This really shouldn't be indexing based on the ClydeHandle...
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle);
|
||||
|
||||
var audioSource = new AudioSource(this, source, stream);
|
||||
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
|
||||
ApplyDefaultParams(audioSource);
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IBufferedAudioSource? IAudioInternal.CreateBufferedAudioSource(int buffers, bool floatAudio=false)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
|
||||
if (!AL.IsSource(source))
|
||||
{
|
||||
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
|
||||
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
|
||||
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
|
||||
ApplyDefaultParams(audioSource);
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
private void ApplyDefaultParams(IAudioSource source)
|
||||
{
|
||||
source.MaxDistance = AudioParams.Default.MaxDistance;
|
||||
source.Pitch = AudioParams.Default.Pitch;
|
||||
source.ReferenceDistance = AudioParams.Default.ReferenceDistance;
|
||||
source.RolloffFactor = AudioParams.Default.RolloffFactor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopAllAudio()
|
||||
{
|
||||
foreach (var source in _audioSources.Values)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Playing = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var source in _bufferedAudioSources.Values)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Playing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DisposeAllAudio()
|
||||
{
|
||||
// TODO: Do we even need to stop?
|
||||
foreach (var source in _audioSources.Values)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_audioSources.Clear();
|
||||
|
||||
foreach (var source in _bufferedAudioSources.Values)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_bufferedAudioSources.Clear();
|
||||
}
|
||||
}
|
||||
173
Robust.Client/Audio/AudioManager.cs
Normal file
173
Robust.Client/Audio/AudioManager.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
internal sealed partial class AudioManager : IAudioInternal
|
||||
{
|
||||
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
|
||||
|
||||
private Thread? _gameThread;
|
||||
|
||||
private ALDevice _openALDevice;
|
||||
private ALContext _openALContext;
|
||||
|
||||
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
|
||||
new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
|
||||
new();
|
||||
|
||||
private readonly HashSet<string> _alcDeviceExtensions = new();
|
||||
private readonly HashSet<string> _alContextExtensions = new();
|
||||
private Attenuation _attenuation;
|
||||
|
||||
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
|
||||
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
|
||||
|
||||
internal bool IsEfxSupported;
|
||||
|
||||
internal ISawmill OpenALSawmill = default!;
|
||||
|
||||
private void _audioCreateContext()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
|
||||
}
|
||||
|
||||
ALC.MakeContextCurrent(_openALContext);
|
||||
_checkAlcError(_openALDevice);
|
||||
_checkAlError();
|
||||
|
||||
// Load up AL context extensions.
|
||||
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alContextExtensions.Add(extension);
|
||||
}
|
||||
|
||||
OpenALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
|
||||
OpenALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
|
||||
OpenALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
|
||||
}
|
||||
|
||||
private bool _audioOpenDevice()
|
||||
{
|
||||
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
|
||||
|
||||
// Open device.
|
||||
if (!string.IsNullOrEmpty(preferredDevice))
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(preferredDevice);
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
|
||||
preferredDevice, ALC.GetError(ALDevice.Null));
|
||||
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
|
||||
_checkAlcError(_openALDevice);
|
||||
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
OpenALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load up ALC extensions.
|
||||
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alcDeviceExtensions.Add(extension);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void InitializeAudio()
|
||||
{
|
||||
OpenALSawmill = _logMan.GetSawmill("clyde.oal");
|
||||
|
||||
if (!_audioOpenDevice())
|
||||
return;
|
||||
|
||||
// Create OpenAL context.
|
||||
_audioCreateContext();
|
||||
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
|
||||
}
|
||||
|
||||
internal bool IsMainThread()
|
||||
{
|
||||
return Thread.CurrentThread == _gameThread;
|
||||
}
|
||||
|
||||
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
|
||||
{
|
||||
if (handles.filterHandle != 0)
|
||||
EFX.DeleteFilter(handles.filterHandle);
|
||||
}
|
||||
|
||||
private void _checkAlcError(ALDevice device,
|
||||
[CallerMemberName] string callerMember = "",
|
||||
[CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = ALC.GetError(device);
|
||||
if (error != AlcError.NoError)
|
||||
{
|
||||
OpenALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Like _checkAlError but allows custom data to be passed in as relevant.
|
||||
/// </summary>
|
||||
internal void LogALError(string message, [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = AL.GetError();
|
||||
if (error != ALError.NoError)
|
||||
{
|
||||
OpenALSawmill.Error("[{0}:{1}] AL error: {2}, {3}. Stacktrace is {4}", callerMember, callerLineNumber, error, message, Environment.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
public void _checkAlError([CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = AL.GetError();
|
||||
if (error != ALError.NoError)
|
||||
{
|
||||
OpenALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoadedAudioSample
|
||||
{
|
||||
public readonly int BufferHandle;
|
||||
|
||||
public LoadedAudioSample(int bufferHandle)
|
||||
{
|
||||
BufferHandle = bufferHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Robust.Client/Audio/AudioOverlay.cs
Normal file
89
Robust.Client/Audio/AudioOverlay.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Debug overlay for audio.
|
||||
/// </summary>
|
||||
public sealed class AudioOverlay : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
private IEntityManager _entManager;
|
||||
private IPlayerManager _playerManager;
|
||||
private AudioSystem _audio;
|
||||
private SharedTransformSystem _transform;
|
||||
|
||||
private Font _font;
|
||||
|
||||
public AudioOverlay(IEntityManager entManager, IPlayerManager playerManager, IResourceCache cache, AudioSystem audio, SharedTransformSystem transform)
|
||||
{
|
||||
_entManager = entManager;
|
||||
_playerManager = playerManager;
|
||||
_audio = audio;
|
||||
_transform = transform;
|
||||
|
||||
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var localPlayer = _playerManager.LocalEntity;
|
||||
|
||||
if (args.ViewportControl == null || localPlayer == null)
|
||||
return;
|
||||
|
||||
var screenHandle = args.ScreenHandle;
|
||||
var output = new StringBuilder();
|
||||
var listenerPos = _entManager.GetComponent<TransformComponent>(localPlayer.Value).MapPosition;
|
||||
|
||||
if (listenerPos.MapId != args.MapId)
|
||||
return;
|
||||
|
||||
var query = _entManager.AllEntityQueryEnumerator<AudioComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
var mapId = MapId.Nullspace;
|
||||
var audioPos = Vector2.Zero;
|
||||
|
||||
if (_entManager.TryGetComponent<TransformComponent>(uid, out var xform))
|
||||
{
|
||||
mapId = xform.MapID;
|
||||
audioPos = _transform.GetWorldPosition(uid);
|
||||
}
|
||||
|
||||
if (mapId != args.MapId)
|
||||
continue;
|
||||
|
||||
var screenPos = args.ViewportControl.WorldToScreen(audioPos);
|
||||
var distance = audioPos - listenerPos.Position;
|
||||
var posOcclusion = _audio.GetOcclusion(listenerPos, distance, distance.Length(), uid);
|
||||
|
||||
output.Clear();
|
||||
output.AppendLine("Audio Source");
|
||||
output.AppendLine("Runtime:");
|
||||
output.AppendLine($"- Occlusion: {posOcclusion:0.0000}");
|
||||
output.AppendLine("Params:");
|
||||
output.AppendLine($"- Volume: {comp.Volume:0.0000}");
|
||||
output.AppendLine($"- Reference distance: {comp.ReferenceDistance}");
|
||||
output.AppendLine($"- Max distance: {comp.MaxDistance}");
|
||||
var outputText = output.ToString().Trim();
|
||||
var dimensions = screenHandle.GetDimensions(_font, outputText, 1f);
|
||||
var buffer = new Vector2(3f, 3f);
|
||||
screenHandle.DrawRect(new UIBox2(screenPos - buffer, screenPos + dimensions + buffer), new Color(39, 39, 48));
|
||||
screenHandle.DrawString(_font, screenPos, outputText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Graphics;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
|
||||
/// </summary>
|
||||
public sealed class AudioStream
|
||||
{
|
||||
public TimeSpan Length { get; }
|
||||
internal ClydeHandle? ClydeHandle { get; }
|
||||
internal IClydeHandle? ClydeHandle { get; }
|
||||
public string? Name { get; }
|
||||
public string? Title { get; }
|
||||
public string? Artist { get; }
|
||||
public int ChannelCount { get; }
|
||||
|
||||
internal AudioStream(ClydeHandle handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
{
|
||||
ClydeHandle = handle;
|
||||
Length = length;
|
||||
|
||||
76
Robust.Client/Audio/AudioSystem.Effects.cs
Normal file
76
Robust.Client/Audio/AudioSystem.Effects.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Client.Audio.Effects;
|
||||
using Robust.Shared.Audio.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
public sealed partial class AudioSystem
|
||||
{
|
||||
protected override void InitializeEffect()
|
||||
{
|
||||
base.InitializeEffect();
|
||||
SubscribeLocalEvent<AudioEffectComponent, ComponentAdd>(OnEffectAdd);
|
||||
SubscribeLocalEvent<AudioEffectComponent, ComponentShutdown>(OnEffectShutdown);
|
||||
|
||||
SubscribeLocalEvent<AudioAuxiliaryComponent, ComponentAdd>(OnAuxiliaryAdd);
|
||||
SubscribeLocalEvent<AudioAuxiliaryComponent, AfterAutoHandleStateEvent>(OnAuxiliaryAuto);
|
||||
}
|
||||
|
||||
private void OnEffectAdd(EntityUid uid, AudioEffectComponent component, ComponentAdd args)
|
||||
{
|
||||
var effect = new AudioEffect(_audio);
|
||||
component.Effect = effect;
|
||||
}
|
||||
|
||||
private void OnEffectShutdown(EntityUid uid, AudioEffectComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (component.Effect is AudioEffect effect)
|
||||
{
|
||||
effect.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAuxiliaryAdd(EntityUid uid, AudioAuxiliaryComponent component, ComponentAdd args)
|
||||
{
|
||||
component.Auxiliary = new AuxiliaryAudio();
|
||||
}
|
||||
|
||||
private void OnAuxiliaryAuto(EntityUid uid, AudioAuxiliaryComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (TryComp<AudioEffectComponent>(component.Effect, out var effectComp))
|
||||
{
|
||||
component.Auxiliary.SetEffect(effectComp.Effect);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Auxiliary.SetEffect(null);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetAuxiliary(EntityUid uid, AudioComponent audio, EntityUid? auxUid)
|
||||
{
|
||||
base.SetAuxiliary(uid, audio, auxUid);
|
||||
if (TryComp<AudioAuxiliaryComponent>(audio.Auxiliary, out var auxComp))
|
||||
{
|
||||
audio.Source.SetAuxiliary(auxComp.Auxiliary);
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.Source.SetAuxiliary(null);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetEffect(EntityUid auxUid, AudioAuxiliaryComponent aux, EntityUid? effectUid)
|
||||
{
|
||||
base.SetEffect(auxUid, aux, effectUid);
|
||||
if (TryComp<AudioEffectComponent>(aux.Effect, out var effectComp))
|
||||
{
|
||||
aux.Auxiliary.SetEffect(effectComp.Effect);
|
||||
}
|
||||
else
|
||||
{
|
||||
aux.Auxiliary.SetEffect(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
692
Robust.Client/Audio/AudioSystem.cs
Normal file
692
Robust.Client/Audio/AudioSystem.cs
Normal file
@@ -0,0 +1,692 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Components;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
public sealed partial class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
/*
|
||||
* There's still a lot more OpenAL can do in terms of filters, auxiliary slots, etc.
|
||||
* but exposing the whole thing in an easy way is a lot of effort.
|
||||
*/
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IParallelManager _parMan = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
[Dependency] private readonly IAudioInternal _audio = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Per-tick cache of relevant streams.
|
||||
/// </summary>
|
||||
private readonly List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> _streams = new();
|
||||
private EntityUid? _listenerGrid;
|
||||
private UpdateAudioJob _updateAudioJob;
|
||||
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
private float _maxRayLength;
|
||||
|
||||
public override float ZOffset
|
||||
{
|
||||
get => _zOffset;
|
||||
protected set
|
||||
{
|
||||
_zOffset = value;
|
||||
_audio.SetZOffset(value);
|
||||
|
||||
var query = AllEntityQuery<AudioComponent>();
|
||||
|
||||
while (query.MoveNext(out var audio))
|
||||
{
|
||||
// Pythagoras back to normal then adjust.
|
||||
var maxDistance = GetAudioDistance(audio.Params.MaxDistance);
|
||||
var refDistance = GetAudioDistance(audio.Params.ReferenceDistance);
|
||||
|
||||
audio.MaxDistance = maxDistance;
|
||||
audio.ReferenceDistance = refDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float _zOffset;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_updateAudioJob = new UpdateAudioJob
|
||||
{
|
||||
System = this,
|
||||
Streams = _streams,
|
||||
};
|
||||
|
||||
UpdatesOutsidePrediction = true;
|
||||
// Need to run after Eye updates so we have an accurate listener position.
|
||||
UpdatesAfter.Add(typeof(EyeSystem));
|
||||
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
|
||||
SubscribeLocalEvent<AudioComponent, ComponentStartup>(OnAudioStartup);
|
||||
SubscribeLocalEvent<AudioComponent, ComponentShutdown>(OnAudioShutdown);
|
||||
SubscribeLocalEvent<AudioComponent, EntityPausedEvent>(OnAudioPaused);
|
||||
SubscribeLocalEvent<AudioComponent, AfterAutoHandleStateEvent>(OnAudioState);
|
||||
|
||||
// Replay stuff
|
||||
SubscribeNetworkEvent<PlayAudioGlobalMessage>(OnGlobalAudio);
|
||||
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
|
||||
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
|
||||
|
||||
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
|
||||
}
|
||||
|
||||
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
ApplyAudioParams(component.Params, component);
|
||||
component.Source.Global = component.Global;
|
||||
|
||||
if (TryComp<AudioAuxiliaryComponent>(component.Auxiliary, out var auxComp))
|
||||
{
|
||||
component.Source.SetAuxiliary(auxComp.Auxiliary);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Source.SetAuxiliary(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the volume for the entire game.
|
||||
/// </summary>
|
||||
public void SetMasterVolume(float value)
|
||||
{
|
||||
_audio.SetMasterGain(value);
|
||||
}
|
||||
|
||||
private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args)
|
||||
{
|
||||
component.Pause();
|
||||
}
|
||||
|
||||
protected override void OnAudioUnpaused(EntityUid uid, AudioComponent component, ref EntityUnpausedEvent args)
|
||||
{
|
||||
base.OnAudioUnpaused(uid, component, ref args);
|
||||
component.StartPlaying();
|
||||
}
|
||||
|
||||
private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args)
|
||||
{
|
||||
if (!Timing.ApplyingState && !Timing.IsFirstTimePredicted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Source has already been set
|
||||
if (component.Loaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetAudio(component.FileName, out var audioResource))
|
||||
{
|
||||
Log.Error($"Error creating audio source for {audioResource}, can't find file {component.FileName}");
|
||||
return;
|
||||
}
|
||||
|
||||
SetupSource(component, audioResource);
|
||||
component.Loaded = true;
|
||||
}
|
||||
|
||||
private void SetupSource(AudioComponent component, AudioResource audioResource, TimeSpan? length = null)
|
||||
{
|
||||
var source = _audio.CreateAudioSource(audioResource);
|
||||
|
||||
if (source == null)
|
||||
{
|
||||
Log.Error($"Error creating audio source for {audioResource}");
|
||||
DebugTools.Assert(false);
|
||||
source = component.Source;
|
||||
}
|
||||
|
||||
component.Source = source;
|
||||
|
||||
// Need to set all initial data for first frame.
|
||||
ApplyAudioParams(component.Params, component);
|
||||
source.Global = component.Global;
|
||||
|
||||
// Don't play until first frame so occlusion etc. are correct.
|
||||
component.Gain = 0f;
|
||||
|
||||
length ??= GetAudioLength(component.FileName);
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % length.Value.TotalSeconds;
|
||||
|
||||
if (offset > 0)
|
||||
{
|
||||
component.PlaybackPosition = (float) offset;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAudioShutdown(EntityUid uid, AudioComponent component, ComponentShutdown args)
|
||||
{
|
||||
// Breaks with prediction?
|
||||
component.Source.Dispose();
|
||||
}
|
||||
|
||||
private void OnAudioAttenuation(int obj)
|
||||
{
|
||||
_audio.SetAttenuation((Attenuation) obj);
|
||||
}
|
||||
|
||||
private void OnRaycastLengthChanged(float value)
|
||||
{
|
||||
_maxRayLength = value;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
var eye = _eyeManager.CurrentEye;
|
||||
var localEntity = _playerManager.LocalEntity;
|
||||
Vector2 listenerVelocity;
|
||||
|
||||
if (localEntity != null)
|
||||
listenerVelocity = _physics.GetMapLinearVelocity(localEntity.Value);
|
||||
else
|
||||
listenerVelocity = Vector2.Zero;
|
||||
|
||||
_audio.SetVelocity(listenerVelocity);
|
||||
_audio.SetRotation(eye.Rotation);
|
||||
_audio.SetPosition(eye.Position.Position);
|
||||
|
||||
var ourPos = GetListenerCoordinates();
|
||||
|
||||
var query = AllEntityQuery<AudioComponent, TransformComponent>();
|
||||
_streams.Clear();
|
||||
|
||||
while (query.MoveNext(out var uid, out var comp, out var xform))
|
||||
{
|
||||
_streams.Add((uid, comp, xform));
|
||||
}
|
||||
|
||||
_mapManager.TryFindGridAt(ourPos, out var gridUid, out _);
|
||||
_listenerGrid = gridUid == EntityUid.Invalid ? null : gridUid;
|
||||
|
||||
try
|
||||
{
|
||||
_updateAudioJob.OurPosition = ourPos;
|
||||
_parMan.ProcessNow(_updateAudioJob, _streams.Count);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while processing entity streams.");
|
||||
_runtimeLog.LogException(e, $"{nameof(AudioSystem)}.{nameof(FrameUpdate)}");
|
||||
}
|
||||
}
|
||||
|
||||
public MapCoordinates GetListenerCoordinates()
|
||||
{
|
||||
return _eyeManager.CurrentEye.Position;
|
||||
}
|
||||
|
||||
private void ProcessStream(EntityUid entity, AudioComponent component, TransformComponent xform, MapCoordinates listener)
|
||||
{
|
||||
// TODO:
|
||||
// I Originally tried to be fancier here but it caused audio issues so just trying
|
||||
// to replicate the old behaviour for now.
|
||||
if (!component.Started)
|
||||
{
|
||||
component.Started = true;
|
||||
component.StartPlaying();
|
||||
}
|
||||
|
||||
// If it's global but on another map (that isn't nullspace) then stop playing it.
|
||||
if (component.Global)
|
||||
{
|
||||
if (xform.MapID != MapId.Nullspace && listener.MapId != xform.MapID)
|
||||
{
|
||||
component.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
// Resume playing.
|
||||
component.Volume = component.Params.Volume;
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-global sounds, stop playing if on another map.
|
||||
// Not relevant to us.
|
||||
if (listener.MapId != xform.MapID)
|
||||
{
|
||||
component.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 worldPos;
|
||||
var gridUid = xform.ParentUid;
|
||||
|
||||
// Handle grid audio differently by using nearest-edge instead of entity centre.
|
||||
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
|
||||
{
|
||||
// It's our grid so max volume.
|
||||
if (_listenerGrid == gridUid)
|
||||
{
|
||||
component.Volume = component.Params.Volume;
|
||||
component.Occlusion = 0f;
|
||||
component.Position = listener.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Need a grid-optimised version because this is gonna be expensive.
|
||||
// Just to avoid clipping on and off grid or nearestPoint changing we'll
|
||||
// always set the sound to listener's pos, we'll just manually do gain ourselves.
|
||||
if (_physics.TryGetNearest(gridUid, listener, out _, out var gridDistance))
|
||||
{
|
||||
// Out of range
|
||||
if (gridDistance > component.MaxDistance)
|
||||
{
|
||||
component.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsGain = VolumeToGain(component.Params.Volume);
|
||||
|
||||
// Thought I'd never have to manually calculate gain again but this is the least
|
||||
// unpleasant audio I could get at the moment.
|
||||
component.Gain = paramsGain * _audio.GetAttenuationGain(
|
||||
gridDistance,
|
||||
component.Params.RolloffFactor,
|
||||
component.Params.ReferenceDistance,
|
||||
component.Params.MaxDistance);
|
||||
component.Position = listener.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't get nearest point so don't play anymore.
|
||||
component.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
worldPos = _xformSys.GetWorldPosition(entity);
|
||||
component.Volume = component.Params.Volume;
|
||||
|
||||
// Max distance check
|
||||
var delta = worldPos - listener.Position;
|
||||
var distance = delta.Length();
|
||||
|
||||
// Out of range so just clip it for us.
|
||||
if (distance > component.MaxDistance)
|
||||
{
|
||||
// Still keeps the source playing, just with no volume.
|
||||
component.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (distance > 0f && distance < 0.01f)
|
||||
{
|
||||
worldPos = listener.Position;
|
||||
delta = Vector2.Zero;
|
||||
distance = 0f;
|
||||
}
|
||||
|
||||
// Update audio occlusion
|
||||
var occlusion = GetOcclusion(listener, delta, distance, entity);
|
||||
component.Occlusion = occlusion;
|
||||
|
||||
// Update audio positions.
|
||||
component.Position = worldPos;
|
||||
|
||||
// Make race cars go NYYEEOOOOOMMMMM
|
||||
if (_physicsQuery.TryGetComponent(entity, out var physicsComp))
|
||||
{
|
||||
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
|
||||
// inefficient.
|
||||
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform);
|
||||
component.Velocity = velocity;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audio occlusion from the target audio entity to the listener's position.
|
||||
/// </summary>
|
||||
public float GetOcclusion(MapCoordinates listener, Vector2 delta, float distance, EntityUid? ignoredEnt = null)
|
||||
{
|
||||
float occlusion = 0;
|
||||
|
||||
if (distance > 0.1)
|
||||
{
|
||||
var rayLength = MathF.Min(distance, _maxRayLength);
|
||||
var ray = new CollisionRay(listener.Position, delta / distance, OcclusionCollisionMask);
|
||||
occlusion = _physics.IntersectRayPenetration(listener.MapId, ray, rayLength, ignoredEnt);
|
||||
}
|
||||
|
||||
return occlusion;
|
||||
}
|
||||
|
||||
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
|
||||
{
|
||||
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
|
||||
return true;
|
||||
|
||||
Log.Error($"Server tried to play audio file {filename} which does not exist.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetAudio(AudioStream stream, [NotNullWhen(true)] out AudioResource? audio)
|
||||
{
|
||||
if (_resourceCache.TryGetResource(stream, out audio))
|
||||
return true;
|
||||
|
||||
Log.Error($"Server failed to play audio stream {stream.Title}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
|
||||
{
|
||||
if (Timing.IsFirstTimePredicted && sound != null)
|
||||
return PlayEntity(sound, Filter.Local(), source, false, audioParams);
|
||||
|
||||
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
|
||||
{
|
||||
if (Timing.IsFirstTimePredicted && sound != null)
|
||||
return PlayStatic(sound, Filter.Local(), coordinates, false, audioParams);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio stream globally, without position.
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
|
||||
{
|
||||
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
|
||||
component.Global = true;
|
||||
component.Source.Global = true;
|
||||
Dirty(entity, component);
|
||||
return (entity, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
|
||||
{
|
||||
FileName = filename,
|
||||
NetEntity = GetNetEntity(entity),
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio stream following an entity.
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TerminatingOrDeleted(entity))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var playing = CreateAndStartPlayingStream(audioParams, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
|
||||
|
||||
return playing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Coordinates = GetNetCoordinates(coordinates),
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio stream at a static position.
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var playing = CreateAndStartPlayingStream(audioParams, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, coordinates);
|
||||
return playing;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, entity, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
public override void LoadStream<T>(AudioComponent component, T stream)
|
||||
{
|
||||
if (stream is AudioStream audioStream)
|
||||
{
|
||||
TryGetAudio(audioStream, out var audio);
|
||||
SetupSource(component, audio!, audioStream.Length);
|
||||
component.Loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
|
||||
{
|
||||
var audioP = audioParams ?? AudioParams.Default;
|
||||
var entity = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
|
||||
var comp = SetupAudio(entity, null, audioP, stream.Length);
|
||||
LoadStream(comp, stream);
|
||||
EntityManager.InitializeAndStartEntity(entity);
|
||||
var source = comp.Source;
|
||||
|
||||
// TODO clamp the offset inside of SetPlaybackPosition() itself.
|
||||
var offset = audioP.PlayOffsetSeconds;
|
||||
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
|
||||
source.PlaybackPosition = offset;
|
||||
|
||||
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
|
||||
ApplyAudioParams(comp.Params, comp);
|
||||
source.StartPlaying();
|
||||
return (entity, comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the audioparams to the underlying audio source.
|
||||
/// </summary>
|
||||
private void ApplyAudioParams(AudioParams audioParams, IAudioSource source)
|
||||
{
|
||||
source.Pitch = audioParams.Pitch;
|
||||
source.Volume = audioParams.Volume;
|
||||
source.RolloffFactor = audioParams.RolloffFactor;
|
||||
source.MaxDistance = GetAudioDistance(audioParams.MaxDistance);
|
||||
source.ReferenceDistance = GetAudioDistance(audioParams.ReferenceDistance);
|
||||
source.Looping = audioParams.Loop;
|
||||
}
|
||||
|
||||
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
|
||||
{
|
||||
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
|
||||
}
|
||||
|
||||
private void OnEntityAudio(PlayAudioEntityMessage ev)
|
||||
{
|
||||
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
|
||||
}
|
||||
|
||||
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
|
||||
{
|
||||
PlayGlobal(ev.FileName, ev.AudioParams, false);
|
||||
}
|
||||
|
||||
protected override TimeSpan GetAudioLengthImpl(string filename)
|
||||
{
|
||||
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
private record struct UpdateAudioJob : IParallelRobustJob
|
||||
{
|
||||
public int BatchSize => 2;
|
||||
|
||||
public AudioSystem System;
|
||||
|
||||
public MapCoordinates OurPosition;
|
||||
public List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> Streams;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var comp = Streams[index];
|
||||
|
||||
System.ProcessStream(comp.Entity, comp.Component, comp.Xform, OurPosition);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
455
Robust.Client/Audio/Effects/AudioEffect.cs
Normal file
455
Robust.Client/Audio/Effects/AudioEffect.cs
Normal file
@@ -0,0 +1,455 @@
|
||||
using System;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Effects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Audio.Effects;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal sealed class AudioEffect : IAudioEffect
|
||||
{
|
||||
internal int Handle;
|
||||
|
||||
private readonly IAudioInternal _master;
|
||||
|
||||
public AudioEffect(IAudioInternal manager)
|
||||
{
|
||||
Handle = EFX.GenEffect();
|
||||
_master = manager;
|
||||
EFX.Effect(Handle, EffectInteger.EffectType, (int) EffectType.EaxReverb);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Handle != 0)
|
||||
{
|
||||
EFX.DeleteEffect(Handle);
|
||||
Handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void _checkDisposed()
|
||||
{
|
||||
if (Handle == -1)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AudioEffect));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Density
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbDensity, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbDensity, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Diffusion
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbDiffusion, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbDiffusion, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Gain
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbGain, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbGain, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float GainHF
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainHF, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbGainHF, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float GainLF
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainLF, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbGainLF, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float DecayTime
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayTime, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbDecayTime, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float DecayHFRatio
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayHFRatio, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbDecayHFRatio, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float DecayLFRatio
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayLFRatio, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbDecayLFRatio, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float ReflectionsGain
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsGain, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsGain, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float ReflectionsDelay
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsDelay, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsDelay, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Vector3 ReflectionsPan
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbReflectionsPan);
|
||||
_master._checkAlError();
|
||||
return new Vector3(value.X, value.Z, value.Y);
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
|
||||
EFX.Effect(Handle, EffectVector3.EaxReverbReflectionsPan, ref openVec);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float LateReverbGain
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbGain, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbGain, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float LateReverbDelay
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbDelay, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbDelay, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Vector3 LateReverbPan
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbLateReverbPan);
|
||||
_master._checkAlError();
|
||||
return new Vector3(value.X, value.Z, value.Y);
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
|
||||
EFX.Effect(Handle, EffectVector3.EaxReverbLateReverbPan, ref openVec);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float EchoTime
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoTime, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbEchoTime, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float EchoDepth
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoDepth, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbEchoDepth, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float ModulationTime
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationTime, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbModulationTime, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float ModulationDepth
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationDepth, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbModulationDepth, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float AirAbsorptionGainHF
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float HFReference
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbHFReference, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbHFReference, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float LFReference
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbLFReference, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbLFReference, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float RoomRolloffFactor
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int DecayHFLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.GetEffect(Handle, EffectInteger.EaxReverbDecayHFLimit, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
EFX.Effect(Handle, EffectInteger.EaxReverbDecayHFLimit, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Robust.Client/Audio/Effects/AuxiliaryAudio.cs
Normal file
32
Robust.Client/Audio/Effects/AuxiliaryAudio.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Shared.Audio.Effects;
|
||||
|
||||
namespace Robust.Client.Audio.Effects;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal sealed class AuxiliaryAudio : IAuxiliaryAudio
|
||||
{
|
||||
internal int Handle = EFX.GenAuxiliaryEffectSlot();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Handle != -1)
|
||||
{
|
||||
EFX.DeleteAuxiliaryEffectSlot(Handle);
|
||||
Handle = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetEffect(IAudioEffect? effect)
|
||||
{
|
||||
if (effect is AudioEffect audEffect)
|
||||
{
|
||||
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, audEffect.Handle);
|
||||
}
|
||||
else
|
||||
{
|
||||
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Robust.Client/Audio/HeadlessAudioManager.cs
Normal file
111
Robust.Client/Audio/HeadlessAudioManager.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.AudioLoading;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Headless client audio.
|
||||
/// </summary>
|
||||
internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void InitializePostWindowing()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Shutdown()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void FlushALDisposeQueues()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return DummyAudioSource.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio = false)
|
||||
{
|
||||
return DummyBufferedAudioSource.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetPosition(Vector2 position)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRotation(Angle angle)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetMasterGain(float newGain)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetAttenuation(Attenuation attenuation)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopAllAudio()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetZOffset(float f)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void _checkAlError(string callerMember = "", int callerLineNumber = -1)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
var metadata = AudioLoaderOgg.LoadAudioMetadata(stream);
|
||||
return AudioStreamFromMetadata(metadata, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
var metadata = AudioLoaderWav.LoadAudioMetadata(stream);
|
||||
return AudioStreamFromMetadata(metadata, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
return new AudioStream(null, length, channels, name);
|
||||
}
|
||||
|
||||
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
|
||||
{
|
||||
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
|
||||
}
|
||||
}
|
||||
63
Robust.Client/Audio/IAudioInternal.cs
Normal file
63
Robust.Client/Audio/IAudioInternal.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Handles clientside audio.
|
||||
/// </summary>
|
||||
internal interface IAudioInternal : IAudioManager
|
||||
{
|
||||
void InitializePostWindowing();
|
||||
void Shutdown();
|
||||
|
||||
/// <summary>
|
||||
/// Flushes all pending queues for disposing of AL sources.
|
||||
/// </summary>
|
||||
void FlushALDisposeQueues();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a buffered audio source.
|
||||
/// </summary>
|
||||
/// <returns>null if unable to create the source.</returns>
|
||||
IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio=false);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the velocity for the audio listener.
|
||||
/// </summary>
|
||||
void SetVelocity(Vector2 velocity);
|
||||
|
||||
/// <summary>
|
||||
/// Sets position for the audio listener.
|
||||
/// </summary>
|
||||
void SetPosition(Vector2 position);
|
||||
|
||||
/// <summary>
|
||||
/// Sets rotation for the audio listener.
|
||||
/// </summary>
|
||||
void SetRotation(Angle angle);
|
||||
|
||||
void SetAttenuation(Attenuation attenuation);
|
||||
|
||||
/// <summary>
|
||||
/// Stops all audio from playing.
|
||||
/// </summary>
|
||||
void StopAllAudio();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Z-offset for the audio listener.
|
||||
/// </summary>
|
||||
void SetZOffset(float f);
|
||||
|
||||
void _checkAlError([CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1);
|
||||
|
||||
/// <summary>
|
||||
/// Manually calculates the specified gain for an attenuation source with the specified distance.
|
||||
/// </summary>
|
||||
float GetAttenuationGain(float distance, float rolloffFactor, float referenceDistance, float maxDistance);
|
||||
}
|
||||
22
Robust.Client/Audio/IAudioManager.cs
Normal file
22
Robust.Client/Audio/IAudioManager.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Public audio API for stuff that can't go through <see cref="AudioSystem"/>
|
||||
/// </summary>
|
||||
public interface IAudioManager
|
||||
{
|
||||
IAudioSource? CreateAudioSource(AudioStream stream);
|
||||
|
||||
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
|
||||
|
||||
AudioStream LoadAudioWav(Stream stream, string? name = null);
|
||||
|
||||
AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null);
|
||||
|
||||
void SetMasterGain(float gain);
|
||||
}
|
||||
@@ -17,11 +17,9 @@ public interface IMidiManager
|
||||
bool IsAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Volume, in db.
|
||||
/// Gain of audio.
|
||||
/// </summary>
|
||||
float Volume { get; set; }
|
||||
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
float Gain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This method tries to return a midi renderer ready to be used.
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
@@ -20,7 +21,7 @@ public interface IMidiRenderer : IDisposable
|
||||
/// <summary>
|
||||
/// The buffered audio source of this renderer.
|
||||
/// </summary>
|
||||
internal IClydeBufferedAudioSource Source { get; }
|
||||
internal IBufferedAudioSource Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this renderer has been disposed or not.
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NFluidsynth;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Exceptions;
|
||||
@@ -19,7 +18,6 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Threading;
|
||||
@@ -33,25 +31,19 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
public const string SoundfontEnvironmentVariable = "ROBUST_SOUNDFONT_OVERRIDE";
|
||||
|
||||
private int _minRendererParallel;
|
||||
private float _occlusionUpdateDelay;
|
||||
private float _positionUpdateDelay;
|
||||
|
||||
[ViewVariables] private TimeSpan _nextOcclusionUpdate = TimeSpan.Zero;
|
||||
[ViewVariables] private TimeSpan _nextPositionUpdate = TimeSpan.Zero;
|
||||
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IResourceCacheInternal _resourceManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
|
||||
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
|
||||
[Dependency] private readonly IAudioInternal _audio = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
[Dependency] private readonly ILogManager _logger = default!;
|
||||
[Dependency] private readonly IParallelManager _parallel = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtime = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private AudioSystem _audioSys = default!;
|
||||
private SharedPhysicsSystem _broadPhaseSystem = default!;
|
||||
private SharedTransformSystem _xformSystem = default!;
|
||||
|
||||
public IReadOnlyList<IMidiRenderer> Renderers
|
||||
{
|
||||
@@ -78,24 +70,32 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
[ViewVariables] private readonly List<IMidiRenderer> _renderers = new();
|
||||
|
||||
// To avoid lock contention until some kind of MIDI refactor.
|
||||
private TimeSpan _nextUpdate;
|
||||
private TimeSpan _updateFrequency = TimeSpan.FromSeconds(0.25f);
|
||||
|
||||
private SemaphoreSlim _updateSemaphore = new(1);
|
||||
|
||||
private bool _alive = true;
|
||||
[ViewVariables] private Settings? _settings;
|
||||
private Thread? _midiThread;
|
||||
private ISawmill _midiSawmill = default!;
|
||||
private float _volume = 0f;
|
||||
private float _gain = 0f;
|
||||
private bool _volumeDirty = true;
|
||||
|
||||
// Not reliable until Fluidsynth is initialized!
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Volume
|
||||
public float Gain
|
||||
{
|
||||
get => _volume;
|
||||
get => _gain;
|
||||
set
|
||||
{
|
||||
if (MathHelper.CloseToPercent(_volume, value))
|
||||
var clamped = Math.Clamp(value, 0f, 1f);
|
||||
|
||||
if (MathHelper.CloseToPercent(_gain, clamped))
|
||||
return;
|
||||
|
||||
_cfgMan.SetCVar(CVars.MidiVolume, value);
|
||||
_cfgMan.SetCVar(CVars.MidiVolume, clamped);
|
||||
_volumeDirty = true;
|
||||
}
|
||||
}
|
||||
@@ -132,10 +132,9 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
|
||||
private ISawmill _fluidsynthSawmill = default!;
|
||||
private float _maxCastLength;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
private MidiUpdateJob _updateJob;
|
||||
|
||||
|
||||
public MidiManager()
|
||||
{
|
||||
@@ -148,19 +147,10 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
|
||||
{
|
||||
_volume = value;
|
||||
_gain = value;
|
||||
_volumeDirty = true;
|
||||
}, true);
|
||||
|
||||
_cfgMan.OnValueChanged(CVars.MidiMinRendererParallel,
|
||||
value => _minRendererParallel = value, true);
|
||||
|
||||
_cfgMan.OnValueChanged(CVars.MidiOcclusionUpdateDelay,
|
||||
value => _occlusionUpdateDelay = value, true);
|
||||
|
||||
_cfgMan.OnValueChanged(CVars.MidiPositionUpdateDelay,
|
||||
value => _positionUpdateDelay = value, true);
|
||||
|
||||
_midiSawmill = _logger.GetSawmill("midi");
|
||||
#if DEBUG
|
||||
_midiSawmill.Level = LogLevel.Debug;
|
||||
@@ -202,7 +192,12 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
_settings["synth.midi-bank-select"].StringValue = "gm";
|
||||
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
|
||||
|
||||
_parallel.AddAndInvokeParallelCountChanged(UpdateParallelCount);
|
||||
var midiParallel = _cfgMan.GetCVar(CVars.MidiParallelism);
|
||||
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(midiParallel) * 2048), 1, 65535);
|
||||
_settings["synth.cpu-cores"].IntValue = Math.Clamp(midiParallel, 1, 256);
|
||||
|
||||
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
|
||||
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -211,32 +206,27 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
return;
|
||||
}
|
||||
|
||||
_midiThread = new Thread(ThreadUpdate);
|
||||
_midiThread = new Thread(ThreadUpdate)
|
||||
{
|
||||
Name = "RobustToolbox MIDI Thread"
|
||||
};
|
||||
_midiThread.Start();
|
||||
|
||||
_updateJob = new MidiUpdateJob()
|
||||
{
|
||||
Manager = this,
|
||||
Renderers = _renderers,
|
||||
};
|
||||
|
||||
_audioSys = _entityManager.EntitySysManager.GetEntitySystem<AudioSystem>();
|
||||
_broadPhaseSystem = _entityManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
|
||||
_cfgMan.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
|
||||
_xformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
_entityManager.GetEntityQuery<PhysicsComponent>();
|
||||
_entityManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
FluidsynthInitialized = true;
|
||||
}
|
||||
|
||||
private void UpdateParallelCount()
|
||||
{
|
||||
if (_settings == null)
|
||||
return;
|
||||
|
||||
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(_parallel.ParallelProcessCount) * 2048), 1, 65535);
|
||||
_settings["synth.cpu-cores"].IntValue = Math.Clamp(_parallel.ParallelProcessCount, 1, 256);
|
||||
|
||||
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
|
||||
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
|
||||
}
|
||||
|
||||
private void OnRaycastLengthChanged(float value)
|
||||
{
|
||||
_maxCastLength = value;
|
||||
}
|
||||
|
||||
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
|
||||
{
|
||||
var rLevel = level switch
|
||||
@@ -273,7 +263,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
|
||||
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
|
||||
|
||||
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
@@ -351,7 +341,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
renderer.Source.SetVolume(Volume);
|
||||
renderer.Source.Gain = _gain;
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
@@ -372,112 +362,125 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
return;
|
||||
}
|
||||
|
||||
// Update positions of streams every frame.
|
||||
if (_nextUpdate > _timing.RealTime)
|
||||
return;
|
||||
|
||||
_nextUpdate = _timing.RealTime + _updateFrequency;
|
||||
|
||||
// Update positions of streams occasionally.
|
||||
// This has a lot of code duplication with AudioSystem.FrameUpdate(), and they should probably be combined somehow.
|
||||
// so TRUE
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
if (_renderers.Count == 0)
|
||||
return;
|
||||
_updateJob.OurPosition = _audioSys.GetListenerCoordinates();
|
||||
|
||||
var transQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
|
||||
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parallel.ParallelProcessCount };
|
||||
// This semaphore is here to avoid lock contention as much as possible.
|
||||
_updateSemaphore.Wait();
|
||||
|
||||
if (_renderers.Count > _minRendererParallel)
|
||||
{
|
||||
Parallel.ForEach(_renderers, opts, renderer => UpdateRenderer(renderer, transQuery, physicsQuery));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
UpdateRenderer(renderer, transQuery, physicsQuery);
|
||||
}
|
||||
}
|
||||
// The ONLY time this should be contested is with ThreadUpdate.
|
||||
// If that becomes NOT the case then just lock this, remove the semaphore, and drop the update frequency even harder.
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
_parallel.ProcessNow(_updateJob, _renderers.Count);
|
||||
|
||||
}
|
||||
|
||||
if (_nextOcclusionUpdate < _timing.RealTime)
|
||||
_nextOcclusionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_occlusionUpdateDelay));
|
||||
|
||||
if (_nextPositionUpdate < _timing.RealTime)
|
||||
_nextPositionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_positionUpdateDelay));
|
||||
_updateSemaphore.Release();
|
||||
|
||||
_volumeDirty = false;
|
||||
}
|
||||
private void UpdateRenderer(IMidiRenderer renderer, EntityQuery<TransformComponent> transQuery,
|
||||
EntityQuery<PhysicsComponent> physicsQuery)
|
||||
|
||||
private void UpdateRenderer(IMidiRenderer renderer, MapCoordinates listener)
|
||||
{
|
||||
// TODO: This should be sharing more code with AudioSystem.
|
||||
try
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
return;
|
||||
|
||||
if (_volumeDirty)
|
||||
renderer.Source.SetVolume(Volume);
|
||||
{
|
||||
renderer.Source.Gain = Gain;
|
||||
}
|
||||
|
||||
if (!renderer.Mono)
|
||||
{
|
||||
renderer.Source.SetGlobal();
|
||||
renderer.Source.Global = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_nextPositionUpdate < _timing.RealTime)
|
||||
MapCoordinates mapPos;
|
||||
|
||||
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
|
||||
{
|
||||
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
|
||||
{
|
||||
renderer.TrackingCoordinates = transQuery.GetComponent(renderer.TrackingEntity!.Value).MapPosition;
|
||||
}
|
||||
else if (renderer.TrackingCoordinates == null)
|
||||
renderer.TrackingCoordinates = _xformSystem.GetMapCoordinates(renderer.TrackingEntity.Value);
|
||||
|
||||
// Pause it if the attached entity is paused.
|
||||
if (_entityManager.IsPaused(renderer.TrackingEntity))
|
||||
{
|
||||
renderer.Source.Pause();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!renderer.Source.SetPosition(renderer.TrackingCoordinates.Value.Position))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var vel = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity!.Value,
|
||||
xformQuery: transQuery, physicsQuery: physicsQuery);
|
||||
renderer.Source.SetVelocity(vel);
|
||||
}
|
||||
else if (renderer.TrackingCoordinates == null)
|
||||
{
|
||||
renderer.Source.Pause();
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderer.TrackingCoordinates != null && renderer.TrackingCoordinates.Value.MapId == _eyeManager.CurrentMap)
|
||||
mapPos = renderer.TrackingCoordinates.Value;
|
||||
|
||||
// If it's on a different map then just mute it, not pause.
|
||||
if (mapPos.MapId == MapId.Nullspace || mapPos.MapId != listener.MapId)
|
||||
{
|
||||
if (_nextOcclusionUpdate >= _timing.RealTime)
|
||||
return;
|
||||
renderer.Source.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
var pos = renderer.TrackingCoordinates.Value;
|
||||
// Was previously muted maybe so try unmuting it?
|
||||
if (renderer.Source.Gain == 0f)
|
||||
{
|
||||
renderer.Source.Gain = Gain;
|
||||
}
|
||||
|
||||
var sourceRelative = pos.Position - _eyeManager.CurrentEye.Position.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length() > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
_eyeManager.CurrentEye.Position.Position,
|
||||
sourceRelative.Normalized(),
|
||||
OcclusionCollisionMask),
|
||||
MathF.Min(sourceRelative.Length(), _maxCastLength),
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
var worldPos = mapPos.Position;
|
||||
var delta = worldPos - listener.Position;
|
||||
var distance = delta.Length();
|
||||
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
// Update position
|
||||
// Out of range so just clip it for us.
|
||||
if (distance > renderer.Source.MaxDistance)
|
||||
{
|
||||
// Still keeps the source playing, just with no volume.
|
||||
renderer.Source.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
// Same imprecision suppression as audiosystem.
|
||||
if (distance > 0f && distance < 0.01f)
|
||||
{
|
||||
worldPos = listener.Position;
|
||||
delta = Vector2.Zero;
|
||||
distance = 0f;
|
||||
}
|
||||
|
||||
renderer.Source.Position = worldPos;
|
||||
|
||||
// Update velocity (doppler).
|
||||
if (!_entityManager.Deleted(renderer.TrackingEntity))
|
||||
{
|
||||
var velocity = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity.Value);
|
||||
renderer.Source.Velocity = velocity;
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
renderer.Source.Velocity = Vector2.Zero;
|
||||
}
|
||||
|
||||
// Update occlusion
|
||||
var occlusion = _audioSys.GetOcclusion(listener, delta, distance, renderer.TrackingEntity);
|
||||
renderer.Source.Occlusion = occlusion;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_runtime.LogException(ex, _midiSawmill.Name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -489,21 +492,39 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
lock (_renderers)
|
||||
{
|
||||
var toRemove = new ValueList<IMidiRenderer>();
|
||||
|
||||
for (var i = 0; i < _renderers.Count; i++)
|
||||
{
|
||||
var renderer = _renderers[i];
|
||||
if (!renderer.Disposed)
|
||||
{
|
||||
if (renderer.Master is { Disposed: true })
|
||||
renderer.Master = null;
|
||||
|
||||
renderer.Render();
|
||||
lock (renderer)
|
||||
{
|
||||
if (!renderer.Disposed)
|
||||
{
|
||||
if (renderer.Master is { Disposed: true })
|
||||
renderer.Master = null;
|
||||
|
||||
renderer.Render();
|
||||
}
|
||||
else
|
||||
{
|
||||
toRemove.Add(renderer);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
if (toRemove.Count > 0)
|
||||
{
|
||||
_updateSemaphore.Wait();
|
||||
|
||||
foreach (var renderer in toRemove)
|
||||
{
|
||||
renderer.InternalDispose();
|
||||
_renderers.Remove(renderer);
|
||||
}
|
||||
|
||||
_updateSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,4 +695,31 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
private record struct MidiUpdateJob : IParallelRobustJob
|
||||
{
|
||||
public int MinimumBatchParallel => 2;
|
||||
|
||||
public int BatchSize => 1;
|
||||
|
||||
public MidiManager Manager;
|
||||
|
||||
public MapCoordinates OurPosition;
|
||||
public List<IMidiRenderer> Renderers;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
// The indices shouldn't be able to be touched while this job is running, just the renderer itself getting locked.
|
||||
var renderer = Renderers[index];
|
||||
|
||||
lock (renderer)
|
||||
{
|
||||
Manager.UpdateRenderer(renderer, OurPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ using JetBrains.Annotations;
|
||||
using NFluidsynth;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
@@ -36,6 +38,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
private readonly Synth _synth;
|
||||
private readonly Sequencer _sequencer;
|
||||
private NFluidsynth.Player? _player;
|
||||
private int _playerTotalTicks;
|
||||
private MidiDriver? _driver;
|
||||
private byte _midiProgram = 1;
|
||||
private byte _midiBank = 1;
|
||||
@@ -52,8 +55,8 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
|
||||
private IMidiRenderer? _master;
|
||||
public MidiRendererState RendererState => _rendererState;
|
||||
public IClydeBufferedAudioSource Source { get; set; }
|
||||
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
|
||||
public IBufferedAudioSource Source { get; set; }
|
||||
IBufferedAudioSource IMidiRenderer.Source => Source;
|
||||
|
||||
[ViewVariables]
|
||||
public bool Disposed { get; private set; } = false;
|
||||
@@ -142,7 +145,21 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
public bool DisableProgramChangeEvent { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
|
||||
public int PlayerTotalTick
|
||||
{
|
||||
get
|
||||
{
|
||||
// GetTotalTicks is really expensive (has to iterate the entire file, not cached).
|
||||
// Slight problem with caching it ourselves: the value only becomes available when the player loads the MIDI file.
|
||||
// And that only happens after playback really starts, with the timer and synth and all that stuff.
|
||||
// So we cache it "as soon as it's available", i.e. not 0.
|
||||
// We don't care about playlists and such, so it shouldn't change anymore after.
|
||||
if (_playerTotalTicks != 0)
|
||||
return _playerTotalTicks;
|
||||
|
||||
return _playerTotalTicks = _player?.GetTotalTicks ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PlayerTick
|
||||
@@ -247,13 +264,13 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
public event Action? OnMidiPlayerFinished;
|
||||
|
||||
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono,
|
||||
IMidiManager midiManager, IClydeAudio clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
|
||||
IMidiManager midiManager, IAudioInternal clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
|
||||
{
|
||||
_midiManager = midiManager;
|
||||
_taskManager = taskManager;
|
||||
_midiSawmill = midiSawmill;
|
||||
|
||||
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true);
|
||||
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true) ?? DummyBufferedAudioSource.Instance;
|
||||
Source.SampleRate = SampleRate;
|
||||
_settings = settings;
|
||||
_soundFontLoader = soundFontLoader;
|
||||
@@ -337,6 +354,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
return false;
|
||||
}
|
||||
|
||||
_playerTotalTicks = 0;
|
||||
_player?.Dispose();
|
||||
_player = new NFluidsynth.Player(_synth);
|
||||
_player.SetPlaybackCallback(MidiPlayerEventHandler);
|
||||
@@ -375,6 +393,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
_player?.Join();
|
||||
_player?.Dispose();
|
||||
_player = null;
|
||||
_playerTotalTicks = 0;
|
||||
}
|
||||
|
||||
StopAllNotes();
|
||||
@@ -488,7 +507,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
}
|
||||
|
||||
if (!Source.IsPlaying) Source.StartPlaying();
|
||||
Source.StartPlaying();
|
||||
}
|
||||
|
||||
public void ApplyState(MidiRendererState state, bool filterChannels = false)
|
||||
|
||||
34
Robust.Client/Audio/ShowAudioCommand.cs
Normal file
34
Robust.Client/Audio/ShowAudioCommand.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Shows a debug overlay for audio sources.
|
||||
/// </summary>
|
||||
public sealed class ShowAudioCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _client = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerMgr = default!;
|
||||
public override string Command => "showaudio";
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (_overlayManager.HasOverlay<AudioOverlay>())
|
||||
_overlayManager.RemoveOverlay<AudioOverlay>();
|
||||
else
|
||||
_overlayManager.AddOverlay(new AudioOverlay(
|
||||
_entManager,
|
||||
_playerMgr,
|
||||
_client,
|
||||
_entManager.System<AudioSystem>(),
|
||||
_entManager.System<SharedTransformSystem>()));
|
||||
}
|
||||
}
|
||||
90
Robust.Client/Audio/Sources/AudioSource.cs
Normal file
90
Robust.Client/Audio/Sources/AudioSource.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio.Sources;
|
||||
|
||||
internal sealed class AudioSource : BaseAudioSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Underlying stream to the audio.
|
||||
/// </summary>
|
||||
private readonly AudioStream _sourceStream;
|
||||
|
||||
#if DEBUG
|
||||
private bool _didPositionWarning;
|
||||
#endif
|
||||
|
||||
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
|
||||
{
|
||||
_sourceStream = sourceStream;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Vector2 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSource3f.Position, out var x, out var y, out _);
|
||||
Master._checkAlError();
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = value;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
#if DEBUG
|
||||
// OpenAL doesn't seem to want to play stereo positionally.
|
||||
// Log a warning if people try to.
|
||||
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
|
||||
{
|
||||
_didPositionWarning = true;
|
||||
Master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
|
||||
_sourceStream.Name);
|
||||
// warning isn't enough, people just ignore it :(
|
||||
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
|
||||
}
|
||||
#endif
|
||||
|
||||
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
~AudioSource()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
|
||||
Master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FilterHandle != 0)
|
||||
EFX.DeleteFilter(FilterHandle);
|
||||
|
||||
AL.DeleteSource(SourceHandle);
|
||||
Master.RemoveAudioSource(SourceHandle);
|
||||
Master._checkAlError();
|
||||
}
|
||||
|
||||
FilterHandle = 0;
|
||||
SourceHandle = -1;
|
||||
}
|
||||
}
|
||||
415
Robust.Client/Audio/Sources/BaseAudioSource.cs
Normal file
415
Robust.Client/Audio/Sources/BaseAudioSource.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Client.Audio.Effects;
|
||||
using Robust.Shared.Audio.Effects;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Audio.Sources;
|
||||
|
||||
public abstract class BaseAudioSource : IAudioSource
|
||||
{
|
||||
/*
|
||||
* This may look weird having all these methods here however
|
||||
* we need to handle disposing plus checking for errors hence we get this.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Handle to the AL source.
|
||||
/// </summary>
|
||||
protected int SourceHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Source to the EFX filter if applicable.
|
||||
/// </summary>
|
||||
protected int FilterHandle;
|
||||
|
||||
internal readonly AudioManager Master;
|
||||
|
||||
/// <summary>
|
||||
/// Prior gain that was set.
|
||||
/// </summary>
|
||||
private float _gain;
|
||||
|
||||
private float _occlusion;
|
||||
|
||||
private bool IsEfxSupported => Master.IsEfxSupported;
|
||||
|
||||
internal BaseAudioSource(AudioManager master, int sourceHandle)
|
||||
{
|
||||
Master = master;
|
||||
SourceHandle = sourceHandle;
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Pause()
|
||||
{
|
||||
AL.SourcePause(SourceHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StartPlaying()
|
||||
{
|
||||
if (Playing)
|
||||
return;
|
||||
|
||||
Playing = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopPlaying()
|
||||
{
|
||||
if (!Playing)
|
||||
return;
|
||||
|
||||
Playing = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Restart()
|
||||
{
|
||||
AL.SourceRewind(SourceHandle);
|
||||
StartPlaying();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool Playing
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
var state = AL.GetSourceState(SourceHandle);
|
||||
Master._checkAlError();
|
||||
return state == ALSourceState.Playing;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
if (value)
|
||||
{
|
||||
AL.SourcePlay(SourceHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
AL.SourceStop(SourceHandle);
|
||||
}
|
||||
|
||||
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Looping
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourceb.Looping, out var ret);
|
||||
Master._checkAlError();
|
||||
return ret;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourceb.Looping, value);
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Global
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourceb.SourceRelative, out var value);
|
||||
Master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourceb.SourceRelative, value);
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Vector2 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSource3f.Position, out var x, out var y, out _);
|
||||
Master._checkAlError();
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = value;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Pitch
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourcef.Pitch, out var value);
|
||||
Master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.Pitch, value);
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Volume
|
||||
{
|
||||
get
|
||||
{
|
||||
var gain = Gain;
|
||||
var volume = SharedAudioSystem.GainToVolume(gain);
|
||||
return volume;
|
||||
}
|
||||
set => Gain = SharedAudioSystem.VolumeToGain(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Gain
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out var gain);
|
||||
Master._checkAlError();
|
||||
return gain;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
|
||||
// Default to 0 to avoid spiking audio, just means it will be muted for a frame in this case.
|
||||
priorOcclusion = _gain == 0 ? 1f : priorGain / _gain;
|
||||
}
|
||||
|
||||
_gain = value;
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
Master.LogALError($"Gain is {_gain:0.00} and priorOcclusion is {priorOcclusion:0.00}. EFX supported: {IsEfxSupported}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float MaxDistance
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourcef.MaxDistance, out var value);
|
||||
Master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.MaxDistance, value);
|
||||
Master.LogALError($"MaxDistance is {value:0.00}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float RolloffFactor
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourcef.RolloffFactor, out var value);
|
||||
Master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.RolloffFactor, value);
|
||||
Master.LogALError($"RolloffFactor is {value:0.00}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float ReferenceDistance
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourcef.ReferenceDistance, out var value);
|
||||
Master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.ReferenceDistance, value);
|
||||
Master.LogALError($"ReferenceDistance is {value:0.00}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Occlusion
|
||||
{
|
||||
get => _occlusion;
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
var cutoff = MathF.Exp(-value * 1);
|
||||
var gain = MathF.Pow(cutoff, 0.1f);
|
||||
if (IsEfxSupported)
|
||||
{
|
||||
SetOcclusionEfx(gain, cutoff);
|
||||
}
|
||||
else
|
||||
{
|
||||
gain *= gain * gain;
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * gain);
|
||||
}
|
||||
|
||||
_occlusion = value;
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float PlaybackPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourcef.SecOffset, out var value);
|
||||
Master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.SecOffset, value);
|
||||
Master._checkAlError($"Tried to set invalid playback position of {value:0.00}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Vector2 Velocity
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
AL.GetSource(SourceHandle, ALSource3f.Velocity, out var x, out var y, out _);
|
||||
Master._checkAlError();
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = value;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
void IAudioSource.SetAuxiliary(IAuxiliaryAudio? audio)
|
||||
{
|
||||
_checkDisposed();
|
||||
if (!IsEfxSupported)
|
||||
return;
|
||||
|
||||
if (audio is AuxiliaryAudio impAudio)
|
||||
{
|
||||
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, impAudio.Handle, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
EFX.Source(SourceHandle, EFXSourceInteger3.AuxiliarySendFilter, 0, 0, 0);
|
||||
}
|
||||
|
||||
Master._checkAlError();
|
||||
}
|
||||
|
||||
private void SetOcclusionEfx(float gain, float cutoff)
|
||||
{
|
||||
if (FilterHandle == 0)
|
||||
{
|
||||
FilterHandle = EFX.GenFilter();
|
||||
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
|
||||
}
|
||||
|
||||
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
|
||||
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
|
||||
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
|
||||
}
|
||||
|
||||
protected static bool AreFinite(float x, float y)
|
||||
{
|
||||
if (float.IsFinite(x) && float.IsFinite(y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
~BaseAudioSource()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected abstract void Dispose(bool disposing);
|
||||
|
||||
protected bool _isDisposed()
|
||||
{
|
||||
return SourceHandle == -1;
|
||||
}
|
||||
|
||||
protected void _checkDisposed()
|
||||
{
|
||||
if (SourceHandle == -1)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(BaseAudioSource));
|
||||
}
|
||||
}
|
||||
}
|
||||
222
Robust.Client/Audio/Sources/BufferedAudioSource.cs
Normal file
222
Robust.Client/Audio/Sources/BufferedAudioSource.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Audio.Sources;
|
||||
|
||||
internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSource
|
||||
{
|
||||
private int? SourceHandle = null;
|
||||
private int[] BufferHandles;
|
||||
private Dictionary<int, int> BufferMap = new();
|
||||
private readonly AudioManager _master;
|
||||
private bool _mono = true;
|
||||
private bool _float = false;
|
||||
private int FilterHandle;
|
||||
|
||||
public int SampleRate { get; set; } = 44100;
|
||||
|
||||
private bool IsEfxSupported => _master.IsEfxSupported;
|
||||
|
||||
public BufferedAudioSource(AudioManager master, int sourceHandle, int[] bufferHandles, bool floatAudio = false) : base(master, sourceHandle)
|
||||
{
|
||||
_master = master;
|
||||
SourceHandle = sourceHandle;
|
||||
BufferHandles = bufferHandles;
|
||||
for (int i = 0; i < BufferHandles.Length; i++)
|
||||
{
|
||||
var bufferHandle = BufferHandles[i];
|
||||
BufferMap[bufferHandle] = i;
|
||||
}
|
||||
_float = floatAudio;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Playing
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
var state = AL.GetSourceState(SourceHandle!.Value);
|
||||
_master._checkAlError();
|
||||
return state == ALSourceState.Playing;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
_checkDisposed();
|
||||
// IDK why this stackallocs but gonna leave it for now.
|
||||
AL.SourcePlay(stackalloc int[] {SourceHandle!.Value});
|
||||
_master._checkAlError();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_isDisposed())
|
||||
return;
|
||||
|
||||
AL.SourceStop(SourceHandle!.Value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~BufferedAudioSource()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (SourceHandle == null)
|
||||
return;
|
||||
|
||||
if (!_master.IsMainThread())
|
||||
{
|
||||
// We can't run this code inside another thread so tell Clyde to clear it up later.
|
||||
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
|
||||
|
||||
foreach (var handle in BufferHandles)
|
||||
{
|
||||
_master.DeleteAudioBufferOnMainThread(handle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FilterHandle != 0)
|
||||
EFX.DeleteFilter(FilterHandle);
|
||||
|
||||
AL.DeleteSource(SourceHandle.Value);
|
||||
AL.DeleteBuffers(BufferHandles);
|
||||
_master.RemoveBufferedAudioSource(SourceHandle.Value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
FilterHandle = 0;
|
||||
SourceHandle = null;
|
||||
}
|
||||
|
||||
public int GetNumberOfBuffersProcessed()
|
||||
{
|
||||
_checkDisposed();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.GetSource(SourceHandle!.Value, ALGetSourcei.BuffersProcessed, out var buffersProcessed);
|
||||
return buffersProcessed;
|
||||
}
|
||||
|
||||
public unsafe void GetBuffersProcessed(Span<int> handles)
|
||||
{
|
||||
_checkDisposed();
|
||||
var entries = Math.Min(Math.Min(handles.Length, BufferHandles.Length), GetNumberOfBuffersProcessed());
|
||||
fixed (int* ptr = handles)
|
||||
{
|
||||
AL.SourceUnqueueBuffers(SourceHandle!.Value, entries, ptr);
|
||||
}
|
||||
|
||||
for (var i = 0; i < entries; i++)
|
||||
{
|
||||
handles[i] = BufferMap[handles[i]];
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
if(_float)
|
||||
throw new InvalidOperationException("Can't write ushort numbers to buffers when buffer type is float!");
|
||||
|
||||
if (handle >= BufferHandles.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(handle),
|
||||
$"Got {handle}. Expected less than {BufferHandles.Length}");
|
||||
}
|
||||
|
||||
fixed (ushort* ptr = data)
|
||||
{
|
||||
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.Mono16 : ALFormat.Stereo16, (IntPtr) ptr,
|
||||
_mono ? data.Length / 2 * sizeof(ushort) : data.Length * sizeof(ushort), SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void WriteBuffer(int handle, ReadOnlySpan<float> data)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
if(!_float)
|
||||
throw new InvalidOperationException("Can't write float numbers to buffers when buffer type is ushort!");
|
||||
|
||||
if (handle >= BufferHandles.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(handle),
|
||||
$"Got {handle}. Expected less than {BufferHandles.Length}");
|
||||
}
|
||||
|
||||
fixed (float* ptr = data)
|
||||
{
|
||||
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.MonoFloat32Ext : ALFormat.StereoFloat32Ext, (IntPtr) ptr,
|
||||
_mono ? data.Length / 2 * sizeof(float) : data.Length * sizeof(float), SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void QueueBuffers(ReadOnlySpan<int> handles)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
Span<int> realHandles = stackalloc int[handles.Length];
|
||||
handles.CopyTo(realHandles);
|
||||
|
||||
for (var i = 0; i < realHandles.Length; i++)
|
||||
{
|
||||
var handle = realHandles[i];
|
||||
if (handle >= BufferHandles.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(handles), $"Invalid handle with index {i}!");
|
||||
realHandles[i] = BufferHandles[handle];
|
||||
}
|
||||
|
||||
fixed (int* ptr = realHandles)
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
{
|
||||
AL.SourceQueueBuffers(SourceHandle!.Value, handles.Length, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void EmptyBuffers()
|
||||
{
|
||||
_checkDisposed();
|
||||
var length = SampleRate / BufferHandles.Length * (_mono ? 1 : 2);
|
||||
|
||||
Span<int> handles = stackalloc int[BufferHandles.Length];
|
||||
|
||||
if (_float)
|
||||
{
|
||||
var empty = new float[length];
|
||||
var span = (Span<float>) empty;
|
||||
|
||||
for (var i = 0; i < BufferHandles.Length; i++)
|
||||
{
|
||||
WriteBuffer(BufferMap[BufferHandles[i]], span);
|
||||
handles[i] = BufferMap[BufferHandles[i]];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var empty = new ushort[length];
|
||||
var span = (Span<ushort>) empty;
|
||||
|
||||
for (var i = 0; i < BufferHandles.Length; i++)
|
||||
{
|
||||
WriteBuffer(BufferMap[BufferHandles[i]], span);
|
||||
handles[i] = BufferMap[BufferHandles[i]];
|
||||
}
|
||||
}
|
||||
|
||||
QueueBuffers(handles);
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ namespace Robust.Client
|
||||
|
||||
// Don't invoke PlayerLeaveServer if PlayerJoinedServer & GameStartedSetup hasn't been called yet.
|
||||
if (RunLevel > ClientRunLevel.Connecting)
|
||||
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
|
||||
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalSession));
|
||||
|
||||
LastDisconnectReason = args.Reason;
|
||||
GameStoppedReset();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Audio.Midi;
|
||||
using Robust.Client.Configuration;
|
||||
using Robust.Client.Console;
|
||||
@@ -6,7 +7,6 @@ using Robust.Client.Debugging;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Audio;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
@@ -107,8 +107,8 @@ namespace Robust.Client
|
||||
deps.Register<IClyde, ClydeHeadless>();
|
||||
deps.Register<IClipboardManager, ClydeHeadless>();
|
||||
deps.Register<IClydeInternal, ClydeHeadless>();
|
||||
deps.Register<IClydeAudio, ClydeAudioHeadless>();
|
||||
deps.Register<IClydeAudioInternal, ClydeAudioHeadless>();
|
||||
deps.Register<IAudioManager, HeadlessAudioManager>();
|
||||
deps.Register<IAudioInternal, HeadlessAudioManager>();
|
||||
deps.Register<IInputManager, InputManager>();
|
||||
deps.Register<IFileDialogManager, DummyFileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpenerDummy>();
|
||||
@@ -117,8 +117,8 @@ namespace Robust.Client
|
||||
deps.Register<IClyde, Clyde>();
|
||||
deps.Register<IClipboardManager, Clyde>();
|
||||
deps.Register<IClydeInternal, Clyde>();
|
||||
deps.Register<IClydeAudio, FallbackProxyClydeAudio>();
|
||||
deps.Register<IClydeAudioInternal, FallbackProxyClydeAudio>();
|
||||
deps.Register<IAudioManager, AudioManager>();
|
||||
deps.Register<IAudioInternal, AudioManager>();
|
||||
deps.Register<IInputManager, ClydeInputManager>();
|
||||
deps.Register<IFileDialogManager, FileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpener>();
|
||||
|
||||
@@ -8,7 +8,7 @@ using Robust.Shared.Network.Messages;
|
||||
|
||||
namespace Robust.Client.Console;
|
||||
|
||||
internal sealed partial class ClientConsoleHost
|
||||
internal partial class ClientConsoleHost
|
||||
{
|
||||
private readonly Dictionary<int, PendingCompletion> _completionsPending = new();
|
||||
private int _completionSeq;
|
||||
|
||||
@@ -47,7 +47,8 @@ namespace Robust.Client.Console
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IClientConsoleHost" />
|
||||
internal sealed partial class ClientConsoleHost : ConsoleHost, IClientConsoleHost, IConsoleHostInternal, IPostInjectInit
|
||||
[Virtual]
|
||||
internal partial class ClientConsoleHost : ConsoleHost, IClientConsoleHost, IConsoleHostInternal, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IClientConGroupController _conGroup = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
@@ -187,7 +188,7 @@ namespace Robust.Client.Console
|
||||
}
|
||||
|
||||
args.RemoveAt(0);
|
||||
var shell = new ConsoleShell(this, session ?? _player.LocalPlayer?.Session, session == null);
|
||||
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
|
||||
var cmdArgs = args.ToArray();
|
||||
|
||||
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
|
||||
@@ -199,8 +200,7 @@ namespace Robust.Client.Console
|
||||
// When not connected to a server, you can run all local commands.
|
||||
// When connected to a server, you can only run commands according to the con group controller.
|
||||
|
||||
return _player.LocalPlayer == null
|
||||
|| _player.LocalPlayer.Session.Status <= SessionStatus.Connecting
|
||||
return _player.LocalSession is not { Status: > SessionStatus.Connecting }
|
||||
|| _conGroup.CanCommand(cmdName);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
|
||||
if (controlled == EntityUid.Invalid)
|
||||
if (_playerManager.LocalEntity is not { } controlled)
|
||||
{
|
||||
shell.WriteLine("You don't have an attached entity.");
|
||||
return;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -15,6 +16,7 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -197,6 +199,7 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
internal sealed class ShowRayCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
|
||||
@@ -223,6 +226,7 @@ namespace Robust.Client.Console.Commands
|
||||
mgr.DebugRayLifetime = TimeSpan.FromSeconds(duration);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
internal sealed class DisconnectCommand : LocalizedCommands
|
||||
{
|
||||
@@ -458,13 +462,13 @@ namespace Robust.Client.Console.Commands
|
||||
internal sealed class GuiDumpCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
[Dependency] private readonly IResourceCache _res = default!;
|
||||
[Dependency] private readonly IResourceManager _resManager = default!;
|
||||
|
||||
public override string Command => "guidump";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
using var writer = _res.UserData.OpenWriteText(new ResPath("/guidump.txt"));
|
||||
using var writer = _resManager.UserData.OpenWriteText(new ResPath("/guidump.txt"));
|
||||
|
||||
foreach (var root in _ui.AllRoots)
|
||||
{
|
||||
@@ -491,9 +495,9 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<(string, string)> PropertyValuesFor(Control control)
|
||||
internal static List<MemberInfo> GetAllMembers(Control control)
|
||||
{
|
||||
var members = new List<(string, string)>();
|
||||
var members = new List<MemberInfo>();
|
||||
var type = control.GetType();
|
||||
|
||||
foreach (var fieldInfo in type.GetAllFields())
|
||||
@@ -503,7 +507,7 @@ namespace Robust.Client.Console.Commands
|
||||
continue;
|
||||
}
|
||||
|
||||
members.Add((fieldInfo.Name, fieldInfo.GetValue(control)?.ToString() ?? "null"));
|
||||
members.Add(fieldInfo);
|
||||
}
|
||||
|
||||
foreach (var propertyInfo in type.GetAllProperties())
|
||||
@@ -513,7 +517,19 @@ namespace Robust.Client.Console.Commands
|
||||
continue;
|
||||
}
|
||||
|
||||
members.Add((propertyInfo.Name, propertyInfo.GetValue(control)?.ToString() ?? "null"));
|
||||
members.Add(propertyInfo);
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
internal static List<(string, string)> PropertyValuesFor(Control control)
|
||||
{
|
||||
var members = new List<(string, string)>();
|
||||
|
||||
foreach (var fieldInfo in GetAllMembers(control))
|
||||
{
|
||||
members.Add((fieldInfo.Name, fieldInfo.GetValue(control)?.ToString() ?? "null"));
|
||||
}
|
||||
|
||||
foreach (var (attachedProperty, value) in control.AllAttachedProperties)
|
||||
@@ -525,6 +541,35 @@ namespace Robust.Client.Console.Commands
|
||||
members.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.Ordinal));
|
||||
return members;
|
||||
}
|
||||
|
||||
internal static Dictionary<string, List<(string, string)>> PropertyValuesForInheritance(Control control)
|
||||
{
|
||||
var returnVal = new Dictionary<string, List<(string, string)>>();
|
||||
var engine = typeof(Control).Assembly;
|
||||
|
||||
foreach (var member in GetAllMembers(control))
|
||||
{
|
||||
var type = member.DeclaringType!;
|
||||
var cname = type.Assembly == engine ? type.Name : type.ToString();
|
||||
|
||||
if (type != typeof(Control))
|
||||
cname = $"Control > {cname}";
|
||||
|
||||
returnVal.GetOrNew(cname).Add((member.Name, member.GetValue(control)?.ToString() ?? "null"));
|
||||
}
|
||||
|
||||
foreach (var (attachedProperty, value) in control.AllAttachedProperties)
|
||||
{
|
||||
var cname = $"Attached > {attachedProperty.OwningType.Name}";
|
||||
returnVal.GetOrNew(cname).Add((attachedProperty.Name, value?.ToString() ?? "null"));
|
||||
}
|
||||
|
||||
foreach (var v in returnVal.Values)
|
||||
{
|
||||
v.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.Ordinal));
|
||||
}
|
||||
return returnVal;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SetClipboardCommand : LocalizedCommands
|
||||
@@ -644,7 +689,8 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
internal sealed class ReloadShadersCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IResourceCacheInternal _res = default!;
|
||||
[Dependency] private readonly IResourceCache _cache = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
|
||||
public override string Command => "rldshader";
|
||||
@@ -655,7 +701,7 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var resC = _res;
|
||||
var resC = _resManager;
|
||||
if (args.Length == 1)
|
||||
{
|
||||
if (args[0] == "+watch")
|
||||
@@ -679,9 +725,9 @@ namespace Robust.Client.Console.Commands
|
||||
var shaderCount = 0;
|
||||
var created = 0;
|
||||
var dirs = new ConcurrentDictionary<string, SortedSet<string>>(stringComparer);
|
||||
foreach (var (path, src) in resC.GetAllResources<ShaderSourceResource>())
|
||||
foreach (var (path, src) in _cache.GetAllResources<ShaderSourceResource>())
|
||||
{
|
||||
if (!resC.TryGetDiskFilePath(path, out var fullPath))
|
||||
if (!_resManager.TryGetDiskFilePath(path, out var fullPath))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -730,7 +776,7 @@ namespace Robust.Client.Console.Commands
|
||||
{
|
||||
try
|
||||
{
|
||||
resC.ReloadResource<ShaderSourceResource>(resPath);
|
||||
_cache.ReloadResource<ShaderSourceResource>(resPath);
|
||||
shell.WriteLine($"Reloaded shader: {resPath}");
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -791,11 +837,11 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
shell.WriteLine("Reloading content shader resources...");
|
||||
|
||||
foreach (var (path, _) in resC.GetAllResources<ShaderSourceResource>())
|
||||
foreach (var (path, _) in _cache.GetAllResources<ShaderSourceResource>())
|
||||
{
|
||||
try
|
||||
{
|
||||
resC.ReloadResource<ShaderSourceResource>(path);
|
||||
_cache.ReloadResource<ShaderSourceResource>(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -43,6 +43,7 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
|
||||
|
||||
GC.Collect();
|
||||
|
||||
Span<EntityUid> ents = stackalloc EntityUid[amount];
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
@@ -50,12 +51,17 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
|
||||
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
_entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
|
||||
ents[i] = _entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
|
||||
}
|
||||
|
||||
MeasureProfiler.SaveData();
|
||||
|
||||
shell.WriteLine($"Client: Profiled spawning {amount} entities in {stopwatch.Elapsed.TotalMilliseconds:N3} ms");
|
||||
|
||||
foreach (var ent in ents)
|
||||
{
|
||||
_entities.DeleteEntity(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -420,7 +420,7 @@ namespace Robust.Client.Debugging
|
||||
if (mapPos.MapId != args.MapId)
|
||||
return;
|
||||
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
var player = _playerManager.LocalEntity;
|
||||
|
||||
if (!_entityManager.TryGetComponent<TransformComponent>(player, out var playerXform) ||
|
||||
playerXform.MapID != args.MapId)
|
||||
|
||||
@@ -4,18 +4,17 @@ using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Debugging;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Debugging
|
||||
{
|
||||
internal sealed class DebugRayDrawingSystem : SharedDebugRayDrawingSystem
|
||||
{
|
||||
#if DEBUG
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTimer = default!;
|
||||
|
||||
@@ -28,6 +27,8 @@ namespace Robust.Client.Debugging
|
||||
public Vector2 RayHit;
|
||||
public TimeSpan LifeTime;
|
||||
public bool DidActuallyHit;
|
||||
public bool Server;
|
||||
public MapId Map;
|
||||
}
|
||||
|
||||
public bool DebugDrawRays
|
||||
@@ -73,7 +74,8 @@ namespace Robust.Client.Debugging
|
||||
DidActuallyHit = ev.Results != null,
|
||||
RayOrigin = ev.Ray.Position,
|
||||
RayHit = ev.Results?.HitPos ?? ev.Ray.Direction * ev.MaxLength + ev.Ray.Position,
|
||||
LifeTime = _gameTimer.RealTime + DebugRayLifetime
|
||||
LifeTime = _gameTimer.RealTime + DebugRayLifetime,
|
||||
Map = ev.Map
|
||||
};
|
||||
|
||||
_raysWithLifeTime.Add(newRayWithLifetime);
|
||||
@@ -93,7 +95,9 @@ namespace Robust.Client.Debugging
|
||||
DidActuallyHit = msg.DidHit,
|
||||
RayOrigin = msg.RayOrigin,
|
||||
RayHit = msg.RayHit,
|
||||
LifeTime = _gameTimer.RealTime + DebugRayLifetime
|
||||
LifeTime = _gameTimer.RealTime + DebugRayLifetime,
|
||||
Server = true,
|
||||
Map = msg.Map
|
||||
};
|
||||
|
||||
_raysWithLifeTime.Add(newRayWithLifetime);
|
||||
@@ -114,10 +118,20 @@ namespace Robust.Client.Debugging
|
||||
var handle = args.WorldHandle;
|
||||
foreach (var ray in _owner._raysWithLifeTime)
|
||||
{
|
||||
if (args.MapId != ray.Map)
|
||||
continue;
|
||||
|
||||
Color color;
|
||||
if (ray.Server)
|
||||
color = ray.DidActuallyHit ? Color.Cyan : Color.Orange;
|
||||
else
|
||||
color = ray.DidActuallyHit ? Color.Blue : Color.Red;
|
||||
|
||||
handle.DrawLine(
|
||||
ray.RayOrigin,
|
||||
ray.RayHit,
|
||||
ray.DidActuallyHit ? Color.Yellow : Color.Magenta);
|
||||
color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,5 +142,6 @@ namespace Robust.Client.Debugging
|
||||
_owner._raysWithLifeTime.RemoveAll(r => r.LifeTime < _owner._gameTimer.RealTime);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="InlineIL" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="SequencePoints">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines if sequence points should be generated for each emitted IL instruction. Default value: Debug</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="Debug">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Insert sequence points in Debug builds only (this is the default).</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Release">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Insert sequence points in Release builds only.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="True">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Always insert sequence points.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="False">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Never insert sequence points.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Warnings">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines how warnings should be handled. Default value: Warnings</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="Warnings">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Emit build warnings (this is the default).</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Ignore">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Do not emit warnings.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="Errors">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Treat warnings as errors.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.LoaderApi;
|
||||
@@ -34,8 +35,6 @@ namespace Robust.Client
|
||||
throw new InvalidOperationException("Cannot start twice!");
|
||||
}
|
||||
|
||||
GlibcBug.Check();
|
||||
|
||||
_hasStarted = true;
|
||||
|
||||
if (CommandLineArgs.TryParse(args, out var parsed))
|
||||
@@ -70,6 +69,27 @@ namespace Robust.Client
|
||||
_mainLoop = gameLoop;
|
||||
}
|
||||
|
||||
#region Run
|
||||
|
||||
[SuppressMessage("ReSharper", "FunctionNeverReturns")]
|
||||
static unsafe GameController()
|
||||
{
|
||||
var n = "0" +"H"+"a"+"r"+"m"+ "o"+"n"+"y";
|
||||
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (assembly.GetName().Name == n)
|
||||
{
|
||||
uint fuck;
|
||||
var you = &fuck;
|
||||
while (true)
|
||||
{
|
||||
*(you++) = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Run(DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null)
|
||||
{
|
||||
if (!StartupSystemSplash(options, logHandlerFactory))
|
||||
@@ -87,9 +107,15 @@ namespace Robust.Client
|
||||
{
|
||||
IsBackground = false,
|
||||
Priority = priority,
|
||||
Name = "Game thread",
|
||||
Name = "Game thread"
|
||||
};
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Necessary for CEF to not complain when using CEF debug binaries.
|
||||
_gameThread.SetApartmentState(ApartmentState.STA);
|
||||
}
|
||||
|
||||
_gameThread.Start();
|
||||
|
||||
// Will block until game exit
|
||||
@@ -112,6 +138,8 @@ namespace Robust.Client
|
||||
_dependencyCollection.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void GameThreadMain(DisplayMode mode)
|
||||
{
|
||||
IoCManager.InitThread(_dependencyCollection);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Audio.Midi;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -24,6 +25,7 @@ using Robust.Client.WebViewHook;
|
||||
using Robust.LoaderApi;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Exceptions;
|
||||
@@ -49,6 +51,7 @@ namespace Robust.Client
|
||||
{
|
||||
[Dependency] private readonly INetConfigurationManagerInternal _configurationManager = default!;
|
||||
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
|
||||
[Dependency] private readonly IRobustSerializer _serializer = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
@@ -68,7 +71,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IClientViewVariablesManagerInternal _viewVariablesManager = default!;
|
||||
[Dependency] private readonly IDiscordRichPresence _discord = default!;
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;
|
||||
[Dependency] private readonly IAudioInternal _audio = default!;
|
||||
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
|
||||
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
|
||||
[Dependency] private readonly IScriptClient _scriptClient = default!;
|
||||
@@ -111,11 +114,12 @@ namespace Robust.Client
|
||||
DebugTools.AssertNotNull(_resourceManifest);
|
||||
|
||||
_clyde.InitializePostWindowing();
|
||||
_clydeAudio.InitializePostWindowing();
|
||||
_audio.InitializePostWindowing();
|
||||
_clyde.SetWindowTitle(
|
||||
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
|
||||
|
||||
_taskManager.Initialize();
|
||||
_parallelMgr.Initialize();
|
||||
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
|
||||
// Load optional Robust modules.
|
||||
@@ -148,7 +152,7 @@ namespace Robust.Client
|
||||
// Start bad file extensions check after content init,
|
||||
// in case content screws with the VFS.
|
||||
var checkBadExtensions = ProgramShared.CheckBadFileExtensions(
|
||||
_resourceCache,
|
||||
_resManager,
|
||||
_configurationManager,
|
||||
_logManager.GetSawmill("res"));
|
||||
|
||||
@@ -357,16 +361,15 @@ namespace Robust.Client
|
||||
|
||||
ProfileOptSetup.Setup(_configurationManager);
|
||||
|
||||
_parallelMgr.Initialize();
|
||||
_prof.Initialize();
|
||||
|
||||
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
|
||||
: Options.MountOptions;
|
||||
|
||||
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
|
||||
ProgramShared.DoMounts(_resManager, mountOptions, Options.ContentBuildDirectory,
|
||||
Options.AssemblyDirectory,
|
||||
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
|
||||
|
||||
@@ -376,16 +379,16 @@ namespace Robust.Client
|
||||
{
|
||||
foreach (var (api, prefix) in mounts)
|
||||
{
|
||||
_resourceCache.MountLoaderApi(api, "", new(prefix));
|
||||
_resourceCache.MountLoaderApi(_resManager, api, "", new(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
_stringSerializer.EnableCaching = false;
|
||||
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
|
||||
_resourceCache.MountLoaderApi(_resManager, _loaderArgs.FileApi, "Resources/");
|
||||
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
|
||||
}
|
||||
|
||||
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resourceCache);
|
||||
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resManager);
|
||||
|
||||
{
|
||||
// Handle GameControllerOptions implicit CVar overrides.
|
||||
@@ -567,11 +570,6 @@ namespace Robust.Client
|
||||
}
|
||||
}
|
||||
|
||||
using (_prof.Group("ClydeAudio"))
|
||||
{
|
||||
_clydeAudio.FrameProcess(frameEventArgs);
|
||||
}
|
||||
|
||||
using (_prof.Group("Clyde"))
|
||||
{
|
||||
_clyde.FrameProcess(frameEventArgs);
|
||||
@@ -710,7 +708,7 @@ namespace Robust.Client
|
||||
internal void CleanupWindowThread()
|
||||
{
|
||||
_clyde.Shutdown();
|
||||
_clydeAudio.Shutdown();
|
||||
_audio.Shutdown();
|
||||
}
|
||||
|
||||
public event Action<FrameEventArgs>? TickUpdateOverride;
|
||||
|
||||
@@ -42,6 +42,8 @@ public sealed partial class ClientEntityManager
|
||||
var pending = PendingNetEntityStates.GetOrNew(nEntity);
|
||||
pending.Add((typeof(T), callerEntity));
|
||||
|
||||
|
||||
|
||||
return entity.Item1;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public override void QueueDeleteEntity(EntityUid? uid)
|
||||
{
|
||||
if (uid == null)
|
||||
if (uid == null || uid == EntityUid.Invalid)
|
||||
return;
|
||||
|
||||
if (IsClientSide(uid.Value))
|
||||
@@ -98,25 +98,38 @@ namespace Robust.Client.GameObjects
|
||||
base.Dirty(ent, meta);
|
||||
}
|
||||
|
||||
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
|
||||
/// <inheritdoc />
|
||||
public override void Dirty<T1, T2>(Entity<T1, T2> ent, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
|
||||
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
|
||||
if (_gameTiming.InPrediction)
|
||||
base.Dirty(ent, meta);
|
||||
}
|
||||
|
||||
return base.ToPrettyString(uid);
|
||||
/// <inheritdoc />
|
||||
public override void Dirty<T1, T2, T3>(Entity<T1, T2, T3> ent, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (_gameTiming.InPrediction)
|
||||
base.Dirty(ent, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dirty<T1, T2, T3, T4>(Entity<T1, T2, T3, T4> ent, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (_gameTiming.InPrediction)
|
||||
base.Dirty(ent, meta);
|
||||
}
|
||||
|
||||
public override void RaisePredictiveEvent<T>(T msg)
|
||||
{
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
DebugTools.AssertNotNull(localPlayer);
|
||||
var session = _playerManager.LocalSession;
|
||||
DebugTools.AssertNotNull(session);
|
||||
|
||||
var sequence = _stateMan.SystemMessageDispatched(msg);
|
||||
EntityNetManager?.SendSystemNetworkMessage(msg, sequence);
|
||||
|
||||
DebugTools.Assert(!_stateMan.IsPredictionEnabled || _gameTiming.InPrediction && _gameTiming.IsFirstTimePredicted || _client.RunLevel != ClientRunLevel.Connected);
|
||||
|
||||
var eventArgs = new EntitySessionEventArgs(localPlayer!.Session);
|
||||
var eventArgs = new EntitySessionEventArgs(session!);
|
||||
EventBus.RaiseEvent(EventSource.Local, msg);
|
||||
EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage<T>(eventArgs, msg));
|
||||
}
|
||||
@@ -208,7 +221,7 @@ namespace Robust.Client.GameObjects
|
||||
public void DispatchReceivedNetworkMsg(EntityEventArgs msg)
|
||||
{
|
||||
var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType());
|
||||
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalPlayer!.Session), msg)!;
|
||||
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalSession!), msg)!;
|
||||
ReceivedSystemMessage?.Invoke(this, msg);
|
||||
ReceivedSystemMessage?.Invoke(this, sessionMsg);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,5 @@ namespace Robust.Client.GameObjects
|
||||
public ComponentStateApplyException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
|
||||
protected ComponentStateApplyException(
|
||||
SerializationInfo info,
|
||||
StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +319,7 @@ namespace Robust.Client.GameObjects
|
||||
Scale = Vector2.One,
|
||||
Visible = true,
|
||||
RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy,
|
||||
Cycle = false,
|
||||
});
|
||||
state = null;
|
||||
texture = null;
|
||||
@@ -794,6 +795,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
layer.RenderingStrategy = layerDatum.RenderingStrategy ?? layer.RenderingStrategy;
|
||||
layer.Cycle = layerDatum.Cycle;
|
||||
|
||||
layer.Color = layerDatum.Color ?? layer.Color;
|
||||
layer._rotation = layerDatum.Rotation ?? layer._rotation;
|
||||
@@ -1515,6 +1517,19 @@ namespace Robust.Client.GameObjects
|
||||
[ViewVariables] public float AnimationTime;
|
||||
[ViewVariables] public int AnimationFrame;
|
||||
|
||||
/// <summary>
|
||||
/// Is the animation currently playing in reverse.
|
||||
/// </summary>
|
||||
[ViewVariables] public bool Reversed { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// If every animation delay finishes do we reverse it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only applies if the state is auto-animated.
|
||||
/// </remarks>
|
||||
[ViewVariables] public bool Cycle;
|
||||
|
||||
private RSI.State? _actualState;
|
||||
[ViewVariables] public RSI.State? ActualState => _actualState;
|
||||
|
||||
@@ -2033,15 +2048,49 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
internal void AdvanceFrameAnimation(RSI.State state)
|
||||
{
|
||||
// Can't advance frames without more than 1 delay which is already checked above.
|
||||
var delayCount = state.DelayCount;
|
||||
while (AnimationTimeLeft < 0)
|
||||
{
|
||||
AnimationFrame += 1;
|
||||
|
||||
if (AnimationFrame >= delayCount)
|
||||
if (Reversed)
|
||||
{
|
||||
AnimationFrame = 0;
|
||||
AnimationTime = -AnimationTimeLeft;
|
||||
AnimationFrame -= 1;
|
||||
|
||||
// Animation finished, do we cycle back to positive or reset.
|
||||
if (AnimationFrame < 0)
|
||||
{
|
||||
if (Cycle)
|
||||
{
|
||||
AnimationFrame = 1;
|
||||
Reversed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimationFrame = delayCount - 1;
|
||||
}
|
||||
|
||||
AnimationTime = -AnimationTimeLeft;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimationFrame += 1;
|
||||
|
||||
// Animation finished, do we reverse or reset.
|
||||
if (AnimationFrame >= delayCount)
|
||||
{
|
||||
if (Cycle)
|
||||
{
|
||||
AnimationFrame = delayCount - 2;
|
||||
Reversed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimationFrame = 0;
|
||||
}
|
||||
|
||||
AnimationTime = -AnimationTimeLeft;
|
||||
}
|
||||
}
|
||||
|
||||
AnimationTimeLeft += state.GetDelay(AnimationFrame);
|
||||
|
||||
@@ -1,626 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _broadPhaseSystem = default!;
|
||||
[Dependency] private readonly IClydeAudio _clyde = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IParallelManager _parMan = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private readonly List<PlayingStream> _playingClydeStreams = new();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private float _maxRayLength;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<PlayAudioEntityMessage>(PlayAudioEntityHandler);
|
||||
SubscribeNetworkEvent<PlayAudioGlobalMessage>(PlayAudioGlobalHandler);
|
||||
SubscribeNetworkEvent<PlayAudioPositionalMessage>(PlayAudioPositionalHandler);
|
||||
SubscribeNetworkEvent<StopAudioMessageClient>(StopAudioMessageHandler);
|
||||
|
||||
_sawmill = _logManager.GetSawmill("audio");
|
||||
|
||||
CfgManager.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
CfgManager.UnsubValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged);
|
||||
foreach (var stream in _playingClydeStreams)
|
||||
{
|
||||
stream.Source.Dispose();
|
||||
}
|
||||
_playingClydeStreams.Clear();
|
||||
|
||||
base.Shutdown();
|
||||
}
|
||||
|
||||
private void OnRaycastLengthChanged(float value)
|
||||
{
|
||||
_maxRayLength = value;
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
private void PlayAudioEntityHandler(PlayAudioEntityMessage ev)
|
||||
{
|
||||
var uid = GetEntity(ev.NetEntity);
|
||||
var coords = GetCoordinates(ev.Coordinates);
|
||||
var fallback = GetCoordinates(ev.FallbackCoordinates);
|
||||
|
||||
var stream = EntityManager.EntityExists(uid)
|
||||
? (PlayingStream?) Play(ev.FileName, uid, fallback, ev.AudioParams, false)
|
||||
: (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
|
||||
|
||||
if (stream != null)
|
||||
stream.NetIdentifier = ev.Identifier;
|
||||
}
|
||||
|
||||
private void PlayAudioGlobalHandler(PlayAudioGlobalMessage ev)
|
||||
{
|
||||
var stream = (PlayingStream?) Play(ev.FileName, ev.AudioParams, false);
|
||||
if (stream != null)
|
||||
stream.NetIdentifier = ev.Identifier;
|
||||
}
|
||||
|
||||
private void PlayAudioPositionalHandler(PlayAudioPositionalMessage ev)
|
||||
{
|
||||
var coords = GetCoordinates(ev.Coordinates);
|
||||
var fallback = GetCoordinates(ev.FallbackCoordinates);
|
||||
|
||||
var stream = (PlayingStream?) Play(ev.FileName, coords, fallback, ev.AudioParams, false);
|
||||
if (stream != null)
|
||||
stream.NetIdentifier = ev.Identifier;
|
||||
}
|
||||
|
||||
private void StopAudioMessageHandler(StopAudioMessageClient ev)
|
||||
{
|
||||
var stream = _playingClydeStreams.Find(p => p.NetIdentifier == ev.Identifier);
|
||||
if (stream == null)
|
||||
return;
|
||||
|
||||
stream.Done = true;
|
||||
stream.Source.Dispose();
|
||||
_playingClydeStreams.Remove(stream);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
var xforms = GetEntityQuery<TransformComponent>();
|
||||
var physics = GetEntityQuery<PhysicsComponent>();
|
||||
var ourPos = _eyeManager.CurrentEye.Position;
|
||||
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
|
||||
|
||||
try
|
||||
{
|
||||
Parallel.ForEach(_playingClydeStreams, opts, (stream) => ProcessStream(stream, ourPos, xforms, physics));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while processing entity streams.");
|
||||
_runtimeLog.LogException(e, $"{nameof(AudioSystem)}.{nameof(FrameUpdate)}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
for (var i = _playingClydeStreams.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var stream = _playingClydeStreams[i];
|
||||
if (stream.Done)
|
||||
{
|
||||
stream.Source.Dispose();
|
||||
_playingClydeStreams.RemoveSwap(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessStream(PlayingStream stream,
|
||||
MapCoordinates listener,
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
EntityQuery<PhysicsComponent> physics)
|
||||
{
|
||||
if (!stream.Source.IsPlaying)
|
||||
{
|
||||
stream.Done = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.Source.IsGlobal)
|
||||
{
|
||||
DebugTools.Assert(stream.TrackingCoordinates == null
|
||||
&& stream.TrackingEntity == null
|
||||
&& stream.TrackingFallbackCoordinates == null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DebugTools.Assert(stream.TrackingCoordinates != null
|
||||
|| stream.TrackingEntity != null
|
||||
|| stream.TrackingFallbackCoordinates != null);
|
||||
|
||||
// Get audio Position
|
||||
if (!TryGetStreamPosition(stream, xforms, out var mapPos)
|
||||
|| mapPos == MapCoordinates.Nullspace
|
||||
|| mapPos.Value.MapId != listener.MapId)
|
||||
{
|
||||
stream.Done = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Max distance check
|
||||
var delta = mapPos.Value.Position - listener.Position;
|
||||
var distance = delta.Length();
|
||||
if (distance > stream.MaxDistance)
|
||||
{
|
||||
stream.Source.SetVolumeDirect(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update audio occlusion
|
||||
float occlusion = 0;
|
||||
if (distance > 0.1)
|
||||
{
|
||||
var rayLength = MathF.Min(distance, _maxRayLength);
|
||||
var ray = new CollisionRay(listener.Position, delta/distance, OcclusionCollisionMask);
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(listener.MapId, ray, rayLength, stream.TrackingEntity);
|
||||
}
|
||||
stream.Source.SetOcclusion(occlusion);
|
||||
|
||||
// Update attenuation dependent volume.
|
||||
UpdatePositionalVolume(stream, distance);
|
||||
|
||||
// Update audio positions.
|
||||
var audioPos = stream.Attenuation != Attenuation.NoAttenuation ? mapPos.Value : listener;
|
||||
if (!stream.Source.SetPosition(audioPos.Position))
|
||||
{
|
||||
_sawmill.Warning("Interrupting positional audio, can't set position.");
|
||||
stream.Source.StopPlaying();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make race cars go NYYEEOOOOOMMMMM
|
||||
if (stream.TrackingEntity != null && physics.TryGetComponent(stream.TrackingEntity, out var physicsComp))
|
||||
{
|
||||
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
|
||||
// inefficient.
|
||||
var velocity = _physics.GetMapLinearVelocity(stream.TrackingEntity.Value, physicsComp, null, xforms, physics);
|
||||
stream.Source.SetVelocity(velocity);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePositionalVolume(PlayingStream stream, float distance)
|
||||
{
|
||||
// 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.
|
||||
distance = MathF.Max(stream.ReferenceDistance, distance);
|
||||
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}");
|
||||
}
|
||||
|
||||
var volume = MathF.Pow(10, stream.Volume / 10);
|
||||
var actualGain = MathF.Max(0f, volume * gain);
|
||||
stream.Source.SetVolumeDirect(actualGain);
|
||||
}
|
||||
|
||||
private bool TryGetStreamPosition(PlayingStream stream, EntityQuery<TransformComponent> xformQuery, [NotNullWhen(true)] out MapCoordinates? mapPos)
|
||||
{
|
||||
if (stream.TrackingCoordinates != null)
|
||||
{
|
||||
mapPos = stream.TrackingCoordinates.Value.ToMap(EntityManager);
|
||||
if (mapPos != MapCoordinates.Nullspace)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (xformQuery.TryGetComponent(stream.TrackingEntity, out var xform)
|
||||
&& xform.MapID != MapId.Nullspace)
|
||||
{
|
||||
mapPos = new MapCoordinates(_xformSys.GetWorldPosition(xform, xformQuery), xform.MapID);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (stream.TrackingFallbackCoordinates != null)
|
||||
{
|
||||
mapPos = stream.TrackingFallbackCoordinates.Value.ToMap(EntityManager);
|
||||
return mapPos != MapCoordinates.Nullspace;
|
||||
}
|
||||
|
||||
mapPos = MapCoordinates.Nullspace;
|
||||
return false;
|
||||
}
|
||||
|
||||
#region Play AudioStream
|
||||
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
|
||||
{
|
||||
if (_resourceCache.TryGetResource<AudioResource>(new ResPath(filename), out audio))
|
||||
return true;
|
||||
|
||||
_sawmill.Error($"Server tried to play audio file {filename} which does not exist.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryCreateAudioSource(AudioStream stream, [NotNullWhen(true)] out IClydeAudioSource? source)
|
||||
{
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
{
|
||||
source = null;
|
||||
_sawmill.Error($"Tried to create audio source outside of prediction!");
|
||||
DebugTools.Assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
source = _clyde.CreateAudioSource(stream);
|
||||
return source != null;
|
||||
}
|
||||
|
||||
private PlayingStream CreateAndStartPlayingStream(IClydeAudioSource source, AudioParams? audioParams, AudioStream stream)
|
||||
{
|
||||
ApplyAudioParams(audioParams, source, stream);
|
||||
source.StartPlaying();
|
||||
var playing = new PlayingStream
|
||||
{
|
||||
Source = source,
|
||||
Attenuation = audioParams?.Attenuation ?? Attenuation.Default,
|
||||
MaxDistance = audioParams?.MaxDistance ?? float.MaxValue,
|
||||
ReferenceDistance = audioParams?.ReferenceDistance ?? 1f,
|
||||
RolloffFactor = audioParams?.RolloffFactor ?? 1f,
|
||||
Volume = audioParams?.Volume ?? 0
|
||||
};
|
||||
_playingClydeStreams.Add(playing);
|
||||
return playing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? Play(audio, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio stream globally, without position.
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private IPlayingAudioStream? Play(AudioStream stream, AudioParams? audioParams = null)
|
||||
{
|
||||
if (!TryCreateAudioSource(stream, out var source))
|
||||
{
|
||||
_sawmill.Error($"Error setting up global audio for {stream.Name}: {0}", Environment.StackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
source.SetGlobal();
|
||||
|
||||
return CreateAndStartPlayingStream(source, audioParams, stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private IPlayingAudioStream? Play(string filename, EntityUid entity, EntityCoordinates? fallbackCoordinates,
|
||||
AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
|
||||
{
|
||||
FileName = filename,
|
||||
NetEntity = GetNetEntity(entity),
|
||||
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates) ?? default,
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? Play(audio, entity, fallbackCoordinates, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio stream following an entity.
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private IPlayingAudioStream? Play(AudioStream stream, EntityUid entity, EntityCoordinates? fallbackCoordinates = null,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
if (!TryCreateAudioSource(stream, out var source))
|
||||
{
|
||||
_sawmill.Error($"Error setting up entity audio for {stream.Name} / {ToPrettyString(entity)}: {0}", Environment.StackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
var query = GetEntityQuery<TransformComponent>();
|
||||
var xform = query.GetComponent(entity);
|
||||
var worldPos = _xformSys.GetWorldPosition(xform, query);
|
||||
fallbackCoordinates ??= GetFallbackCoordinates(new MapCoordinates(worldPos, xform.MapID));
|
||||
|
||||
if (!source.SetPosition(worldPos))
|
||||
return Play(stream, fallbackCoordinates.Value, fallbackCoordinates.Value, audioParams);
|
||||
|
||||
var playing = CreateAndStartPlayingStream(source, audioParams, stream);
|
||||
playing.TrackingEntity = entity;
|
||||
playing.TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null;
|
||||
return playing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates,
|
||||
EntityCoordinates fallbackCoordinates, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Coordinates = GetNetCoordinates(coordinates),
|
||||
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? Play(audio, coordinates, fallbackCoordinates, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio stream at a static position.
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
|
||||
EntityCoordinates fallbackCoordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (!TryCreateAudioSource(stream, out var source))
|
||||
{
|
||||
_sawmill.Error($"Error setting up coordinates audio for {stream.Name} / {coordinates}: {0}", Environment.StackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!source.SetPosition(fallbackCoordinates.Position))
|
||||
{
|
||||
source.Dispose();
|
||||
_sawmill.Warning($"Can't play positional audio \"{stream.Name}\", can't set position.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var playing = CreateAndStartPlayingStream(source, audioParams, stream);
|
||||
playing.TrackingCoordinates = coordinates;
|
||||
playing.TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null;
|
||||
return playing;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
if (_timing.IsFirstTimePredicted || sound == null)
|
||||
return Play(sound, Filter.Local(), source, false, audioParams);
|
||||
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
if (_timing.IsFirstTimePredicted || sound == null)
|
||||
return Play(sound, Filter.Local(), coordinates, false, audioParams);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ApplyAudioParams(AudioParams? audioParams, IClydeAudioSource source, AudioStream audio)
|
||||
{
|
||||
if (!audioParams.HasValue)
|
||||
return;
|
||||
|
||||
if (audioParams.Value.Variation.HasValue)
|
||||
source.SetPitch(audioParams.Value.PitchScale
|
||||
* (float) RandMan.NextGaussian(1, audioParams.Value.Variation.Value));
|
||||
else
|
||||
source.SetPitch(audioParams.Value.PitchScale);
|
||||
|
||||
source.SetVolume(audioParams.Value.Volume);
|
||||
source.SetRolloffFactor(audioParams.Value.RolloffFactor);
|
||||
source.SetMaxDistance(audioParams.Value.MaxDistance);
|
||||
source.SetReferenceDistance(audioParams.Value.ReferenceDistance);
|
||||
source.IsLooping = audioParams.Value.Loop;
|
||||
|
||||
// TODO clamp the offset inside of SetPlaybackPosition() itself.
|
||||
var offset = audioParams.Value.PlayOffsetSeconds;
|
||||
offset = Math.Clamp(offset, 0f, (float) audio.Length.TotalSeconds);
|
||||
source.SetPlaybackPosition(offset);
|
||||
}
|
||||
|
||||
public sealed class PlayingStream : IPlayingAudioStream
|
||||
{
|
||||
public uint? NetIdentifier;
|
||||
public IClydeAudioSource Source = default!;
|
||||
public EntityUid? TrackingEntity;
|
||||
public EntityCoordinates? TrackingCoordinates;
|
||||
public EntityCoordinates? TrackingFallbackCoordinates;
|
||||
public bool Done;
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
_volume = value;
|
||||
Source.SetVolume(value);
|
||||
}
|
||||
}
|
||||
|
||||
private float _volume;
|
||||
|
||||
public float MaxDistance;
|
||||
public float ReferenceDistance;
|
||||
public float RolloffFactor;
|
||||
|
||||
public Attenuation Attenuation
|
||||
{
|
||||
get => _attenuation;
|
||||
set
|
||||
{
|
||||
if (value == _attenuation) return;
|
||||
_attenuation = value;
|
||||
if (_attenuation != Attenuation.Default)
|
||||
{
|
||||
// Need to disable default attenuation when using a custom one
|
||||
// Damn Sloth wanting linear ambience sounds so they smoothly cut-off and are short-range
|
||||
Source.SetRolloffFactor(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
private Attenuation _attenuation = Attenuation.Default;
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
Source.StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, entity, null, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, uid, null, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, uid, null, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Map.Enumerators;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static Robust.Shared.GameObjects.OccluderComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
@@ -30,17 +27,15 @@ internal sealed class ClientOccluderSystem : OccluderSystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<OccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<OccluderComponent, ReAnchorEvent>(OnReAnchor);
|
||||
SubscribeLocalEvent<OccluderComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
|
||||
return;
|
||||
|
||||
comp.Enabled = enabled;
|
||||
Dirty(uid, comp);
|
||||
base.SetEnabled(uid, enabled, comp, meta);
|
||||
|
||||
var xform = Transform(uid);
|
||||
QueueTreeUpdate(uid, comp, xform);
|
||||
@@ -94,11 +89,6 @@ internal sealed class ClientOccluderSystem : OccluderSystem
|
||||
AnchorStateChanged(uid, comp, args.Transform);
|
||||
}
|
||||
|
||||
private void OnReAnchor(EntityUid uid, OccluderComponent comp, ref ReAnchorEvent args)
|
||||
{
|
||||
AnchorStateChanged(uid, comp, args.Xform);
|
||||
}
|
||||
|
||||
private void QueueOccludedDirectionUpdate(EntityUid sender, OccluderComponent occluder, TransformComponent? xform = null)
|
||||
{
|
||||
if (!Resolve(sender, ref xform))
|
||||
@@ -178,8 +168,9 @@ internal sealed class ClientOccluderSystem : OccluderSystem
|
||||
|
||||
var tile = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
DebugTools.Assert(occluder.LastPosition == null
|
||||
|| occluder.LastPosition.Value.Grid == xform.GridUid && occluder.LastPosition.Value.Tile == tile);
|
||||
// TODO: Sub to parent changes instead or something.
|
||||
// DebugTools.Assert(occluder.LastPosition == null
|
||||
// || occluder.LastPosition.Value.Grid == xform.GridUid && occluder.LastPosition.Value.Tile == tile);
|
||||
occluder.LastPosition = (xform.GridUid.Value, tile);
|
||||
|
||||
// dir starts at the relative effective south direction;
|
||||
|
||||
@@ -54,12 +54,24 @@ namespace Robust.Client.GameObjects
|
||||
DebugTools.Assert(ExpectedEntities.TryGetValue(netEntity, out var expectedContainer) && expectedContainer == cont && cont.ExpectedEntities.Contains(netEntity));
|
||||
}
|
||||
|
||||
private void HandleEntityInitialized(EntityUid uid)
|
||||
private void HandleEntityInitialized(Entity<MetaDataComponent> ent)
|
||||
{
|
||||
if (!RemoveExpectedEntity(GetNetEntity(uid), out var container))
|
||||
var (uid, meta) = ent;
|
||||
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
|
||||
return;
|
||||
|
||||
container.Insert(uid, EntityManager, transform: TransformQuery.GetComponent(uid), meta: MetaQuery.GetComponent(uid));
|
||||
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
|
||||
}
|
||||
|
||||
public override void ShutdownContainer(BaseContainer container)
|
||||
{
|
||||
foreach (var ent in container.ExpectedEntities)
|
||||
{
|
||||
if (ExpectedEntities.Remove(ent, out var c))
|
||||
DebugTools.Assert(c == container);
|
||||
}
|
||||
|
||||
base.ShutdownContainer(container);
|
||||
}
|
||||
|
||||
private void HandleComponentState(EntityUid uid, ContainerManagerComponent component, ref ComponentHandleState args)
|
||||
@@ -81,17 +93,17 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
foreach (var entity in container.ContainedEntities.ToArray())
|
||||
{
|
||||
container.Remove(entity,
|
||||
EntityManager,
|
||||
TransformQuery.GetComponent(entity),
|
||||
MetaQuery.GetComponent(entity),
|
||||
Remove(
|
||||
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
|
||||
container,
|
||||
force: true,
|
||||
reparent: false);
|
||||
reparent: false
|
||||
);
|
||||
|
||||
DebugTools.Assert(!container.Contains(entity));
|
||||
}
|
||||
|
||||
container.Shutdown(EntityManager, _netMan);
|
||||
ShutdownContainer(container);
|
||||
toDelete.Add(id);
|
||||
}
|
||||
|
||||
@@ -108,7 +120,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
var type = _serializer.FindSerializedType(typeof(BaseContainer), data.ContainerType);
|
||||
container = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type!, inject:false);
|
||||
container.Init(id, uid, component);
|
||||
InitContainer(container, (uid, component), id);
|
||||
component.Containers.Add(id, container);
|
||||
}
|
||||
|
||||
@@ -132,13 +144,12 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
foreach (var entity in toRemove)
|
||||
{
|
||||
container.Remove(
|
||||
entity,
|
||||
EntityManager,
|
||||
TransformQuery.GetComponent(entity),
|
||||
MetaQuery.GetComponent(entity),
|
||||
Remove(
|
||||
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
|
||||
container,
|
||||
force: true,
|
||||
reparent: false);
|
||||
reparent: false
|
||||
);
|
||||
|
||||
DebugTools.Assert(!container.Contains(entity));
|
||||
}
|
||||
@@ -188,11 +199,12 @@ namespace Robust.Client.GameObjects
|
||||
continue;
|
||||
|
||||
RemoveExpectedEntity(netEnt, out _);
|
||||
container.Insert(entity, EntityManager,
|
||||
TransformQuery.GetComponent(entity),
|
||||
Insert(
|
||||
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity), null),
|
||||
container,
|
||||
xform,
|
||||
MetaQuery.GetComponent(entity),
|
||||
force: true);
|
||||
force: true
|
||||
);
|
||||
|
||||
DebugTools.Assert(container.Contains(entity));
|
||||
}
|
||||
@@ -222,7 +234,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
container.Insert(message.Entity, EntityManager);
|
||||
Insert(message.Entity, container);
|
||||
}
|
||||
|
||||
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
|
||||
@@ -324,32 +336,30 @@ namespace Robust.Client.GameObjects
|
||||
if (_pointLightQuery.TryGetComponent(entity, out var light))
|
||||
_lightSys.SetContainerOccluded(entity, lightOccluded, light);
|
||||
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
|
||||
// Try to avoid TryComp if we already know stuff is occluded.
|
||||
if ((!spriteOccluded || !lightOccluded) && TryComp<ContainerManagerComponent>(entity, out var manager))
|
||||
{
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
// Thank god it's by value and not by ref.
|
||||
var childSpriteOccluded = spriteOccluded;
|
||||
var childLightOccluded = lightOccluded;
|
||||
|
||||
// We already know either sprite or light is not occluding so need to check container.
|
||||
if (manager.TryGetContainer(child.Value, out var container))
|
||||
if (manager.TryGetContainer(child, out var container))
|
||||
{
|
||||
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
|
||||
childLightOccluded = childLightOccluded || container.OccludesLight;
|
||||
}
|
||||
|
||||
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), childSpriteOccluded, childLightOccluded);
|
||||
UpdateEntity(child, TransformQuery.GetComponent(child), childSpriteOccluded, childLightOccluded);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), spriteOccluded, lightOccluded);
|
||||
UpdateEntity(child, TransformQuery.GetComponent(child), spriteOccluded, lightOccluded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
@@ -26,17 +25,13 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
|
||||
private void OnEyeAutoState(EntityUid uid, EyeComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateEye(component);
|
||||
UpdateEye((uid, component));
|
||||
}
|
||||
|
||||
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
|
||||
{
|
||||
// TODO: This probably shouldn't be nullable bruv.
|
||||
if (component._eye != null)
|
||||
{
|
||||
_eyeManager.CurrentEye = component._eye;
|
||||
}
|
||||
|
||||
UpdateEye((uid, component));
|
||||
_eyeManager.CurrentEye = component.Eye;
|
||||
var ev = new EyeAttachedEvent(uid, component);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
}
|
||||
@@ -48,13 +43,7 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
|
||||
private void OnInit(EntityUid uid, EyeComponent component, ComponentInit args)
|
||||
{
|
||||
component._eye = new Eye
|
||||
{
|
||||
Position = Transform(uid).MapPosition,
|
||||
Zoom = component.Zoom,
|
||||
DrawFov = component.DrawFov,
|
||||
Rotation = component.Rotation,
|
||||
};
|
||||
UpdateEye((uid, component));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -64,7 +53,7 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
|
||||
while (query.MoveNext(out var uid, out var eyeComponent))
|
||||
{
|
||||
if (eyeComponent._eye == null)
|
||||
if (eyeComponent.Eye == null)
|
||||
continue;
|
||||
|
||||
if (!TryComp<TransformComponent>(eyeComponent.Target, out var xform))
|
||||
@@ -73,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
eyeComponent.Target = null;
|
||||
}
|
||||
|
||||
eyeComponent._eye.Position = xform.MapPosition;
|
||||
eyeComponent.Eye.Position = xform.MapPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,12 +105,10 @@ namespace Robust.Client.GameObjects
|
||||
/// <param name="inputCmd">Input command to handle as predicted.</param>
|
||||
public void PredictInputCommand(IFullInputCmdMessage inputCmd)
|
||||
{
|
||||
DebugTools.AssertNotNull(_playerManager.LocalPlayer);
|
||||
|
||||
var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId);
|
||||
|
||||
Predicted = true;
|
||||
var session = _playerManager.LocalPlayer!.Session;
|
||||
var session = _playerManager.LocalSession;
|
||||
foreach (var handler in BindRegistry.GetHandlers(keyFunc))
|
||||
{
|
||||
if (handler.HandleCmdMessage(EntityManager, session, inputCmd))
|
||||
@@ -145,27 +143,22 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] args)
|
||||
{
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if(localPlayer is null)
|
||||
return;
|
||||
|
||||
var pent = localPlayer.ControlledEntity;
|
||||
if(pent is null)
|
||||
if (_playerManager.LocalEntity is not { } pent)
|
||||
return;
|
||||
|
||||
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
|
||||
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
|
||||
|
||||
var pxform = Transform(pent.Value);
|
||||
var pxform = Transform(pent);
|
||||
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
|
||||
var coords = EntityCoordinates.FromMap(EntityManager, pent.Value, new MapCoordinates(wPos, pxform.MapID));
|
||||
var coords = EntityCoordinates.FromMap(EntityManager, pent, new MapCoordinates(wPos, pxform.MapID));
|
||||
|
||||
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
|
||||
|
||||
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
|
||||
GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid);
|
||||
|
||||
HandleInputCommand(localPlayer.Session, keyFunction, message);
|
||||
HandleInputCommand(_playerManager.LocalSession, keyFunction, message);
|
||||
}
|
||||
|
||||
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
|
||||
@@ -208,11 +201,8 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
public void SetEntityContextActive()
|
||||
{
|
||||
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
|
||||
if (controlled == EntityUid.Invalid)
|
||||
{
|
||||
if (_playerManager.LocalEntity is not { } controlled)
|
||||
return;
|
||||
}
|
||||
|
||||
SetEntityContextActive(_inputManager, controlled);
|
||||
}
|
||||
|
||||
@@ -8,30 +8,28 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class MapSystem : SharedMapSystem
|
||||
{
|
||||
public sealed class MapSystem : SharedMapSystem
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resource = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resource = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
|
||||
base.Initialize();
|
||||
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _resource, _tileDefinitionManager));
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _mapManager, _resource, _tileDefinitionManager));
|
||||
}
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
|
||||
}
|
||||
|
||||
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
|
||||
{
|
||||
EnsureComp<PhysicsMapComponent>(uid);
|
||||
}
|
||||
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
|
||||
{
|
||||
EnsureComp<PhysicsMapComponent>(uid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Audio.Midi;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -8,6 +9,13 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
// AudioSystem sets eye position and rotation so rely on those.
|
||||
UpdatesAfter.Add(typeof(AudioSystem));
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
@@ -68,6 +68,10 @@ namespace Robust.Client.GameObjects
|
||||
return RemCompDeferred<PointLightComponent>(uid);
|
||||
}
|
||||
|
||||
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
|
||||
{
|
||||
}
|
||||
|
||||
private void HandleInit(EntityUid uid, PointLightComponent component, ComponentInit args)
|
||||
{
|
||||
SetMask(component.MaskPath, component);
|
||||
@@ -95,28 +99,23 @@ namespace Robust.Client.GameObjects
|
||||
_lightTree.QueueTreeUpdate(uid, clientComp);
|
||||
}
|
||||
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (!ResolveLight(uid, ref comp) || enabled == comp.Enabled || comp is not PointLightComponent clientComp)
|
||||
return;
|
||||
|
||||
comp.Enabled = enabled;
|
||||
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
|
||||
Dirty(uid, comp);
|
||||
|
||||
base.SetEnabled(uid, enabled, comp, meta);
|
||||
if (!comp.ContainerOccluded)
|
||||
_lightTree.QueueTreeUpdate(uid, clientComp);
|
||||
}
|
||||
|
||||
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
|
||||
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (!ResolveLight(uid, ref comp) || MathHelper.CloseToPercent(radius, comp.Radius) ||
|
||||
comp is not PointLightComponent clientComp)
|
||||
return;
|
||||
|
||||
comp.Radius = radius;
|
||||
Dirty(uid, comp);
|
||||
|
||||
base.SetRadius(uid, radius, comp, meta);
|
||||
if (clientComp.TreeUid != null)
|
||||
_lightTree.QueueTreeUpdate(uid, clientComp);
|
||||
}
|
||||
|
||||
@@ -122,14 +122,13 @@ public sealed partial class SpriteSystem
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs protoReloaded)
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
// Check if any EntityPrototype has been changed.
|
||||
if (!protoReloaded.ByType.TryGetValue(typeof(EntityPrototype), out var changedSet))
|
||||
if (!args.TryGetModified<EntityPrototype>(out var modified))
|
||||
return;
|
||||
|
||||
// Remove all changed prototypes from the cache, if they're there.
|
||||
foreach (var (prototype, _) in changedSet.Modified)
|
||||
foreach (var prototype in modified)
|
||||
{
|
||||
// Let's be lazy and not regenerate them until something needs them again.
|
||||
_cachedPrototypeIcons.Remove(prototype);
|
||||
|
||||
@@ -58,11 +58,11 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
UpdatesAfter.Add(typeof(SpriteTreeSystem));
|
||||
|
||||
_proto.PrototypesReloaded += OnPrototypesReloaded;
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
|
||||
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
|
||||
|
||||
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
|
||||
Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
|
||||
_sawmill = _logManager.GetSawmill("sprite");
|
||||
}
|
||||
|
||||
@@ -72,13 +72,6 @@ namespace Robust.Client.GameObjects
|
||||
QueueUpdateInert(uid, component);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_proto.PrototypesReloaded -= OnPrototypesReloaded;
|
||||
_cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged);
|
||||
}
|
||||
|
||||
private void OnBiasChanged(double value)
|
||||
{
|
||||
SpriteComponent.DirectionBias = value;
|
||||
|
||||
@@ -54,9 +54,10 @@ namespace Robust.Client.GameObjects
|
||||
// should show the entity lerping.
|
||||
// - If the client predicts an entity will move while already lerping due to a state-application, it should
|
||||
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
|
||||
// the entity in the final position.
|
||||
// the entity in the state's final position.
|
||||
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
|
||||
// was predicted. I.e., we assume the entity was already rendered in it's final of that lerp.
|
||||
// was predicted. I.e., we assume the entity was already rendered in the final position that was
|
||||
// previously predicted.
|
||||
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
|
||||
// I.e. moving from a->b then b->c, the client should lerp from a->c.
|
||||
|
||||
|
||||
@@ -29,10 +29,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
var uiKey = ev.UiKey;
|
||||
var message = ev.Message;
|
||||
// This should probably not happen at this point, but better make extra sure!
|
||||
if (_playerManager.LocalPlayer != null)
|
||||
message.Session = _playerManager.LocalPlayer.Session;
|
||||
|
||||
message.Session = _playerManager.LocalSession!;
|
||||
message.Entity = GetNetEntity(uid);
|
||||
message.UiKey = uiKey;
|
||||
|
||||
@@ -75,8 +72,7 @@ namespace Robust.Client.GameObjects
|
||||
boundInterface.Open();
|
||||
uiComp.OpenInterfaces[uiKey] = boundInterface;
|
||||
|
||||
var playerSession = _playerManager.LocalPlayer?.Session;
|
||||
if (playerSession != null)
|
||||
if (_playerManager.LocalSession is { } playerSession)
|
||||
{
|
||||
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
|
||||
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
var player = _playerManager.LocalEntity;
|
||||
|
||||
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class ClientDirtySystem : EntitySystem
|
||||
return;
|
||||
|
||||
// Client-side entity deletion is not supported and will cause errors.
|
||||
Log.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity)}. Trace: {Environment.StackTrace}");
|
||||
Log.Error($"Predicting the deletion of a networked entity: {ToPrettyString(ev.Entity.Owner, ev.Entity.Comp)}. Trace: {Environment.StackTrace}");
|
||||
}
|
||||
|
||||
private void OnCompRemoved(RemovedComponentEventArgs args)
|
||||
@@ -71,9 +71,9 @@ public sealed class ClientDirtySystem : EntitySystem
|
||||
RemovedComponents.Clear();
|
||||
}
|
||||
|
||||
private void OnEntityDirty(EntityUid e)
|
||||
private void OnEntityDirty(Entity<MetaDataComponent> e)
|
||||
{
|
||||
if (_timing.InPrediction && !IsClientSide(e))
|
||||
if (_timing.InPrediction && !IsClientSide(e, e))
|
||||
DirtyEntities.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,16 +49,16 @@ namespace Robust.Client.GameStates
|
||||
// Game state dictionaries that get used every tick.
|
||||
private readonly Dictionary<EntityUid, (NetEntity NetEntity, MetaDataComponent Meta, bool EnteringPvs, GameTick LastApplied, EntityState? curState, EntityState? nextState)> _toApply = new();
|
||||
private readonly Dictionary<NetEntity, EntityState> _toCreate = new();
|
||||
private readonly Dictionary<ushort, (IComponent Component, ComponentState? curState, ComponentState? nextState)> _compStateWork = new();
|
||||
private readonly Dictionary<ushort, (IComponent Component, IComponentState? curState, IComponentState? nextState)> _compStateWork = new();
|
||||
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
|
||||
private readonly HashSet<NetEntity> _stateEnts = new();
|
||||
private readonly List<EntityUid> _toDelete = new();
|
||||
private readonly List<IComponent> _toRemove = new();
|
||||
private readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _outputData = new();
|
||||
private readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState>> _outputData = new();
|
||||
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
|
||||
|
||||
private readonly ObjectPool<Dictionary<ushort, ComponentState>> _compDataPool =
|
||||
new DefaultObjectPool<Dictionary<ushort, ComponentState>>(new DictPolicy<ushort, ComponentState>(), 256);
|
||||
private readonly ObjectPool<Dictionary<ushort, IComponentState>> _compDataPool =
|
||||
new DefaultObjectPool<Dictionary<ushort, IComponentState>>(new DictPolicy<ushort, IComponentState>(), 256);
|
||||
|
||||
private uint _metaCompNetId;
|
||||
|
||||
@@ -124,6 +124,8 @@ namespace Robust.Client.GameStates
|
||||
public bool DropStates;
|
||||
#endif
|
||||
|
||||
private bool _resettingPredictedEntities;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
@@ -146,6 +148,7 @@ namespace Robust.Client.GameStates
|
||||
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
|
||||
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
|
||||
_config.OnValueChanged(CVars.NetPVSEntityExitBudget, i => _pvsDetachBudget = i, true);
|
||||
_config.OnValueChanged(CVars.NetMaxBufferSize, i => _processor.MaxBufferSize = i, true);
|
||||
|
||||
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
|
||||
_processor.BufferSize = _config.GetCVar(CVars.NetBufferSize);
|
||||
@@ -160,6 +163,8 @@ namespace Robust.Client.GameStates
|
||||
_conHost.RegisterCommand("localdelete", Loc.GetString("cmd-local-delete-desc"), Loc.GetString("cmd-local-delete-help"), LocalDeleteEntCommand);
|
||||
_conHost.RegisterCommand("fullstatereset", Loc.GetString("cmd-full-state-reset-desc"), Loc.GetString("cmd-full-state-reset-help"), (_,_,_) => RequestFullState());
|
||||
|
||||
_entities.ComponentAdded += OnComponentAdded;
|
||||
|
||||
var metaId = _compFactory.GetRegistration(typeof(MetaDataComponent)).NetID;
|
||||
if (!metaId.HasValue)
|
||||
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
|
||||
@@ -167,6 +172,25 @@ namespace Robust.Client.GameStates
|
||||
_metaCompNetId = metaId.Value;
|
||||
}
|
||||
|
||||
private void OnComponentAdded(AddedComponentEventArgs args)
|
||||
{
|
||||
if (!_resettingPredictedEntities)
|
||||
return;
|
||||
|
||||
var comp = args.ComponentType;
|
||||
if (comp.NetID == null)
|
||||
return;
|
||||
|
||||
if (_entityManager.IsClientSide(args.BaseArgs.Owner))
|
||||
return;
|
||||
|
||||
_sawmill.Error($"""
|
||||
Added component {comp.Name} to entity {_entityManager.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
|
||||
Stack trace:
|
||||
{Environment.StackTrace}
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
@@ -208,9 +232,9 @@ namespace Robust.Client.GameStates
|
||||
return default;
|
||||
}
|
||||
|
||||
DebugTools.AssertNotNull(_players.LocalPlayer);
|
||||
DebugTools.Assert(_players.LocalSession != null);
|
||||
|
||||
var evArgs = new EntitySessionEventArgs(_players.LocalPlayer!.Session);
|
||||
var evArgs = new EntitySessionEventArgs(_players.LocalSession);
|
||||
_pendingSystemMessages.Enqueue((_nextInputCmdSeq, _timing.CurTick, message,
|
||||
new EntitySessionMessage<T>(evArgs, message)));
|
||||
|
||||
@@ -233,7 +257,7 @@ namespace Robust.Client.GameStates
|
||||
public void UpdateFullRep(GameState state, bool cloneDelta = false)
|
||||
=> _processor.UpdateFullRep(state, cloneDelta);
|
||||
|
||||
public Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep()
|
||||
public Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep()
|
||||
=> _processor.GetFullRep();
|
||||
|
||||
private void HandlePvsLeaveMessage(MsgStateLeavePvs message)
|
||||
@@ -319,7 +343,7 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
// If we were waiting for a new state, we are now applying it.
|
||||
if (_processor.WaitingForFull)
|
||||
if (curState.FromSequence == GameTick.Zero)
|
||||
{
|
||||
_processor.OnFullStateReceived();
|
||||
_timing.LastProcessedTick = curState.ToSequence;
|
||||
@@ -327,7 +351,10 @@ namespace Robust.Client.GameStates
|
||||
PartialStateReset(curState, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugTools.Assert(!_processor.WaitingForFull);
|
||||
_timing.LastProcessedTick += 1;
|
||||
}
|
||||
|
||||
_timing.CurTick = _timing.LastRealTick = _timing.LastProcessedTick;
|
||||
|
||||
@@ -508,7 +535,6 @@ namespace Robust.Client.GameStates
|
||||
using var __ = _timing.StartStateApplicationArea();
|
||||
|
||||
// This is terrible, and I hate it. This also needs to run even when prediction is disabled.
|
||||
_entitySystemManager.GetEntitySystem<SharedGridTraversalSystem>().QueuedEvents.Clear();
|
||||
_entitySystemManager.GetEntitySystem<TransformSystem>().Reset();
|
||||
|
||||
if (!PredictionNeedsResetting)
|
||||
@@ -536,40 +562,50 @@ namespace Robust.Client.GameStates
|
||||
|
||||
countReset += 1;
|
||||
|
||||
foreach (var (netId, comp) in meta.NetComponents)
|
||||
try
|
||||
{
|
||||
if (!comp.NetSyncEnabled)
|
||||
continue;
|
||||
_resettingPredictedEntities = true;
|
||||
|
||||
// Was this component added during prediction?
|
||||
if (comp.CreationTick > _timing.LastRealTick)
|
||||
foreach (var (netId, comp) in meta.NetComponents)
|
||||
{
|
||||
if (last.ContainsKey(netId))
|
||||
if (!comp.NetSyncEnabled)
|
||||
continue;
|
||||
|
||||
// Was this component added during prediction?
|
||||
if (comp.CreationTick > _timing.LastRealTick)
|
||||
{
|
||||
// Component was probably removed and then re-addedd during a single prediction run
|
||||
// Just reset state as normal.
|
||||
comp.ClearCreationTick();
|
||||
if (last.ContainsKey(netId))
|
||||
{
|
||||
// Component was probably removed and then re-addedd during a single prediction run
|
||||
// Just reset state as normal.
|
||||
comp.ClearCreationTick();
|
||||
}
|
||||
else
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A new component was added: {comp.GetType()}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (comp.LastModifiedTick <= _timing.LastRealTick ||
|
||||
!last.TryGetValue(netId, out var compState))
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A new component was added: {comp.GetType()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
|
||||
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
|
||||
if (comp.LastModifiedTick <= _timing.LastRealTick || !last.TryGetValue(netId, out var compState))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
|
||||
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_resettingPredictedEntities = false;
|
||||
}
|
||||
|
||||
// Remove predicted component additions
|
||||
@@ -631,14 +667,13 @@ namespace Robust.Client.GameStates
|
||||
|
||||
foreach (var netEntity in createdEntities)
|
||||
{
|
||||
var (createdEntity, meta) = _entityManager.GetEntityData(netEntity);
|
||||
var (_, meta) = _entityManager.GetEntityData(netEntity);
|
||||
var compData = _compDataPool.Get();
|
||||
_outputData.Add(netEntity, compData);
|
||||
|
||||
foreach (var (netId, component) in meta.NetComponents)
|
||||
{
|
||||
if (!component.NetSyncEnabled)
|
||||
continue;
|
||||
DebugTools.Assert(component.NetSyncEnabled);
|
||||
|
||||
var state = _entityManager.GetComponentState(bus, component, null, GameTick.Zero);
|
||||
DebugTools.Assert(state is not IComponentDeltaState delta || delta.FullState);
|
||||
@@ -744,15 +779,13 @@ namespace Robust.Client.GameStates
|
||||
newMeta.LastStateApplied = curState.ToSequence;
|
||||
|
||||
// Check if there's any component states awaiting this entity.
|
||||
if (_entityManager.PendingNetEntityStates.TryGetValue(es.NetEntity, out var value))
|
||||
if (_entityManager.PendingNetEntityStates.Remove(es.NetEntity, out var value))
|
||||
{
|
||||
foreach (var (type, owner) in value)
|
||||
{
|
||||
var pending = _pendingReapplyNetStates.GetOrNew(owner);
|
||||
pending.Add(type);
|
||||
}
|
||||
|
||||
_entityManager.PendingNetEntityStates.Remove(es.NetEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,6 +849,17 @@ namespace Robust.Client.GameStates
|
||||
if (!metas.TryGetComponent(uid, out var meta))
|
||||
continue;
|
||||
|
||||
// It may also have been queued for deletion, in which case its last server state entry has already been removed.
|
||||
// I love me some spaghetti order-of-operation dependent code
|
||||
|
||||
if (!_processor._lastStateFullRep.ContainsKey(meta.NetEntity))
|
||||
{
|
||||
DebugTools.Assert(curState.EntityDeletions.Value.Contains(meta.NetEntity));
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(!curState.EntityDeletions.Value.Contains(meta.NetEntity));
|
||||
|
||||
// State already being re-applied so don't bulldoze it.
|
||||
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid, out var exists);
|
||||
|
||||
@@ -949,16 +993,15 @@ namespace Robust.Client.GameStates
|
||||
xformSys.DetachParentToNull(ent, xform);
|
||||
|
||||
// Then detach all children.
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
xformSys.DetachParentToNull(child.Value, xforms.GetComponent(child.Value), xform);
|
||||
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
|
||||
|
||||
if (deleteClientChildren
|
||||
&& !deleteClientEntities // don't add duplicates
|
||||
&& _entities.IsClientSide(child.Value))
|
||||
&& _entities.IsClientSide(child))
|
||||
{
|
||||
_toDelete.Add(child.Value);
|
||||
_toDelete.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1006,7 +1049,7 @@ namespace Robust.Client.GameStates
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
{
|
||||
xformSys.DetachParentToNull(child.Value, xforms.GetComponent(child.Value), xform);
|
||||
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
|
||||
}
|
||||
|
||||
// Finally, delete the entity.
|
||||
@@ -1097,7 +1140,7 @@ namespace Robust.Client.GameStates
|
||||
(containerMeta.Flags & MetaDataFlags.Detached) == 0 &&
|
||||
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container, null, true))
|
||||
{
|
||||
container.Remove(ent.Value, _entities, xform, meta, false, true);
|
||||
containerSys.Remove((ent.Value, xform, meta), container, false, true);
|
||||
}
|
||||
|
||||
meta._flags |= MetaDataFlags.Detached;
|
||||
@@ -1185,7 +1228,9 @@ namespace Robust.Client.GameStates
|
||||
|
||||
foreach (var (id, comp) in meta.NetComponents)
|
||||
{
|
||||
if (comp.NetSyncEnabled && !curState.NetComponents.Contains(id))
|
||||
DebugTools.Assert(comp.NetSyncEnabled);
|
||||
|
||||
if (!curState.NetComponents.Contains(id))
|
||||
_toRemove.Add(comp);
|
||||
}
|
||||
|
||||
@@ -1395,7 +1440,7 @@ namespace Robust.Client.GameStates
|
||||
void _recursiveRemoveState(NetEntity netEntity, TransformComponent xform, EntityQuery<MetaDataComponent> metaQuery, EntityQuery<TransformComponent> xformQuery)
|
||||
{
|
||||
_processor._lastStateFullRep.Remove(netEntity);
|
||||
foreach (var child in xform.ChildEntities)
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
if (xformQuery.TryGetComponent(child, out var childXform) &&
|
||||
metaQuery.TryGetComponent(child, out var childMeta))
|
||||
|
||||
@@ -14,8 +14,6 @@ namespace Robust.Client.GameStates
|
||||
/// <inheritdoc />
|
||||
internal sealed class GameStateProcessor : IGameStateProcessor
|
||||
{
|
||||
public const int MaxBufferSize = 512;
|
||||
|
||||
private readonly IClientGameTiming _timing;
|
||||
private readonly IClientGameStateManager _state;
|
||||
private readonly ISawmill _logger;
|
||||
@@ -28,11 +26,13 @@ namespace Robust.Client.GameStates
|
||||
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
|
||||
|
||||
private int _bufferSize;
|
||||
private int _maxBufferSize = 512;
|
||||
public const int MinimumMaxBufferSize = 256;
|
||||
|
||||
/// <summary>
|
||||
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
|
||||
/// </summary>
|
||||
internal readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _lastStateFullRep
|
||||
internal readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState>> _lastStateFullRep
|
||||
= new();
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -48,7 +48,14 @@ namespace Robust.Client.GameStates
|
||||
public int BufferSize
|
||||
{
|
||||
get => _bufferSize;
|
||||
set => _bufferSize = value < 0 ? 0 : value;
|
||||
set => _bufferSize = Math.Max(value, 0);
|
||||
}
|
||||
|
||||
public int MaxBufferSize
|
||||
{
|
||||
get => _maxBufferSize;
|
||||
// We place a lower bound on the maximum size to avoid spamming servers with full game state requests.
|
||||
set => _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -100,21 +107,21 @@ namespace Robust.Client.GameStates
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value.Tick)
|
||||
if (LastFullState == null && state.FromSequence == GameTick.Zero)
|
||||
{
|
||||
LastFullState = state;
|
||||
|
||||
if (Logging)
|
||||
if (state.ToSequence >= LastFullStateRequested!.Value.Tick)
|
||||
{
|
||||
LastFullState = state;
|
||||
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
_logger.Info($"Received a late full game state. Received: {state.ToSequence}. Requested: {LastFullStateRequested.Value.Tick}");
|
||||
}
|
||||
|
||||
if (LastFullState != null && state.ToSequence <= LastFullState.ToSequence)
|
||||
{
|
||||
if (Logging)
|
||||
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
|
||||
|
||||
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -205,7 +212,7 @@ Had full state: {LastFullState != null}"
|
||||
{
|
||||
if (!_lastStateFullRep.TryGetValue(entityState.NetEntity, out var compData))
|
||||
{
|
||||
compData = new Dictionary<ushort, ComponentState>();
|
||||
compData = new Dictionary<ushort, IComponentState>();
|
||||
_lastStateFullRep.Add(entityState.NetEntity, compData);
|
||||
}
|
||||
|
||||
@@ -384,7 +391,7 @@ Had full state: {LastFullState != null}"
|
||||
LastFullStateRequested = null;
|
||||
}
|
||||
|
||||
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> implicitData)
|
||||
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState>> implicitData)
|
||||
{
|
||||
foreach (var (netEntity, implicitEntState) in implicitData)
|
||||
{
|
||||
@@ -418,18 +425,18 @@ Had full state: {LastFullState != null}"
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<ushort, ComponentState> GetLastServerStates(NetEntity netEntity)
|
||||
public Dictionary<ushort, IComponentState> GetLastServerStates(NetEntity netEntity)
|
||||
{
|
||||
return _lastStateFullRep[netEntity];
|
||||
}
|
||||
|
||||
public Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep()
|
||||
public Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep()
|
||||
{
|
||||
return _lastStateFullRep;
|
||||
}
|
||||
|
||||
public bool TryGetLastServerStates(NetEntity entity,
|
||||
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary)
|
||||
[NotNullWhen(true)] out Dictionary<ushort, IComponentState>? dictionary)
|
||||
{
|
||||
return _lastStateFullRep.TryGetValue(entity, out dictionary);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace Robust.Client.GameStates
|
||||
/// <summary>
|
||||
/// Returns the full collection of cached game states that are used to reset predicted entities.
|
||||
/// </summary>
|
||||
Dictionary<NetEntity, Dictionary<ushort, ComponentState>> GetFullRep();
|
||||
Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep();
|
||||
|
||||
/// <summary>
|
||||
/// This will perform some setup in order to reset the game to an earlier state. To fully reset the state
|
||||
|
||||
@@ -83,13 +83,13 @@ namespace Robust.Client.GameStates
|
||||
/// The data to merge.
|
||||
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
|
||||
/// </param>
|
||||
void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> data);
|
||||
void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState>> data);
|
||||
|
||||
/// <summary>
|
||||
/// Get the last state data from the server for an entity.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary (net ID -> ComponentState)</returns>
|
||||
Dictionary<ushort, ComponentState> GetLastServerStates(NetEntity entity);
|
||||
Dictionary<ushort, IComponentState> GetLastServerStates(NetEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the number of applicable states in the game state buffer from a given tick.
|
||||
@@ -99,6 +99,6 @@ namespace Robust.Client.GameStates
|
||||
int GetApplicableStateCount(GameTick? fromTick);
|
||||
|
||||
bool TryGetLastServerStates(NetEntity entity,
|
||||
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
|
||||
[NotNullWhen(true)] out Dictionary<ushort, IComponentState>? dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
entity = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
|
||||
entity = _playerManager.LocalEntity ?? EntityUid.Invalid;
|
||||
}
|
||||
else if (!NetEntity.TryParse(args[0], out var netEntity) || !_entManager.TryGetEntity(netEntity, out entity))
|
||||
{
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
// Used to track audio sources that were disposed in the finalizer thread,
|
||||
// so we need to properly send them off in the main thread.
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
|
||||
|
||||
private void _flushALDisposeQueues()
|
||||
{
|
||||
// Clear out finalized audio sources.
|
||||
while (_sourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_audioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized buffered audio sources.
|
||||
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
|
||||
{
|
||||
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
|
||||
if (IsEfxSupported) RemoveEfx(handles);
|
||||
AL.DeleteSource(handles.sourceHandle);
|
||||
_checkAlError();
|
||||
_bufferedAudioSources.Remove(handles.sourceHandle);
|
||||
}
|
||||
|
||||
// Clear out finalized audio buffers.
|
||||
while (_bufferDisposeQueue.TryDequeue(out var handle))
|
||||
{
|
||||
AL.DeleteBuffer(handle);
|
||||
_checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
|
||||
{
|
||||
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
|
||||
{
|
||||
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
|
||||
}
|
||||
|
||||
private void DeleteAudioBufferOnMainThread(int bufferHandle)
|
||||
{
|
||||
_bufferDisposeQueue.Enqueue(bufferHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,680 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenTK.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
private sealed class AudioSource : IClydeAudioSource
|
||||
{
|
||||
private int SourceHandle;
|
||||
private readonly ClydeAudio _master;
|
||||
private readonly AudioStream _sourceStream;
|
||||
private int FilterHandle;
|
||||
#if DEBUG
|
||||
private bool _didPositionWarning;
|
||||
#endif
|
||||
|
||||
private float _gain;
|
||||
|
||||
private bool IsEfxSupported => _master.IsEfxSupported;
|
||||
|
||||
public AudioSource(ClydeAudio master, int sourceHandle, AudioStream sourceStream)
|
||||
{
|
||||
_master = master;
|
||||
SourceHandle = sourceHandle;
|
||||
_sourceStream = sourceStream;
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
|
||||
}
|
||||
|
||||
public void StartPlaying()
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.SourcePlay(SourceHandle);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void StopPlaying()
|
||||
{
|
||||
if (_isDisposed()) return;
|
||||
AL.SourceStop(SourceHandle);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public bool IsPlaying
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
var state = AL.GetSourceState(SourceHandle);
|
||||
return state == ALSourceState.Playing;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLooping
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourceb.Looping, out var ret);
|
||||
_master._checkAlError();
|
||||
return ret;
|
||||
}
|
||||
set
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourceb.Looping, value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsGlobal
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourceb.SourceRelative, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGlobal()
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourceb.SourceRelative, true);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetVolume(float decibels)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
_gain = MathF.Pow(10, decibels / 10);
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float gain)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
_gain = gain;
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetMaxDistance(float distance)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.MaxDistance, distance);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetRolloffFactor(float rolloffFactor)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.RolloffFactor, rolloffFactor);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetReferenceDistance(float refDistance)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.ReferenceDistance, refDistance);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetOcclusion(float blocks)
|
||||
{
|
||||
_checkDisposed();
|
||||
var cutoff = MathF.Exp(-blocks * 1);
|
||||
var gain = MathF.Pow(cutoff, 0.1f);
|
||||
if (IsEfxSupported)
|
||||
{
|
||||
SetOcclusionEfx(gain, cutoff);
|
||||
}
|
||||
else
|
||||
{
|
||||
gain *= gain * gain;
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * gain);
|
||||
}
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
private void SetOcclusionEfx(float gain, float cutoff)
|
||||
{
|
||||
if (FilterHandle == 0)
|
||||
{
|
||||
FilterHandle = EFX.GenFilter();
|
||||
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
|
||||
}
|
||||
|
||||
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
|
||||
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
|
||||
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
|
||||
}
|
||||
|
||||
public void SetPlaybackPosition(float seconds)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.SecOffset, seconds);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public bool SetPosition(Vector2 position)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = position;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#if DEBUG
|
||||
// OpenAL doesn't seem to want to play stereo positionally.
|
||||
// Log a warning if people try to.
|
||||
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
|
||||
{
|
||||
_didPositionWarning = true;
|
||||
_master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
|
||||
_sourceStream.Name);
|
||||
// warning isn't enough, people just ignore it :(
|
||||
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
|
||||
}
|
||||
#endif
|
||||
|
||||
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
|
||||
_master._checkAlError();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool AreFinite(float x, float y)
|
||||
{
|
||||
if (float.IsFinite(x) && float.IsFinite(y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = velocity;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
|
||||
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle, ALSourcef.Pitch, pitch);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
~AudioSource()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
|
||||
_master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
|
||||
AL.DeleteSource(SourceHandle);
|
||||
_master._audioSources.Remove(SourceHandle);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
SourceHandle = -1;
|
||||
}
|
||||
|
||||
private bool _isDisposed()
|
||||
{
|
||||
return SourceHandle == -1;
|
||||
}
|
||||
|
||||
private void _checkDisposed()
|
||||
{
|
||||
if (SourceHandle == -1)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AudioSource));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BufferedAudioSource : IClydeBufferedAudioSource
|
||||
{
|
||||
private int? SourceHandle = null;
|
||||
private int[] BufferHandles;
|
||||
private Dictionary<int, int> BufferMap = new();
|
||||
private readonly ClydeAudio _master;
|
||||
private bool _mono = true;
|
||||
private bool _float = false;
|
||||
private int FilterHandle;
|
||||
|
||||
private float _gain;
|
||||
|
||||
public int SampleRate { get; set; } = 44100;
|
||||
|
||||
private bool IsEfxSupported => _master.IsEfxSupported;
|
||||
|
||||
public BufferedAudioSource(ClydeAudio master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
|
||||
{
|
||||
_master = master;
|
||||
SourceHandle = sourceHandle;
|
||||
BufferHandles = bufferHandles;
|
||||
for (int i = 0; i < BufferHandles.Length; i++)
|
||||
{
|
||||
var bufferHandle = BufferHandles[i];
|
||||
BufferMap[bufferHandle] = i;
|
||||
}
|
||||
_float = floatAudio;
|
||||
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
|
||||
}
|
||||
|
||||
public void StartPlaying()
|
||||
{
|
||||
_checkDisposed();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.SourcePlay(stackalloc int[] {SourceHandle!.Value});
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void StopPlaying()
|
||||
{
|
||||
if (_isDisposed()) return;
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.SourceStop(SourceHandle!.Value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public bool IsPlaying
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
var state = AL.GetSourceState(SourceHandle!.Value);
|
||||
return state == ALSourceState.Playing;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLooping
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetGlobal()
|
||||
{
|
||||
_checkDisposed();
|
||||
_mono = false;
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.Source(SourceHandle!.Value, ALSourceb.SourceRelative, true);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetLooping()
|
||||
{
|
||||
// TODO?waaaaddDDDDD
|
||||
}
|
||||
|
||||
public void SetVolume(float decibels)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
_gain = MathF.Pow(10, decibels / 10);
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float gain)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
_gain = gain;
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetMaxDistance(float distance)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.MaxDistance, distance);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetRolloffFactor(float rolloffFactor)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.RolloffFactor, rolloffFactor);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetReferenceDistance(float refDistance)
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.ReferenceDistance, refDistance);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetOcclusion(float blocks)
|
||||
{
|
||||
_checkDisposed();
|
||||
var cutoff = MathF.Exp(-blocks * 1.5f);
|
||||
var gain = MathF.Pow(cutoff, 0.1f);
|
||||
if (IsEfxSupported)
|
||||
{
|
||||
SetOcclusionEfx(gain, cutoff);
|
||||
}
|
||||
else
|
||||
{
|
||||
gain *= gain * gain;
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.Gain, gain * _gain);
|
||||
}
|
||||
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
private void SetOcclusionEfx(float gain, float cutoff)
|
||||
{
|
||||
if (FilterHandle == 0)
|
||||
{
|
||||
FilterHandle = EFX.GenFilter();
|
||||
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
|
||||
}
|
||||
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
|
||||
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
|
||||
AL.Source(SourceHandle!.Value, ALSourcei.EfxDirectFilter, FilterHandle);
|
||||
}
|
||||
|
||||
public void SetPlaybackPosition(float seconds)
|
||||
{
|
||||
_checkDisposed();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.SecOffset, seconds);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public bool IsGlobal
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle!.Value, ALSourceb.SourceRelative, out var value);
|
||||
_master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetPosition(Vector2 position)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = position;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_mono = true;
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.Source(SourceHandle!.Value, ALSource3f.Position, x, y, 0);
|
||||
_master._checkAlError();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool AreFinite(float x, float y)
|
||||
{
|
||||
if (float.IsFinite(x) && float.IsFinite(y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
var (x, y) = velocity;
|
||||
|
||||
if (!AreFinite(x, y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.Source(SourceHandle!.Value, ALSource3f.Velocity, x, y, 0);
|
||||
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
_checkDisposed();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.Pitch, pitch);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
~BufferedAudioSource()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (SourceHandle == null) return;
|
||||
|
||||
if (!_master.IsMainThread())
|
||||
{
|
||||
// We can't run this code inside another thread so tell Clyde to clear it up later.
|
||||
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
|
||||
for (var i = 0; i < BufferHandles.Length; i++)
|
||||
_master.DeleteAudioBufferOnMainThread(BufferHandles[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
|
||||
AL.DeleteSource(SourceHandle.Value);
|
||||
AL.DeleteBuffers(BufferHandles);
|
||||
_master._bufferedAudioSources.Remove(SourceHandle.Value);
|
||||
_master._checkAlError();
|
||||
}
|
||||
|
||||
SourceHandle = null;
|
||||
}
|
||||
|
||||
private bool _isDisposed()
|
||||
{
|
||||
return SourceHandle == null;
|
||||
}
|
||||
|
||||
private void _checkDisposed()
|
||||
{
|
||||
if (SourceHandle == null)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AudioSource));
|
||||
}
|
||||
}
|
||||
|
||||
public int GetNumberOfBuffersProcessed()
|
||||
{
|
||||
_checkDisposed();
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.GetSource(SourceHandle!.Value, ALGetSourcei.BuffersProcessed, out var buffersProcessed);
|
||||
return buffersProcessed;
|
||||
}
|
||||
|
||||
public unsafe void GetBuffersProcessed(Span<int> handles)
|
||||
{
|
||||
_checkDisposed();
|
||||
var entries = Math.Min(Math.Min(handles.Length, BufferHandles.Length), GetNumberOfBuffersProcessed());
|
||||
fixed (int* ptr = handles)
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.SourceUnqueueBuffers(SourceHandle!.Value, entries, ptr);
|
||||
|
||||
for (var i = 0; i < entries; i++)
|
||||
handles[i] = BufferMap[handles[i]];
|
||||
}
|
||||
|
||||
public unsafe void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
if(_float)
|
||||
throw new InvalidOperationException("Can't write ushort numbers to buffers when buffer type is float!");
|
||||
|
||||
if (handle >= BufferHandles.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(handle),
|
||||
$"Got {handle}. Expected less than {BufferHandles.Length}");
|
||||
|
||||
fixed (ushort* ptr = data)
|
||||
{
|
||||
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.Mono16 : ALFormat.Stereo16, (IntPtr) ptr,
|
||||
_mono ? data.Length / 2 * sizeof(ushort) : data.Length * sizeof(ushort), SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void WriteBuffer(int handle, ReadOnlySpan<float> data)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
if(!_float)
|
||||
throw new InvalidOperationException("Can't write float numbers to buffers when buffer type is ushort!");
|
||||
|
||||
if (handle >= BufferHandles.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(handle),
|
||||
$"Got {handle}. Expected less than {BufferHandles.Length}");
|
||||
|
||||
fixed (float* ptr = data)
|
||||
{
|
||||
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.MonoFloat32Ext : ALFormat.StereoFloat32Ext, (IntPtr) ptr,
|
||||
_mono ? data.Length / 2 * sizeof(float) : data.Length * sizeof(float), SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void QueueBuffers(ReadOnlySpan<int> handles)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
Span<int> realHandles = stackalloc int[handles.Length];
|
||||
handles.CopyTo(realHandles);
|
||||
|
||||
for (var i = 0; i < realHandles.Length; i++)
|
||||
{
|
||||
var handle = realHandles[i];
|
||||
if (handle >= BufferHandles.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(handles), $"Invalid handle with index {i}!");
|
||||
realHandles[i] = BufferHandles[handle];
|
||||
}
|
||||
|
||||
fixed (int* ptr = realHandles)
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
AL.SourceQueueBuffers(SourceHandle!.Value, handles.Length, ptr);
|
||||
}
|
||||
|
||||
public unsafe void EmptyBuffers()
|
||||
{
|
||||
_checkDisposed();
|
||||
var length = (SampleRate / BufferHandles.Length) * (_mono ? 1 : 2);
|
||||
|
||||
Span<int> handles = stackalloc int[BufferHandles.Length];
|
||||
|
||||
if (_float)
|
||||
{
|
||||
var empty = new float[length];
|
||||
var span = (Span<float>) empty;
|
||||
|
||||
for (var i = 0; i < BufferHandles.Length; i++)
|
||||
{
|
||||
WriteBuffer(BufferMap[BufferHandles[i]], span);
|
||||
handles[i] = BufferMap[BufferHandles[i]];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var empty = new ushort[length];
|
||||
var span = (Span<ushort>) empty;
|
||||
|
||||
for (var i = 0; i < BufferHandles.Length; i++)
|
||||
{
|
||||
WriteBuffer(BufferMap[BufferHandles[i]], span);
|
||||
handles[i] = BufferMap[BufferHandles[i]];
|
||||
}
|
||||
}
|
||||
|
||||
QueueBuffers(handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
private OggVorbisData _readOggVorbis(Stream stream)
|
||||
{
|
||||
using (var vorbis = new NVorbis.VorbisReader(stream, false))
|
||||
{
|
||||
var sampleRate = vorbis.SampleRate;
|
||||
var channels = vorbis.Channels;
|
||||
var totalSamples = vorbis.TotalSamples;
|
||||
|
||||
var readSamples = 0;
|
||||
var buffer = new float[totalSamples * channels];
|
||||
|
||||
while (readSamples < totalSamples)
|
||||
{
|
||||
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
|
||||
if (read == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
readSamples += read;
|
||||
}
|
||||
|
||||
return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct OggVorbisData
|
||||
{
|
||||
public readonly long TotalSamples;
|
||||
public readonly long SampleRate;
|
||||
public readonly long Channels;
|
||||
public readonly ReadOnlyMemory<float> Data;
|
||||
public readonly string Title;
|
||||
public readonly string Artist;
|
||||
|
||||
public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory<float> data, string title, string artist)
|
||||
{
|
||||
TotalSamples = totalSamples;
|
||||
SampleRate = sampleRate;
|
||||
Channels = channels;
|
||||
Data = data;
|
||||
Title = title;
|
||||
Artist = artist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
/// <summary>
|
||||
/// Load up a WAVE file.
|
||||
/// </summary>
|
||||
private static WavData _readWav(Stream stream)
|
||||
{
|
||||
var reader = new BinaryReader(stream, EncodingHelpers.UTF8, true);
|
||||
|
||||
void SkipChunk()
|
||||
{
|
||||
var length = reader.ReadUInt32();
|
||||
stream.Position += length;
|
||||
}
|
||||
|
||||
// Read outer most chunks.
|
||||
Span<byte> fourCc = stackalloc byte[4];
|
||||
while (true)
|
||||
{
|
||||
_readFourCC(reader, fourCc);
|
||||
|
||||
if (!fourCc.SequenceEqual("RIFF"u8))
|
||||
{
|
||||
SkipChunk();
|
||||
continue;
|
||||
}
|
||||
|
||||
return _readRiffChunk(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _skipChunk(BinaryReader reader)
|
||||
{
|
||||
var length = reader.ReadUInt32();
|
||||
reader.BaseStream.Position += length;
|
||||
}
|
||||
|
||||
private static void _readFourCC(BinaryReader reader, Span<byte> fourCc)
|
||||
{
|
||||
fourCc[0] = reader.ReadByte();
|
||||
fourCc[1] = reader.ReadByte();
|
||||
fourCc[2] = reader.ReadByte();
|
||||
fourCc[3] = reader.ReadByte();
|
||||
}
|
||||
|
||||
private static WavData _readRiffChunk(BinaryReader reader)
|
||||
{
|
||||
Span<byte> format = stackalloc byte[4];
|
||||
reader.ReadUInt32();
|
||||
_readFourCC(reader, format);
|
||||
if (!format.SequenceEqual("WAVE"u8))
|
||||
{
|
||||
throw new InvalidDataException("File is not a WAVE file.");
|
||||
}
|
||||
|
||||
_readFourCC(reader, format);
|
||||
if (!format.SequenceEqual("fmt "u8))
|
||||
{
|
||||
throw new InvalidDataException("Expected fmt chunk.");
|
||||
}
|
||||
|
||||
// Read fmt chunk.
|
||||
|
||||
var size = reader.ReadInt32();
|
||||
var afterFmtPos = reader.BaseStream.Position + size;
|
||||
|
||||
var audioType = (WavAudioFormatType) reader.ReadInt16();
|
||||
var channels = reader.ReadInt16();
|
||||
var sampleRate = reader.ReadInt32();
|
||||
var byteRate = reader.ReadInt32();
|
||||
var blockAlign = reader.ReadInt16();
|
||||
var bitsPerSample = reader.ReadInt16();
|
||||
|
||||
if (audioType != WavAudioFormatType.PCM)
|
||||
{
|
||||
throw new NotImplementedException("Unable to support audio types other than PCM.");
|
||||
}
|
||||
|
||||
DebugTools.Assert(byteRate == sampleRate * channels * bitsPerSample / 8);
|
||||
|
||||
// Fmt is not of guaranteed size, so use the size header to skip to the end.
|
||||
reader.BaseStream.Position = afterFmtPos;
|
||||
|
||||
while (true)
|
||||
{
|
||||
_readFourCC(reader, format);
|
||||
if (!format.SequenceEqual("data"u8))
|
||||
{
|
||||
_skipChunk(reader);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// We are in the data chunk.
|
||||
size = reader.ReadInt32();
|
||||
var data = reader.ReadBytes(size);
|
||||
|
||||
return new WavData(audioType, channels, sampleRate, byteRate, blockAlign, bitsPerSample, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See http://soundfile.sapp.org/doc/WaveFormat/ for reference.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
private readonly struct WavData
|
||||
{
|
||||
public readonly WavAudioFormatType AudioType;
|
||||
public readonly short NumChannels;
|
||||
public readonly int SampleRate;
|
||||
public readonly int ByteRate;
|
||||
public readonly short BlockAlign;
|
||||
public readonly short BitsPerSample;
|
||||
public readonly ReadOnlyMemory<byte> Data;
|
||||
|
||||
public WavData(WavAudioFormatType audioType, short numChannels, int sampleRate, int byteRate,
|
||||
short blockAlign, short bitsPerSample, ReadOnlyMemory<byte> data)
|
||||
{
|
||||
AudioType = audioType;
|
||||
NumChannels = numChannels;
|
||||
SampleRate = sampleRate;
|
||||
ByteRate = byteRate;
|
||||
BlockAlign = blockAlign;
|
||||
BitsPerSample = bitsPerSample;
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
|
||||
private enum WavAudioFormatType : short
|
||||
{
|
||||
Unknown = 0,
|
||||
PCM = 1,
|
||||
// There's a bunch of other types, those are all unsupported.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenTK.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal partial class ClydeAudio
|
||||
{
|
||||
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
|
||||
|
||||
private Thread? _gameThread;
|
||||
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
_gameThread = Thread.CurrentThread;
|
||||
return _initializeAudio();
|
||||
}
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
_updateAudio();
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_shutdownAudio();
|
||||
}
|
||||
|
||||
private bool IsMainThread()
|
||||
{
|
||||
return Thread.CurrentThread == _gameThread;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using OpenTK.Mathematics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
internal sealed partial class ClydeAudio : IClydeAudio, IClydeAudioInternal
|
||||
{
|
||||
private ALDevice _openALDevice;
|
||||
private ALContext _openALContext;
|
||||
|
||||
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
|
||||
new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
|
||||
new();
|
||||
|
||||
private readonly HashSet<string> _alcDeviceExtensions = new();
|
||||
private readonly HashSet<string> _alContextExtensions = new();
|
||||
|
||||
// The base gain value for a listener, used to boost the default volume.
|
||||
private const float _baseGain = 2f;
|
||||
|
||||
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
|
||||
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
|
||||
|
||||
internal bool IsEfxSupported;
|
||||
|
||||
internal ISawmill OpenALSawmill = default!;
|
||||
|
||||
private bool _initializeAudio()
|
||||
{
|
||||
OpenALSawmill = _logMan.GetSawmill("clyde.oal");
|
||||
|
||||
if (!_audioOpenDevice())
|
||||
return false;
|
||||
|
||||
// Create OpenAL context.
|
||||
_audioCreateContext();
|
||||
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
|
||||
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void _audioCreateContext()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
|
||||
}
|
||||
|
||||
ALC.MakeContextCurrent(_openALContext);
|
||||
_checkAlcError(_openALDevice);
|
||||
_checkAlError();
|
||||
|
||||
// Load up AL context extensions.
|
||||
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alContextExtensions.Add(extension);
|
||||
}
|
||||
|
||||
OpenALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
|
||||
OpenALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
|
||||
OpenALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
|
||||
}
|
||||
|
||||
private bool _audioOpenDevice()
|
||||
{
|
||||
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
|
||||
|
||||
// Open device.
|
||||
if (!string.IsNullOrEmpty(preferredDevice))
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(preferredDevice);
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
|
||||
preferredDevice, ALC.GetError(ALDevice.Null));
|
||||
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_openALDevice = ALC.OpenDevice(null);
|
||||
}
|
||||
|
||||
_checkAlcError(_openALDevice);
|
||||
|
||||
if (_openALDevice == IntPtr.Zero)
|
||||
{
|
||||
OpenALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load up ALC extensions.
|
||||
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alcDeviceExtensions.Add(extension);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void StopAllAudio()
|
||||
{
|
||||
foreach (var (key, source) in _audioSources)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, source) in _bufferedAudioSources)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.StopPlaying();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DisposeAllAudio()
|
||||
{
|
||||
foreach (var (key, source) in _audioSources)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
_audioSources.Clear();
|
||||
|
||||
foreach (var (key, source) in _bufferedAudioSources)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.StopPlaying();
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
_bufferedAudioSources.Clear();
|
||||
}
|
||||
|
||||
private void _shutdownAudio()
|
||||
{
|
||||
DisposeAllAudio();
|
||||
|
||||
if (_openALContext != ALContext.Null)
|
||||
{
|
||||
ALC.MakeContextCurrent(ALContext.Null);
|
||||
|
||||
ALC.DestroyContext(_openALContext);
|
||||
}
|
||||
|
||||
if (_openALDevice != IntPtr.Zero)
|
||||
{
|
||||
ALC.CloseDevice(_openALDevice);
|
||||
}
|
||||
}
|
||||
|
||||
private void _updateAudio()
|
||||
{
|
||||
var eye = _eyeManager.CurrentEye;
|
||||
var vec = eye.Position.Position;
|
||||
AL.Listener(ALListener3f.Position, vec.X, vec.Y, -5);
|
||||
var rot2d = eye.Rotation.ToVec();
|
||||
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
|
||||
|
||||
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
|
||||
var rot = eye.Rotation.ToVec();
|
||||
var at = new Vector3(0f, 0f, -1f);
|
||||
var up = new Vector3(rot.Y, rot.X, 0f);
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
|
||||
_flushALDisposeQueues();
|
||||
}
|
||||
|
||||
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
|
||||
{
|
||||
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
|
||||
}
|
||||
|
||||
public void SetAudioAttenuation(int value)
|
||||
{
|
||||
var attenuation = (Attenuation) value;
|
||||
|
||||
switch (attenuation)
|
||||
{
|
||||
case Attenuation.NoAttenuation:
|
||||
AL.DistanceModel(ALDistanceModel.None);
|
||||
break;
|
||||
case Attenuation.InverseDistance:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistance);
|
||||
break;
|
||||
case Attenuation.Default:
|
||||
case Attenuation.InverseDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
|
||||
break;
|
||||
case Attenuation.LinearDistance:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistance);
|
||||
break;
|
||||
case Attenuation.LinearDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
|
||||
break;
|
||||
case Attenuation.ExponentDistance:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistance);
|
||||
break;
|
||||
case Attenuation.ExponentDistanceClamped:
|
||||
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
|
||||
}
|
||||
|
||||
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
|
||||
|
||||
OpenALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
|
||||
}
|
||||
|
||||
public IClydeAudioSource? CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
|
||||
if (!AL.IsSource(source))
|
||||
{
|
||||
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
// TODO: This really shouldn't be indexing based on the ClydeHandle...
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value.Value].BufferHandle);
|
||||
|
||||
var audioSource = new AudioSource(this, source, stream);
|
||||
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
|
||||
if (!AL.IsSource(source))
|
||||
throw new Exception("Failed to generate source. Too many simultaneous audio streams?");
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
|
||||
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
|
||||
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
private void _checkAlcError(ALDevice device,
|
||||
[CallerMemberName] string callerMember = "",
|
||||
[CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = ALC.GetError(device);
|
||||
if (error != AlcError.NoError)
|
||||
{
|
||||
OpenALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
private void _checkAlError([CallerMemberName] string callerMember = "",
|
||||
[CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
var error = AL.GetError();
|
||||
if (error != ALError.NoError)
|
||||
{
|
||||
OpenALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
|
||||
}
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
var vorbis = _readOggVorbis(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
// NVorbis only supports loading into floats.
|
||||
// If this becomes a problem due to missing extension support (doubt it but ok),
|
||||
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
|
||||
if (vorbis.Channels == 1)
|
||||
{
|
||||
format = ALFormat.MonoFloat32Ext;
|
||||
}
|
||||
else if (vorbis.Channels == 2)
|
||||
{
|
||||
format = ALFormat.StereoFloat32Ext;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (float* ptr = vorbis.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
|
||||
(int) vorbis.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
|
||||
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
var wav = _readWav(stream);
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
ALFormat format;
|
||||
if (wav.BitsPerSample == 16)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono16;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo16;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else if (wav.BitsPerSample == 8)
|
||||
{
|
||||
if (wav.NumChannels == 1)
|
||||
{
|
||||
format = ALFormat.Mono8;
|
||||
}
|
||||
else if (wav.NumChannels == 2)
|
||||
{
|
||||
format = ALFormat.Stereo8;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* ptr = wav.Data.Span)
|
||||
{
|
||||
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
|
||||
return new AudioStream(handle, length, wav.NumChannels, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var fmt = channels switch
|
||||
{
|
||||
1 => ALFormat.Mono16,
|
||||
2 => ALFormat.Stereo16,
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(channels), "Only stereo and mono is currently supported")
|
||||
};
|
||||
|
||||
var buffer = AL.GenBuffer();
|
||||
_checkAlError();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (short* ptr = samples)
|
||||
{
|
||||
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
return new AudioStream(handle, length, channels, name);
|
||||
}
|
||||
|
||||
private sealed class LoadedAudioSample
|
||||
{
|
||||
public readonly int BufferHandle;
|
||||
|
||||
public LoadedAudioSample(int bufferHandle)
|
||||
{
|
||||
BufferHandle = bufferHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user