Compare commits

...

33 Commits

Author SHA1 Message Date
Pieter-Jan Briers
15eded3da8 Version: 210.0.2 2024-02-13 16:05:59 +01:00
Pieter-Jan Briers
b4165e8661 ALSO revert changes to TextureRect from #4841
Stretch modes are broken or something, SS14 lobby art looks wrong. Can't be arsed to debug it myself.
2024-02-13 16:05:46 +01:00
Pieter-Jan Briers
ad339b5bfd Version: 210.0.1 2024-02-13 15:30:20 +01:00
Pieter-Jan Briers
e1197af8ce Revert changes to TextureButton from #4841
Breaks SS14 stylesheets due to not responding to style properties anymore.

At least one of those seems to be unfixable (ModulateSelf usage) which makes me think we should just deprecate ModulateSelf instead. However I'm not fixing that here.
2024-02-13 15:29:46 +01:00
metalgearsloth
102cadf3a6 Version: 210.0.0 2024-02-13 18:26:01 +11:00
Hannah Giovanna Dawson
e7723b61bc Add UnicodeRange to sandbox (#4894)
* Add GetEncoding to sandbox (#4892)
Need this struct allowlisted to for nice unicode sanitization.

* Add UnicodeRanges too

* Changelog

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-11 17:22:11 +01:00
Moony
a9d17337a3 RT Patches for UI improvements (#4841)
* fix up buttons

* wah

* ough

* huhwuhuahsdhsfdj

* loud incorrect buzzer

* wawa

* Allow XmlnsDefinition

* wawa

* Release notes.

* Expose keybind loading.

* address reviews and other things

---------

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-11 17:18:39 +01:00
Pieter-Jan Briers
74622bac83 Add DateTimeSerializer 2024-02-11 16:40:52 +01:00
Pieter-Jan Briers
a3047b1687 More warning fixes 2024-02-11 15:51:07 +01:00
Pieter-Jan Briers
3a55118143 Replace CVar OnValueChanged in systems with Subs.CVar 2024-02-11 13:29:27 +01:00
Pieter-Jan Briers
3c5fbc648a Add Subs.CVar helper for subscribing to CVar changes from entity systems 2024-02-11 13:29:27 +01:00
Pieter-Jan Briers
f9c39bce0b Use ValueList for EntitySystem subscriptions list.
That's a hundred something lists just gone and I don't have to ??=
2024-02-11 13:29:27 +01:00
Hannah Giovanna Dawson
0e8c803c0f Add GetEncoding to sandbox (#4892) 2024-02-10 17:59:13 +01:00
c4llv07e
6bb7b88c69 Save discord rich presense to the user config (#4884)
Signed-off-by: c4llv07e <kseandi@gmail.com>
2024-02-02 00:36:59 +01:00
metalgearsloth
9e0fc7017c Version: 209.0.1 2024-02-02 01:02:42 +11:00
metalgearsloth
76317b7ab3 Fix bad import (#4882)
fork why are you so SLOW.
2024-02-02 01:01:21 +11:00
metalgearsloth
d5f4d4bf2f Version: 209.0.0 2024-02-02 00:40:18 +11:00
metalgearsloth
156d1a6b14 Add SharedMapSystem helper for tile methods (#4845)
* Add SharedMapSystem helper for tile methods

* weh

* release

* Resolves

* Move some of these

* note

* Remove obsolete

* note
2024-02-02 00:39:20 +11:00
metalgearsloth
3fa456fd44 Fix relay refreshes where the entity gets contained to itself (#4865)
If the parent is somewhere below the transform hierarchy should still be okay, this just fixes where something getting pulled gets picked up as pulling doesn't seem to be handling this anymore.
2024-02-01 20:33:33 +11:00
LordCarve
bf9bb46154 Added ReferenceEquals() tests for RobustIntegrationTest (#4839)
* Added a test that checks that RobustIntegrationTest Client and Server do not end up with same sub-ComponentState reference objects.

* Un-ignore and adjust the test.

* review

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-02-01 20:22:52 +11:00
metalgearsloth
fb3da0b53c Add entitylookup methods for parent / map (#4870)
* Add entitylookup methods for parent / map

Content's done it a bunch so make it reusable.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-31 17:21:41 +01:00
Pieter-Jan Briers
5c635c09b4 Replay int overflows, fixed again (#4878)
* Reapply "Fix replay int overflow issues." (#4802)

This reverts commit 32049e34f2.

* IConfigurationManager.LoadDefaultsFromTomlStream now does required type conversions.

This fixes scenarios like loading of `long` CVars.
2024-01-31 17:17:28 +01:00
Pieter-Jan Briers
fad539212d Cache TotalTicks in MIDI renderer
This is the "total length" of the MIDI file, but it's not cached by FluidSynth and expensive to calculate.

This property was used in the render code (for local files only) and the instrument menu. Caching it drastically improves performance.
2024-01-31 00:29:03 +01:00
Pieter-Jan Briers
7275302639 Stop multithreading FluidSynth with synth.cpu-cores.
It makes no sense for our use case, and it caused FluidSynth to allocate a different thread pool *per* mixer. And every one of those threads have *high* priority. That's like *really* bad.

Furthermore, it was based on ParallelProcessCount which is currently bugged, and because of that we were always allocating  256 (!!!) real OS threads for a MIDI synthesizer. CHRIST. (fix for ParallelProcessCount is separate)

I assume this is responsible for a ton of people's MIDI lag, it just murdering their PC's CPU scheduler.

The ability to multithread FluidSynth still exists as a CVar but it'll default to 1 and I don't think it makes sense to ever change it.

Also there was code to dynamically change the parameter, as far as I could test this just always crashed the process so out it goes.
2024-01-31 00:27:30 +01:00
metalgearsloth
5057ff97a3 Add MaxDimension property to Box2 (#4871)
* Add MaxDimension property to Box2

Sometimes I want to pretend it's a circle radius.

* a

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-30 13:28:05 +01:00
metalgearsloth
754d5a1fbb Add GetLocalPosition to controls (#4872)
* Add GetLocalPosition to controls

In my case I want the mouse's position inside of the control to show something under it unless there's a better way.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-30 13:27:28 +01:00
Kara
7cee5b67a7 More easings (#4876)
* More easings

* yeah
2024-01-30 13:24:16 +01:00
Kara
21729e7e48 Smarter EntitySystem sawmill name conversion (#4875) 2024-01-29 22:52:39 +01:00
metalgearsloth
394d1e6cc2 Add global rectangles for controls (#4873)
* Add global rectangles for controls

Like my other PR used to check if mouse is inbounds on the control without doing some skrunkly caching with mousemove.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-29 11:08:02 +01:00
metalgearsloth
f73d7f7285 Add dotted line drawing to screen handle (#4874)
Probably needs anti-aliasing but idk an easy way to do it.
2024-01-29 11:07:15 +01:00
DrSmugleaf
e505cfffd8 Fix TileEdgeOverlay flickering with tiles that are barely out of view (#4868) 2024-01-29 14:13:19 +11:00
collinlunn
d0fe3591ef Fixes parsing of ShaderBlendMode in ShaderPrototype (#4867)
* Fixes parsing of ShaderBlendMode in ShaderPrototype

* Changes ShaderPrototype parser to use case insensitive overload
2024-01-28 12:20:06 -08:00
James Simonson
1868f32457 Exposed "Bottom Margin" to itemlist (#4862)
* Exposed "Bottom Margin" to itemlist

* Changed from ItemBottomMargin to ItemSeparation

* Update ItemList.cs
2024-01-27 17:26:37 +01:00
67 changed files with 1378 additions and 269 deletions

View File

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

View File

@@ -54,6 +54,80 @@ END TEMPLATE-->
*None yet*
## 210.0.2
## 210.0.1
### Bugfixes
* Revert changes to `TextureButton` that broke style property handling.
## 210.0.0
### New features
* Controls can now hook before, after, and during rendering of their children.
* IRenderHandle is now a public API, with the caveat that it's properties and methods are unstable.
* ButtonGroup now exposes what buttons it contains, alongside which is currently pressed.
* OptionButton has additional styleclasses, and has a hook for modifying it's internal buttons.
* PanelContainer.GetStyleBox() is now protected rather than private.
* TextureButton now uses a TextureRect instead of custom drawing code.
* TextureRect has additional style properties exposed.
* A new property, TextureSizeTarget, was added, which allows specifying a size in virtual pixels that the control should attempt to draw at.
* Stretch mode is now a style property.
* Scale is now a style property.
* Avalonia.Metadata.XmlnsDefinitionAttribute is now permitted by the sandbox.
* Add MaxDimension property to Box2 to return the higher of the Width or Height.
* Add GetLocalPosition to convert ScreenCoordinates to coordinates relative to the control. Ignores window.
* Add GlobalRect and GlobalPixelRect for controls to get their UIBox2i in screen terms.
* Add dotted line drawing to DrawingHandleScreen.
* You can use `Subs.CVar()` from an entity systems to subscribe to CVar changes. This is more convenient than `IConfigurationManager.OnValueChanged` as it automatically unsubscribes on system shutdown.
* There is now a built-in type serializer for `DateTime`, so you can put `DateTime`s in your data fields.
* `System.Text.Unicode.UnicodeRange` and `UnicodeRanges` are now available in the sandbox.
### Bugfixes
* UI drawing now properly accounts for a control's draw routine potentially mangling the current matrix.
* UI roots now properly update when the global stylesheet is changed. They previously only did so if they had a dedicated stylesheet (which is the one case where they would be unaffected by a global sheet update.
## 209.0.1
### Bugfixes
* Fix missed import from 209.0.0.
## 209.0.0
### Breaking changes
* `replay.max_compressed_size` and `replay.max_uncompressed_size` CVars are now `long`.
* Remove obsolete CoordinatesExtension for ToEntityCoordinates from GridUid / Vector2i.
### New features
* Add GetEntitiesOnMap / GetChildEntities to EntityLookupSystem to return components on the specified map and components with the specified parent respectively.
* Add MaxDimension property to Box2 to return the higher of the Width or Height.
* Add GetLocalPosition to convert ScreenCoordinates to coordinates relative to the control. Ignores window.
* Add GlobalRect and GlobalPixelRect for controls to get their UIBox2i in screen terms.
* Add dotted line drawing to DrawingHandleScreen.
* `IConfigurationManager.LoadDefaultsFromTomlStream` properly does type conversions. This fixes scenarios like loading of `long` CVars.
* Add helper methods for TileRef / Vector2i to SharedMapSystem for ToCenterCoordinates (tile center EntityCoordinates) and ToCoordinates (tile origin to EntityCoordinates).
* Copy some of the coordinates extensions to SharedTransformSystem.
### Bugfixes
* Fixed integer overflows in replay max size calculation.
* Explicitly capped `replay.replay_tick_batchSize` internally to avoid high values causing allocation failures.
### Other
* Important MIDI performance improvements.
## 208.0.0
### Breaking changes

View File

@@ -39,7 +39,7 @@ public sealed class AudioOverlay : Overlay
protected internal override void Draw(in OverlayDrawArgs args)
{
var localPlayer = _playerManager.LocalPlayer?.ControlledEntity;
var localPlayer = _playerManager.LocalEntity;
if (args.ViewportControl == null || localPlayer == null)
return;

View File

@@ -106,8 +106,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
CfgManager.OnValueChanged(CVars.AudioAttenuation, OnAudioAttenuation, true);
CfgManager.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
}
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
@@ -133,13 +133,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
_audio.SetMasterGain(value);
}
public override void Shutdown()
{
CfgManager.UnsubValueChanged(CVars.AudioAttenuation, OnAudioAttenuation);
CfgManager.UnsubValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged);
base.Shutdown();
}
private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args)
{
component.Pause();

View File

@@ -192,7 +192,12 @@ internal sealed partial class MidiManager : IMidiManager
_settings["synth.midi-bank-select"].StringValue = "gm";
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
_parallel.AddAndInvokeParallelCountChanged(UpdateParallelCount);
var midiParallel = _cfgMan.GetCVar(CVars.MidiParallelism);
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(midiParallel) * 2048), 1, 65535);
_settings["synth.cpu-cores"].IntValue = Math.Clamp(midiParallel, 1, 256);
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
}
catch (Exception e)
{
@@ -201,7 +206,10 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
_midiThread = new Thread(ThreadUpdate);
_midiThread = new Thread(ThreadUpdate)
{
Name = "RobustToolbox MIDI Thread"
};
_midiThread.Start();
_updateJob = new MidiUpdateJob()
@@ -219,18 +227,6 @@ internal sealed partial class MidiManager : IMidiManager
FluidsynthInitialized = true;
}
private void UpdateParallelCount()
{
if (_settings == null)
return;
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(_parallel.ParallelProcessCount) * 2048), 1, 65535);
_settings["synth.cpu-cores"].IntValue = Math.Clamp(_parallel.ParallelProcessCount, 1, 256);
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch

View File

@@ -38,6 +38,7 @@ internal sealed class MidiRenderer : IMidiRenderer
private readonly Synth _synth;
private readonly Sequencer _sequencer;
private NFluidsynth.Player? _player;
private int _playerTotalTicks;
private MidiDriver? _driver;
private byte _midiProgram = 1;
private byte _midiBank = 1;
@@ -144,7 +145,21 @@ internal sealed class MidiRenderer : IMidiRenderer
public bool DisableProgramChangeEvent { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
public int PlayerTotalTick
{
get
{
// GetTotalTicks is really expensive (has to iterate the entire file, not cached).
// Slight problem with caching it ourselves: the value only becomes available when the player loads the MIDI file.
// And that only happens after playback really starts, with the timer and synth and all that stuff.
// So we cache it "as soon as it's available", i.e. not 0.
// We don't care about playlists and such, so it shouldn't change anymore after.
if (_playerTotalTicks != 0)
return _playerTotalTicks;
return _playerTotalTicks = _player?.GetTotalTicks ?? 0;
}
}
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTick
@@ -339,6 +354,7 @@ internal sealed class MidiRenderer : IMidiRenderer
return false;
}
_playerTotalTicks = 0;
_player?.Dispose();
_player = new NFluidsynth.Player(_synth);
_player.SetPlaybackCallback(MidiPlayerEventHandler);
@@ -377,6 +393,7 @@ internal sealed class MidiRenderer : IMidiRenderer
_player?.Join();
_player?.Dispose();
_player = null;
_playerTotalTicks = 0;
}
StopAllNotes();

View File

@@ -229,7 +229,7 @@ namespace Robust.Client
// Don't invoke PlayerLeaveServer if PlayerJoinedServer & GameStartedSetup hasn't been called yet.
if (RunLevel > ClientRunLevel.Connecting)
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalSession));
LastDisconnectReason = args.Reason;
GameStoppedReset();

View File

@@ -188,7 +188,7 @@ namespace Robust.Client.Console
}
args.RemoveAt(0);
var shell = new ConsoleShell(this, session ?? _player.LocalPlayer?.Session, session == null);
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
var cmdArgs = args.ToArray();
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
@@ -200,8 +200,7 @@ namespace Robust.Client.Console
// When not connected to a server, you can run all local commands.
// When connected to a server, you can only run commands according to the con group controller.
return _player.LocalPlayer == null
|| _player.LocalPlayer.Session.Status <= SessionStatus.Connecting
return _player.LocalSession is not { Status: > SessionStatus.Connecting }
|| _conGroup.CanCommand(cmdName);
}

View File

@@ -16,8 +16,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
if (_playerManager.LocalEntity is not { } controlled)
{
shell.WriteLine("You don't have an attached entity.");
return;

View File

@@ -420,7 +420,7 @@ namespace Robust.Client.Debugging
if (mapPos.MapId != args.MapId)
return;
var player = _playerManager.LocalPlayer?.ControlledEntity;
var player = _playerManager.LocalEntity;
if (!_entityManager.TryGetComponent<TransformComponent>(player, out var playerXform) ||
playerXform.MapID != args.MapId)

View File

@@ -42,6 +42,8 @@ public sealed partial class ClientEntityManager
var pending = PendingNetEntityStates.GetOrNew(nEntity);
pending.Add((typeof(T), callerEntity));
return entity.Item1;
}

View File

@@ -221,7 +221,7 @@ namespace Robust.Client.GameObjects
public void DispatchReceivedNetworkMsg(EntityEventArgs msg)
{
var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType());
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalPlayer!.Session), msg)!;
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalSession!), msg)!;
ReceivedSystemMessage?.Invoke(this, msg);
ReceivedSystemMessage?.Invoke(this, sessionMsg);
}

View File

@@ -105,12 +105,10 @@ namespace Robust.Client.GameObjects
/// <param name="inputCmd">Input command to handle as predicted.</param>
public void PredictInputCommand(IFullInputCmdMessage inputCmd)
{
DebugTools.AssertNotNull(_playerManager.LocalPlayer);
var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId);
Predicted = true;
var session = _playerManager.LocalPlayer!.Session;
var session = _playerManager.LocalSession;
foreach (var handler in BindRegistry.GetHandlers(keyFunc))
{
if (handler.HandleCmdMessage(EntityManager, session, inputCmd))
@@ -145,27 +143,22 @@ namespace Robust.Client.GameObjects
private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] args)
{
var localPlayer = _playerManager.LocalPlayer;
if(localPlayer is null)
return;
var pent = localPlayer.ControlledEntity;
if(pent is null)
if (_playerManager.LocalEntity is not { } pent)
return;
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
var pxform = Transform(pent.Value);
var pxform = Transform(pent);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
var coords = EntityCoordinates.FromMap(EntityManager, pent.Value, new MapCoordinates(wPos, pxform.MapID));
var coords = EntityCoordinates.FromMap(EntityManager, pent, new MapCoordinates(wPos, pxform.MapID));
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid);
HandleInputCommand(localPlayer.Session, keyFunction, message);
HandleInputCommand(_playerManager.LocalSession, keyFunction, message);
}
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
@@ -208,11 +201,8 @@ namespace Robust.Client.GameObjects
/// </summary>
public void SetEntityContextActive()
{
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
{
if (_playerManager.LocalEntity is not { } controlled)
return;
}
SetEntityContextActive(_inputManager, controlled);
}

View File

@@ -62,7 +62,7 @@ namespace Robust.Client.GameObjects
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
_sawmill = _logManager.GetSawmill("sprite");
}
@@ -72,12 +72,6 @@ namespace Robust.Client.GameObjects
QueueUpdateInert(uid, component);
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged);
}
private void OnBiasChanged(double value)
{
SpriteComponent.DirectionBias = value;

View File

@@ -29,10 +29,7 @@ namespace Robust.Client.GameObjects
var uiKey = ev.UiKey;
var message = ev.Message;
// This should probably not happen at this point, but better make extra sure!
if (_playerManager.LocalPlayer != null)
message.Session = _playerManager.LocalPlayer.Session;
message.Session = _playerManager.LocalSession!;
message.Entity = GetNetEntity(uid);
message.UiKey = uiKey;
@@ -75,8 +72,7 @@ namespace Robust.Client.GameObjects
boundInterface.Open();
uiComp.OpenInterfaces[uiKey] = boundInterface;
var playerSession = _playerManager.LocalPlayer?.Session;
if (playerSession != null)
if (_playerManager.LocalSession is { } playerSession)
{
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects
return;
}
var player = _playerManager.LocalPlayer?.ControlledEntity;
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{

View File

@@ -232,9 +232,9 @@ namespace Robust.Client.GameStates
return default;
}
DebugTools.AssertNotNull(_players.LocalPlayer);
DebugTools.Assert(_players.LocalSession != null);
var evArgs = new EntitySessionEventArgs(_players.LocalPlayer!.Session);
var evArgs = new EntitySessionEventArgs(_players.LocalSession);
_pendingSystemMessages.Enqueue((_nextInputCmdSeq, _timing.CurTick, message,
new EntitySessionMessage<T>(evArgs, message)));

View File

@@ -313,7 +313,7 @@ namespace Robust.Client.GameStates
if (args.Length == 0)
{
entity = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
entity = _playerManager.LocalEntity ?? EntityUid.Invalid;
}
else if (!NetEntity.TryParse(args[0], out var netEntity) || !_entManager.TryGetEntity(netEntity, out entity))
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
@@ -14,6 +15,79 @@ namespace Robust.Client.Graphics
{
}
/// <summary>
/// Simialr to DrawLine but has dashes interspersed.
/// </summary>
/// <param name="offset">Offset from the start of the line.</param>
/// <param name="dashSize">How long a dash is.</param>
/// <param name="gapSize">How long the gap between dashes is.</param>
public void DrawDottedLine(Vector2 from, Vector2 to, Color color, float offset = 0f, float dashSize = 8f, float gapSize = 2f)
{
var lineVector = to - from;
// No drawing for you.
if (lineVector.LengthSquared() < 10f * float.Epsilon)
return;
var lineAndGap = gapSize + dashSize;
var lines = new ValueList<Vector2>();
// Minimum distance.
if (lineVector.Length() < lineAndGap)
{
lines.Add(from);
lines.Add(to);
}
else
{
var maxLength = lineVector.Length();
var normalizedLine = lineVector.Normalized();
var dashVector = normalizedLine * dashSize;
var gapVector = normalizedLine * gapSize;
var position = from;
offset %= (dashSize + gapSize);
var length = offset;
var dashLength = dashSize;
// If offset is less than gap size then start with a gap
// otherwise start with a partial line
if (offset > 0f)
{
if (offset < gapSize)
{
position += normalizedLine * offset;
length += offset;
}
else
{
dashLength = (offset - gapSize);
}
}
while (length < maxLength)
{
lines.Add(position);
position += normalizedLine * dashLength;
var lengthFromStart = (position - from).Length();
// if over length then cap the thing.
if (lengthFromStart > maxLength)
{
position = to;
}
lines.Add(position);
dashLength = dashVector.Length();
position += gapVector;
length = (position - from).Length();
}
}
DrawPrimitives(DrawPrimitiveTopology.LineList, lines.Span, color);
}
public abstract void DrawRect(UIBox2 rect, Color color, bool filled = true);
public abstract void DrawTextureRectRegion(Texture texture, UIBox2 rect, UIBox2? subRegion = null, Color? modulate = null);

View File

@@ -6,7 +6,10 @@ using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
internal interface IRenderHandle
/// <remarks>
/// Unstable API. Likely to break hard during renderer rewrite if you rely on it.
/// </remarks>
public interface IRenderHandle
{
DrawingHandleScreen DrawingHandleScreen { get; }
DrawingHandleWorld DrawingHandleWorld { get; }

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.ResourceManagement;
@@ -69,7 +69,7 @@ namespace Robust.Client.Graphics
ShaderBlendMode? blend = null;
if (_rawBlendMode != null)
{
if (!Enum.TryParse<ShaderBlendMode>(_rawBlendMode.ToUpper(), out var parsed))
if (!Enum.TryParse<ShaderBlendMode>(_rawBlendMode, true, out var parsed))
Logger.Error($"invalid mode: {_rawBlendMode}");
else
blend = parsed;

View File

@@ -126,7 +126,7 @@ namespace Robust.Client.Input
{
try
{
LoadKeyFile(path, true);
LoadKeyFile(path, false, true);
}
catch (Exception e)
{
@@ -136,7 +136,7 @@ namespace Robust.Client.Input
if (_resourceMan.ContentFileExists(path))
{
LoadKeyFile(path, false);
LoadKeyFile(path, true);
}
}
@@ -496,7 +496,13 @@ namespace Robust.Client.Input
return true;
}
private void LoadKeyFile(ResPath file, bool userData)
/// <summary>
/// Loads a keybind file, configuring keybinds.
/// </summary>
/// <param name="file">File to load from the content package</param>
/// <param name="defaultRegistration">Whether or not this is a "default" keybind set. If it is, then it won't override the current configuration, only the defaults.</param>
/// <param name="userData">Whether or not to load from the user data directory instead of the content package.</param>
public void LoadKeyFile(ResPath file, bool defaultRegistration, bool userData = false)
{
TextReader reader;
if (userData)
@@ -526,7 +532,7 @@ namespace Robust.Client.Input
continue;
}
if (!userData)
if (defaultRegistration)
{
_defaultRegistrations.Add(reg);
@@ -538,11 +544,11 @@ namespace Robust.Client.Input
}
}
RegisterBinding(reg, markModified: userData);
RegisterBinding(reg, markModified: defaultRegistration);
}
}
if (userData && mapping.TryGet("leaveEmpty", out var node))
if (!defaultRegistration && mapping.TryGet("leaveEmpty", out var node))
{
var leaveEmpty = _serialization.Read<BoundKeyFunction[]>(node, notNullableOverride: true);

View File

@@ -37,7 +37,9 @@ public sealed class TileEdgeOverlay : GridOverlay
var tileDimensions = new Vector2(tileSize, tileSize);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(Grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var localAABB = invMatrix.TransformBox(args.WorldBounds);
var bounds = args.WorldBounds;
bounds = new Box2Rotated(bounds.Box.Enlarged(1), bounds.Rotation, bounds.Origin);
var localAABB = invMatrix.TransformBox(bounds);
var enumerator = mapSystem.GetLocalTilesEnumerator(Grid.Owner, Grid, localAABB, false);

View File

@@ -495,7 +495,7 @@ namespace Robust.Client.Placement
{
// Try to get current map.
var map = MapId.Nullspace;
if (EntityManager.TryGetComponent(PlayerManager.LocalPlayer?.ControlledEntity, out TransformComponent? xform))
if (EntityManager.TryGetComponent(PlayerManager.LocalEntity, out TransformComponent? xform))
{
map = xform.MapID;
}
@@ -512,7 +512,7 @@ namespace Robust.Client.Placement
private bool CurrentEraserMouseCoordinates(out EntityCoordinates coordinates)
{
var ent = PlayerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
var ent = PlayerManager.LocalEntity ?? EntityUid.Invalid;
if (ent == EntityUid.Invalid)
{
coordinates = new EntityCoordinates();
@@ -640,7 +640,7 @@ namespace Robust.Client.Placement
if (CurrentPermission is not {Range: > 0} ||
!CurrentMode.RangeRequired ||
PlayerManager.LocalPlayer?.ControlledEntity is not {Valid: true} controlled)
PlayerManager.LocalEntity is not {Valid: true} controlled)
return;
var worldPos = EntityManager.GetComponent<TransformComponent>(controlled).WorldPosition;

View File

@@ -216,7 +216,7 @@ namespace Robust.Client.Placement
{
if (!RangeRequired)
return true;
var controlled = pManager.PlayerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
var controlled = pManager.PlayerManager.LocalEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
{
return false;

View File

@@ -10,14 +10,13 @@ namespace Robust.Client.Player
public override Filter FromEntities(Filter filter, params EntityUid[] entities)
{
if (_playerManager.LocalPlayer is not { } localPlayer
|| localPlayer.Session.AttachedEntity is not {Valid: true} attachedUid)
if (_playerManager.LocalEntity is not {Valid: true} attachedUid)
return filter;
foreach (var uid in entities)
{
if (uid == attachedUid)
filter.AddPlayer(localPlayer.Session);
filter.AddPlayer(_playerManager.LocalSession!);
}
return filter;

View File

@@ -36,12 +36,12 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager
private void OnRecordingStarted(MappingDataNode metadata, List<object> messages)
{
if (_player.LocalPlayer == null)
if (_player.LocalSession == null)
return;
// Add information about the user doing the recording. This is used to set the default replay observer position
// when playing back the replay.
var guid = _player.LocalPlayer.UserId.UserId.ToString();
var guid = _player.LocalUser.ToString();
metadata[ReplayConstants.MetaKeyRecordedBy] = new ValueDataNode(guid);
}

View File

@@ -226,6 +226,10 @@ namespace Robust.Client.UserInterface
/// <seealso cref="Rect"/>
public UIBox2i PixelRect => UIBox2i.FromDimensions(PixelPosition, PixelSize);
public UIBox2 GlobalRect => UIBox2.FromDimensions(GlobalPosition, _size);
public UIBox2i GlobalPixelRect => UIBox2i.FromDimensions(GlobalPixelPosition, PixelSize);
/// <summary>
/// Horizontal alignment mode.
/// This determines how the control should be laid out horizontally
@@ -464,6 +468,14 @@ namespace Robust.Client.UserInterface
}
}
/// <summary>
/// Gets the screen coordinates position relative to the control.
/// </summary>
public Vector2 GetLocalPosition(ScreenCoordinates coordinates)
{
return coordinates.Position - GlobalPixelPosition;
}
/// <summary>
/// Notify the layout system that this control's <see cref="Measure"/> result may have changed
/// and must be recalculated.

View File

@@ -545,6 +545,36 @@ namespace Robust.Client.UserInterface
Draw(renderHandle.DrawingHandleScreen);
}
protected internal virtual void PreRenderChildren(ref ControlRenderArguments args)
{
}
protected internal virtual void PostRenderChildren(ref ControlRenderArguments args)
{
}
protected internal virtual void RenderChildOverride(ref ControlRenderArguments args, int childIndex, Vector2i position)
{
RenderControl(ref args, childIndex, position);
}
public ref struct ControlRenderArguments
{
public IRenderHandle Handle;
public ref int Total;
public Vector2i Position;
public Color Modulate;
public UIBox2i? ScissorBox;
public ref Matrix3 CoordinateTransform;
}
protected void RenderControl(ref ControlRenderArguments args, int childIndex, Vector2i position)
{
UserInterfaceManagerInternal.RenderControl(args.Handle, ref args.Total, GetChild(childIndex), position, args.Modulate, args.ScissorBox, args.CoordinateTransform);
}
public void UpdateDraw()
{
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Audio;
using Robust.Client.ResourceManagement;
using Robust.Shared.ContentPack;
@@ -37,8 +38,10 @@ namespace Robust.Client.UserInterface.Controls
get => _group;
set
{
if (value?.InternalButtons.Contains(this) ?? false)
return; // No work to do.
// Remove from old group.
_group?.Buttons.Remove(this);
_group?.InternalButtons.Remove(this);
_group = value;
@@ -47,11 +50,12 @@ namespace Robust.Client.UserInterface.Controls
return;
}
value.Buttons.Add(this);
value.InternalButtons.Add(this);
ToggleMode = true;
// Set us to pressed if we're the first button.
Pressed = value.Buttons.Count == 0;
// Set us to pressed if we're the first button. Doesn't go through the setter to avoid setting off our own error check.
_pressed = value.InternalButtons.Count == 1;
DrawModeChanged();
}
}
@@ -326,7 +330,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
foreach (var button in _group.Buttons)
foreach (var button in _group.InternalButtons)
{
if (button != this && button.Pressed)
{
@@ -440,6 +444,9 @@ namespace Robust.Client.UserInterface.Controls
/// </remarks>
public sealed class ButtonGroup
{
internal readonly List<BaseButton> Buttons = new();
internal readonly List<BaseButton> InternalButtons = new();
public IReadOnlyList<BaseButton> Buttons => InternalButtons;
public BaseButton? Pressed => InternalButtons.FirstOrDefault(x => x.Pressed);
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
@@ -9,6 +9,9 @@ using Robust.Shared.Input;
using Robust.Shared.Maths;
using Timer = Robust.Shared.Timing.Timer;
/// <summary>
/// Represents a scrollable list of items in a user interface.
/// </summary>
namespace Robust.Client.UserInterface.Controls
{
[Virtual]
@@ -29,6 +32,10 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertySelectedItemBackground = "selected-item-background";
public const string StylePropertyDisabledItemBackground = "disabled-item-background";
/// <summary>
/// Gets or sets the ItemSeparation of individual list items
/// </summary>
public int ItemSeparation { get; set; } = 0; // Default value is 0px
public int Count => _itemList.Count;
public bool IsReadOnly => false;
@@ -68,8 +75,10 @@ namespace Robust.Client.UserInterface.Controls
itemHeight += ActualItemBackground.MinimumSize.Y * UIScale;
_totalContentHeight += (int)Math.Ceiling(itemHeight);
_totalContentHeight += ItemSeparation;
}
//Remove unneeded ItemSeparation on last item.
_totalContentHeight -= ItemSeparation;
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
_updateScrollbarVisibility();
}
@@ -390,6 +399,9 @@ namespace Robust.Client.UserInterface.Controls
}
offset += itemHeight;
// Add a ItemSeparation at the bottom of each item.
offset += ItemSeparation;
}
}
@@ -399,7 +411,7 @@ namespace Robust.Client.UserInterface.Controls
var color = ActualFontColor;
var offsetY = (int) (box.Height - font.GetHeight(UIScale)) / 2;
var baseLine = new Vector2i(0, offsetY + font.GetAscent(UIScale)) + box.TopLeft;
var baseLine = new Vector2i(5, offsetY + font.GetAscent(UIScale)) + box.TopLeft;
foreach (var rune in text.EnumerateRunes())
{

View File

@@ -12,6 +12,7 @@ namespace Robust.Client.UserInterface.Controls
public class OptionButton : ContainerButton
{
public const string StyleClassOptionButton = "optionButton";
public const string StyleClassPopup = "optionButtonPopup";
public const string StyleClassOptionTriangle = "optionTriangle";
public readonly ScrollContainer OptionsScroll;
@@ -74,7 +75,8 @@ namespace Robust.Client.UserInterface.Controls
_popup = new Popup()
{
Children = { OptionsScroll }
Children = { new PanelContainer(), OptionsScroll },
StyleClasses = { StyleClassPopup }
};
_popup.OnPopupHide += OnPopupHide;
@@ -99,6 +101,11 @@ namespace Robust.Client.UserInterface.Controls
AddItem(label, id);
}
public virtual void ButtonOverride(Button button)
{
}
public void AddItem(string label, int? id = null)
{
if (id == null)
@@ -132,6 +139,8 @@ namespace Robust.Client.UserInterface.Controls
{
Select(0);
}
ButtonOverride(button);
}
private void TogglePopup(bool show)
@@ -139,6 +148,8 @@ namespace Robust.Client.UserInterface.Controls
if (show)
{
var globalPos = GlobalPosition;
globalPos.Y += Size.Y + 1; // Place it below us, with a safety margin.
globalPos.Y -= Margin.SumVertical;
OptionsScroll.Measure(Window?.Size ?? Vector2Helpers.Infinity);
var (minX, minY) = OptionsScroll.DesiredSize;
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));

View File

@@ -15,13 +15,13 @@ namespace Robust.Client.UserInterface.Controls
{
base.Draw(handle);
var style = _getStyleBox();
var style = GetStyleBox();
style?.Draw(handle, PixelSizeBox, UIScale);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var styleSize = _getStyleBox()?.MinimumSize ?? Vector2.Zero;
var styleSize = GetStyleBox()?.MinimumSize ?? Vector2.Zero;
var measureSize = Vector2.Max(availableSize - styleSize, Vector2.Zero);
var childSize = Vector2.Zero;
foreach (var child in Children)
@@ -36,7 +36,7 @@ namespace Robust.Client.UserInterface.Controls
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var ourSize = UIBox2.FromDimensions(Vector2.Zero, finalSize);
var contentBox = _getStyleBox()?.GetContentBox(ourSize, 1) ?? ourSize;
var contentBox = GetStyleBox()?.GetContentBox(ourSize, 1) ?? ourSize;
foreach (var child in Children)
{
@@ -47,7 +47,7 @@ namespace Robust.Client.UserInterface.Controls
}
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getStyleBox()
protected StyleBox? GetStyleBox()
{
if (PanelOverride != null)
{

View File

@@ -61,6 +61,9 @@ namespace Robust.Client.UserInterface
Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePos);
Color GetMainClearColor();
void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3 coordinateTransform);
}
}

View File

@@ -132,9 +132,10 @@ internal sealed partial class UserInterfaceManager
try
{
var total = 0;
_render(renderHandle, ref total, root, Vector2i.Zero, Color.White, null);
var drawingHandle = renderHandle.DrawingHandleScreen;
drawingHandle.SetTransform(Vector2.Zero, Angle.Zero, Vector2.One);
drawingHandle.SetTransform(Matrix3.Identity);
RenderControl(renderHandle, ref total, root, Vector2i.Zero, Color.White, null, Matrix3.Identity);
drawingHandle.SetTransform(Matrix3.Identity);
OnPostDrawUIRoot?.Invoke(new PostDrawUIRootEventArgs(root, drawingHandle));
_prof.WriteValue("Controls rendered", ProfData.Int32(total));

View File

@@ -75,7 +75,7 @@ namespace Robust.Client.UserInterface
foreach (var root in _roots)
{
if (root.Stylesheet != null)
if (root.Stylesheet == null)
{
root.StylesheetUpdateRecursive();
}
@@ -329,8 +329,8 @@ namespace Robust.Client.UserInterface
}
}
private void _render(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox)
public void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3 coordinateTransform)
{
if (!control.Visible)
{
@@ -377,7 +377,10 @@ namespace Robust.Client.UserInterface
total += 1;
var handle = renderHandle.DrawingHandleScreen;
handle.SetTransform(position, Angle.Zero, Vector2.One);
var oldXform = handle.GetTransform();
var xform = oldXform;
xform.Multiply(Matrix3.CreateTransform(position, Angle.Zero, Vector2.One));
handle.SetTransform(xform);
modulate *= control.Modulate;
if (_rendering || control.AlwaysRender)
@@ -389,16 +392,32 @@ namespace Robust.Client.UserInterface
handle.Modulate = oldMod;
handle.UseShader(null);
}
handle.SetTransform(oldXform);
var args = new Control.ControlRenderArguments()
{
Handle = renderHandle,
Total = ref total,
Modulate = modulate,
ScissorBox = scissorRegion,
CoordinateTransform = ref coordinateTransform
};
control.PreRenderChildren(ref args);
foreach (var child in control.Children)
{
_render(renderHandle, ref total, child, position + child.PixelPosition, modulate, scissorRegion);
var pos = position + (Vector2i) coordinateTransform.Transform(child.PixelPosition);
control.RenderChildOverride(ref args, child.GetPositionInParent(), pos);
}
control.PostRenderChildren(ref args);
if (clip)
{
renderHandle.SetScissor(scissorBox);
}
handle.SetTransform(oldXform);
}
public Color GetMainClearColor() => RootControl.ActualBgColor;

View File

@@ -28,7 +28,7 @@ namespace Robust.Server.GameObjects
base.Initialize();
SubscribeLocalEvent<MapGridComponent, EmptyGridEvent>(HandleGridEmpty);
_cfg.OnValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
Subs.CVar(_cfg, CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
@@ -64,13 +64,6 @@ namespace Robust.Server.GameObjects
return !(grid.GetAllTiles().Any());
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion);
}
private void HandleGridEmpty(EntityUid uid, MapGridComponent component, EmptyGridEvent args)
{
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))

