mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a1fa1fba | ||
|
|
fb08451849 | ||
|
|
ebea0d7572 | ||
|
|
eb6f28cce0 | ||
|
|
a1d02d7c55 | ||
|
|
777ab85cff | ||
|
|
d33a8465b0 | ||
|
|
6572fdb404 | ||
|
|
6273b1b80d | ||
|
|
a09a60efe9 | ||
|
|
d3339964ee | ||
|
|
3ffef625ec | ||
|
|
4fd9b2bc3b | ||
|
|
adc5051841 | ||
|
|
2733435218 | ||
|
|
24b0165ec9 | ||
|
|
7b9aa09b18 | ||
|
|
7bee6f6fc1 | ||
|
|
ff75495894 | ||
|
|
4cb51af733 | ||
|
|
89c1e90646 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -13,6 +13,3 @@
|
||||
[submodule "cefglue"]
|
||||
path = cefglue
|
||||
url = https://github.com/space-wizards/cefglue.git
|
||||
[submodule "Arch/Arch"]
|
||||
path = Arch/Arch
|
||||
url = https://github.com/space-wizards/Arch.git
|
||||
|
||||
Submodule Arch/Arch deleted from c76d18feb7
@@ -1,94 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
|
||||
<PackageId>Arch</PackageId>
|
||||
<Title>Arch</Title>
|
||||
<Version>1.2.7.1-alpha</Version>
|
||||
<Authors>genaray</Authors>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<Description>A high performance c# net.6 and net.7 archetype based ECS ( Entity component system ).</Description>
|
||||
<PackageReleaseNotes>Updated LowLevel which fixes bugs. </PackageReleaseNotes>
|
||||
<PackageTags>c#;.net;.net6;.net7;ecs;game;entity;gamedev; game-development; game-engine; entity-component-system;stride;unity;godot;</PackageTags>
|
||||
|
||||
<PackageProjectUrl>https://github.com/genaray/Arch</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/genaray/Arch.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
<LangVersion>11</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Copyright>Apache2.0</Copyright>
|
||||
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
|
||||
<Configurations>Debug;Debug-PureECS;Debug-Events;Release;Release-PureECS;Release-Events;</Configurations>
|
||||
|
||||
<AssemblyName>Arch</AssemblyName>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<DefaultItemExcludes>src/Arch/**/*</DefaultItemExcludes>
|
||||
<DefineConstants>$(DefineConstants);PURE_ECS;CONTRACTS_FULL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DefineConstants>$(DefineConstants);PURE_ECS;CONTRACTS_FULL;TRACE;</DefineConstants>
|
||||
<Optimize>false</Optimize>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Arch.Benchmarks" />
|
||||
<InternalsVisibleTo Include="Arch.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System" />
|
||||
<Using Include="System.Collections" />
|
||||
<Using Include="System.Collections.Generic" />
|
||||
<Using Include="System.Diagnostics" />
|
||||
<Using Include="System.Diagnostics.CodeAnalysis" />
|
||||
<Using Include="System.IO" />
|
||||
<Using Include="System.Linq" />
|
||||
<Using Include="System.Runtime.CompilerServices" />
|
||||
<Using Include="System.Runtime.InteropServices" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include=".\Arch\src\Arch.SourceGen\Arch.SourceGen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Arch.LowLevel" Version="1.1.0" />
|
||||
<PackageReference Include="Collections.Pooled" Version="2.0.0-preview.27" />
|
||||
<PackageReference Include="CommunityToolkit.HighPerformance" Version="7.1.2" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
|
||||
<PackageReference Include="ZeroAllocJobScheduler" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Arch\src\Arch\**\*.cs">
|
||||
<Link>Arch\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Compile>
|
||||
<Compile Remove="Arch\src\Arch\obj\**\*.cs" />
|
||||
<InternalsVisibleTo Include="Arch.Benchmarks" />
|
||||
<InternalsVisibleTo Include="Arch.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="../MSBuild/Robust.Properties.targets" />
|
||||
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -54,6 +54,67 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -2203,207 +2203,3 @@
|
||||
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.
|
||||
|
||||
- name: Arch
|
||||
license: |
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 Lars Matthäus/genaray
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
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
@@ -561,3 +561,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>
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Arch.Core;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Engines;
|
||||
using Robust.Shared.Analyzers;
|
||||
using static Robust.Benchmarks.EntityManager.ArchetypeComponentAccessBenchmark;
|
||||
|
||||
namespace Robust.Benchmarks.Arch;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
[Virtual]
|
||||
public class ArchComponentAccessBenchmark
|
||||
{
|
||||
private const int N = 10000;
|
||||
|
||||
private static readonly Consumer Consumer = new();
|
||||
private Entity _entity;
|
||||
private World _world = default!;
|
||||
private QueryDescription _singleQuery;
|
||||
private QueryDescription _tenQuery;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
var _ = new JobScheduler.JobScheduler("ArchBenchmark");
|
||||
|
||||
_world = World.Create();
|
||||
|
||||
for (var i = 0; i < N; i++)
|
||||
{
|
||||
var entity = _world.Create();
|
||||
|
||||
// Randomly chosen id
|
||||
if (entity.Id == 1584)
|
||||
_entity = entity;
|
||||
|
||||
_world.Add(
|
||||
entity,
|
||||
new Struct1(),
|
||||
new Struct2(),
|
||||
new Struct3(),
|
||||
new Struct4(),
|
||||
new Struct5(),
|
||||
new Struct6(),
|
||||
new Struct7(),
|
||||
new Struct8(),
|
||||
new Struct9(),
|
||||
new Struct10()
|
||||
);
|
||||
}
|
||||
|
||||
_singleQuery = new QueryDescription().WithAll<Struct1>();
|
||||
_tenQuery = new QueryDescription().WithAll<Struct1, Struct2, Struct3, Struct4, Struct5, Struct6, Struct7, Struct8, Struct9, Struct10>();
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public void GlobalCleanup()
|
||||
{
|
||||
JobScheduler.JobScheduler.Instance.Dispose();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public Struct1 GetSingle()
|
||||
{
|
||||
return _world.Get<Struct1>(_entity);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public (Struct1, Struct2, Struct3, Struct4, Struct5, Struct6, Struct7, Struct8, Struct9, Struct10)
|
||||
GetTen()
|
||||
{
|
||||
return (
|
||||
_world.Get<Struct1>(_entity),
|
||||
_world.Get<Struct2>(_entity),
|
||||
_world.Get<Struct3>(_entity),
|
||||
_world.Get<Struct4>(_entity),
|
||||
_world.Get<Struct5>(_entity),
|
||||
_world.Get<Struct6>(_entity),
|
||||
_world.Get<Struct7>(_entity),
|
||||
_world.Get<Struct8>(_entity),
|
||||
_world.Get<Struct9>(_entity),
|
||||
_world.Get<Struct10>(_entity)
|
||||
);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public bool HasSingle()
|
||||
{
|
||||
return _world.Has<Struct1>(_entity);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public bool HasTen()
|
||||
{
|
||||
return _world.Has<Struct1>(_entity) &&
|
||||
_world.Has<Struct2>(_entity) &&
|
||||
_world.Has<Struct3>(_entity) &&
|
||||
_world.Has<Struct4>(_entity) &&
|
||||
_world.Has<Struct5>(_entity) &&
|
||||
_world.Has<Struct6>(_entity) &&
|
||||
_world.Has<Struct7>(_entity) &&
|
||||
_world.Has<Struct8>(_entity) &&
|
||||
_world.Has<Struct9>(_entity) &&
|
||||
_world.Has<Struct10>(_entity);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateSingle()
|
||||
{
|
||||
_world.Query(_singleQuery, static (ref Struct1 s) => Consumer.Consume(s));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateSingleInline()
|
||||
{
|
||||
_world.InlineQuery<QueryConsumer>(_singleQuery);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateSingleParallel()
|
||||
{
|
||||
_world.ParallelQuery(_singleQuery, static (ref Struct1 s) => Consumer.Consume(s));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateSingleInlineParallel()
|
||||
{
|
||||
_world.InlineParallelQuery<QueryConsumer>(_singleQuery);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateTen()
|
||||
{
|
||||
_world.Query(_tenQuery,
|
||||
static (
|
||||
ref Struct1 s1, ref Struct2 s2, ref Struct3 s3, ref Struct4 s4,
|
||||
ref Struct5 s5, ref Struct6 s6, ref Struct7 s7, ref Struct8 s8,
|
||||
ref Struct9 s9, ref Struct10 s10) =>
|
||||
Consumer.Consume((s1, s2, s3, s4, s5, s6, s7, s8, s9, s10)));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateTenInline()
|
||||
{
|
||||
_world.InlineQuery<QueryConsumer>(_tenQuery);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateTenParallel()
|
||||
{
|
||||
_world.ParallelQuery(_tenQuery,
|
||||
static (
|
||||
ref Struct1 s1, ref Struct2 s2, ref Struct3 s3, ref Struct4 s4,
|
||||
ref Struct5 s5, ref Struct6 s6, ref Struct7 s7, ref Struct8 s8,
|
||||
ref Struct9 s9, ref Struct10 s10) =>
|
||||
Consumer.Consume((s1, s2, s3, s4, s5, s6, s7, s8, s9, s10)));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void IterateTenInlineParallel()
|
||||
{
|
||||
_world.InlineParallelQuery<QueryConsumer>(_tenQuery);
|
||||
}
|
||||
|
||||
private struct QueryConsumer : IForEach
|
||||
{
|
||||
private static readonly Consumer Consumer = new();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Update(Entity entity)
|
||||
{
|
||||
Consumer.Consume(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public partial class AddRemoveComponentBenchmark
|
||||
{
|
||||
for (var i = 2; i <= N+1; i++)
|
||||
{
|
||||
var uid = new EntityUid(i, -1);
|
||||
var uid = new EntityUid(i);
|
||||
_entityManager.AddComponent<A>(uid);
|
||||
_entityManager.RemoveComponent<A>(uid);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class ComponentIndexBenchmark
|
||||
private static class CompArrayIndex<T>
|
||||
{
|
||||
// ReSharper disable once StaticMemberInGenericType
|
||||
public static readonly CompIdx Idx = new(Interlocked.Increment(ref _compIndexMaster), typeof(T));
|
||||
public static readonly CompIdx Idx = new(Interlocked.Increment(ref _compIndexMaster));
|
||||
}
|
||||
|
||||
private static CompIdx GetCompIdIndex(Type type)
|
||||
|
||||
@@ -46,7 +46,7 @@ public partial class GetComponentBenchmark
|
||||
{
|
||||
for (var i = 2; i <= N+1; i++)
|
||||
{
|
||||
Comps[i] = _entityManager.GetComponent<A>(new EntityUid(i, -1));
|
||||
Comps[i] = _entityManager.GetComponent<A>(new EntityUid(i));
|
||||
}
|
||||
|
||||
// Return something so the JIT doesn't optimize out all the GetComponent calls.
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<NoWarn>RA0003</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Arch\Arch.csproj" />
|
||||
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
|
||||
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
|
||||
<ProjectReference Include="..\Robust.UnitTesting\Robust.UnitTesting.csproj" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
339
Robust.Client/Audio/AudioManager.Public.cs
Normal file
339
Robust.Client/Audio/AudioManager.Public.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
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 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.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);
|
||||
}
|
||||
|
||||
/// <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 SetMasterVolume(float newVolume)
|
||||
{
|
||||
AL.Listener(ALListenerf.Gain, newVolume);
|
||||
}
|
||||
|
||||
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));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
public IBufferedAudioSource 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);
|
||||
}
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
|
||||
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
|
||||
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
/// <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.Playing = false;
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_audioSources.Clear();
|
||||
|
||||
foreach (var source in _bufferedAudioSources.Values)
|
||||
{
|
||||
if (source.TryGetTarget(out var target))
|
||||
{
|
||||
target.Playing = false;
|
||||
target.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_bufferedAudioSources.Clear();
|
||||
}
|
||||
}
|
||||
161
Robust.Client/Audio/AudioManager.cs
Normal file
161
Robust.Client/Audio/AudioManager.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
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, SetMasterVolume, 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);
|
||||
}
|
||||
}
|
||||
|
||||
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.LocalPlayer?.ControlledEntity;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
637
Robust.Client/Audio/AudioSystem.cs
Normal file
637
Robust.Client/Audio/AudioSystem.cs
Normal file
@@ -0,0 +1,637 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
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;
|
||||
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
|
||||
|
||||
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 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<MapGridComponent> _gridQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
private float _maxRayLength;
|
||||
|
||||
/// <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));
|
||||
|
||||
_gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
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);
|
||||
|
||||
CfgManager.OnValueChanged(CVars.AudioAttenuation, OnAudioAttenuation, true);
|
||||
CfgManager.OnValueChanged(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.SetMasterVolume(value);
|
||||
}
|
||||
|
||||
protected override void SetZOffset(float value)
|
||||
{
|
||||
base.SetZOffset(value);
|
||||
_audio.SetZOffset(value);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
CfgManager.UnsubValueChanged(CVars.AudioAttenuation, OnAudioAttenuation);
|
||||
CfgManager.UnsubValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged);
|
||||
base.Shutdown();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!TryGetAudio(component.FileName, out var audioResource))
|
||||
{
|
||||
Log.Error($"Error creating audio source for {audioResource}, can't find file {component.FileName}");
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % GetAudioLength(component.FileName).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;
|
||||
_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 = MathF.Pow(10, component.Params.Volume / 10);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 TryCreateAudioSource(AudioStream stream, [NotNullWhen(true)] out IAudioSource? source)
|
||||
{
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
{
|
||||
source = null;
|
||||
Log.Error($"Tried to create audio source outside of prediction!");
|
||||
DebugTools.Assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
source = _audio.CreateAudioSource(stream);
|
||||
return source != null;
|
||||
}
|
||||
|
||||
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>
|
||||
private (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>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = 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>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = 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);
|
||||
}
|
||||
|
||||
/// <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, stream.Name!, audioP);
|
||||
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;
|
||||
|
||||
ApplyAudioParams(audioP, comp);
|
||||
comp.Params = audioP;
|
||||
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 = audioParams.MaxDistance;
|
||||
source.ReferenceDistance = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Robust.Client/Audio/HeadlessAudioManager.cs
Normal file
106
Robust.Client/Audio/HeadlessAudioManager.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
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 SetPosition(Vector2 position)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRotation(Angle angle)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetMasterVolume(float value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
void InitializePostWindowing();
|
||||
void Shutdown();
|
||||
|
||||
/// <summary>
|
||||
/// Flushes all pending queues for disposing of AL sources.
|
||||
/// </summary>
|
||||
void FlushALDisposeQueues();
|
||||
|
||||
IAudioSource? CreateAudioSource(AudioStream stream);
|
||||
|
||||
IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false);
|
||||
|
||||
/// <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 SetMasterVolume(float value);
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -21,8 +21,6 @@ public interface IMidiManager
|
||||
/// </summary>
|
||||
float Volume { get; set; }
|
||||
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This method tries to return a midi renderer ready to be used.
|
||||
/// You only need to set the <see cref="IMidiRenderer.MidiProgram"/> afterwards.
|
||||
|
||||
@@ -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,13 +1,11 @@
|
||||
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;
|
||||
@@ -19,11 +17,9 @@ 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;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -34,24 +30,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 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
|
||||
{
|
||||
@@ -132,10 +123,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()
|
||||
{
|
||||
@@ -155,12 +145,6 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
_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;
|
||||
@@ -214,8 +198,17 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
_midiThread = new Thread(ThreadUpdate);
|
||||
_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;
|
||||
}
|
||||
@@ -232,11 +225,6 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
_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 +261,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 +339,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
renderer.Source.SetVolume(Volume);
|
||||
renderer.Source.Volume = _volume;
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
@@ -374,110 +362,107 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
// Update positions of streams every frame.
|
||||
// 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;
|
||||
|
||||
var transQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
|
||||
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parallel.ParallelProcessCount };
|
||||
|
||||
if (_renderers.Count > _minRendererParallel)
|
||||
{
|
||||
Parallel.ForEach(_renderers, opts, renderer => UpdateRenderer(renderer, transQuery, physicsQuery));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
UpdateRenderer(renderer, transQuery, physicsQuery);
|
||||
}
|
||||
}
|
||||
|
||||
_updateJob.OurPosition = _audioSys.GetListenerCoordinates();
|
||||
_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));
|
||||
|
||||
_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.Volume = Volume;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.Volume = Volume;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
renderer.Source.Position = worldPos;
|
||||
|
||||
// Update velocity (doppler).
|
||||
if (renderer.TrackingEntity != null)
|
||||
{
|
||||
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>
|
||||
@@ -674,4 +659,25 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
private record struct MidiUpdateJob : IParallelRobustJob
|
||||
{
|
||||
public int MinimumBatchParallel => 2;
|
||||
|
||||
public int BatchSize => 2;
|
||||
|
||||
public MidiManager Manager;
|
||||
|
||||
public MapCoordinates OurPosition;
|
||||
public List<IMidiRenderer> Renderers;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
Manager.UpdateRenderer(Renderers[index], 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;
|
||||
@@ -52,8 +54,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;
|
||||
@@ -247,7 +249,7 @@ 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;
|
||||
@@ -488,7 +490,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;
|
||||
}
|
||||
}
|
||||
391
Robust.Client/Audio/Sources/BaseAudioSource.cs
Normal file
391
Robust.Client/Audio/Sources/BaseAudioSource.cs
Normal file
@@ -0,0 +1,391 @@
|
||||
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;
|
||||
|
||||
internal 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;
|
||||
|
||||
protected readonly AudioManager Master;
|
||||
|
||||
/// <summary>
|
||||
/// Prior gain that was set.
|
||||
/// </summary>
|
||||
private float _gain;
|
||||
|
||||
private bool IsEfxSupported => Master.IsEfxSupported;
|
||||
|
||||
protected BaseAudioSource(AudioManager master, int sourceHandle)
|
||||
{
|
||||
Master = master;
|
||||
SourceHandle = sourceHandle;
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
|
||||
}
|
||||
|
||||
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 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; set; }
|
||||
|
||||
/// <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);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
|
||||
_gain = value;
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
Master._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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._checkAlError();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Occlusion
|
||||
{
|
||||
get
|
||||
{
|
||||
_checkDisposed();
|
||||
AL.GetSource(SourceHandle, ALSourcef.MaxDistance, out var value);
|
||||
Master._checkAlError();
|
||||
return value;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAuxiliary(IAuxiliaryAudio? audio)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
225
Robust.Client/Audio/Sources/BufferedAudioSource.cs
Normal file
225
Robust.Client/Audio/Sources/BufferedAudioSource.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
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;
|
||||
|
||||
private float _gain;
|
||||
|
||||
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;
|
||||
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
@@ -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,7 @@ 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<IAudioInternal, HeadlessAudioManager>();
|
||||
deps.Register<IInputManager, InputManager>();
|
||||
deps.Register<IFileDialogManager, DummyFileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpenerDummy>();
|
||||
@@ -117,8 +116,7 @@ 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<IAudioInternal, AudioManager>();
|
||||
deps.Register<IInputManager, ClydeInputManager>();
|
||||
deps.Register<IFileDialogManager, FileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpener>();
|
||||
|
||||
@@ -15,6 +15,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;
|
||||
@@ -256,7 +257,7 @@ namespace Robust.Client.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
var uid = EntityUid.Parse(args[0], "-1");
|
||||
var uid = EntityUid.Parse(args[0]);
|
||||
var entmgr = _entityManager;
|
||||
if (!entmgr.EntityExists(uid))
|
||||
{
|
||||
@@ -458,13 +459,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)
|
||||
{
|
||||
@@ -644,7 +645,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 +657,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 +681,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 +732,7 @@ namespace Robust.Client.Console.Commands
|
||||
{
|
||||
try
|
||||
{
|
||||
resC.ReloadResource<ShaderSourceResource>(resPath);
|
||||
_cache.ReloadResource<ShaderSourceResource>(resPath);
|
||||
shell.WriteLine($"Reloaded shader: {resPath}");
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -791,11 +793,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,7 +43,6 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
|
||||
|
||||
GC.Collect();
|
||||
|
||||
Span<EntityUid> ents = stackalloc EntityUid[amount];
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
@@ -51,17 +50,12 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
|
||||
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
ents[i] = _entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
|
||||
_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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.LoaderApi;
|
||||
@@ -70,6 +71,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))
|
||||
@@ -112,6 +134,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;
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, out MetaDataComponent metadata)
|
||||
{
|
||||
return base.CreateEntity(prototypeName, out metadata, out _);
|
||||
return base.CreateEntity(prototypeName, out metadata);
|
||||
}
|
||||
|
||||
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta)
|
||||
|
||||
@@ -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,9 +1,12 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -25,5 +28,10 @@ namespace Robust.Client.GameObjects
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class ClientDirtySystem : EntitySystem
|
||||
|
||||
private void OnTerminate(ref EntityTerminatingEvent ev)
|
||||
{
|
||||
if (!_timing.InPrediction || IsClientSide(ev.Entity, ev.Metadata))
|
||||
if (!_timing.InPrediction || IsClientSide(ev.Entity))
|
||||
return;
|
||||
|
||||
// Client-side entity deletion is not supported and will cause errors.
|
||||
|
||||
@@ -5,8 +5,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Arch.Core;
|
||||
using Collections.Pooled;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -31,9 +29,7 @@ using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Toolshed.TypeParsers;
|
||||
using Robust.Shared.Utility;
|
||||
using ComponentType = Arch.Core.Utils.ComponentType;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
{
|
||||
@@ -56,12 +52,11 @@ namespace Robust.Client.GameStates
|
||||
private readonly Dictionary<ushort, (IComponent Component, ComponentState? curState, ComponentState? 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 List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
|
||||
|
||||
private readonly List<NetEntity> _created = new();
|
||||
private readonly List<NetEntity> _detached = new();
|
||||
|
||||
private readonly ObjectPool<Dictionary<ushort, ComponentState>> _compDataPool =
|
||||
new DefaultObjectPool<Dictionary<ushort, ComponentState>>(new DictPolicy<ushort, ComponentState>(), 256);
|
||||
|
||||
@@ -545,9 +540,7 @@ namespace Robust.Client.GameStates
|
||||
var countReset = 0;
|
||||
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
|
||||
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
|
||||
using var toRemove = new PooledList<IComponent>();
|
||||
using var toAdd = new PooledList<ushort>();
|
||||
using var toAddStates = new PooledList<ComponentState>();
|
||||
RemQueue<IComponent> toRemove = new();
|
||||
|
||||
foreach (var entity in system.DirtyEntities)
|
||||
{
|
||||
@@ -611,17 +604,12 @@ namespace Robust.Client.GameStates
|
||||
_resettingPredictedEntities = false;
|
||||
}
|
||||
|
||||
if (toRemove.Count > 0)
|
||||
// Remove predicted component additions
|
||||
foreach (var comp in toRemove)
|
||||
{
|
||||
// Remove predicted component additions
|
||||
// TODO: 1 archetype change.
|
||||
foreach (var comp in toRemove)
|
||||
{
|
||||
_entities.RemoveComponent(entity, comp, meta);
|
||||
}
|
||||
|
||||
toRemove.Clear();
|
||||
_entities.RemoveComponent(entity, comp);
|
||||
}
|
||||
toRemove.Clear();
|
||||
|
||||
// Re-add predicted removals
|
||||
if (system.RemovedComponents.TryGetValue(entity, out var netIds))
|
||||
@@ -634,29 +622,15 @@ namespace Robust.Client.GameStates
|
||||
if (!last.TryGetValue(netId, out var state))
|
||||
continue;
|
||||
|
||||
toAdd.Add(netId);
|
||||
toAddStates.Add(state);
|
||||
}
|
||||
var comp = _entityManager.AddComponent(entity, netId, meta);
|
||||
|
||||
if (toAdd.Count > 0)
|
||||
{
|
||||
for (var i = 0; i < toAdd.Count; i++)
|
||||
{
|
||||
var netId = toAdd[i];
|
||||
var state = toAddStates[i];
|
||||
var comp = _entityManager.AddComponent(entity, netId, meta);
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was removed: {comp.GetType()}");
|
||||
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was removed: {comp.GetType()}");
|
||||
|
||||
var stateEv = new ComponentHandleState(state, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
|
||||
comp.ClearCreationTick(); // don't undo the re-adding.
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
|
||||
toAdd.Clear();
|
||||
toAddStates.Clear();
|
||||
var stateEv = new ComponentHandleState(state, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
|
||||
comp.ClearCreationTick(); // don't undo the re-adding.
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,9 +713,10 @@ namespace Robust.Client.GameStates
|
||||
_config.TickProcessMessages();
|
||||
}
|
||||
|
||||
(IEnumerable<NetEntity> Created, List<NetEntity> Detached) output;
|
||||
using (_prof.Group("Entity"))
|
||||
{
|
||||
ApplyEntityStates(curState, nextState);
|
||||
output = ApplyEntityStates(curState, nextState);
|
||||
}
|
||||
|
||||
using (_prof.Group("Player"))
|
||||
@@ -751,13 +726,13 @@ namespace Robust.Client.GameStates
|
||||
|
||||
using (_prof.Group("Callback"))
|
||||
{
|
||||
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, _detached));
|
||||
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, output.Detached));
|
||||
}
|
||||
|
||||
return _created;
|
||||
return output.Created;
|
||||
}
|
||||
|
||||
private void ApplyEntityStates(GameState curState, GameState? nextState)
|
||||
private (IEnumerable<NetEntity> Created, List<NetEntity> Detached) ApplyEntityStates(GameState curState, GameState? nextState)
|
||||
{
|
||||
var metas = _entities.GetEntityQuery<MetaDataComponent>();
|
||||
var xforms = _entities.GetEntityQuery<TransformComponent>();
|
||||
@@ -766,8 +741,6 @@ namespace Robust.Client.GameStates
|
||||
var enteringPvs = 0;
|
||||
_toApply.Clear();
|
||||
_toCreate.Clear();
|
||||
_detached.Clear();
|
||||
_created.Clear();
|
||||
_pendingReapplyNetStates.Clear();
|
||||
var curSpan = curState.EntityStates.Span;
|
||||
|
||||
@@ -792,7 +765,6 @@ namespace Robust.Client.GameStates
|
||||
|
||||
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
|
||||
_toCreate.Add(es.NetEntity, es);
|
||||
_created.Add(es.NetEntity);
|
||||
_toApply.Add(uid, (es.NetEntity, newMeta, false, GameTick.Zero, es, null));
|
||||
|
||||
// Client creates a client-side net entity for the newly created entity.
|
||||
@@ -846,7 +818,7 @@ namespace Robust.Client.GameStates
|
||||
// Detach entities to null space
|
||||
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
|
||||
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
|
||||
ProcessPvsDeparture(_detached, curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
|
||||
var detached = ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
|
||||
|
||||
// Check next state (AFTER having created new entities introduced in curstate)
|
||||
if (nextState != null)
|
||||
@@ -948,6 +920,8 @@ namespace Robust.Client.GameStates
|
||||
|
||||
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
|
||||
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
|
||||
|
||||
return (_toCreate.Keys, detached);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -978,7 +952,7 @@ namespace Robust.Client.GameStates
|
||||
var xforms = _entities.GetEntityQuery<TransformComponent>();
|
||||
var xformSys = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
|
||||
|
||||
using var toDelete = new PooledList<EntityUid>();
|
||||
_toDelete.Clear();
|
||||
|
||||
// Client side entities won't need the transform, but that should always be a tiny minority of entities
|
||||
var metaQuery = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
|
||||
@@ -989,7 +963,7 @@ namespace Robust.Client.GameStates
|
||||
if (metadata.NetEntity.IsClientSide())
|
||||
{
|
||||
if (deleteClientEntities)
|
||||
toDelete.Add(ent);
|
||||
_toDelete.Add(ent);
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -1016,14 +990,14 @@ namespace Robust.Client.GameStates
|
||||
&& !deleteClientEntities // don't add duplicates
|
||||
&& _entities.IsClientSide(child.Value))
|
||||
{
|
||||
toDelete.Add(child.Value);
|
||||
_toDelete.Add(child.Value);
|
||||
}
|
||||
}
|
||||
|
||||
toDelete.Add(ent);
|
||||
_toDelete.Add(ent);
|
||||
}
|
||||
|
||||
foreach (var ent in toDelete)
|
||||
foreach (var ent in _toDelete)
|
||||
{
|
||||
_entities.DeleteEntity(ent);
|
||||
}
|
||||
@@ -1083,8 +1057,7 @@ namespace Robust.Client.GameStates
|
||||
Detach(GameTick.MaxValue, null, entities, metas, xforms, xformSys, containerSys, lookupSys);
|
||||
}
|
||||
|
||||
private void ProcessPvsDeparture(
|
||||
IList<NetEntity> detached,
|
||||
private List<NetEntity> ProcessPvsDeparture(
|
||||
GameTick toTick,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
@@ -1092,17 +1065,18 @@ namespace Robust.Client.GameStates
|
||||
ContainerSystem containerSys,
|
||||
EntityLookupSystem lookupSys)
|
||||
{
|
||||
using var toDetach = new PooledList<(GameTick Tick, List<NetEntity> Entities)>();
|
||||
_processor.GetEntitiesToDetach(toDetach, toTick, _pvsDetachBudget);
|
||||
var toDetach = _processor.GetEntitiesToDetach(toTick, _pvsDetachBudget);
|
||||
var detached = new List<NetEntity>();
|
||||
|
||||
if (toDetach.Count == 0)
|
||||
return;
|
||||
return detached;
|
||||
|
||||
// TODO optimize
|
||||
// If an entity is leaving PVS, so are all of its children. If we can preserve the hierarchy we can avoid
|
||||
// things like container insertion and ejection.
|
||||
|
||||
using var _ = _prof.Group("Leave PVS");
|
||||
detached.EnsureCapacity(toDetach.Count);
|
||||
|
||||
foreach (var (tick, ents) in toDetach)
|
||||
{
|
||||
@@ -1110,6 +1084,7 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
_prof.WriteValue("Count", ProfData.Int32(detached.Count));
|
||||
return detached;
|
||||
}
|
||||
|
||||
private void Detach(GameTick maxTick,
|
||||
@@ -1120,7 +1095,7 @@ namespace Robust.Client.GameStates
|
||||
SharedTransformSystem xformSys,
|
||||
ContainerSystem containerSys,
|
||||
EntityLookupSystem lookupSys,
|
||||
IList<NetEntity>? detached = null)
|
||||
List<NetEntity>? detached = null)
|
||||
{
|
||||
foreach (var netEntity in entities)
|
||||
{
|
||||
@@ -1174,7 +1149,7 @@ namespace Robust.Client.GameStates
|
||||
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
using var brokenEnts = new PooledList<EntityUid>();
|
||||
var brokenEnts = new List<EntityUid>();
|
||||
#endif
|
||||
using (_prof.Group("Initialize Entity"))
|
||||
{
|
||||
@@ -1238,28 +1213,18 @@ namespace Robust.Client.GameStates
|
||||
// First remove any deleted components
|
||||
if (curState?.NetComponents != null)
|
||||
{
|
||||
using var toRemove = new PooledList<IComponent>();
|
||||
using var compTypes = new PooledList<ComponentType>();
|
||||
_toRemove.Clear();
|
||||
|
||||
foreach (var (id, comp) in meta.NetComponents)
|
||||
{
|
||||
if (comp.NetSyncEnabled && !curState.NetComponents.Contains(id))
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
compTypes.Add(comp.GetType());
|
||||
}
|
||||
_toRemove.Add(comp);
|
||||
}
|
||||
|
||||
if (toRemove.Count > 0)
|
||||
foreach (var comp in _toRemove)
|
||||
{
|
||||
foreach (var comp in toRemove)
|
||||
{
|
||||
_entityManager.RemoveComponentInternal(uid, comp, terminating: false, archetypeChange: false, meta);
|
||||
}
|
||||
_entities.RemoveComponent(uid, comp, meta);
|
||||
}
|
||||
|
||||
if (compTypes.Count > 0)
|
||||
_entityManager.RemoveComponentRange(uid, compTypes);
|
||||
}
|
||||
|
||||
if (enteringPvs)
|
||||
@@ -1274,7 +1239,7 @@ namespace Robust.Client.GameStates
|
||||
if (!meta.NetComponents.TryGetValue(id, out var comp))
|
||||
{
|
||||
comp = _compFactory.GetComponent(id);
|
||||
_entityManager.AddComponent(uid, comp, metadata: meta);
|
||||
_entityManager.AddComponent(uid, comp, true, metadata: meta);
|
||||
}
|
||||
|
||||
_compStateWork[id] = (comp, state, null);
|
||||
@@ -1282,49 +1247,18 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
else if (curState != null)
|
||||
{
|
||||
using var addedComps = new PooledList<IComponent>();
|
||||
using var addedCompTypes = new PooledList<ComponentType>();
|
||||
using var addedRegistrations = new PooledList<ComponentRegistration>();
|
||||
|
||||
foreach (var compChange in curState.ComponentChanges.Span)
|
||||
{
|
||||
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
|
||||
{
|
||||
var registration = _compFactory.GetRegistration(compChange.NetID);
|
||||
addedRegistrations.Add(registration);
|
||||
comp = _compFactory.GetComponent(registration);
|
||||
comp.Owner = uid;
|
||||
addedComps.Add(comp);
|
||||
addedCompTypes.Add(comp.GetType());
|
||||
comp = _compFactory.GetComponent(compChange.NetID);
|
||||
_entityManager.AddComponent(uid, comp, true, metadata:meta);
|
||||
}
|
||||
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
|
||||
continue;
|
||||
|
||||
_compStateWork[compChange.NetID] = (comp, compChange.State, null);
|
||||
}
|
||||
|
||||
// To avoid shuffling the archetype we'll set the component range up-front.
|
||||
if (addedComps.Count > 0)
|
||||
{
|
||||
// TODO: This fucking sucks but
|
||||
// - Frequent archetype changes PER COMPONENT sucks
|
||||
// - the components will be null in event handlers until it's done.
|
||||
_entityManager.AddComponentRange(uid, addedCompTypes);
|
||||
|
||||
for (var i = 0; i < addedComps.Count; i++)
|
||||
{
|
||||
var component = addedComps[i];
|
||||
var reg = addedRegistrations[i];
|
||||
_entityManager.AddComponentInternalOnly(uid, component, reg, meta);
|
||||
}
|
||||
|
||||
for (var i = 0; i < addedComps.Count; i++)
|
||||
{
|
||||
var component = addedComps[i];
|
||||
var reg = addedRegistrations[i];
|
||||
_entityManager.AddComponentEvents(uid, component, reg, false, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextState != null)
|
||||
@@ -1414,7 +1348,7 @@ namespace Robust.Client.GameStates
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], "-1", out uid))
|
||||
if (!EntityUid.TryParse(args[0], out uid))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-uid", ("arg", args[0])));
|
||||
meta = null;
|
||||
@@ -1546,22 +1480,23 @@ namespace Robust.Client.GameStates
|
||||
if (!meta.NetComponents.TryGetValue(id, out var comp))
|
||||
{
|
||||
comp = _compFactory.GetComponent(id);
|
||||
_entityManager.AddComponent(uid, comp, meta);
|
||||
_entityManager.AddComponent(uid, comp, true, meta);
|
||||
}
|
||||
|
||||
var handleState = new ComponentHandleState(state, null);
|
||||
_entityManager.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
}
|
||||
|
||||
using var toRemove = new PooledList<IComponent>();
|
||||
// ensure we don't have any extra components
|
||||
_toRemove.Clear();
|
||||
|
||||
foreach (var (id, comp) in meta.NetComponents)
|
||||
{
|
||||
if (comp.NetSyncEnabled && !lastState.ContainsKey(id))
|
||||
toRemove.Add(comp);
|
||||
_toRemove.Add(comp);
|
||||
}
|
||||
|
||||
foreach (var comp in toRemove)
|
||||
foreach (var comp in _toRemove)
|
||||
{
|
||||
_entities.RemoveComponent(uid, comp);
|
||||
}
|
||||
@@ -1574,10 +1509,10 @@ namespace Robust.Client.GameStates
|
||||
|
||||
public sealed class GameStateAppliedArgs : EventArgs
|
||||
{
|
||||
public readonly GameState AppliedState;
|
||||
public readonly IReadOnlyList<NetEntity> Detached;
|
||||
public GameState AppliedState { get; }
|
||||
public readonly List<NetEntity> Detached;
|
||||
|
||||
public GameStateAppliedArgs(GameState appliedState, IReadOnlyList<NetEntity> detached)
|
||||
public GameStateAppliedArgs(GameState appliedState, List<NetEntity> detached)
|
||||
{
|
||||
AppliedState = appliedState;
|
||||
Detached = detached;
|
||||
|
||||
@@ -304,8 +304,9 @@ Had full state: {LastFullState != null}"
|
||||
|
||||
public void ClearDetachQueue() => _pvsDetachMessages.Clear();
|
||||
|
||||
public void GetEntitiesToDetach(IList<(GameTick Tick, List<NetEntity> Entities)> result, GameTick toTick, int budget)
|
||||
public List<(GameTick Tick, List<NetEntity> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
|
||||
{
|
||||
var result = new List<(GameTick Tick, List<NetEntity> Entities)>();
|
||||
foreach (var (tick, entities) in _pvsDetachMessages)
|
||||
{
|
||||
if (tick > toTick)
|
||||
@@ -324,6 +325,7 @@ Had full state: {LastFullState != null}"
|
||||
entities.RemoveRange(index, budget);
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool TryGetDeltaState(out GameState? curState, out GameState? nextState)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Hey look, it's ClydeAudio's evil twin brother!
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal sealed class ClydeAudioHeadless : IClydeAudio, IClydeAudioInternal
|
||||
{
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
// TODO: Might wanna actually load this so the length gets reported correctly.
|
||||
return new(default, default, channels, name);
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return DummyAudioSource.Instance;
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
|
||||
{
|
||||
return DummyBufferedAudioSource.Instance;
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void DisposeAllAudio()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void StopAllAudio()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Hey look, it's ClydeAudio.AudioSource's evil twin brother!
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
internal class DummyAudioSource : IClydeAudioSource
|
||||
{
|
||||
public static DummyAudioSource Instance { get; } = new();
|
||||
|
||||
public bool IsPlaying => default;
|
||||
public bool IsLooping { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void StartPlaying()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void StopPlaying()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public bool IsGlobal { get; }
|
||||
|
||||
public bool SetPosition(Vector2 position)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetGlobal()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolume(float decibels)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float gain)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetMaxDistance(float maxDistance)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetRolloffFactor(float rolloffFactor)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetReferenceDistance(float refDistance)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetOcclusion(float blocks)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetPlaybackPosition(float seconds)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// For "start ss14 with no audio devices" Smugleaf
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal sealed class FallbackProxyClydeAudio : ProxyClydeAudio
|
||||
{
|
||||
[Dependency] private readonly IDependencyCollection _deps = default!;
|
||||
|
||||
public override bool InitializePostWindowing()
|
||||
{
|
||||
// Deliberate lack of base call here (see base implementation for comments as to why there even is a base)
|
||||
|
||||
ActualImplementation = new ClydeAudio();
|
||||
_deps.InjectDependencies(ActualImplementation, true);
|
||||
if (ActualImplementation.InitializePostWindowing())
|
||||
return true;
|
||||
|
||||
// If we get here, that failed, so use the fallback
|
||||
ActualImplementation = new ClydeAudioHeadless();
|
||||
_deps.InjectDependencies(ActualImplementation, true);
|
||||
return ActualImplementation.InitializePostWindowing();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// For "start ss14 with no audio devices" Smugleaf
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal abstract class ProxyClydeAudio : IClydeAudio, IClydeAudioInternal
|
||||
{
|
||||
protected IClydeAudioInternal ActualImplementation = default!;
|
||||
|
||||
public virtual bool InitializePostWindowing()
|
||||
{
|
||||
// This particular implementation exists to be overridden because removing this method causes C# to complain
|
||||
return ActualImplementation.InitializePostWindowing();
|
||||
}
|
||||
|
||||
public void FrameProcess(FrameEventArgs eventArgs)
|
||||
{
|
||||
ActualImplementation.FrameProcess(eventArgs);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
ActualImplementation.Shutdown();
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
return ActualImplementation.LoadAudioOggVorbis(stream, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioWav(Stream stream, string? name = null)
|
||||
{
|
||||
return ActualImplementation.LoadAudioWav(stream, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
return ActualImplementation.LoadAudioRaw(samples, channels, sampleRate, name);
|
||||
}
|
||||
|
||||
public IClydeAudioSource? CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return ActualImplementation.CreateAudioSource(stream);
|
||||
}
|
||||
|
||||
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
|
||||
{
|
||||
return ActualImplementation.CreateBufferedAudioSource(buffers, floatAudio);
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
ActualImplementation.SetMasterVolume(newVolume);
|
||||
}
|
||||
|
||||
public void DisposeAllAudio()
|
||||
{
|
||||
ActualImplementation.DisposeAllAudio();
|
||||
}
|
||||
|
||||
public void StopAllAudio()
|
||||
{
|
||||
ActualImplementation.StopAllAudio();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,14 +260,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var file in _resourceCache.ContentFindFiles(_windowIconPath))
|
||||
foreach (var file in _resManager.ContentFindFiles(_windowIconPath))
|
||||
{
|
||||
if (file.Extension != "png")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stream = _resourceCache.ContentFileRead(file);
|
||||
using var stream = _resManager.ContentFileRead(file);
|
||||
yield return Image.Load<Rgba32>(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -36,6 +37,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IResourceManager _resManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
@@ -292,123 +292,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
private class DummyAudioSource : IClydeAudioSource
|
||||
{
|
||||
public static DummyAudioSource Instance { get; } = new();
|
||||
|
||||
public bool IsPlaying => default;
|
||||
public bool IsLooping { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void StartPlaying()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void StopPlaying()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public bool IsGlobal { get; }
|
||||
|
||||
public bool SetPosition(Vector2 position)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetGlobal()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolume(float decibels)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float gain)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetMaxDistance(float maxDistance)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetRolloffFactor(float rolloffFactor)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetReferenceDistance(float refDistance)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetOcclusion(float blocks)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetPlaybackPosition(float seconds)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVelocity(Vector2 velocity)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
|
||||
{
|
||||
public new static DummyBufferedAudioSource Instance { get; } = new();
|
||||
public int SampleRate { get; set; } = 0;
|
||||
|
||||
public void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void WriteBuffer(int handle, ReadOnlySpan<float> data)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void QueueBuffers(ReadOnlySpan<int> handles)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void EmptyBuffers()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void GetBuffersProcessed(Span<int> handles)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public int GetNumberOfBuffersProcessed()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyTexture : OwnedTexture
|
||||
{
|
||||
public DummyTexture(Vector2i size) : base(size)
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
using System;
|
||||
using Robust.Shared.Graphics;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
internal readonly struct ClydeHandle : IEquatable<ClydeHandle>, IClydeHandle
|
||||
{
|
||||
internal struct ClydeHandle : IEquatable<ClydeHandle>
|
||||
public ClydeHandle(long value)
|
||||
{
|
||||
public ClydeHandle(long value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public readonly long Value;
|
||||
public long Value { get; }
|
||||
|
||||
public static explicit operator ClydeHandle(long x)
|
||||
{
|
||||
return new(x);
|
||||
}
|
||||
public static explicit operator ClydeHandle(long x)
|
||||
{
|
||||
return new(x);
|
||||
}
|
||||
|
||||
public static explicit operator long(ClydeHandle h)
|
||||
{
|
||||
return h.Value;
|
||||
}
|
||||
public static explicit operator long(ClydeHandle h)
|
||||
{
|
||||
return h.Value;
|
||||
}
|
||||
|
||||
public bool Equals(ClydeHandle other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
public bool Equals(ClydeHandle other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ClydeHandle other && Equals(other);
|
||||
}
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ClydeHandle other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
|
||||
public static bool operator ==(ClydeHandle left, ClydeHandle right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
public static bool operator ==(ClydeHandle left, ClydeHandle right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(ClydeHandle left, ClydeHandle right)
|
||||
{
|
||||
return left.Value != right.Value;
|
||||
}
|
||||
public static bool operator !=(ClydeHandle left, ClydeHandle right)
|
||||
{
|
||||
return left.Value != right.Value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"ClydeHandle {Value}";
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return $"ClydeHandle {Value}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +114,10 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
if (rune == new Rune('\n'))
|
||||
{
|
||||
baseLine.X = 0f;
|
||||
baseLine.Y += lineHeight;
|
||||
advanceTotal.Y += lineHeight;
|
||||
advanceTotal.X = Math.Max(advanceTotal.X, baseLine.X);
|
||||
baseLine.X = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -126,7 +127,6 @@ namespace Robust.Client.Graphics
|
||||
continue;
|
||||
|
||||
var advance = metrics.Value.Advance;
|
||||
advanceTotal.X += advance;
|
||||
baseLine += new Vector2(advance, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Client.Audio;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
public interface IClydeAudio
|
||||
{
|
||||
// AUDIO SYSTEM DOWN BELOW.
|
||||
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 SetMasterVolume(float newVolume);
|
||||
|
||||
void DisposeAllAudio();
|
||||
|
||||
void StopAllAudio();
|
||||
|
||||
IClydeAudioSource? CreateAudioSource(AudioStream stream);
|
||||
IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
internal interface IClydeAudioInternal : IClydeAudio
|
||||
{
|
||||
bool InitializePostWindowing();
|
||||
void FrameProcess(FrameEventArgs eventArgs);
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
public interface IClydeAudioSource : IDisposable
|
||||
{
|
||||
void StartPlaying();
|
||||
void StopPlaying();
|
||||
|
||||
bool IsPlaying { get; }
|
||||
|
||||
bool IsLooping { get; set; }
|
||||
bool IsGlobal { get; }
|
||||
|
||||
[MustUseReturnValue]
|
||||
bool SetPosition(Vector2 position);
|
||||
void SetPitch(float pitch);
|
||||
void SetGlobal();
|
||||
void SetVolume(float decibels);
|
||||
void SetVolumeDirect(float gain);
|
||||
void SetMaxDistance(float maxDistance);
|
||||
void SetRolloffFactor(float rolloffFactor);
|
||||
void SetReferenceDistance(float refDistance);
|
||||
void SetOcclusion(float blocks);
|
||||
void SetPlaybackPosition(float seconds);
|
||||
void SetVelocity(Vector2 velocity);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
public interface IClydeBufferedAudioSource : IClydeAudioSource
|
||||
{
|
||||
int SampleRate { get; set; }
|
||||
int GetNumberOfBuffersProcessed();
|
||||
void GetBuffersProcessed(Span<int> handles);
|
||||
void WriteBuffer(int handle, ReadOnlySpan<ushort> data);
|
||||
void WriteBuffer(int handle, ReadOnlySpan<float> data);
|
||||
void QueueBuffers(ReadOnlySpan<int> handles);
|
||||
void EmptyBuffers();
|
||||
}
|
||||
}
|
||||
@@ -4,32 +4,29 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
[PublicAPI]
|
||||
public interface IOverlayManager
|
||||
{
|
||||
bool AddOverlay(Overlay overlay);
|
||||
|
||||
[PublicAPI]
|
||||
public interface IOverlayManager
|
||||
{
|
||||
bool AddOverlay(Overlay overlay);
|
||||
bool RemoveOverlay(Overlay overlay);
|
||||
bool RemoveOverlay(Type overlayClass);
|
||||
bool RemoveOverlay<T>() where T : Overlay;
|
||||
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
|
||||
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
|
||||
|
||||
bool RemoveOverlay(Overlay overlay);
|
||||
bool RemoveOverlay(Type overlayClass);
|
||||
bool RemoveOverlay<T>() where T : Overlay;
|
||||
Overlay GetOverlay(Type overlayClass);
|
||||
T GetOverlay<T>() where T : Overlay;
|
||||
|
||||
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
|
||||
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
|
||||
bool HasOverlay(Type overlayClass);
|
||||
bool HasOverlay<T>() where T : Overlay;
|
||||
|
||||
Overlay GetOverlay(Type overlayClass);
|
||||
T GetOverlay<T>() where T : Overlay;
|
||||
|
||||
bool HasOverlay(Type overlayClass);
|
||||
bool HasOverlay<T>() where T : Overlay;
|
||||
|
||||
IEnumerable<Overlay> AllOverlays { get; }
|
||||
}
|
||||
|
||||
internal interface IOverlayManagerInternal : IOverlayManager
|
||||
{
|
||||
void FrameUpdate(FrameEventArgs args);
|
||||
}
|
||||
IEnumerable<Overlay> AllOverlays { get; }
|
||||
}
|
||||
|
||||
internal interface IOverlayManagerInternal : IOverlayManager
|
||||
{
|
||||
void FrameUpdate(FrameEventArgs args);
|
||||
}
|
||||
|
||||
@@ -6,107 +6,106 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
|
||||
{
|
||||
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
|
||||
private ISawmill _logger = default!;
|
||||
|
||||
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
|
||||
|
||||
public void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
|
||||
private ISawmill _logger = default!;
|
||||
|
||||
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
|
||||
|
||||
public void FrameUpdate(FrameEventArgs args)
|
||||
foreach (var overlay in _overlays.Values)
|
||||
{
|
||||
foreach (var overlay in _overlays.Values)
|
||||
{
|
||||
overlay.FrameUpdate(args);
|
||||
}
|
||||
overlay.FrameUpdate(args);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddOverlay(Overlay overlay)
|
||||
public bool AddOverlay(Overlay overlay)
|
||||
{
|
||||
if (_overlays.ContainsKey(overlay.GetType()))
|
||||
return false;
|
||||
_overlays.Add(overlay.GetType(), overlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveOverlay(Type overlayClass)
|
||||
{
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
|
||||
{
|
||||
if (_overlays.ContainsKey(overlay.GetType()))
|
||||
return false;
|
||||
_overlays.Add(overlay.GetType(), overlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveOverlay(Type overlayClass)
|
||||
{
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
|
||||
{
|
||||
_logger.Error($"RemoveOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return _overlays.Remove(overlayClass);
|
||||
}
|
||||
|
||||
public bool RemoveOverlay<T>() where T : Overlay
|
||||
{
|
||||
return RemoveOverlay(typeof(T));
|
||||
}
|
||||
|
||||
public bool RemoveOverlay(Overlay overlay)
|
||||
{
|
||||
return _overlays.Remove(overlay.GetType());
|
||||
}
|
||||
|
||||
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
|
||||
{
|
||||
overlay = null;
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
|
||||
{
|
||||
_logger.Error($"TryGetOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return _overlays.TryGetValue(overlayClass, out overlay);
|
||||
}
|
||||
|
||||
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay
|
||||
{
|
||||
overlay = null;
|
||||
if (_overlays.TryGetValue(typeof(T), out Overlay? toReturn))
|
||||
{
|
||||
overlay = (T)toReturn;
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.Error($"RemoveOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
|
||||
return false;
|
||||
}
|
||||
|
||||
public Overlay GetOverlay(Type overlayClass)
|
||||
return _overlays.Remove(overlayClass);
|
||||
}
|
||||
|
||||
public bool RemoveOverlay<T>() where T : Overlay
|
||||
{
|
||||
return RemoveOverlay(typeof(T));
|
||||
}
|
||||
|
||||
public bool RemoveOverlay(Overlay overlay)
|
||||
{
|
||||
return _overlays.Remove(overlay.GetType());
|
||||
}
|
||||
|
||||
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
|
||||
{
|
||||
overlay = null;
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
|
||||
{
|
||||
return _overlays[overlayClass];
|
||||
_logger.Error($"TryGetOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
|
||||
return false;
|
||||
}
|
||||
|
||||
public T GetOverlay<T>() where T : Overlay
|
||||
return _overlays.TryGetValue(overlayClass, out overlay);
|
||||
}
|
||||
|
||||
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay
|
||||
{
|
||||
overlay = null;
|
||||
if (_overlays.TryGetValue(typeof(T), out Overlay? toReturn))
|
||||
{
|
||||
return (T)_overlays[typeof(T)];
|
||||
overlay = (T)toReturn;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HasOverlay(Type overlayClass)
|
||||
{
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
|
||||
{
|
||||
_logger.Error($"HasOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return _overlays.ContainsKey(overlayClass);
|
||||
public Overlay GetOverlay(Type overlayClass)
|
||||
{
|
||||
return _overlays[overlayClass];
|
||||
}
|
||||
|
||||
public T GetOverlay<T>() where T : Overlay
|
||||
{
|
||||
return (T)_overlays[typeof(T)];
|
||||
}
|
||||
|
||||
public bool HasOverlay(Type overlayClass)
|
||||
{
|
||||
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
|
||||
{
|
||||
_logger.Error($"HasOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
|
||||
}
|
||||
|
||||
public bool HasOverlay<T>() where T : Overlay
|
||||
{
|
||||
return _overlays.ContainsKey(typeof(T));
|
||||
}
|
||||
return _overlays.ContainsKey(overlayClass);
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_logger = _logMan.GetSawmill("overlay");
|
||||
}
|
||||
public bool HasOverlay<T>() where T : Overlay
|
||||
{
|
||||
return _overlays.ContainsKey(typeof(T));
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_logger = _logMan.GetSawmill("overlay");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Client.Map;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -22,7 +23,7 @@ namespace Robust.Client.Map
|
||||
{
|
||||
internal sealed class ClydeTileDefinitionManager : TileDefinitionManager, IClydeTileDefinitionManager
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IResourceManager _manager = default!;
|
||||
|
||||
private Texture? _tileTextureAtlas;
|
||||
|
||||
@@ -86,7 +87,7 @@ namespace Robust.Client.Map
|
||||
0, (h - EyeManager.PixelsPerMeter) / h,
|
||||
tileSize / w, tileSize / h);
|
||||
Image<Rgba32> image;
|
||||
using (var stream = _resourceCache.ContentFileRead("/Textures/noTile.png"))
|
||||
using (var stream = _manager.ContentFileRead("/Textures/noTile.png"))
|
||||
{
|
||||
image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
@@ -110,7 +111,7 @@ namespace Robust.Client.Map
|
||||
// Already know it's not null above
|
||||
var path = def.Sprite!.Value;
|
||||
|
||||
using (var stream = _resourceCache.ContentFileRead(path))
|
||||
using (var stream = _manager.ContentFileRead(path))
|
||||
{
|
||||
image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Robust.Client.Placement.Modes
|
||||
|
||||
var closestEntity = snapToEntities[0];
|
||||
var closestTransform = pManager.EntityManager.GetComponent<TransformComponent>(closestEntity);
|
||||
if (!pManager.EntityManager.TryGetComponent(closestEntity, out SpriteComponent? component) || component.BaseRSI == null)
|
||||
if (!pManager.EntityManager.TryGetComponent<SpriteComponent?>(closestEntity, out var component) || component.BaseRSI == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Audio.Midi;
|
||||
using Robust.Client.Configuration;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -10,6 +11,7 @@ using Robust.Client.Player;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.Upload;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -28,7 +30,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
[Dependency] private readonly IMidiManager _midi = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
|
||||
[Dependency] private readonly IAudioInternal _clydeAudio = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
[Dependency] private readonly IClientNetManager _netMan = default!;
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.ResourceManagement
|
||||
namespace Robust.Client.ResourceManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Base resource for the cache.
|
||||
/// </summary>
|
||||
public abstract class BaseResource : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Base resource for the cache.
|
||||
/// Fallback resource path if this one does not exist.
|
||||
/// </summary>
|
||||
public abstract class BaseResource : IDisposable
|
||||
public virtual ResPath? Fallback => null;
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this resource.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
/// <summary>
|
||||
/// Fallback resource path if this one does not exist.
|
||||
/// </summary>
|
||||
public virtual ResPath? Fallback => null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this resource.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Deserializes the resource from the VFS.
|
||||
/// </summary>
|
||||
public abstract void Load(IDependencyCollection dependencies, ResPath path);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the resource from the VFS.
|
||||
/// </summary>
|
||||
/// <param name="cache">ResourceCache this resource is being loaded into.</param>
|
||||
/// <param name="path">Path of the resource requested on the VFS.</param>
|
||||
public abstract void Load(IResourceCache cache, ResPath path);
|
||||
public virtual void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
public virtual void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.ResourceManagement
|
||||
namespace Robust.Client.ResourceManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Handles caching of <see cref="BaseResource"/>
|
||||
/// </summary>
|
||||
public interface IResourceCache : IResourceManager
|
||||
{
|
||||
public interface IResourceCache : IResourceManager
|
||||
{
|
||||
T GetResource<T>(string path, bool useFallback = true)
|
||||
where T : BaseResource, new();
|
||||
T GetResource<T>(string path, bool useFallback = true)
|
||||
where T : BaseResource, new();
|
||||
|
||||
T GetResource<T>(ResPath path, bool useFallback = true)
|
||||
where T : BaseResource, new();
|
||||
T GetResource<T>(ResPath path, bool useFallback = true)
|
||||
where T : BaseResource, new();
|
||||
|
||||
bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource)
|
||||
where T : BaseResource, new();
|
||||
bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource)
|
||||
where T : BaseResource, new();
|
||||
|
||||
bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource)
|
||||
where T : BaseResource, new();
|
||||
bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource)
|
||||
where T : BaseResource, new();
|
||||
|
||||
void ReloadResource<T>(string path)
|
||||
where T : BaseResource, new();
|
||||
void ReloadResource<T>(string path)
|
||||
where T : BaseResource, new();
|
||||
|
||||
void ReloadResource<T>(ResPath path)
|
||||
where T : BaseResource, new();
|
||||
void ReloadResource<T>(ResPath path)
|
||||
where T : BaseResource, new();
|
||||
|
||||
void CacheResource<T>(string path, T resource)
|
||||
where T : BaseResource, new();
|
||||
void CacheResource<T>(string path, T resource)
|
||||
where T : BaseResource, new();
|
||||
|
||||
void CacheResource<T>(ResPath path, T resource)
|
||||
where T : BaseResource, new();
|
||||
void CacheResource<T>(ResPath path, T resource)
|
||||
where T : BaseResource, new();
|
||||
|
||||
T GetFallback<T>()
|
||||
where T : BaseResource, new();
|
||||
T GetFallback<T>()
|
||||
where T : BaseResource, new();
|
||||
|
||||
IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new();
|
||||
IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new();
|
||||
|
||||
// Resource load callbacks so content can hook stuff like click maps.
|
||||
event Action<TextureLoadedEventArgs> OnRawTextureLoaded;
|
||||
event Action<RsiLoadedEventArgs> OnRsiLoaded;
|
||||
|
||||
IClyde Clyde { get; }
|
||||
IClydeAudio ClydeAudio { get; }
|
||||
IFontManager FontManager { get; }
|
||||
}
|
||||
// Resource load callbacks so content can hook stuff like click maps.
|
||||
event Action<TextureLoadedEventArgs> OnRawTextureLoaded;
|
||||
event Action<RsiLoadedEventArgs> OnRsiLoaded;
|
||||
|
||||
IClyde Clyde { get; }
|
||||
IFontManager FontManager { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
internal interface IResourceCacheInternal : IResourceCache, IResourceManagerInternal
|
||||
{
|
||||
void TextureLoaded(TextureLoadedEventArgs eventArgs);
|
||||
void RsiLoaded(RsiLoadedEventArgs eventArgs);
|
||||
namespace Robust.Client.ResourceManagement;
|
||||
|
||||
void MountLoaderApi(IFileApi api, string apiPrefix, ResPath? prefix=null);
|
||||
void PreloadTextures();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
internal interface IResourceCacheInternal : IResourceCache
|
||||
{
|
||||
void TextureLoaded(TextureLoadedEventArgs eventArgs);
|
||||
void RsiLoaded(RsiLoadedEventArgs eventArgs);
|
||||
void PreloadTextures();
|
||||
|
||||
void MountLoaderApi(IResourceManager manager, IFileApi api, string apiPrefix, ResPath? prefix = null);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
internal partial class ResourceCache
|
||||
{
|
||||
public void MountLoaderApi(IResourceManager manager, IFileApi api, string apiPrefix, ResPath? prefix = null)
|
||||
{
|
||||
prefix ??= ResPath.Root;
|
||||
var root = new LoaderApiLoader(api, apiPrefix);
|
||||
manager.AddRoot(prefix.Value, root);
|
||||
}
|
||||
|
||||
private sealed class LoaderApiLoader : IContentRoot
|
||||
{
|
||||
private readonly IFileApi _api;
|
||||
|
||||
@@ -3,10 +3,13 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -19,7 +22,8 @@ namespace Robust.Client.ResourceManagement
|
||||
internal partial class ResourceCache
|
||||
{
|
||||
[field: Dependency] public IClyde Clyde { get; } = default!;
|
||||
[field: Dependency] public IClydeAudio ClydeAudio { get; } = default!;
|
||||
[field: Dependency] public IAudioInternal ClydeAudio { get; } = default!;
|
||||
[Dependency] private readonly IResourceManager _manager = default!;
|
||||
[field: Dependency] public IFontManager FontManager { get; } = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
@@ -44,7 +48,7 @@ namespace Robust.Client.ResourceManagement
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resList = GetTypeDict<TextureResource>();
|
||||
|
||||
var texList = ContentFindFiles("/Textures/")
|
||||
var texList = _manager.ContentFindFiles("/Textures/")
|
||||
// Skip PNG files inside RSIs.
|
||||
.Where(p => p.Extension == "png" && !p.ToString().Contains(".rsi/") && !resList.ContainsKey(p))
|
||||
.Select(p => new TextureResource.LoadStepData {Path = p})
|
||||
@@ -54,7 +58,7 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
try
|
||||
{
|
||||
TextureResource.LoadPreTexture(this, data);
|
||||
TextureResource.LoadPreTexture(_manager, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -116,7 +120,7 @@ namespace Robust.Client.ResourceManagement
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resList = GetTypeDict<RSIResource>();
|
||||
|
||||
var rsiList = ContentFindFiles("/Textures/")
|
||||
var rsiList = _manager.ContentFindFiles("/Textures/")
|
||||
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
|
||||
.Select(c => c.Directory)
|
||||
.Where(p => !resList.ContainsKey(p))
|
||||
@@ -127,7 +131,7 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
try
|
||||
{
|
||||
RSIResource.LoadPreTexture(this, data);
|
||||
RSIResource.LoadPreTexture(_manager, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -1,221 +1,219 @@
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.LoaderApi;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.ResourceManagement
|
||||
namespace Robust.Client.ResourceManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Handles caching of <see cref="BaseResource"/>
|
||||
/// </summary>
|
||||
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
|
||||
{
|
||||
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
|
||||
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> _cachedResources =
|
||||
new();
|
||||
|
||||
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
|
||||
|
||||
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
|
||||
{
|
||||
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> CachedResources =
|
||||
new();
|
||||
return GetResource<T>(new ResPath(path), useFallback);
|
||||
}
|
||||
|
||||
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
|
||||
|
||||
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
|
||||
public T GetResource<T>(ResPath path, bool useFallback = true) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
if (cache.TryGetValue(path, out var cached))
|
||||
{
|
||||
return GetResource<T>(new ResPath(path), useFallback);
|
||||
return (T) cached;
|
||||
}
|
||||
|
||||
public T GetResource<T>(ResPath path, bool useFallback = true) where T : BaseResource, new()
|
||||
var resource = new T();
|
||||
try
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
if (cache.TryGetValue(path, out var cached))
|
||||
{
|
||||
return (T) cached;
|
||||
}
|
||||
|
||||
var _resource = new T();
|
||||
try
|
||||
{
|
||||
_resource.Load(this, path);
|
||||
cache[path] = _resource;
|
||||
return _resource;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (useFallback && _resource.Fallback != null)
|
||||
{
|
||||
Logger.Error(
|
||||
$"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}");
|
||||
return GetResource<T>(_resource.Fallback.Value, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error(
|
||||
$"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
var dependencies = IoCManager.Instance!;
|
||||
resource.Load(dependencies, path);
|
||||
cache[path] = resource;
|
||||
return resource;
|
||||
}
|
||||
|
||||
public bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
|
||||
catch (Exception e)
|
||||
{
|
||||
return TryGetResource(new ResPath(path), out resource);
|
||||
}
|
||||
|
||||
public bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
if (cache.TryGetValue(path, out var cached))
|
||||
if (useFallback && resource.Fallback != null)
|
||||
{
|
||||
resource = (T) cached;
|
||||
return true;
|
||||
Logger.Error(
|
||||
$"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}");
|
||||
return GetResource<T>(resource.Fallback.Value, false);
|
||||
}
|
||||
|
||||
var _resource = new T();
|
||||
try
|
||||
else
|
||||
{
|
||||
_resource.Load(this, path);
|
||||
resource = _resource;
|
||||
cache[path] = resource;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
resource = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadResource<T>(string path) where T : BaseResource, new()
|
||||
{
|
||||
ReloadResource<T>(new ResPath(path));
|
||||
}
|
||||
|
||||
public void ReloadResource<T>(ResPath path) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
|
||||
if (!cache.TryGetValue(path, out var res))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
res.Reload(this, path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
|
||||
Logger.Error(
|
||||
$"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasResource<T>(string path) where T : BaseResource, new()
|
||||
public bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
|
||||
{
|
||||
return TryGetResource(new ResPath(path), out resource);
|
||||
}
|
||||
|
||||
public bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
if (cache.TryGetValue(path, out var cached))
|
||||
{
|
||||
return HasResource<T>(new ResPath(path));
|
||||
resource = (T) cached;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HasResource<T>(ResPath path) where T : BaseResource, new()
|
||||
var _resource = new T();
|
||||
try
|
||||
{
|
||||
return TryGetResource<T>(path, out var _);
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_resource.Load(dependencies, path);
|
||||
resource = _resource;
|
||||
cache[path] = resource;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
resource = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadResource<T>(string path) where T : BaseResource, new()
|
||||
{
|
||||
ReloadResource<T>(new ResPath(path));
|
||||
}
|
||||
|
||||
public void ReloadResource<T>(ResPath path) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
|
||||
if (!cache.TryGetValue(path, out var res))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public void CacheResource<T>(string path, T resource) where T : BaseResource, new()
|
||||
try
|
||||
{
|
||||
CacheResource(new ResPath(path), resource);
|
||||
var dependencies = IoCManager.Instance!;
|
||||
res.Reload(dependencies, path);
|
||||
}
|
||||
|
||||
public void CacheResource<T>(ResPath path, T resource) where T : BaseResource, new()
|
||||
catch (Exception e)
|
||||
{
|
||||
GetTypeDict<T>()[path] = resource;
|
||||
Logger.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public T GetFallback<T>() where T : BaseResource, new()
|
||||
public bool HasResource<T>(string path) where T : BaseResource, new()
|
||||
{
|
||||
return HasResource<T>(new ResPath(path));
|
||||
}
|
||||
|
||||
public bool HasResource<T>(ResPath path) where T : BaseResource, new()
|
||||
{
|
||||
return TryGetResource<T>(path, out var _);
|
||||
}
|
||||
|
||||
public void CacheResource<T>(string path, T resource) where T : BaseResource, new()
|
||||
{
|
||||
CacheResource(new ResPath(path), resource);
|
||||
}
|
||||
|
||||
public void CacheResource<T>(ResPath path, T resource) where T : BaseResource, new()
|
||||
{
|
||||
GetTypeDict<T>()[path] = resource;
|
||||
}
|
||||
|
||||
public T GetFallback<T>() where T : BaseResource, new()
|
||||
{
|
||||
if (_fallbacks.TryGetValue(typeof(T), out var fallback))
|
||||
{
|
||||
if (_fallbacks.TryGetValue(typeof(T), out var fallback))
|
||||
{
|
||||
return (T) fallback;
|
||||
}
|
||||
|
||||
var res = new T();
|
||||
if (res.Fallback == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback.");
|
||||
}
|
||||
|
||||
fallback = GetResource<T>(res.Fallback.Value, useFallback: false);
|
||||
_fallbacks.Add(typeof(T), fallback);
|
||||
return (T) fallback;
|
||||
}
|
||||
|
||||
public IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new()
|
||||
var res = new T();
|
||||
if (res.Fallback == null)
|
||||
{
|
||||
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
|
||||
throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback.");
|
||||
}
|
||||
|
||||
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
|
||||
public event Action<RsiLoadedEventArgs>? OnRsiLoaded;
|
||||
fallback = GetResource<T>(res.Fallback.Value, useFallback: false);
|
||||
_fallbacks.Add(typeof(T), fallback);
|
||||
return (T) fallback;
|
||||
}
|
||||
|
||||
#region IDisposable Members
|
||||
public IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new()
|
||||
{
|
||||
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
|
||||
}
|
||||
|
||||
private bool disposed = false;
|
||||
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
|
||||
public event Action<RsiLoadedEventArgs>? OnRsiLoaded;
|
||||
|
||||
public void Dispose()
|
||||
#region IDisposable Members
|
||||
|
||||
private bool disposed = false;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
return;
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
if (disposing)
|
||||
{
|
||||
if (disposed)
|
||||
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Values))
|
||||
{
|
||||
return;
|
||||
res.Dispose();
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var res in CachedResources.Values.SelectMany(dict => dict.Values))
|
||||
{
|
||||
res.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
~ResourceCache()
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
~ResourceCache()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion IDisposable Members
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected Dictionary<ResPath, BaseResource> GetTypeDict<T>()
|
||||
{
|
||||
if (!_cachedResources.TryGetValue(typeof(T), out var ret))
|
||||
{
|
||||
Dispose(false);
|
||||
ret = new Dictionary<ResPath, BaseResource>();
|
||||
_cachedResources.Add(typeof(T), ret);
|
||||
}
|
||||
|
||||
#endregion IDisposable Members
|
||||
return ret;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Dictionary<ResPath, BaseResource> GetTypeDict<T>()
|
||||
{
|
||||
if (!CachedResources.TryGetValue(typeof(T), out var ret))
|
||||
{
|
||||
ret = new Dictionary<ResPath, BaseResource>();
|
||||
CachedResources.Add(typeof(T), ret);
|
||||
}
|
||||
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
|
||||
{
|
||||
OnRawTextureLoaded?.Invoke(eventArgs);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
|
||||
{
|
||||
OnRawTextureLoaded?.Invoke(eventArgs);
|
||||
}
|
||||
|
||||
public void RsiLoaded(RsiLoadedEventArgs eventArgs)
|
||||
{
|
||||
OnRsiLoaded?.Invoke(eventArgs);
|
||||
}
|
||||
|
||||
public void MountLoaderApi(IFileApi api, string apiPrefix, ResPath? prefix=null)
|
||||
{
|
||||
prefix ??= ResPath.Root;
|
||||
var root = new LoaderApiLoader(api, apiPrefix);
|
||||
AddRoot(prefix.Value, root);
|
||||
}
|
||||
public void RsiLoaded(RsiLoadedEventArgs eventArgs)
|
||||
{
|
||||
OnRsiLoaded?.Invoke(eventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,46 @@
|
||||
using System;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.ResourceManagement
|
||||
namespace Robust.Client.ResourceManagement;
|
||||
|
||||
public sealed class AudioResource : BaseResource
|
||||
{
|
||||
public sealed class AudioResource : BaseResource
|
||||
public AudioStream AudioStream { get; private set; } = default!;
|
||||
|
||||
public override void Load(IDependencyCollection dependencies, ResPath path)
|
||||
{
|
||||
public AudioStream AudioStream { get; private set; } = default!;
|
||||
var cache = dependencies.Resolve<IResourceManager>();
|
||||
|
||||
public override void Load(IResourceCache cache, ResPath path)
|
||||
if (!cache.ContentFileExists(path))
|
||||
{
|
||||
if (!cache.ContentFileExists(path))
|
||||
{
|
||||
throw new FileNotFoundException("Content file does not exist for audio sample.");
|
||||
}
|
||||
|
||||
using (var fileStream = cache.ContentFileRead(path))
|
||||
{
|
||||
if (path.Extension == "ogg")
|
||||
{
|
||||
AudioStream = cache.ClydeAudio.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
{
|
||||
AudioStream = cache.ClydeAudio.LoadAudioWav(fileStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
|
||||
}
|
||||
}
|
||||
throw new FileNotFoundException("Content file does not exist for audio sample.");
|
||||
}
|
||||
|
||||
public static implicit operator AudioStream(AudioResource res)
|
||||
using (var fileStream = cache.ContentFileRead(path))
|
||||
{
|
||||
return res.AudioStream;
|
||||
var audioManager = dependencies.Resolve<IAudioInternal>();
|
||||
if (path.Extension == "ogg")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator AudioStream(AudioResource res)
|
||||
{
|
||||
return res.AudioStream;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -9,16 +10,17 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
internal IFontFaceHandle FontFaceHandle { get; private set; } = default!;
|
||||
|
||||
public override void Load(IResourceCache cache, ResPath path)
|
||||
public override void Load(IDependencyCollection dependencies, ResPath path)
|
||||
{
|
||||
if (!cache.TryContentFileRead(path, out var stream))
|
||||
|
||||
if (!dependencies.Resolve<IResourceManager>().TryContentFileRead(path, out var stream))
|
||||
{
|
||||
throw new FileNotFoundException("Content file does not exist for font");
|
||||
}
|
||||
|
||||
using (stream)
|
||||
{
|
||||
FontFaceHandle = ((IFontManagerInternal)cache.FontManager).Load(stream);
|
||||
FontFaceHandle = dependencies.Resolve<IFontManagerInternal>().Load(stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Graphics.RSI;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -34,26 +35,27 @@ namespace Robust.Client.ResourceManagement
|
||||
/// </summary>
|
||||
public const uint MAXIMUM_RSI_VERSION = RsiLoading.MAXIMUM_RSI_VERSION;
|
||||
|
||||
public override void Load(IResourceCache cache, ResPath path)
|
||||
public override void Load(IDependencyCollection dependencies, ResPath path)
|
||||
{
|
||||
var loadStepData = new LoadStepData {Path = path};
|
||||
LoadPreTexture(cache, loadStepData);
|
||||
var manager = dependencies.Resolve<IResourceManager>();
|
||||
LoadPreTexture(manager, loadStepData);
|
||||
|
||||
loadStepData.AtlasTexture = cache.Clyde.LoadTextureFromImage(
|
||||
loadStepData.AtlasTexture = dependencies.Resolve<IClyde>().LoadTextureFromImage(
|
||||
loadStepData.AtlasSheet,
|
||||
loadStepData.Path.ToString());
|
||||
|
||||
LoadPostTexture(loadStepData);
|
||||
LoadFinish(cache, loadStepData);
|
||||
LoadFinish(dependencies.Resolve<IResourceCacheInternal>(), loadStepData);
|
||||
|
||||
loadStepData.AtlasSheet.Dispose();
|
||||
}
|
||||
|
||||
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
|
||||
internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
|
||||
{
|
||||
var manifestPath = data.Path / "meta.json";
|
||||
RsiLoading.RsiMetadata metadata;
|
||||
using (var manifestFile = cache.ContentFileRead(manifestPath))
|
||||
using (var manifestFile = manager.ContentFileRead(manifestPath))
|
||||
{
|
||||
metadata = RsiLoading.LoadRsiMetadata(manifestFile);
|
||||
}
|
||||
@@ -86,7 +88,7 @@ namespace Robust.Client.ResourceManagement
|
||||
var stateObject = metadata.States[index];
|
||||
// Load image from disk.
|
||||
var texPath = data.Path / (stateObject.StateId + ".png");
|
||||
using (var stream = cache.ContentFileRead(texPath))
|
||||
using (var stream = manager.ContentFileRead(texPath))
|
||||
{
|
||||
reg.Src = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
@@ -212,14 +214,10 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
}
|
||||
|
||||
internal void LoadFinish(IResourceCache cache, LoadStepData data)
|
||||
internal void LoadFinish(IResourceCacheInternal cache, LoadStepData data)
|
||||
{
|
||||
RSI = data.Rsi;
|
||||
|
||||
if (cache is IResourceCacheInternal cacheInternal)
|
||||
{
|
||||
cacheInternal.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
|
||||
}
|
||||
cache.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,28 +20,31 @@ namespace Robust.Client.ResourceManagement
|
||||
[ViewVariables]
|
||||
internal ParsedShader ParsedShader { get; private set; } = default!;
|
||||
|
||||
public override void Load(IResourceCache cache, ResPath path)
|
||||
public override void Load(IDependencyCollection dependencies, ResPath path)
|
||||
{
|
||||
using (var stream = cache.ContentFileRead(path))
|
||||
var manager = dependencies.Resolve<IResourceManager>();
|
||||
|
||||
using (var stream = manager.ContentFileRead(path))
|
||||
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
|
||||
{
|
||||
ParsedShader = ShaderParser.Parse(reader, cache);
|
||||
ParsedShader = ShaderParser.Parse(reader, manager);
|
||||
}
|
||||
|
||||
ClydeHandle = ((IClydeInternal)cache.Clyde).LoadShader(ParsedShader, path.ToString());
|
||||
ClydeHandle = dependencies.Resolve<IClydeInternal>().LoadShader(ParsedShader, path.ToString());
|
||||
}
|
||||
|
||||
public override void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
|
||||
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
|
||||
{
|
||||
var manager = dependencies.Resolve<IResourceManager>();
|
||||
ct = ct != default ? ct : new CancellationTokenSource(30000).Token;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = cache.ContentFileRead(path);
|
||||
using var stream = manager.ContentFileRead(path);
|
||||
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
|
||||
ParsedShader = ShaderParser.Parse(reader, cache);
|
||||
ParsedShader = ShaderParser.Parse(reader, manager);
|
||||
break;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
@@ -57,7 +60,7 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
}
|
||||
|
||||
((IClydeInternal)cache.Clyde).ReloadShader(ClydeHandle, ParsedShader);
|
||||
dependencies.Resolve<IClydeInternal>().ReloadShader(ClydeHandle, ParsedShader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
@@ -19,7 +20,7 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
public Texture Texture => _texture;
|
||||
|
||||
public override void Load(IResourceCache cache, ResPath path)
|
||||
public override void Load(IDependencyCollection dependencies, ResPath path)
|
||||
{
|
||||
if (path.Directory.Filename.EndsWith(".rsi"))
|
||||
{
|
||||
@@ -31,12 +32,12 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
LoadPreTexture(cache, data);
|
||||
LoadTexture(cache.Clyde, data);
|
||||
LoadFinish(cache, data);
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTexture(dependencies.Resolve<IClyde>(), data);
|
||||
LoadFinish(dependencies.Resolve<IResourceCache>(), data);
|
||||
}
|
||||
|
||||
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
|
||||
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
|
||||
{
|
||||
using (var stream = cache.ContentFileRead(data.Path))
|
||||
{
|
||||
@@ -63,7 +64,7 @@ namespace Robust.Client.ResourceManagement
|
||||
data.Image.Dispose();
|
||||
}
|
||||
|
||||
private static TextureLoadParameters? TryLoadTextureParameters(IResourceCache cache, ResPath path)
|
||||
private static TextureLoadParameters? TryLoadTextureParameters(IResourceManager cache, ResPath path)
|
||||
{
|
||||
var metaPath = path.WithName(path.Filename + ".yml");
|
||||
if (cache.TryContentFileRead(metaPath, out var stream))
|
||||
@@ -90,12 +91,11 @@ namespace Robust.Client.ResourceManagement
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
|
||||
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
|
||||
{
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
|
||||
var data = new LoadStepData {Path = path};
|
||||
LoadPreTexture(cache, data);
|
||||
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
|
||||
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
|
||||
{
|
||||
@@ -106,7 +106,7 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
// Dimensions do not match, make new texture.
|
||||
_texture.Dispose();
|
||||
LoadTexture(clyde, data);
|
||||
LoadTexture(dependencies.Resolve<IClyde>(), data);
|
||||
_texture = data.Texture;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,20 @@
|
||||
<PackageReference Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.2" Condition="'$(UseSystemSqlite)' == 'True'" PrivateAssets="compile" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.2" Condition="'$(UseSystemSqlite)' != 'True'" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.NFluidsynth" Version="0.1.1" PrivateAssets="compile" />
|
||||
<PackageReference Include="NVorbis" Version="0.10.1" PrivateAssets="compile" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||
<PackageReference Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" PrivateAssets="compile" />
|
||||
<PackageReference Include="OpenTK.OpenAL" Version="4.7.5" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" PrivateAssets="compile" />
|
||||
<PackageReference Include="Robust.Natives" Version="0.1.1" />
|
||||
<PackageReference Include="System.Numerics.Vectors" Version="4.4.0" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-rc2" PrivateAssets="compile" />
|
||||
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" Version="1.2.0" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.Sodium" Version="0.2.1" PrivateAssets="compile" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.0.1" PrivateAssets="compile" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.0.1" PrivateAssets="compile" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" PrivateAssets="compile" />
|
||||
|
||||
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
@@ -37,10 +38,11 @@ public sealed class UITheme : IPrototype
|
||||
public Dictionary<string, Color>? Colors { get; }
|
||||
public ResPath Path => _path == default ? new ResPath(DefaultPath+"/"+ID) : _path;
|
||||
|
||||
private void ValidateFilePath(IResourceCache resourceCache)
|
||||
private void ValidateFilePath(IResourceManager manager)
|
||||
{
|
||||
var foundFolders = resourceCache.ContentFindFiles(Path.ToRootedPath());
|
||||
if (!foundFolders.Any()) throw new Exception("UITheme: "+ID+" not found in resources!");
|
||||
var foundFolders = manager.ContentFindFiles(Path.ToRootedPath());
|
||||
if (!foundFolders.Any())
|
||||
throw new Exception("UITheme: "+ID+" not found in resources!");
|
||||
}
|
||||
|
||||
public Texture ResolveTexture(string texturePath)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -40,6 +41,7 @@ internal partial class UserInterfaceManager
|
||||
private bool _showingTooltip;
|
||||
private Control? _suppliedTooltip;
|
||||
private const float TooltipDelay = 0.25f;
|
||||
private readonly Dictionary<BoundKeyFunction, Control> _focusedControls = new();
|
||||
|
||||
private WindowRoot? _focusedRoot;
|
||||
|
||||
@@ -109,13 +111,13 @@ internal partial class UserInterfaceManager
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
_focusedControls[args.Function] = control;
|
||||
OnKeyBindDown?.Invoke(control);
|
||||
}
|
||||
|
||||
public void KeyBindUp(BoundKeyEventArgs args)
|
||||
{
|
||||
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
|
||||
if (control == null)
|
||||
if (!_focusedControls.TryGetValue(args.Function, out var control))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -129,6 +131,7 @@ internal partial class UserInterfaceManager
|
||||
// Always mark this as handled.
|
||||
// The only case it should not be is if we do not have a control to click on,
|
||||
// in which case we never reach this.
|
||||
_focusedControls.Remove(args.Function);
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Robust.Client.ViewVariables.Editors
|
||||
|
||||
void OnEntered(LineEdit.LineEditEventArgs e)
|
||||
{
|
||||
var gridVal = EntityUid.Parse(gridId.Text, "-1");
|
||||
var gridVal = EntityUid.Parse(gridId.Text);
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
var xVal = float.Parse(x.Text, CultureInfo.InvariantCulture);
|
||||
var yVal = float.Parse(y.Text, CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Robust.Client.ViewVariables.Editors
|
||||
if (!ReadOnly)
|
||||
{
|
||||
lineEdit.OnTextEntered += e =>
|
||||
ValueChanged(EntityUid.Parse(e.Text, ", -1"));
|
||||
ValueChanged(EntityUid.Parse(e.Text));
|
||||
}
|
||||
|
||||
var vvButton = new Button()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.AudioLoading;
|
||||
using Robust.Shared.Serialization;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Packaging.AssetProcessing.Passes;
|
||||
|
||||
/// <summary>
|
||||
/// Strips out audio files and writes them to a metadata .yml
|
||||
/// Used for server packaging to avoid bundling entire audio files on the server.
|
||||
/// </summary>
|
||||
public sealed class AssetPassAudioMetadata : AssetPass
|
||||
{
|
||||
private readonly List<AudioMetadataPrototype> _audioMetadata = new();
|
||||
private readonly string _metadataPath;
|
||||
|
||||
public AssetPassAudioMetadata(string metadataPath = "Prototypes/_audio_metadata.yml")
|
||||
{
|
||||
_metadataPath = metadataPath;
|
||||
}
|
||||
|
||||
protected override AssetFileAcceptResult AcceptFile(AssetFile file)
|
||||
{
|
||||
if (!AudioLoader.IsLoadableAudioFile(file.Path))
|
||||
return AssetFileAcceptResult.Pass;
|
||||
|
||||
using var stream = file.Open();
|
||||
var metadata = AudioLoader.LoadAudioMetadata(stream, file.Path);
|
||||
|
||||
lock (_audioMetadata)
|
||||
{
|
||||
_audioMetadata.Add(new AudioMetadataPrototype()
|
||||
{
|
||||
ID = "/" + file.Path,
|
||||
Length = metadata.Length,
|
||||
});
|
||||
}
|
||||
|
||||
return AssetFileAcceptResult.Consumed;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentlySynchronizedField")]
|
||||
protected override void AcceptFinished()
|
||||
{
|
||||
if (_audioMetadata.Count == 0)
|
||||
{
|
||||
Logger?.Debug("Have no audio metadata, not writing anything");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger?.Debug("Writing audio metadata for {0} audio files", _audioMetadata.Count);
|
||||
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
var root = new YamlSequenceNode();
|
||||
var document = new YamlDocument(root);
|
||||
|
||||
foreach (var prototype in _audioMetadata)
|
||||
{
|
||||
// TODO: I know but sermanager and please get me out of this hell.
|
||||
var jaml = new YamlMappingNode
|
||||
{
|
||||
{ "type", AudioMetadataPrototype.ProtoName },
|
||||
{ "id", new YamlScalarNode(prototype.ID) },
|
||||
{ "length", new YamlScalarNode(prototype.Length.TotalSeconds.ToString(CultureInfo.InvariantCulture)) }
|
||||
};
|
||||
root.Add(jaml);
|
||||
}
|
||||
|
||||
RunJob(() =>
|
||||
{
|
||||
using var memory = new MemoryStream();
|
||||
using var writer = new StreamWriter(memory);
|
||||
var yamlStream = new YamlStream(document);
|
||||
yamlStream.Save(new YamlNoDocEndDotsFix(new YamlMappingFix(new Emitter(writer))), false);
|
||||
writer.Flush();
|
||||
var result = new AssetFileMemory(_metadataPath, memory.ToArray());
|
||||
SendFile(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
28
Robust.Packaging/AssetProcessing/Passes/AssetPassPrefix.cs
Normal file
28
Robust.Packaging/AssetProcessing/Passes/AssetPassPrefix.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Robust.Packaging.AssetProcessing.Passes;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a prefix to file paths of passed-through files.
|
||||
/// </summary>
|
||||
public sealed class AssetPassPrefix : AssetPass
|
||||
{
|
||||
public string Prefix { get; set; }
|
||||
|
||||
public AssetPassPrefix(string prefix)
|
||||
{
|
||||
Prefix = prefix;
|
||||
}
|
||||
|
||||
protected override AssetFileAcceptResult AcceptFile(AssetFile file)
|
||||
{
|
||||
var newPath = Prefix + file.Path;
|
||||
var newFile = file switch
|
||||
{
|
||||
AssetFileDisk disk => (AssetFile) new AssetFileDisk(newPath, disk.DiskPath),
|
||||
AssetFileMemory memory => new AssetFileMemory(newPath, memory.Memory),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(file))
|
||||
};
|
||||
|
||||
SendFile(newFile);
|
||||
return AssetFileAcceptResult.Consumed;
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,9 @@
|
||||
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NVorbis" Version="0.10.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class RobustClientAssetGraph
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<AssetPass> AllPasses { get; }
|
||||
|
||||
/// <param name="parallel">Should inputs be run in parallel. Should only be turned off for debugging.</param>
|
||||
public RobustClientAssetGraph(bool parallel = true)
|
||||
{
|
||||
// The code injecting the list of source files is assumed to be pretty single-threaded.
|
||||
@@ -40,7 +41,7 @@ public sealed class RobustClientAssetGraph
|
||||
Input,
|
||||
PresetPasses,
|
||||
Output,
|
||||
NormalizeText
|
||||
NormalizeText,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
129
Robust.Packaging/RobustServerAssetGraph.cs
Normal file
129
Robust.Packaging/RobustServerAssetGraph.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Robust.Packaging.AssetProcessing;
|
||||
using Robust.Packaging.AssetProcessing.Passes;
|
||||
|
||||
namespace Robust.Packaging;
|
||||
|
||||
/// <summary>
|
||||
/// Standard asset graph for packaging server files. Extend by wiring things up to <see cref="InputCore"/>, <see cref="InputResources"/>, and <see cref="Output"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This graph has two inputs: one for "core" server files such as the main engine executable, and another for resource files.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If you want to add extra passes to run before preset passes, depend them on the relevant input pass, with a before of the relevant preset pass.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See the following graph (Mermaid syntax) to understand this asset graph:
|
||||
/// </para>
|
||||
/// <code>
|
||||
/// flowchart LR
|
||||
/// InputCore --> PresetPassesCore
|
||||
/// PresetPassesCore --1--> NormalizeTextCore
|
||||
/// NormalizeTextCore --> Output
|
||||
/// PresetPassesCore --2--> Output
|
||||
/// InputResources --> PresetPassesResources
|
||||
/// PresetPassesResources --1--> AudioMetadata
|
||||
/// PresetPassesResources --2--> NormalizeTextResources
|
||||
/// PresetPassesResources --3--> PrefixResources
|
||||
/// AudioMetadata --> PrefixResources
|
||||
/// NormalizeTextResources --> PrefixResources
|
||||
/// PrefixResources --> Output
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public sealed class RobustServerAssetGraph
|
||||
{
|
||||
public AssetPassPipe Output { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Input pass for core server files, such as <c>Robust.Server.exe</c>.
|
||||
/// </summary>
|
||||
/// <seealso cref="InputResources"/>
|
||||
public AssetPassPipe InputCore { get; }
|
||||
|
||||
public AssetPassPipe PresetPassesCore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes text files in core files.
|
||||
/// </summary>
|
||||
public AssetPassNormalizeText NormalizeTextCore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Input pass for server resource files. Everything that will go into <c>Resources/</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not prefix file paths with <c>Resources/</c>, the asset pass will automatically remap them.
|
||||
/// </remarks>
|
||||
/// <seealso cref="InputCore"/>
|
||||
public AssetPassPipe InputResources { get; }
|
||||
public AssetPassPipe PresetPassesResources { get; }
|
||||
public AssetPassAudioMetadata AudioMetadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes text files in resources.
|
||||
/// </summary>
|
||||
public AssetPassNormalizeText NormalizeTextResources { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for putting resources into the "<c>Resources/</c>" folder.
|
||||
/// </summary>
|
||||
public AssetPassPrefix PrefixResources { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of all passes in this preset graph.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<AssetPass> AllPasses { get; }
|
||||
|
||||
/// <param name="parallel">Should inputs be run in parallel. Should only be turned off for debugging.</param>
|
||||
public RobustServerAssetGraph(bool parallel = true)
|
||||
{
|
||||
Output = new AssetPassPipe { Name = "RobustServerAssetGraphOutput", CheckDuplicates = true };
|
||||
|
||||
//
|
||||
// Core files
|
||||
//
|
||||
|
||||
// The code injecting the list of source files is assumed to be pretty single-threaded.
|
||||
// We use a parallelizing input to break out all the work on files coming in onto multiple threads.
|
||||
InputCore = new AssetPassPipe { Name = "RobustServerAssetGraphInputCore", Parallelize = parallel };
|
||||
PresetPassesCore = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesCore" };
|
||||
NormalizeTextCore = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextCore" };
|
||||
|
||||
PresetPassesCore.AddDependency(InputCore);
|
||||
NormalizeTextCore.AddDependency(PresetPassesCore).AddBefore(Output);
|
||||
Output.AddDependency(PresetPassesCore);
|
||||
Output.AddDependency(NormalizeTextCore);
|
||||
|
||||
//
|
||||
// Resource files
|
||||
//
|
||||
|
||||
// Ditto about parallelizing
|
||||
InputResources = new AssetPassPipe { Name = "RobustServerAssetGraphInputResources", Parallelize = parallel };
|
||||
PresetPassesResources = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesResources" };
|
||||
NormalizeTextResources = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextResources" };
|
||||
AudioMetadata = new AssetPassAudioMetadata { Name = "RobustServerAssetGraphAudioMetadata" };
|
||||
PrefixResources = new AssetPassPrefix("Resources/") { Name = "RobustServerAssetGraphPrefixResources" };
|
||||
|
||||
PresetPassesResources.AddDependency(InputResources);
|
||||
AudioMetadata.AddDependency(PresetPassesResources).AddBefore(NormalizeTextResources);
|
||||
NormalizeTextResources.AddDependency(PresetPassesResources).AddBefore(PrefixResources);
|
||||
PrefixResources.AddDependency(PresetPassesResources);
|
||||
PrefixResources.AddDependency(AudioMetadata);
|
||||
PrefixResources.AddDependency(NormalizeTextResources);
|
||||
Output.AddDependency(PrefixResources);
|
||||
|
||||
AllPasses = new AssetPass[]
|
||||
{
|
||||
Output,
|
||||
InputCore,
|
||||
PresetPassesCore,
|
||||
NormalizeTextCore,
|
||||
InputResources,
|
||||
PresetPassesResources,
|
||||
NormalizeTextResources,
|
||||
AudioMetadata,
|
||||
PrefixResources,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ public sealed class RobustServerPackaging
|
||||
{
|
||||
public static IReadOnlySet<string> ServerIgnoresResources { get; } = new HashSet<string>
|
||||
{
|
||||
"Audio",
|
||||
"Textures",
|
||||
"Fonts",
|
||||
"Shaders",
|
||||
@@ -19,15 +18,16 @@ public sealed class RobustServerPackaging
|
||||
{
|
||||
var ignoreSet = ServerIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
|
||||
|
||||
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"),
|
||||
await RobustSharedPackaging.DoResourceCopy(
|
||||
Path.Combine(contentDir, "Resources"),
|
||||
pass,
|
||||
ignoreSet,
|
||||
"Resources",
|
||||
cancel);
|
||||
await RobustSharedPackaging.DoResourceCopy(Path.Combine("RobustToolbox", "Resources"),
|
||||
cancel: cancel);
|
||||
|
||||
await RobustSharedPackaging.DoResourceCopy(
|
||||
Path.Combine("RobustToolbox", "Resources"),
|
||||
pass,
|
||||
ignoreSet,
|
||||
"Resources",
|
||||
cancel);
|
||||
cancel: cancel);
|
||||
}
|
||||
}
|
||||
|
||||
79
Robust.Server/Audio/AudioSystem.Effects.cs
Normal file
79
Robust.Server/Audio/AudioSystem.Effects.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Components;
|
||||
using Robust.Shared.Audio.Effects;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.Audio;
|
||||
|
||||
public sealed partial class AudioSystem
|
||||
{
|
||||
protected override void InitializeEffect()
|
||||
{
|
||||
base.InitializeEffect();
|
||||
SubscribeLocalEvent<AudioEffectComponent, ComponentAdd>(OnEffectAdd);
|
||||
SubscribeLocalEvent<AudioAuxiliaryComponent, ComponentAdd>(OnAuxiliaryAdd);
|
||||
}
|
||||
|
||||
private void ShutdownEffect()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads all <see cref="AudioPresetPrototype"/> entities.
|
||||
/// </summary>
|
||||
public void ReloadPresets()
|
||||
{
|
||||
var query = AllEntityQuery<AudioPresetComponent>();
|
||||
var toDelete = new ValueList<EntityUid>();
|
||||
|
||||
while (query.MoveNext(out var uid, out _))
|
||||
{
|
||||
toDelete.Add(uid);
|
||||
}
|
||||
|
||||
foreach (var ent in toDelete)
|
||||
{
|
||||
Del(ent);
|
||||
}
|
||||
|
||||
foreach (var proto in ProtoMan.EnumeratePrototypes<AudioPresetPrototype>())
|
||||
{
|
||||
if (!proto.CreateAuxiliary)
|
||||
continue;
|
||||
|
||||
var effect = CreateEffect();
|
||||
var aux = CreateAuxiliary();
|
||||
SetEffectPreset(effect.Entity, effect.Component, proto);
|
||||
SetEffect(aux.Entity, aux.Component, effect.Entity);
|
||||
var preset = AddComp<AudioPresetComponent>(aux.Entity);
|
||||
_auxiliaries.Remove(preset.Preset);
|
||||
preset.Preset = proto.ID;
|
||||
_auxiliaries[preset.Preset] = aux.Entity;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEffectAdd(EntityUid uid, AudioEffectComponent component, ComponentAdd args)
|
||||
{
|
||||
component.Effect = new DummyAudioEffect();
|
||||
}
|
||||
|
||||
private void OnAuxiliaryAdd(EntityUid uid, AudioAuxiliaryComponent component, ComponentAdd args)
|
||||
{
|
||||
component.Auxiliary = new DummyAuxiliaryAudio();
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioAuxiliaryComponent Component) CreateAuxiliary()
|
||||
{
|
||||
var (ent, comp) = base.CreateAuxiliary();
|
||||
_pvs.AddGlobalOverride(GetNetEntity(ent));
|
||||
return (ent, comp);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioEffectComponent Component) CreateEffect()
|
||||
{
|
||||
var (ent, comp) = base.CreateEffect();
|
||||
_pvs.AddGlobalOverride(GetNetEntity(ent));
|
||||
return (ent, comp);
|
||||
}
|
||||
}
|
||||
224
Robust.Server/Audio/AudioSystem.cs
Normal file
224
Robust.Server/Audio/AudioSystem.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.AudioLoading;
|
||||
using Robust.Shared.Audio.Components;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Audio;
|
||||
|
||||
public sealed partial class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
|
||||
private readonly Dictionary<string, TimeSpan> _cachedAudioLengths = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AudioComponent, ComponentStartup>(OnAudioStartup);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
ShutdownEffect();
|
||||
}
|
||||
|
||||
private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args)
|
||||
{
|
||||
component.Source = new DummyAudioSource();
|
||||
}
|
||||
|
||||
private void AddAudioFilter(EntityUid uid, AudioComponent component, Filter filter)
|
||||
{
|
||||
var count = filter.Count;
|
||||
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
var nent = GetNetEntity(uid);
|
||||
_pvs.AddSessionOverrides(nent, filter);
|
||||
|
||||
var ents = new HashSet<EntityUid>(count);
|
||||
|
||||
foreach (var session in filter.Recipients)
|
||||
{
|
||||
var ent = session.AttachedEntity;
|
||||
|
||||
if (ent == null)
|
||||
continue;
|
||||
|
||||
ents.Add(ent.Value);
|
||||
}
|
||||
|
||||
DebugTools.Assert(component.IncludedEntities == null);
|
||||
component.IncludedEntities = ents;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
var entity = Spawn("Audio", MapCoordinates.Nullspace);
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
AddAudioFilter(entity, audio, playerFilter);
|
||||
audio.Global = true;
|
||||
|
||||
return (entity, audio);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if (!Exists(uid))
|
||||
return null;
|
||||
|
||||
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
AddAudioFilter(entity, audio, playerFilter);
|
||||
|
||||
return (entity, audio);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
if (!Exists(uid))
|
||||
return null;
|
||||
|
||||
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
|
||||
return (entity, audio);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if (!coordinates.IsValid(EntityManager))
|
||||
return null;
|
||||
|
||||
var entity = Spawn("Audio", coordinates);
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
AddAudioFilter(entity, audio, playerFilter);
|
||||
|
||||
return (entity, audio);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
if (!coordinates.IsValid(EntityManager))
|
||||
return null;
|
||||
|
||||
var entity = Spawn("Audio", coordinates);
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
|
||||
return (entity, audio);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
|
||||
{
|
||||
if (sound == null)
|
||||
return null;
|
||||
|
||||
var audio = PlayPvs(GetSound(sound), source, audioParams ?? sound.Params);
|
||||
|
||||
if (audio == null)
|
||||
return null;
|
||||
|
||||
audio.Value.Component.ExcludedEntity = user;
|
||||
return audio;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
|
||||
{
|
||||
if (sound == null)
|
||||
return null;
|
||||
|
||||
var audio = PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
|
||||
|
||||
if (audio == null)
|
||||
return null;
|
||||
|
||||
audio.Value.Component.ExcludedEntity = user;
|
||||
return audio;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayGlobal(filename, actor.PlayerSession, audioParams);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override TimeSpan GetAudioLengthImpl(string filename)
|
||||
{
|
||||
// Check shipped metadata from packaging.
|
||||
if (ProtoMan.TryIndex(filename, out AudioMetadataPrototype? metadata))
|
||||
return metadata.Length;
|
||||
|
||||
// Try loading audio files directly.
|
||||
// This is necessary in development and environments,
|
||||
// and when working with audio files uploaded dynamically at runtime.
|
||||
if (_cachedAudioLengths.TryGetValue(filename, out var length))
|
||||
return length;
|
||||
|
||||
if (!_resourceManager.TryContentFileRead(filename, out var stream))
|
||||
throw new FileNotFoundException($"Unable to find metadata for audio file {filename}");
|
||||
|
||||
using (stream)
|
||||
{
|
||||
var loadedMetadata = AudioLoader.LoadAudioMetadata(stream, filename);
|
||||
_cachedAudioLengths.Add(filename, loadedMetadata.Length);
|
||||
return loadedMetadata.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,8 +204,6 @@ namespace Robust.Server
|
||||
|
||||
ProfileOptSetup.Setup(_config);
|
||||
|
||||
_parallelMgr.Initialize();
|
||||
|
||||
//Sets up Logging
|
||||
_logHandlerFactory = logHandlerFactory;
|
||||
|
||||
@@ -268,6 +266,7 @@ namespace Robust.Server
|
||||
|
||||
// Has to be done early because this guy's in charge of the main thread Synchronization Context.
|
||||
_taskManager.Initialize();
|
||||
_parallelMgr.Initialize();
|
||||
|
||||
LoadSettings();
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
|
||||
|
||||
GC.Collect();
|
||||
|
||||
Span<EntityUid> ents = stackalloc EntityUid[amount];
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
@@ -51,17 +50,12 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
|
||||
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
ents[i] = _entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
|
||||
_entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
|
||||
}
|
||||
|
||||
MeasureProfiler.SaveData();
|
||||
|
||||
shell.WriteLine($"Server: Profiled spawning {amount} entities in {stopwatch.Elapsed.TotalMilliseconds:N3} ms");
|
||||
|
||||
foreach (var ent in ents)
|
||||
{
|
||||
_entities.DeleteEntity(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
[UsedImplicitly]
|
||||
public sealed class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
|
||||
private uint _streamIndex;
|
||||
|
||||
private sealed class AudioSourceServer : IPlayingAudioStream
|
||||
{
|
||||
private readonly uint _id;
|
||||
private readonly AudioSystem _audioSystem;
|
||||
private readonly IEnumerable<ICommonSession>? _sessions;
|
||||
|
||||
internal AudioSourceServer(AudioSystem parent, uint identifier, IEnumerable<ICommonSession>? sessions = null)
|
||||
{
|
||||
_audioSystem = parent;
|
||||
_id = identifier;
|
||||
_sessions = sessions;
|
||||
}
|
||||
public void Stop()
|
||||
{
|
||||
_audioSystem.InternalStop(_id, _sessions);
|
||||
}
|
||||
}
|
||||
|
||||
private void InternalStop(uint id, IEnumerable<ICommonSession>? sessions = null)
|
||||
{
|
||||
var msg = new StopAudioMessageClient
|
||||
{
|
||||
Identifier = id
|
||||
};
|
||||
|
||||
if (sessions == null)
|
||||
RaiseNetworkEvent(msg);
|
||||
else
|
||||
{
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
RaiseNetworkEvent(msg, session.ConnectedClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private uint CacheIdentifier()
|
||||
{
|
||||
return unchecked(_streamIndex++);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
var id = CacheIdentifier();
|
||||
var msg = new PlayAudioGlobalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
AudioParams = audioParams ?? AudioParams.Default,
|
||||
Identifier = id
|
||||
};
|
||||
|
||||
RaiseNetworkEvent(msg, playerFilter, recordReplay);
|
||||
|
||||
return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray());
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if(!EntityManager.TryGetComponent<TransformComponent>(uid, out var transform))
|
||||
return null;
|
||||
|
||||
var id = CacheIdentifier();
|
||||
|
||||
var fallbackCoordinates = GetFallbackCoordinates(transform.MapPosition);
|
||||
|
||||
var msg = new PlayAudioEntityMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Coordinates = GetNetCoordinates(transform.Coordinates),
|
||||
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
|
||||
NetEntity = GetNetEntity(uid),
|
||||
AudioParams = audioParams ?? AudioParams.Default,
|
||||
Identifier = id,
|
||||
};
|
||||
|
||||
RaiseNetworkEvent(msg, playerFilter, recordReplay);
|
||||
|
||||
return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
var id = CacheIdentifier();
|
||||
|
||||
var fallbackCoordinates = GetFallbackCoordinates(coordinates.ToMap(EntityManager, _transform));
|
||||
|
||||
var msg = new PlayAudioPositionalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Coordinates = GetNetCoordinates(coordinates),
|
||||
FallbackCoordinates = GetNetCoordinates(fallbackCoordinates),
|
||||
AudioParams = audioParams ?? AudioParams.Default,
|
||||
Identifier = id
|
||||
};
|
||||
|
||||
RaiseNetworkEvent(msg, playerFilter, recordReplay);
|
||||
|
||||
return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
|
||||
{
|
||||
if (sound == null)
|
||||
return null;
|
||||
|
||||
var filter = Filter.Pvs(source, entityManager: EntityManager, playerManager: PlayerManager, cfgManager: CfgManager).RemoveWhereAttachedEntity(e => e == user);
|
||||
return Play(sound, filter, source, true, audioParams);
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
if (sound == null)
|
||||
return null;
|
||||
|
||||
var filter = Filter.Pvs(coordinates, entityMan: EntityManager, playerMan: PlayerManager).RemoveWhereAttachedEntity(e => e == user);
|
||||
return Play(sound, filter, coordinates, true, audioParams);
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayGlobal(filename, actor.PlayerSession, audioParams);
|
||||
return null;
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
|
||||
return null;
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return Play(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
|
||||
}
|
||||
|
||||
public override IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Arch.Core;
|
||||
using Arch.Core.Utils;
|
||||
using Collections.Pooled;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -157,11 +154,10 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
result = Deserialize(data);
|
||||
_logLoader.Debug($"Loaded map in {sw.Elapsed}");
|
||||
|
||||
_logLoader.Info($"Loaded map {resPath} in {sw.Elapsed}");
|
||||
var xformQuery = _serverEntityManager.GetEntityQuery<TransformComponent>();
|
||||
var mapEnt = _mapManager.GetMapEntityId(mapId);
|
||||
|
||||
var xformQuery = _serverEntityManager.GetEntityQuery<TransformComponent>();
|
||||
var rootEnts = new List<EntityUid>();
|
||||
// aeoeoeieioe content
|
||||
|
||||
@@ -178,8 +174,6 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
// EntityManager.CleanupArch();
|
||||
|
||||
rootUids = rootEnts;
|
||||
}
|
||||
|
||||
@@ -434,10 +428,6 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
if (data.Version >= 4)
|
||||
{
|
||||
var metaEntities = data.RootMappingNode.Get<SequenceDataNode>("entities");
|
||||
using var mapSaveCompType = new PooledSet<Type>()
|
||||
{
|
||||
typeof(MapSaveIdComponent)
|
||||
};
|
||||
|
||||
foreach (var metaDef in metaEntities.Cast<MappingDataNode>())
|
||||
{
|
||||
@@ -457,30 +447,9 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
|
||||
var entities = (SequenceDataNode) metaDef["entities"];
|
||||
EntityPrototype? proto = null;
|
||||
var count = entities.Count;
|
||||
var entTotal = data.Entities.Count + count;
|
||||
data.Entities.EnsureCapacity(entTotal);
|
||||
data.UidEntityMap.EnsureCapacity(entTotal);
|
||||
data.EntitiesToDeserialize.EnsureCapacity(entTotal);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
if (_prototypeManager.TryIndex(type, out proto) && count > 1)
|
||||
{
|
||||
ComponentType[] compTypes;
|
||||
|
||||
if (data.Options.StoreMapUids)
|
||||
{
|
||||
compTypes = EntityManager.GetComponentType(proto, mapSaveCompType);
|
||||
}
|
||||
else
|
||||
{
|
||||
compTypes = EntityManager.GetComponentType(proto);
|
||||
}
|
||||
|
||||
EntityManager.Reserve(compTypes, count);
|
||||
}
|
||||
}
|
||||
_prototypeManager.TryIndex(type, out proto);
|
||||
|
||||
foreach (var entityDef in entities.Cast<MappingDataNode>())
|
||||
{
|
||||
@@ -495,7 +464,6 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
{
|
||||
deletedPrototypeUids.Add(entity);
|
||||
}
|
||||
// TODO: Move this elsewhere?
|
||||
else if (data.Options.StoreMapUids)
|
||||
{
|
||||
var comp = _serverEntityManager.AddComponent<MapSaveIdComponent>(entity);
|
||||
@@ -604,7 +572,6 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
_context.CurrentlyIgnoredComponents = missingComponentList.Cast<ValueDataNode>().Select(x => x.Value).ToHashSet();
|
||||
|
||||
_serverEntityManager.FinishEntityLoad(uid, meta.EntityPrototype, _context);
|
||||
|
||||
if (_context.CurrentlyIgnoredComponents.Count > 0)
|
||||
meta.LastComponentRemoved = _timing.CurTick;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ namespace Robust.Server.GameObjects
|
||||
_cfg.OnValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
|
||||
}
|
||||
|
||||
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
|
||||
{
|
||||
EnsureComp<PhysicsMapComponent>(uid);
|
||||
}
|
||||
|
||||
private void SetGridDeletion(bool value)
|
||||
{
|
||||
_deleteEmptyGrids = value;
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Robust.Server.GameObjects
|
||||
/// Manager for entities -- controls things like template loading and instantiation
|
||||
/// </summary>
|
||||
[UsedImplicitly] // DI Container
|
||||
public sealed partial class ServerEntityManager : EntityManager, IServerEntityManagerInternal
|
||||
public sealed class ServerEntityManager : EntityManager, IServerEntityManagerInternal
|
||||
{
|
||||
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
|
||||
"robust_entities_count",
|
||||
@@ -62,7 +62,7 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
|
||||
{
|
||||
return AllocEntity(prototype, out _, out _);
|
||||
return AllocEntity(prototype, out _);
|
||||
}
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, IEntityLoadContext? context)
|
||||
@@ -85,15 +85,15 @@ namespace Robust.Server.GameObjects
|
||||
StartEntity(entity);
|
||||
}
|
||||
|
||||
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, out TransformComponent xform, IEntityLoadContext? context = null)
|
||||
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
|
||||
{
|
||||
if (prototypeName == null)
|
||||
return base.CreateEntity(prototypeName, out metadata, out xform, context);
|
||||
return base.CreateEntity(prototypeName, out metadata, context);
|
||||
|
||||
if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
|
||||
throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");
|
||||
|
||||
var entity = base.CreateEntity(prototype, out metadata, out xform, context);
|
||||
var entity = base.CreateEntity(prototype, out metadata, context);
|
||||
|
||||
// At this point in time, all data configure on the entity *should* be purely from the prototype.
|
||||
// As such, we can reset the modified ticks to Zero,
|
||||
@@ -159,7 +159,7 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
base.TickUpdate(frameTime, noPredictions, histogram);
|
||||
|
||||
EntitiesCount.Set(EntityCount);
|
||||
EntitiesCount.Set(Entities.Count);
|
||||
}
|
||||
|
||||
public uint GetLastMessageSequence(ICommonSession session)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user