Compare commits

..

60 Commits

Author SHA1 Message Date
metalgearsloth
b6cadfedd5 Update arch (#4605) 2023-11-25 14:56:56 +11:00
metalgearsloth
9f57b705d7 Version: 182.0.0 2023-11-24 00:21:00 +11:00
metalgearsloth
68be9712ad Add entity gen to hashcode (#4601) 2023-11-24 00:19:58 +11:00
metalgearsloth
aaa446254c Version: 181.0.2 2023-11-23 23:43:47 +11:00
metalgearsloth
5e2d2ab317 Fix too many pointlights causing blackscreen (#4599) 2023-11-23 23:39:51 +11:00
metalgearsloth
20ae63fbbd Replace tile intersecting with enumerator (#4595) 2023-11-23 22:36:40 +11:00
metalgearsloth
a92c0cbef4 Fix nullable comps being raised for client gamestates (#4596) 2023-11-23 22:07:27 +11:00
metalgearsloth
95649a2dd0 Version: 181.0.1 2023-11-23 16:53:03 +11:00
metalgearsloth
861807f8b4 Fix HasComp(uid, Type) (#4594) 2023-11-23 16:52:31 +11:00
metalgearsloth
bd73f1c05a Version: 181.0.0 2023-11-23 15:28:35 +11:00
metalgearsloth
7dce51e2cf Arch PR two electric boogaloo (#4388)
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-11-23 14:29:37 +11:00
DrSmugleaf
d9b0f3a227 Version: 180.2.1 2023-11-22 17:02:13 -08:00
Vasilis
05766a2eaa Fix not using dotnet 7 for actions in engine (#4591) 2023-11-23 00:43:15 +01:00
metalgearsloth
a761fbc09e Version: 180.2.0 2023-11-22 22:00:05 +11:00
metalgearsloth
f69440b3f2 Minor PVS stuff (#4573)
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-11-22 21:54:07 +11:00
Leon Friedrich
b459d2ce21 Add more map system helper methods. (#4589) 2023-11-22 21:51:46 +11:00
Leon Friedrich
202182e3d4 Add new EnsureEntity variants (#4586) 2023-11-20 17:44:36 +11:00
metalgearsloth
96cb52e5d2 Version: 180.1.0 2023-11-20 16:31:41 +11:00
metalgearsloth
82e0c0baeb Add cvar for lidgren pool size (#4585) 2023-11-20 16:28:13 +11:00
Leon Friedrich
54d6552164 Fix shape lookups for non-hard fixtures (#4583) 2023-11-20 16:15:27 +11:00
Jordan Dominion
c21b6c993c Fix potential error when writing runtime log (#4575) 2023-11-19 15:47:53 +01:00
metalgearsloth
2fe4a8b859 Add map name to lsmap (#4576) 2023-11-19 15:47:19 +01:00
metalgearsloth
8325966dbb Fix contact constraints allocs (#4581) 2023-11-19 15:47:07 +01:00
metalgearsloth
2459a9d688 Version: 180.0.0 2023-11-19 15:09:59 +11:00
Leon Friedrich
2cd2d1edd6 Add misc helpful methods (#4577) 2023-11-19 15:05:26 +11:00
metalgearsloth
b982350851 Use NetEntities for F3 panel (#4571) 2023-11-16 20:53:23 +11:00
DrSmugleaf
4a50bc2154 Add AddEntitiesIntersecting for phys shapes, change float range overload to use circles, remove obsolete methods (#4572) 2023-11-16 20:44:21 +11:00
metalgearsloth
4c85e205b9 Add chain support to TryGetNearest (#4567) 2023-11-16 20:40:23 +11:00
ElectroJr
0b447d9d82 Version: 179.0.0 2023-11-12 13:35:17 -05:00
Leon Friedrich
ceb205ad52 Allow per-eye lighting toggling. (#4569) 2023-11-13 05:30:00 +11:00
Leon Friedrich
a48ff3dbf1 Fix PlacementManager bug (#4568) 2023-11-13 05:29:18 +11:00
DrSmugleaf
2b85fa88c1 Print stack trace when adding a component while iterating net comps in ResetPredictedEntities (#4541) 2023-11-13 05:26:13 +11:00
Leon Friedrich
19564a421b Fix deserialization of empty grid chunks (#4565) 2023-11-13 05:22:20 +11:00
Leon Friedrich
b3f0e467ee Improve UnknownPrototypeException error message (#4566) 2023-11-13 05:22:05 +11:00
Leon Friedrich
216292c849 Make EyeComponent.Eye not nullable (#4564) 2023-11-13 04:20:09 +11:00
Jerry
68753d15e0 Fix stack overflow error on planet station (#4563) 2023-11-12 13:28:35 +11:00
ElectroJr
2a357051ae Version: 178.0.0 2023-11-10 20:58:35 -05:00
Leon Friedrich
58e0b62145 Merge ActorSystem and IPlayerManager (#4530) 2023-11-11 12:50:21 +11:00
Leon Friedrich
14cc273997 Add NetListAsArray<T>.Value to sandbox whitelist (#4537) 2023-11-11 11:57:58 +11:00
DrSmugleaf
93f4428635 Version: 177.0.0 2023-11-08 00:21:12 -08:00
DrSmugleaf
164bf68aca Move TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi to SharedUserInterfaceSystem (#4562) 2023-11-08 16:52:38 +11:00
Leon Friedrich
773b87672b Fix terminating entity reparenting bug (#4549) 2023-11-08 15:39:08 +11:00
metalgearsloth
eecf834039 Fix PlacementManager warnings (#4557) 2023-11-08 15:34:54 +11:00
Leon Friedrich
325fe46aa3 Add More Entity<T> query methods (#4550) 2023-11-07 20:24:42 -08:00
metalgearsloth
2f6c29ab43 Add GetMapCoordinates to TransformSystem (#4556) 2023-11-07 20:23:05 -08:00
metalgearsloth
aab1a2dba9 Fix transform test warnings (#4558) 2023-11-07 20:22:07 -08:00
DrSmugleaf
f36fbd9c83 Fix inverted GetAllMapGrids mapid check (#4561) 2023-11-07 14:26:01 -08:00
metalgearsloth
126c863f45 Hotfix containersystem.remove (#4560) 2023-11-07 16:18:08 +11:00
Leon Friedrich
618a8491bf Add BeforeApplyState event to replay playback (#4536) 2023-11-07 15:07:26 +11:00
Leon Friedrich
2743b64a2b Mark container methods as obsolete (#4551) 2023-11-07 15:05:32 +11:00
Leon Friedrich
28cc91934c Change PVS error log into warning (#4548) 2023-11-07 15:02:13 +11:00
metalgearsloth
eadfcd4c09 Specify RichTextLabel VAlignment as Center (#4520) 2023-11-07 10:27:49 +11:00
metalgearsloth
7871b0010e Version: 176.0.0 2023-11-07 09:51:32 +11:00
metalgearsloth
3da04ed17e Robust.Packaging updates (#4547) 2023-11-07 09:36:33 +11:00
metalgearsloth
170d192791 Revert audio rework (#4554) 2023-11-07 09:34:09 +11:00
Leon Friedrich
dcd9939554 Fix PVS initial list capacity bug (#4546) 2023-11-06 04:41:56 +11:00
Leon Friedrich
98ef58eca6 Add max game state buffer size cvar (#4543) 2023-11-05 02:58:48 +11:00
metalgearsloth
ab1e99a0df Add GetEntitiesInRange that takes in a set (#4544) 2023-11-04 15:02:20 +11:00
Leon Friedrich
499c236798 Fix replay lerp error spam (#4534) 2023-10-30 04:29:47 +11:00
metalgearsloth
8dc2345ceb Fix audio position on first tick (#4533) 2023-10-29 15:30:59 +11:00
274 changed files with 7876 additions and 14609 deletions

3
.gitmodules vendored
View File

@@ -13,3 +13,6 @@
[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

1
Arch/Arch Submodule

Submodule Arch/Arch added at c76d18feb7

94
Arch/Arch.csproj Normal file
View File

@@ -0,0 +1,94 @@
<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>

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,168 @@ END TEMPLATE-->
*None yet*
## 182.0.0
### Breaking changes
* Add EntityUid's generation / version to the hashcode.
## 181.0.2
### Bugfixes
* Fix exceptions from having too many lights on screen and causing the game to go black.
* Fix components having events raised in ClientGameStateManager before fully set and causing nullable reference exceptions.
* Replace tile intersection IEnumerables with TileEnumerator internally. Also made it public for external callers that wish to avoid IEnumerable.
## 181.0.1
### Bugfixes
* Fix the non-generic HasComp and add a test for good measure.
## 181.0.0
### Breaking changes
- Arch is merged refactoring how components are stored on engine. There's minimal changes on the API end to facilitate component nullability with much internal refactoring.
## 180.2.1
## 180.2.0
### New features
* Add EnsureEntity variants that take in collections.
* Add more MapSystem helper methods.
### Internal
* Cache some more PVS data to avoid re-allocating every tick.
## 180.1.0
### New features
* Add the map name to lsmap.
* Add net.pool_size to CVars to control the message data pool size in Lidgren and to also toggle pooling.
### Bugfixes
* Fix physics contraints causing enormous heap allocations.
* Fix potential error when writing a runtime log.
* Fix shape lookups for non-hard fixtures in EntityLookupSystem from 180.0.0
## 180.0.0
### Breaking changes
* Removed some obsolete methods from EntityLookupSystem.
### New features
* PhysicsSystem.TryGetNearest now supports chain shapes.
* Add IPhysShape methods to EntityLookupSystem rather than relying on AABB checks.
* Add some more helper methods to SharedTransformSystem.
* Add GetOrNew dictionary extension that also returns a bool on whether the key existed.
* Add a GetAnchoredEntities overload that takes in a list.
### Other
* Use NetEntities for the F3 debug panel to align with command usage.
## 179.0.0
### Breaking changes
* EyeComponent.Eye is no longer nullable
### New features
* Light rendering can now be enabled or disable per eye.
### Bugfixes
* Deserializing old maps with empty grid chunks should now just ignore those chunks.
### Other
* UnknownPrototypeException now also tells you the prototype kind instead of just the unkown ID.
* Adding or removing networked components while resetting predicted entities now results in a more informative exception.
## 178.0.0
### Breaking changes
* Most methods in ActorSystem have been moved to ISharedPlayerManager.
* Several actor/player related components and events have been moved to shared.
### New features
* Added `NetListAsArray<T>.Value` to the sandbox whitelist
## 177.0.0
### Breaking changes
* Removed toInsertXform and added containerXform in SharedContainerSystem.CanInsert.
* Removed EntityQuery parameters from SharedContainerSystem.IsEntityOrParentInContainer.
* Changed the signature of ContainsEntity in SharedTransformSystem to use Entity<T>.
* Removed one obsoleted SharedTransformSystem.AnchorEntity method.
* Changed signature of SharedTransformSystem.SetCoordinates to use Entity<T>.
### New features
* Added more Entity<T> query methods.
* Added BeforeApplyState event to replay playback.
### Bugfixes
* Fixed inverted GetAllMapGrids map id check.
* Fixed transform test warnings.
* Fixed PlacementManager warnings.
* Fixed reparenting bug for entities that are being deleted.
### Other
* Changed VerticalAlignment of RichTextLabel to Center to be consistent with Label.
* Changed PVS error log to be a warning instead.
* Marked insert and remove container methods as obsolete, added container system methods to replace them.
* Marked TransformComponent.MapPosition as obsolete, added GetMapCoordinates system method to replace it.
### Internal
* Moved TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi methods from UserInterfaceSystem to SharedUserInterfaceSystem.
## 176.0.0
### Breaking changes
* Reverted audio rework temporarily until packaging is fixed.
* Changes to Robust.Packaging to facilitate Content.Packaging ports from the python packaging scripts.
### New features
* Add a cvar for max game state buffer size.
* Add an overload for GetEntitiesInRange that takes in a set.
### Bugfixes
* Fix PVS initial list capacity always being 0.
* Fix replay lerp error spam.
## 175.0.0
### Breaking changes

View File

@@ -2203,3 +2203,207 @@
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.

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
- type: entity
id: Audio
name: Audio
description: Audio entity used by engine
save: false

View File

@@ -0,0 +1,177 @@
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);
}
}
}

View File

@@ -41,7 +41,7 @@ public partial class AddRemoveComponentBenchmark
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var uid = new EntityUid(i, -1);
_entityManager.AddComponent<A>(uid);
_entityManager.RemoveComponent<A>(uid);
}

View File

@@ -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));
public static readonly CompIdx Idx = new(Interlocked.Increment(ref _compIndexMaster), typeof(T));
}
private static CompIdx GetCompIdIndex(Type type)

View File

@@ -46,7 +46,7 @@ public partial class GetComponentBenchmark
{
for (var i = 2; i <= N+1; i++)
{
Comps[i] = _entityManager.GetComponent<A>(new EntityUid(i));
Comps[i] = _entityManager.GetComponent<A>(new EntityUid(i, -1));
}
// Return something so the JIT doesn't optimize out all the GetComponent calls.

View File

@@ -8,6 +8,7 @@
<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" />

View File

@@ -1,10 +1,8 @@
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
@@ -39,12 +37,7 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
var audioParams = keyFrame.AudioParamsFunc.Invoke();
var audio = new SoundPathSpecifier(keyFrame.Resource)
{
Params = audioParams
};
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
SoundSystem.Play(keyFrame.Resource, Filter.Local(), entity, keyFrame.AudioParamsFunc.Invoke());
}
return (keyFrameIndex, playingTime);

View File

@@ -1,58 +0,0 @@
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);
}
}

View File

@@ -1,338 +0,0 @@
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.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 override 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);
}
/// <inheritdoc/>
public override 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);
}
/// <inheritdoc/>
public override 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();
}
}

View File

@@ -1,161 +0,0 @@
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 : SharedAudioManager, 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;
}
}
}

View File

@@ -1,89 +0,0 @@
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, IClientResourceCache 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(uid, listenerPos, distance, distance.Length());
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);
}
}
}

View File

@@ -1,21 +1,18 @@
using System;
using Robust.Shared.Graphics;
using Robust.Client.Graphics;
namespace Robust.Shared.Audio;
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 IClydeHandle? ClydeHandle { get; }
internal ClydeHandle? ClydeHandle { get; }
public string? Name { get; }
public string? Title { get; }
public string? Artist { get; }
public int ChannelCount { get; }
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
internal AudioStream(ClydeHandle handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
{
ClydeHandle = handle;
Length = length;

View File

@@ -1,76 +0,0 @@
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);
}
}
}

View File

@@ -1,596 +0,0 @@
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.ResourceManagement.ResourceTypes;
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 IClientResourceCache _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 EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private float _maxRayLength;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
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}");
component.Source = new DummyAudioSource();
return;
}
var source = _audio.CreateAudioSource(audioResource);
if (source == null)
{
Log.Error($"Error creating audio source for {audioResource}");
DebugTools.Assert(false);
source = new DummyAudioSource();
}
// Need to set all initial data for first frame.
component.Source = source;
ApplyAudioParams(component.Params, component);
component.Global = component.Global;
// Don't play until first frame so occlusion etc. are correct.
component.Gain = 0f;
if (!MetaData(uid).EntityPaused)
{
component.StartPlaying();
}
// 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 = eye.Position;
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
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
{
Parallel.ForEach(_streams, opts, comp => ProcessStream(comp.Entity, comp.Component, comp.Xform, ourPos));
}
catch (Exception e)
{
Log.Error($"Caught exception while processing entity streams.");
_runtimeLog.LogException(e, $"{nameof(AudioSystem)}.{nameof(FrameUpdate)}");
}
}
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 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 (_gridQuery.HasComponent(gridUid))
{
// 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(entity, listener, delta, distance);
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, _xformQuery, _physicsQuery);
component.Velocity = velocity;
}
}
internal float GetOcclusion(EntityUid entity, MapCoordinates listener, Vector2 delta, float distance)
{
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, entity);
}
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);
}
}

View File

@@ -1,455 +0,0 @@
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();
}
}
}

View File

@@ -1,32 +0,0 @@
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);
}
}
}

View File

@@ -3,7 +3,6 @@ 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;
@@ -21,7 +20,7 @@ public interface IMidiRenderer : IDisposable
/// <summary>
/// The buffered audio source of this renderer.
/// </summary>
internal IBufferedAudioSource Source { get; }
internal IClydeBufferedAudioSource Source { get; }
/// <summary>
/// Whether this renderer has been disposed or not.

View File

@@ -10,7 +10,6 @@ using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
@@ -42,10 +41,10 @@ internal sealed partial class MidiManager : IMidiManager
[ViewVariables] private TimeSpan _nextPositionUpdate = TimeSpan.Zero;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IResourceCacheInternal _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly ILogManager _logger = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
@@ -274,7 +273,7 @@ internal sealed partial class MidiManager : IMidiManager
{
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
@@ -352,7 +351,7 @@ internal sealed partial class MidiManager : IMidiManager
renderer.LoadSoundfont(file.ToString());
}
renderer.Source.Volume = _volume;
renderer.Source.SetVolume(Volume);
lock (_renderers)
{
@@ -375,7 +374,6 @@ 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)
{
@@ -417,13 +415,11 @@ internal sealed partial class MidiManager : IMidiManager
return;
if (_volumeDirty)
{
renderer.Source.Volume = Volume;
}
renderer.Source.SetVolume(Volume);
if (!renderer.Mono)
{
renderer.Source.Global = true;
renderer.Source.SetGlobal();
return;
}
@@ -438,19 +434,14 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
var position = renderer.TrackingCoordinates.Value;
if (position.MapId == MapId.Nullspace)
if (!renderer.Source.SetPosition(renderer.TrackingCoordinates.Value.Position))
{
return;
}
renderer.Source.Position = position.Position;
var vel = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity!.Value,
xformQuery: transQuery, physicsQuery: physicsQuery);
renderer.Source.Velocity = vel;
renderer.Source.SetVelocity(vel);
}
if (renderer.TrackingCoordinates != null && renderer.TrackingCoordinates.Value.MapId == _eyeManager.CurrentMap)
@@ -474,11 +465,11 @@ internal sealed partial class MidiManager : IMidiManager
renderer.TrackingEntity);
}
renderer.Source.Occlusion = occlusion;
renderer.Source.SetOcclusion(occlusion);
}
else
{
renderer.Source.Occlusion = float.MaxValue;
renderer.Source.SetOcclusion(float.MaxValue);
}
}

View File

@@ -4,9 +4,7 @@ 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;
@@ -54,8 +52,8 @@ internal sealed class MidiRenderer : IMidiRenderer
private IMidiRenderer? _master;
public MidiRendererState RendererState => _rendererState;
public IBufferedAudioSource Source { get; set; }
IBufferedAudioSource IMidiRenderer.Source => Source;
public IClydeBufferedAudioSource Source { get; set; }
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
[ViewVariables]
public bool Disposed { get; private set; } = false;
@@ -249,7 +247,7 @@ internal sealed class MidiRenderer : IMidiRenderer
public event Action? OnMidiPlayerFinished;
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono,
IMidiManager midiManager, IAudioInternal clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
IMidiManager midiManager, IClydeAudio clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
{
_midiManager = midiManager;
_taskManager = taskManager;
@@ -490,7 +488,7 @@ internal sealed class MidiRenderer : IMidiRenderer
}
}
Source.StartPlaying();
if (!Source.IsPlaying) Source.StartPlaying();
}
public void ApplyState(MidiRendererState state, bool filterChannels = false)