View File

@@ -20,7 +20,8 @@ namespace Robust.Server.GameObjects
{
base.Initialize();
LoadMetricCVar();
_configurationManager.OnValueChanged(CVars.MetricsEnabled, _ => LoadMetricCVar());
Subs.CVar(_configurationManager, CVars.MetricsEnabled, _ => LoadMetricCVar());
}
private void LoadMetricCVar()

View File

@@ -126,11 +126,12 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent += OnEntityMove;
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
_configManager.OnValueChanged(CVars.NetLowLodRange, OnLodChanged, true);
_configManager.OnValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged, true);
_configManager.OnValueChanged(CVars.NetPvsAsync, OnAsyncChanged, true);
Subs.CVar(_configManager, CVars.NetPVS, SetPvs, true);
Subs.CVar(_configManager, CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
Subs.CVar(_configManager, CVars.NetLowLodRange, OnLodChanged, true);
Subs.CVar(_configManager, CVars.NetForceAckThreshold, OnForceAckChanged, true);
Subs.CVar(_configManager, CVars.NetPvsAsync, OnAsyncChanged, true);
Subs.CVar(_configManager, CVars.NetPvsCompressLevel, ResetParallelism, true);
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
@@ -138,7 +139,6 @@ internal sealed partial class PvsSystem : EntitySystem
InitializeDirty();
_parallelMgr.ParallelCountChanged += ResetParallelism;
_configManager.OnValueChanged(CVars.NetPvsCompressLevel, ResetParallelism, true);
}
public override void Shutdown()
@@ -148,10 +148,6 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent -= OnEntityMove;
_configManager.UnsubValueChanged(CVars.NetPVS, SetPvs);
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged);
_configManager.UnsubValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged);
_configManager.UnsubValueChanged(CVars.NetPvsCompressLevel, ResetParallelism);
_parallelMgr.ParallelCountChanged -= ResetParallelism;
_serverGameStateManager.ClientAck -= OnClientAck;

