Compare commits

...

30 Commits

Author SHA1 Message Date
PJB3005
9a7e207e3b Version: 247.2.5 2025-12-02 00:57:21 +01:00
PJB3005
d28b50de49 Fix NetBitArraySerializer compatibility.
Apparently NetSerializer treats IDynamicTypeSerializer and IStaticTypeSerializer differently for sealed types??

(cherry-picked from 6bbeaeeba6, without the test changes)

(cherry picked from commit d187018834dfa1cdead9ce5a7b72c38b07f8c81f)
2025-12-02 00:57:20 +01:00
PJB3005
4c31e9071f Version: 247.2.4 2025-12-01 16:07:21 +01:00
PJB3005
1000e2cf2c Backport BitArray .NET 10 serializer fix
83ad6042a7 & b267cd6fb4

Does not include test code to avoid risking merge conflicts.

(cherry picked from commit 415585a30d74fcae61f581808220a7aaeca3eaf5)
(cherry picked from commit e36628a6d436ea08d6d31441c101a88a5504c515)
2025-12-01 16:07:20 +01:00
PJB3005
7212d6d6b2 Version: 247.2.3 2025-09-26 13:40:50 +02:00
PJB3005
2d5e6c2b77 Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:50 +02:00
PJB3005
19307875cf Version: 247.2.2 2025-09-19 09:17:35 +02:00
Skye
a76c57f841 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:35 +02:00
PJB3005
0a604e4a04 Version: 247.2.1 2025-09-14 14:58:24 +02:00
PJB3005
6218ef6e3f Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
(cherry picked from commit 4413695c77fb705054c2f81fa18ec0a189b685dd)
2025-09-14 14:58:24 +02:00
PJB3005
76b46479b6 Version: 247.2.0 2025-02-23 01:44:44 +01:00
PJB3005
de9a8d286a Release notes 2025-02-23 01:43:58 +01:00
Milon
a1df0fb4af fix some issues with ClientDisconnect (#5625)
* fix

* actually fix for real this time

* just use HappyEyeballsHttp :godo:

* review
2025-02-23 01:33:58 +01:00
pathetic meowmeow
e6bc5a1057 Proxy scrollbar values and value targets in ScrollContainer (#5697) 2025-02-22 22:10:32 +01:00
beck-thompson
11b24579a2 Fix MultiRootInheritanceGraph not detecting circular inheritance (#5672)
* Fix

* This is better!

* Fixes
2025-02-22 22:08:31 +01:00
metalgearsloth
685d002bb7 Move VisibilitySystem to shared (#5694)
* Move VisibilitySystem to shared

* this

* Remove redundant qualifiers.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-02-22 21:36:49 +01:00
Tobias Berger
2e0d18aeaf Fix wrong parameters for Regex.Escape in Sandbox whitelist (#5688) 2025-02-22 18:08:28 +01:00
Kyle Tyo
06dbff0429 believe that should be all of em. (#5691) 2025-02-22 18:01:07 +01:00
Southbridge
15958a9447 Fix issue regarding Tilemaps not saving when modified (#5696)
* One line C# fix

* fixed typo

* after spending way too long trying to figure out the problem, turns out all we needed was a simple one line change
2025-02-22 17:33:33 +01:00
pathetic meowmeow
fd5a4d9b8a Refactor audio system to send collection IDs over the network (#5540)
This is important groundwork for future features such as captioning,
as a caption and other data can be associated with the collection
prototype instead of passing extra data everywhere with the sound.
2025-02-22 17:29:47 +01:00
DrSmugleaf
6d958847cb Fix prototype hot reloading crashing when adding a component that an existing entity already has (#5695) 2025-02-22 16:30:05 +01:00
Milon
8a04a4f3a5 Add a method for copying components (#5654)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-02-21 09:46:13 +11:00
ElectroJr
7104a4f459 Version: 247.1.0 2025-02-20 16:26:04 +13:00
Leon Friedrich
f29949a32c Revert "Add ICloneable support to ComponentNetworkGenerator (#5656)" (#5687)
This reverts commit e14537074e.
2025-02-20 14:22:36 +11:00
Leon Friedrich
3bbbabf238 Update map format validator (#5686)
* Update map format validator

* string -> str
2025-02-20 12:11:12 +11:00
metalgearsloth
d95aca3d9e Fix DirtyFields proxy method (#5684) 2025-02-20 00:15:05 +11:00
Tayrtahn
e14537074e Add ICloneable support to ComponentNetworkGenerator (#5656) 2025-02-18 23:21:40 +11:00
DrSmugleaf
af2d01981f Add optional minimumDistance parameter to SharedJointSystem.CreateDistanceJoint (#5682) 2025-02-18 14:18:15 +11:00
Fildrance
7df23e047c feat: shaders now can accept array of Color as parameter (#5679)
* feat: shaders now can accept array of Color as parameter

* fix: Clyde.SetUniformDirect for Color[] doesn't mutate original array, removed invalid 'in' keyword on SetUniform

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-02-16 14:13:04 +01:00
ElectroJr
5c7ab43049 Fix typo in RELEASE-NOTES.md 2025-02-17 00:15:27 +13:00
55 changed files with 1052 additions and 382 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

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

View File

@@ -54,6 +54,60 @@ END TEMPLATE-->
*None yet*
## 247.2.5
## 247.2.4
## 247.2.3
## 247.2.2
## 247.2.1
## 247.2.0
### New features
* Added functions for copying components to `IEntityManager` and `EntitySystem`.
* Sound played from sound collections is now sent as "collection ID + index" over the network instead of the final filename.
* This enables integration of future accessibility systems.
* Added a new `ResolvedSoundSpecifier` to represent played sounds. Methods that previously took a filename now take a `ResolvedSoundSpecifier`, with an implicit cast from string being interpreted as a raw filename.
* `VisibilitySystem` has been made accessible to shared as `SharedVisibilitySystem`.
* `ScrollContainer` now has properties exposing `Value` and `ValueTarget` on its internal scroll bars.
### Bugfixes
* Fix prototype hot reload crashing when adding a new component already exists on an entity.
* Fix maps failing to save in some cases related to tilemap IDs.
* Fix `Regex.Escape(string)` not being available in sandbox.
* Prototypes that parent themselves directly won't cause the game to hang on an infinite loop anymore.
* Fixed disconnecting during a connection attempt leaving the client stuck in a phantom state.
### Internal
* More warning cleanup.
## 247.1.0
### New features
* Added support for `Color[]` shader uniforms
* Added optional minimumDistance parameter to `SharedJointSystem.CreateDistanceJoint()`
### Bugfixes
* Fixed `EntitySystem.DirtyFields()` not actually marking fields as dirty.
### Other
* Updated the Yamale map file format validator to support v7 map/grid files.
## 247.0.0
### Breaking changes
@@ -64,7 +118,7 @@ END TEMPLATE-->
when warnings are logged.
* The minimum supported map format / version has been increased from 2 to 3.
* The server-side `MapLoaderSystem` and associated classes & structs has been moved to `Robust.Shared`, and has been significantly modified.
* The`TryLoad` and `Save` methods have been replaced with grid, map, generic entity variants. I.e, `SaveGrid`, `SaveMap`, and `SaveEntities`.
* The `TryLoad` and `Save` methods have been replaced with grid, map, generic entity variants. I.e, `SaveGrid`, `SaveMap`, and `SaveEntities`.
* Most of the serialization logic and methods have been moved out of `MapLoaderSystem` and into new `EntitySerializer`
and `EntityDeserializer` classes, which also replace the old `MapSerializationContext`.
* The `MapLoadOptions` class has been split into `MapLoadOptions`, `SerializationOptions`, and `DeserializationOptions`

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -40,11 +40,7 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
var audioParams = keyFrame.AudioParamsFunc.Invoke();
var audio = new SoundPathSpecifier(keyFrame.Resource)
{
Params = audioParams
};
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(keyFrame.Specifier, Filter.Local(), entity, true, audioParams);
}
return (keyFrameIndex, playingTime);
@@ -55,7 +51,7 @@ namespace Robust.Client.Animations
/// <summary>
/// The RSI state to play when this keyframe gets triggered.
/// </summary>
public readonly string Resource;
public readonly ResolvedSoundSpecifier Specifier;
/// <summary>
/// A function that returns the audio parameter to be used.
@@ -69,9 +65,9 @@ namespace Robust.Client.Animations
/// </summary>
public readonly float KeyTime;
public KeyFrame(string resource, float keyTime, Func<AudioParams>? audioParams = null)
public KeyFrame(ResolvedSoundSpecifier specifier, float keyTime, Func<AudioParams>? audioParams = null)
{
Resource = resource;
Specifier = specifier;
KeyTime = keyTime;
AudioParamsFunc = audioParams ?? (() => AudioParams.Default);
}

View File

@@ -415,6 +415,16 @@ public sealed partial class AudioSystem : SharedAudioSystem
return occlusion;
}
private bool TryGetAudio(ResolvedSoundSpecifier specifier, [NotNullWhen(true)] out AudioResource? audio)
{
var filename = GetAudioPath(specifier);
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
return true;
Log.Error($"Server tried to play audio file {filename} which does not exist.");
return false;
}
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
@@ -433,15 +443,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
return false;
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
return PlayStatic(specifier, Filter.Local(), coordinates, true, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
return PlayEntity(specifier, Filter.Local(), uid, true, audioParams);
}
/// <inheritdoc />
@@ -477,21 +487,21 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </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)
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
Specifier = specifier,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayGlobal(audio, specifier, audioParams) : default;
}
/// <summary>
@@ -499,9 +509,9 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
var (entity, component) = CreateAndStartPlayingStream(audioParams, specifier, stream);
component.Global = true;
component.Source.Global = true;
DirtyField(entity, component, nameof(AudioComponent.Global));
@@ -513,22 +523,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </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)
private (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
Specifier = specifier,
NetEntity = GetNetEntity(entity),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayEntity(audio, entity, specifier, audioParams) : default;
}
/// <summary>
@@ -537,7 +547,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(entity))
{
@@ -545,7 +555,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
return playing;
@@ -557,22 +567,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <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)
private (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Specifier = specifier,
Coordinates = GetNetCoordinates(coordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayStatic(audio, coordinates, specifier, audioParams) : default;
}
/// <summary>
@@ -581,7 +591,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
@@ -589,33 +599,33 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, 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)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
return PlayEntity(specifier, entity, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
@@ -629,39 +639,39 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, ResolvedSoundSpecifier? specifier, AudioStream stream)
{
var audioP = audioParams ?? AudioParams.Default;
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
var entity = SetupAudio(specifier, audioP, initialize: false, length: stream.Length);
LoadStream(entity, stream);
EntityManager.InitializeAndStartEntity(entity);
var comp = entity.Comp;
@@ -694,17 +704,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
{
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
}
private void OnEntityAudio(PlayAudioEntityMessage ev)
{
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false);
}
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
{
PlayGlobal(ev.FileName, ev.AudioParams, false);
PlayGlobal(ev.Specifier, ev.AudioParams, false);
}
protected override TimeSpan GetAudioLengthImpl(string filename)

View File

@@ -115,10 +115,6 @@ namespace Robust.Client
/// <inheritdoc />
public void DisconnectFromServer(string reason)
{
DebugTools.Assert(RunLevel > ClientRunLevel.Initialize);
DebugTools.Assert(_net.IsConnected);
// run level changed in OnNetDisconnect()
// are both of these *really* needed?
_net.ClientDisconnect(reason);
}

View File

@@ -382,7 +382,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects;
public sealed class VisibilitySystem : SharedVisibilitySystem
{
}

View File

@@ -523,6 +523,9 @@ namespace Robust.Client.Graphics.Clyde
case Color color:
program.SetUniform(name, color);
break;
case Color[] colorArr:
program.SetUniform(name, colorArr);
break;
case int i:
program.SetUniform(name, i);
break;

View File

@@ -485,6 +485,13 @@ namespace Robust.Client.Graphics.Clyde
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Color[] value)
{
var data = Parent._shaderInstances[Handle];
data.ParametersDirty = true;
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, int value)
{
var data = Parent._shaderInstances[Handle];

View File

@@ -369,6 +369,10 @@ namespace Robust.Client.Graphics.Clyde
{
}
private protected override void SetParameterImpl(string name, Color[] value)
{
}
private protected override void SetParameterImpl(string name, int value)
{
}

View File

@@ -334,7 +334,7 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, in Color color, bool convertToLinear=true)
private void SetUniformDirect(int slot, in Color color, bool convertToLinear = true)
{
var converted = color;
if (convertToLinear)
@@ -349,6 +349,39 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniform(string uniformName, Color[] colors)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, colors);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, Color[] colors, bool convertToLinear = true)
{
scoped Span<Color> colorsToPass;
if (convertToLinear)
{
colorsToPass = stackalloc Color[colors.Length];
for (int i = 0; i < colors.Length; i++)
{
colorsToPass[i] = Color.FromSrgb(colors[i]);
}
}
else
{
colorsToPass = colors;
}
unsafe
{
fixed (Color* ptr = &colorsToPass[0])
{
GL.Uniform4(slot, colorsToPass.Length, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, in Vector3 vector)
{
var uniformId = GetUniform(uniformName);

View File

@@ -221,11 +221,12 @@ namespace Robust.Client.Graphics
public static bool TypeSupportsArrays(this ShaderDataType type)
{
// TODO: add support for int, and vec3/4 arrays
// TODO: add support for int, and vec3 arrays
return
(type == ShaderDataType.Float) ||
(type == ShaderDataType.Vec2) ||
(type == ShaderDataType.Bool);
(type == ShaderDataType.Bool) ||
(type == ShaderDataType.Vec4);
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]

View File

@@ -113,6 +113,13 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, Color[] value)
{
EnsureAlive();
EnsureMutable();
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector4 value)
{
EnsureAlive();
@@ -223,6 +230,7 @@ namespace Robust.Client.Graphics
private protected abstract void SetParameterImpl(string name, Vector3 value);
private protected abstract void SetParameterImpl(string name, Vector4 value);
private protected abstract void SetParameterImpl(string name, Color value);
private protected abstract void SetParameterImpl(string name, Color[] value);
private protected abstract void SetParameterImpl(string name, int value);
private protected abstract void SetParameterImpl(string name, Vector2i value);
private protected abstract void SetParameterImpl(string name, bool value);

View File

@@ -144,7 +144,7 @@ namespace Robust.Client.Placement
if (mousePos.MapId == MapId.Nullspace)
yield break;
var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
var (_, (x, y)) = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
float iterations;
Vector2 distance;
if (Math.Abs(x) > Math.Abs(y))
@@ -176,7 +176,7 @@ namespace Robust.Client.Placement
if (mousePos.MapId == MapId.Nullspace)
yield break;
var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
var placementdiff = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
var xSign = Math.Sign(placementdiff.X);
var ySign = Math.Sign(placementdiff.Y);
@@ -264,13 +264,15 @@ namespace Robust.Client.Placement
protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords)
{
var mapCoords = pManager.EyeManager.PixelToMap(coords.Position);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
{
return EntityCoordinates.FromMap(pManager.MapManager, mapCoords);
return transformSys.ToCoordinates(mapCoords);
}
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager);
return transformSys.ToCoordinates(gridUid, mapCoords);
}
}
}

View File

@@ -28,6 +28,30 @@ namespace Robust.Client.UserInterface.Controls
public int ScrollSpeedX { get; set; } = 50;
public int ScrollSpeedY { get; set; } = 50;
public float VScroll
{
get => _vScrollBar.Value;
set => _vScrollBar.Value = value;
}
public float VScrollTarget
{
get => _vScrollBar.ValueTarget;
set => _vScrollBar.ValueTarget = value;
}
public float HScroll
{
get => _hScrollBar.Value;
set => _hScrollBar.Value = value;
}
public float HScrollTarget
{
get => _hScrollBar.ValueTarget;
set => _hScrollBar.ValueTarget = value;
}
private bool _reserveScrollbarSpace;
public bool ReserveScrollbarSpace
{

View File

@@ -81,27 +81,27 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
AddAudioFilter(entity, entity.Comp, playerFilter);
entity.Comp.Global = true;
return (entity, entity.Comp);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(uid))
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
// Move it after setting it up
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
@@ -115,24 +115,24 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(uid))
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
return (entity, entity.Comp);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(coordinates.EntityId))
@@ -144,7 +144,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (!coordinates.IsValid(EntityManager))
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
XformSystem.SetCoordinates(entity, coordinates);
AddAudioFilter(entity, entity.Comp, playerFilter);
@@ -152,10 +152,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(coordinates.EntityId))
@@ -168,7 +168,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
// TODO: Transform TryFindGridAt mess + optimisation required.
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
XformSystem.SetCoordinates(entity, coordinates);
return (entity, entity.Comp);
@@ -191,7 +191,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (sound == null)
return null;
var audio = PlayPvs(GetSound(sound), source, audioParams ?? sound.Params);
var audio = PlayPvs(ResolveSound(sound), source, audioParams ?? sound.Params);
if (audio == null)
return null;
@@ -206,7 +206,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (sound == null)
return null;
var audio = PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
var audio = PlayPvs(ResolveSound(sound), coordinates, audioParams ?? sound.Params);
if (audio == null)
return null;
@@ -215,12 +215,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return audio;
}
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, EntityUid recipient, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayGlobal(filename, actor.PlayerSession, audioParams);
@@ -228,12 +228,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
@@ -241,12 +241,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -6,7 +6,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Server.GameObjects
{
public sealed class VisibilitySystem : EntitySystem
public sealed class VisibilitySystem : SharedVisibilitySystem
{
[Dependency] private readonly PvsSystem _pvs = default!;
[Dependency] private readonly IViewVariablesManager _vvManager = default!;
@@ -40,7 +40,7 @@ namespace Robust.Server.GameObjects
EntityManager.EntityInitialized -= OnEntityInit;
}
public void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
public override void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
@@ -53,7 +53,7 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
public void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
public override void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
if (!_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false))
return;
@@ -67,7 +67,7 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
public void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
public override void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
@@ -90,14 +90,14 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent.Owner, null, ent.Comp);
}
public void RefreshVisibility(EntityUid uid,
public override void RefreshVisibility(EntityUid uid,
VisibilityComponent? visibilityComponent = null,
MetaDataComponent? meta = null)
{
RefreshVisibility((uid, visibilityComponent, meta));
}
public void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
public override void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
{
if (!_metaQuery.Resolve(ent, ref ent.Comp2, false))
return;

View File

@@ -6,4 +6,3 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

View File

@@ -0,0 +1,90 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Utility;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes;
namespace Robust.Shared.Audio;
/// <summary>
/// Represents a path to a sound resource, either as a literal path or as a collection ID and index.
/// </summary>
/// <seealso cref="ResolvedPathSpecifier"/>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public abstract partial class ResolvedSoundSpecifier {
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
public static implicit operator ResolvedSoundSpecifier(ResPath s) => new ResolvedPathSpecifier(s);
/// <summary>
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
/// </summary>
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
return s switch {
null => true,
ResolvedPathSpecifier path => path.Path.ToString() == "",
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
_ => throw new ArgumentOutOfRangeException("s", s, "argument is not a ResolvedPathSpecifier or a ResolvedCollectionSpecifier"),
};
}
}
/// <summary>
/// Represents a path to a sound resource as a literal path.
/// </summary>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The resource path of the sound.
/// </summary>
public ResPath Path { get; private set; }
override public string ToString() =>
$"ResolvedPathSpecifier({Path})";
[UsedImplicitly]
private ResolvedPathSpecifier()
{
}
public ResolvedPathSpecifier(ResPath path)
{
Path = path;
}
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
{
}
}
/// <summary>
/// Represents a path to a sound resource as a collection ID and index.
/// </summary>
/// <seealso cref="ResolvedPathSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
/// </summary>
public ProtoId<SoundCollectionPrototype>? Collection { get; private set; }
/// <summary>
/// The index of the file in the associated sound collection to play.
/// </summary>
public int Index { get; private set; }
override public string ToString() =>
$"ResolvedCollectionSpecifier({Collection}, {Index})";
[UsedImplicitly]
private ResolvedCollectionSpecifier()
{
}
public ResolvedCollectionSpecifier(string collection, int index)
{
Collection = collection;
Index = index;
}
}