View File

@@ -1,34 +0,0 @@
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 IClientResourceCache _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>()));
}
}

View File

@@ -1,90 +0,0 @@
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;
}
}

View File

@@ -1,390 +0,0 @@
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.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 = 10f * MathF.Log10(gain);
return volume;
}
set => Gain = MathF.Pow(10, value / 10);
}
/// <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));
}
}
}

View File

@@ -1,224 +0,0 @@
using System;
using System.Collections.Generic;
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);
}
}

View File

@@ -1,5 +1,4 @@
using System;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Configuration;
using Robust.Client.Console;
@@ -7,6 +6,7 @@ using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Audio;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Input;
using Robust.Client.Map;
@@ -29,7 +29,6 @@ using Robust.Client.UserInterface.Themes;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
@@ -73,11 +72,10 @@ namespace Robust.Client
deps.Register<GameController, GameController>();
deps.Register<IGameController, GameController>();
deps.Register<IGameControllerInternal, GameController>();
deps.Register<IResourceManager, ResourceManager>();
deps.Register<IResourceManagerInternal, ResourceManager>();
deps.Register<IClientResourceCache, ResourceCache>();
deps.Register<IClientResourceCacheInternal, ResourceCache>();
deps.Register<IResourceManager, ResourceCache>();
deps.Register<IResourceManagerInternal, ResourceCache>();
deps.Register<IResourceCache, ResourceCache>();
deps.Register<IResourceCacheInternal, ResourceCache>();
deps.Register<IClientNetManager, NetManager>();
deps.Register<EntityManager, ClientEntityManager>();
deps.Register<ClientEntityManager>();
@@ -109,8 +107,8 @@ namespace Robust.Client
deps.Register<IClyde, ClydeHeadless>();
deps.Register<IClipboardManager, ClydeHeadless>();
deps.Register<IClydeInternal, ClydeHeadless>();
deps.Register<IAudioInternal, HeadlessAudioManager>();
deps.Register<SharedAudioManager, HeadlessAudioManager>();
deps.Register<IClydeAudio, ClydeAudioHeadless>();
deps.Register<IClydeAudioInternal, ClydeAudioHeadless>();
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
deps.Register<IUriOpener, UriOpenerDummy>();
@@ -119,8 +117,8 @@ namespace Robust.Client
deps.Register<IClyde, Clyde>();
deps.Register<IClipboardManager, Clyde>();
deps.Register<IClydeInternal, Clyde>();
deps.Register<IAudioInternal, AudioManager>();
deps.Register<SharedAudioManager, AudioManager>();
deps.Register<IClydeAudio, FallbackProxyClydeAudio>();
deps.Register<IClydeAudioInternal, FallbackProxyClydeAudio>();
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();
deps.Register<IUriOpener, UriOpener>();