View File

@@ -64,7 +64,7 @@ namespace Robust.Server.Physics
SubscribeNetworkEvent<RequestGridNodesMessage>(OnDebugRequest);
SubscribeNetworkEvent<StopGridNodesMessage>(OnDebugStopRequest);
_cfg.OnValueChanged(CVars.GridSplitting, SetSplitAllowed, true);
Subs.CVar(_cfg, CVars.GridSplitting, SetSplitAllowed, true);
}
private void SetSplitAllowed(bool value) => SplitAllowed = value;
@@ -73,7 +73,6 @@ namespace Robust.Server.Physics
{
base.Shutdown();
_subscribedSessions.Clear();
_cfg.UnsubValueChanged(CVars.GridSplitting, SetSplitAllowed);
}
/// <summary>

View File

@@ -75,6 +75,15 @@ namespace Robust.Shared.Maths
get => new(Width, Height);
}
/// <summary>
/// Returns the highest of width or height.
/// </summary>
public readonly float MaxDimension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => MathF.Max(Height, Width);
}
public readonly Vector2 Center
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -177,13 +186,13 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 Intersect(in Box2 other)
{
var ourLeftBottom = new System.Numerics.Vector2(Left, Bottom);
var ourRightTop = new System.Numerics.Vector2(Right, Top);
var otherLeftBottom = new System.Numerics.Vector2(other.Left, other.Bottom);
var otherRightTop = new System.Numerics.Vector2(other.Right, other.Top);
var ourLeftBottom = new Vector2(Left, Bottom);
var ourRightTop = new Vector2(Right, Top);
var otherLeftBottom = new Vector2(other.Left, other.Bottom);
var otherRightTop = new Vector2(other.Right, other.Top);
var max = System.Numerics.Vector2.Max(ourLeftBottom, otherLeftBottom);
var min = System.Numerics.Vector2.Min(ourRightTop, otherRightTop);
var max = Vector2.Max(ourLeftBottom, otherLeftBottom);
var min = Vector2.Min(ourRightTop, otherRightTop);
if (max.X <= min.X && max.Y <= min.Y)
return new Box2(max.X, max.Y, min.X, min.Y);
@@ -210,13 +219,13 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 Union(in Box2 other)
{
var ourLeftBottom = new System.Numerics.Vector2(Left, Bottom);
var otherLeftBottom = new System.Numerics.Vector2(other.Left, other.Bottom);
var ourRightTop = new System.Numerics.Vector2(Right, Top);
var otherRightTop = new System.Numerics.Vector2(other.Right, other.Top);
var ourLeftBottom = new Vector2(Left, Bottom);
var otherLeftBottom = new Vector2(other.Left, other.Bottom);
var ourRightTop = new Vector2(Right, Top);
var otherRightTop = new Vector2(other.Right, other.Top);
var leftBottom = System.Numerics.Vector2.Min(ourLeftBottom, otherLeftBottom);
var rightTop = System.Numerics.Vector2.Max(ourRightTop, otherRightTop);
var leftBottom = Vector2.Min(ourLeftBottom, otherLeftBottom);
var rightTop = Vector2.Max(ourRightTop, otherRightTop);
if (leftBottom.X <= rightTop.X && leftBottom.Y <= rightTop.Y)
return new Box2(leftBottom.X, leftBottom.Y, rightTop.X, rightTop.Y);
@@ -396,11 +405,11 @@ namespace Robust.Shared.Maths
[Pure]
public static Box2 Union(in Vector2 a, in Vector2 b)
{
var vecA = new System.Numerics.Vector2(a.X, a.Y);
var vecB = new System.Numerics.Vector2(b.X, b.Y);
var vecA = new Vector2(a.X, a.Y);
var vecB = new Vector2(b.X, b.Y);
var min = System.Numerics.Vector2.Min(vecA, vecB);
var max = System.Numerics.Vector2.Max(vecA, vecB);
var min = Vector2.Min(vecA, vecB);
var max = Vector2.Max(vecA, vecB);
return new Box2(min.X, min.Y, max.X, max.Y);
}
@@ -412,12 +421,12 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 ExtendToContain(Vector2 vec)
{
var leftBottom = new System.Numerics.Vector2(Left, Bottom);
var rightTop = new System.Numerics.Vector2(Right, Top);
var vector = new System.Numerics.Vector2(vec.X, vec.Y);
var leftBottom = new Vector2(Left, Bottom);
var rightTop = new Vector2(Right, Top);
var vector = new Vector2(vec.X, vec.Y);
var min = System.Numerics.Vector2.Min(vector, leftBottom);
var max = System.Numerics.Vector2.Max(vector, rightTop);
var min = Vector2.Min(vector, leftBottom);
var max = Vector2.Max(vector, rightTop);
return new Box2(min.X, min.Y, max.X, max.Y);
}

View File

@@ -1,13 +1,229 @@
using System;
using JetBrains.Annotations;
namespace Robust.Shared.Maths;
// Reference: https://easings.net/
internal static class Easings
/// <summary>
/// A static class for computing easings for animations.
/// The parameter "p" is the absolute progress of the animation between 0 and 1.
/// </summary>
[PublicAPI]
public static class Easings
{
#region Trig
public static float InSine(float p)
{
return 1.0f - MathF.Cos(p * MathF.PI / 2.0f);
}
public static float OutSine(float p)
{
return MathF.Sin(p * MathF.PI / 2);
}
public static float InOutSine(float p)
{
return -(MathF.Cos(MathF.PI * p) - 1.0f) / 2.0f;
}
#endregion
#region Polynomial
public static float InQuad(float p)
{
return p * p;
}
public static float OutQuad(float p)
{
return 1 - (1 - p) * (1 - p);
}
public static float InOutQuad(float p)
{
return p < 0.5 ? 2 * p * p : 1 - MathF.Pow(-2 * p + 2, 2) / 2;
}
public static float InCubic(float p)
{
return p * p * p;
}
public static float OutCubic(float p)
{
return 1 - MathF.Pow(1 - p, 3);
}
public static float InOutCubic(float p)
{
return p < 0.5 ? 4 * p * p * p : 1 - MathF.Pow(-2 * p + 2, 3) / 2;
}
public static float InQuart(float p)
{
return p * p * p * p;
}
public static float OutQuart(float p)
{
return 1 - MathF.Pow(1 - p, 4);
}
public static float InOutQuart(float p)
{
return p < 0.5 ? 8 * p * p * p * p : 1 - MathF.Pow(-2 * p + 2, 4) / 2;
}
public static float InQuint(float p)
{
return p * p * p * p * p;
}
public static float OutQuint(float p)
{
return 1 - MathF.Pow(1 - p, 5);
}
public static float InOutQuint(float p)
{
return p < 0.5f ? (16 * p * p * p * p * p) : 1 - MathF.Pow(-2 * p + 2, 5) / 2;
return p < 0.5f ? 16 * p * p * p * p * p : 1 - MathF.Pow(-2 * p + 2, 5) / 2;
}
#endregion
#region Other
public static float InExpo(float p)
{
return p == 0 ? 0 : MathF.Pow(2, 10 * p - 10);
}
public static float OutExpo(float p)
{
return Math.Abs(p - 1) < 0.0001f ? 1 : 1 - MathF.Pow(2, -10 * p);
}
public static float InOutExpo(float p)
{
return p == 0.0f
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: p < 0.5f
? MathF.Pow(2, 20 * p - 10) / 2
: (2 - MathF.Pow(2, -20 * p + 10)) / 2;
}
public static float InCirc(float p)
{
return 1 - MathF.Sqrt(1 - MathF.Pow(p, 2));
}
public static float OutCirc(float p)
{
return MathF.Sqrt(1 - MathF.Pow(p - 1, 2));
}
public static float InOutCirc(float p)
{
return p < 0.5
? (1 - MathF.Sqrt(1 - MathF.Pow(2 * p, 2))) / 2
: (MathF.Sqrt(1 - MathF.Pow(-2 * p + 2, 2)) + 1) / 2;
}
public static float InBack(float p)
{
var c1 = 1.70158f;
var c3 = c1 + 1;
return c3 * p * p * p - c1 * p * p;
}
public static float OutBack(float p)
{
const float c1 = 1.70158f;
const float c3 = c1 + 1;
return 1 + c3 * MathF.Pow(p - 1, 3) + c1 * MathF.Pow(p - 1, 2);
}
public static float InOutBack(float p)
{
const float c1 = 1.70158f;
const float c2 = c1 * 1.525f;
return p < 0.5
? MathF.Pow(2 * p, 2) * ((c2 + 1) * 2 * p - c2) / 2
: (MathF.Pow(2 * p - 2, 2) * ((c2 + 1) * (p * 2 - 2) + c2) + 2) / 2;
}
/// <remarks>
/// elastic in, not "inelastic"
/// </remarks>
public static float InElastic(float p)
{
const float c4 = 2 * MathF.PI / 3;
return p == 0
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: -MathF.Pow(2, 10 * p - 10) * MathF.Sin((p * 10 - 10.75f) * c4);
}
public static float OutElastic(float p)
{
const float c4 = 2.0f * MathF.PI / 3.0f;
return p == 0
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: MathF.Pow(2, -10 * p) * MathF.Sin((p * 10.0f - 0.75f) * c4) + 1.0f;
}
public static float InOutElastic(float p)
{
const float c5 = 2.0f * MathF.PI / 4.5f;
return p == 0
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: p < 0.5
? -(MathF.Pow(2, 20 * p - 10) * MathF.Sin((20.0f * p - 11.125f) * c5)) / 2.0f
: MathF.Pow(2, -20.0f * p + 10.0f) * MathF.Sin((20.0f * p - 11.125f) * c5) / 2.0f + 1.0f;
}
public static float InBounce(float p)
{
return 1 - OutBounce(1 - p);
}
public static float OutBounce(float p)
{
const float n1 = 7.5625f;
const float d1 = 2.75f;
if (p < 1 / d1) return n1 * p * p;
if (p < 2 / d1) return n1 * (p -= 1.5f / d1) * p + 0.75f;
if (p < 2.5 / d1) return n1 * (p -= 2.25f / d1) * p + 0.9375f;
return n1 * (p -= 2.625f / d1) * p + 0.984375f;
}
public static float InOutBounce(float p)
{
return p < 0.5
? (1 - OutBounce(1 - 2 * p)) / 2
: (1 + OutBounce(2 * p - 1)) / 2;
}
#endregion
}

View File

@@ -50,17 +50,11 @@ public abstract partial class SharedAudioSystem : EntitySystem
base.Initialize();
InitializeEffect();
ZOffset = CfgManager.GetCVar(CVars.AudioZOffset);
CfgManager.OnValueChanged(CVars.AudioZOffset, SetZOffset);
Subs.CVar(CfgManager, CVars.AudioZOffset, SetZOffset);
SubscribeLocalEvent<AudioComponent, ComponentGetStateAttemptEvent>(OnAudioGetStateAttempt);
SubscribeLocalEvent<AudioComponent, EntityUnpausedEvent>(OnAudioUnpaused);
}
public override void Shutdown()
{
base.Shutdown();
CfgManager.UnsubValueChanged(CVars.AudioZOffset, SetZOffset);
}
protected void SetZOffset(float value)
{
ZOffset = value;

View File

@@ -1239,7 +1239,7 @@ namespace Robust.Shared
/// Enable Discord rich presence integration.
/// </summary>
public static readonly CVarDef<bool> DiscordEnabled =
CVarDef.Create("discord.enabled", true, CVar.CLIENTONLY);
CVarDef.Create("discord.enabled", true, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<string> DiscordRichPresenceMainIconId =
CVarDef.Create("discord.rich_main_icon_id", "devstation", CVar.SERVER | CVar.REPLICATED);
@@ -1323,6 +1323,16 @@ namespace Robust.Shared
public static readonly CVarDef<float> MidiVolume =
CVarDef.Create("midi.volume", 0.50f, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Controls amount of CPU cores and (by extension) polyphony for Fluidsynth.
/// </summary>
/// <remarks>
/// You probably don't want to set this to be multithreaded, the way Fluidsynth's multithreading works is
/// probably worse-than-nothing for Robust's usage.
/// </remarks>
public static readonly CVarDef<int> MidiParallelism =
CVarDef.Create("midi.parallelism", 1, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* HUB
* CVars related to public master server hub
@@ -1487,14 +1497,14 @@ namespace Robust.Shared
/// <summary>
/// Maximum compressed size of a replay recording (in kilobytes) before recording automatically stops.
/// </summary>
public static readonly CVarDef<int> ReplayMaxCompressedSize = CVarDef.Create("replay.max_compressed_size",
1024 * 256, CVar.ARCHIVE);
public static readonly CVarDef<long> ReplayMaxCompressedSize = CVarDef.Create("replay.max_compressed_size",
1024L * 256, CVar.ARCHIVE);
/// <summary>
/// Maximum uncompressed size of a replay recording (in kilobytes) before recording automatically stops.
/// </summary>
public static readonly CVarDef<int> ReplayMaxUncompressedSize = CVarDef.Create("replay.max_uncompressed_size",
1024 * 1024, CVar.ARCHIVE);
public static readonly CVarDef<long> ReplayMaxUncompressedSize = CVarDef.Create("replay.max_uncompressed_size",
1024L * 1024, CVar.ARCHIVE);
/// <summary>
/// Uncompressed size of individual files created by the replay (in kilobytes), where each file contains data

View File

@@ -113,10 +113,46 @@ namespace Robust.Shared.Configuration
{
var loaded = new HashSet<string>();
foreach (var (cvar, value) in ParseCVarValuesFromToml(stream))
var callbackEvents = new ValueList<ValueChangedInvoke>();
// Ensure callbacks are raised OUTSIDE the write lock.
using (Lock.WriteGuard())
{
loaded.Add(cvar);
OverrideDefault(cvar, value);
foreach (var (cVarName, value) in ParseCVarValuesFromToml(stream))
{
if (!_configVars.TryGetValue(cVarName, out var cVar) || !cVar.Registered)
{
_sawmill.Error($"Trying to set unregistered variable '{cVarName}'");
continue;
}
var convertedValue = value;
if (!cVar.DefaultValue.GetType().IsEnum && cVar.DefaultValue.GetType() != value.GetType())
{
try
{
convertedValue = ConvertToCVarType(value, cVar.DefaultValue.GetType());
}
catch
{
_sawmill.Error($"Override TOML parsed cvar does not match registered cvar type. Name: {cVarName}. Code Type: {cVar.DefaultValue.GetType()}. Toml type: {value.GetType()}");
continue;
}
}
cVar.DefaultValue = value;
if (cVar.OverrideValue == null && cVar.Value == null)
{
if (SetupInvokeValueChanged(cVar, convertedValue) is { } invoke)
callbackEvents.Add(invoke);
}
}
}
foreach (var callback in callbackEvents)
{
InvokeValueChanged(callback);
}
return loaded;
@@ -303,8 +339,7 @@ namespace Robust.Shared.Configuration
{
try
{
// try convert thing like int to float.
cVar.Value = Convert.ChangeType(cVar.Value, type);
cVar.Value = ConvertToCVarType(cVar.Value, type);
}
catch
{
@@ -711,6 +746,26 @@ namespace Robust.Shared.Configuration
}
}
/// <summary>
/// Try to convert a compatible value to the actual registration type of a CVar.
/// </summary>
/// <remarks>
/// When CVars are parsed from TOML, their in-code type is not known.
/// This function does the necessary conversions from e.g. int to long.
/// </remarks>
/// <param name="value">
/// The value to convert.
/// This must be a simple type like strings or integers.
/// </param>
/// <param name="cVar">
/// The registration type of the CVar.
/// </param>
/// <returns></returns>
private static object ConvertToCVarType(object value, Type cVar)
{
return Convert.ChangeType(value, cVar);
}
/// <summary>
/// Holds the data for a single configuration variable.
/// </summary>

View File

@@ -73,6 +73,9 @@ WhitelistedNamespaces:
# * The API is not *relevant* to content. e.g. System.Type.IsAnsiClass.
# * I am lazy these API lists are huge dude.
Types:
Avalonia.Metadata:
XmlnsDefinitionAttribute:
All: True
NetSerializer:
NetListAsArray`1:
Fields:
@@ -626,6 +629,7 @@ Types:
- "System.Text.Encoding get_UTF7()"
- "System.Text.Encoding get_UTF8()"
- "System.Text.Encoding get_UTF32()"
- "System.Text.Encoding GetEncoding(string)"
NormalizationForm: { } # Enum
Rune: { All: True }
StringBuilder:
@@ -716,6 +720,9 @@ Types:
ChunkEnumerator: { All: True }
AppendInterpolatedStringHandler: { All: True }
StringRuneEnumerator: { All: True }
System.Text.Unicode:
UnicodeRange: { All: True }
UnicodeRanges: { All: True }
System.Threading.Tasks:
Task: { All: True }
Task`1: { All: True }

View File

@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Collections;
namespace Robust.Shared.GameObjects
{
public abstract partial class EntitySystem
{
private List<SubBase>? _subscriptions;
private ValueList<SubBase> _subscriptions;
/// <summary>
/// A handle to allow subscription on this entity system's behalf.
@@ -84,7 +84,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeEvent(src, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<T>(src));
}
@@ -96,7 +95,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeEvent(src, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<T>(src));
}
@@ -108,7 +106,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeSessionEvent(src, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<EntitySessionMessage<T>>(src));
}
@@ -122,7 +119,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeLocalEvent(handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubLocal<TComp, TEvent>());
}
@@ -134,7 +130,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeLocalEvent(handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubLocal<TComp, TEvent>());
}
@@ -146,21 +141,17 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeLocalEvent(handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubLocal<TComp, TEvent>());
}
private void ShutdownSubscriptions()
{
if (_subscriptions == null)
return;
foreach (var sub in _subscriptions)
{
sub.Unsubscribe(this, EntityManager.EventBus);
}
_subscriptions = null;
_subscriptions = default;
}
/// <summary>
@@ -232,6 +223,19 @@ namespace Robust.Shared.GameObjects
{
System.SubscribeLocalEvent(handler, before, after);
}
/// <summary>
/// Register an action to be ran when this entity system is shut down.
/// </summary>
/// <remarks>
/// This can be used by extension methods for <see cref="Subscriptions"/>
/// to unsubscribe from from external sources such as CVars.
/// </remarks>
/// <param name="action">An action to be ran when the entity system is shut down.</param>
public void RegisterUnsubscription(Action action)
{
System._subscriptions.Add(new SubAction(action));
}
}
private abstract class SubBase
@@ -261,5 +265,13 @@ namespace Robust.Shared.GameObjects
bus.UnsubscribeLocalEvent<TComp, TBase>();
}
}
private sealed class SubAction(Action action) : SubBase
{
public override void Unsubscribe(EntitySystem sys, IEventBus bus)
{
action();
}
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.IoC;
@@ -41,8 +42,16 @@ namespace Robust.Shared.GameObjects
name = name.Substring(0, name.Length - "System".Length);
// Convert CamelCase to snake_case
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
// Ignore if all uppercase, assume acronym (e.g. NPC or HTN)
if (name.All(char.IsUpper))
{
name = name.ToLower(CultureInfo.InvariantCulture);
}
else
{
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
}
return $"system.{name}";
}

