mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee330d0ae9 | ||
|
|
6c44dd9665 | ||
|
|
c81413b0b4 | ||
|
|
88b3a557da | ||
|
|
572eb01290 | ||
|
|
9dab74c9d5 | ||
|
|
e1cb1e1b9c | ||
|
|
a23da702b1 | ||
|
|
ae9c2423ff | ||
|
|
a6dae8e30a |
@@ -247,7 +247,6 @@ namespace {nameSpace}
|
||||
DiagnosticSeverity.Error,
|
||||
true),
|
||||
Location.None));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Broadphase;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Logger = Robust.Shared.Log.Logger;
|
||||
|
||||
namespace Robust.Client.Audio.Midi
|
||||
@@ -32,24 +34,6 @@ namespace Robust.Client.Audio.Midi
|
||||
/// </returns>
|
||||
IMidiRenderer? GetNewRenderer();
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Checks whether the file at the given path is a valid midi file or not.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We add this here so content doesn't need to reference NFluidsynth.
|
||||
/// </remarks>
|
||||
bool IsMidiFile(string filename);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the file at the given path is a valid midi file or not.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We add this here so content doesn't need to reference NFluidsynth.
|
||||
/// </remarks>
|
||||
bool IsSoundfontFile(string filename);
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Method called every frame.
|
||||
/// Should be used to update positional audio.
|
||||
@@ -57,6 +41,11 @@ namespace Robust.Client.Audio.Midi
|
||||
/// <param name="frameTime"></param>
|
||||
void FrameUpdate(float frameTime);
|
||||
|
||||
/// <summary>
|
||||
/// Volume, in db.
|
||||
/// </summary>
|
||||
float Volume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, MIDI support is available.
|
||||
/// </summary>
|
||||
@@ -75,6 +64,7 @@ namespace Robust.Client.Audio.Midi
|
||||
|
||||
private SharedBroadPhaseSystem _broadPhaseSystem = default!;
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsAvailable
|
||||
{
|
||||
get
|
||||
@@ -85,12 +75,29 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
private readonly List<IMidiRenderer> _renderers = new();
|
||||
|
||||
private bool _alive = true;
|
||||
private Settings? _settings;
|
||||
private Thread? _midiThread;
|
||||
private ISawmill _midiSawmill = default!;
|
||||
private float _volume = 0f;
|
||||
private bool _volumeDirty = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
if (MathHelper.CloseTo(_volume, value))
|
||||
return;
|
||||
|
||||
_volume = value;
|
||||
_volumeDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string[] LinuxSoundfonts =
|
||||
{
|
||||
@@ -117,6 +124,7 @@ namespace Robust.Client.Audio.Midi
|
||||
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int OcclusionCollisionMask { get; set; }
|
||||
|
||||
private void InitializeFluidsynth()
|
||||
@@ -258,76 +266,79 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
|
||||
// Update positions of streams every frame.
|
||||
lock (_renderers)
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
foreach (var renderer in _renderers)
|
||||
if (renderer.Disposed)
|
||||
continue;
|
||||
|
||||
if(_volumeDirty)
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
if (!renderer.Mono)
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
continue;
|
||||
renderer.Source.SetGlobal();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!renderer.Mono)
|
||||
MapCoordinates? mapPos = null;
|
||||
if (renderer.TrackingCoordinates != null)
|
||||
{
|
||||
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
else if (renderer.TrackingEntity != null)
|
||||
{
|
||||
mapPos = renderer.TrackingEntity.Transform.MapPosition;
|
||||
}
|
||||
|
||||
if (mapPos != null)
|
||||
{
|
||||
var pos = mapPos.Value;
|
||||
if (pos.MapId != _eyeManager.CurrentMap)
|
||||
{
|
||||
renderer.Source.SetVolume(-10000000);
|
||||
}
|
||||
else
|
||||
{
|
||||
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
pos.Position,
|
||||
sourceRelative.Normalized,
|
||||
OcclusionCollisionMask),
|
||||
sourceRelative.Length,
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
}
|
||||
|
||||
if (renderer.Source.SetPosition(pos.Position))
|
||||
{
|
||||
renderer.Source.SetGlobal();
|
||||
continue;
|
||||
}
|
||||
|
||||
MapCoordinates? mapPos = null;
|
||||
if (renderer.TrackingCoordinates != null)
|
||||
if (renderer.TrackingEntity != null)
|
||||
{
|
||||
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
else if (renderer.TrackingEntity != null)
|
||||
{
|
||||
mapPos = renderer.TrackingEntity.Transform.MapPosition;
|
||||
renderer.Source.SetVelocity(renderer.TrackingEntity.GlobalLinearVelocity());
|
||||
}
|
||||
|
||||
if (mapPos != null)
|
||||
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
|
||||
{
|
||||
var pos = mapPos.Value;
|
||||
if (pos.MapId != _eyeManager.CurrentMap)
|
||||
{
|
||||
renderer.Source.SetVolume(-10000000);
|
||||
}
|
||||
else
|
||||
{
|
||||
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
pos.Position,
|
||||
sourceRelative.Normalized,
|
||||
OcclusionCollisionMask),
|
||||
sourceRelative.Length,
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
}
|
||||
|
||||
if (renderer.Source.SetPosition(pos.Position))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (renderer.TrackingEntity != null)
|
||||
{
|
||||
renderer.Source.SetVelocity(renderer.TrackingEntity.GlobalLinearVelocity());
|
||||
}
|
||||
|
||||
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
|
||||
{
|
||||
// just duck out instead of move to NaN
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
_midiSawmill?.Warning("Interrupting positional audio, can't set position.");
|
||||
renderer.Source.StopPlaying();
|
||||
// just duck out instead of move to NaN
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
_midiSawmill?.Warning("Interrupting positional audio, can't set position.");
|
||||
renderer.Source.StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
_volumeDirty = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using MidiEvent = NFluidsynth.MidiEvent;
|
||||
|
||||
namespace Robust.Client.Audio.Midi
|
||||
@@ -205,8 +206,11 @@ namespace Robust.Client.Audio.Midi
|
||||
private SequencerClientId _synthRegister;
|
||||
public IClydeBufferedAudioSource Source { get; set; }
|
||||
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
|
||||
|
||||
[ViewVariables]
|
||||
public bool Disposed { get; private set; } = false;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public byte MidiProgram
|
||||
{
|
||||
get => _midiProgram;
|
||||
@@ -220,6 +224,7 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public byte MidiBank
|
||||
{
|
||||
get => _midiBank;
|
||||
@@ -233,6 +238,7 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public uint MidiSoundfont
|
||||
{
|
||||
get => _midiSoundfont;
|
||||
@@ -246,10 +252,16 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool DisablePercussionChannel { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool DisableProgramChangeEvent { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int PlayerTick
|
||||
{
|
||||
get => _player?.CurrentTick ?? 0;
|
||||
@@ -260,12 +272,19 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public uint SequencerTick => _sequencer?.Tick ?? 0;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public double SequencerTimeScale => _sequencer?.TimeScale ?? 0;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Mono { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public MidiRendererStatus Status { get; private set; } = MidiRendererStatus.None;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool LoopMidi
|
||||
{
|
||||
get => _loopMidi;
|
||||
@@ -277,10 +296,11 @@ namespace Robust.Client.Audio.Midi
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public IEntity? TrackingEntity { get; set; } = null;
|
||||
public EntityCoordinates? TrackingCoordinates { get; set; } = null;
|
||||
|
||||
internal bool Free { get; set; } = false;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public EntityCoordinates? TrackingCoordinates { get; set; } = null;
|
||||
|
||||
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono = true)
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IPlayerManager _playMan = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityLookup _entityLookup = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IDiscordRichPresence _discord = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
@@ -176,15 +177,6 @@ namespace Robust.Client
|
||||
PlayerJoinedServer?.Invoke(this, new PlayerEventArgs(session));
|
||||
}
|
||||
|
||||
private void GameStartedSetup()
|
||||
{
|
||||
_entityManager.Startup();
|
||||
_mapManager.Startup();
|
||||
|
||||
_timing.ResetSimTime();
|
||||
_timing.Paused = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Player is joining the game
|
||||
/// </summary>
|
||||
@@ -219,12 +211,22 @@ namespace Robust.Client
|
||||
GameStoppedReset();
|
||||
}
|
||||
|
||||
private void GameStartedSetup()
|
||||
{
|
||||
_entityManager.Startup();
|
||||
_mapManager.Startup();
|
||||
_entityLookup.Startup();
|
||||
|
||||
_timing.ResetSimTime();
|
||||
_timing.Paused = false;
|
||||
}
|
||||
|
||||
private void GameStoppedReset()
|
||||
{
|
||||
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
|
||||
_gameStates.Reset();
|
||||
_playMan.Shutdown();
|
||||
IoCManager.Resolve<IEntityLookup>().Shutdown();
|
||||
_entityLookup.Shutdown();
|
||||
_entityManager.Shutdown();
|
||||
_mapManager.Shutdown();
|
||||
_discord.ClearPresence();
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace Robust.Client
|
||||
IoCManager.Register<IMapManagerInternal, ClientMapManager>();
|
||||
IoCManager.Register<IClientMapManager, ClientMapManager>();
|
||||
IoCManager.Register<IEntityManager, ClientEntityManager>();
|
||||
IoCManager.Register<IEntityLookup, SharedEntityLookup>();
|
||||
IoCManager.Register<IEntityLookup, EntityLookup>();
|
||||
IoCManager.Register<IComponentFactory, ClientComponentFactory>();
|
||||
IoCManager.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
|
||||
IoCManager.Register<IClydeTileDefinitionManager, ClydeTileDefinitionManager>();
|
||||
|
||||
@@ -30,7 +30,8 @@ namespace Robust.Client.Console
|
||||
|
||||
var console = new ScriptConsoleServer(this, session);
|
||||
_activeConsoles.Add(session, console);
|
||||
console.Open();
|
||||
// FIXME: When this is Open(), resizing the window will cause its position to get NaN'd.
|
||||
console.OpenCentered();
|
||||
}
|
||||
|
||||
private void ReceiveScriptResponse(MsgScriptResponse message)
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Robust.Client
|
||||
// Disable load context usage on content start.
|
||||
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
|
||||
_modLoader.SetUseLoadContext(!ContentStart);
|
||||
_modLoader.SetEnableSandboxing(Options.Sandboxing);
|
||||
_modLoader.SetEnableSandboxing(false && Options.Sandboxing);
|
||||
|
||||
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), Options.ContentModulePrefix))
|
||||
{
|
||||
@@ -125,7 +125,6 @@ namespace Robust.Client
|
||||
_prototypeManager.Resync();
|
||||
_mapManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
IoCManager.Resolve<IEntityLookup>().Initialize();
|
||||
_gameStateManager.Initialize();
|
||||
_placementManager.Initialize();
|
||||
_viewVariablesManager.Initialize();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Animations;
|
||||
@@ -167,7 +168,7 @@ namespace Robust.Client.GameObjects
|
||||
set
|
||||
{
|
||||
_radius = MathF.Max(value, 0.01f); // setting radius to 0 causes exceptions, so just use a value close enough to zero that it's unnoticeable.
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedMessage(this));
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedEvent(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +180,18 @@ namespace Robust.Client.GameObjects
|
||||
Mask = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What MapId we are intersecting for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
|
||||
|
||||
/// <summary>
|
||||
/// What grids we're on for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal List<GridId> IntersectingGrids = new();
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
if (_maskPath != null)
|
||||
@@ -230,7 +243,7 @@ namespace Robust.Client.GameObjects
|
||||
if (map != MapId.Nullspace)
|
||||
{
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
|
||||
new RenderTreeRemoveLightMessage(this, map));
|
||||
new RenderTreeRemoveLightEvent(this, map));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,11 +261,11 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public struct PointLightRadiusChangedMessage
|
||||
public class PointLightRadiusChangedEvent : EntityEventArgs
|
||||
{
|
||||
public PointLightComponent PointLightComponent { get; }
|
||||
|
||||
public PointLightRadiusChangedMessage(PointLightComponent pointLightComponent)
|
||||
public PointLightRadiusChangedEvent(PointLightComponent pointLightComponent)
|
||||
{
|
||||
PointLightComponent = pointLightComponent;
|
||||
}
|
||||
|
||||
@@ -125,6 +125,18 @@ namespace Robust.Client.GameObjects
|
||||
[DataField("directional")]
|
||||
private bool _directional = true;
|
||||
|
||||
/// <summary>
|
||||
/// What MapId we are intersecting for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
|
||||
|
||||
/// <summary>
|
||||
/// What grids we're on for RenderingTreeSystem.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
internal List<GridId> IntersectingGrids { get; } = new();
|
||||
|
||||
[DataField("layerDatums")]
|
||||
private List<PrototypeLayerData> LayerDatums
|
||||
{
|
||||
@@ -1355,7 +1367,7 @@ namespace Robust.Client.GameObjects
|
||||
if (map != MapId.Nullspace)
|
||||
{
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
|
||||
new RenderTreeRemoveSpriteMessage(this, map));
|
||||
new RenderTreeRemoveSpriteEvent(this, map));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -6,6 +8,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -47,145 +50,176 @@ namespace Robust.Client.GameObjects
|
||||
_mapManager.OnGridCreated += MapManagerOnGridCreated;
|
||||
_mapManager.OnGridRemoved += MapManagerOnGridRemoved;
|
||||
|
||||
SubscribeLocalEvent<EntMapIdChangedMessage>(EntMapIdChanged);
|
||||
SubscribeLocalEvent<MoveEvent>(EntMoved);
|
||||
SubscribeLocalEvent<EntParentChangedMessage>(EntParentChanged);
|
||||
SubscribeLocalEvent<PointLightRadiusChangedMessage>(PointLightRadiusChanged);
|
||||
SubscribeLocalEvent<RenderTreeRemoveSpriteMessage>(RemoveSprite);
|
||||
SubscribeLocalEvent<RenderTreeRemoveLightMessage>(RemoveLight);
|
||||
SubscribeLocalEvent<SpriteComponent, EntMapIdChangedMessage>(SpriteMapChanged);
|
||||
SubscribeLocalEvent<SpriteComponent, MoveEvent>(SpriteMoved);
|
||||
SubscribeLocalEvent<SpriteComponent, EntParentChangedMessage>(SpriteParentChanged);
|
||||
SubscribeLocalEvent<SpriteComponent, RenderTreeRemoveSpriteEvent>(RemoveSprite);
|
||||
|
||||
SubscribeLocalEvent<PointLightComponent, EntMapIdChangedMessage>(LightMapChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, MoveEvent>(LightMoved);
|
||||
SubscribeLocalEvent<PointLightComponent, EntParentChangedMessage>(LightParentChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, PointLightRadiusChangedEvent>(PointLightRadiusChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, RenderTreeRemoveLightEvent>(RemoveLight);
|
||||
}
|
||||
|
||||
// For the RemoveX methods
|
||||
// If the Transform is removed BEFORE the Sprite/Light,
|
||||
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
|
||||
// Otherwise these will still have their past MapId and that's all we need..
|
||||
|
||||
#region SpriteHandlers
|
||||
private void SpriteMapChanged(EntityUid uid, SpriteComponent component, EntMapIdChangedMessage args)
|
||||
{
|
||||
QueueSpriteUpdate(component);
|
||||
}
|
||||
|
||||
private void SpriteMoved(EntityUid uid, SpriteComponent component, MoveEvent args)
|
||||
{
|
||||
QueueSpriteUpdate(component);
|
||||
}
|
||||
|
||||
private void SpriteParentChanged(EntityUid uid, SpriteComponent component, EntParentChangedMessage args)
|
||||
{
|
||||
QueueSpriteUpdate(component);
|
||||
}
|
||||
|
||||
private void RemoveSprite(EntityUid uid, SpriteComponent component, RenderTreeRemoveSpriteEvent args)
|
||||
{
|
||||
ClearSprite(component);
|
||||
}
|
||||
|
||||
private void ClearSprite(SpriteComponent component)
|
||||
{
|
||||
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
|
||||
{
|
||||
foreach (var gridId in component.IntersectingGrids)
|
||||
{
|
||||
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
|
||||
tree.SpriteTree.Remove(component);
|
||||
}
|
||||
}
|
||||
|
||||
component.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
private void QueueSpriteUpdate(SpriteComponent component)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
|
||||
component.TreeUpdateQueued = true;
|
||||
_spriteQueue.Add(component);
|
||||
|
||||
foreach (var child in component.Owner.Transform.Children)
|
||||
{
|
||||
QueueSpriteUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueSpriteUpdate(IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out SpriteComponent? spriteComponent)) return;
|
||||
QueueSpriteUpdate(spriteComponent);
|
||||
|
||||
foreach (var child in entity.Transform.Children)
|
||||
{
|
||||
QueueSpriteUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LightHandlers
|
||||
private void LightMapChanged(EntityUid uid, PointLightComponent component, EntMapIdChangedMessage args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void LightMoved(EntityUid uid, PointLightComponent component, MoveEvent args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void LightParentChanged(EntityUid uid, PointLightComponent component, EntParentChangedMessage args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void PointLightRadiusChanged(EntityUid uid, PointLightComponent component, PointLightRadiusChangedEvent args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void RemoveLight(EntityUid uid, PointLightComponent component, RenderTreeRemoveLightEvent args)
|
||||
{
|
||||
ClearLight(component);
|
||||
}
|
||||
|
||||
private void ClearLight(PointLightComponent component)
|
||||
{
|
||||
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
|
||||
{
|
||||
foreach (var gridId in component.IntersectingGrids)
|
||||
{
|
||||
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
|
||||
tree.LightTree.Remove(component);
|
||||
}
|
||||
}
|
||||
|
||||
component.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
private void QueueLightUpdate(PointLightComponent component)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
|
||||
component.TreeUpdateQueued = true;
|
||||
_lightQueue.Add(component);
|
||||
|
||||
foreach (var child in component.Owner.Transform.Children)
|
||||
{
|
||||
QueueLightUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueLightUpdate(IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out PointLightComponent? lightComponent)) return;
|
||||
QueueLightUpdate(lightComponent);
|
||||
|
||||
foreach (var child in entity.Transform.Children)
|
||||
{
|
||||
QueueLightUpdate(child.Owner);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_mapManager.MapCreated -= MapManagerOnMapCreated;
|
||||
_mapManager.MapDestroyed -= MapManagerOnMapDestroyed;
|
||||
_mapManager.OnGridCreated -= MapManagerOnGridCreated;
|
||||
_mapManager.OnGridRemoved -= MapManagerOnGridRemoved;
|
||||
}
|
||||
|
||||
// For these next 2 methods (the Remove* ones):
|
||||
// If the Transform is removed BEFORE the Sprite/Light,
|
||||
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
|
||||
// Otherwise these will still have their past MapId and that's all we need..
|
||||
private void RemoveLight(RenderTreeRemoveLightMessage ev)
|
||||
{
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.LightAabbFunc(ev.Light), true))
|
||||
{
|
||||
_gridTrees[ev.Map][gridId].LightTree.Remove(ev.Light);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveSprite(RenderTreeRemoveSpriteMessage ev)
|
||||
{
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.SpriteAabbFunc(ev.Sprite), true))
|
||||
{
|
||||
_gridTrees[ev.Map][gridId].SpriteTree.Remove(ev.Sprite);
|
||||
}
|
||||
}
|
||||
|
||||
private void PointLightRadiusChanged(PointLightRadiusChangedMessage ev)
|
||||
{
|
||||
QueueUpdateLight(ev.PointLightComponent);
|
||||
}
|
||||
|
||||
private void EntParentChanged(EntParentChangedMessage ev)
|
||||
{
|
||||
UpdateEntity(ev.Entity);
|
||||
}
|
||||
|
||||
private void EntMoved(MoveEvent ev)
|
||||
{
|
||||
UpdateEntity(ev.Sender);
|
||||
}
|
||||
|
||||
private void UpdateEntity(IEntity entity)
|
||||
{
|
||||
if (entity.TryGetComponent(out SpriteComponent? spriteComponent))
|
||||
{
|
||||
if (!spriteComponent.TreeUpdateQueued)
|
||||
{
|
||||
spriteComponent.TreeUpdateQueued = true;
|
||||
|
||||
_spriteQueue.Add(spriteComponent);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out PointLightComponent? light))
|
||||
{
|
||||
QueueUpdateLight(light);
|
||||
}
|
||||
|
||||
foreach (var child in entity.Transform.ChildEntityUids)
|
||||
{
|
||||
UpdateEntity(EntityManager.GetEntity(child));
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueUpdateLight(PointLightComponent light)
|
||||
{
|
||||
if (!light.TreeUpdateQueued)
|
||||
{
|
||||
light.TreeUpdateQueued = true;
|
||||
|
||||
_lightQueue.Add(light);
|
||||
}
|
||||
}
|
||||
|
||||
private void EntMapIdChanged(EntMapIdChangedMessage ev)
|
||||
{
|
||||
// Nullspace is a valid map ID for stuff to have but we also aren't gonna bother indexing it.
|
||||
// So that's why there's a GetValueOrDefault.
|
||||
var oldMapTrees = _gridTrees.GetValueOrDefault(ev.OldMapId);
|
||||
var newMapTrees = _gridTrees.GetValueOrDefault(ev.Entity.Transform.MapID);
|
||||
|
||||
// TODO: MMMM probably a better way to do this.
|
||||
if (ev.Entity.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
if (oldMapTrees != null)
|
||||
{
|
||||
foreach (var (_, gridTree) in oldMapTrees)
|
||||
{
|
||||
gridTree.SpriteTree.Remove(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = MapTrees.SpriteAabbFunc(sprite);
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
|
||||
{
|
||||
var gridBounds = gridId == GridId.Invalid
|
||||
? bounds : bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
newMapTrees?[gridId].SpriteTree.AddOrUpdate(sprite, gridBounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.Entity.TryGetComponent(out PointLightComponent? light))
|
||||
{
|
||||
if (oldMapTrees != null)
|
||||
{
|
||||
foreach (var (_, gridTree) in oldMapTrees)
|
||||
{
|
||||
gridTree.LightTree.Remove(light);
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = MapTrees.LightAabbFunc(light);
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
|
||||
{
|
||||
var gridBounds = gridId == GridId.Invalid
|
||||
? bounds : bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
newMapTrees?[gridId].LightTree.AddOrUpdate(light, gridBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MapManagerOnMapDestroyed(object? sender, MapEventArgs e)
|
||||
{
|
||||
foreach (var (_, gridTree) in _gridTrees[e.Map])
|
||||
{
|
||||
foreach (var comp in gridTree.LightTree)
|
||||
{
|
||||
comp.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
foreach (var comp in gridTree.SpriteTree)
|
||||
{
|
||||
comp.IntersectingGrids.Clear();
|
||||
}
|
||||
|
||||
// Just in case?
|
||||
gridTree.LightTree.Clear();
|
||||
gridTree.SpriteTree.Clear();
|
||||
}
|
||||
|
||||
_gridTrees.Remove(e.Map);
|
||||
}
|
||||
|
||||
@@ -209,47 +243,109 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void MapManagerOnGridRemoved(MapId mapId, GridId gridId)
|
||||
{
|
||||
var gridTree = _gridTrees[mapId][gridId];
|
||||
|
||||
foreach (var sprite in gridTree.SpriteTree)
|
||||
{
|
||||
sprite.IntersectingGrids.Remove(gridId);
|
||||
}
|
||||
|
||||
foreach (var light in gridTree.LightTree)
|
||||
{
|
||||
light.IntersectingGrids.Remove(gridId);
|
||||
}
|
||||
|
||||
// Clear in case
|
||||
gridTree.LightTree.Clear();
|
||||
gridTree.SpriteTree.Clear();
|
||||
_gridTrees[mapId].Remove(gridId);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
foreach (var queuedUpdateSprite in _spriteQueue)
|
||||
foreach (var sprite in _spriteQueue)
|
||||
{
|
||||
var map = queuedUpdateSprite.Owner.Transform.MapID;
|
||||
if (map == MapId.Nullspace)
|
||||
var mapId = sprite.Owner.Transform.MapID;
|
||||
|
||||
// If we're on a new map then clear the old one.
|
||||
if (sprite.IntersectingMapId != mapId)
|
||||
{
|
||||
continue;
|
||||
ClearSprite(sprite);
|
||||
}
|
||||
|
||||
var mapTree = _gridTrees[map];
|
||||
sprite.IntersectingMapId = mapId;
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
|
||||
MapTrees.SpriteAabbFunc(queuedUpdateSprite), true))
|
||||
if (mapId == MapId.Nullspace) continue;
|
||||
|
||||
var mapTree = _gridTrees[mapId];
|
||||
var aabb = MapTrees.SpriteAabbFunc(sprite);
|
||||
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
|
||||
|
||||
// Remove from old
|
||||
foreach (var gridId in sprite.IntersectingGrids)
|
||||
{
|
||||
mapTree[gridId].SpriteTree.AddOrUpdate(queuedUpdateSprite);
|
||||
if (intersectingGrids.Contains(gridId)) continue;
|
||||
mapTree[gridId].SpriteTree.Remove(sprite);
|
||||
}
|
||||
|
||||
queuedUpdateSprite.TreeUpdateQueued = false;
|
||||
// Rebuild in the update below
|
||||
sprite.IntersectingGrids.Clear();
|
||||
|
||||
// Update / add to new
|
||||
foreach (var gridId in intersectingGrids)
|
||||
{
|
||||
var translated = aabb.Translated(gridId == GridId.Invalid
|
||||
? Vector2.Zero
|
||||
: -_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
mapTree[gridId].SpriteTree.AddOrUpdate(sprite, translated);
|
||||
|
||||
sprite.IntersectingGrids.Add(gridId);
|
||||
}
|
||||
|
||||
sprite.TreeUpdateQueued = false;
|
||||
}
|
||||
|
||||
foreach (var queuedUpdateLight in _lightQueue)
|
||||
foreach (var light in _lightQueue)
|
||||
{
|
||||
var map = queuedUpdateLight.Owner.Transform.MapID;
|
||||
if (map == MapId.Nullspace)
|
||||
var mapId = light.Owner.Transform.MapID;
|
||||
|
||||
// If we're on a new map then clear the old one.
|
||||
if (light.IntersectingMapId != mapId)
|
||||
{
|
||||
continue;
|
||||
ClearLight(light);
|
||||
}
|
||||
|
||||
var mapTree = _gridTrees[map];
|
||||
light.IntersectingMapId = mapId;
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
|
||||
MapTrees.LightAabbFunc(queuedUpdateLight), true))
|
||||
if (mapId == MapId.Nullspace) continue;
|
||||
|
||||
var mapTree = _gridTrees[mapId];
|
||||
var aabb = MapTrees.LightAabbFunc(light);
|
||||
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
|
||||
|
||||
// Remove from old
|
||||
foreach (var gridId in intersectingGrids)
|
||||
{
|
||||
mapTree[gridId].LightTree.AddOrUpdate(queuedUpdateLight);
|
||||
if (intersectingGrids.Contains(gridId)) continue;
|
||||
mapTree[gridId].LightTree.Remove(light);
|
||||
}
|
||||
|
||||
queuedUpdateLight.TreeUpdateQueued = false;
|
||||
// Rebuild in the update below
|
||||
light.IntersectingGrids.Clear();
|
||||
|
||||
// Update / add to new
|
||||
foreach (var gridId in intersectingGrids)
|
||||
{
|
||||
var translated = aabb.Translated(gridId == GridId.Invalid
|
||||
? Vector2.Zero
|
||||
: -_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
mapTree[gridId].LightTree.AddOrUpdate(light, translated);
|
||||
light.IntersectingGrids.Add(gridId);
|
||||
}
|
||||
|
||||
light.TreeUpdateQueued = false;
|
||||
}
|
||||
|
||||
_spriteQueue.Clear();
|
||||
@@ -284,9 +380,9 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
internal struct RenderTreeRemoveSpriteMessage
|
||||
internal class RenderTreeRemoveSpriteEvent : EntityEventArgs
|
||||
{
|
||||
public RenderTreeRemoveSpriteMessage(SpriteComponent sprite, MapId map)
|
||||
public RenderTreeRemoveSpriteEvent(SpriteComponent sprite, MapId map)
|
||||
{
|
||||
Sprite = sprite;
|
||||
Map = map;
|
||||
@@ -296,9 +392,9 @@ namespace Robust.Client.GameObjects
|
||||
public MapId Map { get; }
|
||||
}
|
||||
|
||||
internal struct RenderTreeRemoveLightMessage
|
||||
internal class RenderTreeRemoveLightEvent : EntityEventArgs
|
||||
{
|
||||
public RenderTreeRemoveLightMessage(PointLightComponent light, MapId map)
|
||||
public RenderTreeRemoveLightEvent(PointLightComponent light, MapId map)
|
||||
{
|
||||
Light = light;
|
||||
Map = map;
|
||||
|
||||
@@ -16,11 +16,17 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private RenderingTreeSystem _treeSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_treeSystem = Get<RenderingTreeSystem>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
var renderTreeSystem = EntitySystemManager.GetEntitySystem<RenderingTreeSystem>();
|
||||
|
||||
// So we could calculate the correct size of the entities based on the contents of their sprite...
|
||||
// Or we can just assume that no entity is larger than 10x10 and get a stupid easy check.
|
||||
var pvsBounds = _eyeManager.GetWorldViewport().Enlarged(5);
|
||||
@@ -33,18 +39,9 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
foreach (var gridId in _mapManager.FindGridIdsIntersecting(currentMap, pvsBounds, true))
|
||||
{
|
||||
Box2 gridBounds;
|
||||
var gridBounds = gridId == GridId.Invalid ? pvsBounds : pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
|
||||
if (gridId == GridId.Invalid)
|
||||
{
|
||||
gridBounds = pvsBounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
gridBounds = pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
|
||||
}
|
||||
|
||||
var mapTree = renderTreeSystem.GetSpriteTreeForMap(currentMap, gridId);
|
||||
var mapTree = _treeSystem.GetSpriteTreeForMap(currentMap, gridId);
|
||||
|
||||
mapTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
|
||||
{
|
||||
|
||||
@@ -146,13 +146,12 @@ namespace Robust.Client.GameStates
|
||||
private void DrawWorld(in OverlayDrawArgs args)
|
||||
{
|
||||
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
|
||||
|
||||
if(!pvsEnabled)
|
||||
return;
|
||||
|
||||
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
|
||||
var pvsCenter = _eyeManager.CurrentEye.Position;
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize, pvsSize));
|
||||
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange * 2, pvsRange * 2));
|
||||
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
|
||||
@@ -183,6 +183,32 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate)
|
||||
{
|
||||
var buffer = AL.GenBuffer();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (short* ptr = samples)
|
||||
{
|
||||
AL.BufferData(
|
||||
buffer,
|
||||
channels == 1 ? ALFormat.Mono16 : ALFormat.Stereo16,
|
||||
(IntPtr) ptr,
|
||||
samples.Length * 2,
|
||||
sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
var length = TimeSpan.FromSeconds(samples.Length / channels / (double) sampleRate);
|
||||
return new AudioStream(handle, length, channels);
|
||||
}
|
||||
|
||||
public void SetMasterVolume(float newVolume)
|
||||
{
|
||||
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
|
||||
@@ -439,6 +465,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float decibels)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
|
||||
_gain = decibels;
|
||||
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetOcclusion(float blocks)
|
||||
{
|
||||
_checkDisposed();
|
||||
@@ -747,6 +788,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float masterVolumeDecay)
|
||||
{
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
{
|
||||
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
|
||||
priorOcclusion = priorGain / _gain;
|
||||
}
|
||||
|
||||
_gain = masterVolumeDecay;
|
||||
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
|
||||
_checkAlError();
|
||||
}
|
||||
|
||||
public void SetPitch(float pitch)
|
||||
{
|
||||
_checkDisposed();
|
||||
|
||||
@@ -248,6 +248,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return new(default, default, 1, name);
|
||||
}
|
||||
|
||||
public AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IClydeAudioSource CreateAudioSource(AudioStream stream)
|
||||
{
|
||||
return DummyAudioSource.Instance;
|
||||
@@ -337,6 +342,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public void SetVolumeDirect(float masterVolumeDecay)
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Robust.Client.Graphics
|
||||
// AUDIO SYSTEM DOWN BELOW.
|
||||
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
|
||||
AudioStream LoadAudioWav(Stream stream, string? name = null);
|
||||
AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate);
|
||||
|
||||
void SetMasterVolume(float newVolume);
|
||||
|
||||
|
||||
@@ -21,5 +21,6 @@ namespace Robust.Client.Graphics
|
||||
void SetOcclusion(float blocks);
|
||||
void SetPlaybackPosition(float seconds);
|
||||
void SetVelocity(Vector2 velocity);
|
||||
void SetVolumeDirect(float masterVolumeDecay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
internal interface IRenderHandle
|
||||
public interface IRenderHandle
|
||||
{
|
||||
DrawingHandleScreen DrawingHandleScreen { get; }
|
||||
DrawingHandleWorld DrawingHandleWorld { get; }
|
||||
|
||||
@@ -463,7 +463,7 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
}
|
||||
|
||||
internal virtual void DrawInternal(IRenderHandle renderHandle)
|
||||
public virtual void DrawInternal(IRenderHandle renderHandle)
|
||||
{
|
||||
Draw(renderHandle.DrawingHandleScreen);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return (32, 32) * Scale;
|
||||
}
|
||||
|
||||
internal override void DrawInternal(IRenderHandle renderHandle)
|
||||
public override void DrawInternal(IRenderHandle renderHandle)
|
||||
{
|
||||
if (Sprite == null || Sprite.Deleted)
|
||||
{
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
var (spaceX, spaceY) = Parent!.Size;
|
||||
if (Position.Y > spaceY)
|
||||
{
|
||||
LayoutContainer.SetPosition(this, (Position.X, spaceY - HEADER_SIZE_Y));
|
||||
LayoutContainer.SetPosition(this, (Position.X, spaceY + HEADER_SIZE_Y));
|
||||
}
|
||||
|
||||
if (Position.X > spaceX)
|
||||
@@ -104,6 +104,16 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
// 50 is arbitrary here. As long as it's bumped back into view.
|
||||
LayoutContainer.SetPosition(this, (spaceX - 50, Position.Y));
|
||||
}
|
||||
|
||||
if (Position.Y < 0)
|
||||
{
|
||||
LayoutContainer.SetPosition(this, (Position.X, 0));
|
||||
}
|
||||
|
||||
if (Position.X < 0)
|
||||
{
|
||||
LayoutContainer.SetPosition(this, (0, Position.Y));
|
||||
}
|
||||
}
|
||||
|
||||
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||
|
||||
@@ -306,7 +306,6 @@ namespace Robust.Server
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
|
||||
_entityManager.Initialize();
|
||||
IoCManager.Resolve<IEntityLookup>().Initialize();
|
||||
|
||||
IoCManager.Resolve<ISerializationManager>().Initialize();
|
||||
|
||||
@@ -319,6 +318,7 @@ namespace Robust.Server
|
||||
|
||||
IoCManager.Resolve<IServerConsoleHost>().Initialize();
|
||||
_entityManager.Startup();
|
||||
IoCManager.Resolve<IEntityLookup>().Startup();
|
||||
_stateManager.Initialize();
|
||||
_scriptHost.Initialize();
|
||||
|
||||
|
||||
@@ -57,9 +57,8 @@ namespace Robust.Server.Console.Commands
|
||||
else
|
||||
{
|
||||
var mapEnt = mapMgr.GetMapEntity(mapId);
|
||||
|
||||
transform.AttachParent(mapEnt);
|
||||
transform.WorldPosition = position;
|
||||
transform.AttachParent(mapEnt);
|
||||
}
|
||||
|
||||
shell.WriteLine($"Teleported {player} to {mapId}:{posX},{posY}.");
|
||||
|
||||
@@ -318,7 +318,12 @@ namespace Robust.Server.GameObjects
|
||||
/// <param name="moveEvent"></param>
|
||||
private void HandleEntityMove(MoveEvent moveEvent)
|
||||
{
|
||||
if (moveEvent.Sender.Deleted || moveEvent.NewPosition.GetGridId(EntityManager) == GridId.Invalid || !moveEvent.NewPosition.IsValid(EntityManager))
|
||||
// TODO: When Acruid does TileEntities we may actually be able to delete this system if tile lookups become fast enough
|
||||
var gridId = moveEvent.NewPosition.GetGridId(EntityManager);
|
||||
|
||||
if (moveEvent.Sender.Deleted ||
|
||||
gridId == GridId.Invalid ||
|
||||
!moveEvent.NewPosition.IsValid(EntityManager))
|
||||
{
|
||||
HandleEntityRemove(moveEvent.Sender);
|
||||
return;
|
||||
@@ -330,7 +335,7 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
|
||||
// Memory leak protection
|
||||
var gridBounds = _mapManager.GetGrid(moveEvent.Sender.Transform.GridID).WorldBounds;
|
||||
var gridBounds = _mapManager.GetGrid(gridId).WorldBounds;
|
||||
if (!gridBounds.Contains(moveEvent.Sender.Transform.WorldPosition))
|
||||
{
|
||||
HandleEntityRemove(moveEvent.Sender);
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Robust.Server
|
||||
IoCManager.Register<IMapManagerInternal, ServerMapManager>();
|
||||
IoCManager.Register<IServerMapManager, ServerMapManager>();
|
||||
IoCManager.Register<IEntityManager, ServerEntityManager>();
|
||||
IoCManager.Register<IEntityLookup, SharedEntityLookup>();
|
||||
IoCManager.Register<IEntityLookup, EntityLookup>();
|
||||
IoCManager.Register<IEntityNetworkManager, ServerEntityManager>();
|
||||
IoCManager.Register<IServerEntityNetworkManager, ServerEntityManager>();
|
||||
IoCManager.Register<IMapLoader, MapLoader>();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Map;
|
||||
@@ -93,9 +94,6 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
bool IsMapTransform { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
Vector2? LerpDestination { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -52,6 +52,17 @@ namespace Robust.Shared.GameObjects
|
||||
_gridIndex = GridId.Invalid;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var mapId = Owner.Transform.MapID;
|
||||
|
||||
if (_mapManager.HasMapEntity(mapId))
|
||||
{
|
||||
Owner.Transform.AttachParent(_mapManager.GetMapEntity(mapId));
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="player"></param>
|
||||
/// <inheritdoc />
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -66,25 +67,21 @@ namespace Robust.Shared.GameObjects
|
||||
[ViewVariables]
|
||||
public GridId GridID
|
||||
{
|
||||
get
|
||||
get => _gridId;
|
||||
private set
|
||||
{
|
||||
// root node, grid id is undefined
|
||||
if (Owner.HasComponent<IMapComponent>())
|
||||
return GridId.Invalid;
|
||||
|
||||
// second level node, terminates recursion up the branch of the tree
|
||||
if (Owner.TryGetComponent(out IMapGridComponent? gridComp))
|
||||
return gridComp.GridIndex;
|
||||
|
||||
// branch or leaf node
|
||||
if (_parent.IsValid())
|
||||
return Parent!.GridID;
|
||||
|
||||
// Not on a grid
|
||||
return GridId.Invalid;
|
||||
if (_gridId.Equals(value)) return;
|
||||
_gridId = value;
|
||||
foreach (var transformComponent in Children)
|
||||
{
|
||||
var child = (TransformComponent) transformComponent;
|
||||
child.GridID = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GridId _gridId = GridId.Invalid;
|
||||
|
||||
/// <inheritdoc />
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool NoLocalRotation
|
||||
@@ -264,6 +261,7 @@ namespace Robust.Shared.GameObjects
|
||||
set
|
||||
{
|
||||
var oldPosition = Coordinates;
|
||||
_localPosition = value.Position;
|
||||
|
||||
if (value.EntityId != _parent)
|
||||
{
|
||||
@@ -271,7 +269,6 @@ namespace Robust.Shared.GameObjects
|
||||
AttachParent(newEntity);
|
||||
}
|
||||
|
||||
_localPosition = value.Position;
|
||||
Dirty();
|
||||
|
||||
if (!DeferUpdates)
|
||||
@@ -425,6 +422,28 @@ namespace Robust.Shared.GameObjects
|
||||
// so duplicate additions (which will happen) don't matter.
|
||||
((TransformComponent) Parent!)._children.Add(Owner.Uid);
|
||||
}
|
||||
|
||||
GridID = GetGridIndex();
|
||||
}
|
||||
|
||||
private GridId GetGridIndex()
|
||||
{
|
||||
if (Owner.HasComponent<IMapComponent>())
|
||||
{
|
||||
return GridId.Invalid;
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out IMapGridComponent? gridComponent))
|
||||
{
|
||||
return gridComponent.GridIndex;
|
||||
}
|
||||
|
||||
if (_parent.IsValid())
|
||||
{
|
||||
return Parent!.GridID;
|
||||
}
|
||||
|
||||
return _mapManager.TryFindGridAt(MapID, WorldPosition, out var mapgrid) ? mapgrid.Index : GridId.Invalid;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -572,7 +591,6 @@ namespace Robust.Shared.GameObjects
|
||||
// offset position from world to parent
|
||||
SetPosition(newParent.InvWorldMatrix.Transform(WorldPosition));
|
||||
_parent = newParentEnt.Uid;
|
||||
|
||||
ChangeMapId(newConcrete.MapID);
|
||||
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, entMessage);
|
||||
@@ -580,6 +598,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
RebuildMatrices();
|
||||
Dirty();
|
||||
GridID = GetGridIndex();
|
||||
}
|
||||
|
||||
internal void ChangeMapId(MapId newMapId)
|
||||
|
||||
@@ -2,7 +2,7 @@ using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
public readonly struct EntMapIdChangedMessage
|
||||
public class EntMapIdChangedMessage : EntityEventArgs
|
||||
{
|
||||
public EntMapIdChangedMessage(IEntity entity, MapId oldMapId)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -12,7 +14,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
// Not an EntitySystem given EntityManager has a dependency on it which means it's just easier to IoC it for tests.
|
||||
|
||||
void Initialize();
|
||||
void Startup();
|
||||
|
||||
void Shutdown();
|
||||
|
||||
@@ -61,7 +63,8 @@ namespace Robust.Shared.GameObjects
|
||||
Box2 GetWorldAabbFromEntity(in IEntity ent);
|
||||
}
|
||||
|
||||
public class SharedEntityLookup : IEntityLookup, IEntityEventSubscriber
|
||||
[UsedImplicitly]
|
||||
public class EntityLookup : IEntityLookup, IEntityEventSubscriber
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IMapManager _mapManager;
|
||||
@@ -69,25 +72,32 @@ namespace Robust.Shared.GameObjects
|
||||
private readonly Dictionary<MapId, DynamicTree<IEntity>> _entityTreesPerMap = new();
|
||||
|
||||
// Using stacks so we always use latest data (given we only run it once per entity).
|
||||
private Stack<MoveEvent> _moveQueue = new();
|
||||
private Stack<RotateEvent> _rotateQueue = new();
|
||||
private Queue<EntMapIdChangedMessage> _mapChangeQueue = new();
|
||||
private readonly Stack<MoveEvent> _moveQueue = new();
|
||||
private readonly Stack<RotateEvent> _rotateQueue = new();
|
||||
private readonly Queue<EntMapIdChangedMessage> _mapChangeQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Move and rotate events generate the same update so no point duplicating work in the same tick.
|
||||
/// </summary>
|
||||
private HashSet<EntityUid> _handledThisTick = new();
|
||||
private readonly HashSet<EntityUid> _handledThisTick = new();
|
||||
|
||||
// TODO: Should combine all of the methods that check for IPhysBody and just use the one GetWorldAabbFromEntity method
|
||||
|
||||
public SharedEntityLookup(IEntityManager entityManager, IMapManager mapManager)
|
||||
public bool Started { get; private set; } = false;
|
||||
|
||||
public EntityLookup(IEntityManager entityManager, IMapManager mapManager)
|
||||
{
|
||||
_entityManager = entityManager;
|
||||
_mapManager = mapManager;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
public void Startup()
|
||||
{
|
||||
if (Started)
|
||||
{
|
||||
throw new InvalidOperationException("Startup() called multiple times.");
|
||||
}
|
||||
|
||||
var eventBus = _entityManager.EventBus;
|
||||
eventBus.SubscribeEvent<MoveEvent>(EventSource.Local, this, ev => _moveQueue.Push(ev));
|
||||
eventBus.SubscribeEvent<RotateEvent>(EventSource.Local, this, ev => _rotateQueue.Push(ev));
|
||||
@@ -96,15 +106,27 @@ namespace Robust.Shared.GameObjects
|
||||
_entityManager.EntityStarted += HandleEntityStarted;
|
||||
_mapManager.MapCreated += HandleMapCreated;
|
||||
_mapManager.MapDestroyed += HandleMapDestroyed;
|
||||
Started = true;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// If we haven't even started up, there's nothing to clean up then.
|
||||
if (!Started)
|
||||
return;
|
||||
|
||||
_moveQueue.Clear();
|
||||
_rotateQueue.Clear();
|
||||
_handledThisTick.Clear();
|
||||
_mapChangeQueue.Clear();
|
||||
_entityTreesPerMap.Clear();
|
||||
|
||||
_entityManager.EventBus.UnsubscribeEvents(this);
|
||||
_entityManager.EntityDeleted -= HandleEntityDeleted;
|
||||
_entityManager.EntityStarted -= HandleEntityStarted;
|
||||
_mapManager.MapCreated -= HandleMapCreated;
|
||||
_mapManager.MapDestroyed -= HandleMapDestroyed;
|
||||
Started = false;
|
||||
}
|
||||
|
||||
private void HandleEntityDeleted(object? sender, EntityUid uid)
|
||||
@@ -193,14 +215,14 @@ namespace Robust.Shared.GameObjects
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 position, bool approximate = false)
|
||||
{
|
||||
if (mapId == MapId.Nullspace)
|
||||
if (!_entityTreesPerMap.TryGetValue(mapId, out var mapTree))
|
||||
{
|
||||
return Enumerable.Empty<IEntity>();
|
||||
}
|
||||
|
||||
var list = new List<IEntity>();
|
||||
|
||||
_entityTreesPerMap[mapId].QueryAabb(ref list, (ref List<IEntity> list, in IEntity ent) =>
|
||||
mapTree.QueryAabb(ref list, (ref List<IEntity> list, in IEntity ent) =>
|
||||
{
|
||||
if (!ent.Deleted)
|
||||
{
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
@@ -12,49 +11,63 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private Queue<MoveEvent> _queuedMoveEvents = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<MoveEvent>(QueueMoveEvent);
|
||||
SubscribeLocalEvent<MoveEvent>(HandleMove);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
private void HandleMove(MoveEvent moveEvent)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
while (_queuedMoveEvents.Count > 0)
|
||||
var entity = moveEvent.Sender;
|
||||
|
||||
if (entity.Deleted ||
|
||||
entity.HasComponent<IMapComponent>() ||
|
||||
entity.HasComponent<IMapGridComponent>() ||
|
||||
entity.IsInContainer())
|
||||
{
|
||||
var moveEvent = _queuedMoveEvents.Dequeue();
|
||||
var entity = moveEvent.Sender;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.Deleted || !entity.HasComponent<PhysicsComponent>() || entity.IsInContainer()) continue;
|
||||
var transform = entity.Transform;
|
||||
|
||||
var transform = entity.Transform;
|
||||
// Change parent if necessary
|
||||
// Given islands will probably have a bunch of static bodies in them then we'll verify velocities first as it's way cheaper
|
||||
|
||||
// This shoouullddnnn'''tt de-parent anything in a container because none of that should have physics applied to it.
|
||||
if (_mapManager.TryFindGridAt(transform.MapID, moveEvent.NewPosition.ToMapPos(EntityManager), out var grid) &&
|
||||
grid.GridEntityId.IsValid() &&
|
||||
grid.GridEntityId != entity.Uid)
|
||||
// Change parent if necessary
|
||||
if (_mapManager.TryFindGridAt(transform.MapID, moveEvent.NewPosition.ToMapPos(EntityManager), out var grid) &&
|
||||
grid.GridEntityId.IsValid() &&
|
||||
grid.GridEntityId != entity.Uid)
|
||||
{
|
||||
// Some minor duplication here with AttachParent but only happens when going on/off grid so not a big deal ATM.
|
||||
if (grid.Index != transform.GridID)
|
||||
{
|
||||
// Also this may deparent if 2 entities are parented but not using containers so fix that
|
||||
if (grid.Index != transform.GridID)
|
||||
{
|
||||
transform.AttachParent(EntityManager.GetEntity(grid.GridEntityId));
|
||||
}
|
||||
transform.AttachParent(EntityManager.GetEntity(grid.GridEntityId));
|
||||
RaiseLocalEvent(entity.Uid, new ChangedGridMessage(entity, transform.GridID, grid.Index));
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldGridId = transform.GridID;
|
||||
|
||||
// Attach them to map / they are on an invalid grid
|
||||
if (oldGridId != GridId.Invalid)
|
||||
{
|
||||
transform.AttachParent(_mapManager.GetMapEntity(transform.MapID));
|
||||
RaiseLocalEvent(entity.Uid, new ChangedGridMessage(entity, oldGridId, GridId.Invalid));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueMoveEvent(MoveEvent moveEvent)
|
||||
public sealed class ChangedGridMessage : EntityEventArgs
|
||||
{
|
||||
public IEntity Entity;
|
||||
public GridId OldGrid;
|
||||
public GridId NewGrid;
|
||||
|
||||
public ChangedGridMessage(IEntity entity, GridId oldGrid, GridId newGrid)
|
||||
{
|
||||
_queuedMoveEvents.Enqueue(moveEvent);
|
||||
Entity = entity;
|
||||
OldGrid = oldGrid;
|
||||
NewGrid = newGrid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
|
||||
bool skipHook,
|
||||
ISerializationContext? context)
|
||||
{
|
||||
return new();
|
||||
return new(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Robust.UnitTesting
|
||||
entMan.Startup();
|
||||
}
|
||||
|
||||
IoCManager.Resolve<IEntityLookup>().Initialize();
|
||||
IoCManager.Resolve<IEntityLookup>().Startup();
|
||||
var mapMan = IoCManager.Resolve<IMapManager>();
|
||||
mapMan.Initialize();
|
||||
mapMan.Startup();
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace Robust.UnitTesting.Server
|
||||
//Tier 2: Simulation
|
||||
container.Register<IEntityManager, EntityManager>();
|
||||
container.Register<IMapManager, MapManager>();
|
||||
container.Register<IEntityLookup, SharedEntityLookup>();
|
||||
container.Register<IEntityLookup, EntityLookup>();
|
||||
container.Register<ISerializationManager, SerializationManager>();
|
||||
container.Register<IComponentManager, ComponentManager>();
|
||||
container.Register<IPrototypeManager, PrototypeManager>();
|
||||
@@ -203,9 +203,9 @@ namespace Robust.UnitTesting.Server
|
||||
|
||||
var entityMan = container.Resolve<IEntityManager>();
|
||||
entityMan.Initialize();
|
||||
IoCManager.Resolve<IEntityLookup>().Initialize();
|
||||
_systemDelegate?.Invoke(container.Resolve<IEntitySystemManager>());
|
||||
entityMan.Startup();
|
||||
IoCManager.Resolve<IEntityLookup>().Startup();
|
||||
|
||||
var mapManager = container.Resolve<IMapManager>();
|
||||
mapManager.Initialize();
|
||||
|
||||
Reference in New Issue
Block a user