View File

@@ -27,6 +27,9 @@ public sealed partial class SoundPathSpecifier : SoundSpecifier
[DataField(Node, customTypeSerializer: typeof(ResPathSerializer), required: true)]
public ResPath Path { get; private set; }
override public string ToString() =>
$"SoundPathSpecifier({Path})";
[UsedImplicitly]
private SoundPathSpecifier()
{
@@ -52,6 +55,9 @@ public sealed partial class SoundCollectionSpecifier : SoundSpecifier
[DataField(Node, customTypeSerializer: typeof(PrototypeIdSerializer<SoundCollectionPrototype>), required: true)]
public string? Collection { get; private set; }
override public string ToString() =>
$"SoundCollectionSpecifier({Collection})";
[UsedImplicitly]
public SoundCollectionSpecifier() { }

View File

@@ -283,33 +283,60 @@ public abstract partial class SharedAudioSystem : EntitySystem
}
/// <summary>
/// Resolves the filepath to a sound file.
/// Resolve a sound specifier so it can be consistently played back on all clients.
/// </summary>
public string GetSound(SoundSpecifier specifier)
public ResolvedSoundSpecifier ResolveSound(SoundSpecifier specifier)
{
switch (specifier)
{
case SoundPathSpecifier path:
return path.Path == default ? string.Empty : path.Path.ToString();
return new ResolvedPathSpecifier(path.Path == default ? string.Empty : path.Path.ToString());
case SoundCollectionSpecifier collection:
{
if (collection.Collection == null)
return string.Empty;
return new ResolvedPathSpecifier(string.Empty);
var soundCollection = ProtoMan.Index<SoundCollectionPrototype>(collection.Collection);
return RandMan.Pick(soundCollection.PickFiles).ToString();
var index = RandMan.Next(soundCollection.PickFiles.Count);
return new ResolvedCollectionSpecifier(collection.Collection, index);
}
}
return string.Empty;
return new ResolvedPathSpecifier(string.Empty);
}
/// <summary>
/// Resolves the filepath to a sound file.
/// </summary>
[Obsolete("Use ResolveSound() and pass around resolved sound specifiers instead.")]
public string GetSound(SoundSpecifier specifier)
{
var resolved = ResolveSound(specifier);
return GetAudioPath(resolved);
}
#region AudioParams
protected Entity<AudioComponent> SetupAudio(string? fileName, AudioParams? audioParams, bool initialize = true, TimeSpan? length = null)
[return: NotNullIfNotNull(nameof(specifier))]
public string? GetAudioPath(ResolvedSoundSpecifier? specifier)
{
return specifier switch {
ResolvedPathSpecifier path =>
path.Path.ToString(),
ResolvedCollectionSpecifier collection =>
collection.Collection is null ?
string.Empty :
ProtoMan.Index<SoundCollectionPrototype>(collection.Collection).PickFiles[collection.Index].ToString(),
null => null,
_ => throw new ArgumentOutOfRangeException("specifier", specifier, "argument is not a ResolvedPathSpecifier or a ResolvedCollectionSpecifier"),
};
}
protected Entity<AudioComponent> SetupAudio(ResolvedSoundSpecifier? specifier, AudioParams? audioParams, bool initialize = true, TimeSpan? length = null)
{
var uid = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
var fileName = GetAudioPath(specifier);
DebugTools.Assert(!string.IsNullOrEmpty(fileName) || length is not null);
MetadataSys.SetEntityName(uid, $"Audio ({fileName})", raiseEvents: false);
audioParams ??= AudioParams.Default;
@@ -395,8 +422,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <summary>
/// Gets the timespan of the specified audio.
/// </summary>
public TimeSpan GetAudioLength(string filename)
public TimeSpan GetAudioLength(ResolvedSoundSpecifier specifier)
{
var filename = GetAudioPath(specifier) ?? string.Empty;
if (!filename.StartsWith("/"))
throw new ArgumentException("Path must be rooted");
@@ -429,7 +457,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
@@ -438,7 +466,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="playerFilter">The set of players that will hear the sound.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), playerFilter, recordReplay, audioParams ?? sound.Params);
return sound == null ? null : PlayGlobal(ResolveSound(sound), playerFilter, recordReplay, audioParams ?? sound.Params);
}
/// <summary>
@@ -446,7 +474,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, ICommonSession recipient, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
@@ -455,7 +483,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="recipient">The player that will hear the sound.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, ICommonSession recipient, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params);
return sound == null ? null : PlayGlobal(ResolveSound(sound), recipient, audioParams ?? sound.Params);
}
public abstract void LoadStream<T>(Entity<AudioComponent> entity, T stream);
@@ -465,7 +493,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, EntityUid recipient, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
@@ -474,7 +502,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="recipient">The player that will hear the sound.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, EntityUid recipient, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params);
return sound == null ? null : PlayGlobal(ResolveSound(sound), recipient, audioParams ?? sound.Params);
}
/// <summary>
@@ -483,7 +511,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
@@ -491,7 +519,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
@@ -499,7 +527,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
@@ -509,7 +537,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params);
return sound == null ? null : PlayEntity(ResolveSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params);
}
/// <summary>
@@ -520,7 +548,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
return sound == null ? null : PlayEntity(ResolveSound(sound), recipient, uid, audioParams ?? sound.Params);
}
/// <summary>
@@ -531,7 +559,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
return sound == null ? null : PlayEntity(ResolveSound(sound), recipient, uid, audioParams ?? sound.Params);
}
/// <summary>
@@ -541,7 +569,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayPvs(GetSound(sound), uid, audioParams ?? sound.Params);
return sound == null ? null : PlayPvs(ResolveSound(sound), uid, audioParams ?? sound.Params);
}
/// <summary>
@@ -551,7 +579,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
return sound == null ? null : PlayPvs(ResolveSound(sound), coordinates, audioParams ?? sound.Params);
}
/// <summary>
@@ -559,7 +587,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename,
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? filename,
EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
@@ -567,7 +595,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename, EntityUid uid,
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? filename, EntityUid uid,
AudioParams? audioParams = null);
/// <summary>
@@ -604,7 +632,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
@@ -612,7 +640,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
@@ -620,7 +648,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
@@ -630,7 +658,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), playerFilter, coordinates, recordReplay, audioParams);
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
}
/// <summary>
@@ -641,7 +669,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
return sound == null ? null : PlayStatic(ResolveSound(sound), recipient, coordinates, audioParams ?? sound.Params);
}
/// <summary>
@@ -652,7 +680,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
return sound == null ? null : PlayStatic(ResolveSound(sound), recipient, coordinates, audioParams ?? sound.Params);
}
// These are just here for replays now.
@@ -665,7 +693,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
[NetSerializable, Serializable]
protected abstract class AudioMessage : EntityEventArgs
{
public string FileName = string.Empty;
public ResolvedSoundSpecifier Specifier = new ResolvedPathSpecifier(string.Empty);
public AudioParams AudioParams;
}

