Compare commits

...

46 Commits

Author SHA1 Message Date
Pieter-Jan Briers
abea3024b4 Raise event when entity paused state changes. 2021-07-02 17:19:34 +02:00
Pieter-Jan Briers
07dafeb6cd Can now rotate anchored entities 2021-07-02 01:16:25 +02:00
Pieter-Jan Briers
a726d42ae3 Fix ResetPredictedEntities applying data early in some cases.
FullState was getting updated before ResetPredictedEntities ran instead of after. So ResetPredictedEntities was applying data to stuff like containers that depends on entities that hadn't been made yet.
2021-07-01 12:16:18 +02:00
metalgearsloth
d02d186a2f Fix light pop-in 2 (#1798)
* Fix light pop-in

Doh

* Uniqueness

* Refactor to be less bad

* Better perf

* Fix perf problems

* Hide debug commands under preprocessor

Can't imagine anyone using this during live-game.

* CVars
2021-06-29 22:29:47 +10:00
Vera Aguilera Puerto
a6be66949d Fix segfault when MIDI renderer is disposed.
Apparently? You need to dispose of the sequencer BEFORE the synth or a funny segfault will occur... T-Thanks libfluidsynth.
Also unregisters the sequencer clients before disposing, just for cleanliness.
2021-06-28 10:59:20 +02:00
Galactic Chimp
0dfd3b7443 #1462 extracted MapGrid's HasGravity property to separate Content component (#1835) 2021-06-27 15:01:58 +10:00
metalgearsloth
6f90e9a76e MapGridSystem for grid initialization (#1838)
* MapGridSystem for grid initialization

Doesn't seem to be a clean way to hook into grids being initialized (can't duplicate directed bus events and a few systems need to EnsureComponent) hence the event.

* Address reviews
2021-06-27 14:59:38 +10:00
Galactic Chimp
9246b88560 #1607 deprecate IEntityQuery (#1837)
* #1607 partial progress

* #1607 almost done

* #1607 small test tweak

* #1607 PR suggestions

* #1607 PR suggestions

* #1607 indentation tweaks
2021-06-27 13:40:00 +10:00
Vera Aguilera Puerto
61af9db362 Fix bug where anchored entities would be spawned at 0,0 always.
This is because the parent was attached first, and then the Entity Coordinates are set, but position doesn't change because the parent is the same and the entity is anchored.
NOTE: I only fixed the EntityCoordinates overload. Unsure if this is even a problem with MapCoordinates.
2021-06-24 21:41:39 +02:00
Vera Aguilera Puerto
5aa950e7f7 GetTilesIntersecting yield returns instead of creating, populating and returning a list of tiles. 2021-06-23 12:40:31 +02:00
Pieter-Jan Briers
b8903af52f Make public DrawingHandleScreen.DrawString helper method.
This method was copy pasted into 4 separate overlays already.
2021-06-23 01:42:47 +02:00
Pieter-Jan Briers
5b35c4e82b Fix disposed exception on client shutdown (I think?). 2021-06-23 01:26:16 +02:00
metalgearsloth
06db80780c Filter contacts for joints properly (#1836)
If you're colliding with something you obtain a joint with it should be filtered correctly now.
2021-06-21 02:27:07 +02:00
Acruid
e5fd4f5700 DependencyCollection Children (#1833)
Adds a new register function for registering a simple Type without an interface.
2021-06-21 02:15:23 +02:00
Galactic Chimp
e1a199e060 Removed old Loc.GetString() use instances (#1814)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-06-21 02:12:53 +02:00
Pieter-Jan Briers
22ac34c7a1 Are you for real, .NET CLI. 2021-06-21 02:01:55 +02:00
Pieter-Jan Briers
04fa6901b1 Fix ordering of command arguments.
Dumb CLI.
2021-06-21 01:57:09 +02:00
Pieter-Jan Briers
8f7d4211e3 Try server GC for content integration tests. 2021-06-21 01:53:40 +02:00
Pieter-Jan Briers
1147d6fd9a Do not cache injector delegates for entity system creation. 2021-06-21 01:44:36 +02:00
Pieter-Jan Briers
e4449c4901 Cache data definitions statically.
Significantly improves integration test times.
2021-06-21 01:31:13 +02:00
Pieter-Jan Briers
dc8963faa5 Use a no-op mapped string serializer in integration tests.
Serialization never happens in integration tests, so...
2021-06-21 01:29:51 +02:00
Pieter-Jan Briers
f9cf9a8fd4 Single-threaded integration tests experiment.
This didn't really seem to improve performance, so it's not enabled, but I'm not deleting the code.
2021-06-21 00:38:37 +02:00
Pieter-Jan Briers
393bdc04bc ItemList doesn't render things outside its clip region. 2021-06-20 19:48:24 +02:00
Pieter-Jan Briers
db4d787b49 use a struct enumerator for EventTables GetReferences. 2021-06-20 18:50:43 +02:00
Pieter-Jan Briers
cd802d6a66 Fix more nullrefs due to dummy sprite IEntity. 2021-06-20 18:50:29 +02:00
Pieter-Jan Briers
be2281a5b3 Remove glue code to allow components to subscribe to EventBus.
No. Just no.
2021-06-20 18:10:21 +02:00
Pieter-Jan Briers
e717396b33 MapManager uses directed remove event instead of hooking global ComponentRemoved event. 2021-06-20 18:07:24 +02:00
Pieter-Jan Briers
db0e49f5dd Defer SpriteComponent.IsInert updates.
Prevents O(n^2) behavior in many cases.
2021-06-20 17:57:45 +02:00
Pieter-Jan Briers
d3b94aa6af Fix name generator xaml file locating logic.
It would do .EndsWidth(), so DropDownDebugConsole and DebugConsole would get confused.

It now properly checks for path separators and also will report an error if there are two candidate XAML files.

Also, fixed the AXN0003 diagnostic having the wrong name.
2021-06-20 15:48:48 +02:00
Acruid
713f702064 Engine Entity Anchoring (#1829)
* Added unit tests for the new anchor system design.

* Amend ME!

* SnapGridComponent now anchors the entity when added or removed.
TransformComponent.Anchored properly replicates it's state to clients.

* Converted all SnapGridPositionChangedEvent subscriptions to AnchorStateChangedEvent.
Removed SnapGridPositionChangedEvent.
Obsoleted SnapGridComponent.

* Allows setting Transform.Anchored in prototypes.

* Changing tile to empty under anchored ents unanchors them.

* More unit testing.

* Migrated transform gamestate tests to RobustServerSimulation.

* Fixed nasty off-by-one error when sending a full server state.

* Adds lifetime stages to Component.

* More test fixing.

* Review changes.
2021-06-19 17:26:18 -07:00
Pieter-Jan Briers
d0b6a9b28c Replace usages of OperatingSystem.IsOSPlatform with OperatingSystem.IsXXX(). 2021-06-19 03:14:39 +02:00
Pieter-Jan Briers
4003781a1b Manually load libEGL.dll to fix ANGLE in dev builds. 2021-06-19 03:05:15 +02:00
Acruid
40586a8f0e Removes all engine component events. (#1832) 2021-06-18 10:44:21 +02:00
metalgearsloth
f4d427f5c5 Pre-reqs for multithreaded physics (#1784)
* Tanks off guard

* Medic

* Diamondback

* Gaming

* Gamingo

* Finalise

* Doh

* Bullet licence

* Marginal cleanup

* Physics licences

* Apply reviews
2021-06-17 16:01:41 +10:00
metalgearsloth
ce2a4282f3 Bandaid GridTileLookup containers (#1789)
* Bandaid GridTileLookup containers

I'm hoping tile entities deprecates this system but this should fix our problems for now.

* Fix stupid
2021-06-16 16:18:23 +02:00
Ygg01
0cc78c1402 Add missing types to type conversion (#1828) 2021-06-15 10:22:24 +02:00
Pieter-Jan Briers
fa0d4da6d1 Implement subscription ordering into EventBus. (#1823)
* Implement subscription ordering into EventBus.

* Topological helpers, rest of code uses shared topological sort, fix bugs.

* Fix tests.

Didn't realize that multi-subscriptions are allowed on input handlers like that??

* Improve and use topological sort helpers more.
2021-06-14 21:34:30 +02:00
Ygg01
33334d6f5c Upgrade Linguini to latest (prevents IDE issue with SG) (#1826) 2021-06-13 22:16:12 +02:00
ShadowCommander
55aba93faf Revert "Update Linguini"
This reverts commit 4f49296a94.
2021-06-13 08:38:01 -07:00
ShadowCommander
4f49296a94 Update Linguini 2021-06-13 08:34:57 -07:00
Vera Aguilera Puerto
65357ee8da ComponentHandleState event (#1778) 2021-06-12 15:50:27 +02:00
Vera Aguilera Puerto
bfcad4001b Adds public methods to ActorSystem. (#1824) 2021-06-12 15:50:01 +02:00
DrSmugleaf
5a3000febb Add directed events for player attach/detach (#1815) 2021-06-12 01:45:43 +02:00
DrSmugleaf
2d236670ab Add SetAndDirtyIfChanged extension method (#1816) 2021-06-12 01:44:28 +02:00
20kdc
796f1480a8 Add more tolerance for if player sessions are connected to dead entities (#1818)
* Add more tolerance for if some calamity causes a player session to be connected to a dead entity

* Fix build issues w/ previous commit (oops)
2021-06-10 09:29:25 +02:00
Ygg01
a8ee7be844 Add linguini loc (#1760)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2021-06-09 19:44:18 +02:00
143 changed files with 3994 additions and 2104 deletions

View File

@@ -38,4 +38,4 @@ jobs:
- name: Content.Tests
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
- name: Content.IntegrationTests
run: dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n

3
.gitmodules vendored
View File

@@ -13,3 +13,6 @@
[submodule "ManagedHttpListener"]
path = ManagedHttpListener
url = https://github.com/space-wizards/ManagedHttpListener.git
[submodule "Linguini"]
path = Linguini
url = https://github.com/space-wizards/Linguini

1
Linguini Submodule

Submodule Linguini added at 62b0e75b91

View File

@@ -23,12 +23,12 @@ namespace OpenToolkit.GraphicsLibraryFramework
return IntPtr.Zero;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
if (OperatingSystem.IsLinux())
{
return NativeLibrary.Load("libglfw.so.3", assembly, path);
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
if (OperatingSystem.IsMacOS())
{
return NativeLibrary.Load("libglfw.3.dylib", assembly, path);
}

View File

@@ -23,6 +23,48 @@
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- name: Box2D
license:
MIT License
Copyright (c) 2019 Erin Catto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- name: Bullet Physics SDK
license:
The files in this repository are licensed under the zlib license, except for the files under 'Extras' and examples/ThirdPartyLibs.
Bullet Continuous Collision Detection and Physics Library
http://bulletphysics.org
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it freely,
subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
- name: Castle Core
license: |
Copyright 2004-2016 Castle Project - http://www.castleproject.org/
@@ -317,6 +359,43 @@
See the License for the specific language governing permissions and
limitations under the License.
- name: Farseer Physics Engine
license:
Microsoft Permissive License (Ms-PL)
This license governs use of the accompanying software.
If you use the software, you accept this license.
If you do not accept the license, do not use the software.
1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.
2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3,
each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution,
prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3,
each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to
make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or
derivative works of the contribution in the software.
3. Conditions and Limitations
(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software,
your patent license from such contributor to the software ends automatically.
(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark,
and attribution notices that are present in the software.
(D) If you distribute any portion of the software in source code form, you may do so only under this license by
including a complete copy of this license with your distribution. If you distribute any portion of the software in
compiled or object code form, you may only do so under a license that complies with this license.
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties,
guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change.
To the extent permitted under your local laws, the contributors exclude the implied warranties of
merchantability, fitness for a particular purpose and non-infringement.
- name: Mono.Cecil
license: |
Copyright (c) 2008 - 2015 Jb Evain

View File

@@ -1 +0,0 @@
console-line-edit-placeholder = Command Here

View File

@@ -0,0 +1,7 @@
## EntitySpawnWindow
entity-spawn-window-title = Entity Spawn Panel
entity-spawn-window-search-bar-placeholder = search
entity-spawn-window-clear-button = Clear
entity-spawn-window-erase-button-text = Erase Mode
entity-spawn-window-override-menu-tooltip = Override placement

View File

@@ -0,0 +1 @@
tab-container-not-tab-title-provided = No title

View File

@@ -0,0 +1,11 @@
## ViewVariablesInstanceEntity
view-variable-instance-entity-server-components-add-component-button-placeholder = Add Component
view-variable-instance-entity-client-variables-tab-title = Client Variables
view-variable-instance-entity-client-components-tab-title = Client Components
view-variable-instance-entity-server-variables-tab-title = Server Variables
view-variable-instance-entity-server-components-tab-title = Server Components
view-variable-instance-entity-client-components-search-bar-placeholder = Search
view-variable-instance-entity-server-components-search-bar-placeholder = Search
view-variable-instance-entity-add-window-server-components = Add Component [S]
view-variable-instance-entity-add-window-client-components = Add Component [C]

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
@@ -147,9 +148,10 @@ namespace {nameSpace}
foreach (var typeSymbol in symbols)
{
var xamlFileName = $"{typeSymbol.Name}.xaml";
var relevantXamlFile = context.AdditionalFiles.FirstOrDefault(t => t.Path.EndsWith(xamlFileName));
var xamlFileNameSep = $"{Path.DirectorySeparatorChar}{xamlFileName}";
var relevantXamlFiles = context.AdditionalFiles.Where(t => t.Path.EndsWith(xamlFileNameSep)).ToArray();
if (relevantXamlFile == null)
if (relevantXamlFiles.Length == 0)
{
context.ReportDiagnostic(
Diagnostic.Create(
@@ -165,13 +167,28 @@ namespace {nameSpace}
continue;
}
var txt = relevantXamlFile.GetText()?.ToString();
if (txt == null)
if (relevantXamlFiles.Length > 1)
{
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"RXN0002",
$"Found multiple candidate XAML files for {typeSymbol}",
$"Multiple files exist with name {xamlFileName}",
"Usage",
DiagnosticSeverity.Error,
true),
typeSymbol.Locations[0]));
continue;
}
var txt = relevantXamlFiles[0].GetText()?.ToString();
if (txt == null)
{
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"RXN0004",
$"Unexpected empty Xaml-File was found at {xamlFileName}",
"Expected Content due to a Class with the same name being annotated with [GenerateTypedNameReferences].",
"Usage",
@@ -192,7 +209,7 @@ namespace {nameSpace}
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"AXN0003",
"RXN0003",
"Unhandled exception occured while generating typed Name references.",
$"Unhandled exception occured while generating typed Name references: {e}",
"Usage",

View File

@@ -225,7 +225,7 @@ namespace Robust.Client.Audio.Midi
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
if (OperatingSystem.IsLinux())
{
foreach (var filepath in LinuxSoundfonts)
{
@@ -243,12 +243,12 @@ namespace Robust.Client.Audio.Midi
break;
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
else if (OperatingSystem.IsMacOS())
{
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
renderer.LoadSoundfont(OsxSoundfont, true);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
renderer.LoadSoundfont(WindowsSoundfont, true);

View File

@@ -628,10 +628,15 @@ namespace Robust.Client.Audio.Midi
void IMidiRenderer.InternalDispose()
{
Source?.Dispose();
_driver?.Dispose();
// Do NOT dispose of the sequencer after the synth or it'll cause a segfault for some fucking reason.
_sequencer?.UnregisterClient(_debugRegister);
_sequencer?.UnregisterClient(_synthRegister);
_sequencer?.Dispose();
_synth?.Dispose();
_player?.Dispose();
_driver?.Dispose();
_sequencer?.Dispose();
}
}
}

View File

@@ -0,0 +1,19 @@
#if DEBUG
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
namespace Robust.Client.Console.Commands
{
internal sealed class LightDebugCommand : IConsoleCommand
{
public string Command => "lightbb";
public string Description => "Toggles whether to show light bounding boxes";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugLightTreeSystem>().Enabled ^= true;
}
}
}
#endif

View File

@@ -1,6 +1,5 @@
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Robust.Client.Console.Commands
{
@@ -41,7 +40,7 @@ namespace Robust.Client.Console.Commands
var mgr = IoCManager.Resolve<IScriptClient>();
if (!mgr.CanScript)
{
shell.WriteError(Loc.GetString("You do not have server side scripting permission."));
shell.WriteError("You do not have server side scripting permission.");
return;
}

View File

@@ -1,5 +1,4 @@
#if CLIENT_SCRIPTING
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -14,7 +13,6 @@ using Microsoft.CodeAnalysis.Text;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Reflection;
using Robust.Shared.Scripting;
@@ -43,14 +41,14 @@ namespace Robust.Client.Console
public ScriptConsoleClient()
{
Title = Loc.GetString("Robust C# Interactive (CLIENT)");
Title = "Robust C# Interactive (CLIENT)";
ScriptInstanceShared.InitDummy();
_globals = new ScriptGlobalsImpl(this);
IoCManager.InjectDependencies(this);
OutputPanel.AddText(Loc.GetString(@"Robust C# interactive console (CLIENT)."));
OutputPanel.AddText("Robust C# interactive console (CLIENT).");
OutputPanel.AddText(">");
}

View File

@@ -8,8 +8,6 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Reflection;
using Robust.Shared.Scripting;
using Robust.Shared.Timing;
@@ -31,7 +29,7 @@ namespace Robust.Client.Console
ScriptInstanceShared.InitDummy();
Title = Loc.GetString("Watch Window");
Title = "Watch Window";
var mainVBox = new VBoxContainer
{
@@ -49,11 +47,11 @@ namespace Robust.Client.Console
(_addWatchEdit = new HistoryLineEdit
{
HorizontalExpand = true,
PlaceHolder = Loc.GetString("Add watch (C# interactive)")
PlaceHolder = "Add watch (C# interactive)"
}),
(_addWatchButton = new Button
{
Text = Loc.GetString("Add")
Text = "Add"
})
}
}
@@ -118,7 +116,7 @@ namespace Robust.Client.Console
}),
(delButton = new Button
{
Text = Loc.GetString("Remove")
Text = "Remove"
}),
}
});
@@ -178,7 +176,7 @@ namespace Robust.Client.Console
ClipText = true,
HorizontalExpand = true
},
(delButton = new Button {Text = Loc.GetString("Remove")})
(delButton = new Button {Text = "Remove"})
}
});

View File

@@ -130,17 +130,17 @@ namespace Robust.Client.Debugging
{
if (body != _hoverBodies[0])
{
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), "------");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), "------");
row++;
}
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
row++;
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Mask: {Convert.ToString(body.CollisionMask, 2)}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Mask: {Convert.ToString(body.CollisionMask, 2)}");
row++;
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Enabled: {body.CanCollide}, Hard: {body.Hard}, Anchored: {(body).BodyType == BodyType.Static}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Enabled: {body.CanCollide}, Hard: {body.Hard}, Anchored: {(body).BodyType == BodyType.Static}");
row++;
}
@@ -199,17 +199,6 @@ namespace Robust.Client.Debugging
}
}
private static void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str)
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
baseLine += new Vector2(advance, 0);
}
}
private class PhysDrawingAdapter : DebugDrawingHandle
{
private readonly DrawingHandleWorld _handle;

View File

@@ -82,7 +82,7 @@ namespace Robust.Client
_commandLineArgs = args;
}
private bool StartupContinue(DisplayMode displayMode)
internal bool StartupContinue(DisplayMode displayMode)
{
_clyde.InitializePostWindowing();
_clyde.SetWindowTitle(Options.DefaultWindowTitle);
@@ -196,7 +196,7 @@ namespace Robust.Client
return true;
}
private bool StartupSystemSplash(Func<ILogHandler>? logHandlerFactory)
internal bool StartupSystemSplash(Func<ILogHandler>? logHandlerFactory)
{
ReadInitialLaunchState();
@@ -471,7 +471,7 @@ namespace Robust.Client
Clyde,
}
private void Cleanup()
internal void Cleanup()
{
_networkManager.Shutdown("Client shutting down");
_midiManager.Shutdown();

View File

@@ -104,7 +104,7 @@ namespace Robust.Client.GameObjects
_appearanceDirty = false;
}
public override void Initialize()
protected override void Initialize()
{
base.Initialize();

View File

@@ -116,7 +116,7 @@ namespace Robust.Client.GameObjects
public MapCoordinates? Position => _eye?.Position;
/// <inheritdoc />
public override void Initialize()
protected override void Initialize()
{
base.Initialize();
@@ -156,7 +156,7 @@ namespace Robust.Client.GameObjects
VisibilityMask = state.VisibilityMask;
}
public override void OnRemove()
protected override void OnRemove()
{
base.OnRemove();

View File

@@ -34,11 +34,11 @@ namespace Robust.Client.GameObjects
if (Owner.Transform.Anchored)
{
SnapGridOnPositionChanged();
AnchorStateChanged();
}
}
public void SnapGridOnPositionChanged()
public void AnchorStateChanged()
{
SendDirty();

View File

@@ -1,10 +1,12 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Animations;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
@@ -135,30 +137,13 @@ namespace Robust.Client.GameObjects
public bool VisibleNested
{
get => _visibleNested;
set
{
if (_visibleNested == value) return;
_visibleNested = value;
if (value)
{
if (Owner.Transform.Parent == null) return;
_lightOnParent = true;
}
else
{
if (!_lightOnParent) return;
_lightOnParent = false;
}
}
set => _visibleNested = value;
}
[DataField("radius")]
private float _radius = 5f;
[DataField("nestedvisible")]
private bool _visibleNested = true;
private bool _lightOnParent;
[DataField("color")]
private Color _color = Color.White;
[DataField("offset")]
@@ -185,6 +170,8 @@ namespace Robust.Client.GameObjects
get => _radius;
set
{
if (MathHelper.CloseTo(value, _radius)) return;
_radius = MathF.Max(value, 0.01f); // setting radius to 0 causes exceptions, so just use a value close enough to zero that it's unnoticeable.
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedEvent(this));
}
@@ -198,17 +185,8 @@ namespace Robust.Client.GameObjects
Mask = null;
}
/// <summary>
/// What MapId we are intersecting for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
/// <summary>
/// What grids we're on for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal List<GridId> IntersectingGrids = new();
internal RenderingTreeComponent? RenderTree { get; set; }
void ISerializationHooks.AfterDeserialization()
{
@@ -218,42 +196,13 @@ namespace Robust.Client.GameObjects
}
}
public override void Initialize()
protected override void Initialize()
{
base.Initialize();
UpdateMask();
}
/// <inheritdoc />
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
if (message is ParentChangedMessage msg)
{
HandleTransformParentChanged(msg);
}
}
private void HandleTransformParentChanged(ParentChangedMessage obj)
{
// TODO: this does not work for things nested multiply layers deep.
if (!VisibleNested)
{
return;
}
if (obj.NewParent != null && obj.NewParent.IsValid())
{
_lightOnParent = true;
}
else
{
_lightOnParent = false;
}
}
public override void OnRemove()
protected override void OnRemove()
{
base.OnRemove();

View File

@@ -46,6 +46,7 @@ namespace Robust.Client.GameObjects
{
if (_visible == value) return;
_visible = value;
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new SpriteUpdateEvent());
}
}
@@ -130,17 +131,8 @@ namespace Robust.Client.GameObjects
[DataField("directional")]
private bool _directional = true;
/// <summary>
/// What MapId we are intersecting for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
/// <summary>
/// What grids we're on for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal List<GridId> IntersectingGrids { get; } = new();
internal RenderingTreeComponent? RenderTree { get; set; } = null;
[DataField("layerDatums")]
private List<PrototypeLayerData> LayerDatums
@@ -271,7 +263,7 @@ namespace Robust.Client.GameObjects
}
_layerMapShared = true;
UpdateIsInert();
QueueUpdateIsInert();
}
}
@@ -336,6 +328,8 @@ namespace Robust.Client.GameObjects
[ViewVariables(VVAccess.ReadWrite)]
public bool TreeUpdateQueued { get; set; }
[ViewVariables(VVAccess.ReadWrite)] private bool _inertUpdateQueued;
[ViewVariables(VVAccess.ReadWrite)]
public ShaderInstance? PostShader { get; set; }
@@ -636,7 +630,7 @@ namespace Robust.Client.GameObjects
index = Layers.Count - 1;
}
UpdateIsInert();
QueueUpdateIsInert();
return index;
}
@@ -663,7 +657,7 @@ namespace Robust.Client.GameObjects
}
}
UpdateIsInert();
QueueUpdateIsInert();
}
public void RemoveLayer(object layerKey)
@@ -1514,8 +1508,20 @@ namespace Robust.Client.GameObjects
};
}
private void UpdateIsInert()
private void QueueUpdateIsInert()
{
if (_inertUpdateQueued)
return;
_inertUpdateQueued = true;
// Yes that null check is valid because of that stupid fucking dummy IEntity.
// Who thought that was a good idea.
Owner?.EntityManager?.EventBus?.RaiseEvent(EventSource.Local, new SpriteUpdateInertEvent {Sprite = this});
}
internal void DoUpdateIsInert()
{
_inertUpdateQueued = false;
IsInert = true;
foreach (var layer in Layers)
@@ -1865,14 +1871,14 @@ namespace Robust.Client.GameObjects
{
AutoAnimated = value;
_parent.UpdateIsInert();
_parent.QueueUpdateIsInert();
}
public void SetVisible(bool value)
{
Visible = value;
_parent.UpdateIsInert();
_parent.QueueUpdateIsInert();
}
public void SetRsi(RSI? rsi)
@@ -1904,7 +1910,7 @@ namespace Robust.Client.GameObjects
}
}
_parent.UpdateIsInert();
_parent.QueueUpdateIsInert();
}
public void SetState(RSI.StateId stateId)
@@ -1936,7 +1942,7 @@ namespace Robust.Client.GameObjects
AnimationTime = 0;
AnimationTimeLeft = state.GetDelay(0);
_parent.UpdateIsInert();
_parent.QueueUpdateIsInert();
}
public void SetTexture(Texture? texture)
@@ -1944,7 +1950,7 @@ namespace Robust.Client.GameObjects
State = default;
Texture = texture;
_parent.UpdateIsInert();
_parent.QueueUpdateIsInert();
}
public void SetOffset(Vector2 offset)
@@ -2235,4 +2241,9 @@ namespace Robust.Client.GameObjects
{
}
internal struct SpriteUpdateInertEvent
{
public SpriteComponent Sprite;
}
}