View File

@@ -15,7 +15,6 @@ 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;
@@ -257,7 +256,7 @@ namespace Robust.Client.Console.Commands
return;
}
var uid = EntityUid.Parse(args[0]);
var uid = EntityUid.Parse(args[0], "-1");
var entmgr = _entityManager;
if (!entmgr.EntityExists(uid))
{
@@ -355,7 +354,7 @@ namespace Robust.Client.Console.Commands
internal sealed class LoadResource : LocalizedCommands
{
[Dependency] private readonly IClientResourceCache _res = default!;
[Dependency] private readonly IResourceCache _res = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
public override string Command => "ldrsc";
@@ -392,7 +391,7 @@ namespace Robust.Client.Console.Commands
internal sealed class ReloadResource : LocalizedCommands
{
[Dependency] private readonly IClientResourceCache _res = default!;
[Dependency] private readonly IResourceCache _res = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
public override string Command => "rldrsc";
@@ -459,13 +458,13 @@ namespace Robust.Client.Console.Commands
internal sealed class GuiDumpCommand : LocalizedCommands
{
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
[Dependency] private readonly IResourceCache _res = default!;
public override string Command => "guidump";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
using var writer = _resManager.UserData.OpenWriteText(new ResPath("/guidump.txt"));
using var writer = _res.UserData.OpenWriteText(new ResPath("/guidump.txt"));
foreach (var root in _ui.AllRoots)
{
@@ -645,8 +644,7 @@ namespace Robust.Client.Console.Commands
internal sealed class ReloadShadersCommand : LocalizedCommands
{
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly IResourceCacheInternal _res = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
public override string Command => "rldshader";
@@ -657,7 +655,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var resC = _resManager;
var resC = _res;
if (args.Length == 1)
{
if (args[0] == "+watch")
@@ -681,9 +679,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 _cache.GetAllResources<ShaderSourceResource>())
foreach (var (path, src) in resC.GetAllResources<ShaderSourceResource>())
{
if (!_resManager.TryGetDiskFilePath(path, out var fullPath))
if (!resC.TryGetDiskFilePath(path, out var fullPath))
{
throw new NotImplementedException();
}
@@ -732,7 +730,7 @@ namespace Robust.Client.Console.Commands
{
try
{
_cache.ReloadResource<ShaderSourceResource>(resPath);
resC.ReloadResource<ShaderSourceResource>(resPath);
shell.WriteLine($"Reloaded shader: {resPath}");
}
catch (Exception)
@@ -793,11 +791,11 @@ namespace Robust.Client.Console.Commands
shell.WriteLine("Reloading content shader resources...");
foreach (var (path, _) in _cache.GetAllResources<ShaderSourceResource>())
foreach (var (path, _) in resC.GetAllResources<ShaderSourceResource>())
{
try
{
_cache.ReloadResource<ShaderSourceResource>(path);
resC.ReloadResource<ShaderSourceResource>(path);
}
catch (Exception)
{

View File

@@ -43,6 +43,7 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
GC.Collect();
Span<EntityUid> ents = stackalloc EntityUid[amount];
var stopwatch = new Stopwatch();
stopwatch.Start();
@@ -50,12 +51,17 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
for (var i = 0; i < amount; i++)
{
_entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
ents[i] = _entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
}
MeasureProfiler.SaveData();
shell.WriteLine($"Client: Profiled spawning {amount} entities in {stopwatch.Elapsed.TotalMilliseconds:N3} ms");
foreach (var ent in ents)
{
_entities.DeleteEntity(ent);
}
}
}
#endif

View File

@@ -96,7 +96,7 @@ namespace Robust.Client.Debugging
IoCManager.Resolve<IInputManager>(),
IoCManager.Resolve<IMapManager>(),
IoCManager.Resolve<IPlayerManager>(),
IoCManager.Resolve<IClientResourceCache>(),
IoCManager.Resolve<IResourceCache>(),
this,
Get<EntityLookupSystem>(),
Get<SharedPhysicsSystem>()));
@@ -208,7 +208,7 @@ namespace Robust.Client.Debugging
private HashSet<Joint> _drawnJoints = new();
private List<Entity<MapGridComponent>> _grids = new();
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IClientResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
{
_entityManager = entityManager;
_eyeManager = eyeManager;

View File

@@ -4,7 +4,6 @@ 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;
@@ -25,7 +24,6 @@ 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;
@@ -50,8 +48,7 @@ namespace Robust.Client
internal sealed partial class GameController : IGameControllerInternal
{
[Dependency] private readonly INetConfigurationManagerInternal _configurationManager = default!;
[Dependency] private readonly IClientResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
@@ -71,7 +68,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 IAudioInternal _audio = default!;
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
[Dependency] private readonly IScriptClient _scriptClient = default!;
@@ -114,7 +111,7 @@ namespace Robust.Client
DebugTools.AssertNotNull(_resourceManifest);
_clyde.InitializePostWindowing();
_audio.InitializePostWindowing();
_clydeAudio.InitializePostWindowing();
_clyde.SetWindowTitle(
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
@@ -151,7 +148,7 @@ namespace Robust.Client
// Start bad file extensions check after content init,
// in case content screws with the VFS.
var checkBadExtensions = ProgramShared.CheckBadFileExtensions(
_resManager,
_resourceCache,
_configurationManager,
_logManager.GetSawmill("res"));
@@ -363,13 +360,13 @@ namespace Robust.Client
_parallelMgr.Initialize();
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
: Options.MountOptions;
ProgramShared.DoMounts(_resManager, mountOptions, Options.ContentBuildDirectory,
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
Options.AssemblyDirectory,
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
@@ -379,16 +376,16 @@ namespace Robust.Client
{
foreach (var (api, prefix) in mounts)
{
_resourceCache.MountLoaderApi(_resManager, api, "", new(prefix));
_resourceCache.MountLoaderApi(api, "", new(prefix));
}
}
_stringSerializer.EnableCaching = false;
_resourceCache.MountLoaderApi(_resManager, _loaderArgs.FileApi, "Resources/");
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resManager);
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resourceCache);
{
// Handle GameControllerOptions implicit CVar overrides.
@@ -570,6 +567,11 @@ namespace Robust.Client
}
}
using (_prof.Group("ClydeAudio"))
{
_clydeAudio.FrameProcess(frameEventArgs);
}
using (_prof.Group("Clyde"))
{
_clyde.FrameProcess(frameEventArgs);
@@ -708,7 +710,7 @@ namespace Robust.Client
internal void CleanupWindowThread()
{
_clyde.Shutdown();
_audio.Shutdown();
_clydeAudio.Shutdown();
}
public event Action<FrameEventArgs>? TickUpdateOverride;

View File

@@ -44,7 +44,7 @@ namespace Robust.Client.GameObjects
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, out MetaDataComponent metadata)
{
return base.CreateEntity(prototypeName, out metadata);
return base.CreateEntity(prototypeName, out metadata, out _);
}
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta)

View File

@@ -36,7 +36,7 @@ namespace Robust.Client.GameObjects
[RegisterComponent]
public sealed partial class SpriteComponent : Component, IComponentDebug, ISerializationHooks, IComponentTreeEntry<SpriteComponent>, IAnimationProperties
{
[Dependency] private readonly IClientResourceCache resourceCache = default!;
[Dependency] private readonly IResourceCache resourceCache = default!;
[Dependency] private readonly IPrototypeManager prototypes = default!;
[Dependency] private readonly IEntityManager entities = default!;
[Dependency] private readonly IReflectionManager reflection = default!;
@@ -1379,7 +1379,7 @@ namespace Robust.Client.GameObjects
}
[Obsolete("Use SpriteSystem instead.")]
internal static RSI.State GetFallbackState(IClientResourceCache cache)
internal static RSI.State GetFallbackState(IResourceCache cache)
{
var rsi = cache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
return rsi["error"];
@@ -2101,12 +2101,12 @@ namespace Robust.Client.GameObjects
}
}
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IClientResourceCache resourceCache)
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache)
{
return GetPrototypeTextures(prototype, resourceCache, out var _);
}
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IClientResourceCache resourceCache, out bool noRot)
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
{
var results = new List<IDirectionalTextureProvider>();
noRot = false;
@@ -2161,7 +2161,7 @@ namespace Robust.Client.GameObjects
}
[Obsolete("Use SpriteSystem")]
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IClientResourceCache resourceCache)
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))