View File

@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
public string SystemAssemblyName = default!;
public HashSet<VerifierError> AllowedVerifierErrors = default!;
public List<string> WhitelistedNamespaces = default!;
public List<string> AllowedAssemblyPrefixes = default!;
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
}

View File

@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
return false;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
{
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
return false;
}
if (VerifyIL)
{
if (!DoVerifyIL(asmName, resolver, peReader, reader))
@@ -179,10 +189,6 @@ namespace Robust.Shared.ContentPack
return true;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.

View File

@@ -60,7 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()));
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -93,19 +93,23 @@ namespace Robust.Shared.ContentPack
{
var sw = Stopwatch.StartNew();
Sawmill.Debug("LOADING modules");
var files = new Dictionary<string, (ResPath Path, string[] references)>();
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
// Find all modules we want to load.
foreach (var fullPath in paths)
{
using var asmFile = _res.ContentFileRead(fullPath);
var refData = GetAssemblyReferenceData(asmFile);
var ms = new MemoryStream();
asmFile.CopyTo(ms);
ms.Position = 0;
var refData = GetAssemblyReferenceData(ms);
if (refData == null)
continue;
var (asmRefs, asmName) = refData.Value;
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
{
Sawmill.Error("Found multiple modules with the same assembly name " +
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
@@ -122,10 +126,10 @@ namespace Robust.Shared.ContentPack
Parallel.ForEach(files, pair =>
{
var (name, (path, _)) = pair;
var (name, (_, data, _)) = pair;
using var stream = _res.ContentFileRead(path);
if (!typeChecker.CheckAssembly(stream, resolver))
data.Position = 0;
if (!typeChecker.CheckAssembly(data, resolver))
{
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
}
@@ -137,14 +141,15 @@ namespace Robust.Shared.ContentPack
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value.Path,
kv => kv.Value,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var path in TopologicalSort.Sort(nodes))
foreach (var item in TopologicalSort.Sort(nodes))
{
var (path, memory, _) = item;
Sawmill.Debug($"Loading module: '{path}'");
try
{
@@ -156,9 +161,9 @@ namespace Robust.Shared.ContentPack
}
else
{
using var assemblyStream = _res.ContentFileRead(path);
memory.Position = 0;
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
}
}
catch (Exception e)
@@ -174,7 +179,7 @@ namespace Robust.Shared.ContentPack
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
{
using var reader = ModLoader.MakePEReader(stream);
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,7 +379,13 @@ namespace Robust.Shared.ContentPack
{
if (root is DirLoader loader)
{
yield return new ResPath(loader.GetPath(new ResPath(@"/")));
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}
}

View File

@@ -17,6 +17,10 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.
@@ -696,7 +700,7 @@ Types:
- "bool IsMatch(string, string, System.Text.RegularExpressions.RegexOptions, System.TimeSpan)"
- "int GroupNumberFromName(string)"
- "int[] GetGroupNumbers()"
- "string Escape()"
- "string Escape(string)"
- "string GroupNameFromNumber(int)"
- "string Replace(string, string)"
- "string Replace(string, string, int)"

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

View File

@@ -271,7 +271,10 @@ public sealed class EntitySerializer : ISerializationContext,
foreach (var (origId, prototypeId) in savedMap)
{
if (_tileDef.TryGetDefinition(prototypeId, out var definition))
{
_tileMap.TryAdd(definition.TileId, origId);
_yamlTileIds.Add(origId); // Make sure we record the IDs we're using so when we need to reserve new ones we can
}
}
}
@@ -593,7 +596,7 @@ public sealed class EntitySerializer : ISerializationContext,
public MappingDataNode Write()
{
DebugTools.AssertEqual(Maps.ToHashSet().Count, Maps.Count, "Duplicate maps?");
DebugTools.AssertEqual(Grids.ToHashSet().Count, Grids.Count, "Duplicate frids?");
DebugTools.AssertEqual(Grids.ToHashSet().Count, Grids.Count, "Duplicate grids?");
DebugTools.AssertEqual(Orphans.ToHashSet().Count, Orphans.Count, "Duplicate orphans?");
DebugTools.AssertEqual(Nullspace.ToHashSet().Count, Nullspace.Count, "Duplicate nullspace?");

View File

@@ -1,14 +1,13 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Server.GameObjects
namespace Robust.Shared.GameObjects
{
/// <summary>
/// Controls PVS visibility of entities. THIS COMPONENT CONTROLS WHETHER ENTITIES ARE NETWORKED TO PLAYERS
/// AND SHOULD NOT BE USED AS THE SOLE WAY TO HIDE AN ENTITY FROM A PLAYER.
/// </summary>
[RegisterComponent]
[Access(typeof(VisibilitySystem))]
[Access(typeof(SharedVisibilitySystem))]
public sealed partial class VisibilityComponent : Component
{
/// <summary>

View File

@@ -1075,6 +1075,97 @@ namespace Robust.Shared.GameObjects
return TryGetComponent(uid.Value, netId, out component, meta);
}
/// <inheritdoc/>
public bool TryCopyComponent<T>(EntityUid source, EntityUid target, ref T? sourceComponent, [NotNullWhen(true)] out T? targetComp, MetaDataComponent? meta = null) where T : IComponent
{
if (!MetaQuery.Resolve(target, ref meta))
{
targetComp = default;
return false;
}
if (sourceComponent == null && !TryGetComponent(source, out sourceComponent))
{
targetComp = default;
return false;
}
targetComp = CopyComponentInternal(source, target, sourceComponent, meta);
return true;
}
/// <inheritdoc/>
public bool TryCopyComponents(
EntityUid source,
EntityUid target,
MetaDataComponent? meta = null,
params Type[] sourceComponents)
{
if (!MetaQuery.TryGetComponent(source, out meta))
return false;
var allCopied = true;
foreach (var type in sourceComponents)
{
if (!TryGetComponent(source, type, out var srcComp))
{
allCopied = false;
continue;
}
CopyComponent(source, target, srcComp, meta: meta);
}
return allCopied;
}
/// <inheritdoc/>
public IComponent CopyComponent(EntityUid source, EntityUid target, IComponent sourceComponent, MetaDataComponent? meta = null)
{
if (!MetaQuery.Resolve(target, ref meta))
{
throw new InvalidOperationException();
}
return CopyComponentInternal(source, target, sourceComponent, meta);
}
/// <inheritdoc/>
public T CopyComponent<T>(EntityUid source, EntityUid target, T sourceComponent,MetaDataComponent? meta = null) where T : IComponent
{
if (!MetaQuery.Resolve(target, ref meta))
{
throw new InvalidOperationException();
}
return CopyComponentInternal(source, target, sourceComponent, meta);
}
/// <inheritdoc/>
public void CopyComponents(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params IComponent[] sourceComponents)
{
if (!MetaQuery.Resolve(target, ref meta))
return;
foreach (var comp in sourceComponents)
{
CopyComponentInternal(source, target, comp, meta);
}
}
private T CopyComponentInternal<T>(EntityUid source, EntityUid target, T sourceComponent, MetaDataComponent meta) where T : IComponent
{
var compReg = ComponentFactory.GetRegistration(sourceComponent.GetType());
var component = (T)ComponentFactory.GetComponent(compReg);
_serManager.CopyTo(sourceComponent, ref component, notNullableOverride: true);
component.Owner = target;
AddComponentInternal(target, component, compReg, true, false, meta);
return component;
}
public EntityQuery<TComp1> GetEntityQuery<TComp1>() where TComp1 : IComponent
{
var comps = _entTraitArray[CompIdx.ArrayIndex<TComp1>()];

View File

@@ -175,7 +175,7 @@ public partial class EntitySystem
protected void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
where T : IComponentDelta
{
EntityManager.DirtyFields(uid, comp, meta);
EntityManager.DirtyFields(uid, comp, meta, fields);
}
/// <summary>
@@ -565,6 +565,52 @@ public partial class EntitySystem
#endregion
#region Component Copy
/// <inheritdoc cref="IEntityManager.TryCopyComponent"/>
protected bool TryCopyComponent<T>(
EntityUid source,
EntityUid target,
ref T? sourceComponent,
[NotNullWhen(true)] out T? targetComp,
MetaDataComponent? meta = null) where T : IComponent
{
return EntityManager.TryCopyComponent(source, target, ref sourceComponent, out targetComp, meta);
}
/// <inheritdoc cref="IEntityManager.TryCopyComponents"/>
protected bool TryCopyComponents(
EntityUid source,
EntityUid target,
MetaDataComponent? meta = null,
params Type[] sourceComponents)
{
return EntityManager.TryCopyComponents(source, target, meta, sourceComponents);
}
/// <inheritdoc cref="IEntityManager.CopyComponent"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected IComponent CopyComp(EntityUid source, EntityUid target, IComponent sourceComponent, MetaDataComponent? meta = null)
{
return EntityManager.CopyComponent(source, target, sourceComponent, meta);
}
/// <inheritdoc cref="IEntityManager.CopyComponent{T}"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected T CopyComp<T>(EntityUid source, EntityUid target, T sourceComponent, MetaDataComponent? meta = null) where T : IComponent
{
return EntityManager.CopyComponent(source, target, sourceComponent, meta);
}
/// <inheritdoc cref="IEntityManager.CopyComponents"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void CopyComps(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params IComponent[] sourceComponents)
{
EntityManager.CopyComponents(source, target, meta, sourceComponents);
}
#endregion
#region Component Has
/// <summary>

View File

@@ -358,6 +358,51 @@ namespace Robust.Shared.GameObjects
/// <returns>If the component existed in the entity.</returns>
bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, ushort netId, [NotNullWhen(true)] out IComponent? component, MetaDataComponent? meta = null);
/// <summary>
/// Tries to run <see cref="CopyComponents"/> without throwing if the component doesn't exist.
/// </summary>
bool TryCopyComponent<T>(
EntityUid source,
EntityUid target,
ref T? sourceComponent,
[NotNullWhen(true)] out T? targetComp,
MetaDataComponent? meta = null) where T : IComponent;
/// <summary>
/// Tries to run <see cref="CopyComponents"/> without throwing if the components don't exist.
/// </summary>
bool TryCopyComponents(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params Type[] sourceComponents);
/// <summary>
/// Copy a single component from source to target entity.
/// </summary>
/// <param name="source">The source entity to copy from.</param>
/// <param name="target">The target entity to copy to.</param>
/// <param name="sourceComponent">The source component instance to copy.</param>
/// <param name="component">The copied component if successful.</param>
/// <param name="meta">Optional metadata of the target entity.</param>
IComponent CopyComponent(EntityUid source, EntityUid target, IComponent sourceComponent, MetaDataComponent? meta = null);
/// <summary>
/// Copy a single component from source to target entity.
/// </summary>
/// <typeparam name="T">The type of component to copy.</typeparam>
/// <param name="source">The source entity to copy from.</param>
/// <param name="target">The target entity to copy to.</param>
/// <param name="sourceComponent">The source component instance to copy.</param>
/// <param name="component">The copied component if successful.</param>
/// <param name="meta">Optional metadata of the target entity.</param>
T CopyComponent<T>(EntityUid source, EntityUid target, T sourceComponent, MetaDataComponent? meta = null) where T : IComponent;
/// <summary>
/// Copy multiple components from source to target entity using existing component instances.
/// </summary>
/// <param name="source">The source entity to copy from.</param>
/// <param name="target">The target entity to copy to.</param>
/// <param name="meta">Optional metadata of the target entity.</param>
/// <param name="sourceComponents">Array of component instances to copy.</param>
void CopyComponents(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params IComponent[] sourceComponents);
/// <summary>
/// Returns a cached struct enumerator with the specified component.
/// </summary>

View File

@@ -72,7 +72,9 @@ internal sealed class PrototypeReloadSystem : EntitySystem
{
var data = newPrototype.Components[name];
var component = _componentFactory.GetComponent(name);
EntityManager.AddComponent(entity, component);
if (!EntityManager.HasComponent(entity, component.GetType()))
EntityManager.AddComponent(entity, component);
}
// Update entity metadata

View File

@@ -0,0 +1,26 @@
namespace Robust.Shared.GameObjects;
public abstract class SharedVisibilitySystem : EntitySystem
{
public virtual void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
}
public virtual void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
}
public virtual void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
}
public virtual void RefreshVisibility(EntityUid uid,
VisibilityComponent? visibilityComponent = null,
MetaDataComponent? meta = null)
{
}
public virtual void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
{
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -11,7 +10,6 @@ using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Lidgren.Network;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages.Handshake;
using Robust.Shared.Utility;
using SpaceWizards.Sodium;
@@ -98,7 +96,7 @@ namespace Robust.Shared.Network
{
await CCDoHandshake(winningPeer, winningConnection, userNameRequest, mainCancelToken);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
winningPeer.Peer.Shutdown("Cancelled");
_toCleanNetPeers.Add(winningPeer.Peer);
@@ -120,7 +118,10 @@ namespace Robust.Shared.Network
_logger.Debug("Handshake completed, connection established.");
}
private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, string userNameRequest,
private async Task CCDoHandshake(
NetPeerData peer,
NetConnection connection,
string userNameRequest,
CancellationToken cancel)
{
var encrypt = _config.GetCVar(CVars.NetEncrypt);
@@ -289,37 +290,51 @@ namespace Robust.Shared.Network
private async Task<(NetPeerData winningPeer, NetConnection winningConnection)?>
CCHappyEyeballs(int port, IPAddress first, IPAddress? second, CancellationToken mainCancelToken)
{
NetPeerData CreatePeerForIp(IPAddress address)
// Try to establish a connection with an IP address and wait for it to either connect or fail
// Returns a disposable wrapper around the peer/connection because ParallelTask
async Task<ConnectionAttempt> AttemptConnection(IPAddress address, CancellationToken cancel)
{
var config = _getBaseNetPeerConfig();
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
config.LocalAddress = IPAddress.IPv6Any;
}
else
{
config.LocalAddress = IPAddress.Any;
}
config.LocalAddress = address.AddressFamily == AddressFamily.InterNetworkV6
? IPAddress.IPv6Any
: IPAddress.Any;
var peer = new NetPeer(config);
peer.Start();
var data = new NetPeerData(peer);
_netPeers.Add(data);
return data;
var peerData = new NetPeerData(peer);
_netPeers.Add(peerData);
var connection = peer.Connect(new IPEndPoint(address, port));
try
{
// We need AwaitNonInitStatusChange to properly handle connection state transitions
var reason = await AwaitNonInitStatusChange(connection, cancel);
if (connection.Status != NetConnectionStatus.Connected)
{
// Connection failed, clean up and yeet an exception
peer.Shutdown(reason);
_toCleanNetPeers.Add(peer);
throw new Exception($"Connection failed: {reason}");
}
return new ConnectionAttempt(peerData, connection, this);
}
catch (Exception)
{
// Something went wrong!
peer.Shutdown("Connection attempt failed");
_toCleanNetPeers.Add(peer);
throw;
}
}
// Create first peer.
var firstPeer = CreatePeerForIp(first);
var firstConnection = firstPeer.Peer.Connect(new IPEndPoint(first, port));
NetPeerData? secondPeer = null;
NetConnection? secondConnection = null;
string? secondReason = null;
// Waits for a connection's status to change from InitiatedConnect to anything else
async Task<string> AwaitNonInitStatusChange(NetConnection connection, CancellationToken cancellationToken)
{
NetConnectionStatus status;
string reason;
NetConnectionStatus status;
do
{
reason = await AwaitStatusChange(connection, cancellationToken);
@@ -329,124 +344,37 @@ namespace Robust.Shared.Network
return reason;
}
async Task ConnectSecondDelayed(CancellationToken cancellationToken)
{
DebugTools.AssertNotNull(second);
// Connecting via second peer is delayed by 25ms to give an advantage to IPv6, if it works.
var delay = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetHappyEyeballsDelay));
await Task.Delay(delay, cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
secondPeer = CreatePeerForIp(second);
secondConnection = secondPeer.Peer.Connect(new IPEndPoint(second, port));
secondReason = await AwaitNonInitStatusChange(secondConnection, cancellationToken);
}
NetPeerData? winningPeer;
NetConnection? winningConnection;
string? firstReason = null;
try
{
if (second != null)
{
// We have two addresses to try.
var cancellation = CancellationTokenSource.CreateLinkedTokenSource(mainCancelToken);
var firstPeerChanged = AwaitNonInitStatusChange(firstConnection, cancellation.Token);
var secondPeerChanged = ConnectSecondDelayed(cancellation.Token);
// Create list of IPs to try
var addresses = second != null
? new[] { first, second }
: new[] { first };
var firstChange = await Task.WhenAny(firstPeerChanged, secondPeerChanged);
// Use ParallelTask to handle the connection attempts
var delay = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetHappyEyeballsDelay));
var (result, _) = await HappyEyeballsHttp.ParallelTask(
addresses.Length,
(i, token) => AttemptConnection(addresses[i], token),
delay,
mainCancelToken);
if (firstChange == firstPeerChanged)
{
_logger.Debug("First peer status changed.");
// First peer responded first.
if (firstConnection.Status == NetConnectionStatus.Connected)
{
// First peer won!
_logger.Debug("First peer succeeded.");
cancellation.Cancel();
if (secondPeer != null)
{
secondPeer.Peer.Shutdown("First connection attempt won.");
_toCleanNetPeers.Add(secondPeer.Peer);
}
winningPeer = firstPeer;
winningConnection = firstConnection;
}
else
{
// First peer failed, try the second one I guess.
_logger.Debug("First peer failed.");
firstPeer.Peer.Shutdown("You failed.");
_toCleanNetPeers.Add(firstPeer.Peer);
firstReason = await firstPeerChanged;
await secondPeerChanged;
winningPeer = secondPeer;
winningConnection = secondConnection;
}
}
else
{
if (secondConnection!.Status == NetConnectionStatus.Connected)
{
// Second peer won!
_logger.Debug("Second peer succeeded.");
cancellation.Cancel();
firstPeer.Peer.Shutdown("Second connection attempt won.");
_toCleanNetPeers.Add(firstPeer.Peer);
winningPeer = secondPeer;
winningConnection = secondConnection;
}
else
{
// First peer failed, try the second one I guess.
_logger.Debug("Second peer failed.");
secondPeer!.Peer.Shutdown("You failed.");
_toCleanNetPeers.Add(secondPeer.Peer);
firstReason = await firstPeerChanged;
winningPeer = firstPeer;
winningConnection = firstConnection;
}
}
}
else
{
// Only one address to try. Pretty straight forward.
firstReason = await AwaitNonInitStatusChange(firstConnection, mainCancelToken);
winningPeer = firstPeer;
winningConnection = firstConnection;
}
return (result.Peer, result.Connection);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
firstPeer.Peer.Shutdown("Cancelled");
_toCleanNetPeers.Add(firstPeer.Peer);
if (secondPeer != null)
{
// ReSharper disable once PossibleNullReferenceException
secondPeer.Peer.Shutdown("Cancelled");
_toCleanNetPeers.Add(secondPeer.Peer);
}
// Connection attempt was cancelled, nothing to see here
OnConnectFailed("Connection attempt cancelled.");
return null;
}
// winningPeer can still be failed at this point.
// If it is, neither succeeded. RIP.
if (winningConnection!.Status != NetConnectionStatus.Connected)
catch (AggregateException ae)
{
winningPeer!.Peer.Shutdown("You failed");
_toCleanNetPeers.Add(winningPeer.Peer);
OnConnectFailed((secondReason ?? firstReason)!);
// ParallelTask throws AggregateException with all connection failures
// We just take the first one
var message = ae.InnerExceptions.First().Message;
OnConnectFailed(message);
return null;
}
return (winningPeer!, winningConnection);
}
private Task<string> AwaitStatusChange(NetConnection connection, CancellationToken cancellationToken = default)
@@ -471,7 +399,8 @@ namespace Robust.Shared.Network
return tcs.Task;
}
private Task<NetIncomingMessage> AwaitData(NetConnection connection,
private Task<NetIncomingMessage> AwaitData(
NetConnection connection,
CancellationToken cancellationToken = default)
{
if (_awaitingData.ContainsKey(connection))
@@ -529,5 +458,17 @@ namespace Robust.Shared.Network
}
private sealed record JoinRequest(string Hash, string? Hwid);
private sealed class ConnectionAttempt(NetPeerData peer, NetConnection connection, NetManager netManager) : IDisposable
{
public NetPeerData Peer { get; } = peer;
public NetConnection Connection { get; } = connection;
public void Dispose()
{
Peer.Peer.Shutdown("Disposing unused connection attempt");
netManager._toCleanNetPeers.Add(Peer.Peer);
}
}
}
}

View File

@@ -587,6 +587,14 @@ namespace Robust.Shared.Network
public void ClientDisconnect(string reason)
{
DebugTools.Assert(IsClient, "Should never be called on the server.");
// First handle any in-progress connection attempt
if (ClientConnectState != ClientConnectionState.NotConnecting)
{
_cancelConnectTokenSource?.Cancel();
}
// Then handle existing connection if any
if (ServerChannel != null)
{
Disconnect?.Invoke(this, new NetDisconnectedArgs(ServerChannel, reason));

View File

@@ -236,7 +236,8 @@ public abstract partial class SharedJointSystem : EntitySystem
Vector2? anchorB = null,
string? id = null,
TransformComponent? xformA = null,
TransformComponent? xformB = null)
TransformComponent? xformB = null,
int? minimumDistance = null)
{
if (!Resolve(bodyA, ref xformA) || !Resolve(bodyB, ref xformB))
{
@@ -246,9 +247,13 @@ public abstract partial class SharedJointSystem : EntitySystem
anchorA ??= Vector2.Zero;
anchorB ??= Vector2.Zero;
var length = Vector2.Transform(anchorA.Value, xformA.WorldMatrix) - Vector2.Transform(anchorB.Value, xformB.WorldMatrix);
var vecA = Vector2.Transform(anchorA.Value, xformA.WorldMatrix);
var vecB = Vector2.Transform(anchorB.Value, xformB.WorldMatrix);
var length = (vecA - vecB).Length();
if (minimumDistance != null)
length = Math.Max(minimumDistance.Value, length);
var joint = new DistanceJoint(bodyA, bodyB, anchorA.Value, anchorB.Value, length.Length());
var joint = new DistanceJoint(bodyA, bodyB, anchorA.Value, anchorB.Value, length);
id ??= GetJointId(joint);
joint.ID = id;
AddJoint(joint);

View File

@@ -9,7 +9,6 @@
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("OpenToolkit.GraphicsLibraryFramework")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
[assembly: InternalsVisibleTo("Content.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
[assembly: InternalsVisibleTo("Robust.Packaging")]

View File

@@ -38,6 +38,9 @@ public sealed class MultiRootInheritanceGraph<T> where T : notnull
//check for circular inheritance
foreach (var parent in parents)
{
if (EqualityComparer<T>.Default.Equals(parent, id))
throw new InvalidOperationException($"Self Inheritance detected for id \"{id}\"!");
var parentsL1 = GetParents(parent);
if(parentsL1 == null) continue;

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Serialization;
using JetBrains.Annotations;
using NetSerializer;
namespace Robust.Shared.Serialization;
/// <summary>
/// Custom serializer implementation for <see cref="BitArray"/>.
/// </summary>
/// <remarks>
/// <para>
/// This type is necessary as, since .NET 10, the internal layout of <see cref="BitArray"/> was changed.
/// The type now (internally) implements <see cref="ISerializable"/> for backwards compatibility with existing
/// <c>BinaryFormatter</c> code, but NetSerializer does not support <see cref="ISerializable"/>.
/// </para>
/// <para>
/// This code is designed to be backportable &amp; network compatible with the previous behavior on .NET 9.
/// </para>
/// </remarks>
internal sealed class NetBitArraySerializer : IDynamicTypeSerializer
{
// NOTE: MUST be a IDynamicTypeSerializer for compatibility!
// Can be changed in the future.
// For reference, the layout of BitArray before .NET 10 was:
// private int[] m_array;
// private int m_length;
// private int _version;
// NetSerializer serialized these in the following order (sorted by name):
// _version, m_array, m_length
public bool Handles(Type type)
{
return type == typeof(BitArray);
}
public IEnumerable<Type> GetSubtypes(Type type)
{
return [typeof(int[]), typeof(int)];
}
public void GenerateWriterMethod(Serializer serializer, Type type, ILGenerator il)
{
var method = typeof(NetBitArraySerializer).GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic)!;
// arg0: Serializer, arg1: Stream, arg2: value
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.EmitCall(OpCodes.Call, method, null);
il.Emit(OpCodes.Ret);
}
public void GenerateReaderMethod(Serializer serializer, Type type, ILGenerator il)
{
var method = typeof(NetBitArraySerializer).GetMethod("Read", BindingFlags.Static | BindingFlags.NonPublic)!;
// arg0: Serializer, arg1: stream, arg2: out value
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.EmitCall(OpCodes.Call, method, null);
il.Emit(OpCodes.Ret);
}
[UsedImplicitly]
private static void Write(Serializer serializer, Stream stream, BitArray value)
{
var intCount = (31 + value.Length) >> 5;
var ints = new int[intCount];
value.CopyTo(ints, 0);
serializer.SerializeDirect(stream, 0); // _version
serializer.SerializeDirect(stream, ints); // m_array
serializer.SerializeDirect(stream, value.Length); // m_length
}
[UsedImplicitly]
private static void Read(Serializer serializer, Stream stream, out BitArray value)
{
serializer.DeserializeDirect<int>(stream, out _); // _version
serializer.DeserializeDirect<int[]>(stream, out var array); // m_array
serializer.DeserializeDirect<int>(stream, out var length); // m_length
value = new BitArray(array)
{
Length = length
};
}
}

View File

@@ -91,6 +91,7 @@ namespace Robust.Shared.Serialization
MappedStringSerializer.TypeSerializer,
new Vector2Serializer(),
new Matrix3x2Serializer(),
new NetBitArraySerializer()
}
};
_serializer = new Serializer(types, settings);

View File

@@ -91,7 +91,7 @@ namespace Robust.UnitTesting.Shared.GameObjects
Assert.That(sContainerSys.Insert(itemUid, container));
// Modify visibility layer so that the item does not get sent ot the player
sEntManager.System<VisibilitySystem>().AddLayer(itemUid, 10 );
sEntManager.System<SharedVisibilitySystem>().AddLayer(itemUid, 10 );
});
// Needs minimum 4 to sync to client because buffer size is 3
@@ -119,7 +119,7 @@ namespace Robust.UnitTesting.Shared.GameObjects
await server.WaitAssertion(() =>
{
// Modify visibility layer so it now gets sent to the client
sEntManager.System<VisibilitySystem>().RemoveLayer(itemUid, 10 );
sEntManager.System<SharedVisibilitySystem>().RemoveLayer(itemUid, 10 );
});
await server.WaitRunTicks(1);
@@ -219,7 +219,7 @@ namespace Robust.UnitTesting.Shared.GameObjects
sContainerSys.Insert(sItemUid, container);
// Modify visibility layer so that the item does not get sent ot the player
sEntManager.System<VisibilitySystem>().AddLayer(sItemUid, 10 );
sEntManager.System<SharedVisibilitySystem>().AddLayer(sItemUid, 10 );
});
await server.WaitRunTicks(1);