View File

@@ -0,0 +1,43 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
namespace Robust.Client.GameObjects
{
[RegisterComponent]
public sealed class RenderingTreeComponent : Component
{
public override string Name => "RenderingTree";
internal DynamicTree<SpriteComponent> SpriteTree { get; private set; } = new(SpriteAabbFunc);
internal DynamicTree<PointLightComponent> LightTree { get; private set; } = new(LightAabbFunc);
private static Box2 SpriteAabbFunc(in SpriteComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
return new Box2(worldPos, worldPos);
}
private static Box2 LightAabbFunc(in PointLightComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
var boxSize = value.Radius * 2;
return Box2.CenteredAround(worldPos, (boxSize, boxSize));
}
internal static Box2 SpriteAabbFunc(SpriteComponent value, Vector2? worldPos = null)
{
worldPos ??= value.Owner.Transform.WorldPosition;
return new Box2(worldPos.Value, worldPos.Value);
}
internal static Box2 LightAabbFunc(PointLightComponent value, Vector2? worldPos = null)
{
worldPos ??= value.Owner.Transform.WorldPosition;
var boxSize = value.Radius * 2;
return Box2.CenteredAround(worldPos.Value, (boxSize, boxSize));
}
}
}

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
SubscribeLocalEvent<ClientOccluderComponent, SnapGridPositionChangedEvent>(HandleSnapGridMove);
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(HandleAnchorChanged);
}
public override void FrameUpdate(float frameTime)
@@ -62,9 +62,9 @@ namespace Robust.Client.GameObjects
}
}
private static void HandleSnapGridMove(EntityUid uid, ClientOccluderComponent component, SnapGridPositionChangedEvent args)
private static void HandleAnchorChanged(EntityUid uid, ClientOccluderComponent component, AnchorStateChangedEvent args)
{
component.SnapGridOnPositionChanged();
component.AnchorStateChanged();
}
private void HandleDirtyEvent(OccluderDirtyEvent ev)

View File

@@ -0,0 +1,84 @@
#if DEBUG
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
internal sealed class DebugLightTreeSystem : EntitySystem
{
private DebugLightOverlay? _lightOverlay;
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value) return;
_enabled = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_enabled)
{
_lightOverlay = new DebugLightOverlay(
IoCManager.Resolve<IEntityLookup>(),
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IMapManager>(),
Get<RenderingTreeSystem>());
overlayManager.AddOverlay(_lightOverlay);
}
else
{
overlayManager.RemoveOverlay(_lightOverlay!);
_lightOverlay = null;
}
}
}
private bool _enabled;
private sealed class DebugLightOverlay : Overlay
{
private IEntityLookup _lookup;
private IEyeManager _eyeManager;
private IMapManager _mapManager;
private RenderingTreeSystem _tree;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public DebugLightOverlay(IEntityLookup lookup, IEyeManager eyeManager, IMapManager mapManager, RenderingTreeSystem tree)
{
_lookup = lookup;
_eyeManager = eyeManager;
_mapManager = mapManager;
_tree = tree;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var map = _eyeManager.CurrentMap;
if (map == MapId.Nullspace) return;
var viewport = _eyeManager.GetWorldViewport();
foreach (var tree in _tree.GetLightTrees(map, viewport))
{
foreach (var light in tree)
{
var aabb = _lookup.GetWorldAabbFromEntity(light.Owner);
if (!aabb.Intersects(viewport)) continue;
args.WorldHandle.DrawRect(aabb, Color.Green.WithAlpha(0.1f));
}
}
}
}
}
}
#endif

View File

@@ -173,4 +173,24 @@ namespace Robust.Client.GameObjects
AttachedEntity = attachedEntity;
}
}
public class PlayerAttachedEvent : EntityEventArgs
{
public PlayerAttachedEvent(IEntity entity)
{
Entity = entity;
}
public IEntity Entity { get; }
}
public class PlayerDetachedEvent : EntityEventArgs
{
public PlayerDetachedEvent(IEntity entity)
{
Entity = entity;
}
public IEntity Entity { get; }
}
}

View File

@@ -3,8 +3,11 @@ using System.Drawing;
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Physics;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
@@ -18,25 +21,58 @@ namespace Robust.Client.GameObjects
[UsedImplicitly]
public sealed class RenderingTreeSystem : EntitySystem
{
internal const string LoggerSawmill = "rendertree";
// Nullspace is not indexed. Keep that in mind.
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
private readonly Dictionary<MapId, Dictionary<GridId, MapTrees>> _gridTrees = new();
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly List<SpriteComponent> _spriteQueue = new();
private readonly List<PointLightComponent> _lightQueue = new();
private HashSet<EntityUid> _checkedChildren = new();
internal DynamicTree<SpriteComponent> GetSpriteTreeForMap(MapId map, GridId grid)
/// <summary>
/// <see cref="CVars.MaxLightRadius"/>
/// </summary>
public float MaxLightRadius { get; private set; }
internal IEnumerable<RenderingTreeComponent> GetRenderTrees(MapId mapId, Box2 worldAABB)
{
return _gridTrees[map][grid].SpriteTree;
if (mapId == MapId.Nullspace) yield break;
var enclosed = false;
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
{
yield return EntityManager.GetEntity(grid.GridEntityId).GetComponent<RenderingTreeComponent>();
// if we're enclosed then we know no other grids relevant + don't need the map's rendertree
if (grid.WorldBounds.Encloses(in worldAABB))
{
enclosed = true;
break;
}
}
if (!enclosed)
yield return _mapManager.GetMapEntity(mapId).GetComponent<RenderingTreeComponent>();
}
internal DynamicTree<PointLightComponent> GetLightTreeForMap(MapId map, GridId grid)
internal IEnumerable<DynamicTree<SpriteComponent>> GetSpriteTrees(MapId mapId, Box2 worldAABB)
{
return _gridTrees[map][grid].LightTree;
foreach (var comp in GetRenderTrees(mapId, worldAABB))
{
yield return comp.SpriteTree;
}
}
internal IEnumerable<DynamicTree<PointLightComponent>> GetLightTrees(MapId mapId, Box2 worldAABB)
{
foreach (var comp in GetRenderTrees(mapId, worldAABB))
{
yield return comp.LightTree;
}
}
public override void Initialize()
@@ -48,9 +84,7 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(PhysicsSystem));
_mapManager.MapCreated += MapManagerOnMapCreated;
_mapManager.MapDestroyed += MapManagerOnMapDestroyed;
_mapManager.OnGridCreated += MapManagerOnGridCreated;
_mapManager.OnGridRemoved += MapManagerOnGridRemoved;
// Due to how recursion works, this must be done.
SubscribeLocalEvent<MoveEvent>(AnythingMoved);
@@ -65,6 +99,11 @@ namespace Robust.Client.GameObjects
SubscribeLocalEvent<PointLightComponent, PointLightRadiusChangedEvent>(PointLightRadiusChanged);
SubscribeLocalEvent<PointLightComponent, RenderTreeRemoveLightEvent>(RemoveLight);
SubscribeLocalEvent<PointLightComponent, PointLightUpdateEvent>(HandleLightUpdate);
SubscribeLocalEvent<RenderingTreeComponent, ComponentRemove>(HandleTreeRemove);
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.MaxLightRadius, value => MaxLightRadius = value, true);
}
private void HandleLightUpdate(EntityUid uid, PointLightComponent component, PointLightUpdateEvent args)
@@ -88,12 +127,12 @@ namespace Robust.Client.GameObjects
{
// To avoid doing redundant updates (and we don't need to update a grid's children ever)
if (!_checkedChildren.Add(sender.Owner.Uid) ||
sender.Owner.HasComponent<MapGridComponent>() ||
sender.Owner.HasComponent<MapComponent>()) return;
sender.Owner.HasComponent<RenderingTreeComponent>()) return;
// This recursive search is needed, as MoveEvent is defined to not care about indirect events like children.
// WHATEVER YOU DO, DON'T REPLACE THIS WITH SPAMMING EVENTS UNLESS YOU HAVE A GUARANTEE IT WON'T LAG THE GC.
// (Struct-based events ok though)
// Ironically this was lagging the GC lolz
if (sender.Owner.TryGetComponent(out SpriteComponent? sprite))
QueueSpriteUpdate(sprite);
@@ -129,16 +168,10 @@ namespace Robust.Client.GameObjects
private void ClearSprite(SpriteComponent component)
{
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
{
foreach (var gridId in component.IntersectingGrids)
{
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
tree.SpriteTree.Remove(component);
}
}
if (component.RenderTree == null) return;
component.IntersectingGrids.Clear();
component.RenderTree.SpriteTree.Remove(component);
component.RenderTree = null;
}
private void QueueSpriteUpdate(SpriteComponent component)
@@ -173,16 +206,10 @@ namespace Robust.Client.GameObjects
private void ClearLight(PointLightComponent component)
{
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
{
foreach (var gridId in component.IntersectingGrids)
{
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
tree.LightTree.Remove(component);
}
}
if (component.RenderTree == null) return;
component.IntersectingGrids.Clear();
component.RenderTree.LightTree.Remove(component);
component.RenderTree = null;
}
private void QueueLightUpdate(PointLightComponent component)
@@ -198,31 +225,23 @@ namespace Robust.Client.GameObjects
{
base.Shutdown();
_mapManager.MapCreated -= MapManagerOnMapCreated;
_mapManager.MapDestroyed -= MapManagerOnMapDestroyed;
_mapManager.OnGridCreated -= MapManagerOnGridCreated;
_mapManager.OnGridRemoved -= MapManagerOnGridRemoved;
}
private void MapManagerOnMapDestroyed(object? sender, MapEventArgs e)
private void HandleTreeRemove(EntityUid uid, RenderingTreeComponent component, ComponentRemove args)
{
foreach (var (_, gridTree) in _gridTrees[e.Map])
foreach (var sprite in component.SpriteTree)
{
foreach (var comp in gridTree.LightTree)
{
comp.IntersectingGrids.Clear();
}
foreach (var comp in gridTree.SpriteTree)
{
comp.IntersectingGrids.Clear();
}
// Just in case?
gridTree.LightTree.Clear();
gridTree.SpriteTree.Clear();
sprite.RenderTree = null;
}
_gridTrees.Remove(e.Map);
foreach (var light in component.LightTree)
{
light.RenderTree = null;
}
component.SpriteTree.Clear();
component.LightTree.Clear();
}
private void MapManagerOnMapCreated(object? sender, MapEventArgs e)
@@ -232,35 +251,31 @@ namespace Robust.Client.GameObjects
return;
}
_gridTrees.Add(e.Map, new Dictionary<GridId, MapTrees>
{
{GridId.Invalid, new MapTrees()}
});
_mapManager.GetMapEntity(e.Map).EnsureComponent<RenderingTreeComponent>();
}
private void MapManagerOnGridCreated(MapId mapId, GridId gridId)
{
_gridTrees[mapId].Add(gridId, new MapTrees());
EntityManager.GetEntity(_mapManager.GetGrid(gridId).GridEntityId).EnsureComponent<RenderingTreeComponent>();
}
private void MapManagerOnGridRemoved(MapId mapId, GridId gridId)
private RenderingTreeComponent? GetRenderTree(IEntity entity)
{
var gridTree = _gridTrees[mapId][gridId];
foreach (var sprite in gridTree.SpriteTree)
if (entity.Transform.MapID == MapId.Nullspace ||
entity.HasComponent<RenderingTreeComponent>()) return null;
var parent = entity.Transform.Parent?.Owner;
while (true)
{
sprite.IntersectingGrids.Remove(gridId);
if (parent == null) break;
if (parent.TryGetComponent(out RenderingTreeComponent? comp)) return comp;
parent = parent.Transform.Parent?.Owner;
}
foreach (var light in gridTree.LightTree)
{
light.IntersectingGrids.Remove(gridId);
}
// Clear in case
gridTree.LightTree.Clear();
gridTree.SpriteTree.Clear();
_gridTrees[mapId].Remove(gridId);
return null;
}
public override void FrameUpdate(float frameTime)
@@ -270,55 +285,43 @@ namespace Robust.Client.GameObjects
foreach (var sprite in _spriteQueue)
{
sprite.TreeUpdateQueued = false;
var mapId = sprite.Owner.Transform.MapID;
if (!sprite.Visible || sprite.ContainerOccluded)
{
ClearSprite(sprite);
continue;
}
// If we're on a new map then clear the old one.
if (sprite.IntersectingMapId != mapId)
var oldMapTree = sprite.RenderTree;
var newMapTree = GetRenderTree(sprite.Owner);
// TODO: Temp PVS guard
var worldPos = sprite.Owner.Transform.WorldPosition;
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
{
ClearSprite(sprite);
continue;
}
sprite.IntersectingMapId = mapId;
var treePos = newMapTree?.Owner.Transform.WorldPosition ?? Vector2.Zero;
var aabb = RenderingTreeComponent.SpriteAabbFunc(sprite, worldPos).Translated(-treePos);
if (mapId == MapId.Nullspace) continue;
var mapTree = _gridTrees[mapId];
var aabb = MapTrees.SpriteAabbFunc(sprite);
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
// Remove from old
foreach (var gridId in sprite.IntersectingGrids)
// If we're on a new map then clear the old one.
if (oldMapTree != newMapTree)
{
if (intersectingGrids.Contains(gridId)) continue;
mapTree[gridId].SpriteTree.Remove(sprite);
ClearSprite(sprite);
newMapTree?.SpriteTree.Add(sprite, aabb);
}
// Rebuild in the update below
sprite.IntersectingGrids.Clear();
// Update / add to new
foreach (var gridId in intersectingGrids)
else
{
var translated = aabb.Translated(gridId == GridId.Invalid
? Vector2.Zero
: -_mapManager.GetGrid(gridId).WorldPosition);
mapTree[gridId].SpriteTree.AddOrUpdate(sprite, translated);
sprite.IntersectingGrids.Add(gridId);
newMapTree?.SpriteTree.Update(sprite, aabb);
}
sprite.RenderTree = newMapTree;
}
foreach (var light in _lightQueue)
{
light.TreeUpdateQueued = false;
var mapId = light.Owner.Transform.MapID;
if (!light.Enabled || light.ContainerOccluded)
{
@@ -326,72 +329,44 @@ namespace Robust.Client.GameObjects
continue;
}
// If we're on a new map then clear the old one.
if (light.IntersectingMapId != mapId)
var oldMapTree = light.RenderTree;
var newMapTree = GetRenderTree(light.Owner);
// TODO: Temp PVS guard
var worldPos = light.Owner.Transform.WorldPosition;
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
{
ClearLight(light);
continue;
}
light.IntersectingMapId = mapId;
if (mapId == MapId.Nullspace) continue;
var mapTree = _gridTrees[mapId];
var aabb = MapTrees.LightAabbFunc(light);
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
// Remove from old
foreach (var gridId in light.IntersectingGrids)
// TODO: Events need a bit of cleanup so we only validate this on initialize and radius changed events
// this is fine for now IMO as it's 1 float check for every light that moves
if (light.Radius > MaxLightRadius)
{
if (intersectingGrids.Contains(gridId)) continue;
mapTree[gridId].LightTree.Remove(light);
Logger.WarningS(LoggerSawmill, $"Light radius for {light.Owner} set above max radius of {MaxLightRadius}. This may lead to pop-in.");
}
// Rebuild in the update below
light.IntersectingGrids.Clear();
var treePos = newMapTree?.Owner.Transform.WorldPosition ?? Vector2.Zero;
var aabb = RenderingTreeComponent.LightAabbFunc(light, worldPos).Translated(-treePos);
// Update / add to new
foreach (var gridId in intersectingGrids)
// If we're on a new map then clear the old one.
if (oldMapTree != newMapTree)
{
var translated = aabb.Translated(gridId == GridId.Invalid
? Vector2.Zero
: -_mapManager.GetGrid(gridId).WorldPosition);
mapTree[gridId].LightTree.AddOrUpdate(light, translated);
light.IntersectingGrids.Add(gridId);
ClearLight(light);
newMapTree?.LightTree.Add(light, aabb);
}
else
{
newMapTree?.LightTree.Update(light, aabb);
}
light.RenderTree = newMapTree;
}
_spriteQueue.Clear();
_lightQueue.Clear();
}
private sealed class MapTrees
{
public readonly DynamicTree<SpriteComponent> SpriteTree;
public readonly DynamicTree<PointLightComponent> LightTree;
public MapTrees()
{
SpriteTree = new DynamicTree<SpriteComponent>(SpriteAabbFunc);
LightTree = new DynamicTree<PointLightComponent>(LightAabbFunc);
}
internal static Box2 SpriteAabbFunc(in SpriteComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
return new Box2(worldPos, worldPos);
}
internal static Box2 LightAabbFunc(in PointLightComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
var boxSize = value.Radius * 2;
return Box2.CenteredAround(worldPos, (boxSize, boxSize));
}
}
}
internal class RenderTreeRemoveLightEvent : EntityEventArgs

