Compare commits

..

40 Commits

Author SHA1 Message Date
metalgearsloth
9b04270178 Version: 175.0.0 2023-10-29 15:03:09 +11:00
metalgearsloth
d75dbc901f Audio rework (#4421) 2023-10-29 14:58:19 +11:00
Leon Friedrich
19a3e82848 Cache prototype data for IEntityManager.IsDefault() (#4531) 2023-10-29 12:54:52 +11:00
Leon Friedrich
911abf2693 Remove empty planet-map chunks (#4529) 2023-10-29 12:52:03 +11:00
ElectroJr
f5874ea402 Version: 174.0.0 2023-10-28 13:26:49 -04:00
metalgearsloth
b486ef885c Add NextAngle for System.Random (#4522) 2023-10-29 04:22:32 +11:00
metalgearsloth
9d55d77e48 Sprite GetFrame (#4528) 2023-10-29 04:21:52 +11:00
Leon Friedrich
5af3cb969c Move ActorComponent to shared (#4527) 2023-10-29 04:21:09 +11:00
metalgearsloth
429bc806dc Version: 173.1.0 2023-10-28 15:36:26 +11:00
metalgearsloth
81484699a8 Add chain shapes (#4523)
* Add chain shapes

* rar only

* that too

* weh

* a

* Update Robust.Shared/Physics/Dynamics/Contacts/Contact.cs

Co-authored-by: Moony <moony@hellomouse.net>

* Update Robust.Shared/Physics/Dynamics/Contacts/Contact.cs

Co-authored-by: Moony <moony@hellomouse.net>

---------

Co-authored-by: Moony <moony@hellomouse.net>
2023-10-28 15:29:30 +11:00
metalgearsloth
7cad8d5ba3 Version: 173.0.0 2023-10-28 14:02:06 +11:00
Leon Friedrich
3aa04a3c86 Fix grid chunk bugs (#4525)
* Fix grid rendering

* Use TileChangedEvent

* Other empty chunk fixes

* Remove assert

Good ol integration tests at it again, adding invalid components
2023-10-28 13:57:54 +11:00
metalgearsloth
9750b113c8 Version: 172.0.0 2023-10-24 20:22:31 +11:00
Leon Friedrich
5a6c4220fc IPlayerManager refactor (#4518) 2023-10-24 20:18:58 +11:00
Leon Friedrich
b2d389f184 Remove TryLifestage() helpers (#4519) 2023-10-24 18:46:46 +11:00
Leon Friedrich
ad0cb05dd6 Add EnsureComponent(ref Entity<T?>) (#4516) 2023-10-24 17:19:38 +11:00
Leon Friedrich
ad134d9e4e Fix game state logging spam (#4517) 2023-10-24 14:09:55 +11:00
Leon Friedrich
be33bc2219 Re-add force ack threshold (#4423) and fix bugs. (#4438)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-10-22 23:27:15 +11:00
metalgearsloth
aa2fd2107d Add mgs to physics codeowners (#4510)
Please ping me for this is creates more work if you do not ping me.
2023-10-22 05:03:47 -07:00
metalgearsloth
554e0777b1 Version: 171.0.0 2023-10-22 16:58:48 +11:00
metalgearsloth
21b7c5f93e Cleanup relays on joint deletion (#4497) 2023-10-22 16:53:10 +11:00
Leon Friedrich
9e5c1e9c95 Change place-next-to helper methods (#4506) 2023-10-22 16:52:58 +11:00
Leon Friedrich
6825f09fb9 Set EntityLastModifiedTick when an entity spawns (#4509) 2023-10-22 16:51:40 +11:00
DrSmugleaf
58e3a4eb4a Version: 170.0.0 2023-10-21 14:31:08 -07:00
DrSmugleaf
9a342f0d11 Fix double delete entity command, fix not being able to delete individual entities (#4508) 2023-10-21 14:19:34 -07:00
DrSmugleaf
f754ddb96d Remove all usages of obsolete Dirty method, remove some obsoleted methods (#4500) 2023-10-21 14:19:07 -07:00
Leon Friedrich
7feede0d95 Fix duplicate command error (#4507) 2023-10-21 14:18:50 -07:00
Jordan Dominion
ea152366e3 Allow deletion of FileLogHandler logs while engine is running (#4501) 2023-10-21 15:07:10 +02:00
DrSmugleaf
ab47d4e009 Version: 169.0.1 2023-10-21 03:55:02 -07:00
DrSmugleaf
81b2a3825e Fix help command, let the client know about server toolshed commands (#4502) 2023-10-21 03:54:17 -07:00
DrSmugleaf
56d850f389 Version: 169.0.0 2023-10-19 12:27:27 -07:00
DrSmugleaf
b737ecf9b3 Add generic EntityUid, remove some usages of .Owner (#4498) 2023-10-19 12:23:48 -07:00
DrSmugleaf
ed5223b592 Remove by-refness subscription test (#4499) 2023-10-19 02:04:32 -07:00
DrSmugleaf
f87012e681 Allow handling by-value events by ref (#4373) 2023-10-18 18:37:43 -07:00
wixoa
54529fdbe3 Respect the manifest's assemblyPrefix value on the server (#4492) 2023-10-18 20:29:28 +02:00
DrSmugleaf
1745a12e5a Remove casts to Component (#4495) 2023-10-17 20:45:21 -07:00
DrSmugleaf
d201d787b7 Remove obsoletion from localized and console commands (#4496) 2023-10-17 20:18:30 -07:00
DrSmugleaf
904ddea274 Version: 168.0.0 2023-10-17 19:38:56 -07:00
DrSmugleaf
6b6ec844e8 Replace all T : Component constraints with T : IComponent (#4494) 2023-10-17 19:37:46 -07:00
Jordan Dominion
f24d18f470 Allow for ushort CVars (#4493) 2023-10-17 16:44:18 -07:00
369 changed files with 17024 additions and 7493 deletions

3
.github/CODEOWNERS vendored
View File

@@ -7,3 +7,6 @@
**/Toolshed/** @moonheart08
*Command.cs @moonheart08
*Commands.cs @moonheart08
# Physics
**/Robust.Shared/Physics/** @metalgearsloth

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,7 +54,133 @@ END TEMPLATE-->
*None yet*
## 167.0.1
## 175.0.0
### Breaking changes
* Removed static SoundSystem.Play methods.
* Moved IPlayingAudioStream onto AudioComponent and entities instead of an abstract stream.
* IResourceCache is in shared and IClientResourceCache is the client version to use for textures.
* Default audio attenuation changed from InverseDistanceClamped to LinearDistanceClamped.
* Removed per-source audio attenuation.
### New features
* Add preliminary support for EFX Reverb presets + auxiliary slots; these are also entities.
* Audio on grid entities is now attached to the grid.
### Bugfixes
* If an audio entity comes into PVS range its track will start at the relevant offset and not the beginning.
* Z-Axis offset is considered for ReferenceDistance / MaxDistance for audio.
* Audio will now pause if the attached entity is paused.
### Other
* Changed audio Z-Axis offset from -5m to -1m.
## 174.0.0
### Breaking changes
* ActorComponent has been moved to `Robust.Shared.Player` (namespace changed).
### New features
* Added `SpriteSystem.GetFrame()` method, which takes in an animated RSI and a time and returns a frame/texture.
* Added `IRobustRandom.NextAngle()`
## 173.1.0
### New features
* Add physics chain shapes from Box2D.
## 173.0.0
### Breaking changes
* Remove GridModifiedEvent in favor of TileChangedEvent.
### Bugfixes
* Fix some grid rendering bugs where chunks don't get destroyed correctly.
## 172.0.0
### Breaking changes
* Remove TryLifestage helper methods.
* Refactor IPlayerManager to remove more IPlayerSession, changed PlayerAttachedEvent etc on client to have the Local prefix, and shuffled namespaces around.
### New features
* Add EnsureComponent(ref Entity<\T?>)
### Bugfixes
* Re-add force ask threshold and fix other PVS bugs.
## 171.0.0
### Breaking changes
* Change PlaceNextTo method names to be more descriptive.
* Rename RefreshRelay for joints to SetRelay to match its behaviour.
### Bugfixes
* Fix PVS error spam for joint relays not being cleaned up.
### Other
* Set EntityLastModifiedTick on entity spawn.
## 170.0.0
### Breaking changes
* Removed obsolete methods and properties in VisibilitySystem, SharedContainerSystem and MetaDataComponent.
### Bugfixes
* Fixed duplicate command error.
* Fixed not being able to delete individual entities with the delete command.
### Other
* FileLogHandler logs can now be deleted while the engine is running.
## 169.0.1
### Other
* The client now knows about registered server-side toolshed commands.
## 169.0.0
### Breaking changes
* Entity<T> has been introduced to hold a component and its owning entity. Some methods that returned and accepted components directly have been removed or obsoleted to reflect this.
### Other
* By-value events may now be subscribed to by-ref.
* The manifest's assemblyPrefix value is now respected on the server.
## 168.0.0
### Breaking changes
* The Component.OnRemove method has been removed. Use SubscribeLocalEvent<TComp, ComponentRemove>(OnRemove) from an EntitySystem instead.
## 167.0.0
@@ -101,7 +227,7 @@ END TEMPLATE-->
### New features
* The YAML validator now checks the default values of ProtoId<T> and EntProtoId data fields.
* The YAML validator now checks the default values of ProtoId<T> and EntProtoId data fields.
### Bugfixes

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -17,15 +17,15 @@ cmd-error-dir-not-found = Could not find directory: {$dir}.
cmd-failure-no-attached-entity = There is no entity attached to this shell.
## 'help' command
cmd-oldhelp-desc = Display general help or help text for a specific command
cmd-oldhelp-help = Usage: help [command name]
cmd-help-desc = Display general help or help text for a specific command
cmd-help-help = Usage: help [command name]
When no command name is provided, displays general-purpose help text. If a command name is provided, displays help text for that command.
cmd-oldhelp-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
cmd-oldhelp-unknown = Unknown command: { $command }
cmd-oldhelp-top = { $command } - { $description }
cmd-oldhelp-invalid-args = Invalid amount of arguments.
cmd-oldhelp-arg-cmdname = [command name]
cmd-help-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
cmd-help-unknown = Unknown command: { $command }
cmd-help-top = { $command } - { $description }
cmd-help-invalid-args = Invalid amount of arguments.
cmd-help-arg-cmdname = [command name]
## 'cvar' command
cmd-cvar-desc = Gets or sets a CVar.

View File

@@ -23,16 +23,6 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
"Make sure that methods subscribing to a ref event have the ref keyword for the event argument."
);
private static readonly DiagnosticDescriptor ByValueEventSubscribedByRefRule = new(
Diagnostics.IdValueEventRaisedByRef,
"Value event subscribed to by-ref",
"Tried to subscribe to a value event '{0}' by-ref.",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure that methods subscribing to value events do not have the ref keyword for the event argument."
);
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
Diagnostics.IdByRefEventRaisedByValue,
"By-ref event raised by value",
@@ -55,7 +45,6 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
ByRefEventSubscribedByValueRule,
ByValueEventSubscribedByRefRule,
ByRefEventRaisedByValueRule,
ByValueEventRaisedByRefRule
);
@@ -64,71 +53,9 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(CheckEventSubscription, OperationKind.Invocation);
context.RegisterOperationAction(CheckEventRaise, OperationKind.Invocation);
}
private void CheckEventSubscription(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation operation)
return;
var subscribeMethods = context.Compilation
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntitySystem")?
.GetMembers()
.Where(m => m.Name.Contains("SubscribeLocalEvent"))
.Cast<IMethodSymbol>();
if (subscribeMethods == null)
return;
if (!subscribeMethods.Any(m => m.Equals(operation.TargetMethod.OriginalDefinition, Default)))
return;
var typeArguments = operation.TargetMethod.TypeArguments;
if (typeArguments.Length < 1 || typeArguments.Length > 2)
return;
if (operation.Arguments.First().Value is not IDelegateCreationOperation delegateCreation)
return;
if (delegateCreation.Target is not IMethodReferenceOperation methodReference)
return;
var eventParameter = methodReference.Method.Parameters.LastOrDefault();
if (eventParameter == null)
return;
ITypeSymbol eventArgument;
switch (typeArguments.Length)
{
case 1:
eventArgument = typeArguments[0];
break;
case 2:
eventArgument = typeArguments[1];
break;
default:
return;
}
var byRefAttribute = context.Compilation.GetTypeByMetadataName(ByRefAttribute);
if (byRefAttribute == null)
return;
var isByRefEventType = eventArgument
.GetAttributes()
.Any(attribute => attribute.AttributeClass?.Equals(byRefAttribute, Default) ?? false);
var parameterIsRef = eventParameter.RefKind == RefKind.Ref;
if (isByRefEventType != parameterIsRef)
{
var descriptor = isByRefEventType ? ByRefEventSubscribedByValueRule : ByValueEventSubscribedByRefRule;
var diagnostic = Diagnostic.Create(descriptor, operation.Syntax.GetLocation(), eventArgument);
context.ReportDiagnostic(diagnostic);
}
}
private void CheckEventRaise(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation operation)

View File

@@ -18,7 +18,6 @@ public static class Diagnostics
public const string IdInvalidNotNullableFlagType = "RA0011";
public const string IdNotNullableFlagValueType = "RA0012";
public const string IdByRefEventSubscribedByValue = "RA0013";
public const string IdValueEventSubscribedByRef = "RA0014";
public const string IdByRefEventRaisedByValue = "RA0015";
public const string IdValueEventRaisedByRef = "RA0016";
public const string IdDataDefinitionPartial = "RA0017";

View File

@@ -54,7 +54,7 @@ public class RecursiveMoveBenchmark
var mapSys = _entMan.System<SharedMapSystem>();
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var gridComp = mapMan.CreateGrid(mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
_mapCoords = new EntityCoordinates(map, 100, 100);

View File

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

View File

@@ -0,0 +1,58 @@
using System.Collections.Concurrent;
using OpenTK.Audio.OpenAL;
namespace Robust.Client.Audio;
internal partial class AudioManager
{
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
public void FlushALDisposeQueues()
{
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer(handle);
_checkAlError();
}
}
internal void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
internal void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
internal void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
}

View File

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

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Sources;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.Audio;
internal sealed partial class AudioManager : 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

@@ -0,0 +1,89 @@
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Audio;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
namespace Robust.Client.Audio;
/// <summary>
/// Debug overlay for audio.
/// </summary>
public sealed class AudioOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
private IEntityManager _entManager;
private IPlayerManager _playerManager;
private AudioSystem _audio;
private SharedTransformSystem _transform;
private Font _font;
public AudioOverlay(IEntityManager entManager, IPlayerManager playerManager, 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

@@ -0,0 +1,76 @@
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Components;
using Robust.Shared.GameObjects;
namespace Robust.Client.Audio;
public sealed partial class AudioSystem
{
protected override void InitializeEffect()
{
base.InitializeEffect();
SubscribeLocalEvent<AudioEffectComponent, ComponentAdd>(OnEffectAdd);
SubscribeLocalEvent<AudioEffectComponent, ComponentShutdown>(OnEffectShutdown);
SubscribeLocalEvent<AudioAuxiliaryComponent, ComponentAdd>(OnAuxiliaryAdd);
SubscribeLocalEvent<AudioAuxiliaryComponent, AfterAutoHandleStateEvent>(OnAuxiliaryAuto);
}
private void OnEffectAdd(EntityUid uid, AudioEffectComponent component, ComponentAdd args)
{
var effect = new AudioEffect(_audio);
component.Effect = effect;
}
private void OnEffectShutdown(EntityUid uid, AudioEffectComponent component, ComponentShutdown args)
{
if (component.Effect is AudioEffect effect)
{
effect.Dispose();
}
}
private void OnAuxiliaryAdd(EntityUid uid, AudioAuxiliaryComponent component, ComponentAdd args)
{
component.Auxiliary = new AuxiliaryAudio();
}
private void OnAuxiliaryAuto(EntityUid uid, AudioAuxiliaryComponent component, ref AfterAutoHandleStateEvent args)
{
if (TryComp<AudioEffectComponent>(component.Effect, out var effectComp))
{
component.Auxiliary.SetEffect(effectComp.Effect);
}
else
{
component.Auxiliary.SetEffect(null);
}
}
public override void SetAuxiliary(EntityUid uid, AudioComponent audio, EntityUid? auxUid)
{
base.SetAuxiliary(uid, audio, auxUid);
if (TryComp<AudioAuxiliaryComponent>(audio.Auxiliary, out var auxComp))
{
audio.Source.SetAuxiliary(auxComp.Auxiliary);
}
else
{
audio.Source.SetAuxiliary(null);
}
}
public override void SetEffect(EntityUid auxUid, AudioAuxiliaryComponent aux, EntityUid? effectUid)
{
base.SetEffect(auxUid, aux, effectUid);
if (TryComp<AudioEffectComponent>(aux.Effect, out var effectComp))
{
aux.Auxiliary.SetEffect(effectComp.Effect);
}
else
{
aux.Auxiliary.SetEffect(null);
}
}
}

View File

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

@@ -0,0 +1,455 @@
using System;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Effects;
/// <inheritdoc />
internal sealed class AudioEffect : IAudioEffect
{
internal int Handle;
private readonly IAudioInternal _master;
public AudioEffect(IAudioInternal manager)
{
Handle = EFX.GenEffect();
_master = manager;
EFX.Effect(Handle, EffectInteger.EffectType, (int) EffectType.EaxReverb);
}
public void Dispose()
{
if (Handle != 0)
{
EFX.DeleteEffect(Handle);
Handle = 0;
}
}
private void _checkDisposed()
{
if (Handle == -1)
{
throw new ObjectDisposedException(nameof(AudioEffect));
}
}
/// <inheritdoc />
public float Density
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDensity, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDensity, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float Diffusion
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDiffusion, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDiffusion, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float Gain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float GainHF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainHF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float GainLF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbGainLF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbGainLF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayHFRatio
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayHFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayHFRatio, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float DecayLFRatio
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbDecayLFRatio, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbDecayLFRatio, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ReflectionsGain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ReflectionsDelay
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbReflectionsDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbReflectionsDelay, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public Vector3 ReflectionsPan
{
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbReflectionsPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
set
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbReflectionsPan, ref openVec);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LateReverbGain
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbGain, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbGain, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LateReverbDelay
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLateReverbDelay, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLateReverbDelay, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public Vector3 LateReverbPan
{
get
{
_checkDisposed();
var value = EFX.GetEffect(Handle, EffectVector3.EaxReverbLateReverbPan);
_master._checkAlError();
return new Vector3(value.X, value.Z, value.Y);
}
set
{
_checkDisposed();
var openVec = new OpenTK.Mathematics.Vector3(value.X, value.Y, value.Z);
EFX.Effect(Handle, EffectVector3.EaxReverbLateReverbPan, ref openVec);
_master._checkAlError();
}
}
/// <inheritdoc />
public float EchoTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float EchoDepth
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbEchoDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbEchoDepth, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ModulationTime
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationTime, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationTime, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float ModulationDepth
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbModulationDepth, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbModulationDepth, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float AirAbsorptionGainHF
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbAirAbsorptionGainHF, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float HFReference
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbHFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbHFReference, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float LFReference
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbLFReference, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbLFReference, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public float RoomRolloffFactor
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectFloat.EaxReverbRoomRolloffFactor, value);
_master._checkAlError();
}
}
/// <inheritdoc />
public int DecayHFLimit
{
get
{
_checkDisposed();
EFX.GetEffect(Handle, EffectInteger.EaxReverbDecayHFLimit, out var value);
_master._checkAlError();
return value;
}
set
{
_checkDisposed();
EFX.Effect(Handle, EffectInteger.EaxReverbDecayHFLimit, value);
_master._checkAlError();
}
}
}

View File

@@ -0,0 +1,32 @@
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio.Effects;
namespace Robust.Client.Audio.Effects;
/// <inheritdoc />
internal sealed class AuxiliaryAudio : IAuxiliaryAudio
{
internal int Handle = EFX.GenAuxiliaryEffectSlot();
public void Dispose()
{
if (Handle != -1)
{
EFX.DeleteAuxiliaryEffectSlot(Handle);
Handle = -1;
}
}
/// <inheritdoc />
public void SetEffect(IAudioEffect? effect)
{
if (effect is AudioEffect audEffect)
{
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, audEffect.Handle);
}
else
{
EFX.AuxiliaryEffectSlot(Handle, EffectSlotInteger.Effect, 0);
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -20,7 +21,7 @@ public interface IMidiRenderer : IDisposable
/// <summary>
/// The buffered audio source of this renderer.
/// </summary>
internal IClydeBufferedAudioSource Source { get; }
internal IBufferedAudioSource Source { get; }
/// <summary>
/// Whether this renderer has been disposed or not.

View File

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

View File

@@ -4,7 +4,9 @@ using JetBrains.Annotations;
using NFluidsynth;
using Robust.Client.Graphics;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -52,8 +54,8 @@ internal sealed class MidiRenderer : IMidiRenderer
private IMidiRenderer? _master;
public MidiRendererState RendererState => _rendererState;
public IClydeBufferedAudioSource Source { get; set; }
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
public IBufferedAudioSource Source { get; set; }
IBufferedAudioSource IMidiRenderer.Source => Source;
[ViewVariables]
public bool Disposed { get; private set; } = false;
@@ -247,7 +249,7 @@ internal sealed class MidiRenderer : IMidiRenderer
public event Action? OnMidiPlayerFinished;
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono,
IMidiManager midiManager, IClydeAudio clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
IMidiManager midiManager, IAudioInternal clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
{
_midiManager = midiManager;
_taskManager = taskManager;
@@ -488,7 +490,7 @@ internal sealed class MidiRenderer : IMidiRenderer
}
}
if (!Source.IsPlaying) Source.StartPlaying();
Source.StartPlaying();
}
public void ApplyState(MidiRendererState state, bool filterChannels = false)

View File

@@ -0,0 +1,34 @@
using Robust.Client.Audio;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Commands;
/// <summary>
/// Shows a debug overlay for audio sources.
/// </summary>
public sealed class ShowAudioCommand : LocalizedCommands
{
[Dependency] private readonly 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

@@ -0,0 +1,90 @@
using System;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Shared.Audio;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Sources;
internal sealed class AudioSource : BaseAudioSource
{
/// <summary>
/// Underlying stream to the audio.
/// </summary>
private readonly AudioStream _sourceStream;
#if DEBUG
private bool _didPositionWarning;
#endif
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
{
_sourceStream = sourceStream;
}
/// <inheritdoc />
public override Vector2 Position
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSource3f.Position, out var x, out var y, out _);
Master._checkAlError();
return new Vector2(x, y);
}
set
{
_checkDisposed();
var (x, y) = value;
if (!AreFinite(x, y))
{
return;
}
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
{
_didPositionWarning = true;
Master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
_sourceStream.Name);
// warning isn't enough, people just ignore it :(
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
}
#endif
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
Master._checkAlError();
}
}
~AudioSource()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
if (!disposing)
{
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
Master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
}
else
{
if (FilterHandle != 0)
EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
Master.RemoveAudioSource(SourceHandle);
Master._checkAlError();
}
FilterHandle = 0;
SourceHandle = -1;
}
}

View File

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

@@ -0,0 +1,224 @@
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,8 +1,6 @@
using System;
using System.Linq;
using System.Net;
using Robust.Client.Configuration;
using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Player;
@@ -10,13 +8,12 @@ using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -65,12 +62,12 @@ namespace Robust.Client
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
_playMan.Initialize();
_playMan.Initialize(0);
_playMan.PlayerListUpdated += OnPlayerListUpdated;
Reset();
}
private void OnPlayerListUpdated(object? sender, EventArgs e)
private void OnPlayerListUpdated()
{
var serverPlayers = _playMan.PlayerCount;
if (_net.ServerChannel != null && GameInfo != null && _net.IsConnected)
@@ -130,9 +127,10 @@ namespace Robust.Client
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connecting);
DebugTools.Assert(!_net.IsConnected);
_playMan.Startup();
_playMan.LocalPlayer!.Name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
var name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
_playMan.SetupSinglePlayer(name);
OnRunLevelChanged(ClientRunLevel.SinglePlayerGame);
_playMan.JoinGame(_playMan.LocalSession!);
GameStartedSetup();
}
@@ -173,22 +171,14 @@ namespace Robust.Client
info.ServerName = serverName;
}
var maxPlayers = _configManager.GetCVar<int>("game.maxplayers");
info.ServerMaxPlayers = maxPlayers;
var userName = _net.ServerChannel!.UserName;
var userId = _net.ServerChannel.UserId;
var channel = _net.ServerChannel!;
// start up player management
_playMan.Startup();
_playMan.LocalPlayer!.UserId = userId;
_playMan.LocalPlayer.Name = userName;
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
_playMan.SetupMultiplayer(channel);
_playMan.PlayerStatusChanged += OnStatusChanged;
var serverPlayers = _playMan.PlayerCount;
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
_discord.Update(info.ServerName, channel.UserName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
}
@@ -221,6 +211,8 @@ namespace Robust.Client
private void Reset()
{
_configManager.ReceivedInitialNwVars -= OnReceivedClientData;
_playMan.PlayerStatusChanged -= OnStatusChanged;
_configManager.ClearReceivedInitialNwVars();
OnRunLevelChanged(ClientRunLevel.Initialize);
}
@@ -263,19 +255,17 @@ namespace Robust.Client
Reset();
}
private void OnLocalStatusChanged(object? obj, StatusEventArgs eventArgs)
private void OnStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session != _playMan.LocalSession)
return;
// player finished fully connecting to the server.
// OldStatus is used here because it can go from connecting-> connected or connecting-> ingame
if (eventArgs.OldStatus == SessionStatus.Connecting)
{
OnPlayerJoinedServer(_playMan.LocalPlayer!.Session);
}
if (eventArgs.NewStatus == SessionStatus.InGame)
{
OnPlayerJoinedGame(_playMan.LocalPlayer!.Session);
}
if (e.OldStatus == SessionStatus.Connecting)
OnPlayerJoinedServer(e.Session);
else if (e.NewStatus == SessionStatus.InGame)
OnPlayerJoinedGame(e.Session);
}
private void OnRunLevelChanged(ClientRunLevel newRunLevel)

View File

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

View File

@@ -13,7 +13,7 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;

View File

@@ -26,10 +26,7 @@ namespace Robust.Client.Console.Commands
var entity = _entityManager.GetEntity(netEntity);
var componentName = args[1];
var component = (Component) _componentFactory.GetComponent(componentName);
component.Owner = entity;
var component = _componentFactory.GetComponent(componentName);
_entityManager.AddComponent(entity, component);
}
}

View File

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

View File

@@ -1,6 +1,7 @@
#if DEBUG
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
@@ -8,7 +9,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Debugging
@@ -19,6 +19,7 @@ namespace Robust.Client.Debugging
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
private Label? _label;
@@ -70,7 +71,7 @@ namespace Robust.Client.Debugging
return;
}
var tile = grid.GetTileRef(spot);
var tile = _mapSystem.GetTileRef(gridUid, grid, spot);
_label.Position = mouseSpot.Position + new Vector2(32, 0);
if (_hovered?.GridId == gridUid && _hovered?.Tile == tile) return;
@@ -79,7 +80,7 @@ namespace Robust.Client.Debugging
var text = new StringBuilder();
foreach (var ent in grid.GetAnchoredEntities(spot))
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
{
if (EntityManager.TryGetComponent<MetaDataComponent>(ent, out var meta))
{

View File

@@ -46,7 +46,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -97,7 +96,7 @@ namespace Robust.Client.Debugging
IoCManager.Resolve<IInputManager>(),
IoCManager.Resolve<IMapManager>(),
IoCManager.Resolve<IPlayerManager>(),
IoCManager.Resolve<IResourceCache>(),
IoCManager.Resolve<IClientResourceCache>(),
this,
Get<EntityLookupSystem>(),
Get<SharedPhysicsSystem>()));
@@ -207,8 +206,9 @@ namespace Robust.Client.Debugging
private readonly Font _font;
private HashSet<Joint> _drawnJoints = new();
private List<Entity<MapGridComponent>> _grids = new();
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IClientResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
{
_entityManager = entityManager;
_eyeManager = eyeManager;
@@ -231,32 +231,33 @@ namespace Robust.Client.Debugging
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
var xform = _physicsSystem.GetPhysicsTransform(physBody);
var comp = physBody.Comp;
const float AlphaModifier = 0.2f;
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
{
// Invalid shape - Box2D doesn't check for IsSensor but we will for sanity.
if (physBody.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
if (comp.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
{
DrawShape(worldHandle, fixture, xform, Color.Red.WithAlpha(AlphaModifier));
}
else if (!physBody.CanCollide)
else if (!comp.CanCollide)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.3f).WithAlpha(AlphaModifier));
}
else if (physBody.BodyType == BodyType.Static)
else if (comp.BodyType == BodyType.Static)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.9f, 0.5f).WithAlpha(AlphaModifier));
}
else if ((physBody.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
else if ((comp.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.9f).WithAlpha(AlphaModifier));
}
else if (!physBody.Awake)
else if (!comp.Awake)
{
DrawShape(worldHandle, fixture, xform, new Color(0.6f, 0.6f, 0.6f).WithAlpha(AlphaModifier));
}
@@ -275,15 +276,18 @@ namespace Robust.Client.Debugging
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
var color = Color.Purple.WithAlpha(Alpha);
var transform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 0.2f, color);
var transform = _physicsSystem.GetPhysicsTransform(physBody);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.Comp.LocalCenter), 0.2f, color);
}
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, viewBounds))
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, viewBounds, ref _grids);
foreach (var grid in _grids)
{
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid.Owner);
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid);
var color = Color.Orange.WithAlpha(Alpha);
var transform = _physicsSystem.GetPhysicsTransform(grid.Owner);
var transform = _physicsSystem.GetPhysicsTransform(grid);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 1f, color);
}
}
@@ -292,14 +296,14 @@ namespace Robust.Client.Debugging
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
var xform = _physicsSystem.GetPhysicsTransform(physBody);
const float AlphaModifier = 0.2f;
Box2? aabb = null;
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
@@ -318,10 +322,11 @@ namespace Robust.Client.Debugging
{
_drawnJoints.Clear();
foreach (var jointComponent in _entityManager.EntityQuery<JointComponent>(true))
var query = _entityManager.AllEntityQueryEnumerator<JointComponent>();
while (query.MoveNext(out var uid, out var jointComponent))
{
if (jointComponent.JointCount == 0 ||
!_entityManager.TryGetComponent(jointComponent.Owner, out TransformComponent? xf1) ||
!_entityManager.TryGetComponent(uid, out TransformComponent? xf1) ||
!viewAABB.Contains(xf1.WorldPosition)) continue;
foreach (var (_, joint) in jointComponent.Joints)
@@ -361,6 +366,9 @@ namespace Robust.Client.Debugging
_debugPhysicsSystem.PointCount = 0;
}
worldHandle.UseShader(null);
worldHandle.SetTransform(Matrix3.Identity);
}
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
@@ -370,28 +378,31 @@ namespace Robust.Client.Debugging
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ShapeInfo) != 0x0)
{
var hoverBodies = new List<PhysicsComponent>();
var hoverBodies = new List<Entity<PhysicsComponent>>();
var bounds = Box2.UnitCentered.Translated(_eyeManager.PixelToMap(mousePos.Position).Position);
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, bounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
hoverBodies.Add(physBody);
var uid = physBody.Owner;
if (_entityManager.HasComponent<MapGridComponent>(uid)) continue;
hoverBodies.Add((uid, physBody));
}
var lineHeight = _font.GetLineHeight(1f);
var drawPos = mousePos.Position + new Vector2(20, 0) + new Vector2(0, -(hoverBodies.Count * 4 * lineHeight / 2f));
int row = 0;
foreach (var body in hoverBodies)
foreach (var bodyEnt in hoverBodies)
{
if (body != hoverBodies[0])
if (bodyEnt != hoverBodies[0])
{
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), "------");
row++;
}
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
var body = bodyEnt.Comp;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;
@@ -430,6 +441,9 @@ namespace Robust.Client.Debugging
}
}
}
screenHandle.UseShader(null);
screenHandle.SetTransform(Matrix3.Identity);
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -451,11 +465,26 @@ namespace Robust.Client.Debugging
{
switch (fixture.Shape)
{
case ChainShape cShape:
{
var count = cShape.Count;
var vertices = cShape.Vertices;
var v1 = Transform.Mul(xform, vertices[0]);
for (var i = 1; i < count; ++i)
{
var v2 = Transform.Mul(xform, vertices[i]);
worldHandle.DrawLine(v1, v2, color);
v1 = v2;
}
}
break;
case PhysShapeCircle circle:
var center = Transform.Mul(xform, circle.Position);
worldHandle.DrawCircle(center, circle.Radius, color);
break;
case EdgeShape edge:
{
var v1 = Transform.Mul(xform, edge.Vertex1);
var v2 = Transform.Mul(xform, edge.Vertex2);
worldHandle.DrawLine(v1, v2, color);
@@ -465,6 +494,7 @@ namespace Robust.Client.Debugging
worldHandle.DrawCircle(v1, 0.1f, color);
worldHandle.DrawCircle(v2, 0.1f, color);
}
}
break;
case PolygonShape poly:

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Robust.Client.WebViewHook;
using Robust.Shared.ContentPack;
using Robust.Shared.Log;
using Robust.Shared.Utility;

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Net;
using System.Runtime;
using System.Threading.Tasks;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Console;
using Robust.Client.GameObjects;
@@ -24,6 +25,7 @@ using Robust.Client.WebViewHook;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
@@ -48,7 +50,8 @@ namespace Robust.Client
internal sealed partial class GameController : IGameControllerInternal
{
[Dependency] private readonly INetConfigurationManagerInternal _configurationManager = default!;
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IClientResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
@@ -68,7 +71,7 @@ namespace Robust.Client
[Dependency] private readonly IClientViewVariablesManagerInternal _viewVariablesManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IClydeAudioInternal _clydeAudio = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
[Dependency] private readonly IScriptClient _scriptClient = default!;
@@ -111,7 +114,7 @@ namespace Robust.Client
DebugTools.AssertNotNull(_resourceManifest);
_clyde.InitializePostWindowing();
_clydeAudio.InitializePostWindowing();
_audio.InitializePostWindowing();
_clyde.SetWindowTitle(
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
@@ -148,7 +151,7 @@ namespace Robust.Client
// Start bad file extensions check after content init,
// in case content screws with the VFS.
var checkBadExtensions = ProgramShared.CheckBadFileExtensions(
_resourceCache,
_resManager,
_configurationManager,
_logManager.GetSawmill("res"));
@@ -287,78 +290,6 @@ namespace Robust.Client
return true;
}
private ResourceManifestData LoadResourceManifest()
{
// Parses /manifest.yml for game-specific settings that cannot be exclusively set up by content code.
if (!_resourceCache.TryContentFileRead("/manifest.yml", out var stream))
return ResourceManifestData.Default;
var yamlStream = new YamlStream();
using (stream)
{
using var streamReader = new StreamReader(stream, EncodingHelpers.UTF8);
yamlStream.Load(streamReader);
}
if (yamlStream.Documents.Count == 0)
return ResourceManifestData.Default;
if (yamlStream.Documents.Count != 1 || yamlStream.Documents[0].RootNode is not YamlMappingNode mapping)
{
throw new InvalidOperationException(
"Expected a single YAML document with root mapping for /manifest.yml");
}
var modules = ReadStringArray(mapping, "modules") ?? Array.Empty<string>();
string? assemblyPrefix = null;
if (mapping.TryGetNode("assemblyPrefix", out var prefixNode))
assemblyPrefix = prefixNode.AsString();
string? defaultWindowTitle = null;
if (mapping.TryGetNode("defaultWindowTitle", out var winTitleNode))
defaultWindowTitle = winTitleNode.AsString();
string? windowIconSet = null;
if (mapping.TryGetNode("windowIconSet", out var iconSetNode))
windowIconSet = iconSetNode.AsString();
string? splashLogo = null;
if (mapping.TryGetNode("splashLogo", out var splashNode))
splashLogo = splashNode.AsString();
bool autoConnect = true;
if (mapping.TryGetNode("autoConnect", out var autoConnectNode))
autoConnect = autoConnectNode.AsBool();
var clientAssemblies = ReadStringArray(mapping, "clientAssemblies");
return new ResourceManifestData(
modules,
assemblyPrefix,
defaultWindowTitle,
windowIconSet,
splashLogo,
autoConnect,
clientAssemblies
);
static string[]? ReadStringArray(YamlMappingNode mapping, string key)
{
if (!mapping.TryGetNode(key, out var node))
return null;
var sequence = (YamlSequenceNode)node;
var array = new string[sequence.Children.Count];
for (var i = 0; i < array.Length; i++)
{
array[i] = sequence[i].AsString();
}
return array;
}
}
internal bool StartupSystemSplash(
GameControllerOptions options,
Func<ILogHandler>? logHandlerFactory,
@@ -432,13 +363,13 @@ namespace Robust.Client
_parallelMgr.Initialize();
_prof.Initialize();
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
: Options.MountOptions;
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
ProgramShared.DoMounts(_resManager, mountOptions, Options.ContentBuildDirectory,
Options.AssemblyDirectory,
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
@@ -448,16 +379,16 @@ namespace Robust.Client
{
foreach (var (api, prefix) in mounts)
{
_resourceCache.MountLoaderApi(api, "", new(prefix));
_resourceCache.MountLoaderApi(_resManager, api, "", new(prefix));
}
}
_stringSerializer.EnableCaching = false;
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
_resourceCache.MountLoaderApi(_resManager, _loaderArgs.FileApi, "Resources/");
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_resourceManifest = LoadResourceManifest();
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resManager);
{
// Handle GameControllerOptions implicit CVar overrides.
@@ -639,11 +570,6 @@ namespace Robust.Client
}
}
using (_prof.Group("ClydeAudio"))
{
_clydeAudio.FrameProcess(frameEventArgs);
}
using (_prof.Group("Clyde"))
{
_clyde.FrameProcess(frameEventArgs);
@@ -704,7 +630,6 @@ namespace Robust.Client
logManager.GetSawmill("ogl.debug.other").Level = LogLevel.Warning;
logManager.GetSawmill("gdparse").Level = LogLevel.Error;
logManager.GetSawmill("discord").Level = LogLevel.Warning;
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
logManager.GetSawmill("szr").Level = LogLevel.Info;
logManager.GetSawmill("loc").Level = LogLevel.Warning;
@@ -783,21 +708,7 @@ namespace Robust.Client
internal void CleanupWindowThread()
{
_clyde.Shutdown();
_clydeAudio.Shutdown();
}
private sealed record ResourceManifestData(
string[] Modules,
string? AssemblyPrefix,
string? DefaultWindowTitle,
string? WindowIconSet,
string? SplashLogo,
bool AutoConnect,
string[]? ClientAssemblies
)
{
public static readonly ResourceManifestData Default =
new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true, null);
_audio.Shutdown();
}
public event Action<FrameEventArgs>? TickUpdateOverride;

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Prometheus;
using Robust.Client.GameStates;
using Robust.Client.Player;
@@ -86,11 +85,17 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public override void Dirty(EntityUid uid, Component component, MetaDataComponent? meta = null)
public override void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null)
{
Dirty(new Entity<IComponent>(uid, component), meta);
}
/// <inheritdoc />
public override void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null)
{
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(uid, component, meta);
base.Dirty(ent, meta);
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
@@ -165,7 +170,7 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel? channel)
{
throw new NotSupportedException();
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using Robust.Client.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using static Robust.Client.Animations.AnimationPlaybackShared;
namespace Robust.Client.GameObjects
@@ -21,42 +19,5 @@ namespace Robust.Client.GameObjects
= new();
internal bool HasPlayingAnimation = false;
/// <summary>
/// Start playing an animation.
/// </summary>
/// <param name="animation">The animation to play.</param>
/// <param name="key">
/// The key for this animation play. This key can be used to stop playback short later.
/// </param>
[Obsolete("Use AnimationPlayerSystem.Play() instead")]
public void Play(Animation animation, string key)
{
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AnimationPlayerSystem>().AddComponent(this);
var playback = new AnimationPlayback(animation);
PlayingAnimations.Add(key, playback);
}
[Obsolete("Use AnimationPlayerSystem.HasRunningAnimation() instead")]
public bool HasRunningAnimation(string key)
{
return PlayingAnimations.ContainsKey(key);
}
[Obsolete("Use AnimationPlayerSystem.Stop() instead")]
public void Stop(string key)
{
PlayingAnimations.Remove(key);
}
[Obsolete("Temporary method until the event is replaced with eventbus")]
internal void AnimationComplete(string key)
{
AnimationCompleted?.Invoke(key);
}
[Obsolete("Use AnimationCompletedEvent instead")]
public event Action<string>? AnimationCompleted;
}
}

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 IResourceCache resourceCache = default!;
[Dependency] private readonly IClientResourceCache 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(IResourceCache cache)
internal static RSI.State GetFallbackState(IClientResourceCache 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, IResourceCache resourceCache)
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IClientResourceCache resourceCache)
{
return GetPrototypeTextures(prototype, resourceCache, out var _);
}
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IClientResourceCache 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, IResourceCache resourceCache)
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IClientResourceCache resourceCache)
{
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))

View File

@@ -1,22 +1,19 @@
using System;
using System.Collections.Generic;
using Robust.Client.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
public sealed class AnimationPlayerSystem : EntitySystem, IPostInjectInit
public sealed class AnimationPlayerSystem : EntitySystem
{
private readonly List<AnimationPlayerComponent> _activeAnimations = new();
private readonly List<Entity<AnimationPlayerComponent>> _activeAnimations = new();
private EntityQuery<MetaDataComponent> _metaQuery;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
@@ -38,22 +35,22 @@ namespace Robust.Client.GameObjects
continue;
}
if (!Update(uid, anim, frameTime))
if (!Update(uid, anim.Comp, frameTime))
{
continue;
}
_activeAnimations.RemoveSwap(i);
i--;
anim.HasPlayingAnimation = false;
anim.Comp.HasPlayingAnimation = false;
}
}
internal void AddComponent(AnimationPlayerComponent component)
internal void AddComponent(Entity<AnimationPlayerComponent> ent)
{
if (component.HasPlayingAnimation) return;
_activeAnimations.Add(component);
component.HasPlayingAnimation = true;
if (ent.Comp.HasPlayingAnimation) return;
_activeAnimations.Add(ent);
ent.Comp.HasPlayingAnimation = true;
}
private bool Update(EntityUid uid, AnimationPlayerComponent component, float frameTime)
@@ -78,7 +75,6 @@ namespace Robust.Client.GameObjects
{
component.PlayingAnimations.Remove(key);
EntityManager.EventBus.RaiseLocalEvent(uid, new AnimationCompletedEvent {Uid = uid, Key = key}, true);
component.AnimationComplete(key);
}
return false;
@@ -89,22 +85,29 @@ namespace Robust.Client.GameObjects
/// </summary>
public void Play(EntityUid uid, Animation animation, string key)
{
var component = EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(component, animation, key);
var component = EnsureComp<AnimationPlayerComponent>(uid);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key)
{
component ??= EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(component, animation, key);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
/// <summary>
/// Start playing an animation.
/// </summary>
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(AnimationPlayerComponent component, Animation animation, string key)
{
AddComponent(component);
Play(new Entity<AnimationPlayerComponent>(component.Owner, component), animation, key);
}
public void Play(Entity<AnimationPlayerComponent> ent, Animation animation, string key)
{
AddComponent(ent);
var playback = new AnimationPlaybackShared.AnimationPlayback(animation);
#if DEBUG
@@ -116,18 +119,18 @@ namespace Robust.Client.GameObjects
if (compTrack.ComponentType == null)
{
_sawmill.Error("Attempted to play a component animation without any component specified.");
Log.Error("Attempted to play a component animation without any component specified.");
return;
}
if (!EntityManager.TryGetComponent(component.Owner, compTrack.ComponentType, out var animatedComp))
if (!EntityManager.TryGetComponent(ent, compTrack.ComponentType, out var animatedComp))
{
_sawmill.Error(
$"Attempted to play a component animation, but the entity {ToPrettyString(component.Owner)} does not have the component to be animated: {compTrack.ComponentType}.");
Log.Error(
$"Attempted to play a component animation, but the entity {ToPrettyString(ent)} does not have the component to be animated: {compTrack.ComponentType}.");
return;
}
if (IsClientSide(component.Owner) || !animatedComp.NetSyncEnabled)
if (IsClientSide(ent) || !animatedComp.NetSyncEnabled)
continue;
var reg = _compFact.GetRegistration(animatedComp);
@@ -140,13 +143,13 @@ namespace Robust.Client.GameObjects
if (animatedComp.GetType().GetProperty(compTrack.Property) is { } property &&
property.HasCustomAttribute<AutoNetworkedFieldAttribute>())
{
_sawmill.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(component.Owner)}");
Log.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(ent)}");
}
}
}
#endif
component.PlayingAnimations.Add(key, playback);
ent.Comp.PlayingAnimations.Add(key, playback);
}
public bool HasRunningAnimation(EntityUid uid, string key)
@@ -175,19 +178,18 @@ namespace Robust.Client.GameObjects
public void Stop(EntityUid uid, string key)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var player)) return;
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
return;
player.PlayingAnimations.Remove(key);
}
public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key)
{
if (!Resolve(uid, ref component, false)) return;
component.PlayingAnimations.Remove(key);
}
if (!Resolve(uid, ref component, false))
return;
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("anim");
component.PlayingAnimations.Remove(key);
}
}

View File

@@ -1,627 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Players;
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,7 +1,7 @@
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
@@ -15,8 +15,8 @@ public sealed class EyeSystem : SharedEyeSystem
{
base.Initialize();
SubscribeLocalEvent<EyeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<EyeComponent, PlayerDetachedEvent>(OnEyeDetached);
SubscribeLocalEvent<EyeComponent, PlayerAttachedEvent>(OnEyeAttached);
SubscribeLocalEvent<EyeComponent, LocalPlayerDetachedEvent>(OnEyeDetached);
SubscribeLocalEvent<EyeComponent, LocalPlayerAttachedEvent>(OnEyeAttached);
SubscribeLocalEvent<EyeComponent, AfterAutoHandleStateEvent>(OnEyeAutoState);
// Make sure this runs *after* entities have been moved by interpolation and movement.
@@ -29,7 +29,7 @@ public sealed class EyeSystem : SharedEyeSystem
UpdateEye(component);
}
private void OnEyeAttached(EntityUid uid, EyeComponent component, PlayerAttachedEvent args)
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
{
// TODO: This probably shouldn't be nullable bruv.
if (component._eye != null)
@@ -41,7 +41,7 @@ public sealed class EyeSystem : SharedEyeSystem
RaiseLocalEvent(uid, ref ev, true);
}
private void OnEyeDetached(EntityUid uid, EyeComponent component, PlayerDetachedEvent args)
private void OnEyeDetached(EntityUid uid, EyeComponent component, LocalPlayerDetachedEvent args)
{
_eyeManager.ClearCurrentEye();
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -58,6 +59,8 @@ namespace Robust.Client.GameObjects
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private List<Entity<MapGridComponent>> _grids = new();
public GridChunkBoundsOverlay(IEntityManager entManager, IEyeManager eyeManager, IMapManager mapManager)
{
_entityManager = entManager;
@@ -71,13 +74,15 @@ namespace Robust.Client.GameObjects
var viewport = args.WorldBounds;
var worldHandle = args.WorldHandle;
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
_grids.Clear();
_mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids);
foreach (var grid in _grids)
{
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid.Owner).WorldMatrix;
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid).WorldMatrix;
worldHandle.SetTransform(worldMatrix);
var transform = new Transform(Vector2.Zero, Angle.Zero);
var chunkEnumerator = grid.GetMapChunks(viewport);
var chunkEnumerator = grid.Comp.GetMapChunks(viewport);
while (chunkEnumerator.MoveNext(out var chunk))
{

View File

@@ -3,16 +3,13 @@ using System.Numerics;
using Robust.Client.GameStates;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -131,7 +128,7 @@ namespace Robust.Client.GameObjects
public override void Initialize()
{
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttachedEntityChanged);
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
@@ -171,11 +168,11 @@ namespace Robust.Client.GameObjects
HandleInputCommand(localPlayer.Session, keyFunction, message);
}
private void OnAttachedEntityChanged(PlayerAttachSysMessage message)
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
{
if (message.AttachedEntity != default) // attach
if (message.Entity != default) // attach
{
SetEntityContextActive(_inputManager, message.AttachedEntity);
SetEntityContextActive(_inputManager, message.Entity);
}
else // detach
{
@@ -227,44 +224,4 @@ namespace Robust.Client.GameObjects
_sawmillInputContext = _logManager.GetSawmill("input.context");
}
}
/// <summary>
/// Entity system message that is raised when the player changes attached entities.
/// </summary>
public sealed class PlayerAttachSysMessage : EntityEventArgs
{
/// <summary>
/// New entity the player is attached to.
/// </summary>
public EntityUid AttachedEntity { get; }
/// <summary>
/// Creates a new instance of <see cref="PlayerAttachSysMessage"/>.
/// </summary>
/// <param name="attachedEntity">New entity the player is attached to.</param>
public PlayerAttachSysMessage(EntityUid attachedEntity)
{
AttachedEntity = attachedEntity;
}
}
public sealed class PlayerAttachedEvent : EntityEventArgs
{
public PlayerAttachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
public sealed class PlayerDetachedEvent : EntityEventArgs
{
public PlayerDetachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
}

View File

@@ -14,7 +14,7 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly IClientResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
public override void Initialize()

View File

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

View File

@@ -1,13 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -15,6 +18,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects
@@ -28,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 IResourceCache _resourceCache = default!;
[Dependency] private readonly IClientResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
@@ -183,6 +187,48 @@ namespace Robust.Client.GameObjects
{
_queuedFrameUpdate.Add(uid);
}
/// <summary>
/// Gets the specified frame for this sprite at the specified time.
/// </summary>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime)
{
Texture? sprite = null;
switch (spriteSpec)
{
case SpriteSpecifier.Rsi rsi:
var rsiActual = _resourceCache.GetResource<RSIResource>(rsi.RsiPath).RSI;
rsiActual.TryGetState(rsi.RsiState, out var state);
var frames = state!.GetFrames(RsiDirection.South);
var delays = state.GetDelays();
var totalDelay = delays.Sum();
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
for (var i = 0; i < delays.Length; i++)
{
var delay = delays[i];
delaySum += delay;
if (time > delaySum)
continue;
sprite = frames[i];
break;
}
sprite ??= Frame0(spriteSpec);
break;
case SpriteSpecifier.Texture texture:
sprite = texture.GetTexture(_resourceCache);
break;
default:
throw new NotImplementedException();
}
return sprite;
}
}
/// <summary>

View File

@@ -28,11 +28,11 @@ namespace Robust.Client.GameObjects
// Only keep track of transforms actively lerping.
// Much faster than iterating 3000+ transforms every frame.
[ViewVariables] private readonly List<TransformComponent> _lerpingTransforms = new();
[ViewVariables] private readonly List<Entity<TransformComponent>> _lerpingTransforms = new();
public void Reset()
{
foreach (var xform in _lerpingTransforms)
foreach (var (_, xform) in _lerpingTransforms)
{
xform.ActivelyLerping = false;
xform.NextPosition = null;
@@ -77,7 +77,7 @@ namespace Robust.Client.GameObjects
return;
}
_lerpingTransforms.Add(xform);
_lerpingTransforms.Add((uid, xform));
xform.ActivelyLerping = true;
xform.PredictedLerp = false;
xform.LerpParent = xform.ParentUid;
@@ -96,7 +96,7 @@ namespace Robust.Client.GameObjects
if (!xform.ActivelyLerping)
{
_lerpingTransforms.Add(xform);
_lerpingTransforms.Add((uid, xform));
xform.ActivelyLerping = true;
xform.PredictedLerp = true;
xform.PrevRotation = xform._localRotation;
@@ -123,8 +123,7 @@ namespace Robust.Client.GameObjects
for (var i = 0; i < _lerpingTransforms.Count; i++)
{
var transform = _lerpingTransforms[i];
var uid = transform.Owner;
var (uid, transform) = _lerpingTransforms[i];
var found = false;
// Only lerp if parent didn't change.

View File

@@ -26,8 +26,6 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Profiling;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
@@ -37,7 +35,7 @@ namespace Robust.Client.GameStates
{
/// <inheritdoc />
[UsedImplicitly]
public sealed class ClientGameStateManager : IClientGameStateManager, IPostInjectInit
public sealed class ClientGameStateManager : IClientGameStateManager
{
private GameStateProcessor _processor = default!;
@@ -55,7 +53,7 @@ namespace Robust.Client.GameStates
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
private readonly HashSet<NetEntity> _stateEnts = new();
private readonly List<EntityUid> _toDelete = new();
private readonly List<Component> _toRemove = new();
private readonly List<IComponent> _toRemove = new();
private readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _outputData = new();
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
@@ -82,6 +80,13 @@ namespace Robust.Client.GameStates
private ISawmill _sawmill = default!;
/// <summary>
/// If we are waiting for a full game state from the server, we will automatically re-send full state requests
/// if they do not arrive in time. Ideally this should never happen, this here just in case a client gets
/// stuck waiting for a full state that the server doesn't know the client even wants.
/// </summary>
public static readonly TimeSpan FullStateTimeout = TimeSpan.FromSeconds(10);
/// <inheritdoc />
public int MinBufferSize => _processor.MinBufferSize;
@@ -89,7 +94,8 @@ namespace Robust.Client.GameStates
public int TargetBufferSize => _processor.TargetBufferSize;
/// <inheritdoc />
public int CurrentBufferSize => _processor.CalculateBufferSize(_timing.LastRealTick);
public int GetApplicableStateCount() => _processor.GetApplicableStateCount();
public int StateCount => _processor.StateCount;
public bool IsPredictionEnabled { get; private set; }
public bool PredictionNeedsResetting { get; private set; }
@@ -121,7 +127,10 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
public void Initialize()
{
_processor = new GameStateProcessor(_timing);
_sawmill = _logMan.GetSawmill("state");
_sawmill.Level = LogLevel.Info;
_processor = new GameStateProcessor(this, _timing, _sawmill);
_network.RegisterNetMessage<MsgState>(HandleStateMessage);
_network.RegisterNetMessage<MsgStateLeavePvs>(HandlePvsLeaveMessage);
@@ -245,9 +254,19 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
public void ApplyGameState()
{
// If we have been waiting for a full state for a long time, re-request a full state.
if (_processor.WaitingForFull
&& _processor.LastFullStateRequested is {} last
&& DateTime.UtcNow - last.Time > FullStateTimeout)
{
// Re-request a full state.
// We use the previous from-tick, just in case the full state is already on the way,
RequestFullState(null, last.Tick);
}
// Calculate how many states we need to apply this tick.
// Always at least one, but can be more based on StateBufferMergeThreshold.
var curBufSize = CurrentBufferSize;
var curBufSize = GetApplicableStateCount();
var targetBufSize = TargetBufferSize;
var bufferOverflow = curBufSize - targetBufSize - StateBufferMergeThreshold;
@@ -300,9 +319,9 @@ namespace Robust.Client.GameStates
}
// If we were waiting for a new state, we are now applying it.
if (_processor.LastFullStateRequested.HasValue)
if (_processor.WaitingForFull)
{
_processor.LastFullStateRequested = null;
_processor.OnFullStateReceived();
_timing.LastProcessedTick = curState.ToSequence;
DebugTools.Assert(curState.FromSequence == GameTick.Zero);
PartialStateReset(curState, true);
@@ -367,7 +386,7 @@ namespace Robust.Client.GameStates
if (_processor.WaitingForFull)
_timing.TickTimingAdjustment = 0f;
else
_timing.TickTimingAdjustment = (CurrentBufferSize - (float)TargetBufferSize) * 0.10f;
_timing.TickTimingAdjustment = (GetApplicableStateCount() - (float)TargetBufferSize) * 0.10f;
// If we are about to process an another tick in the same frame, lets not bother unnecessarily running prediction ticks
// Really the main-loop ticking just needs to be more specialized for clients.
@@ -412,11 +431,11 @@ namespace Robust.Client.GameStates
}
}
public void RequestFullState(NetEntity? missingEntity = null)
public void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null)
{
_sawmill.Info("Requesting full server state");
_network.ClientSendMessage(new MsgStateRequestFull { Tick = _timing.LastRealTick , MissingEntity = missingEntity ?? NetEntity.Invalid });
_processor.RequestFullState();
_processor.OnFullStateRequested(tick ?? _timing.LastRealTick);
}
public void PredictTicks(GameTick predictionTarget)
@@ -499,7 +518,7 @@ namespace Robust.Client.GameStates
var countReset = 0;
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
RemQueue<Component> toRemove = new();
RemQueue<IComponent> toRemove = new();
foreach (var entity in system.DirtyEntities)
{
@@ -604,7 +623,7 @@ namespace Robust.Client.GameStates
/// Whenever a new entity is created, the server doesn't send full state data, given that much of the data
/// can simply be obtained from the entity prototype information. This function basically creates a fake
/// initial server state for any newly created entity. It does this by simply using the standard <see
/// cref="IEntityManager.GetComponentState(IEventBus, IComponent)"/>.
/// cref="IEntityManager.GetComponentState"/>.
/// </remarks>
private void MergeImplicitData(IEnumerable<NetEntity> createdEntities)
{
@@ -670,7 +689,7 @@ namespace Robust.Client.GameStates
using (_prof.Group("Player"))
{
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<SessionState>());
}
using (_prof.Group("Callback"))
@@ -1187,8 +1206,7 @@ namespace Robust.Client.GameStates
{
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = (Component) _compFactory.GetComponent(id);
comp.Owner = uid;
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, metadata: meta);
}
@@ -1201,8 +1219,7 @@ namespace Robust.Client.GameStates
{
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
{
comp = (Component) _compFactory.GetComponent(compChange.NetID);
comp.Owner = uid;
comp = _compFactory.GetComponent(compChange.NetID);
_entityManager.AddComponent(uid, comp, true, metadata:meta);
}
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
@@ -1272,7 +1289,9 @@ namespace Robust.Client.GameStates
var handleState = new ComponentHandleState(cur, next);
bus.RaiseComponentEvent(comp, ref handleState);
}
#pragma warning disable CS0168 // Variable is declared but never used
catch (Exception e)
#pragma warning restore CS0168 // Variable is declared but never used
{
#if EXCEPTION_TOLERANCE
_sawmill.Error($"Failed to apply comp state: entity={_entities.ToPrettyString(uid)}, comp={comp.GetType()}");
@@ -1428,8 +1447,7 @@ namespace Robust.Client.GameStates
{
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = (Component) _compFactory.GetComponent(id);
comp.Owner = uid;
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, meta);
}
@@ -1455,11 +1473,6 @@ namespace Robust.Client.GameStates
public bool IsQueuedForDetach(NetEntity entity)
=> _processor.IsQueuedForDetach(entity);
void IPostInjectInit.PostInject()
{
_sawmill = _logMan.GetSawmill(CVars.NetPredict.Name);
}
}
public sealed class GameStateAppliedArgs : EventArgs

View File

@@ -1,44 +1,31 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor, IPostInjectInit
internal sealed class GameStateProcessor : IGameStateProcessor
{
[Dependency] private ILogManager _logMan = default!;
public const int MaxBufferSize = 512;
private readonly IClientGameTiming _timing;
private readonly IClientGameStateManager _state;
private readonly ISawmill _logger;
private readonly List<GameState> _stateBuffer = new();
private readonly Dictionary<GameTick, List<NetEntity>> _pvsDetachMessages = new();
private ISawmill _logger = default!;
private ISawmill _stateLogger = default!;
public GameState? LastFullState { get; private set; }
public bool WaitingForFull => LastFullStateRequested.HasValue;
public GameTick? LastFullStateRequested
{
get => _lastFullStateRequested;
set
{
_lastFullStateRequested = value;
LastFullState = null;
}
}
public GameTick? _lastFullStateRequested = GameTick.Zero;
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
private int _bufferSize;
@@ -71,9 +58,12 @@ namespace Robust.Client.GameStates
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
/// </summary>
/// <param name="timing">Timing information of the current state.</param>
public GameStateProcessor(IClientGameTiming timing)
/// <param name="clientGameStateManager"></param>
public GameStateProcessor(IClientGameStateManager state, IClientGameTiming timing, ISawmill logger)
{
_timing = timing;
_state = state;
_logger = logger;
}
/// <inheritdoc />
@@ -83,7 +73,7 @@ namespace Robust.Client.GameStates
if (state.ToSequence <= _timing.LastRealTick)
{
if (Logging)
_stateLogger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -95,7 +85,7 @@ namespace Robust.Client.GameStates
continue;
if (Logging)
_stateLogger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -104,13 +94,13 @@ namespace Robust.Client.GameStates
if (!WaitingForFull)
{
// This is a good state that we will be using.
_stateBuffer.Add(state);
TryAdd(state);
if (Logging)
_stateLogger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return true;
}
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value.Tick)
{
LastFullState = state;
@@ -128,10 +118,44 @@ namespace Robust.Client.GameStates
return false;
}
_stateBuffer.Add(state);
TryAdd(state);
return true;
}
public void TryAdd(GameState state)
{
if (_stateBuffer.Count <= MaxBufferSize)
{
_stateBuffer.Add(state);
return;
}
// This can happen if a required state gets dropped somehow and the client keeps receiving future
// game states that they can't apply. I.e., GetApplicableStateCount() is zero, even though there are many
// states in the list.
//
// This can seemingly happen when the server sends ""reliable"" game states while the client is paused?
// For example, when debugging the client, while the server is running:
// - The client stops sending acks for states that the server sends out.
// - Thus the client will exceed the net.force_ack_threshold cvar
// - The server starts sending some packets ""reliably"" and just force updates the clients last ack.
//
// What should happen is that when the client resumes, it receives the reliably sent states and can just
// resume. However, even though the packets are sent ""reliably"", they just seem to get dropped.
// I don't quite understand how/why yet, but this ensures the client doesn't get stuck.
#if FULL_RELEASE
_logger.Warning(@$"Exceeded maximum state buffer size!
Tick: {_timing.CurTick}/{_timing.LastProcessedTick}/{_timing.LastRealTick}
Size: {_stateBuffer.Count}
Applicable states: {GetApplicableStateCount()}
Was waiting for full: {WaitingForFull} {LastFullStateRequested}
Had full state: {LastFullState != null}"
);
#endif
_state.RequestFullState();
}
/// <summary>
/// Attempts to get the current and next states to apply.
/// </summary>
@@ -152,7 +176,7 @@ namespace Robust.Client.GameStates
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
if (Logging)
_stateLogger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
_logger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
return applyNextState;
@@ -344,14 +368,20 @@ namespace Robust.Client.GameStates
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = GameTick.Zero;
LastFullStateRequested = (GameTick.Zero, DateTime.MaxValue);
}
public void RequestFullState()
public void OnFullStateRequested(GameTick tick)
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = _timing.LastRealTick;
LastFullStateRequested = (tick, DateTime.UtcNow);
}
public void OnFullStateReceived()
{
LastFullState = null;
LastFullStateRequested = null;
}
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> implicitData)
@@ -416,10 +446,11 @@ namespace Robust.Client.GameStates
return false;
}
public int CalculateBufferSize(GameTick fromTick)
public int GetApplicableStateCount(GameTick? fromTick = null)
{
fromTick ??= _timing.LastRealTick;
bool foundState;
var nextTick = fromTick;
var nextTick = fromTick.Value;
do
{
@@ -437,13 +468,9 @@ namespace Robust.Client.GameStates
}
while (foundState);
return (int) (nextTick.Value - fromTick.Value);
return (int) (nextTick.Value - fromTick.Value.Value);
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("net");
_stateLogger = _logMan.GetSawmill("net.state");
}
public int StateCount => _stateBuffer.Count;
}
}

View File

@@ -32,7 +32,15 @@ namespace Robust.Client.GameStates
/// <summary>
/// Number of applicable game states currently in the state buffer.
/// </summary>
int CurrentBufferSize { get; }
int GetApplicableStateCount();
[Obsolete("use GetApplicableStateCount()")]
int CurrentBufferSize => GetApplicableStateCount();
/// <summary>
/// Total number of game states currently in the state buffer.
/// </summary>
int StateCount { get; }
/// <summary>
/// If the buffer size is this many states larger than the target buffer size,
@@ -91,7 +99,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Requests a full state from the server. This should override even implicit entity data.
/// </summary>
void RequestFullState(NetEntity? missingEntity = null);
void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null);
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;

View File

@@ -96,7 +96,7 @@ namespace Robust.Client.GameStates
/// This includes only applicable states. If there is a gap, future buffers are not included.
/// </summary>
/// <param name="fromTick">The tick to calculate from.</param>
int CalculateBufferSize(GameTick fromTick);
int GetApplicableStateCount(GameTick? fromTick);
bool TryGetLastServerStates(NetEntity entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);

View File

@@ -40,7 +40,7 @@ namespace Robust.Client.GameStates
public NetEntityOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
var cache = IoCManager.Resolve<IClientResourceCache>();
_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<IResourceCache>();
var cache = IoCManager.Resolve<IClientResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
_gameStateManager.GameStateApplied += HandleGameStateApplied;
@@ -68,7 +68,7 @@ namespace Robust.Client.GameStates
var lag = _netManager.ServerChannel!.Ping;
// calc interp info
var buffer = _gameStateManager.CurrentBufferSize;
var buffer = _gameStateManager.GetApplicableStateCount();
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, buffer));
@@ -268,7 +268,7 @@ namespace Robust.Client.GameStates
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
// buffer text
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.GetApplicableStateCount().ToString()} states");
}
protected override void DisposeBehavior()

View File

@@ -1,59 +0,0 @@
using System.Collections.Concurrent;
using OpenTK.Audio.OpenAL;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
// Used to track audio sources that were disposed in the finalizer thread,
// so we need to properly send them off in the main thread.
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _sourceDisposeQueue = new();
private readonly ConcurrentQueue<(int sourceHandle, int filterHandle)> _bufferedSourceDisposeQueue = new();
private readonly ConcurrentQueue<int> _bufferDisposeQueue = new();
private void _flushALDisposeQueues()
{
// Clear out finalized audio sources.
while (_sourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_audioSources.Remove(handles.sourceHandle);
}
// Clear out finalized buffered audio sources.
while (_bufferedSourceDisposeQueue.TryDequeue(out var handles))
{
OpenALSawmill.Debug("Cleaning out buffered source {0} which finalized in another thread.", handles.sourceHandle);
if (IsEfxSupported) RemoveEfx(handles);
AL.DeleteSource(handles.sourceHandle);
_checkAlError();
_bufferedAudioSources.Remove(handles.sourceHandle);
}
// Clear out finalized audio buffers.
while (_bufferDisposeQueue.TryDequeue(out var handle))
{
AL.DeleteBuffer(handle);
_checkAlError();
}
}
private void DeleteSourceOnMainThread(int sourceHandle, int filterHandle)
{
_sourceDisposeQueue.Enqueue((sourceHandle, filterHandle));
}
private void DeleteBufferedSourceOnMainThread(int bufferedSourceHandle, int filterHandle)
{
_bufferedSourceDisposeQueue.Enqueue((bufferedSourceHandle, filterHandle));
}
private void DeleteAudioBufferOnMainThread(int bufferHandle)
{
_bufferDisposeQueue.Enqueue(bufferHandle);
}
}
}

View File

@@ -1,680 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Vector2 = System.Numerics.Vector2;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
private sealed class AudioSource : IClydeAudioSource
{
private int SourceHandle;
private readonly ClydeAudio _master;
private readonly AudioStream _sourceStream;
private int FilterHandle;
#if DEBUG
private bool _didPositionWarning;
#endif
private float _gain;
private bool IsEfxSupported => _master.IsEfxSupported;
public AudioSource(ClydeAudio master, int sourceHandle, AudioStream sourceStream)
{
_master = master;
SourceHandle = sourceHandle;
_sourceStream = sourceStream;
AL.GetSource(SourceHandle, ALSourcef.Gain, out _gain);
}
public void StartPlaying()
{
_checkDisposed();
AL.SourcePlay(SourceHandle);
_master._checkAlError();
}
public void StopPlaying()
{
if (_isDisposed()) return;
AL.SourceStop(SourceHandle);
_master._checkAlError();
}
public bool IsPlaying
{
get
{
_checkDisposed();
var state = AL.GetSourceState(SourceHandle);
return state == ALSourceState.Playing;
}
}
public bool IsLooping
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourceb.Looping, out var ret);
_master._checkAlError();
return ret;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourceb.Looping, value);
_master._checkAlError();
}
}
public bool IsGlobal
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourceb.SourceRelative, out var value);
_master._checkAlError();
return value;
}
}
public void SetGlobal()
{
_checkDisposed();
AL.Source(SourceHandle, ALSourceb.SourceRelative, true);
_master._checkAlError();
}
public void SetVolume(float decibels)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = MathF.Pow(10, decibels / 10);
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetVolumeDirect(float gain)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = gain;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetMaxDistance(float distance)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.MaxDistance, distance);
_master._checkAlError();
}
public void SetRolloffFactor(float rolloffFactor)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.RolloffFactor, rolloffFactor);
_master._checkAlError();
}
public void SetReferenceDistance(float refDistance)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.ReferenceDistance, refDistance);
_master._checkAlError();
}
public void SetOcclusion(float blocks)
{
_checkDisposed();
var cutoff = MathF.Exp(-blocks * 1);
var gain = MathF.Pow(cutoff, 0.1f);
if (IsEfxSupported)
{
SetOcclusionEfx(gain, cutoff);
}
else
{
gain *= gain * gain;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * gain);
}
_master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle, ALSourcei.EfxDirectFilter, FilterHandle);
}
public void SetPlaybackPosition(float seconds)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.SecOffset, seconds);
_master._checkAlError();
}
public bool SetPosition(Vector2 position)
{
_checkDisposed();
var (x, y) = position;
if (!AreFinite(x, y))
{
return false;
}
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
{
_didPositionWarning = true;
_master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
_sourceStream.Name);
// warning isn't enough, people just ignore it :(
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
}
#endif
AL.Source(SourceHandle, ALSource3f.Position, x, y, 0);
_master._checkAlError();
return true;
}
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
return true;
}
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
_master._checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.Pitch, pitch);
_master._checkAlError();
}
~AudioSource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposing)
{
// We can't run this code inside the finalizer thread so tell Clyde to clear it up later.
_master.DeleteSourceOnMainThread(SourceHandle, FilterHandle);
}
else
{
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle);
_master._audioSources.Remove(SourceHandle);
_master._checkAlError();
}
SourceHandle = -1;
}
private bool _isDisposed()
{
return SourceHandle == -1;
}
private void _checkDisposed()
{
if (SourceHandle == -1)
{
throw new ObjectDisposedException(nameof(AudioSource));
}
}
}
private sealed class BufferedAudioSource : IClydeBufferedAudioSource
{
private int? SourceHandle = null;
private int[] BufferHandles;
private Dictionary<int, int> BufferMap = new();
private readonly ClydeAudio _master;
private bool _mono = true;
private bool _float = false;
private int FilterHandle;
private float _gain;
public int SampleRate { get; set; } = 44100;
private bool IsEfxSupported => _master.IsEfxSupported;
public BufferedAudioSource(ClydeAudio master, int sourceHandle, int[] bufferHandles, bool floatAudio = false)
{
_master = master;
SourceHandle = sourceHandle;
BufferHandles = bufferHandles;
for (int i = 0; i < BufferHandles.Length; i++)
{
var bufferHandle = BufferHandles[i];
BufferMap[bufferHandle] = i;
}
_float = floatAudio;
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
}
public void StartPlaying()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.SourcePlay(stackalloc int[] {SourceHandle!.Value});
_master._checkAlError();
}
public void StopPlaying()
{
if (_isDisposed()) return;
// ReSharper disable once PossibleInvalidOperationException
AL.SourceStop(SourceHandle!.Value);
_master._checkAlError();
}
public bool IsPlaying
{
get
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
var state = AL.GetSourceState(SourceHandle!.Value);
return state == ALSourceState.Playing;
}
}
public bool IsLooping
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public void SetGlobal()
{
_checkDisposed();
_mono = false;
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourceb.SourceRelative, true);
_master._checkAlError();
}
public void SetLooping()
{
// TODO?waaaaddDDDDD
}
public void SetVolume(float decibels)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = MathF.Pow(10, decibels / 10);
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetVolumeDirect(float gain)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = gain;
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
_master._checkAlError();
}
public void SetMaxDistance(float distance)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.MaxDistance, distance);
_master._checkAlError();
}
public void SetRolloffFactor(float rolloffFactor)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.RolloffFactor, rolloffFactor);
_master._checkAlError();
}
public void SetReferenceDistance(float refDistance)
{
_checkDisposed();
AL.Source(SourceHandle!.Value, ALSourcef.ReferenceDistance, refDistance);
_master._checkAlError();
}
public void SetOcclusion(float blocks)
{
_checkDisposed();
var cutoff = MathF.Exp(-blocks * 1.5f);
var gain = MathF.Pow(cutoff, 0.1f);
if (IsEfxSupported)
{
SetOcclusionEfx(gain, cutoff);
}
else
{
gain *= gain * gain;
AL.Source(SourceHandle!.Value, ALSourcef.Gain, gain * _gain);
}
_master._checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
{
FilterHandle = EFX.GenFilter();
EFX.Filter(FilterHandle, FilterInteger.FilterType, (int) FilterType.Lowpass);
}
EFX.Filter(FilterHandle, FilterFloat.LowpassGain, gain);
EFX.Filter(FilterHandle, FilterFloat.LowpassGainHF, cutoff);
AL.Source(SourceHandle!.Value, ALSourcei.EfxDirectFilter, FilterHandle);
}
public void SetPlaybackPosition(float seconds)
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourcef.SecOffset, seconds);
_master._checkAlError();
}
public bool IsGlobal
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle!.Value, ALSourceb.SourceRelative, out var value);
_master._checkAlError();
return value;
}
}
public bool SetPosition(Vector2 position)
{
_checkDisposed();
var (x, y) = position;
if (!AreFinite(x, y))
{
return false;
}
_mono = true;
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSource3f.Position, x, y, 0);
_master._checkAlError();
return true;
}
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
return true;
}
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle!.Value, ALSource3f.Velocity, x, y, 0);
_master._checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.Source(SourceHandle!.Value, ALSourcef.Pitch, pitch);
_master._checkAlError();
}
~BufferedAudioSource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (SourceHandle == null) return;
if (!_master.IsMainThread())
{
// We can't run this code inside another thread so tell Clyde to clear it up later.
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);
for (var i = 0; i < BufferHandles.Length; i++)
_master.DeleteAudioBufferOnMainThread(BufferHandles[i]);
}
else
{
if (FilterHandle != 0) EFX.DeleteFilter(FilterHandle);
AL.DeleteSource(SourceHandle.Value);
AL.DeleteBuffers(BufferHandles);
_master._bufferedAudioSources.Remove(SourceHandle.Value);
_master._checkAlError();
}
SourceHandle = null;
}
private bool _isDisposed()
{
return SourceHandle == null;
}
private void _checkDisposed()
{
if (SourceHandle == null)
{
throw new ObjectDisposedException(nameof(AudioSource));
}
}
public int GetNumberOfBuffersProcessed()
{
_checkDisposed();
// ReSharper disable once PossibleInvalidOperationException
AL.GetSource(SourceHandle!.Value, ALGetSourcei.BuffersProcessed, out var buffersProcessed);
return buffersProcessed;
}
public unsafe void GetBuffersProcessed(Span<int> handles)
{
_checkDisposed();
var entries = Math.Min(Math.Min(handles.Length, BufferHandles.Length), GetNumberOfBuffersProcessed());
fixed (int* ptr = handles)
// ReSharper disable once PossibleInvalidOperationException
AL.SourceUnqueueBuffers(SourceHandle!.Value, entries, ptr);
for (var i = 0; i < entries; i++)
handles[i] = BufferMap[handles[i]];
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
_checkDisposed();
if(_float)
throw new InvalidOperationException("Can't write ushort numbers to buffers when buffer type is float!");
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
fixed (ushort* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.Mono16 : ALFormat.Stereo16, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(ushort) : data.Length * sizeof(ushort), SampleRate);
}
}
public unsafe void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
_checkDisposed();
if(!_float)
throw new InvalidOperationException("Can't write float numbers to buffers when buffer type is ushort!");
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handle),
$"Got {handle}. Expected less than {BufferHandles.Length}");
fixed (float* ptr = data)
{
AL.BufferData(BufferHandles[handle], _mono ? ALFormat.MonoFloat32Ext : ALFormat.StereoFloat32Ext, (IntPtr) ptr,
_mono ? data.Length / 2 * sizeof(float) : data.Length * sizeof(float), SampleRate);
}
}
public unsafe void QueueBuffers(ReadOnlySpan<int> handles)
{
_checkDisposed();
Span<int> realHandles = stackalloc int[handles.Length];
handles.CopyTo(realHandles);
for (var i = 0; i < realHandles.Length; i++)
{
var handle = realHandles[i];
if (handle >= BufferHandles.Length)
throw new ArgumentOutOfRangeException(nameof(handles), $"Invalid handle with index {i}!");
realHandles[i] = BufferHandles[handle];
}
fixed (int* ptr = realHandles)
// ReSharper disable once PossibleInvalidOperationException
AL.SourceQueueBuffers(SourceHandle!.Value, handles.Length, ptr);
}
public unsafe void EmptyBuffers()
{
_checkDisposed();
var length = (SampleRate / BufferHandles.Length) * (_mono ? 1 : 2);
Span<int> handles = stackalloc int[BufferHandles.Length];
if (_float)
{
var empty = new float[length];
var span = (Span<float>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
else
{
var empty = new ushort[length];
var span = (Span<ushort>) empty;
for (var i = 0; i < BufferHandles.Length; i++)
{
WriteBuffer(BufferMap[BufferHandles[i]], span);
handles[i] = BufferMap[BufferHandles[i]];
}
}
QueueBuffers(handles);
}
}
}
}

View File

@@ -1,54 +0,0 @@
using System;
using System.IO;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
private OggVorbisData _readOggVorbis(Stream stream)
{
using (var vorbis = new NVorbis.VorbisReader(stream, false))
{
var sampleRate = vorbis.SampleRate;
var channels = vorbis.Channels;
var totalSamples = vorbis.TotalSamples;
var readSamples = 0;
var buffer = new float[totalSamples * channels];
while (readSamples < totalSamples)
{
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
if (read == 0)
{
break;
}
readSamples += read;
}
return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
}
}
private readonly struct OggVorbisData
{
public readonly long TotalSamples;
public readonly long SampleRate;
public readonly long Channels;
public readonly ReadOnlyMemory<float> Data;
public readonly string Title;
public readonly string Artist;
public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory<float> data, string title, string artist)
{
TotalSamples = totalSamples;
SampleRate = sampleRate;
Channels = channels;
Data = data;
Title = title;
Artist = artist;
}
}
}
}

View File

@@ -1,144 +0,0 @@
using System;
using System.IO;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
/// <summary>
/// Load up a WAVE file.
/// </summary>
private static WavData _readWav(Stream stream)
{
var reader = new BinaryReader(stream, EncodingHelpers.UTF8, true);
void SkipChunk()
{
var length = reader.ReadUInt32();
stream.Position += length;
}
// Read outer most chunks.
Span<byte> fourCc = stackalloc byte[4];
while (true)
{
_readFourCC(reader, fourCc);
if (!fourCc.SequenceEqual("RIFF"u8))
{
SkipChunk();
continue;
}
return _readRiffChunk(reader);
}
}
private static void _skipChunk(BinaryReader reader)
{
var length = reader.ReadUInt32();
reader.BaseStream.Position += length;
}
private static void _readFourCC(BinaryReader reader, Span<byte> fourCc)
{
fourCc[0] = reader.ReadByte();
fourCc[1] = reader.ReadByte();
fourCc[2] = reader.ReadByte();
fourCc[3] = reader.ReadByte();
}
private static WavData _readRiffChunk(BinaryReader reader)
{
Span<byte> format = stackalloc byte[4];
reader.ReadUInt32();
_readFourCC(reader, format);
if (!format.SequenceEqual("WAVE"u8))
{
throw new InvalidDataException("File is not a WAVE file.");
}
_readFourCC(reader, format);
if (!format.SequenceEqual("fmt "u8))
{
throw new InvalidDataException("Expected fmt chunk.");
}
// Read fmt chunk.
var size = reader.ReadInt32();
var afterFmtPos = reader.BaseStream.Position + size;
var audioType = (WavAudioFormatType) reader.ReadInt16();
var channels = reader.ReadInt16();
var sampleRate = reader.ReadInt32();
var byteRate = reader.ReadInt32();
var blockAlign = reader.ReadInt16();
var bitsPerSample = reader.ReadInt16();
if (audioType != WavAudioFormatType.PCM)
{
throw new NotImplementedException("Unable to support audio types other than PCM.");
}
DebugTools.Assert(byteRate == sampleRate * channels * bitsPerSample / 8);
// Fmt is not of guaranteed size, so use the size header to skip to the end.
reader.BaseStream.Position = afterFmtPos;
while (true)
{
_readFourCC(reader, format);
if (!format.SequenceEqual("data"u8))
{
_skipChunk(reader);
continue;
}
break;
}
// We are in the data chunk.
size = reader.ReadInt32();
var data = reader.ReadBytes(size);
return new WavData(audioType, channels, sampleRate, byteRate, blockAlign, bitsPerSample, data);
}
/// <summary>
/// See http://soundfile.sapp.org/doc/WaveFormat/ for reference.
/// </summary>
[PublicAPI]
private readonly struct WavData
{
public readonly WavAudioFormatType AudioType;
public readonly short NumChannels;
public readonly int SampleRate;
public readonly int ByteRate;
public readonly short BlockAlign;
public readonly short BitsPerSample;
public readonly ReadOnlyMemory<byte> Data;
public WavData(WavAudioFormatType audioType, short numChannels, int sampleRate, int byteRate,
short blockAlign, short bitsPerSample, ReadOnlyMemory<byte> data)
{
AudioType = audioType;
NumChannels = numChannels;
SampleRate = sampleRate;
ByteRate = byteRate;
BlockAlign = blockAlign;
BitsPerSample = bitsPerSample;
Data = data;
}
}
private enum WavAudioFormatType : short
{
Unknown = 0,
PCM = 1,
// There's a bunch of other types, those are all unsupported.
}
}
}

View File

@@ -1,51 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Vector2 = System.Numerics.Vector2;
namespace Robust.Client.Graphics.Audio
{
internal partial class ClydeAudio
{
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Robust.Shared.IoC.Dependency] private readonly IEyeManager _eyeManager = default!;
[Robust.Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
private Thread? _gameThread;
public bool InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
return _initializeAudio();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
_updateAudio();
}
public void Shutdown()
{
_shutdownAudio();
}
private bool IsMainThread()
{
return Thread.CurrentThread == _gameThread;
}
}
}

View File

@@ -1,432 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using OpenTK.Mathematics;
using Robust.Client.Audio;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Log;
namespace Robust.Client.Graphics.Audio
{
internal sealed partial class ClydeAudio : IClydeAudio, IClydeAudioInternal
{
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<AudioSource>> _audioSources =
new();
private readonly Dictionary<int, WeakReference<BufferedAudioSource>> _bufferedAudioSources =
new();
private readonly HashSet<string> _alcDeviceExtensions = new();
private readonly HashSet<string> _alContextExtensions = new();
// The base gain value for a listener, used to boost the default volume.
private const float _baseGain = 2f;
public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension);
public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension);
internal bool IsEfxSupported;
internal ISawmill OpenALSawmill = default!;
private bool _initializeAudio()
{
OpenALSawmill = _logMan.GetSawmill("clyde.oal");
if (!_audioOpenDevice())
return false;
// Create OpenAL context.
_audioCreateContext();
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioAttenuation, SetAudioAttenuation, true);
return true;
}
private void _audioCreateContext()
{
unsafe
{
_openALContext = ALC.CreateContext(_openALDevice, (int*) 0);
}
ALC.MakeContextCurrent(_openALContext);
_checkAlcError(_openALDevice);
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}
OpenALSawmill.Debug("OpenAL Vendor: {0}", AL.Get(ALGetString.Vendor));
OpenALSawmill.Debug("OpenAL Renderer: {0}", AL.Get(ALGetString.Renderer));
OpenALSawmill.Debug("OpenAL Version: {0}", AL.Get(ALGetString.Version));
}
private bool _audioOpenDevice()
{
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
{
_openALDevice = ALC.OpenDevice(preferredDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Warning("Unable to open preferred audio device '{0}': {1}. Falling back default.",
preferredDevice, ALC.GetError(ALDevice.Null));
_openALDevice = ALC.OpenDevice(null);
}
}
else
{
_openALDevice = ALC.OpenDevice(null);
}
_checkAlcError(_openALDevice);
if (_openALDevice == IntPtr.Zero)
{
OpenALSawmill.Error("Unable to open OpenAL device! {1}", ALC.GetError(ALDevice.Null));
return false;
}
// Load up ALC extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alcDeviceExtensions.Add(extension);
}
return true;
}
public void StopAllAudio()
{
foreach (var (key, source) in _audioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
}
}
foreach (var (key, source) in _bufferedAudioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
}
}
}
public void DisposeAllAudio()
{
foreach (var (key, source) in _audioSources)
{
if (source.TryGetTarget(out var target))
{
target.Dispose();
}
}
_audioSources.Clear();
foreach (var (key, source) in _bufferedAudioSources)
{
if (source.TryGetTarget(out var target))
{
target.StopPlaying();
target.Dispose();
}
}
_bufferedAudioSources.Clear();
}
private void _shutdownAudio()
{
DisposeAllAudio();
if (_openALContext != ALContext.Null)
{
ALC.MakeContextCurrent(ALContext.Null);
ALC.DestroyContext(_openALContext);
}
if (_openALDevice != IntPtr.Zero)
{
ALC.CloseDevice(_openALDevice);
}
}
private void _updateAudio()
{
var eye = _eyeManager.CurrentEye;
var vec = eye.Position.Position;
AL.Listener(ALListener3f.Position, vec.X, vec.Y, -5);
var rot2d = eye.Rotation.ToVec();
AL.Listener(ALListenerfv.Orientation, new []{0, 0, -1, rot2d.X, rot2d.Y, 0});
// Default orientation: at: (0, 0, -1) up: (0, 1, 0)
var rot = eye.Rotation.ToVec();
var at = new Vector3(0f, 0f, -1f);
var up = new Vector3(rot.Y, rot.X, 0f);
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
_flushALDisposeQueues();
}
private static void RemoveEfx((int sourceHandle, int filterHandle) handles)
{
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
}
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
}
public void SetAudioAttenuation(int value)
{
var attenuation = (Attenuation) value;
switch (attenuation)
{
case Attenuation.NoAttenuation:
AL.DistanceModel(ALDistanceModel.None);
break;
case Attenuation.InverseDistance:
AL.DistanceModel(ALDistanceModel.InverseDistance);
break;
case Attenuation.Default:
case Attenuation.InverseDistanceClamped:
AL.DistanceModel(ALDistanceModel.InverseDistanceClamped);
break;
case Attenuation.LinearDistance:
AL.DistanceModel(ALDistanceModel.LinearDistance);
break;
case Attenuation.LinearDistanceClamped:
AL.DistanceModel(ALDistanceModel.LinearDistanceClamped);
break;
case Attenuation.ExponentDistance:
AL.DistanceModel(ALDistanceModel.ExponentDistance);
break;
case Attenuation.ExponentDistanceClamped:
AL.DistanceModel(ALDistanceModel.ExponentDistanceClamped);
break;
default:
throw new ArgumentOutOfRangeException($"No implementation to set {attenuation.ToString()} for DistanceModel!");
}
var attToString = attenuation == Attenuation.Default ? Attenuation.InverseDistanceClamped : attenuation;
OpenALSawmill.Info($"Set audio attenuation to {attToString.ToString()}");
}
public IClydeAudioSource? CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// ReSharper disable once PossibleInvalidOperationException
// TODO: This really shouldn't be indexing based on the ClydeHandle...
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value.Value].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<AudioSource>(audioSource));
return audioSource;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
throw new Exception("Failed to generate source. Too many simultaneous audio streams?");
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
return audioSource;
}
private void _checkAlcError(ALDevice device,
[CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = ALC.GetError(device);
if (error != AlcError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] ALC error: {2}", callerMember, callerLineNumber, error);
}
}
private void _checkAlError([CallerMemberName] string callerMember = "",
[CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] AL error: {2}", callerMember, callerLineNumber, error);
}
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
var vorbis = _readOggVorbis(stream);
var buffer = AL.GenBuffer();
ALFormat format;
// NVorbis only supports loading into floats.
// If this becomes a problem due to missing extension support (doubt it but ok),
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.MonoFloat32Ext;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.StereoFloat32Ext;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
unsafe
{
fixed (float* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
(int) vorbis.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
var wav = _readWav(stream);
var buffer = AL.GenBuffer();
ALFormat format;
if (wav.BitsPerSample == 16)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono16;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo16;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else if (wav.BitsPerSample == 8)
{
if (wav.NumChannels == 1)
{
format = ALFormat.Mono8;
}
else if (wav.NumChannels == 2)
{
format = ALFormat.Stereo8;
}
else
{
throw new InvalidOperationException("Unable to load audio with more than 2 channels.");
}
}
else
{
throw new InvalidOperationException("Unable to load wav with bits per sample different from 8 or 16");
}
unsafe
{
fixed (byte* ptr = wav.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, wav.Data.Length, wav.SampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var fmt = channels switch
{
1 => ALFormat.Mono16,
2 => ALFormat.Stereo16,
_ => throw new ArgumentOutOfRangeException(
nameof(channels), "Only stereo and mono is currently supported")
};
var buffer = AL.GenBuffer();
_checkAlError();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(buffer, fmt, (IntPtr) ptr, samples.Length * sizeof(short), sampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
return new AudioStream(handle, length, channels, name);
}
private sealed class LoadedAudioSample
{
public readonly int BufferHandle;
public LoadedAudioSample(int bufferHandle)
{
BufferHandle = bufferHandle;
}
}
}
}

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's ClydeAudio's evil twin brother!
/// </summary>
[UsedImplicitly]
internal sealed class ClydeAudioHeadless : IClydeAudio, IClydeAudioInternal
{
public bool InitializePostWindowing()
{
return true;
}
public void FrameProcess(FrameEventArgs eventArgs)
{
}
public void Shutdown()
{
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, 1, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
// TODO: Might wanna actually load this so the length gets reported correctly.
return new(default, default, channels, name);
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
public void SetMasterVolume(float newVolume)
{
// Nada.
}
public void DisposeAllAudio()
{
// Nada.
}
public void StopAllAudio()
{
// Nada.
}
}
}

View File

@@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// Hey look, it's ClydeAudio.AudioSource's evil twin brother!
/// </summary>
[Virtual]
internal class DummyAudioSource : IClydeAudioSource
{
public static DummyAudioSource Instance { get; } = new();
public bool IsPlaying => default;
public bool IsLooping { get; set; }
public void Dispose()
{
// Nada.
}
public void StartPlaying()
{
// Nada.
}
public void StopPlaying()
{
// Nada.
}
public bool IsGlobal { get; }
public bool SetPosition(Vector2 position)
{
return true;
}
public void SetPitch(float pitch)
{
// Nada.
}
public void SetGlobal()
{
// Nada.
}
public void SetVolume(float decibels)
{
// Nada.
}
public void SetVolumeDirect(float gain)
{
// Nada.
}
public void SetMaxDistance(float maxDistance)
{
// Nada.
}
public void SetRolloffFactor(float rolloffFactor)
{
// Nada.
}
public void SetReferenceDistance(float refDistance)
{
// Nada.
}
public void SetOcclusion(float blocks)
{
// Nada.
}
public void SetPlaybackPosition(float seconds)
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Shared.Timing;
using Robust.Shared.IoC;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// For "start ss14 with no audio devices" Smugleaf
/// </summary>
[UsedImplicitly]
internal sealed class FallbackProxyClydeAudio : ProxyClydeAudio
{
[Dependency] private readonly IDependencyCollection _deps = default!;
public override bool InitializePostWindowing()
{
// Deliberate lack of base call here (see base implementation for comments as to why there even is a base)
ActualImplementation = new ClydeAudio();
_deps.InjectDependencies(ActualImplementation, true);
if (ActualImplementation.InitializePostWindowing())
return true;
// If we get here, that failed, so use the fallback
ActualImplementation = new ClydeAudioHeadless();
_deps.InjectDependencies(ActualImplementation, true);
return ActualImplementation.InitializePostWindowing();
}
}
}

View File

@@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics.Audio
{
/// <summary>
/// For "start ss14 with no audio devices" Smugleaf
/// </summary>
[UsedImplicitly]
internal abstract class ProxyClydeAudio : IClydeAudio, IClydeAudioInternal
{
protected IClydeAudioInternal ActualImplementation = default!;
public virtual bool InitializePostWindowing()
{
// This particular implementation exists to be overridden because removing this method causes C# to complain
return ActualImplementation.InitializePostWindowing();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
ActualImplementation.FrameProcess(eventArgs);
}
public void Shutdown()
{
ActualImplementation.Shutdown();
}
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
return ActualImplementation.LoadAudioOggVorbis(stream, name);
}
public AudioStream LoadAudioWav(Stream stream, string? name = null)
{
return ActualImplementation.LoadAudioWav(stream, name);
}
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
return ActualImplementation.LoadAudioRaw(samples, channels, sampleRate, name);
}
public IClydeAudioSource? CreateAudioSource(AudioStream stream)
{
return ActualImplementation.CreateAudioSource(stream);
}
public IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return ActualImplementation.CreateBufferedAudioSource(buffers, floatAudio);
}
public void SetMasterVolume(float newVolume)
{
ActualImplementation.SetMasterVolume(newVolume);
}
public void DisposeAllAudio()
{
ActualImplementation.DisposeAllAudio();
}
public void StopAllAudio()
{
ActualImplementation.StopAllAudio();
}
}
}

View File

@@ -4,7 +4,6 @@ using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
@@ -41,22 +40,28 @@ namespace Robust.Client.Graphics.Clyde
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
var grids = new List<Entity<MapGridComponent>>();
_mapManager.FindGridsIntersecting(mapId, worldBounds, ref grids);
foreach (var mapGrid in grids)
{
if (!_mapChunkData.ContainsKey(mapGrid.Owner))
if (!_mapChunkData.ContainsKey(mapGrid))
continue;
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid.Owner);
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
var enumerator = mapGrid.GetMapChunks(worldBounds);
var enumerator = mapGrid.Comp.GetMapChunks(worldBounds);
var data = _mapChunkData[mapGrid];
while (enumerator.MoveNext(out var chunk))
{
if (_isChunkDirty(mapGrid, chunk))
_updateChunkMesh(mapGrid, chunk);
DebugTools.Assert(chunk.FilledTiles > 0);
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
var datum = _mapChunkData[mapGrid.Owner][chunk.Indices];
if (datum.Dirty)
_updateChunkMesh(mapGrid, chunk, datum);
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount == 0)
continue;
@@ -68,22 +73,36 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
}
}
CullEmptyChunks();
}
private void _updateChunkMesh(MapGridComponent grid, MapChunk chunk)
private void CullEmptyChunks()
{
var data = _mapChunkData[grid.Owner];
if (!data.TryGetValue(chunk.Indices, out var datum))
foreach (var (grid, chunks) in _mapChunkData)
{
datum = _initChunkBuffers(grid, chunk);
}
var gridComp = _mapManager.GetGridComp(grid);
foreach (var (index, chunk) in chunks)
{
if (!chunk.Dirty || gridComp.Chunks.ContainsKey(index))
{
DebugTools.Assert(gridComp.Chunks[index].FilledTiles > 0);
continue;
}
DeleteChunk(chunk);
chunks.Remove(index);
}
}
}
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
var i = 0;
var cSz = grid.ChunkSize;
var cSz = grid.Comp.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
{
@@ -130,7 +149,7 @@ namespace Robust.Client.Graphics.Clyde
datum.TileCount = i;
}
private unsafe MapChunkData _initChunkBuffers(MapGridComponent grid, MapChunk chunk)
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
{
var vao = GenVertexArray();
BindVertexArray(vao);
@@ -158,41 +177,22 @@ namespace Robust.Client.Graphics.Clyde
Dirty = true
};
_mapChunkData[grid.Owner].Add(chunk.Indices, datum);
return datum;
}
private bool _isChunkDirty(MapGridComponent grid, MapChunk chunk)
private void DeleteChunk(MapChunkData data)
{
var data = _mapChunkData[grid.Owner];
return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty;
}
public void _setChunkDirty(MapGridComponent grid, Vector2i chunk)
{
var data = _mapChunkData.GetOrNew(grid.Owner);
if (data.TryGetValue(chunk, out var datum))
{
datum.Dirty = true;
}
// Don't need to set it if we don't have an entry since lack of an entry is treated as dirty.
}
private void _updateOnGridModified(GridModifiedEvent args)
{
foreach (var (pos, _) in args.Modified)
{
var grid = args.Grid;
var chunk = grid.GridTileToChunkIndices(pos);
_setChunkDirty(grid, chunk);
}
DeleteVertexArray(data.VAO);
CheckGlError();
data.VBO.Delete();
data.EBO.Delete();
}
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
{
var grid = _mapManager.GetGrid(args.NewTile.GridUid);
var chunk = grid.GridTileToChunkIndices(new Vector2i(args.NewTile.X, args.NewTile.Y));
_setChunkDirty(grid, chunk);
var gridData = _mapChunkData.GetOrNew(args.Entity);
if (gridData.TryGetValue(args.ChunkIndex, out var data))
data.Dirty = true;
}
private void _updateOnGridCreated(GridStartupEvent ev)
@@ -208,10 +208,7 @@ namespace Robust.Client.Graphics.Clyde
var data = _mapChunkData[gridId];
foreach (var chunkDatum in data.Values)
{
DeleteVertexArray(chunkDatum.VAO);
CheckGlError();
chunkDatum.VBO.Delete();
chunkDatum.EBO.Delete();
DeleteChunk(chunkDatum);
}
_mapChunkData.Remove(gridId);

View File

@@ -350,7 +350,7 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
if (entry.Sprite.RaiseShaderEvent)
_entityManager.EventBus.RaiseLocalEvent(entry.Sprite.Owner,
_entityManager.EventBus.RaiseLocalEvent(entry.Uid,
new BeforePostShaderRenderEvent(entry.Sprite, viewport), false);
}
}

View File

@@ -1,11 +1,3 @@
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
using System;
using System.Buffers;
using System.Collections.Generic;
@@ -14,7 +6,15 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Threading.Tasks;
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde;
@@ -260,7 +260,7 @@ internal partial class Clyde
if (cmp != 0)
return cmp;
return a.Sprite.Owner.CompareTo(b.Sprite.Owner);
return a.Uid.CompareTo(b.Uid);
}
}
}

View File

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

View File

@@ -11,17 +11,16 @@ 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;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using Color = Robust.Shared.Maths.Color;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
@@ -37,7 +36,8 @@ 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 IResourceCache _resourceCache = default!;
[Dependency] private readonly IClientResourceCache _resourceCache = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -175,7 +175,6 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.SubscribeEvent<TileChangedEvent>(EventSource.Local, this, _updateTileMapOnUpdate);
_entityManager.EventBus.SubscribeEvent<GridStartupEvent>(EventSource.Local, this, _updateOnGridCreated);
_entityManager.EventBus.SubscribeEvent<GridRemovalEvent>(EventSource.Local, this, _updateOnGridRemoved);
_entityManager.EventBus.SubscribeEvent<GridModifiedEvent>(EventSource.Local, this, _updateOnGridModified);
}
public void ShutdownGridEcsEvents()
@@ -183,7 +182,6 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.UnsubscribeEvent<TileChangedEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridStartupEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridRemovalEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridModifiedEvent>(EventSource.Local, this);
}
private void GLInitBindings(bool gles)

View File

@@ -292,123 +292,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
[Virtual]
private class DummyAudioSource : IClydeAudioSource
{
public static DummyAudioSource Instance { get; } = new();
public bool IsPlaying => default;
public bool IsLooping { get; set; }
public void Dispose()
{
// Nada.
}
public void StartPlaying()
{
// Nada.
}
public void StopPlaying()
{
// Nada.
}
public bool IsGlobal { get; }
public bool SetPosition(Vector2 position)
{
return true;
}
public void SetPitch(float pitch)
{
// Nada.
}
public void SetGlobal()
{
// Nada.
}
public void SetVolume(float decibels)
{
// Nada.
}
public void SetVolumeDirect(float gain)
{
// Nada.
}
public void SetMaxDistance(float maxDistance)
{
// Nada.
}
public void SetRolloffFactor(float rolloffFactor)
{
// Nada.
}
public void SetReferenceDistance(float refDistance)
{
// Nada.
}
public void SetOcclusion(float blocks)
{
// Nada.
}
public void SetPlaybackPosition(float seconds)
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
}
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
{
public new static DummyBufferedAudioSource Instance { get; } = new();
public int SampleRate { get; set; } = 0;
public void WriteBuffer(int handle, ReadOnlySpan<ushort> data)
{
// Nada.
}
public void WriteBuffer(int handle, ReadOnlySpan<float> data)
{
// Nada.
}
public void QueueBuffers(ReadOnlySpan<int> handles)
{
// Nada.
}
public void EmptyBuffers()
{
// Nada.
}
public void GetBuffersProcessed(Span<int> handles)
{
// Nada.
}
public int GetNumberOfBuffersProcessed()
{
return 0;
}
}
private sealed class DummyTexture : OwnedTexture
{
public DummyTexture(Vector2i size) : base(size)

View File

@@ -1,54 +1,54 @@
using System;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics
namespace Robust.Client.Graphics;
internal readonly struct ClydeHandle : IEquatable<ClydeHandle>, IClydeHandle
{
internal struct ClydeHandle : IEquatable<ClydeHandle>
public ClydeHandle(long value)
{
public ClydeHandle(long value)
{
Value = value;
}
Value = value;
}
public readonly long Value;
public long Value { get; }
public static explicit operator ClydeHandle(long x)
{
return new(x);
}
public static explicit operator ClydeHandle(long x)
{
return new(x);
}
public static explicit operator long(ClydeHandle h)
{
return h.Value;
}
public static explicit operator long(ClydeHandle h)
{
return h.Value;
}
public bool Equals(ClydeHandle other)
{
return Value == other.Value;
}
public bool Equals(ClydeHandle other)
{
return Value == other.Value;
}
public override bool Equals(object? obj)
{
return obj is ClydeHandle other && Equals(other);
}
public override bool Equals(object? obj)
{
return obj is ClydeHandle other && Equals(other);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator ==(ClydeHandle left, ClydeHandle right)
{
return left.Value == right.Value;
}
public static bool operator ==(ClydeHandle left, ClydeHandle right)
{
return left.Value == right.Value;
}
public static bool operator !=(ClydeHandle left, ClydeHandle right)
{
return left.Value != right.Value;
}
public static bool operator !=(ClydeHandle left, ClydeHandle right)
{
return left.Value != right.Value;
}
public override string ToString()
{
return $"ClydeHandle {Value}";
}
public override string ToString()
{
return $"ClydeHandle {Value}";
}
}

View File

@@ -114,9 +114,10 @@ namespace Robust.Client.Graphics
{
if (rune == new Rune('\n'))
{
baseLine.X = 0f;
baseLine.Y += lineHeight;
advanceTotal.Y += lineHeight;
advanceTotal.X = Math.Max(advanceTotal.X, baseLine.X);
baseLine.X = 0f;
continue;
}
@@ -126,7 +127,6 @@ namespace Robust.Client.Graphics
continue;
var advance = metrics.Value.Advance;
advanceTotal.X += advance;
baseLine += new Vector2(advance, 0);
}

View File

@@ -1,23 +0,0 @@
using System;
using System.IO;
using Robust.Client.Audio;
namespace Robust.Client.Graphics
{
public interface IClydeAudio
{
// AUDIO SYSTEM DOWN BELOW.
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
AudioStream LoadAudioWav(Stream stream, string? name = null);
AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null);
void SetMasterVolume(float newVolume);
void DisposeAllAudio();
void StopAllAudio();
IClydeAudioSource? CreateAudioSource(AudioStream stream);
IClydeBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false);
}
}

View File

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

View File

@@ -1,31 +0,0 @@
using System;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
public interface IClydeAudioSource : IDisposable
{
void StartPlaying();
void StopPlaying();
bool IsPlaying { get; }
bool IsLooping { get; set; }
bool IsGlobal { get; }
[MustUseReturnValue]
bool SetPosition(Vector2 position);
void SetPitch(float pitch);
void SetGlobal();
void SetVolume(float decibels);
void SetVolumeDirect(float gain);
void SetMaxDistance(float maxDistance);
void SetRolloffFactor(float rolloffFactor);
void SetReferenceDistance(float refDistance);
void SetOcclusion(float blocks);
void SetPlaybackPosition(float seconds);
void SetVelocity(Vector2 velocity);
}
}

View File

@@ -1,15 +0,0 @@
using System;
namespace Robust.Client.Graphics
{
public interface IClydeBufferedAudioSource : IClydeAudioSource
{
int SampleRate { get; set; }
int GetNumberOfBuffersProcessed();
void GetBuffersProcessed(Span<int> handles);
void WriteBuffer(int handle, ReadOnlySpan<ushort> data);
void WriteBuffer(int handle, ReadOnlySpan<float> data);
void QueueBuffers(ReadOnlySpan<int> handles);
void EmptyBuffers();
}
}

View File

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

View File

@@ -6,107 +6,106 @@ using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Graphics
namespace Robust.Client.Graphics;
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
{
internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
[Dependency] private readonly ILogManager _logMan = default!;
[ViewVariables]
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
private ISawmill _logger = default!;
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public void FrameUpdate(FrameEventArgs args)
{
[Dependency] private readonly ILogManager _logMan = default!;
[ViewVariables]
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
private ISawmill _logger = default!;
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public void FrameUpdate(FrameEventArgs args)
foreach (var overlay in _overlays.Values)
{
foreach (var overlay in _overlays.Values)
{
overlay.FrameUpdate(args);
}
overlay.FrameUpdate(args);
}
}
public bool AddOverlay(Overlay overlay)
public bool AddOverlay(Overlay overlay)
{
if (_overlays.ContainsKey(overlay.GetType()))
return false;
_overlays.Add(overlay.GetType(), overlay);
return true;
}
public bool RemoveOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
if (_overlays.ContainsKey(overlay.GetType()))
return false;
_overlays.Add(overlay.GetType(), overlay);
return true;
}
public bool RemoveOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"RemoveOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
return _overlays.Remove(overlayClass);
}
public bool RemoveOverlay<T>() where T : Overlay
{
return RemoveOverlay(typeof(T));
}
public bool RemoveOverlay(Overlay overlay)
{
return _overlays.Remove(overlay.GetType());
}
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
{
overlay = null;
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"TryGetOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
return _overlays.TryGetValue(overlayClass, out overlay);
}
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay
{
overlay = null;
if (_overlays.TryGetValue(typeof(T), out Overlay? toReturn))
{
overlay = (T)toReturn;
return true;
}
_logger.Error($"RemoveOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
public Overlay GetOverlay(Type overlayClass)
return _overlays.Remove(overlayClass);
}
public bool RemoveOverlay<T>() where T : Overlay
{
return RemoveOverlay(typeof(T));
}
public bool RemoveOverlay(Overlay overlay)
{
return _overlays.Remove(overlay.GetType());
}
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
{
overlay = null;
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
return _overlays[overlayClass];
_logger.Error($"TryGetOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
return false;
}
public T GetOverlay<T>() where T : Overlay
return _overlays.TryGetValue(overlayClass, out overlay);
}
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay
{
overlay = null;
if (_overlays.TryGetValue(typeof(T), out Overlay? toReturn))
{
return (T)_overlays[typeof(T)];
overlay = (T)toReturn;
return true;
}
public bool HasOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"HasOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
}
return false;
}
return _overlays.ContainsKey(overlayClass);
public Overlay GetOverlay(Type overlayClass)
{
return _overlays[overlayClass];
}
public T GetOverlay<T>() where T : Overlay
{
return (T)_overlays[typeof(T)];
}
public bool HasOverlay(Type overlayClass)
{
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
{
_logger.Error($"HasOverlay was called with arg: {overlayClass}, which is not a subclass of Overlay!");
}
public bool HasOverlay<T>() where T : Overlay
{
return _overlays.ContainsKey(typeof(T));
}
return _overlays.ContainsKey(overlayClass);
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("overlay");
}
public bool HasOverlay<T>() where T : Overlay
{
return _overlays.ContainsKey(typeof(T));
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("overlay");
}
}

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<IResourceCache>().GetResource<ShaderSourceResource>(_path.Value);
_source = IoCManager.Resolve<IClientResourceCache>().GetResource<ShaderSourceResource>(_path.Value);
if (_paramMapping != null)
{
@@ -142,7 +142,7 @@ namespace Robust.Client.Graphics
case "canvas":
Kind = ShaderKind.Canvas;
_source = IoCManager.Resolve<IResourceCache>().GetResource<ShaderSourceResource>("/Shaders/Internal/default-sprite.swsl");
_source = IoCManager.Resolve<IClientResourceCache>().GetResource<ShaderSourceResource>("/Shaders/Internal/default-sprite.swsl");
break;
default:

View File

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

View File

@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Direction = Robust.Shared.Maths.Direction;
namespace Robust.Client.Map;
@@ -22,6 +23,8 @@ public sealed class TileEdgeOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private List<Entity<MapGridComponent>> _grids = new();
public TileEdgeOverlay(IEntityManager entManager, IMapManager mapManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
{
_entManager = entManager;
@@ -36,16 +39,18 @@ public sealed class TileEdgeOverlay : Overlay
if (args.MapId == MapId.Nullspace)
return;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
foreach (var grid in _grids)
{
var tileSize = grid.TileSize;
var tileSize = grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var xform = xformQuery.GetComponent(grid.Owner);
var xform = xformQuery.GetComponent(grid);
args.WorldHandle.SetTransform(xform.WorldMatrix);
foreach (var tileRef in grid.GetTilesIntersecting(args.WorldBounds, false))
foreach (var tileRef in grid.Comp.GetTilesIntersecting(args.WorldBounds, false))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
@@ -61,7 +66,7 @@ public sealed class TileEdgeOverlay : Overlay
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = grid.GetTileRef(neighborIndices);
var neighborTile = grid.Comp.GetTileRef(neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.

View File

@@ -1,6 +1,6 @@
using System.Buffers;
using System.Collections.Generic;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
@@ -20,8 +20,8 @@ public sealed partial class PhysicsSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<PlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<LocalPlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<PhysicsComponent, JointAddedEvent>(OnJointAdded);
SubscribeLocalEvent<PhysicsComponent, JointRemovedEvent>(OnJointRemoved);
}
@@ -63,12 +63,12 @@ public sealed partial class PhysicsSystem
UpdateIsPredicted(args.Joint.BodyBUid);
}
private void OnAttach(PlayerAttachedEvent ev)
private void OnAttach(LocalPlayerAttachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}
private void OnDetach(PlayerDetachedEvent ev)
private void OnDetach(LocalPlayerDetachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
@@ -26,7 +25,7 @@ namespace Robust.Client.Physics
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
{
var toRemove = new List<PhysicsComponent>();
var toRemove = new List<Entity<PhysicsComponent>>();
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
// (and serializing it over the network isn't necessary?)
@@ -38,13 +37,13 @@ namespace Robust.Client.Physics
body.SleepTime += frameTime;
if (body.SleepTime > TimeToSleep)
{
toRemove.Add(body);
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
}
}
foreach (var body in toRemove)
{
SetAwake(body.Owner, body, false);
SetAwake(body, false);
}
base.Cleanup(component, frameTime);

View File

@@ -8,7 +8,6 @@ using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
@@ -29,7 +28,7 @@ namespace Robust.Client.Placement
{
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] internal readonly IPlayerManager PlayerManager = default!;
[Dependency] internal readonly IResourceCache ResourceCache = default!;
[Dependency] internal readonly IClientResourceCache ResourceCache = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] internal readonly IMapManager MapManager = default!;
[Dependency] private readonly IGameTiming _time = default!;
@@ -287,23 +286,17 @@ namespace Robust.Client.Placement
}, outsidePrediction: true))
.Register<PlacementManager>();
var localPlayer = PlayerManager.LocalPlayer;
localPlayer!.EntityAttached += OnEntityAttached;
PlayerManager.LocalPlayerDetached += OnDetached;
}
private void TearDownInput()
{
CommandBinds.Unregister<PlacementManager>();
if (PlayerManager.LocalPlayer != null)
{
PlayerManager.LocalPlayer.EntityAttached -= OnEntityAttached;
}
PlayerManager.LocalPlayerDetached -= OnDetached;
}
private void OnEntityAttached(EntityAttachedEventArgs eventArgs)
private void OnDetached(EntityUid obj)
{
// player attached to a new entity, basically disable the editor
Clear();
}

View File

@@ -1,44 +1,71 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
using Robust.Shared.Player;
namespace Robust.Client.Player
namespace Robust.Client.Player;
public interface IPlayerManager : ISharedPlayerManager
{
public interface IPlayerManager : ISharedPlayerManager
{
new IEnumerable<ICommonSession> Sessions { get; }
/// <summary>
/// Invoked when the list of sessions/players gets updated.
/// </summary>
event Action? PlayerListUpdated;
[ViewVariables]
IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict { get; }
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets attached to a new entity. See also <see cref="LocalPlayerAttachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerAttached;
[ViewVariables]
LocalPlayer? LocalPlayer { get; }
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets detached from new entity. See also <see cref="LocalPlayerDetachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerDetached;
/// <summary>
/// Invoked after LocalPlayer is changed
/// </summary>
event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
void ApplyPlayerStates(IReadOnlyCollection<SessionState> list);
event EventHandler PlayerListUpdated;
/// <summary>
/// Sets up a single player game. This creates a dummy <see cref="ISharedPlayerManager.LocalSession"/> without an
/// <see cref="INetChannel"/>.
/// </summary>
void SetupSinglePlayer(string name);
void Initialize();
void Startup();
void Shutdown();
/// <summary>
/// Sets up the manager for a multiplayer game. This creates a <see cref="ISharedPlayerManager.LocalSession"/>
/// using the given <see cref="INetChannel"/>.
/// </summary>
void SetupMultiplayer(INetChannel channel);
void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list);
}
public sealed class LocalPlayerChangedEventArgs : EventArgs
{
public readonly LocalPlayer? OldPlayer;
public readonly LocalPlayer? NewPlayer;
public LocalPlayerChangedEventArgs(LocalPlayer? oldPlayer, LocalPlayer? newPlayer)
{
OldPlayer = oldPlayer;
NewPlayer = newPlayer;
}
}
[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

@@ -1,11 +1,6 @@
using System;
using Robust.Client.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
@@ -15,163 +10,30 @@ namespace Robust.Client.Player
/// </summary>
public sealed class LocalPlayer
{
/// <summary>
/// An entity has been attached to the local player.
/// </summary>
public event Action<EntityAttachedEventArgs>? EntityAttached;
/// <summary>
/// An entity has been detached from the local player.
/// </summary>
public event Action<EntityDetachedEventArgs>? EntityDetached;
public LocalPlayer(ICommonSession session)
{
Session = session;
}
/// <summary>
/// Game entity that the local player is controlling. If this is default, the player is not attached to any
/// entity at all.
/// </summary>
[ViewVariables] public EntityUid? ControlledEntity { get; private set; }
[ViewVariables] public NetUserId UserId { get; set; }
/// <summary>
/// Session of the local client.
/// </summary>
[ViewVariables]
public ICommonSession Session => InternalSession;
public EntityUid? ControlledEntity => Session.AttachedEntity;
internal PlayerSession InternalSession { get; set; } = default!;
[ViewVariables]
public NetUserId UserId => Session.UserId;
/// <summary>
/// OOC name of the local player.
/// </summary>
[ViewVariables]
public string Name { get; set; } = default!;
public string Name => Session.Name;
/// <summary>
/// The status of the client's session has changed.
/// Session of the local client.
/// </summary>
public event EventHandler<StatusEventArgs>? StatusChanged;
/// <summary>
/// Attaches a client to an entity.
/// </summary>
/// <param name="entity">Entity to attach the client to.</param>
public void AttachEntity(EntityUid entity, IEntityManager entMan, IBaseClient client)
{
if (ControlledEntity == entity)
return;
// Detach and cleanup first
DetachEntity();
if (!entMan.EntityExists(entity))
{
Logger.Error($"Attempting to attach player to non-existent entity {entity}!");
return;
}
ControlledEntity = entity;
InternalSession.AttachedEntity = entity;
if (!entMan.TryGetComponent<EyeComponent?>(entity, out var eye))
{
eye = entMan.AddComponent<EyeComponent>(entity);
if (client.RunLevel != ClientRunLevel.SinglePlayerGame)
{
Logger.Warning($"Attaching local player to an entity {entMan.ToPrettyString(entity)} without an eye. This eye will not be netsynced and may cause issues.");
}
eye.NetSyncEnabled = false;
}
EntityAttached?.Invoke(new EntityAttachedEventArgs(entity));
// notify ECS Systems
var eventBus = entMan.EventBus;
eventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(entity));
eventBus.RaiseLocalEvent(entity, new PlayerAttachedEvent(entity), true);
}
/// <summary>
/// Detaches the client from an entity.
/// </summary>
public void DetachEntity()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var previous = ControlledEntity;
ControlledEntity = null;
InternalSession.AttachedEntity = null;
if (previous != null)
{
entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default));
entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true);
EntityDetached?.Invoke(new EntityDetachedEventArgs(previous.Value));
}
}
/// <summary>
/// Changes the state of the session.
/// </summary>
public void SwitchState(SessionStatus newStatus)
{
SwitchState(Session.Status, newStatus);
}
/// <summary>
/// Changes the state of the session. This overload allows you to spoof the oldStatus, use with caution.
/// </summary>
public void SwitchState(SessionStatus oldStatus, SessionStatus newStatus)
{
var args = new StatusEventArgs(oldStatus, newStatus);
Session.Status = newStatus;
StatusChanged?.Invoke(this, args);
}
}
/// <summary>
/// Event arguments for when the status of a session changes.
/// </summary>
public sealed class StatusEventArgs : EventArgs
{
/// <summary>
/// Status that the session switched from.
/// </summary>
public SessionStatus OldStatus { get; }
/// <summary>
/// Status that the session switched to.
/// </summary>
public SessionStatus NewStatus { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public StatusEventArgs(SessionStatus oldStatus, SessionStatus newStatus)
{
OldStatus = oldStatus;
NewStatus = newStatus;
}
}
public sealed class EntityDetachedEventArgs : EventArgs
{
public EntityDetachedEventArgs(EntityUid oldEntity)
{
OldEntity = oldEntity;
}
public EntityUid OldEntity { get; }
}
public sealed class EntityAttachedEventArgs : EventArgs
{
public EntityAttachedEventArgs(EntityUid newEntity)
{
NewEntity = newEntity;
}
public EntityUid NewEntity { get; }
public ICommonSession Session;
}
}

View File

@@ -2,16 +2,13 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
{
@@ -20,154 +17,187 @@ namespace Robust.Client.Player
/// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated.
/// This class also communicates with the server to let the server control what entity it is attached to.
/// </summary>
public sealed class PlayerManager : IPlayerManager
internal sealed class PlayerManager : SharedPlayerManager, IPlayerManager
{
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ILogManager _logMan = default!;
/// <summary>
/// Active sessions of connected clients to the server.
/// Received player states that had an unknown <see cref="NetEntity"/>.
/// </summary>
private readonly Dictionary<NetUserId, ICommonSession> _sessions = new();
private Dictionary<NetUserId, SessionState> _pendingStates = new ();
private List<SessionState> _pending = new();
/// <inheritdoc />
public IEnumerable<ICommonSession> NetworkedSessions
public override ICommonSession[] NetworkedSessions
{
get
{
if (LocalPlayer is not null)
return new[] {LocalPlayer.Session};
return Enumerable.Empty<ICommonSession>();
return LocalSession != null
? new [] { LocalSession }
: Array.Empty<ICommonSession>();
}
}
/// <inheritdoc />
IEnumerable<ICommonSession> ISharedPlayerManager.Sessions => _sessions.Values;
public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1;
public LocalPlayer? LocalPlayer { get; set; }
public event Action<SessionStatusEventArgs>? LocalStatusChanged;
public event Action? PlayerListUpdated;
public event Action<EntityUid>? LocalPlayerDetached;
public event Action<EntityUid>? LocalPlayerAttached;
/// <inheritdoc />
public int PlayerCount => _sessions.Values.Count;
/// <inheritdoc />
public int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? 0;
public ICommonSession? LocalSession => LocalPlayer?.Session;
/// <inheritdoc />
[ViewVariables]
public LocalPlayer? LocalPlayer
public override void Initialize(int maxPlayers)
{
get => _localPlayer;
private set
{
if (_localPlayer == value) return;
var oldValue = _localPlayer;
_localPlayer = value;
LocalPlayerChanged?.Invoke(new LocalPlayerChangedEventArgs(oldValue, _localPlayer));
}
}
private LocalPlayer? _localPlayer;
private ISawmill _sawmill = default!;
public event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
/// <inheritdoc />
[ViewVariables]
IEnumerable<ICommonSession> IPlayerManager.Sessions => _sessions.Values;
/// <inheritdoc />
public IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict => _sessions;
/// <inheritdoc />
public event EventHandler? PlayerListUpdated;
/// <inheritdoc />
public void Initialize()
{
_client.RunLevelChanged += OnRunLevelChanged;
_sawmill = _logMan.GetSawmill("player");
base.Initialize(maxPlayers);
_network.RegisterNetMessage<MsgPlayerListReq>();
_network.RegisterNetMessage<MsgPlayerList>(HandlePlayerList);
PlayerStatusChanged += StatusChanged;
}
private void StatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session == LocalPlayer?.Session)
LocalStatusChanged?.Invoke(e);
}
/// <inheritdoc />
public void Startup()
public override void Startup()
{
DebugTools.Assert(LocalPlayer == null);
LocalPlayer = new LocalPlayer();
if (LocalSession == null)
throw new InvalidOperationException("LocalSession cannot be null");
var msgList = new MsgPlayerListReq();
// message is empty
_network.ClientSendMessage(msgList);
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);
Startup();
PlayerListUpdated?.Invoke();
}
public void SetupMultiplayer(INetChannel channel)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
var session = CreateAndAddSession(channel.UserId, channel.UserName);
session.Channel = channel;
LocalSession = session;
Startup();
_network.ClientSendMessage(new MsgPlayerListReq());
}
/// <inheritdoc />
public void Shutdown()
public override void Shutdown()
{
LocalPlayer?.DetachEntity();
if (LocalSession != null)
SetAttachedEntity(LocalSession, null);
LocalPlayer = null;
_sessions.Clear();
LocalSession = null;
_pendingStates.Clear();
base.Shutdown();
PlayerListUpdated?.Invoke();
}
/// <inheritdoc />
public void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list)
public override void SetAttachedEntity(ICommonSession session, EntityUid? uid)
{
if (session.AttachedEntity == uid)
return;
var old = session.AttachedEntity;
base.SetAttachedEntity(session, uid);
if (session != LocalSession)
return;
if (old.HasValue)
{
Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}.");
EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true);
LocalPlayerDetached?.Invoke(old.Value);
}
if (uid == null)
{
Sawmill.Info($"Local player is no longer attached to any entity.");
return;
}
if (!EntManager.EntityExists(uid))
{
Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!");
return;
}
if (!EntManager.EnsureComponent(uid.Value, out EyeComponent eye))
{
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
eye.NetSyncEnabled = false;
}
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}.");
EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true);
LocalPlayerAttached?.Invoke(uid.Value);
}
public void ApplyPlayerStates(IReadOnlyCollection<SessionState> list)
{
var dirty = ApplyStates(list, true);
if (_pendingStates.Count == 0)
{
// This is somewhat inefficient as it might try to re-apply states that failed just a moment ago.
_pending.Clear();
_pending.AddRange(_pendingStates.Values);
_pendingStates.Clear();
dirty |= ApplyStates(_pending, false);
}
if (dirty)
PlayerListUpdated?.Invoke();
}
private bool ApplyStates(IReadOnlyCollection<SessionState> list, bool fullList)
{
if (list.Count == 0)
{
// This happens when the server says "nothing changed!"
return;
}
return false;
DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application.
, "Received player state without being connected?");
DebugTools.Assert(LocalPlayer != null, "Call Startup()");
DebugTools.Assert(LocalPlayer!.Session != null, "Received player state before Session finished setup.");
DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup.");
var myState = list.FirstOrDefault(s => s.UserId == LocalPlayer.UserId);
var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId);
if (myState != null)
bool dirty = false;
if (state != null)
{
var uid = _entManager.GetEntity(myState.ControlledEntity);
if (myState.ControlledEntity is {Valid: true} && !_entManager.EntityExists(uid))
dirty = true;
if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid)
&& state.ControlledEntity is { Valid:true } )
{
_sawmill.Error($"Received player state for local player with an unknown net entity!");
Sawmill.Error($"Received player state for local player with an unknown net entity!");
_pendingStates[state.UserId] = state;
}
else
{
_pendingStates.Remove(state.UserId);
}
UpdateAttachedEntity(uid);
UpdateSessionStatus(myState.Status);
SetAttachedEntity(LocalSession, uid);
SetStatus(LocalSession, state.Status);
}
UpdatePlayerList(list);
}
/// <summary>
/// Compares the server sessionStatus to the client one, and updates if needed.
/// </summary>
private void UpdateSessionStatus(SessionStatus myStateStatus)
{
if (LocalPlayer!.Session.Status != myStateStatus)
LocalPlayer.SwitchState(myStateStatus);
}
/// <summary>
/// Compares the server attachedEntity to the client one, and updates if needed.
/// </summary>
/// <param name="entity">AttachedEntity in the server session.</param>
private void UpdateAttachedEntity(EntityUid? entity)
{
if (LocalPlayer!.ControlledEntity == entity)
{
return;
}
if (entity == null)
{
LocalPlayer.DetachEntity();
return;
}
LocalPlayer.AttachEntity(entity.Value, _entManager, _client);
return UpdatePlayerList(list, fullList) || dirty;
}
/// <summary>
@@ -175,117 +205,87 @@ namespace Robust.Client.Player
/// </summary>
private void HandlePlayerList(MsgPlayerList msg)
{
UpdatePlayerList(msg.Plyrs);
ApplyPlayerStates(msg.Plyrs);
}
/// <summary>
/// Compares the server player list to the client one, and updates if needed.
/// </summary>
private void UpdatePlayerList(IEnumerable<PlayerState> remotePlayers)
private bool UpdatePlayerList(IEnumerable<SessionState> remotePlayers, bool fullList)
{
var dirty = false;
var hitSet = new List<NetUserId>();
var users = new List<NetUserId>();
foreach (var state in remotePlayers)
{
hitSet.Add(state.UserId);
users.Add(state.UserId);
if (_sessions.TryGetValue(state.UserId, out var session))
if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled)
&& state.ControlledEntity is {Valid: true})
{
var local = (PlayerSession) session;
var controlled = _entManager.GetEntity(state.ControlledEntity);
// Exists, update data.
if (local.Name == state.Name
&& local.Status == state.Status
&& local.Ping == state.Ping
&& local.AttachedEntity == controlled)
{
continue;
}
dirty = true;
local.Name = state.Name;
local.Status = state.Status;
local.Ping = state.Ping;
local.AttachedEntity = controlled;
_pendingStates[state.UserId] = state;
}
else
{
// New, give him a slot.
dirty = true;
var newSession = new PlayerSession(state.UserId)
{
Name = state.Name,
Status = state.Status,
Ping = state.Ping,
AttachedEntity = _entManager.GetEntity(state.ControlledEntity),
};
_sessions.Add(state.UserId, newSession);
if (state.UserId == LocalPlayer!.UserId)
{
LocalPlayer.InternalSession = newSession;
newSession.ConnectedClient = _network.ServerChannel!;
// We just connected to the server, hurray!
LocalPlayer.SwitchState(SessionStatus.Connecting, newSession.Status);
}
_pendingStates.Remove(state.UserId);
}
}
foreach (var existing in _sessions.Keys.ToArray())
{
// clear slot, player left
if (!hitSet.Contains(existing))
if (!InternalSessions.TryGetValue(state.UserId, out var session))
{
DebugTools.Assert(LocalPlayer!.UserId != existing || _client.RunLevel == ClientRunLevel.SinglePlayerGame, // replays apply player states.
"I'm still connected to the server, but i left?");
_sessions.Remove(existing);
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
var newSession = CreateAndAddSession(state.UserId, state.Name);
newSession.Ping = state.Ping;
newSession.Name = state.Name;
SetStatus(newSession, state.Status);
SetAttachedEntity(newSession, controlled);
dirty = true;
continue;
}
// Check if the data is actually different
if (session.Name == state.Name
&& session.Status == state.Status
&& session.Ping == state.Ping
&& session.AttachedEntity == controlled)
{
continue;
}
dirty = true;
var local = (CommonSession) session;
local.Name = state.Name;
local.Ping = state.Ping;
SetStatus(local, state.Status);
SetAttachedEntity(local, controlled);
}
// Remove old users. This only works if the provided state is a list of all players
if (fullList)
{
foreach (var oldUser in InternalSessions.Keys.ToArray())
{
// clear slot, player left
if (users.Contains(oldUser))
continue;
DebugTools.Assert(oldUser != LocalUser
|| LocalUser == null
|| LocalUser == default(NetUserId),
"Client is still connected to the server but not in the list of players?");
RemoveSession(oldUser);
_pendingStates.Remove(oldUser);
dirty = true;
}
}
if (dirty)
{
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
return dirty;
}
private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs e)
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (e.NewLevel != ClientRunLevel.SinglePlayerGame)
return;
DebugTools.AssertNotNull(LocalPlayer);
// We do some further setup steps for singleplayer here...
// The local player's GUID in singleplayer will always be the default.
var guid = default(NetUserId);
var session = new PlayerSession(guid)
if (LocalEntity == uid)
{
Name = LocalPlayer!.Name,
Ping = 0,
};
LocalPlayer.UserId = guid;
LocalPlayer.InternalSession = session;
// Add the local session to the list.
_sessions.Add(guid, session);
LocalPlayer.SwitchState(SessionStatus.InGame);
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (LocalPlayer?.ControlledEntity == uid)
{
session = LocalPlayer.Session;
session = LocalSession!;
return true;
}

View File

@@ -1,60 +0,0 @@
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
{
internal sealed class PlayerSession : ICommonSession
{
internal SessionStatus Status { get; set; } = SessionStatus.Connecting;
/// <inheritdoc />
SessionStatus ICommonSession.Status
{
get => this.Status;
set => this.Status = value;
}
/// <inheritdoc />
[ViewVariables]
public EntityUid? AttachedEntity { get; set; }
/// <inheritdoc />
[ViewVariables]
public NetUserId UserId { get; }
[ViewVariables]
internal string Name { get; set; } = "<Unknown>";
/// <inheritdoc />
string ICommonSession.Name
{
get => this.Name;
set => this.Name = value;
}
[ViewVariables]
internal short Ping { get; set; }
/// <inheritdoc />
[ViewVariables]
public INetChannel ConnectedClient { get; internal set; } = null!;
/// <inheritdoc />
short ICommonSession.Ping
{
get => this.Ping;
set => this.Ping = value;
}
/// <summary>
/// Creates an instance of a PlayerSession.
/// </summary>
public PlayerSession(NetUserId user)
{
UserId = user;
}
}
}

View File

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

View File

@@ -7,7 +7,6 @@ using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Threading.Tasks;
using Robust.Client.Upload.Commands;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Replays;
@@ -101,7 +100,7 @@ public sealed partial class ReplayLoadManager
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
var playerSpan = state0.PlayerStates.Value;
Dictionary<NetUserId, PlayerState> playerStates = new(playerSpan.Count);
Dictionary<NetUserId, SessionState> playerStates = new(playerSpan.Count);
foreach (var player in playerSpan)
{
playerStates.Add(player.UserId, player);
@@ -391,7 +390,7 @@ public sealed partial class ReplayLoadManager
return new EntityState(newState.NetEntity, combined, newState.EntityLastModified, newState.NetComponents ?? oldNetComps);
}
private void UpdatePlayerStates(ReadOnlySpan<PlayerState> span, Dictionary<NetUserId, PlayerState> playerStates)
private void UpdatePlayerStates(ReadOnlySpan<SessionState> span, Dictionary<NetUserId, SessionState> playerStates)
{
foreach (var player in span)
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Configuration;
using Robust.Client.GameObjects;
@@ -10,6 +11,7 @@ using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Client.Upload;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -27,7 +29,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IMidiManager _midi = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly IAudioInternal _clydeAudio = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IClientNetManager _netMan = default!;
[Dependency] private readonly IComponentFactory _factory = default!;

View File

@@ -9,7 +9,7 @@ using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Value;
@@ -138,9 +138,9 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager
return (state, detachMsg);
}
private PlayerState GetPlayerState(ICommonSession session)
private SessionState GetPlayerState(ICommonSession session)
{
return new PlayerState
return new SessionState
{
UserId = session.UserId,
Status = session.Status,

View File

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

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

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

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

@@ -1,15 +0,0 @@
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,6 +9,13 @@ namespace Robust.Client.ResourceManagement
{
internal partial class ResourceCache
{
public void MountLoaderApi(IResourceManager manager, IFileApi api, string apiPrefix, ResPath? prefix = null)
{
prefix ??= ResPath.Root;
var root = new LoaderApiLoader(api, apiPrefix);
manager.AddRoot(prefix.Value, root);
}
private sealed class LoaderApiLoader : IContentRoot
{
private readonly IFileApi _api;

View File

@@ -3,10 +3,13 @@ using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
@@ -19,7 +22,8 @@ namespace Robust.Client.ResourceManagement
internal partial class ResourceCache
{
[field: Dependency] public IClyde Clyde { get; } = default!;
[field: Dependency] public IClydeAudio ClydeAudio { get; } = default!;
[field: Dependency] public IAudioInternal ClydeAudio { get; } = default!;
[Dependency] private readonly IResourceManager _manager = default!;
[field: Dependency] public IFontManager FontManager { get; } = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
@@ -44,7 +48,7 @@ namespace Robust.Client.ResourceManagement
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<TextureResource>();
var texList = ContentFindFiles("/Textures/")
var texList = _manager.ContentFindFiles("/Textures/")
// Skip PNG files inside RSIs.
.Where(p => p.Extension == "png" && !p.ToString().Contains(".rsi/") && !resList.ContainsKey(p))
.Select(p => new TextureResource.LoadStepData {Path = p})
@@ -54,7 +58,7 @@ namespace Robust.Client.ResourceManagement
{
try
{
TextureResource.LoadPreTexture(this, data);
TextureResource.LoadPreTexture(_manager, data);
}
catch (Exception e)
{
@@ -116,7 +120,7 @@ namespace Robust.Client.ResourceManagement
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<RSIResource>();
var rsiList = ContentFindFiles("/Textures/")
var rsiList = _manager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
.Select(c => c.Directory)
.Where(p => !resList.ContainsKey(p))
@@ -127,7 +131,7 @@ namespace Robust.Client.ResourceManagement
{
try
{
RSIResource.LoadPreTexture(this, data);
RSIResource.LoadPreTexture(_manager, data);
}
catch (Exception e)
{

View File

@@ -1,221 +1,23 @@
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;
using System;
using Robust.Shared.ResourceManagement;
namespace Robust.Client.ResourceManagement
namespace Robust.Client.ResourceManagement;
/// <summary>
/// Handles caching of <see cref="BaseResource"/>
/// </summary>
internal sealed partial class ResourceCache : SharedResourceCache, IClientResourceCacheInternal, IDisposable
{
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
public event Action<RsiLoadedEventArgs>? OnRsiLoaded;
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
{
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> CachedResources =
new();
OnRawTextureLoaded?.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);
}
public void RsiLoaded(RsiLoadedEventArgs eventArgs)
{
OnRsiLoaded?.Invoke(eventArgs);
}
}

View File

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

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