View File

@@ -0,0 +1,626 @@
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);
}
}

View File

@@ -1,9 +1,8 @@
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Client.GameObjects;
@@ -26,17 +25,13 @@ public sealed class EyeSystem : SharedEyeSystem
private void OnEyeAutoState(EntityUid uid, EyeComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateEye(component);
UpdateEye((uid, component));
}
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
{
// TODO: This probably shouldn't be nullable bruv.
if (component._eye != null)
{
_eyeManager.CurrentEye = component._eye;
}
UpdateEye((uid, component));
_eyeManager.CurrentEye = component.Eye;
var ev = new EyeAttachedEvent(uid, component);
RaiseLocalEvent(uid, ref ev, true);
}
@@ -48,13 +43,7 @@ public sealed class EyeSystem : SharedEyeSystem
private void OnInit(EntityUid uid, EyeComponent component, ComponentInit args)
{
component._eye = new Eye
{
Position = Transform(uid).MapPosition,
Zoom = component.Zoom,
DrawFov = component.DrawFov,
Rotation = component.Rotation,
};
UpdateEye((uid, component));
}
/// <inheritdoc />
@@ -64,7 +53,7 @@ public sealed class EyeSystem : SharedEyeSystem
while (query.MoveNext(out var uid, out var eyeComponent))
{
if (eyeComponent._eye == null)
if (eyeComponent.Eye == null)
continue;
if (!TryComp<TransformComponent>(eyeComponent.Target, out var xform))
@@ -73,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
eyeComponent.Target = null;
}
eyeComponent._eye.Position = xform.MapPosition;
eyeComponent.Eye.Position = xform.MapPosition;
}
}
}

View File

@@ -1,12 +1,9 @@
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
{
@@ -14,7 +11,7 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IClientResourceCache _resource = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
public override void Initialize()
@@ -28,10 +25,5 @@ namespace Robust.Client.GameObjects
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
}
}

View File