View File

@@ -0,0 +1,128 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects;
[TestFixture]
public sealed partial class EntityManagerCopyTests
{
[Test]
public void CopyComponentGeneric()
{
var instant = RobustServerSimulation.NewSimulation();
instant.RegisterComponents(fac =>
{
fac.RegisterClass<AComponent>();
});
var sim = instant.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(out var mapId);
var original = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
var comp = entManager.AddComponent<AComponent>(original);
Assert.That(comp.Value, Is.EqualTo(false));
comp.Value = true;
var target = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(!entManager.HasComponent<AComponent>(target));
var targetComp = entManager.CopyComponent(original, target, comp);
Assert.That(targetComp!.Owner == target);
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
}
[Test]
public void CopyComponentNonGeneric()
{
var instant = RobustServerSimulation.NewSimulation();
instant.RegisterComponents(fac =>
{
fac.RegisterClass<AComponent>();
});
var sim = instant.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(out var mapId);
var original = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
var comp = entManager.AddComponent<AComponent>(original);
Assert.That(comp.Value, Is.EqualTo(false));
comp.Value = true;
var target = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(!entManager.HasComponent<AComponent>(target));
var targetComp = entManager.CopyComponent(original, target, (IComponent) comp);
Assert.That(targetComp!.Owner == target);
Assert.That(((AComponent) targetComp).Value, Is.EqualTo(comp.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
}
[Test]
public void CopyComponentMultiple()
{
var instant = RobustServerSimulation.NewSimulation();
instant.RegisterComponents(fac =>
{
fac.RegisterClass<AComponent>();
fac.RegisterClass<BComponent>();
});
var sim = instant.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(out var mapId);
var original = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
var comp = entManager.AddComponent<AComponent>(original);
var comp2 = entManager.AddComponent<BComponent>(original);
Assert.That(comp.Value, Is.EqualTo(false));
comp.Value = true;
var target = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(!entManager.HasComponent<AComponent>(target));
entManager.CopyComponents(original, target, null, comp, comp2);
var targetComp = entManager.GetComponent<AComponent>(target);
var targetComp2 = entManager.GetComponent<BComponent>(target);
Assert.That(targetComp!.Owner == target);
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(targetComp2!.Owner == target);
Assert.That(targetComp2.Value, Is.EqualTo(comp2.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
Assert.That(!ReferenceEquals(comp2, targetComp2));
}
[DataDefinition]
private sealed partial class AComponent : Component
{
[DataField]
public bool Value = false;
}
[DataDefinition]
private sealed partial class BComponent : Component
{
[DataField]
public bool Value = false;
}
}

View File

@@ -1,12 +1,14 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture, Parallelizable]
sealed class EntityManagerTests
sealed partial class EntityManagerTests
{
private static ISimulation SimulationFactory()
{

View File

@@ -104,4 +104,11 @@ public sealed class MultiRootGraphTest
Assert.Throws<InvalidOperationException>(() => graph.Add(Id3, new[] { Id1 }));
}
[Test]
public void IsOwnParentTest()
{
var graph = new MultiRootInheritanceGraph<string>();
Assert.Throws<InvalidOperationException>(() => graph.Add(Id1, new []{ Id1 }));
}
}

View File

@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
_testDir = Directory.CreateDirectory(_testDirPath);
var subDir = Path.Combine(_testDirPath, "writable");
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
}
[OneTimeTearDown]

View File

@@ -1,8 +1,18 @@
# schema file for Yamale
meta:
format: int()
postmapinit: bool()
postmapinit: bool(required=False)
time: str(required=False) # timestamp() expects yyyy-mm-dd
category: enum("Unknown", "Entity", "Grid", "Map", "Save", required=False) # FileCategory enum
engineVersion: str(required=False)
entityCount: int(required=False)
forkId: str(required=False)
forkVersion: str(required=False)
tilemap: map(str(), key=int())
orphans: list(int(), required=False)
nullspace: list(int(), required=False)
maps: list(int(), required=False)
grids: list(int(), required=False)
entities: list(include('proto'), min=1)
---
proto:
@@ -14,64 +24,3 @@ entity:
components: list(comp())
missingComponents: list(str(), required=False)
# Example
# meta:
# format: 3
# name: DemoStation
# author: Space-Wizards
# postmapinit: false
# tilemap:
# 0: space
# 1: floor_asteroid_coarse_sand0
# 2: floor_asteroid_coarse_sand1
# 3: floor_asteroid_coarse_sand2
# 4: floor_asteroid_coarse_sand_dug
# 5: floor_asteroid_sand
# 6: floor_asteroid_tile
# 7: floor_blue
# 8: floor_dark
# 9: floor_elevator_shaft
# 10: floor_freezer
# 11: floor_glass
# 12: floor_gold
# 13: floor_green_circuit
# 14: floor_hydro
# 15: floor_lino
# 16: floor_mono
# 17: floor_reinforced
# 18: floor_rglass
# 19: floor_rock_vault
# 20: floor_showroom
# 21: floor_snow
# 22: floor_steel
# 23: floor_steel_dirty
# 24: floor_techmaint
# 25: floor_warning1
# 26: floor_warning2
# 27: floor_white
# 28: floor_white_warning1
# 29: floor_white_warning2
# 30: floor_wood
# 31: lattice
# 32: plating
# 33: plating
# entities:
# - uid: 0
# components:
# - parent: null
# type: Transform
# - index: 0
# chunks:
# - ind: "-1,-1"
# tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgAAAA==
# type: MapGrid
# - linearDamping: 0.05
# fixtures: []
# bodyType: Dynamic
# type: Physics
# - uid: 1
# type: SpawnPointLatejoin
# components:
# - parent: 0
# pos: 0,0
# type: Transform