View File

@@ -1,9 +1,9 @@
using JetBrains.Annotations;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
@@ -17,16 +17,29 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IMapManager _mapManager = default!;
private RenderingTreeSystem _treeSystem = default!;
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
public override void Initialize()
{
base.Initialize();
_treeSystem = Get<RenderingTreeSystem>();
SubscribeLocalEvent<SpriteUpdateInertEvent>(QueueUpdateInert);
}
private void QueueUpdateInert(SpriteUpdateInertEvent ev)
{
_inertUpdateQueue.Enqueue(ev.Sprite);
}
/// <inheritdoc />
public override void FrameUpdate(float frameTime)
{
while (_inertUpdateQueue.TryDequeue(out var sprite))
{
sprite.DoUpdateIsInert();
}
// So we could calculate the correct size of the entities based on the contents of their sprite...
// Or we can just assume that no entity is larger than 10x10 and get a stupid easy check.
var pvsBounds = _eyeManager.GetWorldViewport().Enlarged(5);
@@ -37,13 +50,11 @@ namespace Robust.Client.GameObjects
return;
}
foreach (var gridId in _mapManager.FindGridIdsIntersecting(currentMap, pvsBounds, true))
foreach (var comp in _treeSystem.GetRenderTrees(currentMap, pvsBounds))
{
var gridBounds = gridId == GridId.Invalid ? pvsBounds : pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
var bounds = pvsBounds.Translated(-comp.Owner.Transform.WorldPosition);
var mapTree = _treeSystem.GetSpriteTreeForMap(currentMap, gridId);
mapTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
comp.SpriteTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
{
if (value.IsInert)
{
@@ -52,7 +63,7 @@ namespace Robust.Client.GameObjects
value.FrameUpdate(state);
return true;
}, gridBounds, approx: true);
}, bounds, true);
}
}
}

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects

View File

@@ -1,17 +0,0 @@
using System;
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class PlayerAttachedMsg : ComponentMessage
{
}
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class PlayerDetachedMsg : ComponentMessage
{
}
}

View File

@@ -210,6 +210,11 @@ namespace Robust.Client.GameStates
ResetPredictedEntities(_timing.CurTick);
}
if (!curState.Extrapolated)
{
_processor.UpdateFullRep(curState);
}
// Store last tick we got from the GameStateProcessor.
_lastProcessedTick = _timing.CurTick;
@@ -319,6 +324,8 @@ namespace Robust.Client.GameStates
private void ResetPredictedEntities(GameTick curTick)
{
var bus = (EntityEventBus) _entities.EventBus;
foreach (var entity in _entities.GetEntities())
{
// TODO: 99% there's an off-by-one here.
@@ -347,6 +354,7 @@ namespace Robust.Client.GameStates
Logger.DebugS(CVars.NetPredict.Name, $" And also its component {comp.Name}");
// TODO: Handle interpolation.
bus.RaiseComponentEvent(comp, new ComponentHandleState(compState, null));
comp.HandleComponentState(compState, null);
}
}
@@ -419,6 +427,7 @@ namespace Robust.Client.GameStates
//Known entities
if (_entities.TryGetEntity(es.Uid, out var entity))
{
// Logger.Debug($"[{IGameTiming.TickStampStatic}] MOD {es.Uid}");
toApply.Add(entity, (es, null));
}
else //Unknown entities
@@ -429,6 +438,7 @@ namespace Robust.Client.GameStates
{
throw new InvalidOperationException($"Server sent new entity state for {es.Uid} without metadata component!");
}
// Logger.Debug($"[{IGameTiming.TickStampStatic}] CREATE {es.Uid} {metaState.PrototypeId}");
var newEntity = (Entity)_entities.CreateEntity(metaState.PrototypeId, es.Uid);
toApply.Add(newEntity, (es, null));
toInitialize.Add(newEntity);
@@ -455,17 +465,20 @@ namespace Robust.Client.GameStates
}
}
var bus = (EntityEventBus) _entities.EventBus;
// Make sure this is done after all entities have been instantiated.
foreach (var kvStates in toApply)
{
var ent = kvStates.Key;
var entity = (Entity) ent;
HandleEntityState(entity.EntityManager.ComponentManager, entity, kvStates.Value.Item1,
HandleEntityState(entity.EntityManager.ComponentManager, entity, bus, kvStates.Value.Item1,
kvStates.Value.Item2);
}
foreach (var id in deletions)
{
// Logger.Debug($"[{IGameTiming.TickStampStatic}] DELETE {id}");
_entities.DeleteEntity(id);
}
@@ -527,7 +540,7 @@ namespace Robust.Client.GameStates
return created;
}
private void HandleEntityState(IComponentManager compMan, IEntity entity, EntityState? curState,
private void HandleEntityState(IComponentManager compMan, IEntity entity, EntityEventBus bus, EntityState? curState,
EntityState? nextState)
{
var compStateWork = new Dictionary<uint, (ComponentState? curState, ComponentState? nextState)>();
@@ -585,6 +598,7 @@ namespace Robust.Client.GameStates
{
try
{
bus.RaiseComponentEvent(component, new ComponentHandleState(cur, next));
component.HandleComponentState(cur, next);
}
catch (Exception e)

View File

@@ -149,11 +149,6 @@ namespace Robust.Client.GameStates
{
Logger.DebugS("net.state", $"Applying State: ext={curState!.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
if (!curState!.Extrapolated)
{
UpdateFullRep(curState);
}
}
var cState = curState!;
@@ -162,8 +157,10 @@ namespace Robust.Client.GameStates
return applyNextState;
}
private void UpdateFullRep(GameState state)
public void UpdateFullRep(GameState state)
{
// Logger.Debug($"UPDATE FULL REP: {string.Join(", ", state.EntityStates?.Select(e => e.Uid) ?? Enumerable.Empty<EntityUid>())}");
if (state.FromSequence == GameTick.Zero)
{
// Full state.

View File

@@ -178,7 +178,7 @@ namespace Robust.Client.GameStates
var yPos = 10 + _lineHeight * i;
var name = $"({netEnt.Id}) {ent.Prototype?.ID}";
var color = CalcTextColor(ref netEnt);
DrawString(screenHandle, _font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
screenHandle.DrawString(_font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
}
}
@@ -223,17 +223,6 @@ namespace Robust.Client.GameStates
base.DisposeBehavior();
}
private static void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str, Color textColor)
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, rune, baseLine, 1, textColor);
baseLine += new Vector2(advance, 0);
}
}
private struct NetEntity
{
public GameTick LastUpdate;

View File

@@ -184,7 +184,7 @@ namespace Robust.Client.GameStates
// Draw size if above average
if (drawSizeThreshold * 1.5 < state.Payload)
{
DrawString(handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
handle.DrawString(_font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
}
// second tick marks
@@ -224,14 +224,14 @@ namespace Robust.Client.GameStates
handle.DrawLine(new Vector2(leftMargin, midYoff), new Vector2(leftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
// payload text
DrawString(handle, _font, new Vector2(leftMargin + width, warnYoff), "56K");
DrawString(handle, _font, new Vector2(leftMargin + width, midYoff), "33.6K");
handle.DrawString(_font, new Vector2(leftMargin + width, warnYoff), "56K");
handle.DrawString(_font, new Vector2(leftMargin + width, midYoff), "33.6K");
// interp text info
if(lastLagY != -1)
DrawString(handle, _font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
handle.DrawString(_font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
DrawString(handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
handle.DrawString(_font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
}
protected override void DisposeBehavior()
@@ -241,17 +241,6 @@ namespace Robust.Client.GameStates
base.DisposeBehavior();
}
private void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str)
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
baseLine += new Vector2(advance, 0);
}
}
private class NetShowGraphCommand : IConsoleCommand
{
public string Command => "net_graph";

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Graphics.Clyde
{
static Clyde()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
if (OperatingSystem.IsWindows() &&
RuntimeInformation.ProcessArchitecture == Architecture.X64 &&
Environment.GetEnvironmentVariable("ROBUST_INTEGRATED_GPU") != "1")
{
@@ -28,20 +28,20 @@ namespace Robust.Client.Graphics.Clyde
}
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (OperatingSystem.IsWindows())
{
return;
}
NativeLibrary.SetDllImportResolver(typeof(GL).Assembly, (name, assembly, path) =>
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
if (OperatingSystem.IsLinux()
&& _dllMapLinux.TryGetValue(name, out var mappedName))
{
return NativeLibrary.Load(mappedName);
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
if (OperatingSystem.IsMacOS()
&& _dllMapMacOS.TryGetValue(name, out mappedName))
{
return NativeLibrary.Load(mappedName);

View File

@@ -366,24 +366,11 @@ namespace Robust.Client.Graphics.Clyde
private void ProcessSpriteEntities(MapId map, Box2 worldBounds,
RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> list)
{
var spriteSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map, worldBounds, true))
foreach (var comp in _entitySystemManager.GetEntitySystem<RenderingTreeSystem>().GetRenderTrees(map, worldBounds))
{
Box2 gridBounds;
var bounds = worldBounds.Translated(-comp.Owner.Transform.WorldPosition);
if (gridId == GridId.Invalid)
{
gridBounds = worldBounds;
}
else
{
gridBounds = worldBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
var tree = spriteSystem.GetSpriteTreeForMap(map, gridId);
tree.QueryAabb(ref list, ((
comp.SpriteTree.QueryAabb(ref list, ((
ref RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> state,
in SpriteComponent value) =>
{
@@ -398,7 +385,7 @@ namespace Robust.Client.Graphics.Clyde
entry.yWorldPos = worldPos.Y;
return true;
}), gridBounds, approx: true);
}), bounds, true);
}
}

View File

@@ -494,24 +494,16 @@ namespace Robust.Client.Graphics.Clyde
GetLightsToRender(MapId map, in Box2 worldBounds)
{
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
var enlargedBounds = worldBounds.Enlarged(renderingTreeSystem.MaxLightRadius);
// Use worldbounds for this one as we only care if the light intersects our actual bounds
var state = (this, worldBounds, count: 0);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map, worldBounds, true))
foreach (var comp in renderingTreeSystem.GetRenderTrees(map, enlargedBounds))
{
Box2 gridBounds;
var bounds = worldBounds.Translated(-comp.Owner.Transform.WorldPosition);
if (gridId == GridId.Invalid)
{
gridBounds = worldBounds;
}
else
{
gridBounds = worldBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
var lightTree = renderingTreeSystem.GetLightTreeForMap(map, gridId);
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldBounds, int count) state, in PointLightComponent light) =>
comp.LightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldBounds, int count) state, in PointLightComponent light) =>
{
var transform = light.Owner.Transform;
@@ -535,7 +527,7 @@ namespace Robust.Client.Graphics.Clyde
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
return true;
}, gridBounds);
}, bounds);
}
if (state.count > _maxLightsPerScene)

View File

@@ -131,7 +131,7 @@ namespace Robust.Client.Graphics.Clyde
if (!succeeded)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (OperatingSystem.IsWindows())
{
var msgBoxContent = "Failed to create the game window. " +
"This probably means your GPU is too old to play the game. " +
@@ -164,7 +164,7 @@ namespace Robust.Client.Graphics.Clyde
private IEnumerable<Image<Rgba32>> LoadWindowIcons()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
if (OperatingSystem.IsMacOS())
{
// Does nothing on macOS so don't bother.
yield break;

View File

@@ -1,4 +1,4 @@
using System.Runtime.InteropServices;
using System;
using System.Threading.Channels;
using System.Threading.Tasks;
using OpenToolkit.GraphicsLibraryFramework;
@@ -13,11 +13,6 @@ namespace Robust.Client.Graphics.Clyde
{
private sealed partial class GlfwWindowingImpl
{
// glfwPostEmptyEvent is broken on macOS and crashes when not called from the main thread
// (despite what the docs claim, and yes this makes it useless).
// Because of this, we just forego it and use glfwWaitEventsTimeout on macOS instead.
private static readonly bool IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
private bool _windowingRunning;
private ChannelWriter<CmdBase> _cmdWriter = default!;
private ChannelReader<CmdBase> _cmdReader = default!;
@@ -53,7 +48,10 @@ namespace Robust.Client.Graphics.Clyde
while (_windowingRunning)
{
if (IsMacOS)
// glfwPostEmptyEvent is broken on macOS and crashes when not called from the main thread
// (despite what the docs claim, and yes this makes it useless).
// Because of this, we just forego it and use glfwWaitEventsTimeout on macOS instead.
if (OperatingSystem.IsMacOS())
GLFW.WaitEventsTimeout(0.008);
else
GLFW.WaitEvents();
@@ -157,7 +155,7 @@ namespace Robust.Client.Graphics.Clyde
_cmdWriter.TryWrite(cmd);
// Post empty event to unstuck WaitEvents if necessary.
if (!IsMacOS)
if (!OperatingSystem.IsMacOS())
GLFW.PostEmptyEvent();
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Threading.Tasks;
using OpenToolkit;
using OpenToolkit.Graphics.OpenGL4;
@@ -33,6 +34,7 @@ namespace Robust.Client.Graphics.Clyde
private GlfwWindowReg? _mainWindow;
private GlfwBindingsContext _mainGraphicsContext = default!;
private int _nextWindowId = 1;
private static bool _eglLoaded;
public async Task<WindowHandle> WindowCreate(WindowCreateParameters parameters)
{
@@ -474,6 +476,20 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, ContextApi.EglContextApi);
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
if (!_eglLoaded && OperatingSystem.IsWindows())
{
// On non-published builds (so, development), GLFW can't find libEGL.dll
// because it'll be in runtimes/<rid>/native/ instead of next to the actual executable.
// We manually preload the library here so that GLFW will find it when it does its thing.
NativeLibrary.TryLoad(
"libEGL.dll",
typeof(Clyde).Assembly,
DllImportSearchPath.SafeDirectories,
out _);
_eglLoaded = true;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Text;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -39,5 +40,57 @@ namespace Robust.Client.Graphics
DrawPrimitiveTopology type = filled ? DrawPrimitiveTopology.TriangleFan : DrawPrimitiveTopology.LineStrip;
DrawPrimitives(type, buffer, color);
}
/// <summary>
/// Draw a simple string to the screen at the specified position.
/// </summary>
/// <remarks>
/// This method is primarily intended for debug purposes and does not handle things like UI scaling.
/// </remarks>
/// <returns>
/// The space taken up (horizontal and vertical) by the text.
/// </returns>
/// <param name="font">The font to render with.</param>
/// <param name="pos">The top-left corner to start drawing text at.</param>
/// <param name="str">The text to draw.</param>
/// <param name="color">The color of text to draw.</param>
public Vector2 DrawString(Font font, Vector2 pos, string str, Color color)
{
var advanceTotal = Vector2.Zero;
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
var lineHeight = font.GetLineHeight(1);
foreach (var rune in str.EnumerateRunes())
{
if (rune == new Rune('\n'))
{
baseLine.X = pos.X;
baseLine.Y += lineHeight;
advanceTotal.Y += lineHeight;
continue;
}
var advance = font.DrawChar(this, rune, baseLine, 1, color);
advanceTotal.X += advance;
baseLine += new Vector2(advance, 0);
}
return advanceTotal;
}
/// <summary>
/// Draw a simple string to the screen at the specified position.
/// </summary>
/// <remarks>
/// This method is primarily intended for debug purposes and does not handle things like UI scaling.
/// </remarks>
/// <returns>
/// The space taken up (horizontal and vertical) by the text.
/// </returns>
/// <param name="font">The font to render with.</param>
/// <param name="pos">The top-left corner to start drawing text at.</param>
/// <param name="str">The text to draw.</param>
public Vector2 DrawString(Font font, Vector2 pos, string str)
=> DrawString(font, pos, str, Color.White);
}
}

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System;
using System.Collections.Generic;
using Robust.Shared.Localization;
using Robust.Shared.Map;
namespace Robust.Client.Input
{
@@ -179,9 +178,9 @@ namespace Robust.Client.Input
var locId = $"input-key-{key}";
if (key == Key.LSystem || key == Key.RSystem)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (OperatingSystem.IsWindows())
locId += "-win";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
else if (OperatingSystem.IsMacOS())
locId += "-mac";
else
locId += "-linux";

View File

@@ -1,6 +1,5 @@
using System;
using System;
using Robust.Client.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
@@ -71,10 +70,10 @@ namespace Robust.Client.Player
eye.Current = true;
EntityAttached?.Invoke(new EntityAttachedEventArgs(entity));
entity.SendMessage(null, new PlayerAttachedMsg());
// notify ECS Systems
ControlledEntity.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(ControlledEntity));
ControlledEntity.EntityManager.EventBus.RaiseLocalEvent(ControlledEntity.Uid, new PlayerAttachedEvent(ControlledEntity));
}
/// <summary>
@@ -83,13 +82,13 @@ namespace Robust.Client.Player
public void DetachEntity()
{
var previous = ControlledEntity;
if (previous != null && previous.Initialized && !previous.Deleted)
if (previous is {Initialized: true, Deleted: false})
{
previous.GetComponent<EyeComponent>().Current = false;
previous.SendMessage(null, new PlayerDetachedMsg());
// notify ECS Systems
previous.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(null));
previous.EntityManager.EventBus.RaiseLocalEvent(previous.Uid, new PlayerDetachedEvent(previous));
}
ControlledEntity = null;

View File

@@ -318,6 +318,8 @@ namespace Robust.Client.UserInterface.Controls
{
base.Draw(handle);
var sizeBox = PixelSizeBox;
var font = ActualFont;
var listBg = ActualBackground;
var iconBg = ActualItemBackground;
@@ -346,31 +348,38 @@ namespace Robust.Client.UserInterface.Controls
itemHeight = item.IconSize.Y;
}
itemHeight = Math.Max(itemHeight, ActualFont.GetHeight(UIScale));
itemHeight = Math.Max(itemHeight, font.GetHeight(UIScale));
itemHeight += ActualItemBackground.MinimumSize.Y;
item.Region = UIBox2.FromDimensions(0, offset, PixelWidth, itemHeight);
var region = UIBox2.FromDimensions(0, offset, PixelWidth, itemHeight);
item.Region = region;
bg.Draw(handle, item.Region.Value);
var contentBox = bg.GetContentBox(item.Region.Value);
var drawOffset = contentBox.TopLeft;
if (item.Icon != null)
if (region.Intersects(sizeBox))
{
if (item.IconRegion.Size == Vector2.Zero)
{
handle.DrawTextureRect(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size), item.IconModulate);
}
else
{
handle.DrawTextureRectRegion(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size), item.IconRegion, item.IconModulate);
}
}
bg.Draw(handle, item.Region.Value);
if (item.Text != null)
{
var textBox = new UIBox2(contentBox.Left + item.IconSize.X, contentBox.Top, contentBox.Right, contentBox.Bottom);
DrawTextInternal(handle, item.Text, textBox);
var contentBox = bg.GetContentBox(item.Region.Value);
var drawOffset = contentBox.TopLeft;
if (item.Icon != null)
{
if (item.IconRegion.Size == Vector2.Zero)
{
handle.DrawTextureRect(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size),
item.IconModulate);
}
else
{
handle.DrawTextureRectRegion(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size),
item.IconRegion, item.IconModulate);
}
}
if (item.Text != null)
{
var textBox = new UIBox2(contentBox.Left + item.IconSize.X, contentBox.Top, contentBox.Right,
contentBox.Bottom);
DrawTextInternal(handle, item.Text, textBox);
}
}
offset += itemHeight;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Client.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Localization;
@@ -75,7 +75,7 @@ namespace Robust.Client.UserInterface.Controls
var control = GetChild(tab);
var title = control.GetValue(TabTitleProperty);
return title ?? control.Name ?? Loc.GetString("No title");
return title ?? control.Name ?? Loc.GetString("tab-container-not-tab-title-provided");
}
public static string? GetTabTitle(Control control)

View File