@@ -10,7 +10,7 @@ namespace Robust.Client.GameObjects
{
public sealed class PointLightSystem : SharedPointLightSystem
{
[Dependency] private readonly IClientResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly LightTreeSystem _lightTree = default!;
public override void Initialize()

View File

@@ -32,7 +32,7 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IClientResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();

View File

@@ -54,9 +54,10 @@ namespace Robust.Client.GameObjects
// should show the entity lerping.
// - If the client predicts an entity will move while already lerping due to a state-application, it should
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
// the entity in the final position.
// the entity in the state's final position.
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
// was predicted. I.e., we assume the entity was already rendered in it's final of that lerp.
// was predicted. I.e., we assume the entity was already rendered in the final position that was
// previously predicted.
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
// I.e. moving from a->b then b->c, the client should lerp from a->c.

View File

@@ -39,7 +39,7 @@ public sealed class ClientDirtySystem : EntitySystem
private void OnTerminate(ref EntityTerminatingEvent ev)
{
if (!_timing.InPrediction || IsClientSide(ev.Entity))
if (!_timing.InPrediction || IsClientSide(ev.Entity, ev.Metadata))
return;
// Client-side entity deletion is not supported and will cause errors.

View File

@@ -5,6 +5,8 @@ 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;
@@ -29,7 +31,9 @@ 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
{
@@ -52,11 +56,12 @@ 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);
@@ -124,6 +129,8 @@ namespace Robust.Client.GameStates
public bool DropStates;
#endif
private bool _resettingPredictedEntities;
/// <inheritdoc />
public void Initialize()
{
@@ -146,6 +153,7 @@ namespace Robust.Client.GameStates
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
_config.OnValueChanged(CVars.NetPVSEntityExitBudget, i => _pvsDetachBudget = i, true);
_config.OnValueChanged(CVars.NetMaxBufferSize, i => _processor.MaxBufferSize = i, true);
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
_processor.BufferSize = _config.GetCVar(CVars.NetBufferSize);
@@ -160,6 +168,8 @@ namespace Robust.Client.GameStates
_conHost.RegisterCommand("localdelete", Loc.GetString("cmd-local-delete-desc"), Loc.GetString("cmd-local-delete-help"), LocalDeleteEntCommand);
_conHost.RegisterCommand("fullstatereset", Loc.GetString("cmd-full-state-reset-desc"), Loc.GetString("cmd-full-state-reset-help"), (_,_,_) => RequestFullState());
_entities.ComponentAdded += OnComponentAdded;
var metaId = _compFactory.GetRegistration(typeof(MetaDataComponent)).NetID;
if (!metaId.HasValue)
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
@@ -167,6 +177,23 @@ namespace Robust.Client.GameStates
_metaCompNetId = metaId.Value;
}
private void OnComponentAdded(AddedComponentEventArgs args)
{
if (_resettingPredictedEntities)
{
var comp = args.ComponentType;
if (comp.NetID == null)
return;
_sawmill.Error($"""
Added component {comp.Name} with net id {comp.NetID} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
}
}
/// <inheritdoc />
public void Reset()
{
@@ -518,7 +545,9 @@ namespace Robust.Client.GameStates
var countReset = 0;
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
using var toRemove = new PooledList<IComponent>();
using var toAdd = new PooledList<ushort>();
using var toAddStates = new PooledList<ComponentState>();
foreach (var entity in system.DirtyEntities)
{
@@ -536,48 +565,63 @@ namespace Robust.Client.GameStates
countReset += 1;
foreach (var (netId, comp) in meta.NetComponents)
try
{
if (!comp.NetSyncEnabled)
continue;
_resettingPredictedEntities = true;
// Was this component added during prediction?
if (comp.CreationTick > _timing.LastRealTick)
foreach (var (netId, comp) in meta.NetComponents)
{
if (last.ContainsKey(netId))
if (!comp.NetSyncEnabled)
continue;
// Was this component added during prediction?
if (comp.CreationTick > _timing.LastRealTick)
{
// Component was probably removed and then re-addedd during a single prediction run
// Just reset state as normal.
comp.ClearCreationTick();
if (last.ContainsKey(netId))
{
// Component was probably removed and then re-addedd during a single prediction run
// Just reset state as normal.
comp.ClearCreationTick();
}
else
{
toRemove.Add(comp);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A new component was added: {comp.GetType()}");
continue;
}
}
else
if (comp.LastModifiedTick <= _timing.LastRealTick ||
!last.TryGetValue(netId, out var compState))
{
toRemove.Add(comp);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A new component was added: {comp.GetType()}");
continue;
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
comp.LastModifiedTick = _timing.LastRealTick;
}
if (comp.LastModifiedTick <= _timing.LastRealTick || !last.TryGetValue(netId, out var compState))
{
continue;
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
comp.LastModifiedTick = _timing.LastRealTick;
}
// Remove predicted component additions
foreach (var comp in toRemove)
finally
{
_entities.RemoveComponent(entity, comp);
_resettingPredictedEntities = false;
}
if (toRemove.Count > 0)
{
// Remove predicted component additions
// TODO: 1 archetype change.
foreach (var comp in toRemove)
{
_entities.RemoveComponent(entity, comp, meta);
}
toRemove.Clear();
}
toRemove.Clear();
// Re-add predicted removals
if (system.RemovedComponents.TryGetValue(entity, out var netIds))
@@ -590,15 +634,29 @@ namespace Robust.Client.GameStates
if (!last.TryGetValue(netId, out var state))
continue;
var comp = _entityManager.AddComponent(entity, netId, meta);
toAdd.Add(netId);
toAddStates.Add(state);
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was removed: {comp.GetType()}");
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);
var stateEv = new ComponentHandleState(state, null);
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
comp.ClearCreationTick(); // don't undo the re-adding.
comp.LastModifiedTick = _timing.LastRealTick;
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();
}
}
@@ -681,10 +739,9 @@ namespace Robust.Client.GameStates
_config.TickProcessMessages();
}
(IEnumerable<NetEntity> Created, List<NetEntity> Detached) output;
using (_prof.Group("Entity"))
{
output = ApplyEntityStates(curState, nextState);
ApplyEntityStates(curState, nextState);
}
using (_prof.Group("Player"))
@@ -694,13 +751,13 @@ namespace Robust.Client.GameStates
using (_prof.Group("Callback"))
{
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, output.Detached));
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, _detached));
}
return output.Created;
return _created;
}
private (IEnumerable<NetEntity> Created, List<NetEntity> Detached) ApplyEntityStates(GameState curState, GameState? nextState)
private void ApplyEntityStates(GameState curState, GameState? nextState)
{
var metas = _entities.GetEntityQuery<MetaDataComponent>();
var xforms = _entities.GetEntityQuery<TransformComponent>();
@@ -709,6 +766,8 @@ namespace Robust.Client.GameStates
var enteringPvs = 0;
_toApply.Clear();
_toCreate.Clear();
_detached.Clear();
_created.Clear();
_pendingReapplyNetStates.Clear();
var curSpan = curState.EntityStates.Span;
@@ -733,6 +792,7 @@ 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.
@@ -786,7 +846,7 @@ namespace Robust.Client.GameStates
// Detach entities to null space
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
var detached = ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
ProcessPvsDeparture(_detached, curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
// Check next state (AFTER having created new entities introduced in curstate)
if (nextState != null)
@@ -888,8 +948,6 @@ namespace Robust.Client.GameStates
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
return (_toCreate.Keys, detached);
}
/// <inheritdoc />
@@ -920,7 +978,7 @@ namespace Robust.Client.GameStates
var xforms = _entities.GetEntityQuery<TransformComponent>();
var xformSys = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
_toDelete.Clear();
using var toDelete = new PooledList<EntityUid>();
// Client side entities won't need the transform, but that should always be a tiny minority of entities
var metaQuery = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
@@ -931,7 +989,7 @@ namespace Robust.Client.GameStates
if (metadata.NetEntity.IsClientSide())
{
if (deleteClientEntities)
_toDelete.Add(ent);
toDelete.Add(ent);
continue;
}
@@ -958,14 +1016,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);
}
@@ -1025,7 +1083,8 @@ namespace Robust.Client.GameStates
Detach(GameTick.MaxValue, null, entities, metas, xforms, xformSys, containerSys, lookupSys);
}
private List<NetEntity> ProcessPvsDeparture(
private void ProcessPvsDeparture(
IList<NetEntity> detached,
GameTick toTick,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms,
@@ -1033,18 +1092,17 @@ namespace Robust.Client.GameStates
ContainerSystem containerSys,
EntityLookupSystem lookupSys)
{
var toDetach = _processor.GetEntitiesToDetach(toTick, _pvsDetachBudget);
var detached = new List<NetEntity>();
using var toDetach = new PooledList<(GameTick Tick, List<NetEntity> Entities)>();
_processor.GetEntitiesToDetach(toDetach, toTick, _pvsDetachBudget);
if (toDetach.Count == 0)
return detached;
return;
// 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)
{
@@ -1052,7 +1110,6 @@ namespace Robust.Client.GameStates
}
_prof.WriteValue("Count", ProfData.Int32(detached.Count));
return detached;
}
private void Detach(GameTick maxTick,
@@ -1063,7 +1120,7 @@ namespace Robust.Client.GameStates
SharedTransformSystem xformSys,
ContainerSystem containerSys,
EntityLookupSystem lookupSys,
List<NetEntity>? detached = null)
IList<NetEntity>? detached = null)
{
foreach (var netEntity in entities)
{
@@ -1117,7 +1174,7 @@ namespace Robust.Client.GameStates
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
#if EXCEPTION_TOLERANCE
var brokenEnts = new List<EntityUid>();
using var brokenEnts = new PooledList<EntityUid>();
#endif
using (_prof.Group("Initialize Entity"))
{
@@ -1181,18 +1238,28 @@ namespace Robust.Client.GameStates
// First remove any deleted components
if (curState?.NetComponents != null)
{
_toRemove.Clear();
using var toRemove = new PooledList<IComponent>();
using var compTypes = new PooledList<ComponentType>();
foreach (var (id, comp) in meta.NetComponents)
{
if (comp.NetSyncEnabled && !curState.NetComponents.Contains(id))
_toRemove.Add(comp);
{
toRemove.Add(comp);
compTypes.Add(comp.GetType());
}
}
foreach (var comp in _toRemove)
if (toRemove.Count > 0)
{
_entities.RemoveComponent(uid, comp, meta);
foreach (var comp in toRemove)
{
_entityManager.RemoveComponentInternal(uid, comp, terminating: false, archetypeChange: false, meta);
}
}
if (compTypes.Count > 0)
_entityManager.RemoveComponentRange(uid, compTypes);
}
if (enteringPvs)
@@ -1207,7 +1274,7 @@ namespace Robust.Client.GameStates
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, metadata: meta);
_entityManager.AddComponent(uid, comp, metadata: meta);
}
_compStateWork[id] = (comp, state, null);
@@ -1215,18 +1282,49 @@ 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))
{
comp = _compFactory.GetComponent(compChange.NetID);
_entityManager.AddComponent(uid, comp, true, metadata:meta);
var registration = _compFactory.GetRegistration(compChange.NetID);
addedRegistrations.Add(registration);
comp = _compFactory.GetComponent(registration);
comp.Owner = uid;
addedComps.Add(comp);
addedCompTypes.Add(comp.GetType());
}
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)
@@ -1316,7 +1414,7 @@ namespace Robust.Client.GameStates
return false;
}
if (!EntityUid.TryParse(args[0], out uid))
if (!EntityUid.TryParse(args[0], "-1", out uid))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-uid", ("arg", args[0])));
meta = null;
@@ -1448,23 +1546,22 @@ namespace Robust.Client.GameStates
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, meta);
_entityManager.AddComponent(uid, comp, meta);
}
var handleState = new ComponentHandleState(state, null);
_entityManager.EventBus.RaiseComponentEvent(comp, ref handleState);
}
// ensure we don't have any extra components
_toRemove.Clear();
using var toRemove = new PooledList<IComponent>();
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);
}
@@ -1477,10 +1574,10 @@ namespace Robust.Client.GameStates
public sealed class GameStateAppliedArgs : EventArgs
{
public GameState AppliedState { get; }
public readonly List<NetEntity> Detached;
public readonly GameState AppliedState;
public readonly IReadOnlyList<NetEntity> Detached;
public GameStateAppliedArgs(GameState appliedState, List<NetEntity> detached)
public GameStateAppliedArgs(GameState appliedState, IReadOnlyList<NetEntity> detached)
{
AppliedState = appliedState;
Detached = detached;

View File

@@ -14,8 +14,6 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor
{
public const int MaxBufferSize = 512;
private readonly IClientGameTiming _timing;
private readonly IClientGameStateManager _state;
private readonly ISawmill _logger;
@@ -28,6 +26,8 @@ namespace Robust.Client.GameStates
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
private int _bufferSize;
private int _maxBufferSize = 512;
public const int MinimumMaxBufferSize = 256;
/// <summary>
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
@@ -48,7 +48,14 @@ namespace Robust.Client.GameStates
public int BufferSize
{
get => _bufferSize;
set => _bufferSize = value < 0 ? 0 : value;
set => _bufferSize = Math.Max(value, 0);
}
public int MaxBufferSize
{
get => _maxBufferSize;
// We place a lower bound on the maximum size to avoid spamming servers with full game state requests.
set => _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
}
/// <inheritdoc />
@@ -100,21 +107,21 @@ namespace Robust.Client.GameStates
return true;
}
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value.Tick)
if (LastFullState == null && state.FromSequence == GameTick.Zero)
{
LastFullState = state;
if (Logging)
if (state.ToSequence >= LastFullStateRequested!.Value.Tick)
{
LastFullState = state;
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return true;
}
return true;
_logger.Info($"Received a late full game state. Received: {state.ToSequence}. Requested: {LastFullStateRequested.Value.Tick}");
}
if (LastFullState != null && state.ToSequence <= LastFullState.ToSequence)
{
if (Logging)
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
return false;
}
@@ -297,9 +304,8 @@ Had full state: {LastFullState != null}"
public void ClearDetachQueue() => _pvsDetachMessages.Clear();
public List<(GameTick Tick, List<NetEntity> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
public void GetEntitiesToDetach(IList<(GameTick Tick, List<NetEntity> Entities)> result, GameTick toTick, int budget)
{
var result = new List<(GameTick Tick, List<NetEntity> Entities)>();
foreach (var (tick, entities) in _pvsDetachMessages)
{
if (tick > toTick)
@@ -318,7 +324,6 @@ Had full state: {LastFullState != null}"
entities.RemoveRange(index, budget);
break;
}
return result;
}
private bool TryGetDeltaState(out GameState? curState, out GameState? nextState)

View File

@@ -40,7 +40,7 @@ namespace Robust.Client.GameStates
public NetEntityOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IClientResourceCache>();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
_lineHeight = _font.GetLineHeight(1);

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.GameStates
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IClientResourceCache>();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
_gameStateManager.GameStateApplied += HandleGameStateApplied;

View File

@@ -0,0 +1,59 @@
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);
}
}
}

View File

@@ -0,0 +1,680 @@
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);
}
}
}
}

View File

@@ -0,0 +1,54 @@
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;
}
}
}
}

View File

@@ -0,0 +1,144 @@
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.
}
}
}

View File

@@ -0,0 +1,51 @@
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;
}
}
}

View File

@@ -0,0 +1,432 @@
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;
}
}
}
}

View File

@@ -0,0 +1,80 @@
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.
}
}
}

View File

@@ -0,0 +1,102 @@
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.
}
}
}

View File

@@ -1,11 +1,24 @@
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.Shared.Audio.Sources
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's Audio.BufferedAudioSource's evil twin brother!
/// Hey look, it's ClydeAudio.BufferedAudioSource's evil twin brother!
/// </summary>
internal sealed class DummyBufferedAudioSource : DummyAudioSource, IBufferedAudioSource
internal sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
{
public new static DummyBufferedAudioSource Instance { get; } = new();
public int SampleRate { get; set; } = 0;

View File

@@ -0,0 +1,36 @@
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();
}
}
}

View File

@@ -0,0 +1,82 @@
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();
}
}
}

View File

@@ -512,7 +512,7 @@ namespace Robust.Client.Graphics.Clyde
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
}
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
{
ApplyFovToBuffer(viewport, eye);
}

View File

@@ -97,6 +97,9 @@ namespace Robust.Client.Graphics.Clyde
private (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)[] _lightsToRenderList = default!;
private LightCapacityComparer _lightCap = new();
private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
private unsafe void InitLighting()
{
@@ -332,7 +335,7 @@ namespace Robust.Client.Graphics.Clyde
private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
{
if (!_lightManager.Enabled)
if (!_lightManager.Enabled || !eye.DrawLight)
{
return;
}
@@ -570,6 +573,28 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
private sealed class LightCapacityComparer : IComparer<(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)>
{
public int Compare(
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) x,
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) y)
{
if (x.light.CastShadows && !y.light.CastShadows) return 1;
if (!x.light.CastShadows && y.light.CastShadows) return -1;
return 0;
}
}
private sealed class ShadowCapacityComparer : IComparer<(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)>
{
public int Compare(
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) x,
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) y)
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}
}
private (int count, Box2 expandedBounds) GetLightsToRender(
MapId map,
in Box2Rotated worldBounds,
@@ -595,20 +620,10 @@ namespace Robust.Client.Graphics.Clyde
// First, partition the array based on whether the lights are shadow casting or not
// (non shadow casting lights should be the first partition, shadow casting lights the second)
Array.Sort(_lightsToRenderList, 0, state.count,
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
if (x.light.CastShadows && !y.light.CastShadows) return 1;
else if (!x.light.CastShadows && y.light.CastShadows) return -1;
else return 0;
}));
Array.Sort(_lightsToRenderList, 0, state.count, _lightCap);
// Next, sort just the shadow casting lights by distance.
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount,
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}));
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount, _shadowCap);
// Then effectively delete the furthest lights, by setting the end of the array to exclude N
// number of shadow casting lights (where N is the number above the max number per scene.)

View File

@@ -260,14 +260,14 @@ namespace Robust.Client.Graphics.Clyde
yield break;
}
foreach (var file in _resManager.ContentFindFiles(_windowIconPath))
foreach (var file in _resourceCache.ContentFindFiles(_windowIconPath))
{
if (file.Extension != "png")
{
continue;
}
using var stream = _resManager.ContentFileRead(file);
using var stream = _resourceCache.ContentFileRead(file);
yield return Image.Load<Rgba32>(stream);
}
}

View File

@@ -11,7 +11,6 @@ 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,8 +35,7 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IClientResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;

View File

@@ -292,6 +292,123 @@ 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)

View File

@@ -1,54 +1,54 @@
using System;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics;
internal readonly struct ClydeHandle : IEquatable<ClydeHandle>, IClydeHandle
namespace Robust.Client.Graphics
{
public ClydeHandle(long value)
internal struct ClydeHandle : IEquatable<ClydeHandle>
{
Value = value;
}
public ClydeHandle(long value)
{
Value = value;
}
public long Value { get; }
public readonly long Value;
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}";
}
}
}

View File

@@ -114,10 +114,9 @@ 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;
}
@@ -127,6 +126,7 @@ namespace Robust.Client.Graphics
continue;
var advance = metrics.Value.Advance;
advanceTotal.X += advance;
baseLine += new Vector2(advance, 0);
}

View File

@@ -0,0 +1,23 @@
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);
}
}

View File

@@ -0,0 +1,16 @@
using System;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
{
internal interface IClydeAudioInternal : IClydeAudio
{
bool InitializePostWindowing();
void FrameProcess(FrameEventArgs eventArgs);
void Shutdown();
}
}

View File

@@ -0,0 +1,31 @@
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);
}
}

View File

@@ -0,0 +1,15 @@
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();
}
}

View File

@@ -4,29 +4,32 @@ using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics;
[PublicAPI]
public interface IOverlayManager
namespace Robust.Client.Graphics
{
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;
[PublicAPI]
public interface IOverlayManager
{
bool AddOverlay(Overlay overlay);
Overlay GetOverlay(Type overlayClass);
T GetOverlay<T>() where T : Overlay;
bool RemoveOverlay(Overlay overlay);
bool RemoveOverlay(Type overlayClass);
bool RemoveOverlay<T>() where T : Overlay;
bool HasOverlay(Type overlayClass);
bool HasOverlay<T>() where T : Overlay;
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
IEnumerable<Overlay> AllOverlays { get; }
}
internal interface IOverlayManagerInternal : IOverlayManager
{
void FrameUpdate(FrameEventArgs args);
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);
}
}

View File

@@ -6,106 +6,107 @@ using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Graphics;
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
namespace Robust.Client.Graphics
{
[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)
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
{
foreach (var overlay in _overlays.Values)
[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)
{
overlay.FrameUpdate(args);
}
}
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)))
{
_logger.Error($"RemoveOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
foreach (var overlay in _overlays.Values)
{
overlay.FrameUpdate(args);
}
}
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)))
public bool AddOverlay(Overlay 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;
if (_overlays.ContainsKey(overlay.GetType()))
return false;
_overlays.Add(overlay.GetType(), overlay);
return true;
}
return false;
}
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)))
public bool RemoveOverlay(Type overlayClass)
{
_logger.Error($"HasOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
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);
}
return _overlays.ContainsKey(overlayClass);
}
public bool RemoveOverlay<T>() where T : Overlay
{
return RemoveOverlay(typeof(T));
}
public bool HasOverlay<T>() where T : Overlay
{
return _overlays.ContainsKey(typeof(T));
}
public bool RemoveOverlay(Overlay overlay)
{
return _overlays.Remove(overlay.GetType());
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("overlay");
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;
}
return false;
}
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!");
}
return _overlays.ContainsKey(overlayClass);
}
public bool HasOverlay<T>() where T : Overlay
{
return _overlays.ContainsKey(typeof(T));
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("overlay");
}
}
}

View File

@@ -120,7 +120,7 @@ namespace Robust.Client.Graphics
if (_path == null)
throw new InvalidOperationException("Source shaders must specify a source file.");
_source = IoCManager.Resolve<IClientResourceCache>().GetResource<ShaderSourceResource>(_path.Value);
_source = IoCManager.Resolve<IResourceCache>().GetResource<ShaderSourceResource>(_path.Value);
if (_paramMapping != null)
{
@@ -142,7 +142,7 @@ namespace Robust.Client.Graphics
case "canvas":
Kind = ShaderKind.Canvas;
_source = IoCManager.Resolve<IClientResourceCache>().GetResource<ShaderSourceResource>("/Shaders/Internal/default-sprite.swsl");
_source = IoCManager.Resolve<IResourceCache>().GetResource<ShaderSourceResource>("/Shaders/Internal/default-sprite.swsl");
break;
default:

View File

@@ -7,7 +7,6 @@ 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;
@@ -23,7 +22,7 @@ namespace Robust.Client.Map
{
internal sealed class ClydeTileDefinitionManager : TileDefinitionManager, IClydeTileDefinitionManager
{
[Dependency] private readonly IResourceManager _manager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private Texture? _tileTextureAtlas;
@@ -87,7 +86,7 @@ namespace Robust.Client.Map
0, (h - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
Image<Rgba32> image;
using (var stream = _manager.ContentFileRead("/Textures/noTile.png"))
using (var stream = _resourceCache.ContentFileRead("/Textures/noTile.png"))
{
image = Image.Load<Rgba32>(stream);
}
@@ -111,7 +110,7 @@ namespace Robust.Client.Map
// Already know it's not null above
var path = def.Sprite!.Value;
using (var stream = _manager.ContentFileRead(path))
using (var stream = _resourceCache.ContentFileRead(path))
{
image = Image.Load<Rgba32>(stream);
}

View File

@@ -42,15 +42,20 @@ public sealed class TileEdgeOverlay : Overlay
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var mapSystem = _entManager.System<SharedMapSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
foreach (var grid in _grids)
{
var tileSize = grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var xform = xformQuery.GetComponent(grid);
args.WorldHandle.SetTransform(xform.WorldMatrix);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var localAABB = invMatrix.TransformBox(args.WorldBounds);
foreach (var tileRef in grid.Comp.GetTilesIntersecting(args.WorldBounds, false))
var enumerator = mapSystem.GetLocalTilesEnumerator(grid.Owner, grid.Comp, localAABB, false);
while (enumerator.MoveNext(out var tileRef))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
@@ -66,7 +71,7 @@ public sealed class TileEdgeOverlay : Overlay
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = grid.Comp.GetTileRef(neighborIndices);
var neighborTile = mapSystem.GetTileRef(grid.Owner, grid.Comp, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
@@ -118,9 +123,9 @@ public sealed class TileEdgeOverlay : Overlay
}
if (angle == Angle.Zero)
args.WorldHandle.DrawTextureRect(texture, box);
args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
args.WorldHandle.DrawTextureRect(texture, new Box2Rotated(box, angle, box.Center));
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
}
}
}

View File

@@ -1,6 +1,5 @@
using System.Buffers;
using System.Collections.Generic;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
@@ -8,6 +7,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Client.Physics;

View File

@@ -44,7 +44,7 @@ namespace Robust.Client.Placement.Modes
var closestEntity = snapToEntities[0];
var closestTransform = pManager.EntityManager.GetComponent<TransformComponent>(closestEntity);
if (!pManager.EntityManager.TryGetComponent<SpriteComponent?>(closestEntity, out var component) || component.BaseRSI == null)
if (!pManager.EntityManager.TryGetComponent(closestEntity, out SpriteComponent? component) || component.BaseRSI == null)
{
return;
}

View File

@@ -28,7 +28,7 @@ namespace Robust.Client.Placement
{
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] internal readonly IPlayerManager PlayerManager = default!;
[Dependency] internal readonly IClientResourceCache ResourceCache = default!;
[Dependency] internal readonly IResourceCache ResourceCache = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] internal readonly IMapManager MapManager = default!;
[Dependency] private readonly IGameTiming _time = default!;

View File

@@ -15,15 +15,22 @@ public interface IPlayerManager : ISharedPlayerManager
event Action? PlayerListUpdated;
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets attached to a new entity. See also <see cref="LocalPlayerAttachedEvent"/>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets attached to a new entity, or when the local
/// session gets updated. See also <see cref="LocalPlayerAttachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerAttached;
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets detached from new entity. See also <see cref="LocalPlayerDetachedEvent"/>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets detached from an entity, or when the local
/// session gets updated. See also <see cref="LocalPlayerDetachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerDetached;
/// <summary>
/// Invoked whenever <see cref="ISharedPlayerManager.LocalSession"/> changes.
/// </summary>
event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
void ApplyPlayerStates(IReadOnlyCollection<SessionState> list);
/// <summary>
@@ -38,34 +45,8 @@ public interface IPlayerManager : ISharedPlayerManager
/// </summary>
void SetupMultiplayer(INetChannel channel);
void SetLocalSession(ICommonSession session);
[Obsolete("Use LocalSession instead")]
LocalPlayer? LocalPlayer { get;}
}
/// <summary>
/// ECS event that gets raised when the local player gets attached to a new entity. The event is both broadcast and
/// raised directed at the new entity.
/// </summary>
public sealed class LocalPlayerAttachedEvent : EntityEventArgs
{
public LocalPlayerAttachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
/// <summary>
/// ECS event that gets raised when the local player gets detached from an entity. The event is both broadcast and
/// raised directed at the new entity.
/// </summary>
public sealed class LocalPlayerDetachedEvent : EntityEventArgs
{
public LocalPlayerDetachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}

View File

@@ -42,12 +42,13 @@ namespace Robust.Client.Player
/// <inheritdoc />
public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1;
public LocalPlayer? LocalPlayer { get; set; }
public LocalPlayer? LocalPlayer { get; private set; }
public event Action<SessionStatusEventArgs>? LocalStatusChanged;
public event Action? PlayerListUpdated;
public event Action<EntityUid>? LocalPlayerDetached;
public event Action<EntityUid>? LocalPlayerAttached;
public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
/// <inheritdoc />
public override void Initialize(int maxPlayers)
@@ -64,22 +65,14 @@ namespace Robust.Client.Player
LocalStatusChanged?.Invoke(e);
}
/// <inheritdoc />
public override void Startup()
{
if (LocalSession == null)
throw new InvalidOperationException("LocalSession cannot be null");
LocalPlayer = new LocalPlayer(LocalSession);
base.Startup();
}
public void SetupSinglePlayer(string name)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
LocalSession = CreateAndAddSession(default, name);
var session = CreateAndAddSession(default, name);
session.ClientSide = true;
SetLocalSession(session);
Startup();
PlayerListUpdated?.Invoke();
}
@@ -89,18 +82,44 @@ namespace Robust.Client.Player
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
var session = CreateAndAddSession(channel.UserId, channel.UserName);
session.Channel = channel;
LocalSession = session;
SetLocalSession(CreateAndAddSession(channel));
Startup();
_network.ClientSendMessage(new MsgPlayerListReq());
}
public void SetLocalSession(ICommonSession? session)
{
if (session == LocalSession)
return;
var old = LocalSession;
if (old?.AttachedEntity is {} oldUid)
{
LocalSession = null;
LocalPlayer = null;
Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}.");
EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true);
LocalPlayerDetached?.Invoke(oldUid);
}
LocalSession = session;
LocalPlayer = session == null ? null : new LocalPlayer(session);
Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}.");
LocalSessionChanged?.Invoke((old, LocalSession));
if (session?.AttachedEntity is {} newUid)
{
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}.");
EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true);
LocalPlayerAttached?.Invoke(newUid);
}
}
/// <inheritdoc />
public override void Shutdown()
{
if (LocalSession != null)
SetAttachedEntity(LocalSession, null);
SetAttachedEntity(LocalSession, null, out _);
LocalPlayer = null;
LocalSession = null;
_pendingStates.Clear();
@@ -108,16 +127,21 @@ namespace Robust.Client.Player
PlayerListUpdated?.Invoke();
}
public override void SetAttachedEntity(ICommonSession session, EntityUid? uid)
public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false)
{
kicked = null;
if (session == null)
return false;
if (session.AttachedEntity == uid)
return;
return true;
var old = session.AttachedEntity;
base.SetAttachedEntity(session, uid);
if (!base.SetAttachedEntity(session, uid, out kicked, force))
return false;
if (session != LocalSession)
return;
return true;
if (old.HasValue)
{
@@ -129,13 +153,13 @@ namespace Robust.Client.Player
if (uid == null)
{
Sawmill.Info($"Local player is no longer attached to any entity.");
return;
return true;
}
if (!EntManager.EntityExists(uid))
{
Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!");
return;
return true;
}
if (!EntManager.EnsureComponent(uid.Value, out EyeComponent eye))
@@ -148,6 +172,7 @@ namespace Robust.Client.Player
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}.");
EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true);
LocalPlayerAttached?.Invoke(uid.Value);
return true;
}
public void ApplyPlayerStates(IReadOnlyCollection<SessionState> list)
@@ -193,7 +218,7 @@ namespace Robust.Client.Player
_pendingStates.Remove(state.UserId);
}
SetAttachedEntity(LocalSession, uid);
SetAttachedEntity(LocalSession, uid, out _, true);
SetStatus(LocalSession, state.Status);
}
@@ -233,11 +258,10 @@ namespace Robust.Client.Player
{
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
var newSession = CreateAndAddSession(state.UserId, state.Name);
var newSession = (CommonSession) CreateAndAddSession(state.UserId, state.Name);
newSession.Ping = state.Ping;
newSession.Name = state.Name;
SetStatus(newSession, state.Status);
SetAttachedEntity(newSession, controlled);
SetAttachedEntity(newSession, controlled, out _, true);
dirty = true;
continue;
}
@@ -256,7 +280,7 @@ namespace Robust.Client.Player
local.Name = state.Name;
local.Ping = state.Ping;
SetStatus(local, state.Status);
SetAttachedEntity(local, controlled);
SetAttachedEntity(local, controlled, out _, true);
}
// Remove old users. This only works if the provided state is a list of all players
@@ -264,10 +288,12 @@ namespace Robust.Client.Player
{
foreach (var oldUser in InternalSessions.Keys.ToArray())
{
// clear slot, player left
if (users.Contains(oldUser))
continue;
if (InternalSessions[oldUser].ClientSide)
continue;
DebugTools.Assert(oldUser != LocalUser
|| LocalUser == null
|| LocalUser == default(NetUserId),

View File

@@ -13,7 +13,7 @@ namespace Robust.Client.Profiling;
public sealed class LiveProfileViewControl : Control
{
[Dependency] private readonly ProfManager _profManager = default!;
[Dependency] private readonly IClientResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
public int MaxDepth { get; set; } = 2;

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Replays;
using Robust.Shared.Serialization.Markdown.Mapping;
@@ -128,6 +129,11 @@ public interface IReplayPlaybackManager
/// </summary>
event Action? ReplayUnpaused;
/// <summary>
/// Invoked just before a replay applies a game state.
/// </summary>
event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
/// <summary>
/// If currently replaying a client-side recording, this is the user that recorded the replay.
/// Useful for setting default observer spawn positions.
@@ -137,5 +143,5 @@ public interface IReplayPlaybackManager
/// <summary>
/// Fetches the entity that the <see cref="Recorder"/> is currently attached to.
/// </summary>
public bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
}

View File

@@ -66,6 +66,7 @@ internal sealed partial class ReplayPlaybackManager
_gameState.ClearDetachQueue();
EnsureDetachedExist(checkpoint);
_gameState.DetachImmediate(checkpoint.Detached);
BeforeApplyState?.Invoke((checkpoint.State, next));
_gameState.ApplyGameState(checkpoint.State, next);
}

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Shared.Utility;
@@ -58,7 +59,13 @@ internal sealed partial class ReplayPlaybackManager
_timing.LastRealTick = _timing.LastProcessedTick = _timing.CurTick = Replay.CurTick;
_gameState.UpdateFullRep(state, cloneDelta: true);
_gameState.ApplyGameState(state, Replay.NextState);
// Clear existing lerps
_entMan.EntitySysManager.GetEntitySystem<TransformSystem>().Reset();
var next = Replay.NextState;
BeforeApplyState?.Invoke((state, next));
_gameState.ApplyGameState(state, next);
ProcessMessages(Replay.CurMessages, skipEffectEvents);
// TODO REPLAYS block audio

View File

@@ -42,7 +42,9 @@ internal sealed partial class ReplayPlaybackManager
{
var state = Replay.CurState;
_gameState.UpdateFullRep(state, cloneDelta: true);
_gameState.ApplyGameState(state, Replay.NextState);
var next = Replay.NextState;
BeforeApplyState?.Invoke((state, next));
_gameState.ApplyGameState(state, next);
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;

View File

@@ -1,7 +1,6 @@
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;
@@ -11,9 +10,9 @@ 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;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
@@ -29,7 +28,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 IAudioInternal _clydeAudio = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IClientNetManager _netMan = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
@@ -46,6 +45,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
public event Action? ReplayPlaybackStopped;
public event Action? ReplayPaused;
public event Action? ReplayUnpaused;
public event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
public ReplayData? Replay { get; private set; }
public NetUserId? Recorder => Replay?.Recorder;

View File

@@ -0,0 +1,36 @@
using System;
using System.Threading;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
{
/// <summary>
/// Base resource for the cache.
/// </summary>
public abstract class BaseResource : IDisposable
{
/// <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>
/// <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(IResourceCache cache, ResPath path, CancellationToken ct = default)
{
}
}
}

View File

@@ -1,17 +0,0 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
namespace Robust.Client.ResourceManagement;
/// <inheritdoc />
public interface IClientResourceCache : IResourceCache
{
// 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; }
}

View File

@@ -1,15 +0,0 @@
using Robust.LoaderApi;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement;
/// <inheritdoc />
internal interface IClientResourceCacheInternal : IClientResourceCache
{
void TextureLoaded(TextureLoadedEventArgs eventArgs);
void RsiLoaded(RsiLoadedEventArgs eventArgs);
void PreloadTextures();
void MountLoaderApi(IResourceManager manager, IFileApi api, string apiPrefix, ResPath? prefix = null);
}

View File

@@ -0,0 +1,49 @@
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
{
public interface IResourceCache : IResourceManager
{
T GetResource<T>(string 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>(ResPath path, [NotNullWhen(true)] out T? resource)
where T : BaseResource, new();
void ReloadResource<T>(string 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>(ResPath path, T resource)
where T : BaseResource, new();
T GetFallback<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; }
}
}

View File

@@ -0,0 +1,15 @@
using Robust.LoaderApi;
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);
void MountLoaderApi(IFileApi api, string apiPrefix, ResPath? prefix=null);
void PreloadTextures();
}
}

View File

@@ -9,13 +9,6 @@ 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;

View File

@@ -3,13 +3,10 @@ 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;
@@ -22,8 +19,7 @@ namespace Robust.Client.ResourceManagement
internal partial class ResourceCache
{
[field: Dependency] public IClyde Clyde { get; } = default!;
[field: Dependency] public IAudioInternal ClydeAudio { get; } = default!;
[Dependency] private readonly IResourceManager _manager = default!;
[field: Dependency] public IClydeAudio ClydeAudio { get; } = default!;
[field: Dependency] public IFontManager FontManager { get; } = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
@@ -48,7 +44,7 @@ namespace Robust.Client.ResourceManagement
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<TextureResource>();
var texList = _manager.ContentFindFiles("/Textures/")
var texList = 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})
@@ -58,7 +54,7 @@ namespace Robust.Client.ResourceManagement
{
try
{
TextureResource.LoadPreTexture(_manager, data);
TextureResource.LoadPreTexture(this, data);
}
catch (Exception e)
{
@@ -120,7 +116,7 @@ namespace Robust.Client.ResourceManagement
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<RSIResource>();
var rsiList = _manager.ContentFindFiles("/Textures/")
var rsiList = ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
.Select(c => c.Directory)
.Where(p => !resList.ContainsKey(p))
@@ -131,7 +127,7 @@ namespace Robust.Client.ResourceManagement
{
try
{
RSIResource.LoadPreTexture(_manager, data);
RSIResource.LoadPreTexture(this, data);
}
catch (Exception e)
{

View File

@@ -1,23 +1,221 @@
using System;
using Robust.Shared.ResourceManagement;
using Robust.Shared.ContentPack;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.LoaderApi;
namespace Robust.Client.ResourceManagement;
/// <summary>
/// Handles caching of <see cref="BaseResource"/>
/// </summary>
internal sealed partial class ResourceCache : SharedResourceCache, IClientResourceCacheInternal, IDisposable
namespace Robust.Client.ResourceManagement
{
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
public event Action<RsiLoadedEventArgs>? OnRsiLoaded;
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
{
OnRawTextureLoaded?.Invoke(eventArgs);
}
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> CachedResources =
new();
public void RsiLoaded(RsiLoadedEventArgs eventArgs)
{
OnRsiLoaded?.Invoke(eventArgs);
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
{
return GetResource<T>(new ResPath(path), useFallback);
}
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 (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;
}
}
}
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))
{
resource = (T) cached;
return true;
}
var _resource = new T();
try
{
_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}");
throw;
}
}
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))
{
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()
{
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
}
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
public event Action<RsiLoadedEventArgs>? OnRsiLoaded;
#region IDisposable Members
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
foreach (var res in CachedResources.Values.SelectMany(dict => dict.Values))
{
res.Dispose();
}
}
disposed = true;
}
~ResourceCache()
{
Dispose(false);
}
#endregion IDisposable Members
[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);
}
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);
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using Robust.Client.Audio;
using Robust.Shared.Utility;
using System.IO;
using Robust.Client.Graphics;
using Robust.Shared.IoC;
namespace Robust.Client.ResourceManagement
{
public sealed class AudioResource : BaseResource
{
public AudioStream AudioStream { get; private set; } = default!;
public override void Load(IResourceCache cache, ResPath 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");
}
}
}
public static implicit operator AudioStream(AudioResource res)
{
return res.AudioStream;
}
}
}

View File

@@ -1,8 +1,6 @@
using System.IO;
using Robust.Client.Graphics;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.ResourceManagement;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement
@@ -11,17 +9,16 @@ namespace Robust.Client.ResourceManagement
{
internal IFontFaceHandle FontFaceHandle { get; private set; } = default!;
public override void Load(IDependencyCollection dependencies, ResPath path)
public override void Load(IResourceCache cache, ResPath path)
{
if (!dependencies.Resolve<IResourceManager>().TryContentFileRead(path, out var stream))
if (!cache.TryContentFileRead(path, out var stream))
{
throw new FileNotFoundException("Content file does not exist for font");
}
using (stream)
{
FontFaceHandle = dependencies.Resolve<IFontManagerInternal>().Load(stream);
FontFaceHandle = ((IFontManagerInternal)cache.FontManager).Load(stream);
}
}

View File

@@ -3,12 +3,10 @@ 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;
using Robust.Shared.Maths;
using Robust.Shared.ResourceManagement;
using Robust.Shared.Resources;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
@@ -36,27 +34,26 @@ namespace Robust.Client.ResourceManagement
/// </summary>
public const uint MAXIMUM_RSI_VERSION = RsiLoading.MAXIMUM_RSI_VERSION;
public override void Load(IDependencyCollection dependencies, ResPath path)
public override void Load(IResourceCache cache, ResPath path)
{
var loadStepData = new LoadStepData {Path = path};
var manager = dependencies.Resolve<IResourceManager>();
LoadPreTexture(manager, loadStepData);
LoadPreTexture(cache, loadStepData);
loadStepData.AtlasTexture = dependencies.Resolve<IClyde>().LoadTextureFromImage(
loadStepData.AtlasTexture = cache.Clyde.LoadTextureFromImage(
loadStepData.AtlasSheet,
loadStepData.Path.ToString());
LoadPostTexture(loadStepData);
LoadFinish(dependencies.Resolve<IClientResourceCacheInternal>(), loadStepData);
LoadFinish(cache, loadStepData);
loadStepData.AtlasSheet.Dispose();
}
internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
{
var manifestPath = data.Path / "meta.json";
RsiLoading.RsiMetadata metadata;
using (var manifestFile = manager.ContentFileRead(manifestPath))
using (var manifestFile = cache.ContentFileRead(manifestPath))
{
metadata = RsiLoading.LoadRsiMetadata(manifestFile);
}
@@ -89,7 +86,7 @@ namespace Robust.Client.ResourceManagement
var stateObject = metadata.States[index];
// Load image from disk.
var texPath = data.Path / (stateObject.StateId + ".png");
using (var stream = manager.ContentFileRead(texPath))
using (var stream = cache.ContentFileRead(texPath))
{
reg.Src = Image.Load<Rgba32>(stream);
}
@@ -215,10 +212,14 @@ namespace Robust.Client.ResourceManagement
}
}
internal void LoadFinish(IClientResourceCacheInternal cache, LoadStepData data)
internal void LoadFinish(IResourceCache cache, LoadStepData data)
{
RSI = data.Rsi;
cache.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.RsiLoaded(new RsiLoadedEventArgs(data.Path, this, data.AtlasSheet, data.CallbackOffsets));
}
}
/// <summary>

View File

@@ -4,7 +4,6 @@ using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.ResourceManagement;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -21,31 +20,28 @@ namespace Robust.Client.ResourceManagement
[ViewVariables]
internal ParsedShader ParsedShader { get; private set; } = default!;
public override void Load(IDependencyCollection dependencies, ResPath path)
public override void Load(IResourceCache cache, ResPath path)
{
var manager = dependencies.Resolve<IResourceManager>();
using (var stream = manager.ContentFileRead(path))
using (var stream = cache.ContentFileRead(path))
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
{
ParsedShader = ShaderParser.Parse(reader, manager);
ParsedShader = ShaderParser.Parse(reader, cache);
}
ClydeHandle = dependencies.Resolve<IClydeInternal>().LoadShader(ParsedShader, path.ToString());
ClydeHandle = ((IClydeInternal)cache.Clyde).LoadShader(ParsedShader, path.ToString());
}
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
public override void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
{
var manager = dependencies.Resolve<IResourceManager>();
ct = ct != default ? ct : new CancellationTokenSource(30000).Token;
for (;;)
{
try
{
using var stream = manager.ContentFileRead(path);
using var stream = cache.ContentFileRead(path);
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
ParsedShader = ShaderParser.Parse(reader, manager);
ParsedShader = ShaderParser.Parse(reader, cache);
break;
}
catch (IOException ioe)
@@ -61,7 +57,7 @@ namespace Robust.Client.ResourceManagement
}
}
dependencies.Resolve<IClydeInternal>().ReloadShader(ClydeHandle, ParsedShader);
((IClydeInternal)cache.Clyde).ReloadShader(ClydeHandle, ParsedShader);
}
}
}

View File

@@ -1,12 +1,10 @@
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;
using Robust.Shared.Maths;
using Robust.Shared.ResourceManagement;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
@@ -21,7 +19,7 @@ namespace Robust.Client.ResourceManagement
public Texture Texture => _texture;
public override void Load(IDependencyCollection dependencies, ResPath path)
public override void Load(IResourceCache cache, ResPath path)
{
if (path.Directory.Filename.EndsWith(".rsi"))
{
@@ -33,12 +31,12 @@ namespace Robust.Client.ResourceManagement
var data = new LoadStepData {Path = path};
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
LoadTexture(dependencies.Resolve<IClyde>(), data);
LoadFinish(dependencies.Resolve<IClientResourceCache>(), data);
LoadPreTexture(cache, data);
LoadTexture(cache.Clyde, data);
LoadFinish(cache, data);
}
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
{
using (var stream = cache.ContentFileRead(data.Path))
{
@@ -53,11 +51,11 @@ namespace Robust.Client.ResourceManagement
data.Texture = clyde.LoadTextureFromImage(data.Image, data.Path.ToString(), data.LoadParameters);
}
internal void LoadFinish(IClientResourceCache cache, LoadStepData data)
internal void LoadFinish(IResourceCache cache, LoadStepData data)
{
_texture = data.Texture;
if (cache is IClientResourceCacheInternal cacheInternal)
if (cache is IResourceCacheInternal cacheInternal)
{
cacheInternal.TextureLoaded(new TextureLoadedEventArgs(data.Path, data.Image, this));
}
@@ -65,7 +63,7 @@ namespace Robust.Client.ResourceManagement
data.Image.Dispose();
}
private static TextureLoadParameters? TryLoadTextureParameters(IResourceManager cache, ResPath path)
private static TextureLoadParameters? TryLoadTextureParameters(IResourceCache cache, ResPath path)
{
var metaPath = path.WithName(path.Filename + ".yml");
if (cache.TryContentFileRead(metaPath, out var stream))
@@ -92,11 +90,12 @@ namespace Robust.Client.ResourceManagement
return null;
}
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
public override void Reload(IResourceCache cache, ResPath path, CancellationToken ct = default)
{
var data = new LoadStepData {Path = path};
var clyde = IoCManager.Resolve<IClyde>();
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
var data = new LoadStepData {Path = path};
LoadPreTexture(cache, data);
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
{
@@ -107,7 +106,7 @@ namespace Robust.Client.ResourceManagement
{
// Dimensions do not match, make new texture.
_texture.Dispose();
LoadTexture(dependencies.Resolve<IClyde>(), data);
LoadTexture(clyde, data);
_texture = data.Texture;
}

View File

@@ -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.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" />
<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" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />
</ItemGroup>

Some files were not shown because too many files have changed in this diff Show More