mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0a4446fb6 | ||
|
|
64307228cc | ||
|
|
702c616dfe | ||
|
|
7d493f3f7e | ||
|
|
18849be0b4 | ||
|
|
c6a1d82bb1 | ||
|
|
d89e1a43c6 | ||
|
|
d894ef70ef | ||
|
|
c7ea2793ca | ||
|
|
0c61ff2bee | ||
|
|
343a34eac7 | ||
|
|
7be41f4890 | ||
|
|
293470a5fe | ||
|
|
2b8057acf0 | ||
|
|
bec3caa5da | ||
|
|
ea6126563b | ||
|
|
00494ad9eb | ||
|
|
6672b7b1bd | ||
|
|
8dc55e8748 | ||
|
|
44ea2cd396 | ||
|
|
2c5604432b | ||
|
|
c696466522 | ||
|
|
01bb98e400 | ||
|
|
af08e747de |
@@ -57,7 +57,7 @@
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<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" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -54,6 +54,55 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 262.0.2
|
||||
|
||||
|
||||
## 262.0.1
|
||||
|
||||
|
||||
## 262.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Toolshed commands will now validate that each non-generic command argument is parseable (i.e., has a corresponding type parser). This check can be disabled by explicitly marking the argument as unparseable via `CommandArgumentAttribute.Unparseable`.
|
||||
|
||||
### New features
|
||||
|
||||
* `ToolshedManager.TryParse` now also supports nullable value types.
|
||||
* Add an ignoredComponents arg to IsDefault.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `SpriteComponent.Layer.Visible` setter not marking a sprite's bounding box as dirty.
|
||||
* The audio params in the passed SoundSpecifier for PlayStatic(SoundSpecifier, Filter, ...) will now be used as a default like other PlayStatic overrides.
|
||||
* Fix windows not saving their positions correctly when their x position is <= 0.
|
||||
* Fix transform state handling overriding PVS detachment.
|
||||
|
||||
|
||||
## 261.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Implement IEquatable for ResolvedPathSpecifier & ResolvedCollectionSpecifier.
|
||||
* Add NearestChunkEnumerator.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix static entities not having the center of mass updated.
|
||||
* Fix TryQueueDelete.
|
||||
* Fix tpto potentially parenting grids to non-map entities.
|
||||
|
||||
### Other
|
||||
|
||||
* TileChangedEvent is now raised once in clientside grid state handling rather than per tile.
|
||||
* Removed ITileDefinition.ID as it was redundant.
|
||||
* Change the lifestage checks on predicted entity deletion to check for terminating.
|
||||
|
||||
### Internal
|
||||
|
||||
* Update some `GetComponentName<T>` uses to generic.
|
||||
|
||||
|
||||
## 261.1.0
|
||||
|
||||
### New features
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
|
||||
{
|
||||
// TODO SPRITE optimize this
|
||||
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
|
||||
// Because the just take the BB of the rotated BB, I'm pretty sure we do a lot of unnecessary maths.
|
||||
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
|
||||
}
|
||||
|
||||
|
||||
@@ -387,7 +387,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)
|
||||
|
||||
@@ -296,7 +296,7 @@ namespace Robust.Client.GameObjects
|
||||
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
|
||||
{
|
||||
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
@@ -322,7 +322,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
if (IsQueuedForDeletion(ent.Owner)
|
||||
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1224,6 +1224,8 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
_visible = value;
|
||||
|
||||
Owner.Comp.BoundsDirty = true;
|
||||
|
||||
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
||||
if (_parent.Owner != EntityUid.Invalid)
|
||||
Owner.Comp.Sys?.QueueUpdateIsInert(Owner);
|
||||
@@ -1791,76 +1793,15 @@ namespace Robust.Client.GameObjects
|
||||
[Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")]
|
||||
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
|
||||
{
|
||||
var results = new List<IDirectionalTextureProvider>();
|
||||
noRot = false;
|
||||
|
||||
// TODO when moving to a non-static method in a system, pass in IComponentFactory
|
||||
if (prototype.TryGetComponent(out IconComponent? icon))
|
||||
{
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
results.Add(sys.GetIcon(icon));
|
||||
return results;
|
||||
}
|
||||
|
||||
if (!prototype.Components.TryGetValue("Sprite", out _))
|
||||
{
|
||||
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
|
||||
return results;
|
||||
}
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy, spriteComponent);
|
||||
|
||||
foreach (var layer in spriteComponent.AllLayers)
|
||||
{
|
||||
if (!layer.Visible) continue;
|
||||
|
||||
if (layer.Texture != null)
|
||||
{
|
||||
results.Add(layer.Texture);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!layer.RsiState.IsValid) continue;
|
||||
|
||||
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
|
||||
if (rsi == null ||
|
||||
!rsi.TryGetState(layer.RsiState, out var state))
|
||||
continue;
|
||||
|
||||
results.Add(state);
|
||||
}
|
||||
|
||||
noRot = spriteComponent.NoRotation;
|
||||
|
||||
entityManager.DeleteEntity(dummy);
|
||||
|
||||
if (results.Count == 0)
|
||||
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
|
||||
|
||||
return results;
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
return sys.GetPrototypeTextures(prototype, out noRot);
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")]
|
||||
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
{
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
// TODO when moving to a non-static method in a system, pass in IComponentFactory
|
||||
if (prototype.TryGetComponent(out IconComponent? icon))
|
||||
return sys.GetIcon(icon);
|
||||
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
return sys.GetFallbackState();
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
var result = spriteComponent.Icon ?? sys.GetFallbackState();
|
||||
entityManager.DeleteEntity(dummy);
|
||||
|
||||
return result;
|
||||
return sys.GetPrototypeIcon(prototype);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,6 @@ public sealed partial class SpriteSystem
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(string prototype)
|
||||
{
|
||||
// Check if this prototype has been cached before, and if so return the result.
|
||||
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
|
||||
return cachedResult;
|
||||
|
||||
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
|
||||
{
|
||||
// The specified prototype doesn't exist, return the fallback "error" sprite.
|
||||
@@ -67,11 +63,7 @@ public sealed partial class SpriteSystem
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Generate the icon and cache it in case it's ever needed again.
|
||||
var result = GetPrototypeIcon(entityPrototype);
|
||||
_cachedPrototypeIcons[prototype] = result;
|
||||
|
||||
return result;
|
||||
return GetPrototypeIcon(entityPrototype);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,13 +71,19 @@ public sealed partial class SpriteSystem
|
||||
/// This method does NOT cache the result.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
|
||||
{
|
||||
// This method may spawn & delete an entity to get an accruate RSI state, hence we cache the results
|
||||
if (_cachedPrototypeIcons.TryGetValue(prototype.ID, out var cachedResult))
|
||||
return cachedResult;
|
||||
|
||||
return _cachedPrototypeIcons[prototype.ID] = GetPrototypeIconInternal(prototype);
|
||||
}
|
||||
|
||||
private IRsiStateLike GetPrototypeIconInternal(EntityPrototype prototype)
|
||||
{
|
||||
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
|
||||
if (prototype.Components.TryGetValue("Icon", out var compData)
|
||||
&& compData.Component is IconComponent icon)
|
||||
{
|
||||
if (prototype.TryGetComponent(out IconComponent? icon, _factory))
|
||||
return GetIcon(icon);
|
||||
}
|
||||
|
||||
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
@@ -102,6 +100,63 @@ public sealed partial class SpriteSystem
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto) =>
|
||||
GetPrototypeTextures(proto, out _);
|
||||
|
||||
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto, out bool noRot)
|
||||
{
|
||||
var results = new List<IDirectionalTextureProvider>();
|
||||
noRot = false;
|
||||
|
||||
if (proto.TryGetComponent(out IconComponent? icon, _factory))
|
||||
{
|
||||
results.Add(GetIcon(icon));
|
||||
return results;
|
||||
}
|
||||
|
||||
if (!proto.Components.ContainsKey("Sprite"))
|
||||
{
|
||||
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
|
||||
return results;
|
||||
}
|
||||
|
||||
var dummy = Spawn(proto.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
|
||||
|
||||
// TODO SPRITE is this needed?
|
||||
// And if it is, shouldn't GetPrototypeIconInternal also use this?
|
||||
_appearance.OnChangeData(dummy, spriteComponent);
|
||||
|
||||
foreach (var layer in spriteComponent.AllLayers)
|
||||
{
|
||||
if (!layer.Visible)
|
||||
continue;
|
||||
|
||||
if (layer.Texture != null)
|
||||
{
|
||||
results.Add(layer.Texture);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!layer.RsiState.IsValid)
|
||||
continue;
|
||||
|
||||
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
|
||||
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
|
||||
continue;
|
||||
|
||||
results.Add(state);
|
||||
}
|
||||
|
||||
noRot = spriteComponent.NoRotation;
|
||||
Del(dummy);
|
||||
|
||||
if (results.Count == 0)
|
||||
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetFallbackState()
|
||||
{
|
||||
|
||||
@@ -34,8 +34,12 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
|
||||
// Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup()
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _tree = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
|
||||
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
|
||||
@@ -631,7 +631,7 @@ namespace Robust.Client.GameStates
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
|
||||
|
||||
if (compState != null)
|
||||
if ((meta.Flags & MetaDataFlags.Detached) == 0 && compState != null)
|
||||
{
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);
|
||||
|
||||
@@ -145,6 +145,11 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
// This is to avoid unnecessarily setting a position where our size isn't yet fully updated.
|
||||
// This most commonly happens with saved window positions if your window position is <= 0.
|
||||
if (!IsMeasureValid)
|
||||
return;
|
||||
|
||||
var (spaceX, spaceY) = Parent!.Size;
|
||||
|
||||
var maxX = spaceX - ((AllowOffScreen & DirectionFlag.West) == 0 ? Size.X : WindowEdgeSeparation);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Robust.Server.Placement
|
||||
{
|
||||
// Replace existing entities if relevant.
|
||||
if (msg.Replacement && _prototype.Index<EntityPrototype>(entityTemplateName).Components.TryGetValue(
|
||||
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
|
||||
_factory.GetComponentName<PlacementReplacementComponent>(), out var compRegistry))
|
||||
{
|
||||
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
|
||||
var gridUid = _xformSystem.GetGrid(coordinates);
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace Robust.Shared.Audio;
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public abstract partial class ResolvedSoundSpecifier {
|
||||
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")]
|
||||
@@ -22,8 +23,10 @@ public abstract partial class ResolvedSoundSpecifier {
|
||||
/// <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 {
|
||||
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s)
|
||||
{
|
||||
return s switch
|
||||
{
|
||||
null => true,
|
||||
ResolvedPathSpecifier path => path.Path.ToString() == "",
|
||||
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
|
||||
@@ -37,7 +40,8 @@ public abstract partial class ResolvedSoundSpecifier {
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedPathSpecifier>
|
||||
{
|
||||
/// <summary>
|
||||
/// The resource path of the sound.
|
||||
/// </summary>
|
||||
@@ -57,6 +61,21 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
|
||||
{
|
||||
}
|
||||
|
||||
public bool Equals(ResolvedPathSpecifier? other)
|
||||
{
|
||||
return Path.Equals(other?.Path);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as ResolvedPathSpecifier);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Path.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +83,9 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
|
||||
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedCollectionSpecifier>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
|
||||
/// </summary>
|
||||
@@ -87,4 +108,19 @@ public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier
|
||||
Collection = collection;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public bool Equals(ResolvedCollectionSpecifier? other)
|
||||
{
|
||||
return Collection.Equals(other?.Collection) && Index.Equals(other?.Index);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as ResolvedCollectionSpecifier);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Collection, Index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,7 +665,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(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
|
||||
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -137,9 +137,10 @@ public sealed class TeleportToCommand : LocalizedEntityCommands
|
||||
}
|
||||
}
|
||||
|
||||
var targetMapCoords = _transform.ToMapCoordinates(targetCoords);
|
||||
foreach (var victim in victims)
|
||||
{
|
||||
_transform.SetCoordinates(victim.Entity, targetCoords);
|
||||
_transform.SetMapCoordinates(victim.Entity, targetMapCoords);
|
||||
_transform.AttachToGridOrMap(victim.Entity, victim.Transform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
internal string GetPath(ResPath relPath)
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
|
||||
// Sanitise platform-specific path and standardize it for engine use.
|
||||
.Replace(Path.DirectorySeparatorChar, '/');
|
||||
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +379,10 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,20 +547,20 @@ public sealed class EntityDeserializer :
|
||||
_stopwatch.Restart();
|
||||
foreach (var (entity, data) in Entities)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
CurrentReadingEntity = data;
|
||||
LoadEntity(entity, _metaQuery.Comp(entity), data.Components, data.MissingComponents);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#else
|
||||
ToDelete.Add(entity);
|
||||
_log.Error($"Encountered error while loading entity. Yaml uid: {data.YamlId}. Loaded loaded entity: {EntMan.ToPrettyString(entity)}. Error:\n{e}.");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
CurrentReadingEntity = null;
|
||||
|
||||
@@ -162,8 +162,8 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
_log = _logMan.GetSawmill("entity_serializer");
|
||||
SerializerProvider.RegisterSerializer(this);
|
||||
|
||||
_metaName = _factory.GetComponentName(typeof(MetaDataComponent));
|
||||
_xformName = _factory.GetComponentName(typeof(TransformComponent));
|
||||
_metaName = _factory.GetComponentName<MetaDataComponent>();
|
||||
_xformName = _factory.GetComponentName<TransformComponent>();
|
||||
_emptyMetaNode = _serialization.WriteValueAs<MappingDataNode>(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: this);
|
||||
|
||||
CurrentComponent = _xformName;
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Returns true if the entity's data (apart from transform) is default.
|
||||
/// </summary>
|
||||
public bool IsDefault(EntityUid uid)
|
||||
public bool IsDefault(EntityUid uid, ICollection<string>? ignoredComps = null)
|
||||
{
|
||||
if (!MetaQuery.TryGetComponent(uid, out var metadata) || metadata.EntityPrototype == null)
|
||||
return false;
|
||||
@@ -195,6 +195,9 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
var compName = _componentFactory.GetComponentName(compType);
|
||||
|
||||
if (ignoredComps?.Contains(compName) == true)
|
||||
continue;
|
||||
|
||||
// If the component isn't on the prototype then it's custom.
|
||||
if (!protoData.TryGetValue(compName, out var protoMapping))
|
||||
return false;
|
||||
@@ -498,7 +501,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (Deleted(uid.Value))
|
||||
return false;
|
||||
|
||||
if (!QueuedDeletionsSet.Add(uid.Value))
|
||||
if (QueuedDeletionsSet.Contains(uid.Value))
|
||||
return false;
|
||||
|
||||
QueueDeleteEntity(uid);
|
||||
|
||||
@@ -213,7 +213,7 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
if (xform.ParentUid != xform.MapUid && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
|
||||
{
|
||||
Log.Error($"Grid {ToPrettyString(uid, meta)} is not parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}");
|
||||
Log.Error($"Grid {ToPrettyString(uid, meta)} is parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,6 +338,8 @@ public abstract partial class SharedMapSystem
|
||||
chunk.SuppressCollisionRegeneration = true;
|
||||
DebugTools.Assert(data.TileData.Any(x => !x.IsEmpty));
|
||||
DebugTools.Assert(data.TileData.Length == component.ChunkSize * component.ChunkSize);
|
||||
var changedEntry = new ValueList<TileChangedEntry>();
|
||||
|
||||
for (ushort x = 0; x < component.ChunkSize; x++)
|
||||
{
|
||||
for (ushort y = 0; y < component.ChunkSize; y++)
|
||||
@@ -346,12 +348,15 @@ public abstract partial class SharedMapSystem
|
||||
if (!chunk.TrySetTile(x, y, tile, out var oldTile, out _))
|
||||
continue;
|
||||
|
||||
var gridIndices = chunk.ChunkTileToGridTile((x, y));
|
||||
var newTileRef = new TileRef(uid, gridIndices, tile);
|
||||
_mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index);
|
||||
|
||||
var chunkIndex = new Vector2i(x, y);
|
||||
var gridIndices = chunk.ChunkTileToGridTile(chunkIndex);
|
||||
changedEntry.Add(new TileChangedEntry(tile, oldTile, chunk.Indices, gridIndices));
|
||||
}
|
||||
}
|
||||
|
||||
var ev = new TileChangedEvent(gridEnt, changedEntry.ToArray());
|
||||
EntityManager.EventBus.RaiseLocalEvent(gridEnt.Owner, ref ev, true);
|
||||
DebugTools.Assert(chunk.Fixtures.SetEquals(data.Fixtures));
|
||||
|
||||
// These should never refer to the same object
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Map.Components;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
|
||||
64
Robust.Shared/Map/Enumerators/NearestChunkEnumerator.cs
Normal file
64
Robust.Shared/Map/Enumerators/NearestChunkEnumerator.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.Map.Enumerators;
|
||||
|
||||
/// <summary>
|
||||
/// Iterates chunk indices but prefers ones closer towards the center first.
|
||||
/// </summary>
|
||||
public record struct NearestChunkEnumerator
|
||||
{
|
||||
private readonly Vector2i _chunkLB;
|
||||
private readonly Vector2i _chunkRT;
|
||||
|
||||
private ValueList<Vector2i> _chunks = new();
|
||||
|
||||
private int _n;
|
||||
|
||||
public NearestChunkEnumerator(Box2 localAABB, int chunkSize)
|
||||
{
|
||||
_chunkLB = (localAABB.BottomLeft / chunkSize).Floored();
|
||||
_chunkRT = (localAABB.TopRight / chunkSize).Floored();
|
||||
InitializeChunks(new Vector2i(chunkSize, chunkSize));
|
||||
}
|
||||
|
||||
public NearestChunkEnumerator(Box2 localAABB, Vector2i chunkSize)
|
||||
{
|
||||
_chunkLB = (localAABB.BottomLeft / chunkSize).Floored();
|
||||
_chunkRT = (localAABB.TopRight / chunkSize).Floored();
|
||||
InitializeChunks(chunkSize);
|
||||
}
|
||||
|
||||
private void InitializeChunks(Vector2i chunkSize)
|
||||
{
|
||||
var bl = (Vector2)_chunkLB * chunkSize;
|
||||
var tr = (Vector2)_chunkRT * chunkSize;
|
||||
var halfChunk = new Vector2(chunkSize.X / 2f, chunkSize.Y / 2f);
|
||||
|
||||
var center = (tr - bl) / 2 + bl;
|
||||
|
||||
for (var x = _chunkLB.X; x < _chunkRT.X; x++)
|
||||
{
|
||||
for (var y = _chunkLB.Y; y < _chunkRT.Y; y++)
|
||||
{
|
||||
_chunks.Add(new Vector2i(x, y) * chunkSize);
|
||||
}
|
||||
}
|
||||
|
||||
_chunks.Sort((c1, c2) => ((c1 + halfChunk) - center).LengthSquared().CompareTo(((c2 + halfChunk) - center).LengthSquared()));
|
||||
}
|
||||
|
||||
public bool MoveNext([NotNullWhen(true)] out Vector2i? indices)
|
||||
{
|
||||
if (_n >= _chunks.Count)
|
||||
{
|
||||
indices = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
indices = _chunks[_n++] ;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,6 @@ namespace Robust.Shared.Map
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Internal name of the definition.
|
||||
/// </summary>
|
||||
string ID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the sprite to draw.
|
||||
/// </summary>
|
||||
|
||||
@@ -312,13 +312,6 @@ public partial class SharedPhysicsSystem
|
||||
body._inertia += data.I;
|
||||
}
|
||||
|
||||
// Update this after re-calculating mass as content may want to use the sum of fixture masses instead.
|
||||
if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) != 0)
|
||||
{
|
||||
body._localCenter = Vector2.Zero;
|
||||
return;
|
||||
}
|
||||
|
||||
if (body._mass > 0.0f)
|
||||
{
|
||||
body._invMass = 1.0f / body._mass;
|
||||
@@ -348,18 +341,23 @@ public partial class SharedPhysicsSystem
|
||||
var oldCenter = body._localCenter;
|
||||
body._localCenter = localCenter;
|
||||
|
||||
// Update center of mass velocity.
|
||||
var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
|
||||
if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) == 0)
|
||||
{
|
||||
// Update center of mass velocity.
|
||||
var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
|
||||
|
||||
if (comVelocityDiff != Vector2.Zero)
|
||||
{
|
||||
body.LinearVelocity += comVelocityDiff;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
if (comVelocityDiff != Vector2.Zero)
|
||||
{
|
||||
body.LinearVelocity += comVelocityDiff;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
}
|
||||
|
||||
if (body._mass == oldMass && body._inertia == oldInertia && oldCenter == localCenter)
|
||||
return;
|
||||
|
||||
// we always do the full mass and COM calculation and raise this, even for static bodies as content may need the info
|
||||
// examples are stations anchored with the station anchor, shuttles landed on planets, or grids getting an atmosphere above a certain mass threshold
|
||||
var ev = new MassDataChangedEvent((uid, body, manager), oldMass, oldInertia, oldCenter);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
@@ -512,7 +510,6 @@ public partial class SharedPhysicsSystem
|
||||
|
||||
var oldType = body.BodyType;
|
||||
body.BodyType = value;
|
||||
ResetMassData(uid, manager, body);
|
||||
|
||||
body.Force = Vector2.Zero;
|
||||
body.Torque = 0f;
|
||||
|
||||
@@ -170,9 +170,9 @@ namespace Robust.Shared.Prototypes
|
||||
|
||||
[Obsolete("Pass in IComponentFactory")]
|
||||
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component)
|
||||
where T : IComponent
|
||||
where T : IComponent, new()
|
||||
{
|
||||
var compName = IoCManager.Resolve<IComponentFactory>().GetComponentName(typeof(T));
|
||||
var compName = IoCManager.Resolve<IComponentFactory>().GetComponentName<T>();
|
||||
return TryGetComponent(compName, out component);
|
||||
}
|
||||
|
||||
@@ -182,9 +182,9 @@ namespace Robust.Shared.Prototypes
|
||||
return TryGetComponent(compName, out component);
|
||||
}
|
||||
|
||||
public bool TryGetComponent<T>(string name, [NotNullWhen(true)] out T? component) where T : IComponent
|
||||
public bool TryGetComponent<T>(string name, [NotNullWhen(true)] out T? component) where T : IComponent, new()
|
||||
{
|
||||
DebugTools.AssertEqual(IoCManager.Resolve<IComponentFactory>().GetComponentName(typeof(T)), name);
|
||||
DebugTools.AssertEqual(IoCManager.Resolve<IComponentFactory>().GetComponentName<T>(), name);
|
||||
|
||||
if (!Components.TryGetValue(name, out var componentUnCast))
|
||||
{
|
||||
|
||||
@@ -46,8 +46,9 @@ public sealed class PipedArgumentAttribute : Attribute;
|
||||
[MeansImplicitUse]
|
||||
public sealed class CommandArgumentAttribute : Attribute
|
||||
{
|
||||
public CommandArgumentAttribute(Type? customParser = null)
|
||||
public CommandArgumentAttribute(Type? customParser = null, bool unparseable = false)
|
||||
{
|
||||
Unparseable = unparseable;
|
||||
if (customParser == null)
|
||||
return;
|
||||
|
||||
@@ -56,6 +57,13 @@ public sealed class CommandArgumentAttribute : Attribute
|
||||
$"Custom parser {customParser.PrettyName()} does not inherit from {typeof(CustomTypeParser<>).PrettyName()}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Command initialization will validate that all of a command's arguments are parseable (i.e., have a type parser).
|
||||
/// In some niche situations you might want to have a command that accepts unparseable arguments that must be
|
||||
/// supplied via a toolshed variable or command block, in which case the check can be disabled via this property.
|
||||
/// </summary>
|
||||
public bool Unparseable { get; }
|
||||
|
||||
public Type? CustomParser { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ internal sealed class TpCommand : ToolshedCommand
|
||||
{
|
||||
private SharedTransformSystem? _xform;
|
||||
|
||||
// TODO TOOLSHED
|
||||
// add EntityCoordinates parser
|
||||
|
||||
[CommandImplementation("coords")]
|
||||
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, EntityCoordinates target)
|
||||
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, [CommandArgument(unparseable:true)] EntityCoordinates target)
|
||||
{
|
||||
_xform ??= GetSys<SharedTransformSystem>();
|
||||
_xform.SetCoordinates(teleporter, target);
|
||||
@@ -20,7 +23,7 @@ internal sealed class TpCommand : ToolshedCommand
|
||||
}
|
||||
|
||||
[CommandImplementation("coords")]
|
||||
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, EntityCoordinates target)
|
||||
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, [CommandArgument(unparseable:true)] EntityCoordinates target)
|
||||
=> teleporters.Select(x => TpCoords(x, target));
|
||||
|
||||
[CommandImplementation("to")]
|
||||
|
||||
@@ -124,12 +124,12 @@ public abstract partial class ToolshedCommand
|
||||
{
|
||||
var hasAnyAttribute = false;
|
||||
|
||||
if (param.HasCustomAttribute<CommandArgumentAttribute>())
|
||||
if (param.GetCustomAttribute<CommandArgumentAttribute>() is {} cmdAttr)
|
||||
{
|
||||
if (param.Name == null || !argNames.Add(param.Name))
|
||||
throw new InvalidCommandImplementation($"Command arguments must have a unique name");
|
||||
hasAnyAttribute = true;
|
||||
ValidateArg(param);
|
||||
ValidateArg(param, cmdAttr);
|
||||
}
|
||||
|
||||
if (param.HasCustomAttribute<PipedArgumentAttribute>())
|
||||
@@ -232,8 +232,18 @@ public abstract partial class ToolshedCommand
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateArg(ParameterInfo arg)
|
||||
private void ValidateArg(ParameterInfo arg, CommandArgumentAttribute? cmdAttr = null)
|
||||
{
|
||||
if (cmdAttr == null || cmdAttr.CustomParser == null && !cmdAttr.Unparseable)
|
||||
{
|
||||
// This checks that each argument has a corresponding type parser, as people have sometimes created a command
|
||||
// without realising that the type is unparseable.
|
||||
var t = Nullable.GetUnderlyingType(arg.ParameterType) ?? arg.ParameterType;
|
||||
var ignore = t.IsGenericType || t.IsArray || t.ContainsGenericParameters;
|
||||
if (!ignore && Toolshed.GetParserForType(t) == null)
|
||||
throw new InvalidCommandImplementation($"{Name} command argument of type {t.PrettyName()} has no type parser. You either need to add a type parser or explicitly mark the argument as unparseable.");
|
||||
}
|
||||
|
||||
var isParams = arg.HasCustomAttribute<ParamArrayAttribute>();
|
||||
if (!isParams)
|
||||
return;
|
||||
|
||||
@@ -231,7 +231,10 @@ public sealed partial class ToolshedManager
|
||||
/// <returns>Success.</returns>
|
||||
public bool TryParse<T>(ParserContext parserContext, [NotNullWhen(true)] out T? parsed)
|
||||
{
|
||||
var res = TryParse(parserContext, typeof(T), out var p);
|
||||
var t = typeof(T);
|
||||
t = Nullable.GetUnderlyingType(t) ?? t;
|
||||
var res = TryParse(parserContext, t, out var p);
|
||||
|
||||
if (p is not null)
|
||||
parsed = (T?) p;
|
||||
else
|
||||
|
||||
@@ -81,7 +81,7 @@ public abstract class SpanLikeTypeParser<T, TElem> : TypeParser<T>
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromHint(typeof(T).PrettyName());
|
||||
return CompletionResult.FromHint(GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ using Robust.Shared.Player;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Utility;
|
||||
using AppearanceSystem = Robust.Client.GameObjects.AppearanceSystem;
|
||||
using InputSystem = Robust.Server.GameObjects.InputSystem;
|
||||
using MapSystem = Robust.Server.GameObjects.MapSystem;
|
||||
using PointLightComponent = Robust.Client.GameObjects.PointLightComponent;
|
||||
@@ -141,6 +142,7 @@ namespace Robust.UnitTesting
|
||||
systems.LoadExtraSystemType<RecursiveMoveSystem>();
|
||||
systems.LoadExtraSystemType<SpriteSystem>();
|
||||
systems.LoadExtraSystemType<SpriteTreeSystem>();
|
||||
systems.LoadExtraSystemType<AppearanceSystem>();
|
||||
systems.LoadExtraSystemType<GridChunkBoundsDebugSystem>();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -136,35 +136,39 @@ public sealed class PvsPauseTest : RobustIntegrationTest
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: false, clientPaused: true);
|
||||
|
||||
// Unpause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
// Unpause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, false));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, false));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: false, clientPaused: false);
|
||||
}
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: false, clientPaused: false);
|
||||
}
|
||||
|
||||
// Pause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
// Pause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, true));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, true));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: false, clientPaused: true);
|
||||
}
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: false, clientPaused: true);
|
||||
}
|
||||
|
||||
await client.WaitPost(() => netMan.ClientDisconnect(""));
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
Robust.UnitTesting/Server/GameStates/PvsResetTest.cs
Normal file
134
Robust.UnitTesting/Server/GameStates/PvsResetTest.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.UnitTesting.Server.GameStates;
|
||||
|
||||
public sealed class PvsResetTest : RobustIntegrationTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Check that the client doesn't reset dirty detached entities. They should remain in nullspace.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ResetTest()
|
||||
{
|
||||
var server = StartServer();
|
||||
var client = StartClient();
|
||||
|
||||
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
|
||||
|
||||
var sEntMan = server.EntMan;
|
||||
var confMan = server.CfgMan;
|
||||
var sPlayerMan = server.PlayerMan;
|
||||
var xforms = sEntMan.System<SharedTransformSystem>();
|
||||
|
||||
var cEntMan = client.EntMan;
|
||||
var cPlayerMan = client.PlayerMan;
|
||||
var netMan = client.ResolveDependency<IClientNetManager>();
|
||||
|
||||
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
|
||||
client.Post(() => netMan.ClientConnect(null!, 0, null!));
|
||||
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
|
||||
|
||||
async Task RunTicks()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
}
|
||||
|
||||
await RunTicks();
|
||||
|
||||
// Set up map and spawn player
|
||||
EntityUid sMap = default;
|
||||
EntityUid playerUid = default;
|
||||
EntityUid sEnt = default;
|
||||
EntityCoordinates coords = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
sMap = server.System<SharedMapSystem>().CreateMap();
|
||||
coords = new(sMap, default);
|
||||
|
||||
playerUid = sEntMan.SpawnEntity(null, coords);
|
||||
sEnt = sEntMan.SpawnEntity(null, coords);
|
||||
// Attach player.
|
||||
var session = sPlayerMan.Sessions.First();
|
||||
server.PlayerMan.SetAttachedEntity(session, playerUid);
|
||||
sPlayerMan.JoinGame(session);
|
||||
});
|
||||
|
||||
await RunTicks();
|
||||
var farAway = new EntityCoordinates(sMap, new Vector2(100, 100));
|
||||
var netEnt = sEntMan.GetNetEntity(sEnt);
|
||||
var player = sEntMan.GetNetEntity(playerUid);
|
||||
Assert.That(player, Is.Not.EqualTo(NetEntity.Invalid));
|
||||
Assert.That(netEnt, Is.Not.EqualTo(NetEntity.Invalid));
|
||||
|
||||
// Check player got properly attached, and has received the other entity.
|
||||
Assert.That(cEntMan.TryGetEntity(netEnt, out var uid));
|
||||
Assert.That(cEntMan.TryGetEntity(player, out var cPlayerUid));
|
||||
var cEnt = uid!.Value;
|
||||
Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(cPlayerUid));
|
||||
var cMap = cEntMan.GetEntity(sEntMan.GetNetEntity(sMap));
|
||||
|
||||
void AssertDetached(bool detached)
|
||||
{
|
||||
var cXform = client.Transform(cEnt);
|
||||
var sXform = server.Transform(sEnt);
|
||||
var meta = client.MetaData(cEnt);
|
||||
|
||||
Assert.That(sXform.MapUid, Is.EqualTo(sMap));
|
||||
Assert.That(sXform.ParentUid, Is.EqualTo(sMap));
|
||||
|
||||
if (detached)
|
||||
{
|
||||
Assert.That(meta.Flags.HasFlag(MetaDataFlags.Detached));
|
||||
Assert.That(cXform.MapUid, Is.Null);
|
||||
Assert.That(cXform.ParentUid, Is.EqualTo(EntityUid.Invalid));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(!meta.Flags.HasFlag(MetaDataFlags.Detached));
|
||||
Assert.That(cXform.MapUid, Is.EqualTo(cMap));
|
||||
Assert.That(cXform.ParentUid, Is.EqualTo(cMap));
|
||||
}
|
||||
}
|
||||
|
||||
// Entity is initially in view
|
||||
AssertDetached(false);
|
||||
|
||||
// Move the player out of the entity's PVS range
|
||||
await server.WaitPost(() => xforms.SetCoordinates(playerUid, farAway));
|
||||
await RunTicks();
|
||||
|
||||
// Client should now have detached the entity, moving it into nullspace
|
||||
AssertDetached(true);
|
||||
|
||||
// Marking the entity as dirty due to client-side prediction should have effect
|
||||
await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt)));
|
||||
await RunTicks();
|
||||
AssertDetached(true);
|
||||
|
||||
// Move the player back into range
|
||||
await server.WaitPost( () => xforms.SetCoordinates(playerUid, coords));
|
||||
await RunTicks();
|
||||
AssertDetached(false);
|
||||
|
||||
// Marking the entity as dirty due to client-side prediction should have no real effect
|
||||
await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt)));
|
||||
await RunTicks();
|
||||
AssertDetached(false);
|
||||
|
||||
await client.WaitPost(() => netMan.ClientDisconnect(""));
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ public sealed partial class DeferredEntityDeletionTest : RobustIntegrationTest
|
||||
var server = StartServer(options);
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
EntityUid uid1 = default, uid2 = default, uid3 = default;
|
||||
DeferredDeletionTestComponent comp1 = default!, comp2 = default!, comp3 = default!;
|
||||
EntityUid uid1 = default, uid2 = default, uid3 = default, uid4 = default;
|
||||
DeferredDeletionTestComponent comp1 = default!, comp2 = default!, comp3 = default!, comp4 = default!;
|
||||
IEntityManager entMan = default!;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
@@ -46,14 +46,17 @@ public sealed partial class DeferredEntityDeletionTest : RobustIntegrationTest
|
||||
uid1 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
uid2 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
uid3 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
uid4 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
comp1 = entMan.AddComponent<DeferredDeletionTestComponent>(uid1);
|
||||
comp2 = entMan.AddComponent<DeferredDeletionTestComponent>(uid2);
|
||||
comp3 =entMan.AddComponent<DeferredDeletionTestComponent>(uid3);
|
||||
comp3 = entMan.AddComponent<DeferredDeletionTestComponent>(uid3);
|
||||
comp4 = entMan.AddComponent<DeferredDeletionTestComponent>(uid4);
|
||||
|
||||
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid1);
|
||||
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid2);
|
||||
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid3);
|
||||
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid4);
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(1);
|
||||
@@ -76,17 +79,23 @@ public sealed partial class DeferredEntityDeletionTest : RobustIntegrationTest
|
||||
var ev = new DeferredDeletionTestEvent();
|
||||
entMan.EventBus.RaiseLocalEvent(uid2, ev);
|
||||
entMan.EventBus.RaiseLocalEvent(uid3, ev);
|
||||
entMan.EventBus.RaiseLocalEvent(uid4, ev);
|
||||
entMan.DeleteEntity(uid2);
|
||||
entMan.QueueDeleteEntity(uid3);
|
||||
entMan.TryQueueDeleteEntity(uid4);
|
||||
Assert.That(entMan.Deleted(uid2));
|
||||
Assert.That(!entMan.Deleted(uid3));
|
||||
Assert.That(!entMan.Deleted(uid4));
|
||||
Assert.That(comp2.LifeStage == ComponentLifeStage.Deleted);
|
||||
Assert.That(comp3.LifeStage == ComponentLifeStage.Stopped);
|
||||
Assert.That(comp4.LifeStage == ComponentLifeStage.Stopped);
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(1);
|
||||
Assert.That(comp3.LifeStage == ComponentLifeStage.Deleted);
|
||||
Assert.That(comp4.LifeStage == ComponentLifeStage.Deleted);
|
||||
Assert.That(entMan.Deleted(uid3));
|
||||
Assert.That(entMan.Deleted(uid4));
|
||||
await server.WaitIdleAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed.TypeParsers;
|
||||
@@ -43,6 +44,7 @@ public sealed class ToolshedValidationTest : ToolshedTest
|
||||
foreach (var type in types)
|
||||
{
|
||||
var instance = (ToolshedCommand)Activator.CreateInstance(type)!;
|
||||
Server.Resolve<IDependencyCollection>().InjectDependencies(instance, oneOff: true);
|
||||
Assert.Throws<InvalidCommandImplementation>(instance.Init, $"{type.PrettyName()} did not throw a {nameof(InvalidCommandImplementation)} exception");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user