@@ -65,7 +65,7 @@ namespace Robust.Client.UserInterface.CustomControls
this.prototypeManager = prototypeManager;
this.resourceCache = resourceCache;
Title = Loc.GetString("Entity Spawn Panel");
Title = Loc.GetString("entity-spawn-window-title");
SetSize = (250, 300);
@@ -83,13 +83,13 @@ namespace Robust.Client.UserInterface.CustomControls
(SearchBar = new LineEdit
{
HorizontalExpand = true,
PlaceHolder = Loc.GetString("Search")
PlaceHolder = Loc.GetString("entity-spawn-window-search-bar-placeholder")
}),
(ClearButton = new Button
{
Disabled = true,
Text = Loc.GetString("Clear"),
Text = Loc.GetString("entity-spawn-window-clear-button"),
})
}
},
@@ -109,13 +109,13 @@ namespace Robust.Client.UserInterface.CustomControls
(EraseButton = new Button
{
ToggleMode = true,
Text = Loc.GetString("Erase Mode")
Text = Loc.GetString("entity-spawn-window-erase-button-text")
}),
(OverrideMenu = new OptionButton
{
HorizontalExpand = true,
ToolTip = Loc.GetString("Override placement")
ToolTip = Loc.GetString("entity-spawn-window-override-menu-tooltip")
})
}
},

View File

@@ -1,6 +1,5 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.CustomControls
@@ -37,9 +36,9 @@ namespace Robust.Client.UserInterface.CustomControls
(InputBar = new HistoryLineEdit
{
HorizontalExpand = true,
PlaceHolder = Loc.GetString("Your C# code here.")
PlaceHolder = "Your C# code here."
}),
(RunButton = new Button {Text = Loc.GetString("Run")})
(RunButton = new Button {Text = "Run"})
}
},
}

View File

@@ -148,7 +148,7 @@ namespace Robust.Client.UserInterface
// ReSharper disable once MemberCanBeMadeStatic.Local
private Task<string?> RunAsyncMaybe(Func<string?> action)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
if (OperatingSystem.IsMacOS())
{
// macOS seems pretty annoying about having the file dialog opened from the main thread.
// So we are forced to execute this synchronously on the main thread.
@@ -326,7 +326,7 @@ namespace Robust.Client.UserInterface
private async Task<bool> IsKDialogAvailable()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
if (!OperatingSystem.IsLinux())
return false;
if (!_checkedKDialogAvailable)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -120,7 +120,7 @@ namespace Robust.Client.ViewVariables.Instances
var clientVBox = new VBoxContainer {SeparationOverride = 0};
_tabs.AddChild(clientVBox);
_tabs.SetTabTitle(TabClientVars, "Client Variables");
_tabs.SetTabTitle(TabClientVars, Loc.GetString("view-variable-instance-entity-client-variables-tab-title"));
var first = true;
foreach (var group in LocalPropertyList(obj, ViewVariablesManager, _robustSerializer))
@@ -138,7 +138,7 @@ namespace Robust.Client.ViewVariables.Instances
_clientComponents = new VBoxContainer {SeparationOverride = 0};
_tabs.AddChild(_clientComponents);
_tabs.SetTabTitle(TabClientComponents, "Client Components");
_tabs.SetTabTitle(TabClientComponents, Loc.GetString("view-variable-instance-entity-client-components-tab-title"));
PopulateClientComponents();
@@ -146,11 +146,11 @@ namespace Robust.Client.ViewVariables.Instances
{
_serverVariables = new VBoxContainer {SeparationOverride = 0};
_tabs.AddChild(_serverVariables);
_tabs.SetTabTitle(TabServerVars, "Server Variables");
_tabs.SetTabTitle(TabServerVars, Loc.GetString("view-variable-instance-entity-server-variables-tab-title"));
_serverComponents = new VBoxContainer {SeparationOverride = 0};
_tabs.AddChild(_serverComponents);
_tabs.SetTabTitle(TabServerComponents, "Server Components");
_tabs.SetTabTitle(TabServerComponents, Loc.GetString("view-variable-instance-entity-server-components-tab-title"));
PopulateServerComponents(false);
}
@@ -162,13 +162,13 @@ namespace Robust.Client.ViewVariables.Instances
_clientComponents.AddChild(_clientComponentsSearchBar = new LineEdit
{
PlaceHolder = Loc.GetString("Search"),
PlaceHolder = Loc.GetString("view-variable-instance-entity-client-components-search-bar-placeholder"),
HorizontalExpand = true,
});
_clientComponents.AddChild(_clientComponentsAddButton = new Button()
{
Text = Loc.GetString("Add Component"),
Text = Loc.GetString("view-variable-instance-entity-server-components-add-component-button-placeholder"),
HorizontalExpand = true,
});
@@ -198,13 +198,13 @@ namespace Robust.Client.ViewVariables.Instances
_serverComponents.AddChild(_serverComponentsSearchBar = new LineEdit
{
PlaceHolder = Loc.GetString("Search"),
PlaceHolder = Loc.GetString("view-variable-instance-entity-server-components-search-bar-placeholder"),
HorizontalExpand = true,
});
_serverComponents.AddChild(_serverComponentsAddButton = new Button()
{
Text = Loc.GetString("Add Component"),
Text = Loc.GetString("view-variable-instance-entity-server-components-add-component-button-placeholder"),
HorizontalExpand = true,
});
@@ -336,7 +336,7 @@ namespace Robust.Client.ViewVariables.Instances
{
_addComponentWindow?.Dispose();
_addComponentWindow = new ViewVariablesAddWindow(GetValidComponentsForAdding(), "Add Component [C]");
_addComponentWindow = new ViewVariablesAddWindow(GetValidComponentsForAdding(), Loc.GetString("view-variable-instance-entity-add-window-client-components"));
_addComponentWindow.AddButtonPressed += TryAdd;
_addComponentServer = false;
@@ -349,7 +349,7 @@ namespace Robust.Client.ViewVariables.Instances
if (_entitySession == null) return;
_addComponentWindow = new ViewVariablesAddWindow(await GetValidServerComponentsForAdding(), "Add Component [S]");
_addComponentWindow = new ViewVariablesAddWindow(await GetValidServerComponentsForAdding(), Loc.GetString("view-variable-instance-entity-add-window-server-components"));
_addComponentWindow.AddButtonPressed += TryAdd;
_addComponentServer = true;

View File

@@ -355,7 +355,7 @@ namespace Robust.Server
AddFinalStringsToSerializer();
_stringSerializer.LockStrings();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
if (OperatingSystem.IsWindows() && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeBeginPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}
@@ -449,8 +449,7 @@ namespace Robust.Server
}
}
/// <inheritdoc />
public void MainLoop()
internal void SetupMainLoop()
{
if (_mainLoop == null)
{
@@ -472,14 +471,26 @@ namespace Robust.Server
// set GameLoop.Running to false to return from this function.
_time.Paused = false;
_mainLoop.Run();
}
internal void FinishMainLoop()
{
_time.InSimulation = true;
Cleanup();
_shutdownEvent.Set();
}
/// <inheritdoc />
public void MainLoop()
{
SetupMainLoop();
_mainLoop.Run();
FinishMainLoop();
}
public bool ContentStart { get; set; }
public bool LoadConfigAndUserData { private get; set; } = true;
@@ -560,7 +571,7 @@ namespace Robust.Server
//TODO: This should prob shutdown all managers in a loop.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
if (OperatingSystem.IsWindows() && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeEndPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}

View File

@@ -1,4 +1,3 @@
using System;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
@@ -12,34 +11,4 @@ namespace Robust.Server.GameObjects
[ViewVariables]
public IPlayerSession PlayerSession { get; internal set; } = default!;
}
/// <summary>
/// Raised on an entity whenever a player attaches to this entity.
/// </summary>
[Serializable]
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class PlayerAttachedMsg : ComponentMessage
{
public IPlayerSession NewPlayer { get; }
public PlayerAttachedMsg(IPlayerSession newPlayer)
{
NewPlayer = newPlayer;
}
}
/// <summary>
/// Raised on an entity whenever a player detaches from this entity.
/// </summary>
[Serializable]
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class PlayerDetachedMsg : ComponentMessage
{
public IPlayerSession OldPlayer { get; }
public PlayerDetachedMsg(IPlayerSession oldPlayer)
{
OldPlayer = oldPlayer;
}
}
}

View File

@@ -1,4 +1,3 @@
using System;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
@@ -9,7 +8,7 @@ namespace Robust.Server.GameObjects
/// System that handles players being attached/detached from entities.
/// </summary>
[UsedImplicitly]
public class ActorSystem : EntitySystem
public sealed class ActorSystem : EntitySystem
{
public override void Initialize()
{
@@ -22,27 +21,54 @@ namespace Robust.Server.GameObjects
private void OnActorPlayerAttach(AttachPlayerEvent args)
{
args.Result = Attach(args.Entity, args.Player, args.Force, out var forceKicked);
args.ForceKicked = forceKicked;
}
/// <summary>
/// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
/// </summary>
/// <param name="entity">The entity to attach the player to</param>
/// <param name="player">The player to attach to the entity</param>
/// <param name="force">Whether to kick any existing players from the entity</param>
/// <returns>Whether the attach succeeded, or not.</returns>
public bool Attach(IEntity entity, IPlayerSession player, bool force = false)
{
return Attach(entity, player, false, out _);
}
/// <summary>
/// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
/// </summary>
/// <param name="entity">The entity to attach the player to</param>
/// <param name="player">The player to attach to the entity</param>
/// <param name="force">Whether to kick any existing players from the entity</param>
/// <param name="forceKicked">The player that was forcefully kicked, or null.</param>
/// <returns>Whether the attach succeeded, or not.</returns>
public bool Attach(IEntity entity, IPlayerSession player, bool force, out IPlayerSession? forceKicked)
{
// Null by default.
forceKicked = null;
// Cannot attach to a deleted entity.
if (args.Entity.Deleted)
if (entity.Deleted)
{
args.Result = false;
return;
return false;
}
var uid = args.Entity.Uid;
var uid = entity.Uid;
// Check if there was a player attached to the entity already...
if (ComponentManager.TryGetComponent(uid, out ActorComponent actor))
{
// If we're not forcing the attach, this fails.
if (!args.Force)
if (!force)
{
args.Result = false;
return;
return false;
}
// Set the event's force-kicked session before detaching it.
args.ForceKicked = actor.PlayerSession;
forceKicked = actor.PlayerSession;
// This detach cannot fail, as a player is attached to this entity.
// It's important to note that detaching the player removes the component.
@@ -50,18 +76,16 @@ namespace Robust.Server.GameObjects
}
// We add the actor component.
actor = ComponentManager.AddComponent<ActorComponent>(args.Entity);
actor.PlayerSession = args.Player;
args.Player.SetAttachedEntity(args.Entity);
args.Result = true;
// TODO: Remove component message.
args.Entity.SendMessage(actor, new PlayerAttachedMsg(args.Player));
actor = ComponentManager.AddComponent<ActorComponent>(entity);
actor.PlayerSession = player;
player.SetAttachedEntity(entity);
// The player is fully attached now, raise an event!
RaiseLocalEvent(uid, new PlayerAttachedEvent(args.Entity, args.Player));
RaiseLocalEvent(uid, new PlayerAttachedEvent(entity, player, forceKicked));
return true;
}
// Not gonna make this method call Detach as all we have to do is remove a component...
private void OnActorPlayerDetach(EntityUid uid, ActorComponent component, DetachPlayerEvent args)
{
// Removing the component will call shutdown, and our subscription will handle the rest of the detach logic.
@@ -69,15 +93,39 @@ namespace Robust.Server.GameObjects
args.Result = true;
}
/// <summary>
/// Detaches an attached session from the entity, if any.
/// </summary>
/// <param name="entity">The entity player sessions will be detached from.</param>
/// <returns>Whether any player session was detached.</returns>
public bool Detach(IEntity entity)
{
if (!entity.HasComponent<ActorComponent>())
return false;
// Removing the component will call shutdown, and our subscription will handle the rest of the detach logic.
entity.RemoveComponent<ActorComponent>();
return true;
}
/// <summary>
/// Detaches this player from its attached entity, if any.
/// </summary>
/// <param name="player">The player session that will be detached from any attached entities.</param>
/// <returns>Whether the player is now detached from any entities.
/// This returns true if the player wasn't attached to any entity.</returns>
public bool Detach(IPlayerSession player)
{
var entity = player.AttachedEntity;
return entity == null || Detach(entity);
}
private void OnActorShutdown(EntityUid uid, ActorComponent component, ComponentShutdown args)
{
component.PlayerSession.SetAttachedEntity(null);
var entity = EntityManager.GetEntity(uid);
// TODO: Remove component message.
entity.SendMessage(component, new PlayerDetachedMsg(component.PlayerSession));
// The player is fully detached now that the component has shut down.
RaiseLocalEvent(uid, new PlayerDetachedEvent(entity, component.PlayerSession));
}
@@ -86,7 +134,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Raise this broadcast event to attach a player to an entity, optionally detaching the player attached to it.
/// </summary>
public class AttachPlayerEvent : EntityEventArgs
public sealed class AttachPlayerEvent : EntityEventArgs
{
/// <summary>
/// Player to attach to the entity.
@@ -131,7 +179,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Raise this directed event to detach a player from an entity.
/// </summary>
public class DetachPlayerEvent : EntityEventArgs
public sealed class DetachPlayerEvent : EntityEventArgs
{
/// <summary>
/// Whether the player was detached correctly.
@@ -144,22 +192,28 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Event for when a player has been attached to an entity.
/// </summary>
public class PlayerAttachedEvent : EntityEventArgs
public sealed class PlayerAttachedEvent : EntityEventArgs
{
public IEntity Entity { get; }
public IPlayerSession Player { get; }
public PlayerAttachedEvent(IEntity entity, IPlayerSession player)
/// <summary>
/// The player session that was forcefully kicked from the entity, if any.
/// </summary>
public IPlayerSession? Kicked { get; }
public PlayerAttachedEvent(IEntity entity, IPlayerSession player, IPlayerSession? kicked = null)
{
Entity = entity;
Player = player;
Kicked = kicked;
}
}
/// <summary>
/// Event for when a player has been detached from an entity.
/// </summary>
public class PlayerDetachedEvent : EntityEventArgs
public sealed class PlayerDetachedEvent : EntityEventArgs
{
public IEntity Entity { get; }
public IPlayerSession Player { get; }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
@@ -7,7 +8,7 @@ namespace Robust.Server.GameObjects
internal sealed class GridTileLookupNode
{
internal GridTileLookupChunk ParentChunk { get; }
internal Vector2i Indices { get; }
internal IEnumerable<IEntity> Entities
@@ -17,7 +18,18 @@ namespace Robust.Server.GameObjects
foreach (var entity in _entities)
{
if (!entity.Deleted)
{
yield return entity;
}
if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager)) continue;
foreach (var container in containerManager.GetAllContainers())
{
foreach (var child in container.ContainedEntities)
{
yield return child;
}
}
}
}
}
@@ -29,7 +41,7 @@ namespace Robust.Server.GameObjects
ParentChunk = parentChunk;
Indices = indices;
}
internal void AddEntity(IEntity entity)
{
_entities.Add(entity);

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -212,6 +213,8 @@ namespace Robust.Server.GameObjects
SubscribeNetworkEvent<RequestGridTileLookupMessage>(HandleRequest);
#endif
SubscribeLocalEvent<MoveEvent>(HandleEntityMove);
SubscribeLocalEvent<EntInsertedIntoContainerMessage>(HandleContainerInsert);
SubscribeLocalEvent<EntRemovedFromContainerMessage>(HandleContainerRemove);
SubscribeLocalEvent<EntityInitializedMessage>(HandleEntityInitialized);
SubscribeLocalEvent<EntityDeletedMessage>(HandleEntityDeleted);
_mapManager.OnGridCreated += HandleGridCreated;
@@ -219,6 +222,16 @@ namespace Robust.Server.GameObjects
_mapManager.TileChanged += HandleTileChanged;
}
private void HandleContainerRemove(EntRemovedFromContainerMessage ev)
{
HandleEntityAdd(ev.Entity);
}
private void HandleContainerInsert(EntInsertedIntoContainerMessage ev)
{
HandleEntityRemove(ev.Entity);
}
public override void Shutdown()
{
base.Shutdown();
@@ -238,6 +251,11 @@ namespace Robust.Server.GameObjects
private void HandleEntityInitialized(EntityInitializedMessage message)
{
if (message.Entity.IsInContainer())
{
return;
}
HandleEntityAdd(message.Entity);
}

View File

@@ -213,7 +213,8 @@ namespace Robust.Server.GameStates
new TransformComponent.TransformComponentState(Vector2NaN,
oldState.Rotation,
oldState.ParentID,
oldState.NoLocalRotation)
oldState.NoLocalRotation,
oldState.Anchored)
}));
}

View File

@@ -183,6 +183,8 @@ namespace Robust.Server.GameStates
oldestAck = lastAck;
}
DebugTools.Assert(state.MapData?.CreatedMaps is null || (state.MapData?.CreatedMaps is not null && state.EntityStates is not null), "Sending new maps, but no entity state.");
// actually send the state
var stateUpdateMessage = _networkManager.CreateNetMessage<MsgState>();
stateUpdateMessage.State = state;
@@ -296,10 +298,8 @@ namespace Robust.Server.GameStates
DebugTools.Assert(entity.Initialized);
if (entity.LastModifiedTick <= fromTick)
continue;
stateEntities.Add(GetEntityState(entityMan.ComponentManager, player, entity.Uid, fromTick));
if (entity.LastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(entityMan.ComponentManager, player, entity.Uid, fromTick));
}
// no point sending an empty collection

View File

@@ -117,11 +117,7 @@ namespace Robust.Server.Player
if (entity == null)
return;
// This event needs to be broadcast.
var attachPlayer = new AttachPlayerEvent(entity, this);
entity.EntityManager.EventBus.RaiseLocalEvent(entity.Uid, attachPlayer);
if (!attachPlayer.Result)
if (!EntitySystem.Get<ActorSystem>().Attach(entity, this))
{
Logger.Warning($"Couldn't attach player \"{this}\" to entity \"{entity}\"! Did it have a player already attached to it?");
}
@@ -133,10 +129,19 @@ namespace Robust.Server.Player
if (AttachedEntity == null)
return;
var detachPlayer = new DetachPlayerEvent();
AttachedEntity.EntityManager.EventBus.RaiseLocalEvent(AttachedEntity.Uid, detachPlayer, false);
#if EXCEPTION_TOLERANCE
if (AttachedEntity.Deleted)
{
Logger.Warning($"Player \"{this}\" was attached to an entity that was deleted. THIS SHOULD NEVER HAPPEN, BUT DOES.");
// We can't contact ActorSystem because trying to fire an entity event would crash.
// Work around it.
AttachedEntity = null;
UpdatePlayerState();
return;
}
#endif
if (!detachPlayer.Result)
if (!EntitySystem.Get<ActorSystem>().Detach(AttachedEntity))
{
Logger.Warning($"Couldn't detach player \"{this}\" to entity \"{AttachedEntity}\"! Is it missing an ActorComponent?");
}

View File

@@ -98,7 +98,7 @@ namespace Robust.Server
internal static void SetupLogging()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (OperatingSystem.IsWindows())
{
#if WINDOWS_USE_UTF8_CONSOLE
System.Console.OutputEncoding = Encoding.UTF8;

View File

@@ -37,6 +37,7 @@ namespace Robust.Server
IoCManager.Register<IBaseServer, BaseServer>();
IoCManager.Register<IBaseServerInternal, BaseServer>();
IoCManager.Register<BaseServer, BaseServer>();
IoCManager.Register<IGameTiming, GameTiming>();
IoCManager.Register<IComponentFactory, ServerComponentFactory>();
IoCManager.Register<IConGroupController, ConGroupController>();

View File

@@ -11,7 +11,7 @@ namespace Robust.Server.Utility
public static void TimeBeginPeriod(uint period)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (!OperatingSystem.IsWindows())
throw new InvalidOperationException();
var ret = timeBeginPeriod(period);
@@ -21,7 +21,7 @@ namespace Robust.Server.Utility
public static void TimeEndPeriod(uint period)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (!OperatingSystem.IsWindows())
throw new InvalidOperationException();
var ret = timeEndPeriod(period);

View File

@@ -14,6 +14,11 @@ namespace Robust.Shared.Asynchronous
public void Initialize()
{
_mainThreadContext = new RobustSynchronizationContext(_runtimeLog);
ResetSynchronizationContext();
}
public void ResetSynchronizationContext()
{
SynchronizationContext.SetSynchronizationContext(_mainThreadContext);
}

View File

@@ -73,7 +73,7 @@ namespace Robust.Shared
// just has the Lidgren thread go absolute brr polling.
public static readonly CVarDef<float> NetPredictLagBias = CVarDef.Create(
"net.predict_lag_bias",
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 0.016f : 0,
OperatingSystem.IsWindows() ? 0.016f : 0,
CVar.ARCHIVE);
public static readonly CVarDef<int> NetStateBufMergeThreshold =
@@ -211,6 +211,21 @@ namespace Robust.Shared
public static readonly CVarDef<bool> LogRuntimeLog =
CVarDef.Create("log.runtimelog", true, CVar.ARCHIVE | CVar.SERVERONLY);
/*
* Light
*/
/// <summary>
/// This is the maximum the viewport is enlarged to check for any intersecting render-trees for lights.
/// This should be set to your maximum light radius.
/// </summary>
/// <remarks>
/// If this value is too small it just means there may be pop-in where a light is located on a render-tree
/// outside of our viewport.
/// </remarks>
public static readonly CVarDef<float> MaxLightRadius =
CVarDef.Create("light.max_radius", 20.0f, CVar.CLIENTONLY);
/*
* LOKI
*/

View File

@@ -68,6 +68,10 @@ namespace Robust.Shared.Containers
InternalInsert(toinsert);
transform.AttachParent(Owner.Transform);
// This is an edge case where the parent grid is the container being inserted into, so AttachParent would not unanchor.
if (transform.Anchored)
transform.Anchored = false;
// spatially move the object to the location of the container. If you don't want this functionality, the
// calling code can save the local position before calling this function, and apply it afterwords.
transform.LocalPosition = Vector2.Zero;
@@ -147,7 +151,6 @@ namespace Robust.Shared.Containers
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntInsertedIntoContainerMessage(toinsert, this));
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new UpdateContainerOcclusionMessage(toinsert));
Manager.Owner.SendMessage(Manager, new ContainerContentsModifiedMessage(this, toinsert, false));
Manager.Dirty();
}
@@ -164,7 +167,6 @@ namespace Robust.Shared.Containers
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntRemovedFromContainerMessage(toremove, this));
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new UpdateContainerOcclusionMessage(toremove));
Manager.Owner.SendMessage(Manager, new ContainerContentsModifiedMessage(this, toremove, true));
Manager.Dirty();
}
}