View File

@@ -0,0 +1,130 @@
using System;
using Robust.Shared.Configuration;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Extra subscription helpers for <see cref="EntitySystem"/> that are not part of the core entity system behavior.
/// </summary>
public static class EntitySystemSubscriptionExt
{
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="name">The name of the CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately inw this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
string name,
Action<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(name, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(name, onValueChanged));
}
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="cVar">The CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately in this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
CVarDef<T> cVar,
Action<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(cVar, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(cVar, onValueChanged));
}
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="name">The name of the CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately in this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
string name,
CVarChanged<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(name, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(name, onValueChanged));
}
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="cVar">The CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately in this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
CVarDef<T> cVar,
CVarChanged<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(cVar, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(cVar, onValueChanged));
}
}

View File

@@ -703,16 +703,75 @@ public sealed partial class EntityLookupSystem
GetEntitiesIntersecting(mapId, shape, transform, entities, flags);
}
/// <summary>
/// Gets entities with the specified component with the specified map.
/// </summary>
public void GetEntitiesOnMap<TComp1>(MapId mapId, HashSet<Entity<TComp1>> entities) where TComp1 : IComponent
{
var query = AllEntityQuery<TComp1, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (xform.MapID != mapId)
continue;
entities.Add((uid, comp));
}
}
/// <summary>
/// Gets entities with the specified component with the specified parent.
/// </summary>
public void GetEntitiesOnMap<TComp1, TComp2>(MapId mapId, HashSet<Entity<TComp1, TComp2>> entities)
where TComp1 : IComponent
where TComp2 : IComponent
{
var query = AllEntityQuery<TComp1, TComp2, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var comp2, out var xform))
{
if (xform.MapID != mapId)
continue;
entities.Add((uid, comp, comp2));
}
}
#endregion
private readonly record struct GridQueryCompState<T>(
HashSet<Entity<IComponent>> Intersecting,
IPhysShape Shape,
Transform Transform,
EntityLookupSystem Lookup,
LookupFlags Flags,
EntityQuery<T> Query
) where T : IComponent;
/// <summary>
/// Gets entities with the specified component with the specified parent.
/// </summary>
public void GetChildEntities<TComp1>(EntityUid parentUid, HashSet<Entity<TComp1>> entities) where TComp1 : IComponent
{
var query = AllEntityQuery<TComp1, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (xform.ParentUid != parentUid)
continue;
entities.Add((uid, comp));
}
}
/// <summary>
/// Gets entities with the specified component with the specified parent.
/// </summary>
public void GetChildEntities<TComp1, TComp2>(EntityUid parentUid, HashSet<Entity<TComp1, TComp2>> entities)
where TComp1 : IComponent
where TComp2 : IComponent
{
var query = AllEntityQuery<TComp1, TComp2, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var comp2, out var xform))
{
if (xform.ParentUid != parentUid)
continue;
entities.Add((uid, comp, comp2));
}
}
private readonly record struct GridQueryState<T>(
HashSet<Entity<T>> Intersecting,

View File

@@ -36,8 +36,8 @@ namespace Robust.Shared.GameObjects
base.Initialize();
UpdatesBefore.Add(typeof(SharedBroadphaseSystem));
_cfg.OnValueChanged(CVars.GenerateGridFixtures, SetEnabled, true);
_cfg.OnValueChanged(CVars.GridFixtureEnlargement, SetEnlargement, true);
Subs.CVar(_cfg, CVars.GenerateGridFixtures, SetEnabled, true);
Subs.CVar(_cfg, CVars.GridFixtureEnlargement, SetEnlargement, true);
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<RegenerateGridBoundsEvent>(OnGridBoundsRegenerate);
@@ -58,14 +58,6 @@ namespace Robust.Shared.GameObjects
_map.RegenerateCollision(ev.EntityUid, grid, _map.GetMapChunks(ev.EntityUid, grid).Values.ToHashSet());
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.GenerateGridFixtures, SetEnabled);
_cfg.UnsubValueChanged(CVars.GridFixtureEnlargement, SetEnlargement);
}
private void SetEnabled(bool value) => _enabled = value;
private void SetEnlargement(float value) => _fixtureEnlargement = value;