View File

@@ -1,40 +0,0 @@
using System;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Containers
{
/// <summary>
/// The contents of this container have been changed.
/// </summary>
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class ContainerContentsModifiedMessage : ComponentMessage
{
/// <summary>
/// Container whose contents were modified.
/// </summary>
public IContainer Container { get; }
/// <summary>
/// Entity that was added or removed from the container.
/// </summary>
public IEntity Entity { get; }
/// <summary>
/// If true, the entity was removed. If false, it was added to the container.
/// </summary>
public bool Removed { get; }
/// <summary>
/// Constructs a new instance of <see cref="ContainerContentsModifiedMessage"/>.
/// </summary>
/// <param name="container">Container whose contents were modified.</param>
/// <param name="entity">Entity that was added or removed in the container.</param>
/// <param name="removed">If true, the entity was removed. If false, it was added to the container.</param>
public ContainerContentsModifiedMessage(IContainer container, IEntity entity, bool removed)
{
Container = container;
Entity = entity;
Removed = removed;
}
}
}

View File

@@ -33,7 +33,7 @@ namespace Robust.Shared.Containers
public sealed override uint? NetID => NetIDs.CONTAINER_MANAGER;
/// <inheritdoc />
public override void OnRemove()
protected override void OnRemove()
{
base.OnRemove();
@@ -47,7 +47,7 @@ namespace Robust.Shared.Containers
}
/// <inheritdoc />
public override void Initialize()
protected override void Initialize()
{
base.Initialize();

View File

@@ -106,8 +106,16 @@ namespace Robust.Shared.ContentPack
Logger.DebugS("res.mod", $"Verified assemblies in {checkerSw.ElapsedMilliseconds}ms");
}
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value.Path,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var path in TopologicalSortModules(files))
foreach (var path in TopologicalSort.Sort(nodes))
{
Logger.DebugS("res.mod", $"Loading module: '{path}'");
try
@@ -136,38 +144,6 @@ namespace Robust.Shared.ContentPack
return true;
}
private static IEnumerable<ResourcePath> TopologicalSortModules(
IEnumerable<KeyValuePair<string, (ResourcePath Path, string[] references)>> modules)
{
var elems = modules.ToDictionary(
node => node.Key,
node => (node.Value.Path, refs: new HashSet<string>(node.Value.references)));
// Remove assembly references we aren't sorting for.
foreach (var (_, set) in elems.Values)
{
set.RemoveWhere(r => !elems.ContainsKey(r));
}
while (elems.Count > 0)
{
var elem = elems.FirstOrNull(x => x.Value.refs.Count == 0);
if (elem == null)
{
throw new InvalidOperationException(
"Found circular dependency in assembly dependency graph");
}
elems.Remove(elem.Value.Key);
foreach (var sElem in elems)
{
sElem.Value.refs.Remove(elem.Value.Key);
}
yield return elem.Value.Value.Path;
}
}
private static (string[] refs, string name) GetAssemblyReferenceData(Stream stream)
{
using var reader = new PEReader(stream);

View File

@@ -60,8 +60,8 @@ namespace Robust.Shared.ContentPack
// TODO: gaf
public static bool IsFileSystemCaseSensitive() =>
!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
&& !RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
}
}

View File

@@ -33,12 +33,12 @@ namespace Robust.Shared
return IntPtr.Zero;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
if (OperatingSystem.IsLinux())
{
return NativeLibrary.Load(linuxName, assembly, path);
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
if (OperatingSystem.IsMacOS())
{
return NativeLibrary.Load(macName, assembly, path);
}

View File

@@ -41,31 +41,94 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[ViewVariables]
public bool Initialized { get; private set; }
public ComponentLifeStage LifeStage { get; private set; } = ComponentLifeStage.PreAdd;
private bool _running;
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public bool Running
/// <summary>
/// Increases the life stage from <see cref="ComponentLifeStage.PreAdd" /> to <see cref="ComponentLifeStage.Added" />,
/// calling <see cref="OnAdd" />.
/// </summary>
internal void LifeAddToEntity()
{
get => _running;
set
{
if(_running == value)
return;
DebugTools.Assert(LifeStage == ComponentLifeStage.PreAdd);
if(value)
Startup();
else
Shutdown();
LifeStage = ComponentLifeStage.Adding;
OnAdd();
_running = value;
}
DebugTools.Assert(LifeStage == ComponentLifeStage.Added, $"Component {this.GetType().Name} did not call base {nameof(OnAdd)} in derived method.");
}
/// <summary>
/// Increases the life stage from <see cref="ComponentLifeStage.Added" /> to <see cref="ComponentLifeStage.Initialized" />,
/// calling <see cref="Initialize" />.
/// </summary>
internal void LifeInitialize()
{
DebugTools.Assert(LifeStage == ComponentLifeStage.Added);
LifeStage = ComponentLifeStage.Initializing;
Initialize();
DebugTools.Assert(LifeStage == ComponentLifeStage.Initialized, $"Component {this.GetType().Name} did not call base {nameof(Initialize)} in derived method.");
}
/// <summary>
/// Increases the life stage from <see cref="ComponentLifeStage.Initialized" /> to
/// <see cref="ComponentLifeStage.Running" />, calling <see cref="Startup" />.
/// </summary>
internal void LifeStartup()
{
DebugTools.Assert(LifeStage == ComponentLifeStage.Initialized);
LifeStage = ComponentLifeStage.Starting;
Startup();
DebugTools.Assert(LifeStage == ComponentLifeStage.Running, $"Component {this.GetType().Name} did not call base {nameof(Startup)} in derived method.");
}
/// <summary>
/// Increases the life stage from <see cref="ComponentLifeStage.Running" /> to <see cref="ComponentLifeStage.Stopped" />,
/// calling <see cref="Shutdown" />.
/// </summary>
/// <remarks>
/// Components are allowed to remove themselves in their own Startup function.
/// </remarks>
internal void LifeShutdown()
{
// Starting allows a component to remove itself in it's own Startup function.
DebugTools.Assert(LifeStage == ComponentLifeStage.Starting || LifeStage == ComponentLifeStage.Running);
LifeStage = ComponentLifeStage.Stopping;
Shutdown();
DebugTools.Assert(LifeStage == ComponentLifeStage.Stopped, $"Component {this.GetType().Name} did not call base {nameof(Shutdown)} in derived method.");
}
/// <summary>
/// Increases the life stage from <see cref="ComponentLifeStage.Stopped" /> to <see cref="ComponentLifeStage.Deleted" />,
/// calling <see cref="OnRemove" />.
/// </summary>
internal void LifeRemoveFromEntity()
{
// can be called at any time after PreAdd, including inside other life stage events.
DebugTools.Assert(LifeStage != ComponentLifeStage.PreAdd);
LifeStage = ComponentLifeStage.Removing;
OnRemove();
DebugTools.Assert(LifeStage == ComponentLifeStage.Deleted, $"Component {this.GetType().Name} did not call base {nameof(OnRemove)} in derived method.");
}
/// <inheritdoc />
[ViewVariables]
public bool Deleted { get; private set; }
public bool Initialized => LifeStage >= ComponentLifeStage.Initializing;
/// <inheritdoc />
[ViewVariables]
public bool Running => ComponentLifeStage.Starting <= LifeStage && LifeStage <= ComponentLifeStage.Stopping;
/// <inheritdoc />
[ViewVariables]
public bool Deleted => LifeStage >= ComponentLifeStage.Removing;
/// <inheritdoc />
[ViewVariables]
@@ -91,87 +154,56 @@ namespace Robust.Shared.GameObjects
return (EntityEventBus) Owner.EntityManager.EventBus;
}
/// <inheritdoc />
public virtual void OnRemove()
/// <summary>
/// Called when the component gets added to an entity.
/// </summary>
protected virtual void OnAdd()
{
if (Running)
throw new InvalidOperationException("Cannot Remove a running entity!");
// We have been marked for deletion by the Component Manager.
Deleted = true;
GetBus().RaiseComponentEvent(this, CompRemoveInstance);
CreationTick = Owner.EntityManager.CurrentTick;
GetBus().RaiseComponentEvent(this, CompAddInstance);
LifeStage = ComponentLifeStage.Added;
}
/// <summary>
/// Called when the component gets added to an entity.
/// Called when all of the entity's other components have been added and are available,
/// But are not necessarily initialized yet. DO NOT depend on the values of other components to be correct.
/// </summary>
public virtual void OnAdd()
protected virtual void Initialize()
{
if (Initialized)
throw new InvalidOperationException("Cannot Add an Initialized component!");
if (Running)
throw new InvalidOperationException("Cannot Add a running component!");
if (Deleted)
throw new InvalidOperationException("Cannot Add a Deleted component!");
CreationTick = Owner.EntityManager.CurrentTick;
GetBus().RaiseComponentEvent(this, CompAddInstance);
}
/// <inheritdoc />
public virtual void Initialize()
{
if (Initialized)
throw new InvalidOperationException("Component already Initialized!");
if (Running)
throw new InvalidOperationException("Cannot Initialize a running component!");
if (Deleted)
throw new InvalidOperationException("Cannot Initialize a Deleted component!");
Initialized = true;
GetBus().RaiseComponentEvent(this, CompInitInstance);
LifeStage = ComponentLifeStage.Initialized;
}
/// <summary>
/// Starts up a component. This is called automatically after all components are Initialized and the entity is Initialized.
/// This can be called multiple times during the component's life, and at any time.
/// </summary>
/// <remarks>
/// Components are allowed to remove themselves in their own Startup function.
/// </remarks>
protected virtual void Startup()
{
if (!Initialized)
throw new InvalidOperationException("Cannot Start an uninitialized component!");
if (Running)
throw new InvalidOperationException("Cannot Startup a running component!");
if (Deleted)
throw new InvalidOperationException("Cannot Start a Deleted component!");
_running = true;
GetBus().RaiseComponentEvent(this, CompStartupInstance);
LifeStage = ComponentLifeStage.Running;
}
/// <summary>
/// Shuts down the component. The is called Automatically by OnRemove. This can be called multiple times during
/// the component's life, and at any time.
/// Shuts down the component. The is called Automatically by OnRemove.
/// </summary>
protected virtual void Shutdown()
{
if (!Initialized)
throw new InvalidOperationException("Cannot Shutdown an uninitialized component!");
if (!Running)
throw new InvalidOperationException("Cannot Shutdown an unstarted component!");
if (Deleted)
throw new InvalidOperationException("Cannot Shutdown a Deleted component!");
_running = false;
GetBus().RaiseComponentEvent(this, CompShutdownInstance);
LifeStage = ComponentLifeStage.Stopped;
}
/// <summary>
/// Called when the component is removed from an entity.
/// Shuts down the component.
/// The component has already been marked as deleted in the component manager.
/// </summary>
protected virtual void OnRemove()
{
GetBus().RaiseComponentEvent(this, CompRemoveInstance);
LifeStage = ComponentLifeStage.Deleted;
}
/// <inheritdoc />
@@ -244,6 +276,67 @@ namespace Robust.Shared.GameObjects
}
}
/// <summary>
/// The life stages of an ECS component.
/// </summary>
public enum ComponentLifeStage
{
/// <summary>
/// The component has just been allocated.
/// </summary>
PreAdd = 0,
/// <summary>
/// Currently being added to an entity.
/// </summary>
Adding,
/// <summary>
/// Has been added to an entity.
/// </summary>
Added,
/// <summary>
/// Currently being initialized.
/// </summary>
Initializing,
/// <summary>
/// Has been initialized.
/// </summary>
Initialized,
/// <summary>
/// Currently being started up.
/// </summary>
Starting,
/// <summary>
/// Has started up.
/// </summary>
Running,
/// <summary>
/// Currently shutting down.
/// </summary>
Stopping,
/// <summary>
/// Has been shut down.
/// </summary>
Stopped,
/// <summary>
/// Currently being removed from it's entity.
/// </summary>
Removing,
/// <summary>
/// Removed from it's entity, and is deleted.
/// </summary>
Deleted,
}
/// <summary>
/// The component has been added to the entity. This is the first function
/// to be called after the component has been allocated and (optionally) deserialized.

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Log;
@@ -93,5 +94,21 @@ namespace Robust.Shared.GameObjects
return entity.AddComponent<T>();
}
public static IComponent SetAndDirtyIfChanged<TValue>(
this IComponent comp,
ref TValue backingField,
TValue value)
{
if (EqualityComparer<TValue>.Default.Equals(backingField, value))
{
return comp;
}
backingField = value;
comp.Dirty();
return comp;
}
}
}

View File

@@ -164,19 +164,15 @@ namespace Robust.Shared.GameObjects
_componentDependencyManager.OnComponentAdd(entity.Uid, component);
component.OnAdd();
component.LifeAddToEntity();
if (!entity.Initialized && !entity.Initializing) return;
if (!entity.Initialized && !entity.Initializing)
return;
component.Initialize();
DebugTools.Assert(component.Initialized, "Component is not initialized after calling Initialize(). "
+ "Did you forget to call base.Initialize() in an override?");
component.LifeInitialize();
if (entity.Initialized)
{
component.Running = true;
}
component.LifeStartup();
}
/// <inheritdoc />
@@ -190,14 +186,14 @@ namespace Robust.Shared.GameObjects
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent(EntityUid uid, Type type)
{
RemoveComponentDeferred((Component) GetComponent(uid, type), uid, false);
RemoveComponentDeferred((Component)GetComponent(uid, type), uid, false);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent(EntityUid uid, uint netId)
{
RemoveComponentDeferred((Component) GetComponent(uid, netId), uid, false);
RemoveComponentDeferred((Component)GetComponent(uid, netId), uid, false);
}
/// <inheritdoc />
@@ -209,7 +205,7 @@ namespace Robust.Shared.GameObjects
if (component.Owner == null || component.Owner.Uid != uid)
throw new InvalidOperationException("Component is not owned by entity.");
RemoveComponentDeferred((Component) component, uid, false);
RemoveComponentDeferred((Component)component, uid, false);
}
private static IEnumerable<Component> InSafeOrder(IEnumerable<Component> comps, bool forCreation = false)
@@ -260,23 +256,26 @@ namespace Robust.Shared.GameObjects
try
{
#endif
// these two components are required on all entities and cannot be removed normally.
if (!removeProtected && (component is ITransformComponent || component is IMetaDataComponent))
{
DebugTools.Assert("Tried to remove a protected component.");
return;
}
// these two components are required on all entities and cannot be removed normally.
if (!removeProtected && (component is ITransformComponent || component is IMetaDataComponent))
{
DebugTools.Assert("Tried to remove a protected component.");
return;
}
if (!_deleteSet.Add(component))
{
// already deferred deletion
return;
}
if (!_deleteSet.Add(component))
{
// already deferred deletion
return;
}
component.Running = false;
component.OnRemove();
_componentDependencyManager.OnComponentRemove(uid, component);
ComponentRemoved?.Invoke(this, new RemovedComponentEventArgs(component, uid));
if (component.Running)
component.LifeShutdown();
if (component.LifeStage != ComponentLifeStage.PreAdd)
component.LifeRemoveFromEntity();
_componentDependencyManager.OnComponentRemove(uid, component);
ComponentRemoved?.Invoke(this, new RemovedComponentEventArgs(component, uid));
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
@@ -300,8 +299,12 @@ namespace Robust.Shared.GameObjects
return;
}
component.Running = false;
component.OnRemove(); // Sets delete
if (component.Running)
component.LifeShutdown();
if (component.LifeStage != ComponentLifeStage.PreAdd)
component.LifeRemoveFromEntity(); // Sets delete
ComponentRemoved?.Invoke(this, new RemovedComponentEventArgs(component, component.Owner.Uid));
}
@@ -374,7 +377,7 @@ namespace Robust.Shared.GameObjects
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetComponent<T>(EntityUid uid)
{
return (T) GetComponent(uid, typeof(T));
return (T)GetComponent(uid, typeof(T));
}
/// <inheritdoc />
@@ -406,7 +409,7 @@ namespace Robust.Shared.GameObjects
{
if (!comp.Deleted)
{
component = (T) comp;
component = (T)comp;
return true;
}
}
@@ -486,7 +489,7 @@ namespace Robust.Shared.GameObjects
{
if (comp.Deleted || !includePaused && comp.Paused) continue;
yield return (T) (object) comp;
yield return (T)(object)comp;
}
}
@@ -507,7 +510,7 @@ namespace Robust.Shared.GameObjects
if (!trait2.TryGetValue(uid, out var t2Comp) || t2Comp.Deleted || !includePaused && kvComp.Value.Paused)
continue;
yield return ((TComp1) (object) kvComp.Value, (TComp2) (object) t2Comp);
yield return ((TComp1)(object)kvComp.Value, (TComp2)(object)t2Comp);
}
}
@@ -531,9 +534,9 @@ namespace Robust.Shared.GameObjects
if (!trait3.TryGetValue(uid, out var t3Comp) || t3Comp.Deleted)
continue;
yield return ((TComp1) (object) kvComp.Value,
(TComp2) (object) t2Comp,
(TComp3) (object) t3Comp);
yield return ((TComp1)(object)kvComp.Value,
(TComp2)(object)t2Comp,
(TComp3)(object)t3Comp);
}
}
@@ -562,10 +565,10 @@ namespace Robust.Shared.GameObjects
if (!trait4.TryGetValue(uid, out var t4Comp) || t4Comp.Deleted)
continue;
yield return ((TComp1) (object) kvComp.Value,
(TComp2) (object) t2Comp,
(TComp3) (object) t3Comp,
(TComp4) (object) t4Comp);
yield return ((TComp1)(object)kvComp.Value,
(TComp2)(object)t2Comp,
(TComp3)(object)t3Comp,
(TComp4)(object)t4Comp);
}
}

View File

@@ -1,45 +0,0 @@
using System;
using JetBrains.Annotations;
namespace Robust.Shared.GameObjects
{
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class RelayMovementEntityMessage : ComponentMessage
{
[PublicAPI]
public readonly IEntity Entity;
public RelayMovementEntityMessage(IEntity entity)
{
Entity = entity;
}
}
/// <summary>
/// The entity transform parent has been changed.
/// </summary>
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class ParentChangedMessage : ComponentMessage
{
/// <summary>
/// The new parent of the transform.
/// </summary>
public IEntity? NewParent { get; }
/// <summary>
/// The old parent of the transform.
/// </summary>
public IEntity? OldParent { get; }
/// <summary>
/// Constructs a new instance of <see cref="ParentChangedMessage"/>.
/// </summary>
/// <param name="newParent">The new parent of the transform.</param>
/// <param name="oldParent">The old parent of the transform.</param>
public ParentChangedMessage(IEntity? newParent, IEntity? oldParent)
{
NewParent = newParent;
OldParent = oldParent;
}
}
}

View File