View File

@@ -0,0 +1,83 @@
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects;
public abstract partial class SharedMapSystem
{
/// <summary>
/// If the supplied coordinates intersects a grid will align with the tile center, otherwise returns the coordinates.
/// </summary>
/// <param name="coordinates"></param>
/// <returns></returns>
[Pure]
public EntityCoordinates AlignToGrid(EntityCoordinates coordinates)
{
// Check if the parent is already a grid.
if (_gridQuery.TryGetComponent(coordinates.EntityId, out var gridComponent))
{
var tile = CoordinatesToTile(coordinates.EntityId, gridComponent, coordinates);
return ToCenterCoordinates(coordinates.EntityId, tile, gridComponent);
}
// Check if mappos intersects a grid.
var mapPos = coordinates.ToMap(EntityManager, _transform);
if (_mapInternal.TryFindGridAt(mapPos, out var gridUid, out gridComponent))
{
var tile = CoordinatesToTile(gridUid, gridComponent, coordinates);
return ToCenterCoordinates(gridUid, tile, gridComponent);
}
// No grid so just return it.
return coordinates;
}
/// <summary>
/// Converts a tileRef to EntityCoordinates.
/// </summary>
[Pure]
public EntityCoordinates ToCoordinates(TileRef tileRef, MapGridComponent? gridComponent = null)
{
return ToCoordinates(tileRef.GridUid, tileRef.GridIndices, gridComponent);
}
/// <summary>
/// Converts a tileRef to EntityCoordinates.
/// </summary>
[Pure]
public EntityCoordinates ToCoordinates(EntityUid gridUid, Vector2i tile, MapGridComponent? gridComponent = null)
{
if (!_gridQuery.Resolve(gridUid, ref gridComponent))
{
return EntityCoordinates.Invalid;
}
return new EntityCoordinates(gridUid, tile * gridComponent.TileSize);
}
/// <summary>
/// Converts a tileRef to EntityCoordinates for the center of the tile.
/// </summary>
[Pure]
public EntityCoordinates ToCenterCoordinates(TileRef tileRef, MapGridComponent? gridComponent = null)
{
return ToCenterCoordinates(tileRef.GridUid, tileRef.GridIndices, gridComponent);
}
/// <summary>
/// Converts a tileRef to EntityCoordinates for the center of the tile.
/// </summary>
[Pure]
public EntityCoordinates ToCenterCoordinates(EntityUid gridUid, Vector2i tile, MapGridComponent? gridComponent = null)
{
if (!_gridQuery.Resolve(gridUid, ref gridComponent))
{
return EntityCoordinates.Invalid;
}
return new EntityCoordinates(gridUid, tile * gridComponent.TileSize + gridComponent.TileSizeHalfVector);
}
}

View File

@@ -189,7 +189,7 @@ public abstract partial class SharedMapSystem
if (xform.MapUid == null && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
Log.Error($"Grid {ToPrettyString(uid, meta)} was moved to nullspace! AAAAAAAAAAAAAAAAAAAAAAAAA! {Environment.StackTrace}");
DebugTools.Assert(!HasComp<MapComponent>(uid));
DebugTools.Assert(!_mapQuery.HasComponent(uid));
if (xform.ParentUid != xform.MapUid && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
{
@@ -454,7 +454,7 @@ public abstract partial class SharedMapSystem
if (xform.MapUid != null && xform.MapUid != uid)
_transform.SetParent(uid, xform, xform.MapUid.Value);
if (!HasComp<MapComponent>(uid))
if (!_mapQuery.HasComponent(uid))
{
var aabb = GetWorldAABB(uid, component);
@@ -506,7 +506,7 @@ public abstract partial class SharedMapSystem
private void AddGrid(EntityUid uid, MapGridComponent grid)
{
DebugTools.Assert(!EntityManager.HasComponent<MapComponent>(uid));
DebugTools.Assert(!_mapQuery.HasComponent(uid));
var aabb = GetWorldAABB(uid, grid);
if (!_xformQuery.TryGetComponent(uid, out var xform))
@@ -1254,7 +1254,7 @@ public abstract partial class SharedMapSystem
$"Grid {uid} is on map {mapId}, but coords are on map {posWorld.MapId}.",
nameof(posWorld));
if (!TryComp<MapGridComponent>(uid, out var grid))
if (!_gridQuery.TryGetComponent(uid, out var grid))
{
return new EntityCoordinates(MapManager.GetMapEntityId(posWorld.MapId), new Vector2(posWorld.X, posWorld.Y));
}

View File

@@ -21,12 +21,16 @@ namespace Robust.Shared.GameObjects
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private EntityQuery<MapComponent> _mapQuery;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<TransformComponent> _xformQuery;
public override void Initialize()
{
base.Initialize();
_mapQuery = GetEntityQuery<MapComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
InitializeMap();

View File

@@ -0,0 +1,84 @@
using System;
using System.Numerics;
using Robust.Shared.Map;
namespace Robust.Shared.GameObjects;
public abstract partial class SharedTransformSystem
{
/*
* Helper methods for working with EntityCoordinates / MapCoordinates.
* For grid methods see SharedMapSystem.Coordinates
*/
/// <summary>
/// Verifies that this set of coordinates can be currently resolved to a location.
/// </summary>
/// <returns><see langword="true" /> if this set of coordinates can be currently resolved to a location, otherwise <see langword="false" />.</returns>
public bool IsValid(EntityCoordinates coordinates)
{
var entity = coordinates.EntityId;
if (!entity.IsValid() || !Exists(entity))
return false;
if (!float.IsFinite(coordinates.Position.X) || !float.IsFinite(coordinates.Position.Y))
return false;
return true;
}
/// <summary>
/// Returns a new set of EntityCoordinates local to a new entity.
/// </summary>
/// <param name="entity">The entity that the new coordinates will be local to</param>
/// <returns>A new set of EntityCoordinates local to a new entity.</returns>
public EntityCoordinates WithEntityId(EntityCoordinates coordinates, EntityUid entity)
{
var mapPos = ToMapCoordinates(coordinates);
// You'd think this would throw like ToCoordinates does but TODO check that.
if (mapPos.MapId == MapId.Nullspace)
{
return new EntityCoordinates(entity, Vector2.Zero);
}
var xform = XformQuery.GetComponent(entity);
if (xform.MapID != mapPos.MapId)
{
return new EntityCoordinates(entity, Vector2.Zero);
}
var localPos = GetInvWorldMatrix(xform).Transform(mapPos.Position);
return new EntityCoordinates(entity, localPos);
}
/// <summary>
/// Converts entity-local coordinates into map terms.
/// </summary>
public MapCoordinates ToMapCoordinates(EntityCoordinates coordinates)
{
if (!IsValid(coordinates))
return MapCoordinates.Nullspace;
var xform = XformQuery.GetComponent(coordinates.EntityId);
var worldPos = GetWorldMatrix(xform).Transform(coordinates.Position);
return new MapCoordinates(worldPos, xform.MapID);
}
/// <summary>
/// Creates EntityCoordinates given an entity and some MapCoordinates.
/// </summary>
/// <exception cref="InvalidOperationException">If <see cref="entity"/> is not on the same map as the <see cref="coordinates"/>.</exception>
public EntityCoordinates ToCoordinates(EntityUid entity, MapCoordinates coordinates)
{
var xform = XformQuery.GetComponent(entity);
if (xform.MapID != coordinates.MapId)
throw new InvalidOperationException("Entity is not on the same map!");
var localPos = GetInvWorldMatrix(xform).Transform(coordinates.Position);
return new EntityCoordinates(entity, localPos);
}
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map.Components;
@@ -10,16 +8,6 @@ namespace Robust.Shared.Map
{
public static class CoordinatesExtensions
{
public static EntityCoordinates ToEntityCoordinates(this Vector2i vector, EntityUid gridId, IMapManager? mapManager = null)
{
IoCManager.Resolve(ref mapManager);
var grid = mapManager.GetGrid(gridId);
var tile = grid.TileSize;
return new EntityCoordinates(gridId, new Vector2(vector.X * tile, vector.Y * tile));
}
public static EntityCoordinates AlignWithClosestGridTile(this EntityCoordinates coords, float searchBoxSize = 1.5f, IEntityManager? entityManager = null, IMapManager? mapManager = null)
{
IoCManager.Resolve(ref entityManager, ref mapManager);

View File

@@ -185,6 +185,21 @@ public abstract partial class Joint : IEquatable<Joint>
// serializer.DataField(this, x => x.BodyA, "bodyA", EntityUid.Invalid);
// serializer.DataField(this, x => x.BodyB, "bodyB", Ent);
/// <summary>
/// Gets the other entity on this joint or throws if it's not related.
/// </summary>
public EntityUid GetOther(EntityUid uid)
{
if (BodyAUid == uid)
return BodyBUid;
if (BodyBUid == uid)
return BodyAUid;
// Should return EntityUid.Invalid but larger joints refactor first so we can actually log it properly here.
throw new ArgumentOutOfRangeException($"EntityUid {uid} unrelated to joint");
}
protected internal void Dirty(IEntityManager? entMan = null)
{
// TODO: move dirty & setter functions to a system.

View File

@@ -67,14 +67,7 @@ namespace Robust.Shared.Physics.Systems
UpdatesOutsidePrediction = true;
UpdatesAfter.Add(typeof(SharedTransformSystem));
_cfg.OnValueChanged(CVars.BroadphaseExpand, SetBroadphaseExpand, true);
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.BroadphaseExpand, SetBroadphaseExpand);
Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true);
}
private void SetBroadphaseExpand(float value) => _broadphaseExpand = value;

View File

@@ -70,6 +70,18 @@ public abstract partial class SharedJointSystem
if (_container.TryGetOuterContainer(uid, Transform(uid), out var container))
{
relay = container.Owner;
// Validate that the relay target is not being set to our own container.
foreach (var joint in component.Joints.Values)
{
var other = joint.GetOther(uid);
if (other == relay)
{
SetRelay(uid, null, component);
return;
}
}
}
SetRelay(uid, relay, component);

View File

@@ -199,38 +199,20 @@ public abstract partial class SharedPhysicsSystem
private void InitializeIsland()
{
_configManager.OnValueChanged(CVars.NetTickrate, SetTickRate, true);
_configManager.OnValueChanged(CVars.WarmStarting, SetWarmStarting, true);
_configManager.OnValueChanged(CVars.MaxLinearCorrection, SetMaxLinearCorrection, true);
_configManager.OnValueChanged(CVars.MaxAngularCorrection, SetMaxAngularCorrection, true);
_configManager.OnValueChanged(CVars.VelocityIterations, SetVelocityIterations, true);
_configManager.OnValueChanged(CVars.PositionIterations, SetPositionIterations, true);
_configManager.OnValueChanged(CVars.MaxLinVelocity, SetMaxLinearVelocity, true);
_configManager.OnValueChanged(CVars.MaxAngVelocity, SetMaxAngularVelocity, true);
_configManager.OnValueChanged(CVars.SleepAllowed, SetSleepAllowed, true);
_configManager.OnValueChanged(CVars.AngularSleepTolerance, SetAngularToleranceSqr, true);
_configManager.OnValueChanged(CVars.LinearSleepTolerance, SetLinearToleranceSqr, true);
_configManager.OnValueChanged(CVars.TimeToSleep, SetTimeToSleep, true);
_configManager.OnValueChanged(CVars.VelocityThreshold, SetVelocityThreshold, true);
_configManager.OnValueChanged(CVars.Baumgarte, SetBaumgarte, true);
}
private void ShutdownIsland()
{
_configManager.UnsubValueChanged(CVars.NetTickrate, SetTickRate);
_configManager.UnsubValueChanged(CVars.WarmStarting, SetWarmStarting);
_configManager.UnsubValueChanged(CVars.MaxLinearCorrection, SetMaxLinearCorrection);
_configManager.UnsubValueChanged(CVars.MaxAngularCorrection, SetMaxAngularCorrection);
_configManager.UnsubValueChanged(CVars.VelocityIterations, SetVelocityIterations);
_configManager.UnsubValueChanged(CVars.PositionIterations, SetPositionIterations);
_configManager.UnsubValueChanged(CVars.MaxLinVelocity, SetMaxLinearVelocity);
_configManager.UnsubValueChanged(CVars.MaxAngVelocity, SetMaxAngularVelocity);
_configManager.UnsubValueChanged(CVars.SleepAllowed, SetSleepAllowed);
_configManager.UnsubValueChanged(CVars.AngularSleepTolerance, SetAngularToleranceSqr);
_configManager.UnsubValueChanged(CVars.LinearSleepTolerance, SetLinearToleranceSqr);
_configManager.UnsubValueChanged(CVars.TimeToSleep, SetTimeToSleep);
_configManager.UnsubValueChanged(CVars.VelocityThreshold, SetVelocityThreshold);
_configManager.UnsubValueChanged(CVars.Baumgarte, SetBaumgarte);
Subs.CVar(_configManager, CVars.NetTickrate, SetTickRate, true);
Subs.CVar(_configManager, CVars.WarmStarting, SetWarmStarting, true);
Subs.CVar(_configManager, CVars.MaxLinearCorrection, SetMaxLinearCorrection, true);
Subs.CVar(_configManager, CVars.MaxAngularCorrection, SetMaxAngularCorrection, true);
Subs.CVar(_configManager, CVars.VelocityIterations, SetVelocityIterations, true);
Subs.CVar(_configManager, CVars.PositionIterations, SetPositionIterations, true);
Subs.CVar(_configManager, CVars.MaxLinVelocity, SetMaxLinearVelocity, true);
Subs.CVar(_configManager, CVars.MaxAngVelocity, SetMaxAngularVelocity, true);
Subs.CVar(_configManager, CVars.SleepAllowed, SetSleepAllowed, true);
Subs.CVar(_configManager, CVars.AngularSleepTolerance, SetAngularToleranceSqr, true);
Subs.CVar(_configManager, CVars.LinearSleepTolerance, SetLinearToleranceSqr, true);
Subs.CVar(_configManager, CVars.TimeToSleep, SetTimeToSleep, true);
Subs.CVar(_configManager, CVars.VelocityThreshold, SetVelocityThreshold, true);
Subs.CVar(_configManager, CVars.Baumgarte, SetBaumgarte, true);
}
private void SetWarmStarting(bool value) => _warmStarting = value;

View File

@@ -92,9 +92,9 @@ namespace Robust.Shared.Physics.Systems
InitializeIsland();
InitializeContacts();
_configManager.OnValueChanged(CVars.AutoClearForces, OnAutoClearChange);
_configManager.OnValueChanged(CVars.NetTickrate, UpdateSubsteps, true);
_configManager.OnValueChanged(CVars.TargetMinimumTickrate, UpdateSubsteps, true);
Subs.CVar(_configManager, CVars.AutoClearForces, OnAutoClearChange);
Subs.CVar(_configManager, CVars.NetTickrate, UpdateSubsteps, true);
Subs.CVar(_configManager, CVars.TargetMinimumTickrate, UpdateSubsteps, true);
}
private void OnPhysicsShutdown(EntityUid uid, PhysicsComponent component, ComponentShutdown args)
@@ -250,8 +250,6 @@ namespace Robust.Shared.Physics.Systems
base.Shutdown();
ShutdownContacts();
ShutdownIsland();
_configManager.UnsubValueChanged(CVars.AutoClearForces, OnAutoClearChange);
}
private void UpdateMapAwakeState(EntityUid uid, PhysicsComponent body)

View File

@@ -34,6 +34,11 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
// date format for default replay names. Like the sortable template, but without colons.
public const string DefaultReplayNameFormat = "yyyy-MM-dd_HH-mm-ss";
// Kinda arbitrary but (after multiplying by 1024 cuz it's kB)
// needs to be less than (max array size) / 2.
// I don't think anybody's gonna write 256 MB of chunk at once yeah?
private const int MaxTickBatchSize = 256 * 1024;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly INetConfigurationManager NetConf = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
@@ -50,8 +55,8 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
private List<object> _queuedMessages = new();
// Config variables.
private int _maxCompressedSize;
private int _maxUncompressedSize;
private long _maxCompressedSize;
private long _maxUncompressedSize;
private int _tickBatchSize;
private bool _enabled;
@@ -63,9 +68,9 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
{
_sawmill = _logManager.GetSawmill("replay");
NetConf.OnValueChanged(CVars.ReplayMaxCompressedSize, (v) => _maxCompressedSize = v * 1024, true);
NetConf.OnValueChanged(CVars.ReplayMaxUncompressedSize, (v) => _maxUncompressedSize = v * 1024, true);
NetConf.OnValueChanged(CVars.ReplayTickBatchSize, (v) => _tickBatchSize = v * 1024, true);
NetConf.OnValueChanged(CVars.ReplayMaxCompressedSize, (v) => _maxCompressedSize = SaturatingMultiplyKb(v), true);
NetConf.OnValueChanged(CVars.ReplayMaxUncompressedSize, (v) => _maxUncompressedSize = SaturatingMultiplyKb(v), true);
NetConf.OnValueChanged(CVars.ReplayTickBatchSize, (v) => _tickBatchSize = Math.Min(v, MaxTickBatchSize) * 1024, true);
NetConf.OnValueChanged(CVars.NetPvsCompressLevel, OnCompressionChanged);
}
@@ -450,6 +455,18 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
return new ReplayRecordingStats(time, tick, size, altSize);
}
private static long SaturatingMultiplyKb(long kb)
{
var result = kb * 1024;
if (result < kb)
{
// Overflow
return long.MaxValue;
}
return result;
}
/// <summary>
/// Contains all state related to an active recording.
/// </summary>

View File

@@ -0,0 +1,65 @@
using System;
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations;
/// <summary>
/// Implements serialization for <see cref="DateTime"/>.
/// </summary>
/// <remarks>
/// Serialization is implemented with <see cref="DateTimeStyles.RoundtripKind"/> and the "o" format specifier.
/// </remarks>
[TypeSerializer]
public sealed class DateTimeSerializer : ITypeSerializer<DateTime, ValueDataNode>, ITypeCopyCreator<DateTime>
{
public ValidationNode Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
{
return DateTime.TryParse(node.Value, null, DateTimeStyles.RoundtripKind, out _)
? new ValidatedValueNode(node)
: new ErrorNode(node, "Failed parsing DateTime");
}
public DateTime Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<DateTime>? instanceProvider = null)
{
return DateTime.Parse(node.Value, null, DateTimeStyles.RoundtripKind);
}
public DataNode Write(
ISerializationManager serializationManager,
DateTime value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
return new ValueDataNode(value.ToString("o"));
}
[MustUseReturnValue]
public DateTime CreateCopy(
ISerializationManager serializationManager,
DateTime source,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null)
{
return source;
}
}

View File

@@ -759,8 +759,9 @@ namespace Robust.UnitTesting
public sealed class ClientIntegrationInstance : IntegrationInstance
{
[Obsolete("Use Session instead")]
public LocalPlayer? Player => ((IPlayerManager) PlayerMan).LocalPlayer;
public ICommonSession? Session => Player?.Session;
public ICommonSession? Session => ((IPlayerManager) PlayerMan).LocalSession;
public NetUserId? User => Session?.UserId;
public EntityUid? AttachedEntity => Session?.AttachedEntity;

View File

@@ -0,0 +1,115 @@
using System;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using System.Linq;
using System.Threading.Tasks;
using static Robust.UnitTesting.Shared.GameState.ExampleAutogeneratedComponent;
namespace Robust.UnitTesting.Shared.GameState;
/// <summary>
/// This is a test of test engine <see cref="RobustIntegrationTest"/>. Not a test of game engine.
/// </summary>
public sealed partial class NoSharedReferencesTest : RobustIntegrationTest
{
/// <summary>
/// The test performs a basic check to ensure that there is no issue with server's object references leaking to client.
/// It accomplishments this by testing two things: 1) That the reference object on both sides is not the same; and
/// 2) That the client-side copy of server's component state (used for prediction resetting) is not the same.
/// </summary>
[Test]
public async Task ReferencesAreNotShared()
{
var serverOpts = new ServerIntegrationOptions { Pool = false };
var clientOpts = new ClientIntegrationOptions { Pool = false };
var server = StartServer(serverOpts);
var client = StartClient(clientOpts);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var netMan = client.ResolveDependency<IClientNetManager>();
var clientGameStateManager = client.ResolveDependency<IClientGameStateManager>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
// Set up map.
EntityUid map = default;
await server.WaitPost(() =>
{
var mapId = server.MapMan.CreateMap();
map = server.MapMan.GetMapEntityId(mapId);
});
await RunTicks();
EntityUid sPlayer = default;
EntityUid cPlayer = default;
ExampleObject serverObject = default!;
var sEntMan = server.EntMan;
// Set up test entity (player).
await server.WaitPost(() =>
{
sPlayer = sEntMan.Spawn();
serverObject = new ExampleObject(5);
var comp = new ExampleAutogeneratedComponent { ReferenceObject = serverObject };
sEntMan.AddComponent(sPlayer, comp);
var session = server.PlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, sPlayer);
server.PlayerMan.JoinGame(session);
});
// Let Client sync state with Server
await RunTicks();
// Assert that Client's object and client-side server state objects are different to Server's object
Assert.Multiple(() =>
{
// Player attached assertions
var cEntMan = client.EntMan;
cPlayer = cEntMan.GetEntity(server.EntMan.GetNetEntity(sPlayer));
Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer));
Assert.That(cEntMan.EntityExists(cPlayer));
// Assert client and server have different objects of same values
Assert.That(cEntMan.TryGetComponent(cPlayer, out ExampleAutogeneratedComponent? comp));
var clientObject = comp?.ReferenceObject;
Assert.That(clientObject, Is.EqualTo(serverObject));
Assert.That(ReferenceEquals(clientObject, serverObject), Is.False);
// Assert that client-side dictionary of server component state also isn't contaminated by server references
var componentStates = clientGameStateManager.GetFullRep()[cEntMan.GetNetEntity(cPlayer)];
var clientLastTickStateObject = ((ExampleAutogeneratedComponent_AutoState)componentStates.First(x => x.Value is ExampleAutogeneratedComponent_AutoState).Value).ReferenceObject;
Assert.That(clientLastTickStateObject, Is.Not.Null);
Assert.That(ReferenceEquals(clientLastTickStateObject, serverObject), Is.False);
});
// wait for errors.
await RunTicks();
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
}
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExampleAutogeneratedComponent : Component
{
[AutoNetworkedField]
public ExampleObject ReferenceObject;
[Serializable, NetSerializable]
public sealed record ExampleObject(int Value);
}

View File

@@ -73,8 +73,8 @@ public sealed class DisconnectTest : RobustIntegrationTest
session = sPlayerMan.Sessions.Single();
Assert.That(session.Status, Is.EqualTo(SessionStatus.Connected));
Assert.That(session.UserId, Is.EqualTo(cPlayerMan!.LocalPlayer?.UserId));
Assert.That(cPlayerMan.LocalPlayer, Is.Not.Null);
Assert.That(session.UserId, Is.EqualTo(cPlayerMan.LocalUser));
Assert.That(cPlayerMan.LocalSession, Is.Not.Null);
}
void AssertDisconnected()

View File

@@ -0,0 +1,31 @@
using System;
using System.Globalization;
using NUnit.Framework;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers;
[TestFixture]
[TestOf(typeof(DateTimeSerializer))]
internal sealed class DateTimeSerializerTest : SerializationTest
{
[Test]
public void WriteTest()
{
var dateTime = DateTime.UtcNow;
var result = Serialization.WriteValueAs<ValueDataNode>(dateTime);
var parsed = DateTime.Parse(result.Value, null, DateTimeStyles.RoundtripKind);
Assert.That(parsed, Is.EqualTo(dateTime));
}
[Test]
public void ReadTest()
{
var result = Serialization.Read<DateTime>(new ValueDataNode("2020-07-10 15:00:00.000"));
Assert.That(result, Is.EqualTo(new DateTime(2020, 07, 10, 15, 0, 0)));
}
}