@@ -62,8 +62,9 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Store the body's index within the island so we can lookup its data.
/// Key is Island's ID and value is our index.
/// </summary>
public int IslandIndex { get; set; }
public Dictionary<int, int> IslandIndex { get; set; } = new();
// TODO: Actually implement after the initial pr dummy
/// <summary>
@@ -1148,7 +1149,7 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public override void Initialize()
protected override void Initialize()
{
base.Initialize();
@@ -1253,7 +1254,7 @@ namespace Robust.Shared.GameObjects
Logger.DebugS("physics", $"Removed joint id: {joint.ID} type: {joint.GetType().Name} from {Owner}");
}
public override void OnRemove()
protected override void OnRemove()
{
base.OnRemove();
// Need to do these immediately in case collision behaviors deleted the body

View File

@@ -38,7 +38,7 @@ namespace Robust.Shared.GameObjects
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new CollisionWakeStateMessage(), false);
}
public override void OnRemove()
protected override void OnRemove()
{
base.OnRemove();
if (Owner.TryGetComponent(out IPhysBody? body))

View File

@@ -17,7 +17,7 @@ namespace Robust.Shared.GameObjects
{
public override string Name => "DebugExceptionOnAdd";
public override void OnAdd() => throw new NotSupportedException();
protected override void OnAdd() => throw new NotSupportedException();
}
/// <summary>
@@ -27,7 +27,7 @@ namespace Robust.Shared.GameObjects
{
public override string Name => "DebugExceptionInitialize";
public override void Initialize() => throw new NotSupportedException();
protected override void Initialize() => throw new NotSupportedException();
}
/// <summary>

View File

@@ -8,13 +8,13 @@ namespace Robust.Shared.GameObjects
{
public override string Name => "IgnorePause";
public override void OnAdd()
protected override void OnAdd()
{
base.OnAdd();
Owner.Paused = false;
}
public override void OnRemove()
protected override void OnRemove()
{
base.OnRemove();
if (IoCManager.Resolve<IPauseManager>().IsMapPaused(Owner.Transform.MapID))

View File

@@ -1,8 +1,8 @@
using System;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -18,6 +18,9 @@ namespace Robust.Shared.GameObjects
GridId GridIndex { get; }
IMapGrid Grid { get; }
void ClearGridId();
bool AnchorEntity(ITransformComponent transform);
void UnanchorEntity(ITransformComponent transform);
}
/// <inheritdoc cref="IMapGridComponent"/>
@@ -47,12 +50,13 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public IMapGrid Grid => _mapManager.GetGrid(_gridIndex);
/// <inheritdoc />
public void ClearGridId()
{
_gridIndex = GridId.Invalid;
}
public override void Initialize()
protected override void Initialize()
{
base.Initialize();
var mapId = Owner.Transform.MapID;
@@ -63,11 +67,54 @@ namespace Robust.Shared.GameObjects
}
}
/// <inheritdoc />
public bool AnchorEntity(ITransformComponent transform)
{
var xform = (TransformComponent) transform;
var tileIndices = Grid.TileIndicesFor(transform.Coordinates);
var result = Grid.AddToSnapGridCell(tileIndices, transform.Owner.Uid);
if (result)
{
xform.Parent = Owner.Transform;
// anchor snapping
xform.LocalPosition = Grid.GridTileToLocal(Grid.TileIndicesFor(xform.LocalPosition)).Position;
xform.SetAnchored(result);
if (xform.Owner.TryGetComponent<PhysicsComponent>(out var physicsComponent))
{
physicsComponent.BodyType = BodyType.Static;
}
}
return result;
}
/// <inheritdoc />
public void UnanchorEntity(ITransformComponent transform)
{
//HACK: Client grid pivot causes this.
//TODO: make grid components the actual grid
if(GridIndex == GridId.Invalid)
return;
var xform = (TransformComponent)transform;
var tileIndices = Grid.TileIndicesFor(transform.Coordinates);
Grid.RemoveFromSnapGridCell(tileIndices, transform.Owner.Uid);
xform.SetAnchored(false);
if (xform.Owner.TryGetComponent<PhysicsComponent>(out var physicsComponent))
{
physicsComponent.BodyType = BodyType.Dynamic;
}
}
/// <param name="player"></param>
/// <inheritdoc />
public override ComponentState GetComponentState(ICommonSession player)
{
return new MapGridComponentState(_gridIndex, Grid.HasGravity);
return new MapGridComponentState(_gridIndex);
}
/// <inheritdoc />
@@ -75,11 +122,10 @@ namespace Robust.Shared.GameObjects
{
base.HandleComponentState(curState, nextState);
if (!(curState is MapGridComponentState state))
if (curState is not MapGridComponentState state)
return;
_gridIndex = state.GridIndex;
Grid.HasGravity = state.HasGravity;
}
}
@@ -94,17 +140,14 @@ namespace Robust.Shared.GameObjects
/// </summary>
public GridId GridIndex { get; }
public bool HasGravity { get; }
/// <summary>
/// Constructs a new instance of <see cref="MapGridComponentState"/>.
/// </summary>
/// <param name="gridIndex">Index of the grid this component is linked to.</param>
public MapGridComponentState(GridId gridIndex, bool hasGravity)
public MapGridComponentState(GridId gridIndex)
: base(NetIDs.MAP_GRID)
{
GridIndex = gridIndex;
HasGravity = hasGravity;
}
}
}

View File

@@ -1,24 +1,14 @@
using Robust.Shared.Map;
using Robust.Shared.Maths;
using System;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// Makes it possible to look this entity up with the snap grid.
/// </summary>
[Obsolete("Use Transform.Anchored instead of this flag component.")]
internal class SnapGridComponent : Component
{
/// <inheritdoc />
public sealed override string Name => "SnapGrid";
/// <summary>
/// GridId the last time this component was moved.
/// </summary>
internal GridId LastGrid = GridId.Invalid;
/// <summary>
/// TileIndices the last time this component was moved.
/// </summary>
internal Vector2i LastTileIndices;
}
}

View File

@@ -26,6 +26,8 @@ namespace Robust.Shared.GameObjects
private Angle _localRotation; // local rotation
[DataField("noRot")]
private bool _noLocalRotation;
[DataField("anchored")]
private bool _anchored;
private Matrix3 _localMatrix = Matrix3.Identity;
private Matrix3 _invLocalMatrix = Matrix3.Identity;
@@ -160,6 +162,11 @@ namespace Robust.Shared.GameObjects
get => !_parent.IsValid() ? null : Owner.EntityManager.GetEntity(_parent).Transform;
set
{
if (_anchored && value?.Owner.Uid != _parent)
{
Anchored = false;
}
if (value != null)
{
AttachParent(value);
@@ -212,6 +219,7 @@ namespace Robust.Shared.GameObjects
}
}
[Obsolete("Use ContainerHelper to check if this entity is inside a container.")]
public bool IsMapTransform => !Owner.IsInContainer();
/// <inheritdoc />
@@ -261,6 +269,10 @@ namespace Robust.Shared.GameObjects
// NOTE: This setter must be callable from before initialize (inheriting from AttachParent's note)
set
{
// unless the parent is changing, nothing to do here
if(value.EntityId == _parent && _anchored)
return;
var oldPosition = Coordinates;
_localPosition = value.Position;
@@ -268,6 +280,9 @@ namespace Robust.Shared.GameObjects
if (value.EntityId != _parent)
{
if(_parent != EntityUid.Invalid) // Allow setting Transform.Parent in Prototypes
Anchored = false; // changing the parent un-anchors the entity
changedParent = true;
var newParentEnt = Owner.EntityManager.GetEntity(value.EntityId);
var newParent = newParentEnt.Transform;
@@ -284,16 +299,11 @@ namespace Robust.Shared.GameObjects
var newConcrete = (TransformComponent) newParent;
newConcrete._children.Add(uid);
var oldParentOwner = oldParent?.Owner;
var entMessage = new EntParentChangedMessage(Owner, oldParentOwner);
var compMessage = new ParentChangedMessage(newParentEnt, oldParentOwner);
// offset position from world to parent
_parent = newParentEnt.Uid;
ChangeMapId(newConcrete.MapID);
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, entMessage);
Owner.SendMessage(this, compMessage);
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntParentChangedMessage(Owner, oldParent?.Owner));
GridID = GetGridIndex();
}
@@ -310,7 +320,8 @@ namespace Robust.Shared.GameObjects
//TODO: This is a hack, look into WHY we can't call GridPosition before the comp is Running
if (Running)
{
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new MoveEvent(Owner, oldPosition, Coordinates));
if(!oldPosition.Position.Equals(Coordinates.Position))
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new MoveEvent(Owner, oldPosition, Coordinates));
}
}
else
@@ -330,6 +341,9 @@ namespace Robust.Shared.GameObjects
get => _localPosition;
set
{
if(Anchored)
return;
if (_localPosition.EqualsApprox(value, 0.00001))
return;
@@ -353,17 +367,38 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[ViewVariables]
[ViewVariables(VVAccess.ReadWrite)]
public bool Anchored
{
get => Owner.HasComponent<SnapGridComponent>();
get => _anchored;
set
{
if(value && !Owner.HasComponent<SnapGridComponent>())
Owner.AddComponent<SnapGridComponent>();
else if(!value && Owner.HasComponent<SnapGridComponent>())
Owner.RemoveComponent<SnapGridComponent>();
// This will be set again when the transform starts, actually anchoring it.
if (LifeStage < ComponentLifeStage.Starting)
{
_anchored = value;
}
else if (LifeStage == ComponentLifeStage.Starting)
{
if (value && _mapManager.TryFindGridAt(MapPosition, out var grid))
{
_anchored = Owner.EntityManager.GetEntity(grid.GridEntityId).GetComponent<IMapGridComponent>().AnchorEntity(this);
}
}
else if (value && !_anchored && _mapManager.TryFindGridAt(MapPosition, out var grid))
{
_anchored = Owner.EntityManager.GetEntity(grid.GridEntityId).GetComponent<IMapGridComponent>().AnchorEntity(this);
}
else if (!value && _anchored)
{
// An anchored entity is always parented to the grid.
// If Transform.Anchored is true in the prototype but the entity was not spawned with a grid as the parent,
// then this will be false.
if (Owner.EntityManager.ComponentManager.TryGetComponent<IMapGridComponent>(ParentUid, out var gridComp))
gridComp.UnanchorEntity(this);
else
SetAnchored(false);
}
}
}
@@ -404,7 +439,7 @@ namespace Robust.Shared.GameObjects
[ViewVariables] public EntityUid LerpParent { get; private set; }
/// <inheritdoc />
public override void Initialize()
protected override void Initialize()
{
base.Initialize();
@@ -482,6 +517,10 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
protected override void Startup()
{
// Re-Anchor the entity if needed.
if (_anchored)
Anchored = true;
base.Startup();
// Keep the cached matrices in sync with the fields.
@@ -568,17 +607,14 @@ namespace Robust.Shared.GameObjects
return;
}
Anchored = false;
var oldConcrete = (TransformComponent) oldParent;
var uid = Owner.Uid;
oldConcrete._children.Remove(uid);
var oldParentOwner = oldParent?.Owner;
var entMessage = new EntParentChangedMessage(Owner, oldParentOwner);
var compMessage = new ParentChangedMessage(null, oldParentOwner);
_parent = EntityUid.Invalid;
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, entMessage);
Owner.SendMessage(this, compMessage);
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntParentChangedMessage(Owner, oldParent?.Owner));
var oldMapId = MapID;
MapID = MapId.Nullspace;
@@ -687,7 +723,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public override ComponentState GetComponentState(ICommonSession player)
{
return new TransformComponentState(_localPosition, LocalRotation, _parent, _noLocalRotation);
return new TransformComponentState(_localPosition, LocalRotation, _parent, _noLocalRotation, _anchored);
}
/// <inheritdoc />
@@ -737,6 +773,16 @@ namespace Robust.Shared.GameObjects
_prevPosition = newState.LocalPosition;
_prevRotation = newState.Rotation;
Anchored = newState.Anchored;
// This is not possible, because client entities don't exist on the server, so the parent HAS to be a shared entity.
// If this assert fails, the code above that sets the parent is broken.
DebugTools.Assert(!_parent.IsClientSide(), "Transform received a state, but is still parented to a client entity.");
// Whatever happened on the client, these should still be correct
DebugTools.Assert(ParentUid == newState.ParentID);
DebugTools.Assert(Anchored == newState.Anchored);
if (rebuildMatrices)
{
RebuildMatrices();
@@ -837,6 +883,11 @@ namespace Robust.Shared.GameObjects
/// </summary>
public readonly bool NoLocalRotation;
/// <summary>
/// True if the transform is anchored to a tile.
/// </summary>
public readonly bool Anchored;
/// <summary>
/// Constructs a new state snapshot of a TransformComponent.
/// </summary>
@@ -844,15 +895,24 @@ namespace Robust.Shared.GameObjects
/// <param name="rotation">Current direction offset of this entity.</param>
/// <param name="parentId">Current parent transform of this entity.</param>
/// <param name="noLocalRotation"></param>
public TransformComponentState(Vector2 localPosition, Angle rotation, EntityUid parentId, bool noLocalRotation)
public TransformComponentState(Vector2 localPosition, Angle rotation, EntityUid parentId, bool noLocalRotation, bool anchored)
: base(NetIDs.TRANSFORM)
{
LocalPosition = localPosition;
Rotation = rotation;
ParentID = parentId;
NoLocalRotation = noLocalRotation;
Anchored = anchored;
}
}
public void SetAnchored(bool value)
{
_anchored = value;
Dirty();
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new AnchorStateChangedEvent());
}
}
/// <summary>
@@ -900,4 +960,9 @@ namespace Robust.Shared.GameObjects
/// </summary>
public Box2? WorldAABB { get; }
}
/// <summary>
/// Raised when the Anchor state of the transform is changed.
/// </summary>
public class AnchorStateChangedEvent : EntityEventArgs { }
}

View File

@@ -84,6 +84,7 @@ namespace Robust.Shared.GameObjects
return;
_paused = value;
EntityManager.EventBus.RaiseLocalEvent(Uid, new EntityPausedEvent(Uid, value));
}
}
@@ -134,13 +135,13 @@ namespace Robust.Shared.GameObjects
_ => int.MaxValue
});
foreach (var comp in components)
foreach (var component in components)
{
if (comp == null || comp.Initialized) continue;
var comp = (Component) component;
if (comp.Initialized)
continue;
comp.Initialize();
DebugTools.Assert(comp.Initialized, $"Component {comp.Name} did not call base {nameof(comp.Initialize)} in derived method.");
comp.LifeInitialize();
}
#if DEBUG
@@ -173,11 +174,12 @@ namespace Robust.Shared.GameObjects
_ => int.MaxValue
});
foreach (var comp in comps)
foreach (var component in comps)
{
if (comp != null && comp.Initialized && !comp.Deleted)
var comp = (Component) component;
if (comp.LifeStage == ComponentLifeStage.Initialized)
{
comp.Running = true;
comp.LifeStartup();
}
}
}

View File

@@ -24,6 +24,15 @@ namespace Robust.Shared.GameObjects
void SubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber,
EntityEventHandler<T> eventHandler) where T : notnull;
void SubscribeEvent<T>(
EventSource source,
IEntityEventSubscriber subscriber,
EntityEventHandler<T> eventHandler,
Type orderType,
Type[]? before=null,
Type[]? after=null)
where T : notnull;
/// <summary>
/// Unsubscribes all event handlers of a given type.
/// </summary>
@@ -137,7 +146,7 @@ namespace Robust.Shared.GameObjects
return;
// UnsubscribeEvent modifies _inverseEventSubscriptions, requires val to be cached
foreach (var (type, (source, originalHandler, handler)) in val.ToList())
foreach (var (type, (source, originalHandler, handler, _)) in val.ToList())
{
UnsubscribeEvent(source, type, originalHandler, handler, subscriber);
}
@@ -154,7 +163,36 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public void SubscribeEvent<T>(EventSource source, IEntityEventSubscriber subscriber, EntityEventHandler<T> eventHandler) where T : notnull
public void SubscribeEvent<T>(
EventSource source,
IEntityEventSubscriber subscriber,
EntityEventHandler<T> eventHandler)
where T : notnull
{
SubscribeEventCommon(source, subscriber, eventHandler, null);
}
public void SubscribeEvent<T>(
EventSource source,
IEntityEventSubscriber subscriber,
EntityEventHandler<T> eventHandler,
Type orderType,
Type[]? before=null,
Type[]? after=null)
where T : notnull
{
var order = new OrderingData(orderType, before, after);
SubscribeEventCommon(source, subscriber, eventHandler, order);
HandleOrderRegistration(typeof(T), order);
}
private void SubscribeEventCommon<T>(
EventSource source,
IEntityEventSubscriber subscriber,
EntityEventHandler<T> eventHandler,
OrderingData? order)
where T : notnull
{
if (source == EventSource.None)
throw new ArgumentOutOfRangeException(nameof(source));
@@ -166,7 +204,7 @@ namespace Robust.Shared.GameObjects
throw new ArgumentNullException(nameof(subscriber));
var eventType = typeof(T);
var subscriptionTuple = new Registration(source, eventHandler, ev => eventHandler((T) ev), eventHandler);
var subscriptionTuple = new Registration(source, eventHandler, ev => eventHandler((T) ev), eventHandler, order);
if (!_eventSubscriptions.TryGetValue(eventType, out var subscriptions))
_eventSubscriptions.Add(eventType, new List<Registration> {subscriptionTuple});
else if (!subscriptions.Any(p => p.Mask == source && p.Original == (Delegate) eventHandler))
@@ -184,7 +222,6 @@ namespace Robust.Shared.GameObjects
inverseSubscription
);
}
else if (!inverseSubscription.ContainsKey(eventType))
{
inverseSubscription.Add(eventType, subscriptionTuple);
@@ -290,15 +327,13 @@ namespace Robust.Shared.GameObjects
});
}
_awaitingMessages.Add(type, (source, reg, tcs));
return tcs.Task;
}
private void UnsubscribeEvent(EventSource source, Type eventType, Delegate originalHandler, EventHandler handler, IEntityEventSubscriber subscriber)
{
var tuple = new Registration(source, originalHandler, handler, originalHandler);
var tuple = new Registration(source, originalHandler, handler, originalHandler, null);
if (_eventSubscriptions.TryGetValue(eventType, out var subscriptions) && subscriptions.Contains(tuple))
subscriptions.Remove(tuple);
@@ -310,7 +345,11 @@ namespace Robust.Shared.GameObjects
{
var eventType = eventArgs.GetType();
if (_eventSubscriptions.TryGetValue(eventType, out var subs))
if (_orderedEvents.Contains(eventType))
{
ProcessSingleEventOrdered(source, eventArgs, eventType);
}
else if (_eventSubscriptions.TryGetValue(eventType, out var subs))
{
foreach (var handler in subs)
{
@@ -319,25 +358,20 @@ namespace Robust.Shared.GameObjects
}
}
if (_awaitingMessages.TryGetValue(eventType, out var awaiting))
{
var (mask, _, tcs) = awaiting;
if ((source & mask) != 0)
{
tcs.TrySetResult(eventArgs);
_awaitingMessages.Remove(eventType);
}
}
ProcessAwaitingMessages(source, eventArgs, eventType);
}
private void ProcessSingleEvent<T>(EventSource source, T eventArgs) where T : notnull
{
var eventType = typeof(T);
if (_eventSubscriptions.TryGetValue(eventType, out var subs))
if (_orderedEvents.Contains(eventType))
{
foreach (var (mask, originalHandler, _) in subs)
ProcessSingleEventOrdered(source, eventArgs, eventType);
}
else if (_eventSubscriptions.TryGetValue(eventType, out var subs))
{
foreach (var (mask, originalHandler, _, _) in subs)
{
if ((mask & source) != 0)
{
@@ -347,6 +381,13 @@ namespace Robust.Shared.GameObjects
}
}
ProcessAwaitingMessages(source, eventArgs, eventType);
}
// Generic here so we can avoid boxing alloc unless actually awaiting.
private void ProcessAwaitingMessages<T>(EventSource source, T eventArgs, Type eventType)
where T : notnull
{
if (_awaitingMessages.TryGetValue(eventType, out var awaiting))
{
var (mask1, _, tcs) = awaiting;
@@ -366,13 +407,20 @@ namespace Robust.Shared.GameObjects
public readonly Delegate Original;
public readonly EventHandler Handler;
public readonly OrderingData? Ordering;
public Registration(EventSource mask, Delegate original, EventHandler handler, object equalityToken)
public Registration(
EventSource mask,
Delegate original,
EventHandler handler,
object equalityToken,
OrderingData? ordering)
{
Mask = mask;
Original = original;
Handler = handler;
EqualityToken = equalityToken;
Ordering = ordering;
}
public bool Equals(Registration other)
@@ -403,11 +451,16 @@ namespace Robust.Shared.GameObjects
return !left.Equals(right);
}
public void Deconstruct(out EventSource mask, out Delegate originalHandler, out EventHandler handler)
public void Deconstruct(
out EventSource mask,
out Delegate originalHandler,
out EventHandler handler,
out OrderingData? order)
{
mask = Mask;
originalHandler = Original;
handler = Handler;
order = Ordering;
}
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace Robust.Shared.GameObjects
{
@@ -14,6 +16,12 @@ namespace Robust.Shared.GameObjects
where TComp : IComponent
where TEvent : EntityEventArgs;
void SubscribeLocalEvent<TComp, TEvent>(
ComponentEventHandler<TComp, TEvent> handler,
Type orderType, Type[]? before=null, Type[]? after=null)
where TComp : IComponent
where TEvent : EntityEventArgs;
[Obsolete("Use the overload without the handler argument.")]
void UnsubscribeLocalEvent<TComp, TEvent>(ComponentEventHandler<TComp, TEvent> handler)
where TComp : IComponent
@@ -61,6 +69,12 @@ namespace Robust.Shared.GameObjects
public void RaiseLocalEvent<TEvent>(EntityUid uid, TEvent args, bool broadcast = true)
where TEvent : EntityEventArgs
{
if (_orderedEvents.Contains(typeof(TEvent)))
{
RaiseLocalOrdered(uid, args, broadcast);
return;
}
_eventTables.Dispatch(uid, typeof(TEvent), args);
// we also broadcast it so the call site does not have to.
@@ -76,7 +90,24 @@ namespace Robust.Shared.GameObjects
void EventHandler(EntityUid uid, IComponent comp, EntityEventArgs args)
=> handler(uid, (TComp) comp, (TEvent) args);
_eventTables.Subscribe(typeof(TComp), typeof(TEvent), EventHandler);
_eventTables.Subscribe(typeof(TComp), typeof(TEvent), EventHandler, null);
}
public void SubscribeLocalEvent<TComp, TEvent>(
ComponentEventHandler<TComp, TEvent> handler,
Type orderType,
Type[]? before=null,
Type[]? after=null)
where TComp : IComponent
where TEvent : EntityEventArgs
{
void EventHandler(EntityUid uid, IComponent comp, EntityEventArgs args)
=> handler(uid, (TComp) comp, (TEvent) args);
var orderData = new OrderingData(orderType, before, after);
_eventTables.Subscribe(typeof(TComp), typeof(TEvent), EventHandler, orderData);
HandleOrderRegistration(typeof(TEvent), orderData);
}
/// <inheritdoc />
@@ -105,7 +136,7 @@ namespace Robust.Shared.GameObjects
private Dictionary<EntityUid, Dictionary<Type, HashSet<Type>>> _eventTables;
// EventType -> CompType -> Handler
private Dictionary<Type, Dictionary<Type, DirectedEventHandler>> _subscriptions;
private Dictionary<Type, Dictionary<Type, (DirectedEventHandler handler, OrderingData? ordering)>> _subscriptions;
// prevents shitcode, get your subscriptions figured out before you start spawning entities
private bool _subscriptionLock;
@@ -148,25 +179,21 @@ namespace Robust.Shared.GameObjects
RemoveComponent(e.OwnerUid, e.Component.GetType());
}
public void Subscribe(Type compType, Type eventType, DirectedEventHandler handler)
public void Subscribe(Type compType, Type eventType, DirectedEventHandler handler, OrderingData? order)
{
if (_subscriptionLock)
throw new InvalidOperationException("Subscription locked.");
if (!_subscriptions.TryGetValue(compType, out var compSubs))
{
compSubs = new Dictionary<Type, DirectedEventHandler>();
compSubs = new Dictionary<Type, (DirectedEventHandler, OrderingData?)>();
_subscriptions.Add(compType, compSubs);
compSubs.Add(eventType, handler);
}
else
{
if (compSubs.ContainsKey(eventType))
throw new InvalidOperationException($"Duplicate Subscriptions for comp={compType.Name}, event={eventType.Name}");
compSubs.Add(eventType, handler);
}
if (compSubs.ContainsKey(eventType))
throw new InvalidOperationException($"Duplicate Subscriptions for comp={compType.Name}, event={eventType.Name}");
compSubs.Add(eventType, (handler, order));
}
public void Unsubscribe(Type compType, Type eventType)
@@ -196,7 +223,8 @@ namespace Robust.Shared.GameObjects
{
var eventTable = _eventTables[euid];
foreach (var type in GetReferences(compType))
var enumerator = GetReferences(compType);
while (enumerator.MoveNext(out var type))
{
if (!_subscriptions.TryGetValue(type, out var compSubs))
continue;
@@ -218,7 +246,8 @@ namespace Robust.Shared.GameObjects
{
var eventTable = _eventTables[euid];
foreach (var type in GetReferences(compType))
var enumerator = GetReferences(compType);
while (enumerator.MoveNext(out var type))
{
if (!_subscriptions.TryGetValue(type, out var compSubs))
continue;
@@ -245,25 +274,53 @@ namespace Robust.Shared.GameObjects
if(!_subscriptions.TryGetValue(compType, out var compSubs))
return;
if(!compSubs.TryGetValue(eventType, out var handler))
if(!compSubs.TryGetValue(eventType, out var sub))
return;
var (handler, _) = sub;
var component = _entMan.ComponentManager.GetComponent(euid, compType);
handler(euid, component, args);
}
}
public void CollectOrdered(
EntityUid euid,
Type eventType,
List<(EventHandler, OrderingData?)> found)
{
var eventTable = _eventTables[euid];
if(!eventTable.TryGetValue(eventType, out var subscribedComps))
return;
foreach (var compType in subscribedComps)
{
if(!_subscriptions.TryGetValue(compType, out var compSubs))
return;
if(!compSubs.TryGetValue(eventType, out var sub))
return;
var (handler, order) = sub;
var component = _entMan.ComponentManager.GetComponent(euid, compType);
found.Add((ev => handler(euid, component, (EntityEventArgs) ev), order));
}
}
public void DispatchComponent(EntityUid euid, IComponent component, Type eventType, EntityEventArgs args)
{
foreach (var type in GetReferences(component.GetType()))
var enumerator = GetReferences(component.GetType());
while (enumerator.MoveNext(out var type))
{
if (!_subscriptions.TryGetValue(type, out var compSubs))
continue;
if (!compSubs.TryGetValue(eventType, out var handler))
if (!compSubs.TryGetValue(eventType, out var sub))
continue;
var (handler, _) = sub;
handler(euid, component, args);
}
}
@@ -294,9 +351,49 @@ namespace Robust.Shared.GameObjects
_subscriptions = null!;
}
private IEnumerable<Type> GetReferences(Type type)
/// <summary>
/// Enumerates the type's component references, returning the type itself last.
/// </summary>
private ReferencesEnumerator GetReferences(Type type)
{
return _comFac.GetRegistration(type).References;
return new(type, _comFac.GetRegistration(type).References);
}
private struct ReferencesEnumerator
{
private readonly Type _baseType;
private readonly IReadOnlyList<Type> _list;
private readonly int _totalLength;
private int _idx;
public ReferencesEnumerator(Type baseType, IReadOnlyList<Type> list)
{
_baseType = baseType;
_list = list;
_totalLength = list.Count;
_idx = 0;
}
public bool MoveNext([NotNullWhen(true)] out Type? type)
{
if (_idx >= _totalLength)
{
if (_idx++ == _totalLength)
{
type = _baseType;
return true;
}
type = null;
return false;
}
type = _list[_idx++];
if (type == _baseType)
return MoveNext(out type);
return true;
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
{
internal partial class EntityEventBus
{
// TODO: Topological sort is currently done every time an event is emitted.
// This should be fine for low-volume stuff like interactions, but definitely not for anything high volume.
// Not sure if we could pre-cache the topological sort, here.
// Ordered event raising is slow so if this event has any ordering dependencies we use a slower path.
private readonly HashSet<Type> _orderedEvents = new();
private void ProcessSingleEventOrdered(EventSource source, object eventArgs, Type eventType)
{
var found = new List<(EventHandler, OrderingData?)>();
CollectBroadcastOrdered(source, eventType, found);
DispatchOrderedEvents(eventArgs, found);
}
private void CollectBroadcastOrdered(
EventSource source,
Type eventType,
List<(EventHandler, OrderingData?)> found)
{
if (!_eventSubscriptions.TryGetValue(eventType, out var subs))
return;
foreach (var handler in subs)
{
if ((handler.Mask & source) != 0)
found.Add((handler.Handler, handler.Ordering));
}
}
private void RaiseLocalOrdered<TEvent>(
EntityUid uid,
TEvent args,
bool broadcast)
where TEvent : EntityEventArgs
{
var found = new List<(EventHandler, OrderingData?)>();
if (broadcast)
CollectBroadcastOrdered(EventSource.Local, typeof(TEvent), found);
_eventTables.CollectOrdered(uid, typeof(TEvent), found);
DispatchOrderedEvents(args, found);
if (broadcast)
ProcessAwaitingMessages(EventSource.Local, args, typeof(TEvent));
}
private static void DispatchOrderedEvents(object eventArgs, List<(EventHandler, OrderingData?)> found)
{
var nodes = TopologicalSort.FromBeforeAfter(
found.Where(f => f.Item2 != null),
n => n.Item2!.OrderType,
n => n.Item1!,
n => n.Item2!.Before ?? Array.Empty<Type>(),
n => n.Item2!.After ?? Array.Empty<Type>(),
allowMissing: true);
foreach (var handler in TopologicalSort.Sort(nodes))
{
handler(eventArgs);
}
// Go over all handlers that don't have ordering so weren't included in the sort.
foreach (var (handler, orderData) in found)
{
if (orderData == null)
handler(eventArgs);
}
}
private void HandleOrderRegistration(Type eventType, OrderingData? data)
{
if (data == null)
return;
if (data.Before != null || data.After != null)
_orderedEvents.Add(eventType);
}
private sealed record OrderingData(Type OrderType, Type[]? Before, Type[]? After);
}
}

View File

@@ -1,5 +1,8 @@
namespace Robust.Shared.GameObjects
{
/// <summary>
/// The life stages of an ECS Entity.
/// </summary>
public enum EntityLifeStage
{
/// <summary>

View File

@@ -1,14 +1,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Prometheus;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -79,7 +76,6 @@ namespace Robust.Shared.GameObjects
_eventBus = new EntityEventBus(this);
ComponentManager.Initialize();
_componentManager.ComponentRemoved += (sender, args) => _eventBus.UnsubscribeEvents(args.Component);
}
public virtual void Startup()
@@ -158,9 +154,8 @@ namespace Robust.Shared.GameObjects
{
var newEntity = CreateEntity(prototypeName);
if (TryGetEntity(coordinates.EntityId, out var entity))
if (coordinates.IsValid(this))
{
newEntity.Transform.AttachParent(entity);
newEntity.Transform.Coordinates = coordinates;
}
@@ -229,12 +224,7 @@ namespace Robust.Shared.GameObjects
return false;
}
[Obsolete("IEntityQuery is obsolete")]
public IEnumerable<IEntity> GetEntities(IEntityQuery query)
{
return query.Match(this);
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntities()
{
// Need to do an iterator loop to avoid issues with concurrent access.

View File

@@ -1,178 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// An entity query that will let all entities pass.
/// This is the same as matching <c>ITransformComponent</c>, but faster.
/// </summary>
[PublicAPI, Obsolete]
public class AllEntityQuery : IEntityQuery
{
/// <inheritdoc />
public bool Match(IEntity entity) => true;
/// <inheritdoc />
public IEnumerable<IEntity> Match(IEntityManager entityMan)
{
return entityMan.GetEntities();
}
}
/// <summary>
/// An entity query which will match entities based on a predicate.
/// If you only want a single type of Component, use <c>TypeEntityQuery</c>.
/// </summary>
[PublicAPI, Obsolete]
public class PredicateEntityQuery : IEntityQuery
{
private readonly Predicate<IEntity> Predicate;
/// <summary>
/// Constructs a new instance of <c>PredicateEntityQuery</c>.
/// </summary>
/// <param name="predicate"></param>
public PredicateEntityQuery(Predicate<IEntity> predicate)
{
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
Predicate = predicate;
}
/// <inheritdoc />
public bool Match(IEntity entity) => Predicate(entity);
/// <inheritdoc />
public IEnumerable<IEntity> Match(IEntityManager entityMan)
{
return entityMan.GetEntities().Where(entity => Predicate(entity));
}
}
/// <summary>
/// An entity query that will match one type of component.
/// This the fastest and most common query, and should be the default choice.
/// </summary>
[PublicAPI, Obsolete]
public class TypeEntityQuery : IEntityQuery
{
private readonly Type ComponentType;
/// <summary>
/// Constructs a new instance of <c>TypeEntityQuery</c>.
/// </summary>
/// <param name="componentType">Type of the component to match.</param>
public TypeEntityQuery(Type componentType)
{
DebugTools.Assert(typeof(IComponent).IsAssignableFrom(componentType), "componentType must inherit IComponent");
ComponentType = componentType;
}
/// <inheritdoc />
public bool Match(IEntity entity) => entity.HasComponent(ComponentType);
/// <inheritdoc />
public IEnumerable<IEntity> Match(IEntityManager entityMan)
{
return entityMan.ComponentManager.GetAllComponents(ComponentType, true).Select(component => component.Owner);
}
}
/// <summary>
/// An entity query that will match one type of component.
/// This the fastest and most common query, and should be the default choice.
/// </summary>
/// <typeparamref name="T">Type of component to match.</typeparamref>
[PublicAPI, Obsolete]
public class TypeEntityQuery<T> : IEntityQuery where T : IComponent
{
public bool Match(IEntity entity) => entity.HasComponent<T>();
public IEnumerable<IEntity> Match(IEntityManager entityMan)
{
return entityMan.ComponentManager.EntityQuery<T>(true).Select(component => component.Owner);
}
}
/// <summary>
/// An entity query that will match all entities that intersect with the argument entity.
/// </summary>
[PublicAPI, Obsolete]
public class IntersectingEntityQuery : IEntityQuery
{
private readonly IEntity Entity;
/// <summary>
/// Constructs a new instance of <c>TypeEntityQuery</c>.
/// </summary>
/// <param name="componentType">Type of the component to match.</param>
public IntersectingEntityQuery(IEntity entity)
{
Entity = entity;
}
/// <inheritdoc />
public bool Match(IEntity entity)
{
if(Entity.TryGetComponent<IPhysBody>(out var physics))
{
return physics.Owner.Transform.MapID == entity.Transform.MapID && physics.GetWorldAABB().Contains(entity.Transform.WorldPosition);
}
return false;
}
public IEnumerable<IEntity> Match(IEntityManager entityMan)
{
return entityMan.GetEntities().Where(entity => Match(entity));
}
}
/// <summary>
/// An entity query that will match entities that have all of the provided components.
/// </summary>
[PublicAPI, Obsolete]
public class MultipleTypeEntityQuery : IEntityQuery
{
private readonly List<Type> ComponentTypes;
/// <summary>
/// Constructs a new instance of <c>MultipleTypeEntityQuery</c>.
/// </summary>
/// <param name="componentTypes">List of component types to match.</param>
public MultipleTypeEntityQuery(List<Type> componentTypes)
{
foreach (var componentType in componentTypes)
{
DebugTools.Assert(typeof(IComponent).IsAssignableFrom(componentType), "componentType must inherit IComponent");
}
ComponentTypes = componentTypes;
}
/// <inheritdoc />
public bool Match(IEntity entity)
{
foreach (var componentType in ComponentTypes)
{
if (!entity.HasComponent(componentType))
{
return false;
}
}
return true;
}
/// <inheritdoc />
public IEnumerable<IEntity> Match(IEntityManager entityMan)
{
return entityMan.GetEntities(new TypeEntityQuery(ComponentTypes.First())).Where(entity => Match(entity));
}
}
}

View File

@@ -7,16 +7,22 @@ namespace Robust.Shared.GameObjects
{
private List<SubBase>? _subscriptions;
protected void SubscribeNetworkEvent<T>(EntityEventHandler<T> handler)
// NOTE: EntityEventHandler<T> and EntitySessionEventHandler<T> CANNOT BE ORDERED BETWEEN EACH OTHER.
protected void SubscribeNetworkEvent<T>(
EntityEventHandler<T> handler,
Type[]? before=null, Type[]? after=null)
where T : notnull
{
EntityManager.EventBus.SubscribeEvent(EventSource.Network, this, handler);
EntityManager.EventBus.SubscribeEvent(EventSource.Network, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<T>(EventSource.Network));
}
protected void SubscribeNetworkEvent<T>(EntitySessionEventHandler<T> handler)
protected void SubscribeNetworkEvent<T>(
EntitySessionEventHandler<T> handler,
Type[]? before=null, Type[]? after=null)
where T : notnull
{
EntityManager.EventBus.SubscribeSessionEvent(EventSource.Network, this, handler);
@@ -25,16 +31,20 @@ namespace Robust.Shared.GameObjects
_subscriptions.Add(new SubBroadcast<EntitySessionMessage<T>>(EventSource.Network));
}
protected void SubscribeLocalEvent<T>(EntityEventHandler<T> handler)
protected void SubscribeLocalEvent<T>(
EntityEventHandler<T> handler,
Type[]? before=null, Type[]? after=null)
where T : notnull
{
EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, handler);
EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<T>(EventSource.Local));
}
protected void SubscribeLocalEvent<T>(EntitySessionEventHandler<T> handler)
protected void SubscribeLocalEvent<T>(
EntitySessionEventHandler<T> handler,
Type[]? before=null, Type[]? after=null)
where T : notnull
{
EntityManager.EventBus.SubscribeSessionEvent(EventSource.Local, this, handler);
@@ -57,8 +67,9 @@ namespace Robust.Shared.GameObjects
EntityManager.EventBus.UnsubscribeEvent<T>(EventSource.Local, this);
}
protected void SubscribeLocalEvent<TComp, TEvent>(ComponentEventHandler<TComp, TEvent> handler)
protected void SubscribeLocalEvent<TComp, TEvent>(
ComponentEventHandler<TComp, TEvent> handler,
Type[]? before=null, Type[]? after=null)
where TComp : IComponent
where TEvent : EntityEventArgs
{

View File

@@ -114,7 +114,7 @@ namespace Robust.Shared.GameObjects
{
Logger.DebugS("go.sys", "Initializing entity system {0}", type);
// Force IoC inject of all systems
var instance = _typeFactory.CreateInstanceUnchecked<IEntitySystem>(type);
var instance = _typeFactory.CreateInstanceUnchecked<IEntitySystem>(type, oneOff: true);
_systems.Add(type, instance);
@@ -166,15 +166,15 @@ namespace Robust.Shared.GameObjects
Dictionary<Type, IEntitySystem>.ValueCollection systems,
Dictionary<Type, IEntitySystem> supertypeSystems)
{
var allNodes = new List<GraphNode<IEntitySystem>>();
var typeToNode = new Dictionary<Type, GraphNode<IEntitySystem>>();
var allNodes = new List<TopologicalSort.GraphNode<IEntitySystem>>();
var typeToNode = new Dictionary<Type, TopologicalSort.GraphNode<IEntitySystem>>();
foreach (var system in systems)
{
var node = new GraphNode<IEntitySystem>(system);
var node = new TopologicalSort.GraphNode<IEntitySystem>(system);
allNodes.Add(node);
typeToNode.Add(system.GetType(), node);
allNodes.Add(node);
}
foreach (var (type, system) in supertypeSystems)
@@ -183,52 +183,30 @@ namespace Robust.Shared.GameObjects
typeToNode[type] = node;
}
foreach (var node in allNodes)
foreach (var node in typeToNode.Values)
{
foreach (var after in node.System.UpdatesAfter)
foreach (var after in node.Value.UpdatesAfter)
{
var system = typeToNode[after];
node.DependsOn.Add(system);
system.Dependant.Add(node);
}
foreach (var before in node.System.UpdatesBefore)
foreach (var before in node.Value.UpdatesBefore)
{
var system = typeToNode[before];
system.DependsOn.Add(node);
node.Dependant.Add(system);
}
}
var order = TopologicalSort(allNodes).Select(p => p.System).ToArray();
var order = TopologicalSort.Sort(allNodes).ToArray();
var frameUpdate = order.Where(p => NeedsFrameUpdate(p.GetType()));
var update = order.Where(p => NeedsUpdate(p.GetType()));
return (frameUpdate, update);
}
internal static IEnumerable<GraphNode<T>> TopologicalSort<T>(IEnumerable<GraphNode<T>> nodes)
{
var elems = nodes.ToDictionary(node => node,
node => new HashSet<GraphNode<T>>(node.DependsOn));
while (elems.Count > 0)
{
var elem =
elems.FirstOrDefault(x => x.Value.Count == 0);
if (elem.Key == null)
{
throw new InvalidOperationException(
"Found circular dependency when resolving entity system update dependency graph");
}
elems.Remove(elem.Key);
foreach (var selem in elems)
{
selem.Value.Remove(elem.Key);
}
yield return elem.Key;
}
}
private static IEnumerable<Type> GetBaseTypes(Type type) {
if(type.BaseType == null) return type.GetInterfaces();
@@ -344,18 +322,6 @@ namespace Robust.Shared.GameObjects
return mFrameUpdate!.DeclaringType != typeof(EntitySystem);
}
[DebuggerDisplay("GraphNode: {" + nameof(System) + "}")]
internal sealed class GraphNode<T>
{
public readonly T System;
public readonly List<GraphNode<T>> DependsOn = new();
public GraphNode(T system)
{
System = system;
}
}
private struct UpdateReg
{
[ViewVariables] public IEntitySystem System;

View File

@@ -0,0 +1,14 @@
namespace Robust.Shared.GameObjects
{
public sealed class EntityPausedEvent : EntityEventArgs
{
public EntityUid Entity { get; }
public bool Paused { get; }
public EntityPausedEvent(EntityUid entity, bool paused)
{
Entity = entity;
Paused = paused;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
@@ -11,7 +11,7 @@ namespace Robust.Shared.GameObjects
/// All discoverable implementations of IComponent must override the <see cref="Name" />.
/// Instances are dynamically instantiated by a <c>ComponentFactory</c>, and will have their IoC Dependencies resolved.
/// </remarks>
public interface IComponent : IEntityEventSubscriber
public interface IComponent
{
/// <summary>
/// Represents the network ID for the component.
@@ -34,6 +34,12 @@ namespace Robust.Shared.GameObjects
/// </summary>
bool Paused { get; }
/// <summary>
/// The current lifetime stage of this component. You can use this to check
/// if the component is initialized or being deleted.
/// </summary>
ComponentLifeStage LifeStage { get; }
/// <summary>
/// Whether the client should synchronize component additions and removals.
/// If this is false and the component gets added or removed server side, the client will not do the same.
@@ -64,9 +70,9 @@ namespace Robust.Shared.GameObjects
bool Initialized { get; }
/// <summary>
/// This is true when the component is active. Set this value to start or stop the component.
/// This is true when the component is active.
/// </summary>
bool Running { get; set; }
bool Running { get; }
/// <summary>
/// True if the component has been removed from its owner, AKA deleted.
@@ -88,19 +94,6 @@ namespace Robust.Shared.GameObjects
/// </summary>
GameTick LastModifiedTick { get; }
/// <summary>
/// Called when the component is removed from an entity.
/// Shuts down the component.
/// This should be called AFTER any inheriting classes OnRemove code has run. This should be last.
/// </summary>
void OnRemove();
/// <summary>
/// Called when all of the entity's other components have been added and are available,
/// But are not necessarily initialized yet. DO NOT depend on the values of other components to be correct.
/// </summary>
void Initialize();
/// <summary>
/// Handles a local incoming component message.
/// </summary>

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Prometheus;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Robust.Shared.GameObjects
@@ -77,13 +76,9 @@ namespace Robust.Shared.GameObjects
bool TryGetEntity(EntityUid uid, [NotNullWhen(true)] out IEntity? entity);
/// <summary>
/// Returns all entities that match with the provided query.
/// Returns all entities
/// </summary>
/// <param name="query">The query to test.</param>
/// <returns>An enumerable over all matching entities.</returns>
[Obsolete("IEntityQuery is obsolete")]
IEnumerable<IEntity> GetEntities(IEntityQuery query);
/// <returns></returns>
IEnumerable<IEntity> GetEntities();
public void QueueDeleteEntity(IEntity entity);

View File

@@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// An entity query is a type that can filter entities based on certain criteria,
/// for example based on the components that the entity has.
/// </summary>
[Obsolete("Use any of the other entity query methods instead.")]
public interface IEntityQuery
{
/// <summary>
/// Match the entity and see if it passes the criteria.
/// </summary>
/// <param name="entity">The entity to test.</param>
/// <returns>True if the entity is included in this query, false otherwise</returns>
bool Match(IEntity entity);
/// <summary>
/// Matches every entity in an EntityManager to see if it passes the criteria.
/// </summary>
/// <param name="entityMan">An EntityManager containing a set of entities.</param>
/// <returns>Enumeration of all entities that successfully matched the criteria.</returns>
IEnumerable<IEntity> Match(IEntityManager entityMan);
}
}

View File

@@ -0,0 +1,46 @@
using JetBrains.Annotations;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Robust.Shared.GameObjects
{
[UsedImplicitly]
internal sealed class MapSystem : EntitySystem
{
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MapGridComponent, ComponentRemove>(RemoveHandler);
SubscribeLocalEvent<MapGridComponent, ComponentInit>(HandleGridInitialize);
}
private void RemoveHandler(EntityUid uid, MapGridComponent component, ComponentRemove args)
{
_mapManager.OnComponentRemoved(component);
}
private void HandleGridInitialize(EntityUid uid, MapGridComponent component, ComponentInit args)
{
var msg = new GridInitializeEvent(uid, component.GridIndex);
EntityManager.EventBus.RaiseLocalEvent(uid, msg);
}
}
/// <summary>
/// Raised whenever a grid is being initialized.
/// </summary>
public sealed class GridInitializeEvent : EntityEventArgs
{
public EntityUid EntityUid { get; }
public GridId GridId { get; }
public GridInitializeEvent(EntityUid uid, GridId gridId)
{
EntityUid = uid;
GridId = gridId;
}
}
}

View File

@@ -12,8 +12,10 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
using Logger = Robust.Shared.Log.Logger;
@@ -107,6 +109,8 @@ namespace Robust.Shared.GameObjects
SubscribeLocalEvent<EntRemovedFromContainerMessage>(HandleContainerRemoved);
BuildControllers();
Logger.DebugS("physics", $"Found {_controllers.Count} physics controllers.");
IoCManager.Resolve<IIslandManager>().Initialize();
}
@@ -114,45 +118,24 @@ namespace Robust.Shared.GameObjects
{
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
var typeFactory = IoCManager.Resolve<IDynamicTypeFactory>();
var allControllerTypes = new List<Type>();
var instantiated = new List<VirtualController>();
foreach (var type in reflectionManager.GetAllChildren(typeof(VirtualController)))
{
if (type.IsAbstract) continue;
allControllerTypes.Add(type);
if (type.IsAbstract)
continue;
instantiated.Add(typeFactory.CreateInstance<VirtualController>(type));
}
var instantiated = new Dictionary<Type, VirtualController>();
var nodes = TopologicalSort.FromBeforeAfter(
instantiated,
c => c.GetType(),
c => c,
c => c.UpdatesBefore,
c => c.UpdatesAfter);
foreach (var type in allControllerTypes)
{
instantiated.Add(type, (VirtualController) typeFactory.CreateInstance(type));
}
// Build dependency graph, copied from EntitySystemManager *COUGH
var nodes = new Dictionary<Type, EntitySystemManager.GraphNode<VirtualController>>();
foreach (var (_, controller) in instantiated)
{
var node = new EntitySystemManager.GraphNode<VirtualController>(controller);
nodes[controller.GetType()] = node;
}
foreach (var (type, node) in nodes)
{
foreach (var before in instantiated[type].UpdatesBefore)
{
nodes[before].DependsOn.Add(node);
}
foreach (var after in instantiated[type].UpdatesAfter)
{
node.DependsOn.Add(nodes[after]);
}
}
_controllers = GameObjects.EntitySystemManager.TopologicalSort(nodes.Values).Select(c => c.System).ToList();
_controllers = TopologicalSort.Sort(nodes).ToList();
foreach (var controller in _controllers)
{
@@ -268,6 +251,25 @@ namespace Robust.Shared.GameObjects
_maps[mapId].AddBody(physicsComponent);
}
internal void FilterContactsForJoint(Joint joint)
{
var bodyA = joint.BodyA;
var bodyB = joint.BodyB;
var edge = bodyB.ContactEdges;
while (edge != null)
{
if (edge.Other == bodyA)
{
// Flag the contact for filtering at the next time step (where either
// body is awake).
edge.Contact!.FilterFlag = true;
}
edge = edge.Next;
}
}
/// <summary>
/// Simulates the physical world for a given amount of time.
/// </summary>

View File

@@ -1,12 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects
{
internal class SharedTransformSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly Queue<MoveEvent> _gridMoves = new();
private readonly Queue<MoveEvent> _otherMoves = new();
public override void Initialize()
{
base.Initialize();
_mapManager.TileChanged += MapManagerOnTileChanged;
}
public override void Shutdown()
{
_mapManager.TileChanged -= MapManagerOnTileChanged;
base.Shutdown();
}
private void MapManagerOnTileChanged(object? sender, TileChangedEventArgs e)
{
if(e.NewTile.Tile != Tile.Empty)
return;
var grid = _mapManager.GetGrid(e.NewTile.GridIndex);
var tileIndices = e.NewTile.GridIndices;
UnanchorAllEntsOnTile(grid, tileIndices);
}
private void UnanchorAllEntsOnTile(IMapGrid grid, Vector2i tileIndices)
{
var anchoredEnts = grid.GetAnchoredEntities(tileIndices);
foreach (var ent in anchoredEnts.ToList()) // changing anchored modifies this set
{
ComponentManager.GetComponent<TransformComponent>(ent).Anchored = false;
}
}
public void DeferMoveEvent(MoveEvent moveEvent)
{
if (moveEvent.Sender.HasComponent<IMapGridComponent>())

View File

@@ -1,109 +1,25 @@
using JetBrains.Annotations;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects
{
[UsedImplicitly]
internal class SnapGridSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SnapGridComponent, ComponentStartup>(HandleComponentStartup);
SubscribeLocalEvent<SnapGridComponent, ComponentShutdown>(HandleComponentShutdown);
SubscribeLocalEvent<SnapGridComponent, MoveEvent>(HandleMoveEvent);
}
private void HandleComponentStartup(EntityUid uid, SnapGridComponent component, ComponentStartup args)
{
var transform = ComponentManager.GetComponent<ITransformComponent>(uid);
UpdatePosition(uid, transform, component);
}
transform.Anchored = true;
private void HandleComponentShutdown(EntityUid uid, SnapGridComponent component, ComponentShutdown args)
{
if (component.LastGrid == GridId.Invalid)
return;
var transform = ComponentManager.GetComponent<ITransformComponent>(uid);
if (_mapManager.TryGetGrid(component.LastGrid, out var grid))
{
var indices = grid.TileIndicesFor(transform.WorldPosition);
grid.RemoveFromSnapGridCell(indices, uid);
return;
}
component.LastGrid = GridId.Invalid;
}
private void HandleMoveEvent(EntityUid uid, SnapGridComponent component, MoveEvent args)
{
var transform = ComponentManager.GetComponent<ITransformComponent>(uid);
UpdatePosition(uid, transform, component);
}
private void UpdatePosition(EntityUid euid, ITransformComponent transform, SnapGridComponent snapComp)
{
if (snapComp.LastGrid != GridId.Invalid)
{
if (!_mapManager.TryGetGrid(snapComp.LastGrid, out var lastGrid))
{
Logger.WarningS("go.comp.snapgrid", "Entity {0} snapgrid didn't find grid {1}. Race condition?", euid, transform.GridID);
return;
}
lastGrid.RemoveFromSnapGridCell(snapComp.LastTileIndices, euid);
}
if (!_mapManager.TryGetGrid(transform.GridID, out var grid))
{
// Either a race condition, or we're not on any grids.
return;
}
var oldPos = snapComp.LastTileIndices;
var oldGrid = snapComp.LastGrid;
var newPos = grid.TileIndicesFor(transform.MapPosition);
var newGrid = transform.GridID;
grid.AddToSnapGridCell(newPos, euid);
if (oldPos != newPos || oldGrid != newGrid)
{
RaiseLocalEvent(euid, new SnapGridPositionChangedEvent(newPos, oldPos, newGrid, oldGrid));
}
snapComp.LastTileIndices = newPos;
snapComp.LastGrid = newGrid;
}
}
public class SnapGridPositionChangedEvent : EntityEventArgs
{
public GridId OldGrid { get; }
public GridId NewGrid { get; }
public bool SameGrid => OldGrid == NewGrid;
public Vector2i OldPosition { get; }
public Vector2i Position { get; }
public SnapGridPositionChangedEvent(Vector2i position, Vector2i oldPosition, GridId newGrid, GridId oldGrid)
{
Position = position;
OldPosition = oldPosition;
NewGrid = newGrid;
OldGrid = oldGrid;
// Remove us, we have been migrated.
ComponentManager.RemoveComponent<SnapGridComponent>(uid);
}
}
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.GameObjects;
namespace Robust.Shared.GameStates
{
public class ComponentHandleState : EntityEventArgs
{
public ComponentState? Current { get; }
public ComponentState? Next { get; }
public ComponentHandleState(ComponentState? current, ComponentState? next)
{
Current = current;
Next = next;
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Shared.Input.Binding
{
@@ -13,8 +14,8 @@ namespace Robust.Shared.Input.Binding
// handlers in the order they should be resolved for the given key function.
// internally we use a graph to construct this but we render it down to a flattened
// list so we don't need to do any graph traversal at query time
private Dictionary<BoundKeyFunction, List<InputCmdHandler>> _bindingsForKey =
new();
private Dictionary<BoundKeyFunction, List<InputCmdHandler>> _bindingsForKey = new();
private bool _graphDirty = false;
/// <inheritdoc />
public void Register<TOwner>(CommandBinds commandBinds)
@@ -41,12 +42,15 @@ namespace Robust.Shared.Input.Binding
_bindings.Add(new TypedCommandBind(owner, binding));
}
RebuildGraph();
_graphDirty = true;
}
/// <inheritdoc />
public IEnumerable<InputCmdHandler> GetHandlers(BoundKeyFunction function)
{
if (_graphDirty)
RebuildGraph();
if (_bindingsForKey.TryGetValue(function, out var handlers))
{
return handlers;
@@ -58,7 +62,8 @@ namespace Robust.Shared.Input.Binding
public void Unregister(Type owner)
{
_bindings.RemoveAll(binding => binding.ForType == owner);
RebuildGraph();
_graphDirty = true;
}
/// <inheritdoc />
@@ -67,7 +72,7 @@ namespace Robust.Shared.Input.Binding
Unregister(typeof(TOwner));
}
private void RebuildGraph()
internal void RebuildGraph()
{
_bindingsForKey.Clear();
@@ -77,6 +82,7 @@ namespace Robust.Shared.Input.Binding
}
_graphDirty = false;
}
private Dictionary<BoundKeyFunction, List<TypedCommandBind>> FunctionToBindings()
@@ -105,16 +111,16 @@ namespace Robust.Shared.Input.Binding
//TODO: Probably could be optimized if needed! Generally shouldn't be a big issue since there is a relatively
// tiny amount of bindings
List<GraphNode> allNodes = new();
Dictionary<Type,List<GraphNode>> typeToNode = new();
List<TopologicalSort.GraphNode<TypedCommandBind>> allNodes = new();
Dictionary<Type,List<TopologicalSort.GraphNode<TypedCommandBind>>> typeToNode = new();
// build the dict for quick lookup on type
foreach (var binding in bindingsForFunction)
{
if (!typeToNode.ContainsKey(binding.ForType))
{
typeToNode[binding.ForType] = new List<GraphNode>();
typeToNode[binding.ForType] = new List<TopologicalSort.GraphNode<TypedCommandBind>>();
}
var newNode = new GraphNode(binding);
var newNode = new TopologicalSort.GraphNode<TypedCommandBind>(binding);
typeToNode[binding.ForType].Add(newNode);
allNodes.Add(newNode);
}
@@ -122,7 +128,7 @@ namespace Robust.Shared.Input.Binding
//add the graph edges
foreach (var curBinding in allNodes)
{
foreach (var afterType in curBinding.TypedCommandBind.CommandBind.After)
foreach (var afterType in curBinding.Value.CommandBind.After)
{
// curBinding should always fire after bindings associated with this afterType, i.e.
// this binding DEPENDS ON afterTypes' bindings
@@ -130,11 +136,12 @@ namespace Robust.Shared.Input.Binding
{
foreach (var afterBinding in afterBindings)
{
curBinding.DependsOn.Add(afterBinding);
afterBinding.Dependant.Add(curBinding);
}
}
}
foreach (var beforeType in curBinding.TypedCommandBind.CommandBind.Before)
foreach (var beforeType in curBinding.Value.CommandBind.Before)
{
// curBinding should always fire before bindings associated with this beforeType, i.e.
// beforeTypes' bindings DEPENDS ON this binding
@@ -142,7 +149,7 @@ namespace Robust.Shared.Input.Binding
{
foreach (var beforeBinding in beforeBindings)
{
beforeBinding.DependsOn.Add(curBinding);
curBinding.Dependant.Add(beforeBinding);
}
}
}
@@ -151,54 +158,7 @@ namespace Robust.Shared.Input.Binding
//TODO: Log graph structure for debugging
//use toposort to build the final result
var topoSorted = TopologicalSort(allNodes, function);
List<InputCmdHandler> result = new();
foreach (var node in topoSorted)
{
result.Add(node.TypedCommandBind.CommandBind.Handler);
}
return result;
}
//Adapted from https://stackoverflow.com/a/24058279
private static IEnumerable<GraphNode> TopologicalSort(IEnumerable<GraphNode> nodes, BoundKeyFunction function)
{
var elems = nodes.ToDictionary(node => node,
node => new HashSet<GraphNode>(node.DependsOn));
while (elems.Count > 0)
{
var elem =
elems.FirstOrDefault(x => x.Value.Count == 0);
if (elem.Key == null)
{
throw new InvalidOperationException("Found circular dependency when resolving" +
$" command binding handler order for key function {function.FunctionName}." +
$" Please check the systems which register bindings for" +
$" this function and eliminate the circular dependency.");
}
elems.Remove(elem.Key);
foreach (var selem in elems)
{
selem.Value.Remove(elem.Key);
}
yield return elem.Key;
}
}
/// <summary>
/// node in our temporary dependency graph
/// </summary>
private class GraphNode
{
public List<GraphNode> DependsOn = new();
public readonly TypedCommandBind TypedCommandBind;
public GraphNode(TypedCommandBind typedCommandBind)
{
TypedCommandBind = typedCommandBind;
}
return TopologicalSort.Sort(allNodes).Select(c => c.CommandBind.Handler).ToList();
}
/// <summary>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
@@ -38,6 +39,24 @@ namespace Robust.Shared.IoC
private readonly Dictionary<Type, (InjectorDelegate? @delegate, object[]? services)> _injectorCache =
new();
private readonly IDependencyCollection? _parentCollection;
public DependencyCollection() { }
public DependencyCollection(IDependencyCollection parentCollection)
{
_parentCollection = parentCollection;
}
/// <inheritdoc />
public bool TryResolveType(Type objectType, [MaybeNullWhen(false)] out object instance)
{
if(!_services.TryGetValue(objectType, out instance))
return _parentCollection is not null && _parentCollection.TryResolveType(objectType, out instance);
return true;
}
/// <inheritdoc />
public void Register<TInterface, TImplementation>(bool overwrite = false)
where TImplementation : class, TInterface
@@ -57,8 +76,10 @@ namespace Robust.Shared.IoC
{
var param = constructorParams[index];
if (_services.TryGetValue(param.ParameterType, out var instance))
if (TryResolveType(param.ParameterType, out var instance))
{
parameters[index] = instance;
}
else
{
if (_resolveTypes.ContainsKey(param.ParameterType))
@@ -74,6 +95,7 @@ namespace Robust.Shared.IoC
}, overwrite);
}
/// <inheritdoc />
public void Register<TInterface, TImplementation>(DependencyFactoryDelegate<TImplementation> factory, bool overwrite = false)
where TImplementation : class, TInterface
{
@@ -85,6 +107,48 @@ namespace Robust.Shared.IoC
_pendingResolves.Enqueue(interfaceType);
}
/// <inheritdoc />
public void Register(Type implementation, DependencyFactoryDelegate<object>? factory = null, bool overwrite = false)
{
CheckRegisterInterface(implementation, implementation, overwrite);
object DefaultFactory()
{
var constructors = implementation.GetConstructors();
if (constructors.Length != 1)
throw new InvalidOperationException($"Dependency '{implementation.FullName}' requires exactly one constructor.");
var constructorParams = constructors[0].GetParameters();
var parameters = new object[constructorParams.Length];
for (var index = 0; index < constructorParams.Length; index++)
{
var param = constructorParams[index];
if (TryResolveType(param.ParameterType, out var instance))
{
parameters[index] = instance;
}
else
{
if (_resolveTypes.ContainsKey(param.ParameterType))
{
throw new InvalidOperationException($"Dependency '{implementation.FullName}' ctor requires {param.ParameterType.FullName} registered before it.");
}
throw new InvalidOperationException($"Dependency '{implementation.FullName}' ctor has unknown dependency {param.ParameterType.FullName}");
}
}
return Activator.CreateInstance(implementation, parameters)!;
}
_resolveTypes[implementation] = implementation;
_resolveFactories[implementation] = factory ?? DefaultFactory;
_pendingResolves.Enqueue(implementation);
}
[AssertionMethod]
private void CheckRegisterInterface(Type interfaceType, Type implementationType, bool overwrite)
{
@@ -155,7 +219,7 @@ namespace Robust.Shared.IoC
[System.Diagnostics.Contracts.Pure]
public object ResolveType(Type type)
{
if (_services.TryGetValue(type, out var value))
if (TryResolveType(type, out var value))
{
return value;
}
@@ -268,21 +332,22 @@ namespace Robust.Shared.IoC
}
// Not using Resolve<T>() because we're literally building it right now.
if (!_services.ContainsKey(field.FieldType))
if (TryResolveType(field.FieldType, out var dep))
{
// A hard-coded special case so the DependencyCollection can inject itself.
// This is not put into the services so it can be overridden if needed.
if (field.FieldType == typeof(IDependencyCollection))
{
field.SetValue(obj, this);
continue;
}
throw new UnregisteredDependencyException(type, field.FieldType, field.Name);
// Quick note: this DOES work with read only fields, though it may be a CLR implementation detail.
field.SetValue(obj, dep);
continue;
}
// Quick note: this DOES work with read only fields, though it may be a CLR implementation detail.
field.SetValue(obj, _services[field.FieldType]);
// A hard-coded special case so the DependencyCollection can inject itself.
// This is not put into the services so it can be overridden if needed.
if (field.FieldType == typeof(IDependencyCollection))
{
field.SetValue(obj, this);
continue;
}
throw new UnregisteredDependencyException(type, field.FieldType, field.Name);
}
}
@@ -322,7 +387,7 @@ namespace Robust.Shared.IoC
generator.Emit(OpCodes.Ldarg_0);
// Not using Resolve<T>() because we're literally building it right now.
if (!_services.TryGetValue(field.FieldType, out var service))
if (!TryResolveType(field.FieldType, out var service))
{
// A hard-coded special case so the DependencyCollection can inject itself.
// This is not put into the services so it can be overridden if needed.

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