Compare commits

...

86 Commits

Author SHA1 Message Date
metalgearsloth
6491e51f3d Reduce FindGridsIntersecting allocations (#2119)
* Reduce FindGridsIntersecting allocations

I love struct enumerators

* Do slightly smarter pooling in EntityViewCulling

Even more alloc reductions

* Forgot to re-add this pooling

* Fix missing chunks
2021-10-19 13:30:52 +11:00
Pieter-Jan Briers
58e7fb3a17 Work around PEReader crashes on Linux.
Fixes #2130
2021-10-18 14:11:07 +02:00
mirrorcult
a9c7926226 Merge pull request #1942 from ShadowCommander/fix-sound
Change Audio to also use Grid Coordinates as a fallback
2021-10-18 00:28:11 -07:00
metalgearsloth
241dc0b752 Remove empty joint components (#2139) 2021-10-17 22:45:15 +02:00
Javier Guardia Fernández
566948f1c0 Add a virtual array of assemblies for SerializationTest to load so content can use it (#2137) 2021-10-16 15:38:18 +02:00
Javier Guardia Fernández
dbba440f7e Fix deserializing null structs (#2138)
* Fix deserializing null structs

* Fix deserializing for not nullable structs

* Add tests
2021-10-16 15:38:06 +02:00
Vera Aguilera Puerto
5b1e9eec27 Fixes component deletion/removal not being synced with clients. (#2128) 2021-10-15 23:52:09 +02:00
Pieter-Jan Briers
8ef95f0199 Update Lidgren 2021-10-15 15:59:44 +02:00
Leon Friedrich
b71e8f140a fix GridTileToWorldPos (#2134) 2021-10-15 22:59:44 +11:00
metalgearsloth
f8397099de Add a good GetCollidingEntities method that takes in body 2021-10-15 13:40:08 +11:00
metalgearsloth
5bbf1703ac Hack to avoid eye flicker on parent change 2021-10-15 11:55:47 +11:00
metalgearsloth
1357e38759 Fix controlmob crash (#2133) 2021-10-15 11:01:35 +11:00
Pieter-Jan Briers
655ecbab45 Update Lidgren submodule 2021-10-14 19:18:35 +02:00
Vera Aguilera Puerto
a0ef63bd4a Makes RemoveComponentImmediate more like RemoveComponentDeferred. (#2126)
- Exception tolerance added
- UID is now passed in the arguments
- Is able to remove protected components if needed.
- Actually calls `ComponentDependencyManager` to let it know the component has been removed.
2021-10-14 00:41:57 +02:00
Pieter-Jan Briers
f34763f11e Don't pack natives with engine client builds.
These are never used by the launcher due to the deployment model, so removing these saves quite a few megabytes.
2021-10-13 20:13:59 +02:00
Pieter-Jan Briers
b1f6b4cbe0 Remove .appveyor.yml
I doubt we'll ever use appveyor again, so...
2021-10-13 15:56:32 +02:00
Pieter-Jan Briers
94708881b3 Update Lidgren.
Optimizes Socket usage to remove allocations, minor bugfixes.
2021-10-13 15:55:04 +02:00
metalgearsloth
0f6dbac51c Fix gridbuffer broadphase (#2116)
It's for shuttle movement; it's getting bulldozed someday anyway but this fixes it.
2021-10-13 11:16:06 +11:00
metalgearsloth
e67de121ca Use spans for chunk mesh (#2124) 2021-10-11 17:06:21 +02:00
20kdc
17979db216 Technically fixes lathes, but not properly, but doesn't make things worse. (#2123)
I am aware this PR is terrible, but you saw what happened to the proper fix.
2021-10-11 16:10:38 +02:00
Pieter-Jan Briers
8d0070b5c3 Add IsUiOpen helper to UISystem. 2021-10-11 01:50:34 +02:00
Pieter-Jan Briers
034c392cbe Add GetComponentOrNull extension method. 2021-10-11 01:37:19 +02:00
Pieter-Jan Briers
160bbc3a72 Fix layout of entity spawn window. 2021-10-11 01:36:59 +02:00
metalgearsloth
3a4d228e94 Fix container manager deserialization 2021-10-11 02:56:37 +11:00
metalgearsloth
57f57f9d9f Fix PVS dumbdumb (#2122)
Was wondering why this was showing up on the profiler, woops
2021-10-11 02:27:04 +11:00
ShadowCommander
0592444252 Merge branch 'fix-sound' of github.com:ShadowCommander/RobustToolbox into fix-sound 2021-10-10 02:26:00 -07:00
ShadowCommander
d8499f2e60 Merge branch 'master' into fix-sound 2021-10-10 02:24:55 -07:00
metalgearsloth
476b5182f8 Fix y-sorting on rotated grids 2021-10-10 17:36:51 +11:00
metalgearsloth
6c7ab1bd82 Add AABBs to the physics debug drawing 2021-10-10 16:54:22 +11:00
metalgearsloth
526ed31b0d Re-parent unanchored entities (#2046) 2021-10-10 14:27:19 +11:00
metalgearsloth
3ac5552276 Cache more broadphase stuff internally (#2071)
* Don't generate physics contacts for non-predicted bodies

These are all just handled on the server anyway so it's a significant waste of performance on the client for busy scenes.

* Significantly optimise broadphase updates

Cache all of the broadphase data properly now and also save the cache for physics step to boot.

* Fix cross-map broadphases

* Remove unnecessary clear

* fix

* Fixes

* Numerous ray fixes
2021-10-10 14:18:10 +11:00
Flipp Syder
7a51c22514 Fixes SpriteComponent.AddLayer for specific RSI definition (#2115) 2021-10-09 20:21:12 +02:00
metalgearsloth
aa339eb504 Add component that toggles collision on anchoring (#2113) 2021-10-09 18:27:31 +02:00
mirrorcult
ffb3800664 Opt out of shadow casting for some lights (#2085)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-10-09 18:21:22 +02:00
Pieter-Jan Briers
39eb1a7d75 Clyde RenderOverlays now calls FlushRenderQueue.
This fixes association of API calls with the correct debug groups, and reduces (error prone) boilerplate.
2021-10-09 18:20:37 +02:00
20kdc
c25ab2fcb1 Fix FOV wall shadowing not working properly due to broken fov-lighting (#2114) 2021-10-09 17:27:36 +02:00
Vera Aguilera Puerto
22e0fbc6c1 The descendants of my friends are also my friends! Improves FriendAnalyzer.
In short, if you specify a shared type as a friend, any inheriting types will also count as friends, automatically. This could be abused but... That's why "sealed" exists, r-r-right?
2021-10-09 16:04:09 +02:00
Paul Ritter
72071826a4 fix placement grid (#2072) 2021-10-09 15:19:06 +02:00
Pieter-Jan Briers
8b9c1ab9a1 Fix warnings in Robust.Client 2021-10-09 15:08:08 +02:00
Vera Aguilera Puerto
16d5da5414 Check that parent uid is valid before dirtying oneself. 2021-10-09 12:41:57 +02:00
mirrorcult
39f82694bf Merge pull request #2111 from 20kdc/krakensbane-for-workgroups-8p6-focus
Stop spinning from messing as much with shadows
2021-10-08 16:28:50 -07:00
20kdc
79c3c09851 Stop spinning from messing as much with shadows
Shadows now seem to remain pretty stable up to the point where nothing is stable.
Sounds good to me.

Now, I know what you're going to say. "But, but, Kraken *fren*!"
*sighs* No. Kraken BAD. Kraken harm spesspeople.
2021-10-08 21:08:42 +01:00
Leon Friedrich
ad7370f0b3 Add option to make ScrollContainer return size. (#2107) 2021-10-08 16:01:01 +02:00
Pieter-Jan Briers
75cd8a0a12 Fix primitive restart on GLES3.
Was detected as incorrect version and didn't distinguish the difference between FixedIndex and not.
2021-10-08 14:54:53 +02:00
Ygg01
915a812832 Add extra helpers to Direction (#2093) 2021-10-08 13:34:45 +02:00
metalgearsloth
c8e7fe9f1f Reduce angular sleep tolerance even more 2021-10-08 18:34:48 +11:00
metalgearsloth
9803ed9cad Bandaid entity deletion entitylookup (#2102) 2021-10-08 09:14:19 +02:00
Leon Friedrich
f5f8a59c86 Add can-see / visibility function to ContainerHelpers (#2105) 2021-10-08 09:12:34 +02:00
20kdc
f4e3dfa601 Fix FOV shader to allow for s p i n (#2103) 2021-10-08 09:04:21 +02:00
metalgearsloth
9305b261bb Decrease angular sleep tolerance 2021-10-08 14:15:54 +11:00
ike709
3a161af4a5 Changes cefglue to use our fork (#2097)
Co-authored-by: ike709 <ike709@github.com>
2021-10-07 22:08:22 +02:00
Vera Aguilera Puerto
0447d8d3b9 Fix typo in BaseServer comment 2021-10-07 11:17:40 +02:00
ike709
c2eed5c007 Update the comments related to the CEF binary (#2098) 2021-10-07 09:53:14 +02:00
metalgearsloth
d9464e2621 Fix rotated grid collision (#2100) 2021-10-07 13:29:13 +11:00
Swept
1a70813f3c Merge pull request #2099 from 20kdc/oh-my-goodness-were-doomed 2021-10-06 18:45:20 -07:00
20kdc
a3a69f821f Revert "Fix stuck lathes properly. (#2091)" b/c of "Tried to overwrite a protected component." errors
This reverts commit a60993c60d.
2021-10-07 02:23:57 +01:00
Pieter-Jan Briers
c72988b05b Fix TickUpdate not firing on client when prediction is off. 2021-10-06 23:09:49 +02:00
ike709
11bc9c0fe4 Adds everything else OpenDream needs to the sandbox (#2060) 2021-10-06 22:50:37 +02:00
Pieter-Jan Briers
98593b7b33 NetListAsArray uses span instead of array now. 2021-10-05 15:22:46 +02:00
Vera Aguilera Puerto
4b30a94126 EntityManager SpawnEntity overloads consistently run map init on entities.
Fixes #2092
2021-10-05 14:44:43 +02:00
Pieter-Jan Briers
1c8958d312 Reduce .ToArray() in PVS with NetListAsArray<T>. 2021-10-05 14:37:39 +02:00
Pieter-Jan Briers
ef2f81a77a Update NetSerializer and Lidgren submodules.
Lidgren has slightly better buffer management, NetSerializer has NetListAsArray<T>.
2021-10-05 14:37:39 +02:00
Vera Aguilera Puerto
3935c63a80 EntityManager Initialized set by Initialize() method and not InitializeComponents() 2021-10-05 11:40:19 +02:00
Vera Aguilera Puerto
76ab68dc3d SharedContainerSystem proxy methods use UID to add components.
- Also disables logging missing comps on TryX methods.
2021-10-05 11:34:59 +02:00
Vera Aguilera Puerto
a986292aa2 Fix EntityManager.InitializeEntity being static for no reason at all.
- Also fixes `EntityInitialized` not being invoked on many cases. (On the client, on entities created by `ClientGameStateManager`, for example...)
2021-10-05 10:59:15 +02:00
Vera Aguilera Puerto
082fac52cd Remove redundant deleted entity check. 2021-10-05 10:14:25 +02:00
20kdc
a60993c60d Fix stuck lathes properly. (#2091) 2021-10-04 12:33:35 +02:00
Vera Aguilera Puerto
97ffc9ecc2 Resolves now log errors for missing components. (#2088) 2021-10-04 12:29:39 +02:00
Vera Aguilera Puerto
ab4f2c91b4 Move LifeStage and Paused data to MetaDataComponent. (#2068) 2021-10-04 12:29:13 +02:00
Vera Aguilera Puerto
5f2cc942cb Update UserInterfaceSystem proxies to use resolves. 2021-10-04 12:20:03 +02:00
metalgearsloth
5a343477a9 Joints refactor (#1954)
* feex

* Cleanup

* Remove unneeded matrix

* Fixes

* Fix preprocessor

* localanchor cleanup

* Break this build so I remember to fix it later.

* Fixes

* Woops

* Fixes

* Fixup merge

* Stuff

* Update debug drawing

* Also dis thanks fork

* fren

* More colors

* Default drawing

* Fixes

* reviews
2021-10-04 15:34:15 +11:00
Pieter-Jan Briers
7bc847c9a4 Multithread game state serialization, fix some possible thread bugs.
This won't help too much because serialization is extremely alloc-heavy and it causes tons of GC pressure, so GC will drastically reduce the effectiveness of parallelization. Still helps a bit though.
2021-10-04 02:20:13 +02:00
Pieter-Jan Briers
cfc7f26100 Kick command now handles kick reason better. 2021-10-04 01:45:33 +02:00
Vera Aguilera Puerto
6eb3989362 EyeComponent VisibilityMask is set to 1 by default. 2021-10-03 21:02:27 +02:00
20kdc
04fd324e4a Expose LocalBounds in IMapGrid (for space-wizards/space-station-14#4745 ) (#2090) 2021-10-03 23:49:02 +11:00
Vera Aguilera Puerto
c9e999d024 Adds Write/Copy TypeSerializer methods for base SpriteSpecifier. (#2089) 2021-10-03 13:41:41 +02:00
Vera Aguilera Puerto
243f405bab Add Filter for getting players from entities.
- Also adds method to merge two filters.
2021-10-03 11:57:44 +02:00
metalgearsloth
2353f0ed6f Debugdraw enhancements (#2080)
* Debugdraw enhancements

* Fix up the overlays

* Changes

* Box2D licence

* Update Robust.Client/Console/Commands/PhysicsOverlayCommands.cs

Co-authored-by: Paul Ritter <ritter.paul1@googlemail.com>

* Fix dependency

Co-authored-by: Paul Ritter <ritter.paul1@googlemail.com>
2021-10-03 17:58:08 +11:00
Vera Aguilera Puerto
8be4b39449 Adds utility "PrototypeFlags" collection. (#2074) 2021-10-02 12:14:24 +02:00
Vera Aguilera Puerto
e2c7d95519 Don't make SubscriptionsEnumerator nullable for no good reason. 2021-10-02 11:28:38 +02:00
Vera Aguilera Puerto
0e6c00573f EventTables' "TryGetSubscriptions" doesn't throw anymore when it can't find the uid on the event tables. 2021-10-02 11:26:38 +02:00
metalgearsloth
e7e08e5dd6 Merge branch 'master' into fix-sound 2021-09-13 20:39:42 +10:00
ShadowCommander
9ac8db37cc Add invalid check 2021-08-28 02:37:20 -07:00
ShadowCommander
2d6eebfae2 Rename GridCoordinates to FallbackCoordinates and move GetFallbackCoordinates to shared 2021-08-28 02:29:47 -07:00
ShadowCommander
5540d643ef Change from MapCoordinates to GridCoordinates 2021-08-12 01:45:07 -07:00
ShadowCommander
76c1d9f97b Change Audio to also use MapCoordinates as a fallback 2021-08-10 20:42:46 -07:00
145 changed files with 3912 additions and 2289 deletions

View File

@@ -1,52 +0,0 @@
environment:
sonarqubekey:
secure: h3llq6OeVa94hJ71UOEQSQDq75vFt+doso7iFry0gvt/fFcyeonY9wY+ETOIVITK
global:
PYTHONUNBUFFERED: True
HEADLESS: 1 # For the unit tests.
version: 0.1.0.{build}
pull_requests:
do_not_increment_build_number: true
image: Visual Studio 2019
install:
- ps: >
if (-Not $env:APPVEYOR_PULL_REQUEST_NUMBER -And $env:APPVEYOR_REPO_BRANCH -Eq "master")
{
cinst msbuild-sonarqube-runner;
}
before_build:
- cmd: py -3.5 -m pip install --user requests
- cmd: git submodule update --init --recursive
- ps: >
if (-Not $env:APPVEYOR_PULL_REQUEST_NUMBER -And $env:APPVEYOR_REPO_BRANCH -Eq "master")
{
SonarScanner.MSBuild.exe begin /k:"ss14" /d:"sonar.host.url=https://sonarcloud.io" /d:"sonar.login=$env:sonarqubekey" /o:"space-wizards" /d:sonar.cs.nunit.reportsPaths="$(Get-Location)\nunitTestResult.xml";
}
platform: x64
configuration: Debug
cache:
- packages -> **\*.csproj
- Dependencies
build:
project: RobustToolbox.sln
parallel: false
verbosity: minimal
build_script:
- ps: dotnet build RobustToolbox.sln /p:AppVeyor=yes
test_script:
- ps: dotnet test Robust.UnitTesting/Robust.UnitTesting.csproj
after_test:
- ps: >
if (-Not $env:APPVEYOR_PULL_REQUEST_NUMBER -And $env:APPVEYOR_REPO_BRANCH -Eq "master")
{
SonarScanner.MSBuild.exe end /d:"sonar.login=$env:sonarqubekey";
}

2
.gitmodules vendored
View File

@@ -18,4 +18,4 @@
url = https://github.com/space-wizards/Linguini
[submodule "cefglue"]
path = cefglue
url = https://gitlab.com/xiliumhq/chromiumembedded/cefglue/
url = https://github.com/space-wizards/cefglue.git

View File

@@ -3,10 +3,10 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<DefineConstants Condition="'$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'netcoreapp3.1'">$(DefineConstants);HAS_FULL_SPAN</DefineConstants>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DefaultItemExcludes>Lidgren.Network/**/*</DefaultItemExcludes>
<DefineConstants>$(DefineConstants);USE_RELEASE_STATISTICS</DefineConstants>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

@@ -2,29 +2,15 @@ preset raw;
#include "/Shaders/Internal/shadow_cast_shared.swsl"
#include "/Shaders/Internal/fov_shared.swsl"
const highp float g_MinVariance = 0.0;
varying highp vec2 worldPosition;
// Center of the FOV, in world coordinates.
uniform highp vec2 center;
void vertex()
{
highp vec3 transformed = modelMatrix * vec3(VERTEX, 1.0);
worldPosition = transformed.xy;
transformed = projectionMatrix * viewMatrix * transformed;
VERTEX = transformed.xy;
}
void fragment()
{
highp vec2 diff = worldPosition - center;
highp float ourDist = length(worldSpaceDiff);
highp float ourDist = length(diff);
highp vec2 occlDist = occludeDepth(diff, TEXTURE, 0.25);
highp vec2 occlDist = occludeDepth(worldSpaceDiff, TEXTURE, 0.25);
highp float occlusion = ChebyshevUpperBound(occlDist, ourDist);

View File

@@ -2,32 +2,18 @@ preset raw;
#include "/Shaders/Internal/shadow_cast_shared.swsl"
#include "/Shaders/Internal/fov_shared.swsl"
const highp float g_MinVariance = 0.0;
varying highp vec2 worldPosition;
// Center of the FOV, in world coordinates.
uniform highp vec2 center;
void vertex()
{
highp vec3 transformed = modelMatrix * vec3(VERTEX, 1.0);
worldPosition = transformed.xy;
transformed = projectionMatrix * viewMatrix * transformed;
VERTEX = transformed.xy;
}
void fragment()
{
highp vec2 diff = worldPosition - center;
highp float ourDist = length(worldSpaceDiff);
highp float ourDist = length(diff);
highp float occlDist = occludeDepth(diff, TEXTURE, 0.75).r;
highp float occlDist = occludeDepth(worldSpaceDiff, TEXTURE, 0.75).r;
// *Very* simple biased shadow check for FOV.
if (!doesOcclude(diff, TEXTURE, 0.75, -0.75/32.0))
if (!doesOcclude(worldSpaceDiff, TEXTURE, 0.75, -0.75/32.0))
{
discard;
}

View File

@@ -0,0 +1,16 @@
// Shared between fov-lighting.swsl and fov.swsl, which both use the Clyde quad,
// manually transformed into clip-space to cover the entire viewport
// World-space position offset from centre to pixel.
varying highp vec2 worldSpaceDiff;
// Inverted transformation matrix from clip coordinates to difference coordinates.
uniform highp mat3 clipToDiff;
void vertex()
{
// Convert quad-space (0.0 to 1.0) to clip-space (-1.0 to 1.0)
VERTEX = (VERTEX.xy - 0.5) * 2.0;
worldSpaceDiff = (clipToDiff * vec3(VERTEX, 1.0)).xy;
}

View File

@@ -40,7 +40,7 @@ void fragment()
highp vec2 diff = worldPosition - lightCenter;
// Totally not hacky PCF on top of VSM.
highp float occlusion = createOcclusion(diff);
highp float occlusion = lightIndex < 0.0 ? 1.0 : createOcclusion(diff);
if (occlusion == 0.0)
{

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -92,7 +93,7 @@ namespace Robust.Analyzers
continue;
// If we find that the containing class is specified in the attribute, return! All is good.
if (SymbolEqualityComparer.Default.Equals(containingType, t))
if (InheritsFromOrEquals(containingType, t))
return;
}
@@ -103,5 +104,26 @@ namespace Robust.Analyzers
}
}
}
private bool InheritsFromOrEquals(INamedTypeSymbol type, INamedTypeSymbol baseType)
{
foreach (var otherType in GetBaseTypesAndThis(type))
{
if (SymbolEqualityComparer.Default.Equals(otherType, baseType))
return true;
}
return false;
}
private IEnumerable<INamedTypeSymbol> GetBaseTypesAndThis(INamedTypeSymbol namedType)
{
var current = namedType;
while (current != null)
{
yield return current;
current = current.BaseType;
}
}
}
}

View File

@@ -51,15 +51,12 @@ namespace Robust.Client.CEF
// --------------------------- README --------------------------------------------------
// By the way! You're gonna need the CEF binaries in your client's bin folder.
// More specifically, version cef_binary_91.1.21+g9dd45fe+chromium-91.0.4472.114
// https://cef-builds.spotifycdn.com/cef_binary_91.1.21%2Bg9dd45fe%2Bchromium-91.0.4472.114_windows64_minimal.tar.bz2
// https://cef-builds.spotifycdn.com/cef_binary_91.1.21%2Bg9dd45fe%2Bchromium-91.0.4472.114_linux64_minimal.tar.bz2
// More specifically, version cef_binary_94.4.1+g4b61a8c+chromium-94.0.4606.54
// Windows: https://cef-builds.spotifycdn.com/cef_binary_94.4.1%2Bg4b61a8c%2Bchromium-94.0.4606.54_windows64_minimal.tar.bz2
// Linux: https://cef-builds.spotifycdn.com/cef_binary_94.4.1%2Bg4b61a8c%2Bchromium-94.0.4606.54_linux64_minimal.tar.bz2
// Here's how to get it to work:
// 1. Copy all the contents of "Release" to the bin folder.
// 2. Copy all the contents of "Resources" to the bin folder.
// Supposedly, you should just need libcef.so in Release and icudtl.dat in Resources...
// The rest might be optional.
// Maybe. Good luck! If you get odd crashes with no info and a weird exit code, use GDB!
// 1. Copy all the contents of "Release" to the bin/Content.Client folder.
// 2. Copy all the contents of "Resources" to the bin/Content.Client folder.
// -------------------------------------------------------------------------------------
_app = new RobustCefApp();

View File

@@ -65,7 +65,7 @@ namespace Robust.Client.Audio.Midi
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
private SharedBroadphaseSystem _broadPhaseSystem = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
[ViewVariables]
public bool IsAvailable
@@ -175,7 +175,7 @@ namespace Robust.Client.Audio.Midi
_midiThread = new Thread(ThreadUpdate);
_midiThread.Start();
_broadPhaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
FluidsynthInitialized = true;
}

View File

@@ -31,7 +31,6 @@ namespace Robust.Client
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IClientGameStateManager _gameStates = default!;
[Dependency] private readonly IDebugDrawingManager _debugDrawMan = default!;
/// <inheritdoc />
public ushort DefaultPort { get; } = 1212;
@@ -57,7 +56,6 @@ namespace Robust.Client
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
_playMan.Initialize();
_debugDrawMan.Initialize();
Reset();
}

View File

@@ -72,7 +72,6 @@ namespace Robust.Client
IoCManager.Register<IUserInterfaceManager, UserInterfaceManager>();
IoCManager.Register<IUserInterfaceManagerInternal, UserInterfaceManager>();
IoCManager.Register<IDebugDrawing, DebugDrawing>();
IoCManager.Register<IDebugDrawingManager, DebugDrawingManager>();
IoCManager.Register<ILightManager, LightManager>();
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
IoCManager.Register<IMidiManager, MidiManager>();

View File

@@ -161,19 +161,6 @@ namespace Robust.Client.Console.Commands
}
}
internal class ShowBoundingBoxesCommand : IConsoleCommand
{
public string Command => "showbb";
public string Help => "";
public string Description => "Enables debug drawing over all bounding boxes in the game, showing their size.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = IoCManager.Resolve<IDebugDrawing>();
mgr.DebugColliders = !mgr.DebugColliders;
}
}
internal class ShowPositionsCommand : IConsoleCommand
{
public string Command => "showpos";
@@ -201,16 +188,16 @@ namespace Robust.Client.Console.Commands
return;
}
if (!int.TryParse(args[0], out var id))
if (!float.TryParse(args[0], out var duration))
{
shell.WriteError($"{args[0]} is not a valid integer.");
shell.WriteError($"{args[0]} is not a valid float.");
return;
}
var mgr = IoCManager.Resolve<IDebugDrawingManager>();
var mgr = EntitySystem.Get<DebugRayDrawingSystem>();
mgr.DebugDrawRays = !mgr.DebugDrawRays;
shell.WriteError("Toggled showing rays to:" + mgr.DebugDrawRays.ToString());
mgr.DebugRayLifetime = TimeSpan.FromSeconds((double)int.Parse(args[0], CultureInfo.InvariantCulture));
shell.WriteError("Toggled showing rays to:" + mgr.DebugDrawRays);
mgr.DebugRayLifetime = TimeSpan.FromSeconds(duration);
}
}

View File

@@ -7,8 +7,8 @@ namespace Robust.Client.Console.Commands
public sealed class PhysicsOverlayCommands : IConsoleCommand
{
public string Command => "physics";
public string Description => $"{Command} <contactnormals / contactpoints / shapes>";
public string Help => $"{Command} <overlay>";
public string Description => $"Shows a debug physics overlay. The arg supplied specifies the overlay.";
public string Help => $"{Command} <aabbs / contactnormals / contactpoints / joints / shapeinfo / shapes>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
@@ -21,12 +21,21 @@ namespace Robust.Client.Console.Commands
switch (args[0])
{
case "aabbs":
system.Flags ^= PhysicsDebugFlags.AABBs;
break;
case "contactnormals":
system.Flags ^= PhysicsDebugFlags.ContactNormals;
break;
case "contactpoints":
system.Flags ^= PhysicsDebugFlags.ContactPoints;
break;
case "joints":
system.Flags ^= PhysicsDebugFlags.Joints;
break;
case "shapeinfo":
system.Flags ^= PhysicsDebugFlags.ShapeInfo;
break;
case "shapes":
system.Flags ^= PhysicsDebugFlags.Shapes;
break;

View File

@@ -1,19 +1,8 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Prototypes;
namespace Robust.Client.Debugging
{
@@ -22,39 +11,10 @@ namespace Robust.Client.Debugging
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private bool _debugColliders;
private bool _debugPositions;
/// <inheritdoc />
public bool DebugColliders
{
get => _debugColliders;
set
{
if (value == DebugColliders)
{
return;
}
_debugColliders = value;
if (value && !_overlayManager.HasOverlay<PhysicsOverlay>())
{
_overlayManager.AddOverlay(new PhysicsOverlay(_eyeManager,
_prototypeManager, _inputManager, _mapManager));
}
else
{
_overlayManager.RemoveOverlay<PhysicsOverlay>();
}
}
}
/// <inheritdoc />
public bool DebugPositions
{
@@ -79,194 +39,6 @@ namespace Robust.Client.Debugging
}
}
private class PhysicsOverlay : Overlay
{
private readonly IEyeManager _eyeManager;
private readonly IMapManager _mapManager;
private readonly IInputManager _inputManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
private readonly ShaderInstance _shader;
private readonly Font _font;
private Vector2 _hoverStartScreen = Vector2.Zero;
private List<IPhysBody> _hoverBodies = new();
public PhysicsOverlay(IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IMapManager mapManager)
{
_eyeManager = eyeMan;
_inputManager = inputManager;
_mapManager = mapManager;
_shader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
/// <inheritdoc />
protected internal override void Draw(in OverlayDrawArgs args)
{
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreen(args);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args);
break;
}
}
private void DrawScreen(in OverlayDrawArgs args)
{
var screenHandle = args.ScreenHandle;
var lineHeight = _font.GetLineHeight(1f);
Vector2 drawPos = _hoverStartScreen + new Vector2(20, 0) + new Vector2(0, -(_hoverBodies.Count * 4 * lineHeight / 2f));
int row = 0;
foreach (var body in _hoverBodies)
{
if (body != _hoverBodies[0])
{
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), "------");
row++;
}
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Mask: {Convert.ToString(body.CollisionMask, 2)}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Enabled: {body.CanCollide}, Hard: {body.Hard}, Anchored: {(body).BodyType == BodyType.Static}");
row++;
}
}
private void DrawWorld(in OverlayDrawArgs args)
{
var worldHandle = args.WorldHandle;
worldHandle.UseShader(_shader);
var drawing = new PhysDrawingAdapter(worldHandle);
_hoverBodies.Clear();
var mouseScreenPos = _inputManager.MouseScreenPosition;
var mouseWorldPos = _eyeManager.ScreenToMap(mouseScreenPos).Position;
_hoverStartScreen = mouseScreenPos.Position;
var viewport = _eyeManager.GetWorldViewport();
var viewBounds = _eyeManager.GetWorldViewbounds();
if (viewport.IsEmpty()) return;
var mapId = _eyeManager.CurrentMap;
var colorEdge = Color.Red.WithAlpha(0.33f);
var drawnJoints = new HashSet<Joint>();
foreach (var physBody in EntitySystem.Get<SharedBroadphaseSystem>().GetCollidingEntities(mapId, viewBounds))
{
if (physBody.Owner.HasComponent<MapGridComponent>()) continue;
// all entities have a TransformComponent
var transform = physBody.Owner.Transform;
var worldBox = physBody.GetWorldAABB();
if (worldBox.IsEmpty()) continue;
var pTransform = physBody.GetTransform();
foreach (var fixture in physBody.Fixtures)
{
var shape = fixture.Shape;
var sleepPercent = physBody.Awake ? 0.0f : 1.0f;
shape.DebugDraw(drawing, transform.WorldMatrix, in viewport, sleepPercent);
drawing.SetTransform(in Matrix3.Identity);
var aabb = shape.ComputeAABB(pTransform, 0);
worldHandle.DrawRect(aabb, Color.Blue, false);
}
foreach (var joint in physBody.Joints)
{
if (drawnJoints.Contains(joint)) continue;
drawnJoints.Add(joint);
joint.DebugDraw(drawing, in viewport);
drawing.SetTransform(in Matrix3.Identity);
}
if (worldBox.Contains(mouseWorldPos))
{
_hoverBodies.Add(physBody);
}
// draw AABB
worldHandle.DrawRect(worldBox, colorEdge, false);
}
}
private class PhysDrawingAdapter : DebugDrawingHandle
{
private readonly DrawingHandleWorld _handle;
public PhysDrawingAdapter(DrawingHandleWorld worldHandle)
{
_handle = worldHandle;
}
public override Color WakeMixColor => Color.White;
public override Color GridFillColor => Color.Blue.WithAlpha(0.05f);
public override Color RectFillColor => Color.Green.WithAlpha(0.25f);
public override Color CalcWakeColor(Color color, float wakePercent)
{
var percent = MathHelper.Clamp(wakePercent, 0, 1);
var r = 1 - (percent * (1 - color.R));
var g = 1 - (percent * (1 - color.G));
var b = 1 - (percent * (1 - color.B));
return new Color(r, g, b, color.A);
}
public override void DrawRect(in Box2 box, in Color color)
{
_handle.DrawRect(box, color);
}
public override void DrawRect(in Box2Rotated box, in Color color)
{
_handle.DrawRect(box, color);
}
public override void DrawCircle(Vector2 origin, float radius, in Color color)
{
_handle.DrawCircle(origin, radius, color);
}
public override void DrawPolygonShape(Vector2[] vertices, in Color color)
{
_handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, vertices, color);
}
public override void DrawLine(Vector2 start, Vector2 end, in Color color)
{
_handle.DrawLine(start, end, color);
}
public override void SetTransform(in Matrix3 transform)
{
_handle.SetTransform(transform);
}
}
}
private sealed class EntityPositionOverlay : Overlay
{
private readonly IEntityLookup _lookup;

View File

@@ -20,22 +20,49 @@
* 3. This notice may not be removed or altered from any source distribution.
*/
// 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.
/* Heavily inspired by Farseer */
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Dynamics.Joints;
namespace Robust.Client.Debugging
{
internal sealed class DebugPhysicsSystem : SharedDebugPhysicsSystem
public sealed class DebugPhysicsSystem : SharedDebugPhysicsSystem
{
/*
* Used for debugging shapes, controllers, joints, contacts
@@ -46,7 +73,7 @@ namespace Robust.Client.Debugging
private const int MaxContactPoints = 2048;
internal int PointCount;
internal ContactPoint[] _points = new ContactPoint[MaxContactPoints];
internal ContactPoint[] Points = new ContactPoint[MaxContactPoints];
public PhysicsDebugFlags Flags
{
@@ -56,7 +83,14 @@ namespace Robust.Client.Debugging
if (value == _flags) return;
if (_flags == PhysicsDebugFlags.None)
IoCManager.Resolve<IOverlayManager>().AddOverlay(new PhysicsDebugOverlay(this));
IoCManager.Resolve<IOverlayManager>().AddOverlay(
new PhysicsDebugOverlay(
EntityManager,
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IInputManager>(),
IoCManager.Resolve<IResourceCache>(),
this,
Get<SharedPhysicsSystem>()));
if (value == PhysicsDebugFlags.None)
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(typeof(PhysicsDebugOverlay));
@@ -69,7 +103,7 @@ namespace Robust.Client.Debugging
public override void HandlePreSolve(Contact contact, in Manifold oldManifold)
{
if ((Flags & PhysicsDebugFlags.ContactPoints) != 0)
if ((Flags & (PhysicsDebugFlags.ContactPoints | PhysicsDebugFlags.ContactNormals)) != 0)
{
Manifold manifold = contact.Manifold;
@@ -78,22 +112,24 @@ namespace Robust.Client.Debugging
Fixture fixtureA = contact.FixtureA!;
PointState[] state1, state2;
CollisionManager.GetPointStates(out state1, out state2, oldManifold, manifold);
var state1 = new PointState[2];
var state2 = new PointState[2];
CollisionManager.GetPointStates(ref state1, ref state2, oldManifold, manifold);
Span<Vector2> points = stackalloc Vector2[2];
contact.GetWorldManifold(_physicsManager, out var normal, points);
for (int i = 0; i < manifold.PointCount && PointCount < MaxContactPoints; ++i)
ContactPoint cp = Points[PointCount];
for (var i = 0; i < manifold.PointCount && PointCount < MaxContactPoints; ++i)
{
if (fixtureA == null)
_points[i] = new ContactPoint();
Points[i] = new ContactPoint();
ContactPoint cp = _points[PointCount];
cp.Position = points[i];
cp.Normal = normal;
cp.State = state2[i];
_points[PointCount] = cp;
Points[PointCount] = cp;
++PointCount;
}
}
@@ -108,58 +144,288 @@ namespace Robust.Client.Debugging
}
[Flags]
internal enum PhysicsDebugFlags : byte
public enum PhysicsDebugFlags : byte
{
None = 0,
/// <summary>
/// Shows the world point for each contact in the viewport.
/// </summary>
ContactPoints = 1 << 0,
/// <summary>
/// Shows the world normal for each contact in the viewport.
/// </summary>
ContactNormals = 1 << 1,
/// <summary>
/// Shows all physics shapes in the viewport.
/// </summary>
Shapes = 1 << 2,
ShapeInfo = 1 << 3,
Joints = 1 << 4,
AABBs = 1 << 5,
}
internal sealed class PhysicsDebugOverlay : Overlay
{
private DebugPhysicsSystem _physics = default!;
private IEntityManager _entityManager = default!;
private IEyeManager _eyeManager = default!;
private IInputManager _inputManager = default!;
private DebugPhysicsSystem _debugPhysicsSystem = default!;
private SharedPhysicsSystem _physicsSystem = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
public PhysicsDebugOverlay(DebugPhysicsSystem system)
private static readonly Color JointColor = new(0.5f, 0.8f, 0.8f);
private readonly Font _font;
private HashSet<Joint> _drawnJoints = new();
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IResourceCache cache, DebugPhysicsSystem system, SharedPhysicsSystem physicsSystem)
{
_physics = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_inputManager = inputManager;
_debugPhysicsSystem = system;
_physicsSystem = physicsSystem;
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
private void DrawWorld(DrawingHandleWorld worldHandle)
{
var viewport = _eyeManager.GetWorldViewport();
var viewBounds = _eyeManager.GetWorldViewbounds();
var mapId = _eyeManager.CurrentMap;
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.Shapes) != 0 && !viewport.IsEmpty())
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (physBody.Owner.HasComponent<MapGridComponent>()) continue;
var xform = physBody.GetTransform();
const float AlphaModifier = 0.2f;
foreach (var fixture in physBody.Fixtures)
{
// Invalid shape - Box2D doesn't check for IsSensor
if (physBody.BodyType == BodyType.Dynamic && fixture.Mass == 0f)
{
DrawShape(worldHandle, fixture, xform, Color.Red.WithAlpha(AlphaModifier));
}
else if (!physBody.CanCollide)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.3f).WithAlpha(AlphaModifier));
}
else if (physBody.BodyType == BodyType.Static)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.9f, 0.5f).WithAlpha(AlphaModifier));
}
else if ((physBody.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.9f).WithAlpha(AlphaModifier));
}
else if (!physBody.Awake)
{
DrawShape(worldHandle, fixture, xform, new Color(0.6f, 0.6f, 0.6f).WithAlpha(AlphaModifier));
}
else
{
DrawShape(worldHandle, fixture, xform, new Color(0.9f, 0.7f, 0.7f).WithAlpha(AlphaModifier));
}
}
}
}
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.AABBs) != 0 && !viewport.IsEmpty())
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (physBody.Owner.HasComponent<MapGridComponent>()) continue;
var xform = physBody.GetTransform();
const float AlphaModifier = 0.2f;
Box2? aabb = null;
foreach (var fixture in physBody.Fixtures)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
var shapeBB = fixture.Shape.ComputeAABB(xform, i);
aabb = aabb?.Union(shapeBB) ?? shapeBB;
}
}
if (aabb == null) continue;
worldHandle.DrawRect(aabb.Value, Color.Red.WithAlpha(AlphaModifier), false);
}
}
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.Joints) != 0x0)
{
_drawnJoints.Clear();
foreach (var jointComponent in _entityManager.EntityQuery<JointComponent>(true))
{
if (jointComponent.JointCount == 0 ||
!_entityManager.TryGetComponent(jointComponent.Owner.Uid, out TransformComponent? xf1) ||
!viewport.Contains(xf1.WorldPosition)) continue;
foreach (var (_, joint) in jointComponent.Joints)
{
if (_drawnJoints.Contains(joint)) continue;
DrawJoint(worldHandle, joint);
_drawnJoints.Add(joint);
}
}
}
if ((_debugPhysicsSystem.Flags & (PhysicsDebugFlags.ContactPoints | PhysicsDebugFlags.ContactNormals)) != 0)
{
const float axisScale = 0.3f;
for (var i = 0; i < _debugPhysicsSystem.PointCount; ++i)
{
var point = _debugPhysicsSystem.Points[i];
const float radius = 0.1f;
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ContactPoints) != 0)
{
if (point.State == PointState.Add)
worldHandle.DrawCircle(point.Position, radius, new Color(255, 77, 243, 255));
else if (point.State == PointState.Persist)
worldHandle.DrawCircle(point.Position, radius, new Color(255, 77, 77, 255));
}
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ContactNormals) != 0)
{
Vector2 p1 = point.Position;
Vector2 p2 = p1 + point.Normal * axisScale;
worldHandle.DrawLine(p1, p2, new Color(255, 102, 230, 255));
}
}
_debugPhysicsSystem.PointCount = 0;
}
}
private void DrawScreen(DrawingHandleScreen screenHandle)
{
var mapId = _eyeManager.CurrentMap;
var mousePos = _inputManager.MouseScreenPosition;
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ShapeInfo) != 0x0)
{
var hoverBodies = new List<PhysicsComponent>();
var bounds = Box2.UnitCentered.Translated(_eyeManager.ScreenToMap(mousePos.Position).Position);
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, bounds))
{
if (physBody.Owner.HasComponent<MapGridComponent>()) continue;
hoverBodies.Add(physBody);
}
var lineHeight = _font.GetLineHeight(1f);
var drawPos = mousePos.Position + new Vector2(20, 0) + new Vector2(0, -(hoverBodies.Count * 4 * lineHeight / 2f));
int row = 0;
foreach (var body in hoverBodies)
{
if (body != hoverBodies[0])
{
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), "------");
row++;
}
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Mask: {Convert.ToString(body.CollisionMask, 2)}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Enabled: {body.CanCollide}, Hard: {body.Hard}, Anchored: {(body).BodyType == BodyType.Static}");
row++;
}
}
}
protected internal override void Draw(in OverlayDrawArgs args)
{
if (_physics.Flags == PhysicsDebugFlags.None) return;
if (_debugPhysicsSystem.Flags == PhysicsDebugFlags.None) return;
var worldHandle = args.WorldHandle;
if ((_physics.Flags & PhysicsDebugFlags.Shapes) != 0)
switch (args.Space)
{
// Port DebugDrawing over.
case OverlaySpace.ScreenSpace:
DrawScreen((DrawingHandleScreen) args.DrawingHandle);
break;
case OverlaySpace.WorldSpace:
DrawWorld((DrawingHandleWorld) args.DrawingHandle);
break;
}
}
if ((_physics.Flags & PhysicsDebugFlags.ContactPoints) != 0)
private void DrawShape(DrawingHandleWorld worldHandle, Fixture fixture, Transform xform, Color color)
{
switch (fixture.Shape)
{
const float axisScale = 0.3f;
case PhysShapeCircle circle:
var center = Transform.Mul(xform, circle.Position);
worldHandle.DrawCircle(center, circle.Radius, color);
break;
case EdgeShape edge:
var v1 = Transform.Mul(xform, edge.Vertex1);
var v2 = Transform.Mul(xform, edge.Vertex2);
worldHandle.DrawLine(v1, v2, color);
for (int i = 0; i < _physics.PointCount; ++i)
{
DebugPhysicsSystem.ContactPoint point = _physics._points[i];
if (point.State == PointState.Add)
worldHandle.DrawCircle(point.Position, 0.5f, new Color(255, 77, 243, 77));
else if (point.State == PointState.Persist)
worldHandle.DrawCircle(point.Position, 0.5f, new Color(255, 77, 77, 77));
if ((_physics.Flags & PhysicsDebugFlags.ContactNormals) != 0)
if (edge.OneSided)
{
Vector2 p1 = point.Position;
Vector2 p2 = p1 + point.Normal * axisScale;
worldHandle.DrawLine(p1, p2, new Color(255, 102, 230, 102));
worldHandle.DrawCircle(v1, 0.5f, color);
worldHandle.DrawCircle(v2, 0.5f, color);
}
}
_physics.PointCount = 0;
break;
case PolygonShape poly:
Span<Vector2> verts = stackalloc Vector2[poly.Vertices.Length];
for (var i = 0; i < verts.Length; i++)
{
verts[i] = Transform.Mul(xform, poly.Vertices[i]);
}
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color);
break;
default:
return;
}
}
private void DrawJoint(DrawingHandleWorld worldHandle, Joint joint)
{
if (!_entityManager.TryGetComponent(joint.BodyAUid, out TransformComponent? xform1) ||
!_entityManager.TryGetComponent(joint.BodyBUid, out TransformComponent? xform2)) return;
var matrix1 = xform1.WorldMatrix;
var matrix2 = xform2.WorldMatrix;
var xf1 = new Vector2(matrix1.R0C2, matrix1.R1C2);
var xf2 = new Vector2(matrix2.R0C2, matrix2.R1C2);
var p1 = matrix1.Transform(joint.LocalAnchorA);
var p2 = matrix2.Transform(joint.LocalAnchorB);
switch (joint)
{
case DistanceJoint:
worldHandle.DrawLine(xf1, xf2, JointColor);
break;
default:
worldHandle.DrawLine(xf1, p1, JointColor);
worldHandle.DrawLine(p1, p2, JointColor);
worldHandle.DrawLine(xf2, p2, JointColor);
break;
}
}
}

View File

@@ -2,21 +2,22 @@ using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Timing;
namespace Robust.Client.Debugging
{
internal class DebugDrawingManager : IDebugDrawingManager
internal sealed class DebugRayDrawingSystem : EntitySystem
{
[Dependency] private readonly IClientNetManager _net = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IGameTiming _gameTimer = default!;
private readonly List<RayWithLifetime> raysWithLifeTime = new();
private readonly List<RayWithLifetime> _raysWithLifeTime = new();
private bool _debugDrawRays;
private struct RayWithLifetime
@@ -52,9 +53,30 @@ namespace Robust.Client.Debugging
public TimeSpan DebugRayLifetime { get; set; } = TimeSpan.FromSeconds(5);
public void Initialize()
public override void Initialize()
{
_net.RegisterNetMessage<MsgRay>(HandleDrawRay);
base.Initialize();
SubscribeNetworkEvent<MsgRay>(HandleDrawRay);
// To catch anything that's client-only and not sent by the server.
SubscribeLocalEvent<DebugDrawRayMessage>(OnDebugDrawRay);
}
private void OnDebugDrawRay(DebugDrawRayMessage ev)
{
if (!_debugDrawRays)
{
return;
}
var newRayWithLifetime = new RayWithLifetime
{
DidActuallyHit = ev.Data.Results != null,
RayOrigin = ev.Data.Ray.Position,
RayHit = ev.Data.Results?.HitPos ?? ev.Data.Ray.Direction * ev.Data.MaxLength + ev.Data.Ray.Position,
LifeTime = _gameTimer.RealTime + DebugRayLifetime
};
_raysWithLifeTime.Add(newRayWithLifetime);
}
private void HandleDrawRay(MsgRay msg)
@@ -74,15 +96,15 @@ namespace Robust.Client.Debugging
LifeTime = _gameTimer.RealTime + DebugRayLifetime
};
raysWithLifeTime.Add(newRayWithLifetime);
_raysWithLifeTime.Add(newRayWithLifetime);
}
private sealed class DebugDrawRayOverlay : Overlay
{
private readonly DebugDrawingManager _owner;
private readonly DebugRayDrawingSystem _owner;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public DebugDrawRayOverlay(DebugDrawingManager owner)
public DebugDrawRayOverlay(DebugRayDrawingSystem owner)
{
_owner = owner;
}
@@ -90,7 +112,7 @@ namespace Robust.Client.Debugging
protected internal override void Draw(in OverlayDrawArgs args)
{
var handle = args.WorldHandle;
foreach (var ray in _owner.raysWithLifeTime)
foreach (var ray in _owner._raysWithLifeTime)
{
handle.DrawLine(
ray.RayOrigin,
@@ -103,7 +125,7 @@ namespace Robust.Client.Debugging
{
base.FrameUpdate(args);
_owner.raysWithLifeTime.RemoveAll(r => r.LifeTime < _owner._gameTimer.RealTime);
_owner._raysWithLifeTime.RemoveAll(r => r.LifeTime < _owner._gameTimer.RealTime);
}
}
}

View File

@@ -5,11 +5,6 @@
/// </summary>
public interface IDebugDrawing
{
/// <summary>
/// Toggles the visual overlay of physics bodies for each entity on screen.
/// </summary>
bool DebugColliders { get; set; }
/// <summary>
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>

View File

@@ -1,11 +0,0 @@
using System;
namespace Robust.Client.Debugging
{
public interface IDebugDrawingManager
{
bool DebugDrawRays { get; set; }
TimeSpan DebugRayLifetime { get; set; }
void Initialize();
}
}

View File

@@ -40,7 +40,7 @@ namespace Robust.Client.GameObjects
void IClientEntityManagerInternal.InitializeEntity(IEntity entity)
{
EntityManager.InitializeEntity((Entity)entity);
base.InitializeEntity((Entity)entity);
}
void IClientEntityManagerInternal.StartEntity(IEntity entity)

View File

@@ -120,6 +120,12 @@ namespace Robust.Client.GameObjects
set => _visibleNested = value;
}
/// <summary>
/// Whether this pointlight should cast shadows
/// </summary>
[DataField("castShadows")]
public bool CastShadows = true;
[DataField("nestedvisible")]
private bool _visibleNested = true;
[DataField("autoRot")]

View File

@@ -564,7 +564,7 @@ namespace Robust.Client.GameObjects
Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, Environment.StackTrace);
}
return AddLayer(stateId, res?.RSI);
return AddLayer(stateId, res?.RSI, newIndex);
}
public int AddLayerState(string stateId, ResourcePath rsiPath, int? newIndex = null)
@@ -2146,7 +2146,7 @@ namespace Robust.Client.GameObjects
}
public ITransformComponent Transform { get; } = null!;
public IMetaDataComponent MetaData { get; } = null!;
public MetaDataComponent MetaData { get; } = null!;
private Dictionary<Type, IComponent> _components = new();
private EntityLifeStage _lifeStage;

View File

@@ -12,21 +12,20 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
public class AudioSystem : EntitySystem, IAudioSystem
public class AudioSystem : SharedAudioSystem, IAudioSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IClydeAudio _clyde = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadPhaseSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _broadPhaseSystem = default!;
private readonly List<PlayingStream> _playingClydeStreams = new();
@@ -65,7 +64,7 @@ namespace Robust.Client.GameObjects
return;
}
var stream = (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.AudioParams);
var stream = (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams);
if (stream != null)
{
stream.NetIdentifier = ev.Identifier;
@@ -84,8 +83,8 @@ namespace Robust.Client.GameObjects
private void PlayAudioEntityHandler(PlayAudioEntityMessage ev)
{
var stream = EntityManager.TryGetEntity(ev.EntityUid, out var entity) ?
(PlayingStream?) Play(ev.FileName, entity, ev.AudioParams)
: (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.AudioParams);
(PlayingStream?) Play(ev.FileName, entity, ev.FallbackCoordinates, ev.AudioParams)
: (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams);
if (stream != null)
{
@@ -135,6 +134,10 @@ namespace Robust.Client.GameObjects
mapPos = stream.TrackingEntity.Transform.MapPosition;
}
// TODO Remove when coordinates can't be NaN
if (mapPos == null || !float.IsFinite(mapPos.Value.X) || !float.IsFinite(mapPos.Value.Y))
mapPos = stream.TrackingFallbackCoordinates?.ToMap(_entityManager);
if (mapPos != null)
{
var pos = mapPos.Value;
@@ -212,7 +215,6 @@ namespace Robust.Client.GameObjects
{
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
}
}
}
}
@@ -277,12 +279,14 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, IEntity entity, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, IEntity entity, EntityCoordinates fallbackCoordinates,
AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
return Play(audio, entity, audioParams);
return Play(audio, entity, fallbackCoordinates, audioParams);
}
Logger.Error($"Server tried to play audio file {filename} which does not exist.");
@@ -294,8 +298,10 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, IEntity entity, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(AudioStream stream, IEntity entity, EntityCoordinates fallbackCoordinates,
AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);
if (!source.SetPosition(entity.Transform.WorldPosition))
@@ -312,6 +318,7 @@ namespace Robust.Client.GameObjects
{
Source = source,
TrackingEntity = entity,
TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null,
Attenuation = audioParams?.Attenuation ?? Attenuation.Default,
MaxDistance = audioParams?.MaxDistance ?? float.MaxValue,
ReferenceDistance = audioParams?.ReferenceDistance ?? 1f,
@@ -327,12 +334,14 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, EntityCoordinates fallbackCoordinates,
AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
return Play(audio, coordinates, audioParams);
return Play(audio, coordinates, fallbackCoordinates, audioParams);
}
Logger.Error($"Server tried to play audio file {filename} which does not exist.");
@@ -344,18 +353,24 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when coordinates are invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
AudioParams? audioParams = null)
EntityCoordinates fallbackCoordinates, AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);
if (!source.SetPosition(coordinates.ToMapPos(EntityManager)))
if (!source.SetPosition(fallbackCoordinates.Position))
{
source.Dispose();
Logger.Warning($"Can't play positional audio \"{stream.Name}\", can't set position.");
return null;
}
if (!coordinates.IsValid(_entityManager))
{
coordinates = fallbackCoordinates;
}
ApplyAudioParams(audioParams, source);
source.StartPlaying();
@@ -363,6 +378,7 @@ namespace Robust.Client.GameObjects
{
Source = source,
TrackingCoordinates = coordinates,
TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null,
Attenuation = audioParams?.Attenuation ?? Attenuation.Default,
MaxDistance = audioParams?.MaxDistance ?? float.MaxValue,
ReferenceDistance = audioParams?.ReferenceDistance ?? 1f,
@@ -395,6 +411,7 @@ namespace Robust.Client.GameObjects
public IClydeAudioSource Source = default!;
public IEntity TrackingEntity = default!;
public EntityCoordinates? TrackingCoordinates;
public EntityCoordinates? TrackingFallbackCoordinates;
public bool Done;
public float Volume;
@@ -440,19 +457,19 @@ namespace Robust.Client.GameObjects
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, IEntity entity, AudioParams? audioParams = null)
{
return Play(filename, entity, audioParams);
return Play(filename, entity, GetFallbackCoordinates(entity.Transform.MapPosition), audioParams);
}
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityUid uid, AudioParams? audioParams = null)
{
return EntityManager.TryGetEntity(uid, out var entity)
? Play(filename, entity, audioParams) : null;
? Play(filename, entity, GetFallbackCoordinates(entity.Transform.MapPosition), audioParams) : null;
}
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return Play(filename, coordinates, audioParams);
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(_entityManager)), audioParams);
}
}
}

View File

@@ -9,6 +9,7 @@ using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
#nullable enable
@@ -28,6 +29,9 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IPlayerManager _playerManager = default!;
private bool _isLerping = false;
private ITransformComponent? _lastParent;
private float _accumulator;
/// <inheritdoc />
public override void Initialize()
@@ -59,27 +63,6 @@ namespace Robust.Client.GameObjects
var currentEye = _eyeManager.CurrentEye;
// TODO: Content should have its own way of handling this. We should have a default behavior that they can overwrite.
/*
var inputSystem = EntitySystemManager.GetEntitySystem<InputSystem>();
var direction = 0;
if (inputSystem.CmdStates[EngineKeyFunctions.CameraRotateRight] == BoundKeyState.Down)
{
direction += 1;
}
if (inputSystem.CmdStates[EngineKeyFunctions.CameraRotateLeft] == BoundKeyState.Down)
{
direction -= 1;
}
// apply camera rotation
if(direction != 0)
{
currentEye.Rotation += CameraRotateSpeed * frameTime * direction;
currentEye.Rotation = currentEye.Rotation.Reduced();
}
*/
var playerTransform = _playerManager.LocalPlayer?.ControlledEntity?.Transform;
@@ -91,7 +74,22 @@ namespace Robust.Client.GameObjects
gridEnt.Transform
: _mapManager.GetMapEntity(playerTransform.MapID).Transform;
if (!_isLerping)
if (parent != _lastParent)
{
_accumulator += frameTime;
if (_accumulator >= 0.3f)
{
_accumulator = 0f;
_lastParent = parent;
}
}
else
{
_lastParent = parent;
}
if (_lastParent == parent)
{
// TODO: Detect parent change and start lerping
var parentRotation = parent.WorldRotation;

View File

@@ -266,10 +266,10 @@ namespace Robust.Client.GameStates
DebugTools.Assert(_timing.InSimulation);
if (!Predicting) return;
using(var _ = _timing.StartPastPredictionArea())
if (Predicting)
{
using var _ = _timing.StartPastPredictionArea();
if (_pendingInputs.Count > 0)
{
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
@@ -412,64 +412,57 @@ namespace Robust.Client.GameStates
private List<EntityUid> ApplyGameState(GameState curState, GameState? nextState)
{
_config.TickProcessMessages();
_mapManager.ApplyGameStatePre(curState.MapData, curState.EntityStates);
var createdEntities = ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
nextState?.EntityStates);
_players.ApplyPlayerStates(curState.PlayerStates);
_mapManager.ApplyGameStatePre(curState.MapData, curState.EntityStates.Span);
var createdEntities = ApplyEntityStates(curState.EntityStates.Span, curState.EntityDeletions.Span,
nextState != null ? nextState.EntityStates.Span : default);
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
_mapManager.ApplyGameStatePost(curState.MapData);
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
return createdEntities;
}
private List<EntityUid> ApplyEntityStates(EntityState[]? curEntStates, IEnumerable<EntityUid>? deletions,
EntityState[]? nextEntStates)
private List<EntityUid> ApplyEntityStates(ReadOnlySpan<EntityState> curEntStates, ReadOnlySpan<EntityUid> deletions,
ReadOnlySpan<EntityState> nextEntStates)
{
var toApply = new Dictionary<IEntity, (EntityState?, EntityState?)>();
var toInitialize = new List<Entity>();
var created = new List<EntityUid>();
deletions ??= new EntityUid[0];
if (curEntStates != null && curEntStates.Length != 0)
foreach (var es in curEntStates)
{
foreach (var es in curEntStates)
//Known entities
if (_entities.TryGetEntity(es.Uid, out var entity))
{
//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
{
var metaState = (MetaDataComponentState?) es.ComponentChanges.Value?.FirstOrDefault(c => c.NetID == _metaCompNetId).State;
if (metaState == null)
{
// Logger.Debug($"[{IGameTiming.TickStampStatic}] MOD {es.Uid}");
toApply.Add(entity, (es, null));
}
else //Unknown entities
{
var metaState = (MetaDataComponentState?) es.ComponentChanges?.FirstOrDefault(c => c.NetID == _metaCompNetId).State;
if (metaState == null)
{
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);
created.Add(newEntity.Uid);
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);
created.Add(newEntity.Uid);
}
}
if (nextEntStates != null && nextEntStates.Length != 0)
foreach (var es in nextEntStates)
{
foreach (var es in nextEntStates)
if (_entities.TryGetEntity(es.Uid, out var entity))
{
if (_entities.TryGetEntity(es.Uid, out var entity))
if (toApply.TryGetValue(entity, out var state))
{
if (toApply.TryGetValue(entity, out var state))
{
toApply[entity] = (state.Item1, es);
}
else
{
toApply[entity] = (null, es);
}
toApply[entity] = (state.Item1, es);
}
else
{
toApply[entity] = (null, es);
}
}
}
@@ -553,9 +546,9 @@ namespace Robust.Client.GameStates
var compStateWork = new Dictionary<ushort, (ComponentState? curState, ComponentState? nextState)>();
var entityUid = entity.Uid;
if (curState?.ComponentChanges != null)
if (curState != null)
{
foreach (var compChange in curState.ComponentChanges)
foreach (var compChange in curState.ComponentChanges.Span)
{
if (compChange.Deleted)
{
@@ -578,19 +571,16 @@ namespace Robust.Client.GameStates
compStateWork[compChange.NetID] = (compChange.State, null);
}
}
}
if (curState?.ComponentChanges != null)
{
foreach (var compChange in curState.ComponentChanges)
foreach (var compChange in curState.ComponentChanges.Span)
{
compStateWork[compChange.NetID] = (compChange.State, null);
}
}
if (nextState?.ComponentChanges != null)
if (nextState != null)
{
foreach (var compState in nextState.ComponentChanges)
foreach (var compState in nextState.ComponentChanges.Span)
{
if (compStateWork.TryGetValue(compState.NetID, out var state))
{

View File

@@ -168,38 +168,29 @@ namespace Robust.Client.GameStates
}
else
{
if (state.EntityDeletions != null)
foreach (var deletion in state.EntityDeletions.Span)
{
foreach (var deletion in state.EntityDeletions)
{
_lastStateFullRep.Remove(deletion);
}
_lastStateFullRep.Remove(deletion);
}
}
if (state.EntityStates != null)
foreach (var entityState in state.EntityStates.Span)
{
foreach (var entityState in state.EntityStates)
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
{
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
{
compData = new Dictionary<uint, ComponentState>();
_lastStateFullRep.Add(entityState.Uid, compData);
}
compData = new Dictionary<uint, ComponentState>();
_lastStateFullRep.Add(entityState.Uid, compData);
}
if (entityState.ComponentChanges != null)
foreach (var change in entityState.ComponentChanges.Span)
{
if (change.Deleted)
{
foreach (var change in entityState.ComponentChanges)
{
if (change.Deleted)
{
compData.Remove(change.NetID);
}
else if (change.State is not null)
{
compData[change.NetID] = change.State;
}
}
compData.Remove(change.NetID);
}
else if (change.State is not null)
{
compData[change.NetID] = change.State;
}
}
}
@@ -352,7 +343,7 @@ namespace Robust.Client.GameStates
/// </summary>
private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence, uint lastInput)
{
var state = new GameState(fromSequence, toSequence, lastInput, null, null, null, null);
var state = new GameState(fromSequence, toSequence, lastInput, default, default, default, null);
state.Extrapolated = true;
return state;
}

View File

@@ -65,10 +65,10 @@ namespace Robust.Client.GameStates
var gameState = args.AppliedState;
if(gameState.EntityStates is not null)
if(gameState.EntityStates.HasContents)
{
// Loop over every entity that gets updated this state and record the traffic
foreach (var entityState in gameState.EntityStates)
foreach (var entityState in gameState.EntityStates.Span)
{
var newEnt = true;
for(var i=0;i<_netEnts.Count;i++)

View File

@@ -79,17 +79,17 @@ namespace Robust.Client.GameStates
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates is not null)
if (entStates.HasContents)
{
var sb = new StringBuilder();
foreach (var entState in entStates)
foreach (var entState in entStates.Span)
{
if (entState.Uid == WatchEntId)
{
if(entState.ComponentChanges is not null)
if(entState.ComponentChanges.HasContents)
{
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges)
foreach (var compChange in entState.ComponentChanges.Span)
{
var registration = _componentFactory.GetRegistration(compChange.NetID);
var create = compChange.Created ? 'C' : '\0';
@@ -107,10 +107,10 @@ namespace Robust.Client.GameStates
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes is not null)
if (entDeletes.HasContents)
{
var sb = new StringBuilder();
foreach (var entDelete in entDeletes)
foreach (var entDelete in entDeletes.Span)
{
if (entDelete == WatchEntId)
{

View File

@@ -54,6 +54,8 @@ namespace Robust.Client.Graphics
/// <returns>Corresponding point in UI screen space.</returns>
ScreenCoordinates CoordinatesToScreen(EntityCoordinates point);
ScreenCoordinates MapToScreen(MapCoordinates point);
/// <summary>
/// Unprojects a point from UI screen space to world space using the viewport under the screen coordinates.
/// </summary>

View File

@@ -19,6 +19,7 @@ namespace Robust.Client.Graphics.Clyde
private bool _hasGLSamplerObjects;
private bool _hasGLSrgb;
private bool _hasGLPrimitiveRestart;
private bool _hasGLPrimitiveRestartFixedIndex;
private bool _hasGLReadFramebuffer;
private bool _hasGLUniformBuffers;
private bool HasGLAnyVertexArrayObjects => _hasGLVertexArrayObject || _hasGLVertexArrayObjectOes;
@@ -105,7 +106,7 @@ namespace Robust.Client.Graphics.Clyde
CheckGLCap(ref _hasGLPixelBufferObjects, "pixel_buffer_object", (3, 0));
CheckGLCap(ref _hasGLStandardDerivatives, "standard_derivatives", (3, 0), "GL_OES_standard_derivatives");
CheckGLCap(ref _hasGLReadFramebuffer, "read_framebuffer", (3, 0));
CheckGLCap(ref _hasGLPrimitiveRestart, "primitive_restart", (3, 1));
CheckGLCap(ref _hasGLPrimitiveRestartFixedIndex, "primitive_restart", (3, 0));
CheckGLCap(ref _hasGLUniformBuffers, "uniform_buffers", (3, 0));
CheckGLCap(ref _hasGLFloatFramebuffers, "float_framebuffers", (3, 2), "GL_EXT_color_buffer_float");
CheckGLCap(ref _hasGLES3Shaders, "gles3_shaders", (3, 0));

View File

@@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -51,8 +52,9 @@ namespace Robust.Client.Graphics.Clyde
var transform = _entityManager.GetComponent<ITransformComponent>(grid.GridEntityId);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
grid.GetMapChunks(worldBounds, out var enumerator);
foreach (var chunk in grid.GetMapChunks(worldBounds))
while (enumerator.MoveNext(out var chunk))
{
if (_isChunkDirty(grid, chunk))
{
@@ -85,50 +87,39 @@ namespace Robust.Client.Graphics.Clyde
datum = _initChunkBuffers(grid, chunk);
}
var vertexPool = ArrayPool<Vertex2D>.Shared;
var indexPool = ArrayPool<ushort>.Shared;
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
var vertexBuffer = vertexPool.Rent(_verticesPerChunk(chunk));
var indexBuffer = indexPool.Rent(_indicesPerChunk(chunk));
try
var i = 0;
foreach (var tile in chunk)
{
var i = 0;
foreach (var tile in chunk)
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile.Tile);
if (regionMaybe == null)
{
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile.Tile);
if (regionMaybe == null)
{
continue;
}
var region = regionMaybe.Value;
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(tile.X, tile.Y, region.Left, region.Bottom);
vertexBuffer[vIdx + 1] = new Vertex2D(tile.X + 1, tile.Y, region.Right, region.Bottom);
vertexBuffer[vIdx + 2] = new Vertex2D(tile.X + 1, tile.Y + 1, region.Right, region.Top);
vertexBuffer[vIdx + 3] = new Vertex2D(tile.X, tile.Y + 1, region.Left, region.Top);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort) (i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
continue;
}
GL.BindVertexArray(datum.VAO);
CheckGlError();
datum.EBO.Use();
datum.VBO.Use();
datum.EBO.Reallocate(new Span<ushort>(indexBuffer, 0, i * GetQuadBatchIndexCount()));
datum.VBO.Reallocate(new Span<Vertex2D>(vertexBuffer, 0, i * 4));
datum.Dirty = false;
datum.TileCount = i;
}
finally
{
vertexPool.Return(vertexBuffer);
indexPool.Return(indexBuffer);
var region = regionMaybe.Value;
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(tile.X, tile.Y, region.Left, region.Bottom);
vertexBuffer[vIdx + 1] = new Vertex2D(tile.X + 1, tile.Y, region.Right, region.Bottom);
vertexBuffer[vIdx + 2] = new Vertex2D(tile.X + 1, tile.Y + 1, region.Right, region.Top);
vertexBuffer[vIdx + 3] = new Vertex2D(tile.X, tile.Y + 1, region.Left, region.Top);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort) (i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
}
GL.BindVertexArray(datum.VAO);
CheckGlError();
datum.EBO.Use();
datum.VBO.Use();
datum.EBO.Reallocate(indexBuffer[..(i * GetQuadBatchIndexCount())]);
datum.VBO.Reallocate(vertexBuffer[..(i * 4)]);
datum.Dirty = false;
datum.TileCount = i;
}
private MapChunkData _initChunkBuffers(IMapGrid grid, IMapChunk chunk)

View File

@@ -118,6 +118,8 @@ namespace Robust.Client.Graphics.Clyde
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox);
}
FlushRenderQueue();
}
}
@@ -212,8 +214,9 @@ namespace Robust.Client.Graphics.Clyde
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB);
var screenSize = viewport.Size;
eye.GetViewMatrix(out var eyeMatrix, eye.Scale);
ProcessSpriteEntities(mapId, worldBounds, _drawingSpriteList);
ProcessSpriteEntities(mapId, eyeMatrix, worldBounds, _drawingSpriteList);
var worldOverlays = new List<Overlay>();
@@ -362,7 +365,7 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ProcessSpriteEntities(MapId map, Box2Rotated worldBounds,
private void ProcessSpriteEntities(MapId map, Matrix3 eyeMatrix, Box2Rotated worldBounds,
RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> list)
{
foreach (var comp in _entitySystemManager.GetEntitySystem<RenderingTreeSystem>().GetRenderTrees(map, worldBounds))
@@ -380,10 +383,10 @@ namespace Robust.Client.Graphics.Clyde
entry.sprite = value;
entry.worldRot = transform.WorldRotation;
entry.matrix = transform.WorldMatrix;
var worldPos = new Vector2(entry.matrix.R0C2, entry.matrix.R1C2);
var eyePos = eyeMatrix.Transform(new Vector2(entry.matrix.R0C2, entry.matrix.R1C2));
// Didn't use the bounds from the query as that has to be re-calculated (and is probably more expensive than this).
var bounds = value.CalculateBoundingBox(worldPos);
entry.yWorldPos = worldPos.Y - bounds.Extents.Y;
var bounds = value.CalculateBoundingBox(eyePos);
entry.yWorldPos = eyePos.Y - bounds.Extents.Y;
return true;
}, bounds, true);
@@ -469,7 +472,6 @@ namespace Robust.Client.Graphics.Clyde
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldAABB);
FlushRenderQueue();
using (DebugGroup("Grids"))
{
@@ -483,7 +485,6 @@ namespace Robust.Client.Graphics.Clyde
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB);
FlushRenderQueue();
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
{
@@ -517,7 +518,6 @@ namespace Robust.Client.Graphics.Clyde
}
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB);
FlushRenderQueue();
_currentViewport = oldVp;
});

View File

@@ -361,6 +361,8 @@ namespace Robust.Client.Graphics.Clyde
{
var (light, lightPos, _) = lights[i];
if (!light.CastShadows) continue;
DrawOcclusionDepth(lightPos, ShadowMapSize, light.Radius, i);
}
}
@@ -459,7 +461,7 @@ namespace Robust.Client.Graphics.Clyde
}
lightShader.SetUniformMaybe("lightCenter", lightPos);
lightShader.SetUniformMaybe("lightIndex", (i + 0.5f) / ShadowTexture.Height);
lightShader.SetUniformMaybe("lightIndex", component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1);
var offset = new Vector2(component.Radius, component.Radius);
@@ -749,9 +751,8 @@ namespace Robust.Client.Graphics.Clyde
SetTexture(TextureUnit.Texture0, FovTexture);
fovShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
fovShader.SetUniformMaybe("center", eye.Position.Position);
DrawBlit(viewport, fovShader);
FovSetTransformAndBlit(viewport, eye.Position.Position, fovShader);
GL.StencilMask(0x00);
GL.Disable(EnableCap.StencilTest);
@@ -786,7 +787,6 @@ namespace Robust.Client.Graphics.Clyde
}
fovShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
fovShader.SetUniformMaybe("center", eye.Position.Position);
GL.StencilMask(0xFF);
CheckGlError();
@@ -795,7 +795,7 @@ namespace Robust.Client.Graphics.Clyde
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, TKStencilOp.Replace);
CheckGlError();
DrawBlit(viewport, fovShader);
FovSetTransformAndBlit(viewport, eye.Position.Position, fovShader);
if (_hasGLSamplerObjects)
{
@@ -811,12 +811,30 @@ namespace Robust.Client.Graphics.Clyde
}
}
private void DrawBlit(Viewport vp, GLShaderProgram shader)
private void FovSetTransformAndBlit(Viewport vp, Vector2 fovCentre, GLShaderProgram fovShader)
{
var a = ScreenToMap((-1, -1), vp);
var b = ScreenToMap(vp.Size + Vector2i.One, vp);
// It might be an idea if there was a proper way to get the LocalToWorld matrix.
// But actually constructing the matrix tends to be more trouble than it's worth in most cases.
// (Maybe if there was some way to observe Eye matrix changes that wouldn't be the case, as viewport could dynamically update.)
// This is expected to run a grand total of twice per frame for 6 LocalToWorld calls.
// Something else to note is that modifications must be made anyway.
_drawQuad(a, b, Matrix3.Identity, shader);
// Something ELSE to note is that it's absolutely critical that this be calculated in the "right way" due to precision issues!
// Bit of an interesting little trick here - need to set things up correctly.
// 0, 0 in clip-space is the centre of the screen, and 1, 1 is the top-right corner.
var halfSize = vp.Size / 2.0f;
var uZero = vp.LocalToWorld(halfSize).Position;
var uX = vp.LocalToWorld(halfSize + (Vector2.UnitX * halfSize.X)).Position - uZero;
var uY = vp.LocalToWorld(halfSize - (Vector2.UnitY * halfSize.Y)).Position - uZero;
// Second modification is that output must be fov-centred (difference-space)
uZero -= fovCentre;
var clipToDiff = new Matrix3(ref uX, ref uY, ref uZero);
fovShader.SetUniformMaybe("clipToDiff", clipToDiff);
_drawQuad(Vector2.Zero, Vector2.One, Matrix3.Identity, fovShader);
}
private void UpdateOcclusionGeometry(MapId map, Box2 expandedBounds, Matrix3 eyeTransform)

View File

@@ -376,7 +376,7 @@ namespace Robust.Client.Graphics.Clyde
int divisions = Math.Max(16,(int)(radius * 16));
float arcLength = MathF.PI * 2 / divisions;
var filledTriangle = new Vector2[3];
Span<Vector2> filledTriangle = stackalloc Vector2[3];
// Draws a "circle", but its just a polygon with a bunch of sides
// this is the GL_LINES version, not GL_LINE_STRIP
@@ -389,9 +389,9 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.DrawLine(startPos, endPos, color);
else
{
filledTriangle[0] = startPos;
filledTriangle[1] = endPos;
filledTriangle[2] = Vector2.Zero;
filledTriangle[0] = startPos + position;
filledTriangle[1] = endPos + position;
filledTriangle[2] = Vector2.Zero + position;
_renderHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, filledTriangle, color);
}

View File

@@ -273,7 +273,11 @@ namespace Robust.Client.Graphics.Clyde
public Vector2i Size;
public bool IsSrgb;
#pragma warning disable 649
// Gets assigned by (currently commented out) GLContextAngle.
// It's fine don't worry about it.
public bool FlipY;
#pragma warning restore 649
public RTCF ColorFormat;

View File

@@ -28,7 +28,10 @@ namespace Robust.Client.Graphics.Clyde
private WindowReg? _mainWindow;
private IWindowingImpl? _windowing;
#pragma warning disable 414
// Keeping this for if/when we ever get a new renderer.
private Renderer _chosenRenderer;
#pragma warning restore 414
private ResourcePath? _windowIconPath;
private Thread? _windowingThread;

View File

@@ -206,6 +206,11 @@ namespace Robust.Client.Graphics.Clyde
GL.PrimitiveRestartIndex(PrimitiveRestartIndex);
CheckGlError();
}
if (_hasGLPrimitiveRestartFixedIndex)
{
GL.Enable(EnableCap.PrimitiveRestartFixedIndex);
CheckGlError();
}
if (!HasGLAnyVertexArrayObjects)
{
_sawmillOgl.Warning("NO VERTEX ARRAY OBJECTS! Things will probably go terribly, terribly wrong (no fallback path yet)");

View File

@@ -136,7 +136,7 @@ namespace Robust.Client.Graphics.Clyde
if (_clyde._windowMode == WindowMode.Fullscreen)
{
win.PrevWindowSize = win.WindowSize;
win.PrevWindowPos = win.PrevWindowPos;
win.PrevWindowPos = win.WindowPos;
SendCmd(new CmdWinSetFullscreen((nint) win.GlfwWindow));
}
@@ -262,7 +262,10 @@ namespace Robust.Client.Graphics.Clyde
var (reg, errorResult) = task.Result;
if (reg != null)
{
reg.Owner = reg.Handle;
return (reg, null);
}
var (desc, errCode) = errorResult!.Value;
return (null, $"[{errCode}]: {desc}");

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Map
{
internal class ClientMapManager : MapManager, IClientMapManager
{
public void ApplyGameStatePre(GameStateMapData? data, EntityState[]? entityStates)
public void ApplyGameStatePre(GameStateMapData? data, ReadOnlySpan<EntityState> entityStates)
{
// There was no map data this tick, so nothing to do.
if(data == null)
@@ -20,7 +20,7 @@ namespace Robust.Client.Map
// First we need to figure out all the NEW MAPS.
if(data.CreatedMaps != null)
{
DebugTools.Assert(entityStates is not null, "Received new maps, but no entity state.");
DebugTools.Assert(!entityStates.IsEmpty, "Received new maps, but no entity state.");
foreach (var mapId in data.CreatedMaps)
{
@@ -33,10 +33,7 @@ namespace Robust.Client.Map
//get shared euid of map comp entity
foreach (var entityState in entityStates!)
{
if(entityState.ComponentChanges is null)
continue;
foreach (var compChange in entityState.ComponentChanges)
foreach (var compChange in entityState.ComponentChanges.Span)
{
if (compChange.State is not MapComponentState mapCompState || mapCompState.MapId != mapId)
continue;
@@ -68,10 +65,7 @@ namespace Robust.Client.Map
//get shared euid of map comp entity
foreach (var entityState in entityStates!)
{
if (entityState.ComponentChanges is null)
continue;
foreach (var compState in entityState.ComponentChanges)
foreach (var compState in entityState.ComponentChanges.Span)
{
if (compState.State is not MapGridComponentState gridCompState || gridCompState.GridIndex != gridId)
continue;

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
@@ -8,7 +9,7 @@ namespace Robust.Client.Map
{
// Two methods here, so that new grids etc can be made BEFORE entities get states applied,
// but old ones can be deleted after.
void ApplyGameStatePre(GameStateMapData? data, EntityState[]? entityStates);
void ApplyGameStatePre(GameStateMapData? data, ReadOnlySpan<EntityState> entityStates);
void ApplyGameStatePost(GameStateMapData? data);
}
}

View File

@@ -0,0 +1,68 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics.Joints;
namespace Robust.Client.Physics
{
public sealed class JointSystem : SharedJointSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JointComponent, ComponentHandleState>(HandleComponentState);
}
private void HandleComponentState(EntityUid uid, JointComponent component, ref ComponentHandleState args)
{
if (args.Current is not JointComponent.JointComponentState jointState) return;
var changed = new List<string>();
foreach (var (existing, _) in component.Joints)
{
if (!jointState.Joints.ContainsKey(existing))
{
changed.Add(existing);
}
}
foreach (var removed in changed)
{
RemoveJoint(component.Joints[removed]);
}
foreach (var (id, state) in jointState.Joints)
{
Joint joint;
if (!component.Joints.ContainsKey(id))
{
// Add new joint (if possible).
// Need to wait for BOTH joint components to come in first before we can add it. Yay dependencies!
if (!EntityManager.HasComponent<JointComponent>(state.UidA) ||
!EntityManager.HasComponent<JointComponent>(state.UidB)) continue;
joint = state.GetJoint();
AddJoint(joint);
continue;
}
joint = state.GetJoint();
var existing = component.Joints[id];
if (!existing.Equals(joint))
{
existing.ApplyState(state);
component.Dirty();
}
}
if (changed.Count > 0)
{
component.Dirty();
}
}
}
}

View File

@@ -5,11 +5,8 @@ using Robust.Shared.Maths;
namespace Robust.Client.Placement.Modes
{
public class SnapgridBorder : PlacementMode
public class SnapgridBorder : SnapgridCenter
{
private bool onGrid;
private float snapSize;
public override bool HasLineMode => true;
public override bool HasGridMode => true;
@@ -17,79 +14,33 @@ namespace Robust.Client.Placement.Modes
{
}
public override void Render(DrawingHandleWorld handle)
{
if (onGrid)
{
const int ppm = EyeManager.PixelsPerMeter;
var viewportSize = (Vector2)pManager._clyde.ScreenSize;
var position = pManager.eyeManager.ScreenToMap(Vector2.Zero);
var gridstartx = (float) MathF.Round(position.X / snapSize, MidpointRounding.AwayFromZero) * snapSize;
var gridstarty = (float) MathF.Round(position.Y / snapSize, MidpointRounding.AwayFromZero) * snapSize;
var gridstart = pManager.eyeManager.WorldToScreen(
new Vector2( //Find snap grid closest to screen origin and convert back to screen coords
gridstartx,
gridstarty));
for (var a = gridstart.X;
a < viewportSize.X;
a += snapSize * ppm) //Iterate through screen creating gridlines
{
var from = ScreenToWorld(new Vector2(a, 0));
var to = ScreenToWorld(new Vector2(a, viewportSize.Y));
handle.DrawLine(from, to, new Color(0, 0, 1f));
}
for (var a = gridstart.Y; a < viewportSize.Y; a += snapSize * ppm)
{
var from = ScreenToWorld(new Vector2(0, a));
var to = ScreenToWorld(new Vector2(viewportSize.X, a));
handle.DrawLine(from, to, new Color(0, 0, 1f));
}
}
// Draw grid BELOW the ghost.
base.Render(handle);
}
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
snapSize = 1f;
var gridId = MouseCoords.GetGridId(pManager.EntityManager);
SnapSize = 1f;
if (gridId.IsValid())
{
snapSize = pManager.MapManager.GetGrid(gridId).TileSize; //Find snap size for the grid.
onGrid = true;
Grid = pManager.MapManager.GetGrid(gridId);
SnapSize = Grid.TileSize; //Find snap size for the grid.
}
else
{
onGrid = false;
Grid = null;
}
GridDistancing = snapSize;
GridDistancing = SnapSize;
var mouselocal = new Vector2( //Round local coordinates onto the snap grid
(float) MathF.Round(MouseCoords.X / snapSize, MidpointRounding.AwayFromZero) * snapSize,
(float) MathF.Round(MouseCoords.Y / snapSize, MidpointRounding.AwayFromZero) * snapSize);
(float) MathF.Round(MouseCoords.X / SnapSize, MidpointRounding.AwayFromZero) * SnapSize,
(float) MathF.Round(MouseCoords.Y / SnapSize, MidpointRounding.AwayFromZero) * SnapSize);
//Convert back to original world and screen coordinates after applying offset
MouseCoords =
new EntityCoordinates(
MouseCoords.EntityId, mouselocal + new Vector2(pManager.PlacementOffset.X, pManager.PlacementOffset.Y));
}
public override bool IsValidPosition(EntityCoordinates position)
{
if (!RangeCheck(position))
{
return false;
}
return true;
}
}
}

View File

@@ -1,5 +1,7 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -7,8 +9,8 @@ namespace Robust.Client.Placement.Modes
{
public class SnapgridCenter : PlacementMode
{
bool onGrid;
float snapSize;
protected IMapGrid? Grid;
protected float SnapSize;
public override bool HasLineMode => true;
public override bool HasGridMode => true;
@@ -17,22 +19,22 @@ namespace Robust.Client.Placement.Modes
public override void Render(DrawingHandleWorld handle)
{
if (onGrid)
if (Grid != null)
{
var viewportSize = (Vector2)pManager._clyde.ScreenSize;
var position = pManager.eyeManager.ScreenToMap(Vector2.Zero);
var gridPosition = Grid.MapToGrid(pManager.eyeManager.ScreenToMap(Vector2.Zero));
var gridstart = pManager.eyeManager.WorldToScreen(new Vector2( //Find snap grid closest to screen origin and convert back to screen coords
(float)(MathF.Round(position.X / snapSize - 0.5f, MidpointRounding.AwayFromZero) + 0.5f) * snapSize,
(float)(MathF.Round(position.Y / snapSize - 0.5f, MidpointRounding.AwayFromZero) + 0.5f) * snapSize));
for (var a = gridstart.X; a < viewportSize.X; a += snapSize * 32) //Iterate through screen creating gridlines
var gridstart = pManager.eyeManager.CoordinatesToScreen(
gridPosition.WithPosition(new Vector2(MathF.Floor(gridPosition.X), MathF.Floor(gridPosition.Y))));
for (var a = gridstart.X; a < viewportSize.X; a += SnapSize * EyeManager.PixelsPerMeter) //Iterate through screen creating gridlines
{
var from = ScreenToWorld(new Vector2(a, 0));
var to = ScreenToWorld(new Vector2(a, viewportSize.Y));
handle.DrawLine(from, to, new Color(0, 0, 1f));
}
for (var a = gridstart.Y; a < viewportSize.Y; a += snapSize * 32)
for (var a = gridstart.Y; a < viewportSize.Y; a += SnapSize * EyeManager.PixelsPerMeter)
{
var from = ScreenToWorld(new Vector2(0, a));
var to = ScreenToWorld(new Vector2(viewportSize.X, a));
@@ -49,22 +51,22 @@ namespace Robust.Client.Placement.Modes
MouseCoords = ScreenToCursorGrid(mouseScreen);
var gridId = MouseCoords.GetGridId(pManager.EntityManager);
snapSize = 1f;
SnapSize = 1f;
if (gridId.IsValid())
{
snapSize = pManager.MapManager.GetGrid(gridId).TileSize; //Find snap size for the grid.
onGrid = true;
Grid = pManager.MapManager.GetGrid(gridId);
SnapSize = Grid.TileSize; //Find snap size for the grid.
}
else
{
onGrid = false;
Grid = null;
}
GridDistancing = snapSize;
GridDistancing = SnapSize;
var mouseLocal = new Vector2( //Round local coordinates onto the snap grid
(float)(MathF.Round((MouseCoords.Position.X / snapSize - 0.5f), MidpointRounding.AwayFromZero) + 0.5) * snapSize,
(float)(MathF.Round((MouseCoords.Position.Y / snapSize - 0.5f), MidpointRounding.AwayFromZero) + 0.5) * snapSize);
(float)(MathF.Round((MouseCoords.Position.X / SnapSize - 0.5f), MidpointRounding.AwayFromZero) + 0.5) * SnapSize,
(float)(MathF.Round((MouseCoords.Position.Y / SnapSize - 0.5f), MidpointRounding.AwayFromZero) + 0.5) * SnapSize);
//Adjust mouseCoords to new calculated position
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, mouseLocal + new Vector2(pManager.PlacementOffset.X, pManager.PlacementOffset.Y));

View File

@@ -233,7 +233,7 @@ namespace Robust.Client.Placement
bounds.Width,
bounds.Height);
return EntitySystem.Get<SharedBroadphaseSystem>().TryCollideRect(collisionBox, mapCoords.MapId);
return EntitySystem.Get<SharedPhysicsSystem>().TryCollideRect(collisionBox, mapCoords.MapId);
}
protected Vector2 ScreenToWorld(Vector2 point)

View File

@@ -0,0 +1,26 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Client.Player
{
internal class FilterSystem : SharedFilterSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override Filter FromEntities(Filter filter, params EntityUid[] entities)
{
if (_playerManager.LocalPlayer is not { } localPlayer
|| localPlayer.Session.AttachedEntityUid is not {} attachedUid)
return filter;
foreach (var uid in entities)
{
if (uid == attachedUid)
filter.AddPlayer(localPlayer.Session);
}
return filter;
}
}
}

View File

@@ -24,7 +24,7 @@ namespace Robust.Client.Player
void Startup();
void Shutdown();
void ApplyPlayerStates(IEnumerable<PlayerState>? list);
void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list);
}
public class LocalPlayerChangedEventArgs : EventArgs

View File

@@ -103,9 +103,9 @@ namespace Robust.Client.Player
}
/// <inheritdoc />
public void ApplyPlayerStates(IEnumerable<PlayerState>? list)
public void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list)
{
if (list == null)
if (list.Count == 0)
{
// This happens when the server says "nothing changed!"
return;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics.Contracts;
using Robust.Shared.Maths;
@@ -20,6 +20,8 @@ namespace Robust.Client.UserInterface.Controls
public int ScrollSpeedX { get; set; } = 50;
public int ScrollSpeedY { get; set; } = 50;
public bool ReturnMeasure { get; set; } = false;
public ScrollContainer()
{
MouseFilter = MouseFilterMode.Pass;
@@ -89,11 +91,18 @@ namespace Robust.Client.UserInterface.Controls
size = Vector2.ComponentMax(size, child.DesiredSize);
}
// Unlike WPF/Avalonia we report ZERO here instead of available size.
// This is to fix a bunch of jank with e.g. BoxContainer.
// Tbh this might be a mistake.
// DockPanel when.
return Vector2.Zero;
// Unlike WPF/Avalonia we default to reporting ZERO here instead of available size. This is to fix a bunch
// of jank with e.g. BoxContainer.
if (!ReturnMeasure)
return Vector2.Zero;
if (_vScrollEnabled)
size.X += _vScrollBar.DesiredSize.X;
if (_hScrollEnabled)
size.Y += _hScrollBar.DesiredSize.Y;
return size;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)

View File

@@ -450,7 +450,7 @@ namespace Robust.Client.UserInterface.CustomControls
foreach (var child in Children)
{
child.Arrange(UIBox2.FromDimensions(0, offset, Width, height));
child.Arrange(UIBox2.FromDimensions(0, offset, finalSize.X, height));
offset += Separation + height;
}

View File

@@ -33,7 +33,7 @@ namespace Robust.Client.UserInterface
public string Description => "A";
public string Help => "A";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
var monitor = clyde.EnumerateMonitors().First();

View File

@@ -552,7 +552,7 @@ namespace Robust.Server
if (_config.GetCVar(CVars.LogRuntimeLog))
{
// Wrtie down exception log
// Write down exception log
var logPath = _config.GetCVar(CVars.LogPath);
var relPath = PathHelpers.ExecutableRelativeFile(logPath);
Directory.CreateDirectory(relPath);

View File

@@ -153,11 +153,11 @@ namespace Robust.Server.Console.Commands
{
var network = IoCManager.Resolve<IServerNetManager>();
var reason = "Kicked by console.";
string reason;
if (args.Length >= 2)
{
reason = reason + args[1];
}
reason = $"Kicked by console: {string.Join(' ', args[1..])}";
else
reason = "Kicked by console";
network.DisconnectChannel(target.ConnectedClient, reason);
}

View File

@@ -331,24 +331,18 @@ namespace Robust.Server.Console.Commands
fixture.CollisionLayer = 2;
}
// TODO: Should Joints be their own entities? Box2D just adds them directly to the world.
// HMMM
// At the least it should be its own damn component
var revolute = new RevoluteJoint(ground, body)
{
LocalAnchorA = new Vector2(0f, 10f),
LocalAnchorB = new Vector2(0f, 0f),
ReferenceAngle = 0f,
MotorSpeed = 0.05f * MathF.PI,
MaxMotorTorque = 100000000f,
EnableMotor = true
};
body.AddJoint(revolute);
var revolute = EntitySystem.Get<SharedJointSystem>().CreateRevoluteJoint(ground.Owner.Uid, body.Owner.Uid);
revolute.LocalAnchorA = new Vector2(0f, 10f);
revolute.LocalAnchorB = new Vector2(0f, 0f);
revolute.ReferenceAngle = 0f;
revolute.MotorSpeed = 0.05f * MathF.PI;
revolute.MaxMotorTorque = 100000000f;
revolute.EnableMotor = true;
// Box2D has this as 800 which is jesus christo.
// Wouldn't recommend higher than 100 in debug and higher than 300 on release unless
// you really want a profile.
var count = 200;
var count = 300;
var mapManager = IoCManager.Resolve<IMapManager>();
for (var i = 0; i < count; i++)

View File

@@ -1,8 +1,6 @@
using System.Diagnostics;
using JetBrains.Annotations;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
@@ -11,21 +9,19 @@ namespace Robust.Server.Debugging
[UsedImplicitly]
internal class DebugDrawingManager : IDebugDrawingManager, IEntityEventSubscriber
{
[Dependency] private readonly IServerNetManager _net = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
public void Initialize()
{
_net.RegisterNetMessage<MsgRay>();
#if DEBUG
IoCManager.Resolve<IEntityManager>().EventBus.SubscribeEvent<DebugDrawRayMessage>(EventSource.Local, this, PhysicsOnDebugDrawRay);
_entManager.EventBus.SubscribeEvent<DebugDrawRayMessage>(EventSource.Local, this, PhysicsOnDebugDrawRay);
#endif
}
private void PhysicsOnDebugDrawRay(DebugDrawRayMessage @event)
{
var data = @event.Data;
var msg = _net.CreateNetMessage<MsgRay>();
msg.RayOrigin = data.Ray.Position;
var msg = new MsgRay {RayOrigin = data.Ray.Position};
if (data.Results != null)
{
msg.DidHit = true;
@@ -36,7 +32,7 @@ namespace Robust.Server.Debugging
msg.RayHit = data.Ray.Position + data.Ray.Direction * data.MaxLength;
}
_net.ServerSendToAll(msg);
_entManager.EventBus.RaiseEvent(EventSource.Network, msg);
}
}
}

View File

@@ -18,7 +18,7 @@ namespace Robust.Server.GameObjects
private Vector2 _zoom = Vector2.One;
private Vector2 _offset;
private Angle _rotation;
private uint _visibilityMask;
private uint _visibilityMask = 1;
public override bool DrawFov
{

View File

@@ -105,7 +105,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// All of the sessions currently subscribed to this UserInterface.
/// </summary>
public IEnumerable<IPlayerSession> SubscribedSessions => _subscribedSessions;
public IReadOnlyCollection<IPlayerSession> SubscribedSessions => _subscribedSessions;
public event Action<ServerBoundUserInterfaceMessage>? OnReceiveMessage;
public event Action<IPlayerSession>? OnClosed;

View File

@@ -3,6 +3,7 @@ using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
@@ -10,8 +11,10 @@ using Robust.Shared.Players;
namespace Robust.Server.GameObjects
{
[UsedImplicitly]
public class AudioSystem : EntitySystem, IAudioSystem
public class AudioSystem : SharedAudioSystem, IAudioSystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private const int AudioDistanceRange = 25;
private uint _streamIndex;
@@ -101,10 +104,13 @@ namespace Robust.Server.GameObjects
var id = CacheIdentifier();
var fallbackCoordinates = GetFallbackCoordinates(transform.MapPosition);
var msg = new PlayAudioEntityMessage
{
FileName = filename,
Coordinates = transform.Coordinates,
FallbackCoordinates = fallbackCoordinates,
EntityUid = uid,
AudioParams = audioParams ?? AudioParams.Default,
Identifier = id,
@@ -126,10 +132,14 @@ namespace Robust.Server.GameObjects
var range = audioParams is null || audioParams.Value.MaxDistance <= 0 ? AudioDistanceRange : audioParams.Value.MaxDistance;
var id = CacheIdentifier();
var fallbackCoordinates = GetFallbackCoordinates(coordinates.ToMap(_entityManager));
var msg = new PlayAudioPositionalMessage
{
FileName = filename,
Coordinates = coordinates,
FallbackCoordinates = fallbackCoordinates,
AudioParams = audioParams ?? AudioParams.Default,
Identifier = id
};

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
internal sealed class DebugPhysicsSystem : SharedDebugPhysicsSystem
{
}
}

View File

@@ -5,6 +5,7 @@ using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.ViewVariables;
namespace Robust.Server.GameObjects
@@ -120,102 +121,139 @@ namespace Robust.Server.GameObjects
#region Proxy Methods
public bool HasUi(EntityUid uid, object uiKey)
public bool HasUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!EntityManager.TryGetComponent<ServerUserInterfaceComponent>(uid, out var ui))
if (!Resolve(uid, ref ui))
return false;
return ui.HasBoundUserInterface(uiKey);
}
public BoundUserInterface GetUi(EntityUid uid, object uiKey)
public BoundUserInterface GetUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
{
return EntityManager.GetComponent<ServerUserInterfaceComponent>(uid).GetBoundUserInterface(uiKey);
if (!Resolve(uid, ref ui))
throw new InvalidOperationException($"Cannot get {typeof(BoundUserInterface)} from an entity without {typeof(ServerUserInterfaceComponent)}!");
return ui.GetBoundUserInterface(uiKey);
}
public BoundUserInterface? GetUiOrNull(EntityUid uid, object uiKey)
public BoundUserInterface? GetUiOrNull(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
{
return TryGetUi(uid, uiKey, out var ui)
? ui
return TryGetUi(uid, uiKey, out var bui)
? bui
: null;
}
public bool TryGetUi(EntityUid uid, object uiKey, [NotNullWhen(true)] out BoundUserInterface? ui)
public bool TryGetUi(EntityUid uid, object uiKey, [NotNullWhen(true)] out BoundUserInterface? bui, ServerUserInterfaceComponent? ui = null)
{
ui = null;
bui = null;
return EntityManager.TryGetComponent(uid, out ServerUserInterfaceComponent uiComp)
&& uiComp.TryGetBoundUserInterface(uiKey, out ui);
return Resolve(uid, ref ui) && ui.TryGetBoundUserInterface(uiKey, out bui);
}
public bool TrySetUiState(EntityUid uid, object uiKey, BoundUserInterfaceState state, IPlayerSession? session = null)
public bool IsUiOpen(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
ui.SetState(state, session);
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.SubscribedSessions.Count > 0;
}
public bool TrySetUiState(EntityUid uid, object uiKey, BoundUserInterfaceState state, IPlayerSession? session = null, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
bui.SetState(state, session);
return true;
}
public bool TryToggleUi(EntityUid uid, object uiKey, IPlayerSession session)
public bool TryToggleUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
ui.Toggle(session);
if (!TryGetUi(uid, uiKey, out var bui))
return false;
bui.Toggle(session);
return true;
}
public bool TryOpen(EntityUid uid, object uiKey, IPlayerSession session)
public bool TryOpen(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
return ui.Open(session);
if (!TryGetUi(uid, uiKey, out var bui))
return false;
return bui.Open(session);
}
public bool TryClose(EntityUid uid, object uiKey, IPlayerSession session)
public bool TryClose(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
return ui.Close(session);
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.Close(session);
}
public bool TryCloseAll(EntityUid uid, object uiKey)
public bool TryCloseAll(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
ui.CloseAll();
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
bui.CloseAll();
return true;
}
public bool SessionHasOpenUi(EntityUid uid, object uiKey, IPlayerSession session)
public bool SessionHasOpenUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
return ui.SessionHasOpen(session);
if (!TryGetUi(uid, uiKey, out var bui))
return false;
return bui.SessionHasOpen(session);
}
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message)
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
ui.SendMessage(message);
if (!TryGetUi(uid, uiKey, out var bui))
return false;
bui.SendMessage(message);
return true;
}
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, IPlayerSession session)
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var ui))
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
try
{
ui.SendMessage(message, session);
bui.SendMessage(message, session);
}
catch (ArgumentException)
{

View File

@@ -1,10 +1,14 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
namespace Robust.Server.GameObjects
{
public interface IServerEntityNetworkManager : IEntityNetworkManager
{
uint GetLastMessageSequence(IPlayerSession session);
List<ushort> GetDeletedComponents(EntityUid uid, GameTick fromTick);
void CullDeletionHistory(GameTick oldestAck);
}
}

View File

@@ -102,6 +102,8 @@ namespace Robust.Server.GameObjects
private readonly Dictionary<IPlayerSession, uint> _lastProcessedSequencesCmd =
new();
private readonly Dictionary<EntityUid, List<(GameTick tick, ushort netId)>> _componentDeletionHistory = new();
private bool _logLateMsgs;
/// <inheritdoc />
@@ -109,6 +111,10 @@ namespace Robust.Server.GameObjects
{
_networkManager.RegisterNetMessage<MsgEntity>(HandleEntityNetworkMessage);
// For syncing component deletions.
EntityDeleted += OnEntityRemoved;
ComponentRemoved += OnComponentRemoved;
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_configurationManager.OnValueChanged(CVars.NetLogLateMsg, b => _logLateMsgs = b, true);
@@ -135,6 +141,65 @@ namespace Robust.Server.GameObjects
return _lastProcessedSequencesCmd[session];
}
private void OnEntityRemoved(object? sender, EntityUid e)
{
if (_componentDeletionHistory.ContainsKey(e))
_componentDeletionHistory.Remove(e);
}
private void OnComponentRemoved(object? sender, ComponentEventArgs e)
{
var reg = ComponentFactory.GetRegistration(e.Component.GetType());
// We only keep track of networked components being removed.
if (reg.NetID is not {} netId)
return;
var uid = e.OwnerUid;
if (!_componentDeletionHistory.TryGetValue(uid, out var list))
{
list = new List<(GameTick tick, ushort netId)>();
_componentDeletionHistory[uid] = list;
}
list.Add((_gameTiming.CurTick, netId));
}
public List<ushort> GetDeletedComponents(EntityUid uid, GameTick fromTick)
{
// TODO: Maybe make this a struct enumerator? Right now it's a list for consistency...
var list = new List<ushort>();
if (!_componentDeletionHistory.TryGetValue(uid, out var history))
return list;
foreach (var (tick, id) in history)
{
if (tick >= fromTick) list.Add(id);
}
return list;
}
public void CullDeletionHistory(GameTick oldestAck)
{
var remQueue = new RemQueue<EntityUid>();
foreach (var (uid, list) in _componentDeletionHistory)
{
list.RemoveAll(hist => hist.tick < oldestAck);
if(list.Count == 0)
remQueue.Add(uid);
}
foreach (var uid in remQueue)
{
_componentDeletionHistory.Remove(uid);
}
}
/// <inheritdoc />
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public void SendComponentNetworkMessage(INetChannel? channel, IEntity entity, IComponent component,

View File

@@ -17,7 +17,7 @@ namespace Robust.Server.GameStates
{
internal sealed class EntityViewCulling
{
private const int ViewSetCapacity = 128; // starting number of entities that are in view
private const int ViewSetCapacity = 256; // starting number of entities that are in view
private const int PlayerSetSize = 64; // Starting number of players
private const int MaxVisPoolSize = 1024; // Maximum number of pooled objects
@@ -41,11 +41,18 @@ namespace Robust.Server.GameStates
private readonly List<(GameTick tick, EntityUid uid)> _deletionHistory = new();
private readonly ObjectPool<HashSet<EntityUid>> _visSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
= new DefaultObjectPool<HashSet<EntityUid>>(new VisSetPolicy(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _viewerEntsPool
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
private readonly ObjectPool<Dictionary<GridId, HashSet<IMapChunkInternal>>> _includedChunksPool =
new DefaultObjectPool<Dictionary<GridId, HashSet<IMapChunkInternal>>>(new DefaultPooledObjectPolicy<Dictionary<GridId, HashSet<IMapChunkInternal>>>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<IMapChunkInternal>> _chunkPool =
new DefaultObjectPool<HashSet<IMapChunkInternal>>(
new DefaultPooledObjectPolicy<HashSet<IMapChunkInternal>>(), MaxVisPoolSize);
private ushort _transformNetId = 0;
/// <summary>
@@ -58,6 +65,22 @@ namespace Robust.Server.GameStates
/// </summary>
public float ViewSize { get; set; }
private sealed class VisSetPolicy : PooledObjectPolicy<HashSet<EntityUid>>
{
public override HashSet<EntityUid> Create()
{
return new(ViewSetCapacity);
}
public override bool Return(HashSet<EntityUid> obj)
{
// TODO: This clear can be pretty expensive so maybe make a custom datatype given we're swapping
// 70 - 300 entities a tick? Or do we even need to clear given it's just value types?
obj.Clear();
return true;
}
}
public EntityViewCulling(IServerEntityManager entMan, IMapManager mapManager, IEntityLookup lookup)
{
_entMan = entMan;
@@ -97,7 +120,9 @@ namespace Robust.Server.GameStates
// Not thread safe
public void AddPlayer(ICommonSession session)
{
_playerVisibleSets.Add(session, new HashSet<EntityUid>(ViewSetCapacity));
var visSet = _visSetPool.Get();
_playerVisibleSets.Add(session, visSet);
PlayerChunks.Add(session, new Dictionary<IMapChunkInternal, GameTick>(32));
_streamingChunks.Add(session, new ChunkStreamingData());
}
@@ -193,7 +218,7 @@ namespace Robust.Server.GameStates
foreach (var eyeEuid in viewers)
{
var includedChunks = new Dictionary<GridId, HashSet<IMapChunkInternal>>();
var includedChunks = _includedChunksPool.Get();
var (viewBox, mapId) = CalcViewBounds(in eyeEuid);
@@ -201,18 +226,14 @@ namespace Robust.Server.GameStates
if (_entMan.TryGetComponent<EyeComponent>(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
//Always include the map entity of the eye, if it exists.
if(_mapManager.MapExists(mapId))
visibleEnts.Add(_mapManager.GetMapEntityId(mapId));
//Always include viewable ent itself
visibleEnts.Add(eyeEuid);
RecursiveAdd(eyeEuid, visibleEnts, includedChunks, chunksSeen, visMask);
// grid entity should be added through this
// assume there are no deleted ents in here, cull them first in ent/comp manager
_lookup.FastEntitiesIntersecting(in mapId, ref viewBox, entity =>
{
RecursiveAdd(entity.Uid, visibleEnts, includedChunks, visMask);
RecursiveAdd(entity.Uid, visibleEnts, includedChunks, chunksSeen, visMask);
}, LookupFlags.None);
//Calculate states for all visible anchored ents
@@ -222,6 +243,15 @@ namespace Robust.Server.GameStates
// To fix pop-in we'll go through nearby chunks and send them little-by-little
StreamChunks(newChunkCount, session, chunksSeen, entityStates, viewBox, fromTick, mapId);
foreach (var (_, chunks) in includedChunks)
{
chunks.Clear();
_chunkPool.Return(chunks);
}
includedChunks.Clear();
_includedChunksPool.Return(includedChunks);
}
viewers.Clear();
@@ -242,14 +272,16 @@ namespace Robust.Server.GameStates
Dictionary<IMapChunkInternal, GameTick> chunksSeen,
Dictionary<GridId, HashSet<IMapChunkInternal>> includedChunks)
{
foreach (var publicMapGrid in _mapManager.FindGridsIntersecting(mapId, viewBox))
_mapManager.FindGridsIntersectingEnumerator(mapId, viewBox, out var gridEnumerator, true);
while (gridEnumerator.MoveNext(out var mapGrid))
{
var grid = (IMapGridInternal)publicMapGrid;
var grid = (IMapGridInternal) mapGrid;
grid.GetMapChunks(viewBox, out var enumerator);
// Can't really check when grid was modified here because we may need to dump new chunks on the person
// as right now if you make a new chunk the client never actually gets these entities here.
foreach (var chunk in grid.GetMapChunks(viewBox))
while (enumerator.MoveNext(out var chunk))
{
// for each chunk, check dirty
if (chunksSeen.TryGetValue(chunk, out var chunkSeen) && chunk.LastAnchoredModifiedTick < chunkSeen)
@@ -257,7 +289,7 @@ namespace Robust.Server.GameStates
if (!includedChunks.TryGetValue(grid.Index, out var chunks))
{
chunks = new HashSet<IMapChunkInternal>();
chunks = _chunkPool.Get();
includedChunks[grid.Index] = chunks;
}
@@ -280,7 +312,7 @@ namespace Robust.Server.GameStates
foreach (var (gridId, chunks) in includedChunks)
{
// at least 1 anchored entity is going to be added, so add the grid (all anchored ents are parented to grid)
RecursiveAdd(_mapManager.GetGrid(gridId).GridEntityId, visibleEnts, includedChunks, visMask);
RecursiveAdd(_mapManager.GetGrid(gridId).GridEntityId, visibleEnts, includedChunks, chunksSeen, visMask);
foreach (var chunk in chunks)
{
@@ -290,6 +322,7 @@ namespace Robust.Server.GameStates
lastSeenChunk = GameTick.Zero;
count++;
}
// Assume we've already done the chunkdirty check.
chunk.FastGetAllAnchoredEnts(uid =>
{
@@ -372,12 +405,14 @@ namespace Robust.Server.GameStates
// Find a new chunk to start streaming in range.
var enlarged = viewBox.Enlarged(StreamRange);
_mapManager.FindGridsIntersectingEnumerator(mapId, enlarged, out var gridEnumerator, true);
foreach (var publicMapGrid in _mapManager.FindGridsIntersecting(mapId, enlarged))
while (gridEnumerator.MoveNext(out var mapGrid))
{
var grid = (IMapGridInternal) publicMapGrid;
var grid = (IMapGridInternal) mapGrid;
grid.GetMapChunks(enlarged, out var enumerator);
foreach (var chunk in grid.GetMapChunks(enlarged))
while (enumerator.MoveNext(out var chunk))
{
// if we've ever seen this chunk don't worry about it.
if (chunksSeen.ContainsKey(chunk))
@@ -470,7 +505,6 @@ namespace Robust.Server.GameStates
// swap out vis sets
_playerVisibleSets[session] = currentSet;
previousSet.Clear();
_visSetPool.Return(previousSet);
}
@@ -496,7 +530,7 @@ namespace Robust.Server.GameStates
// Read Safe
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private bool RecursiveAdd(EntityUid uid, HashSet<EntityUid> visSet, Dictionary<GridId, HashSet<IMapChunkInternal>> includedChunks, uint visMask)
private bool RecursiveAdd(EntityUid uid, HashSet<EntityUid> visSet, Dictionary<GridId, HashSet<IMapChunkInternal>> includedChunks, Dictionary<IMapChunkInternal, GameTick> chunksSeen, uint visMask)
{
// we are done, this ent has already been checked and is visible
if (visSet.Contains(uid))
@@ -523,7 +557,7 @@ namespace Robust.Server.GameStates
// parent is already in the set
if (visSet.Contains(parentUid))
{
EnsureAnchoredChunk(xform, includedChunks);
EnsureAnchoredChunk(xform, includedChunks, chunksSeen);
if (!xform.Anchored)
visSet.Add(uid);
@@ -531,10 +565,10 @@ namespace Robust.Server.GameStates
}
// parent was not added, so we are not either
if (!RecursiveAdd(parentUid, visSet, includedChunks, visMask))
if (!RecursiveAdd(parentUid, visSet, includedChunks, chunksSeen, visMask))
return false;
EnsureAnchoredChunk(xform, includedChunks);
EnsureAnchoredChunk(xform, includedChunks, chunksSeen);
// add us
if (!xform.Anchored)
@@ -546,7 +580,7 @@ namespace Robust.Server.GameStates
/// <summary>
/// If we recursively get an anchored entity need to ensure the entire chunk is included (as it may be out of view).
/// </summary>
private void EnsureAnchoredChunk(TransformComponent xform, Dictionary<GridId, HashSet<IMapChunkInternal>> includedChunks)
private void EnsureAnchoredChunk(TransformComponent xform, Dictionary<GridId, HashSet<IMapChunkInternal>> includedChunks, Dictionary<IMapChunkInternal, GameTick> chunksSeen)
{
// If we recursively get an anchored entity need to ensure the entire chunk is included (as it may be out of view).
if (!xform.Anchored) return;
@@ -556,9 +590,13 @@ namespace Robust.Server.GameStates
var local = xform.Coordinates;
var chunk = mapGrid.GetChunk(mapGrid.LocalToChunkIndices(local));
// Don't need to worry about getting the parent as we've already seen it before from this chunk.
if (chunksSeen.TryGetValue(chunk, out var lastSeen) && lastSeen >= chunk.LastAnchoredModifiedTick)
return;
if (!includedChunks.TryGetValue(xform.GridID, out var chunks))
{
chunks = new HashSet<IMapChunkInternal>();
chunks = _chunkPool.Get();
includedChunks[xform.GridID] = chunks;
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Map;
@@ -172,6 +172,7 @@ namespace Robust.Server.GameStates
if (!_networkManager.IsConnected)
{
// Prevent deletions piling up if we have no clients.
_entityManager.CullDeletionHistory(GameTick.MaxValue);
_entityView.CullDeletionHistory(GameTick.MaxValue);
_mapManager.CullDeletionHistory(GameTick.MaxValue);
return;
@@ -179,20 +180,20 @@ namespace Robust.Server.GameStates
var inputSystem = _systemManager.GetEntitySystem<InputSystem>();
var oldestAck = GameTick.MaxValue;
var oldestAckValue = GameTick.MaxValue.Value;
var mainThread = Thread.CurrentThread;
(MsgState, INetChannel) GenerateMail(IPlayerSession session)
var parentDeps = IoCManager.Instance!;
void SendStateUpdate(IPlayerSession session)
{
// KILL IT WITH FIRE
if(mainThread != Thread.CurrentThread)
IoCManager.InitThread(new DependencyCollection(), true);
IoCManager.InitThread(new DependencyCollection(parentDeps), true);
// people not in the game don't get states
if (session.Status != SessionStatus.InGame)
{
return default;
}
return;
var channel = session.ConnectedClient;
@@ -208,13 +209,11 @@ namespace Robust.Server.GameStates
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
var lastInputCommand = inputSystem.GetLastInputCommand(session);
var lastSystemMessage = _entityNetworkManager.GetLastMessageSequence(session);
var state = new GameState(lastAck, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage), entStates?.ToArray(), playerStates?.ToArray(), deletions?.ToArray(), mapData);
if (lastAck < oldestAck)
{
oldestAck = lastAck;
}
var state = new GameState(lastAck, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage), entStates, playerStates, deletions, mapData);
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.");
InterlockedHelper.Min(ref oldestAckValue, lastAck.Value);
DebugTools.Assert(state.MapData?.CreatedMaps is null || (state.MapData?.CreatedMaps is not null && state.EntityStates.HasContents), "Sending new maps, but no entity state.");
// actually send the state
var stateUpdateMessage = _networkManager.CreateNetMessage<MsgState>();
@@ -226,45 +225,35 @@ namespace Robust.Server.GameStates
// When we send them reliably, we immediately update the ack so that the next state will not be huge.
if (stateUpdateMessage.ShouldSendReliably())
{
_ackedStates[channel.ConnectionId] = _gameTiming.CurTick;
// TODO: remove this lock by having a single state object per session that contains all per-session state needed.
lock (_ackedStates)
{
_ackedStates[channel.ConnectionId] = _gameTiming.CurTick;
}
}
return (stateUpdateMessage, channel);
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
}
(MsgState, INetChannel?) SafeGenerateMail(IPlayerSession session)
Parallel.ForEach(_playerManager.GetAllPlayers(), session =>
{
try
{
return GenerateMail(session);
SendStateUpdate(session);
}
catch (Exception e) // Catch EVERY exception
{
_logger.Log(LogLevel.Error, e, "Caught exception while generating mail.");
}
});
var msg = _networkManager.CreateNetMessage<MsgState>();
msg.MsgChannel = session.ConnectedClient;
return (msg, null);
}
var mailBag = _playerManager.GetAllPlayers()
.Where(s => s.Status == SessionStatus.InGame)
.AsParallel()
.Select(SafeGenerateMail);
foreach (var (msg, chan) in mailBag)
{
// see session.Status != SessionStatus.InGame above
if (chan == null) continue;
_networkManager.ServerSendMessage(msg, chan);
}
var oldestAck = new GameTick(oldestAckValue);
// keep the deletion history buffers clean
if (oldestAck > _lastOldestAck)
{
_lastOldestAck = oldestAck;
_entityManager.CullDeletionHistory(oldestAck);
_entityView.CullDeletionHistory(oldestAck);
_mapManager.CullDeletionHistory(oldestAck);
}
@@ -278,7 +267,7 @@ namespace Robust.Server.GameStates
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
/// <param name="fromTick">Only provide delta changes from this tick.</param>
/// <returns>New entity State for the given entity.</returns>
internal static EntityState GetEntityState(IEntityManager entMan, ICommonSession player, EntityUid entityUid, GameTick fromTick)
internal static EntityState GetEntityState(IServerEntityManager entMan, ICommonSession player, EntityUid entityUid, GameTick fromTick)
{
var bus = entMan.EventBus;
var changed = new List<ComponentChange>();
@@ -310,12 +299,11 @@ namespace Robust.Server.GameStates
{
changed.Add(ComponentChange.Changed(netId, entMan.GetComponentState(bus, component, player)));
}
else if (component.Deleted && component.LastModifiedTick >= fromTick)
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChange.Removed(netId));
}
}
foreach (var netId in entMan.GetDeletedComponents(entityUid, fromTick))
{
changed.Add(ComponentChange.Removed(netId));
}
return new EntityState(entityUid, changed.ToArray());
@@ -324,7 +312,7 @@ namespace Robust.Server.GameStates
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
internal static List<EntityState>? GetAllEntityStates(IEntityManager entityMan, ICommonSession player, GameTick fromTick)
internal static List<EntityState>? GetAllEntityStates(IServerEntityManager entityMan, ICommonSession player, GameTick fromTick)
{
var stateEntities = new List<EntityState>();
foreach (var entity in entityMan.GetEntities())

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics.Joints;
namespace Robust.Server.Physics
{
public sealed class JointSystem : SharedJointSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JointComponent, ComponentGetState>(GetCompState);
}
private void GetCompState(EntityUid uid, JointComponent component, ref ComponentGetState args)
{
var states = new Dictionary<string, JointState>(component.Joints.Count);
foreach (var (_, joint) in component.Joints)
{
states.Add(joint.ID, joint.GetState());
}
args.State = new JointComponent.JointComponentState(states);
}
}
}

View File

@@ -0,0 +1,20 @@
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Robust.Server.Player
{
internal class FilterSystem : SharedFilterSystem
{
public override Filter FromEntities(Filter filter, params EntityUid[] entities)
{
foreach (var uid in entities)
{
if (EntityManager.TryGetComponent(uid, out ActorComponent? actor))
filter.AddPlayer(actor.PlayerSession);
}
return filter;
}
}
}

View File

@@ -148,7 +148,7 @@ namespace Robust.Server.Player
if (!EntitySystem.Get<ActorSystem>().Detach(AttachedEntity))
{
Logger.Warning($"Couldn't detach player \"{this}\" to entity \"{AttachedEntity}\"! Is it missing an ActorComponent?");
Logger.Warning($"Couldn't detach player \"{this}\" from entity \"{AttachedEntity}\"! Is it missing an ActorComponent?");
}
}

View File

@@ -84,25 +84,62 @@ namespace Robust.Shared.Maths
return ang;
}
private static Vector2[] directionVectors = new[]
{
new Vector2(0, -1),
private static readonly Vector2[] DirectionVectors = {
new (0, -1),
new Vector2(1, -1).Normalized,
new Vector2(1, 0),
new (1, 0),
new Vector2(1, 1).Normalized,
new Vector2(0, 1),
new (0, 1),
new Vector2(-1, 1).Normalized,
new Vector2(-1, 0),
new (-1, 0),
new Vector2(-1, -1).Normalized
};
private static readonly Vector2i[] IntDirectionVectors = {
new (0, -1),
new (1, -1),
new (1, 0),
new (1, 1),
new (0, 1),
new (-1, 1),
new (-1, 0),
new (-1, -1)
};
/// <summary>
/// Converts a Direction to a normalized Direction vector.
/// </summary>
/// <param name="dir"></param>
/// <returns></returns>
/// <returns>a normalized 2D Vector</returns>
/// <exception cref="IndexOutOfRangeException">if invalid Direction is used</exception>
/// <seealso cref="Vector2"/>
public static Vector2 ToVec(this Direction dir)
{
return directionVectors[(int) dir];
return DirectionVectors[(int) dir];
}
/// <summary>
/// Converts a Direction to a Vector2i. Useful for getting adjacent tiles.
/// </summary>
/// <param name="dir">Direction</param>
/// <returns>an 2D int Vector</returns>
/// <exception cref="IndexOutOfRangeException">if invalid Direction is used</exception>
/// <seealso cref="Vector2i"/>
public static Vector2i ToIntVec(this Direction dir)
{
return IntDirectionVectors[(int) dir];
}
/// <summary>
/// Offset 2D integer vector by a given direction.
/// Convenience for adding <see cref="ToIntVec"/> to <see cref="Vector2i"/>
/// </summary>
/// <param name="vec">2D integer vector</param>
/// <param name="dir">Direction by which we offset</param>
/// <returns>a newly vector offset by the <param name="dir">dir</param> or exception if the direction is invalid</returns>
public static Vector2i Offset(this Vector2i vec, Direction dir)
{
return vec + dir.ToIntVec();
}
/// <summary>

View File

@@ -337,6 +337,28 @@ namespace Robust.Shared.Maths
R2C2 = matrix.Row2.Z;
}
/// <summary>
/// Constructs a new instance from 3 vectors to quickly synthesize affine transforms.
/// </summary>
/// <param name="x">A Vector2 for the first, conventionally X basis, vector.</param>
/// <param name="y">A Vector2 for the second, conventionally Y basis, vector.</param>
/// <param name="origin">A Vector2 for the third, conventionally origin vector.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Matrix3(ref Vector2 x, ref Vector2 y, ref Vector2 origin)
{
R0C0 = x.X;
R0C1 = y.X;
R0C2 = origin.X;
R1C0 = x.Y;
R1C1 = y.Y;
R1C2 = origin.Y;
R2C0 = 0;
R2C1 = 0;
R2C2 = 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3 CreateTranslation(float x, float y)
{

View File

@@ -470,7 +470,7 @@ namespace Robust.Shared
// - Sleep
public static readonly CVarDef<float> AngularSleepTolerance =
CVarDef.Create("physics.angsleeptol", 2.0f / 180.0f * MathF.PI);
CVarDef.Create("physics.angsleeptol", 0.25f / 180.0f * MathF.PI);
public static readonly CVarDef<float> LinearSleepTolerance =
CVarDef.Create("physics.linsleeptol", 0.1f);

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Console
{
@@ -19,6 +20,7 @@ namespace Robust.Shared.Console
[Dependency] protected readonly IReflectionManager ReflectionManager = default!;
[Dependency] protected readonly INetManager NetManager = default!;
[ViewVariables]
protected readonly Dictionary<string, IConsoleCommand> AvailableCommands = new();
/// <inheritdoc />

View File

@@ -218,6 +218,47 @@ namespace Robust.Shared.Containers
return userContainer == otherContainer;
}
/// <summary>
/// Check whether a given entity can see another entity despite whatever containers they may be in.
/// </summary>
/// <remarks>
/// This is effectively a variant of <see cref="IsInSameOrParentContainer"/> that also checks whether the
/// containers are transparent. Additionally, an entity can "see" the entity that contains it, but unless
/// otherwise specified the containing entity cannot see into itself. For example, a human in a locker can
/// see the locker and other items in that locker, but the human cannot see their own organs. Note that
/// this means that the two entity arguments are NOT interchangeable.
/// </remarks>
public static bool IsInSameOrTransparentContainer(this IEntity user, IEntity other, bool userSeeInsideSelf = false)
{
DebugTools.AssertNotNull(user);
DebugTools.AssertNotNull(other);
TryGetContainer(user, out IContainer? userContainer);
TryGetContainer(other, out IContainer? otherContainer);
// Are both entities in the same container (or none)?
if (userContainer == otherContainer) return true;
// Is the user contained in the other entity?
if (userContainer?.Owner == other) return true;
// Does the user contain the other and can they see through themselves?
if (userSeeInsideSelf && otherContainer?.Owner == user) return true;
// Next we check for see-through containers. This uses some recursion, but it should be fine unless people
// start spawning in glass matryoshka dolls.
// Is the user in a see-through container?
if (userContainer?.ShowContents ?? false)
return IsInSameOrTransparentContainer(userContainer.Owner, other);
// Is the other entity in a see-through container?
if (otherContainer?.ShowContents ?? false)
return IsInSameOrTransparentContainer(user, otherContainer.Owner);
return false;
}
/// <summary>
/// Shortcut method to make creation of containers easier.
/// Creates a new container on the entity and gives it back to you.

View File

@@ -19,7 +19,7 @@ namespace Robust.Shared.Containers
/// </summary>
[ComponentReference(typeof(IContainerManager))]
[NetworkedComponent]
public class ContainerManagerComponent : Component, IContainerManager
public class ContainerManagerComponent : Component, IContainerManager, ISerializationHooks
{
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
@@ -30,6 +30,15 @@ namespace Robust.Shared.Containers
/// <inheritdoc />
public sealed override string Name => "ContainerContainer";
void ISerializationHooks.AfterDeserialization()
{
foreach (var (_, container) in Containers)
{
var baseContainer = (BaseContainer) container;
baseContainer.Manager = this;
}
}
/// <inheritdoc />
protected override void OnRemove()
{

View File

@@ -22,10 +22,8 @@ namespace Robust.Shared.Containers
public T MakeContainer<T>(EntityUid uid, string id, ContainerManagerComponent? containerManager = null)
where T : IContainer
{
if (!Resolve(uid, ref containerManager))
// TODO: I am a Sad Vera. Merge ComponentManager and EntityManager so we don't have to do this. Pretty please?
// ps. The to-do above is in effect for this whole file.
containerManager = EntityManager.AddComponent<ContainerManagerComponent>(EntityManager.GetEntity(uid));
if (!Resolve(uid, ref containerManager, false))
containerManager = EntityManager.AddComponent<ContainerManagerComponent>(uid); // Happy Vera.
return containerManager.MakeContainer<T>(id);
}
@@ -33,8 +31,8 @@ namespace Robust.Shared.Containers
public T EnsureContainer<T>(EntityUid uid, string id, ContainerManagerComponent? containerManager = null)
where T : IContainer
{
if (!Resolve(uid, ref containerManager))
containerManager = EntityManager.AddComponent<ContainerManagerComponent>(EntityManager.GetEntity(uid));
if (!Resolve(uid, ref containerManager, false))
containerManager = EntityManager.AddComponent<ContainerManagerComponent>(uid);
if (TryGetContainer(uid, id, out var container, containerManager))
return (T)container;
@@ -52,7 +50,7 @@ namespace Robust.Shared.Containers
public bool HasContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager)
{
if (!Resolve(uid, ref containerManager))
if (!Resolve(uid, ref containerManager, false))
return false;
return containerManager.HasContainer(id);
@@ -60,7 +58,7 @@ namespace Robust.Shared.Containers
public bool TryGetContainer(EntityUid uid, string id, [NotNullWhen(true)] out IContainer? container, ContainerManagerComponent? containerManager = null)
{
if (Resolve(uid, ref containerManager))
if (Resolve(uid, ref containerManager, false))
return containerManager.TryGetContainer(id, out container);
container = null;
@@ -69,7 +67,7 @@ namespace Robust.Shared.Containers
public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out IContainer? container, ContainerManagerComponent? containerManager = null)
{
if (Resolve(uid, ref containerManager) && EntityManager.TryGetEntity(containedUid, out var containedEntity))
if (Resolve(uid, ref containerManager, false) && EntityManager.TryGetEntity(containedUid, out var containedEntity))
return containerManager.TryGetContainer(containedEntity, out container);
container = null;
@@ -110,7 +108,7 @@ namespace Robust.Shared.Containers
public bool TryGetContainingContainer(EntityUid uid, [NotNullWhen(true)] out IContainer? container, ITransformComponent? transform = null)
{
container = null;
if (!Resolve(uid, ref transform))
if (!Resolve(uid, ref transform, false))
return false;
if (!transform.ParentUid.IsValid())

View File

@@ -809,13 +809,13 @@ namespace Robust.Shared.ContentPack
continue;
}
return new PEReader(File.OpenRead(path));
return ReaderFromStream(File.OpenRead(path));
}
var extraStream = _parent.ExtraRobustLoader?.Invoke(dllName);
if (extraStream != null)
{
return new PEReader(extraStream);
return ReaderFromStream(extraStream);
}
foreach (var resLoadPath in _resLoadPaths)
@@ -823,7 +823,7 @@ namespace Robust.Shared.ContentPack
try
{
var path = resLoadPath / dllName;
return new PEReader(_parent._res.ContentFileRead(path));
return ReaderFromStream(_parent._res.ContentFileRead(path));
}
catch (FileNotFoundException)
{
@@ -833,6 +833,23 @@ namespace Robust.Shared.ContentPack
return null;
}
private static PEReader ReaderFromStream(Stream stream)
{
if (OperatingSystem.IsLinux() && stream is FileStream)
{
// PEReader is bugged on Linux and not properly thread safe when doing memory mapping.
// As such, we never pass it a file stream so it uses a different, non-bugged code path.
// See https://github.com/dotnet/runtime/issues/60545
var ms = new MemoryStream();
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
stream.Dispose();
stream = ms;
}
return new PEReader(stream);
}
public PEReader? Resolve(string simpleName)
{
return _dictionary.GetOrAdd(simpleName, ResolveCore);

View File

@@ -1,4 +1,4 @@
# This file controls all whitelists and other rules enforced by AssemblyTypeChecker.
# This file controls all whitelists and other rules enforced by AssemblyTypeChecker.
# Yes, I typed most of this out by hand.
# ILVerify errors that are allowed.
@@ -13,6 +13,7 @@ AllowedVerifierErrors:
WhitelistedNamespaces:
- Robust
- Content
- OpenDreamShared
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
@@ -299,6 +300,7 @@ Types:
IEnumerator: { All: True }
IReadOnlyList`1: { All: True }
System.ComponentModel:
CancelEventArgs: { All: True }
PropertyDescriptor: { }
ISite: { All: True }
IComponent: { All: True }
@@ -332,6 +334,7 @@ Types:
CompareOptions: { }
CultureInfo: { All: True }
DateTimeStyles: { All: True } # Enum
NumberStyles: { } # Enum
TextInfo:
Methods:
- "bool get_IsRightToLeft()"
@@ -342,7 +345,11 @@ Types:
- "string ToTitleCase(string)"
- "string ToTitleCase(string)"
- "string ToUpper(string)"
System.IO.Compression:
CompressionMode: { } # Enum
DeflateStream: { All: True }
System.IO:
BinaryReader: { All: True }
FileAccess: { } # Enum
FileMode: { } # Enum
FileShare: { } # Enum
@@ -350,6 +357,7 @@ Types:
InvalidDataException: { All: True }
IOException: { All: True }
MemoryStream: { All: True }
SeekOrigin: { } # Enum
Stream: { All: True }
StreamReader:
Fields:
@@ -467,6 +475,8 @@ Types:
UnmanagedType: { } # `unmanaged` constraint in C# modreqs with this.
System.Runtime.Versioning:
TargetFrameworkAttribute: { All: True }
System.Text.Json.Serialization:
JsonIgnoreAttribute: { All: True }
System.Text.RegularExpressions:
Capture: { All: True }
CaptureCollection: { All: True }
@@ -801,6 +811,9 @@ Types:
Attribute: { All: True }
AttributeTargets: { }
AttributeUsageAttribute: { All: True }
BitConverter:
Methods:
- "uint ToUInt32(System.ReadOnlySpan`1<byte>)"
Base64FormattingOptions: { } # Enum
Boolean: { All: True }
Byte: { All: True }

View File

@@ -0,0 +1,21 @@
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// A component that toggles collision on an entity being toggled.
/// </summary>
[RegisterComponent]
public sealed class CollideOnAnchorComponent : Component
{
public override string Name => "CollideOnAnchor";
/// <summary>
/// Whether we toggle collision on or off when anchoring (and vice versa when unanchoring).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enable")]
public bool Enable { get; set; } = false;
}
}

View File

@@ -37,7 +37,6 @@ using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -141,13 +140,6 @@ namespace Robust.Shared.GameObjects
/// </summary>
internal ContactEdge? ContactEdges { get; set; } = null;
/// <summary>
/// Linked-list of all of our joints.
/// </summary>
internal JointEdge? JointEdges { get; set; } = null;
// TODO: Should there be a VV thing for joints? Would be useful. Same with contacts.
// Though not sure how to do it well with the linked-list.
public bool IgnorePaused { get; set; }
internal SharedPhysicsMapComponent? PhysicsMap { get; set; }
@@ -310,14 +302,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public override ComponentState GetComponentState(ICommonSession session)
{
// TODO: Could optimise the shit out of this because only linear velocity and angular velocity are changing 99% of the time.
var joints = new List<Joint>();
for (var je = JointEdges; je != null; je = je.Next)
{
joints.Add(je.Joint);
}
return new PhysicsComponentState(_canCollide, _sleepingAllowed, _fixedRotation, _bodyStatus, _fixtures, joints, LinearVelocity, AngularVelocity, BodyType);
return new PhysicsComponentState(_canCollide, _sleepingAllowed, _fixedRotation, _bodyStatus, _fixtures, LinearVelocity, AngularVelocity, BodyType);
}
/// <inheritdoc />
@@ -334,72 +319,6 @@ namespace Robust.Shared.GameObjects
// So transform doesn't apply MapId in the HandleComponentState because ??? so MapId can still be 0.
// Fucking kill me, please. You have no idea deep the rabbit hole of shitcode goes to make this work.
// We will pray that this deferred joint is handled properly.
/*
* -- Joints --
*/
// TODO: Iterating like this is inefficient and bloated as fuck but on the other hand the linked-list is very convenient
// for bodies with a large number of fixtures / joints.
// Probably store them in Dictionaries but still store the linked-list stuff on the fixture / joint itself.
var existingJoints = Joints.ToList();
var toAddJoints = new List<Joint>();
var toRemoveJoints = new List<Joint>();
foreach (var newJoint in newState.Joints)
{
var jointFound = false;
foreach (var joint in existingJoints)
{
if (joint.ID.Equals(newJoint.ID))
{
if (!newJoint.Equals(joint) && TrySetupNetworkedJoint(newJoint))
{
toAddJoints.Add(newJoint);
toRemoveJoints.Add(joint);
}
jointFound = true;
break;
}
}
if (!jointFound && TrySetupNetworkedJoint(newJoint))
{
toAddJoints.Add(newJoint);
}
}
foreach (var joint in existingJoints)
{
var jointFound = false;
foreach (var newJoint in newState.Joints)
{
if (joint.ID.Equals(newJoint.ID))
{
jointFound = true;
break;
}
}
if (jointFound) continue;
toRemoveJoints.Add(joint);
}
foreach (var joint in toRemoveJoints)
{
RemoveJoint(joint);
}
foreach (var joint in toAddJoints)
{
AddJoint(joint);
}
/*
* -- Fixtures --
*/
@@ -496,29 +415,6 @@ namespace Robust.Shared.GameObjects
Predict = false;
}
private bool TrySetupNetworkedJoint(Joint joint)
{
// This can fail if we've already deleted the entity or remove physics from it I think?
if (!Owner.EntityManager.TryGetEntity(joint.BodyAUid, out var entityA) ||
!entityA.TryGetComponent(out PhysicsComponent? bodyA))
{
return false;
}
if (!Owner.EntityManager.TryGetEntity(joint.BodyBUid, out var entityB) ||
!entityB.TryGetComponent(out PhysicsComponent? bodyB))
{
return false;
}
joint.BodyA = bodyA;
joint.BodyB = bodyB;
joint.EdgeA = new JointEdge();
joint.EdgeB = new JointEdge();
return true;
}
public Fixture? GetFixture(string name)
{
// Sooo I'd rather have fixtures as a list in serialization but there's not really an easy way to have it as a
@@ -570,27 +466,13 @@ namespace Robust.Shared.GameObjects
return bounds;
}
[ViewVariables]
public int FixtureCount { get; internal set; }
/// <inheritdoc />
[ViewVariables]
public IReadOnlyList<Fixture> Fixtures => _fixtures;
public IEnumerable<Joint> Joints
{
get
{
JointEdge? edge = JointEdges;
while (edge != null)
{
yield return edge.Joint;
edge = edge.Next;
}
}
}
[ViewVariables]
public int FixtureCount { get; internal set; }
[DataField("fixtures")]
[NeverPushInheritance]
internal List<Fixture> _fixtures = new();
@@ -1045,7 +927,7 @@ namespace Robust.Shared.GameObjects
public IEnumerable<PhysicsComponent> GetBodiesIntersecting()
{
foreach (var entity in EntitySystem.Get<SharedBroadphaseSystem>().GetCollidingEntities(Owner.Transform.MapID, GetWorldAABB()))
foreach (var entity in EntitySystem.Get<SharedPhysicsSystem>().GetCollidingEntities(Owner.Transform.MapID, GetWorldAABB()))
{
yield return entity;
}
@@ -1107,38 +989,6 @@ namespace Robust.Shared.GameObjects
}
}
private string GetJointName(Joint joint)
{
var id = joint.ID;
if (!string.IsNullOrEmpty(id)) return id;
var jointCount = Joints.Count();
for (var i = 0; i < jointCount + 1; i++)
{
id = $"joint-{i}";
if (GetJoint(id) != null) continue;
return id;
}
Logger.WarningS("physics", $"Unable to get a joint ID; using its hashcode instead.");
return joint.GetHashCode().ToString();
}
/// <summary>
/// Get a joint with the specified ID.
/// </summary>
public Joint? GetJoint(string id)
{
foreach (var joint in Joints)
{
if (joint.ID == id) return joint;
}
return null;
}
internal Transform GetTransform()
{
return new(Owner.Transform.WorldPosition, (float) Owner.Transform.WorldRotation.Theta);
@@ -1200,7 +1050,7 @@ namespace Robust.Shared.GameObjects
IEnumerable<IPhysBody> IPhysBody.GetCollidingEntities(Vector2 offset, bool approx)
{
return EntitySystem.Get<SharedBroadphaseSystem>().GetCollidingEntities(this, offset, approx);
return EntitySystem.Get<SharedPhysicsSystem>().GetCollidingEntities(this, offset, approx);
}
/// <inheritdoc />
@@ -1249,35 +1099,6 @@ namespace Robust.Shared.GameObjects
}
}
public void ClearJoints()
{
for (var je = JointEdges; je != null; je = je.Next)
{
RemoveJoint(je.Joint);
}
}
public void AddJoint(Joint joint)
{
var id = GetJointName(joint);
foreach (var existing in Joints)
{
// This can happen if a server created joint is sent and applied before the client can create it locally elsewhere
if (existing.ID.Equals(id)) return;
}
PhysicsMap?.AddJoint(joint);
joint.ID = id;
Logger.DebugS("physics", $"Added joint id: {joint.ID} type: {joint.GetType().Name} to {Owner}");
}
public void RemoveJoint(Joint joint)
{
PhysicsMap?.RemoveJoint(joint);
Logger.DebugS("physics", $"Removed joint id: {joint.ID} type: {joint.GetType().Name} from {Owner}");
}
protected override void OnRemove()
{
base.OnRemove();
@@ -1389,11 +1210,29 @@ namespace Robust.Shared.GameObjects
}
// Does a joint prevent collision?
for (var jn = JointEdges; jn != null; jn = jn.Next)
// if one of them doesn't have jointcomp then they can't share a common joint.
// otherwise, only need to iterate over the joints of one component as they both store the same joint.
if (Owner.TryGetComponent(out JointComponent? jointComponentA) &&
other.Owner.TryGetComponent(out JointComponent? jointComponentB))
{
if (jn.Other != other) continue;
if (jn.Joint.CollideConnected) continue;
return false;
var aUid = jointComponentA.Owner.Uid;
var bUid = jointComponentB.Owner.Uid;
ValueTuple<EntityUid, EntityUid> uids;
uids = aUid.CompareTo(bUid) < 0 ?
new ValueTuple<EntityUid, EntityUid>(aUid, bUid) :
new ValueTuple<EntityUid, EntityUid>(bUid, aUid);
foreach (var (_, joint) in jointComponentA.Joints)
{
// Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking.
if (joint.CollideConnected ||
uids.Item1 != joint.BodyAUid ||
uids.Item2 != joint.BodyBUid) continue;
return false;
}
}
var preventCollideMessage = new PreventCollideEvent(this, other);

View File

@@ -16,7 +16,6 @@ namespace Robust.Shared.GameObjects
public readonly bool FixedRotation;
public readonly BodyStatus Status;
public readonly List<Fixture> Fixtures;
public readonly List<Joint> Joints;
public readonly Vector2 LinearVelocity;
public readonly float AngularVelocity;
@@ -40,7 +39,6 @@ namespace Robust.Shared.GameObjects
bool fixedRotation,
BodyStatus status,
List<Fixture> fixtures,
List<Joint> joints,
Vector2 linearVelocity,
float angularVelocity,
BodyType bodyType)
@@ -50,7 +48,6 @@ namespace Robust.Shared.GameObjects
FixedRotation = fixedRotation;
Status = status;
Fixtures = fixtures;
Joints = joints;
LinearVelocity = linearVelocity;
AngularVelocity = angularVelocity;

View File

@@ -46,41 +46,22 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Contains meta data about this entity that isn't component specific.
/// </summary>
public interface IMetaDataComponent : IComponent
[NetworkedComponent]
public class MetaDataComponent : Component
{
/// <summary>
/// The in-game name of this entity.
/// </summary>
string EntityName { get; set; }
/// <summary>
/// The in-game description of this entity.
/// </summary>
string EntityDescription { get; set; }
/// <summary>
/// The prototype this entity was created from, if any.
/// </summary>
EntityPrototype? EntityPrototype { get; set; }
}
/// <inheritdoc cref="IMetaDataComponent"/>
[ComponentReference(typeof(IMetaDataComponent))]
[NetworkedComponent()]
internal class MetaDataComponent : Component, IMetaDataComponent
{
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[DataField("name")]
private string? _entityName;
[DataField("desc")]
private string? _entityDescription;
private EntityPrototype? _entityPrototype;
private bool _entityPaused;
/// <inheritdoc />
public override string Name => "MetaData";
/// <inheritdoc />
/// <summary>
/// The in-game name of this entity.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public string EntityName
{
@@ -104,7 +85,9 @@ namespace Robust.Shared.GameObjects
}
}
/// <inheritdoc />
/// <summary>
/// The in-game description of this entity.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public string EntityDescription
{
@@ -128,7 +111,9 @@ namespace Robust.Shared.GameObjects
}
}
/// <inheritdoc />
/// <summary>
/// The prototype this entity was created from, if any.
/// </summary>
[ViewVariables]
public EntityPrototype? EntityPrototype
{
@@ -140,14 +125,37 @@ namespace Robust.Shared.GameObjects
}
}
/// <param name="player"></param>
/// <inheritdoc />
/// <summary>
/// The current lifetime stage of this entity. You can use this to check
/// if the entity is initialized or being deleted.
/// </summary>
[ViewVariables]
public EntityLifeStage EntityLifeStage { get; internal set; }
[ViewVariables]
public bool EntityPaused
{
get => _entityPaused;
set
{
if (_entityPaused == value || value && Owner.HasComponent<IgnorePauseComponent>())
return;
_entityPaused = value;
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntityPausedEvent(Owner.Uid, value));
}
}
public bool EntityInitialized => EntityLifeStage >= EntityLifeStage.Initialized;
public bool EntityInitializing => EntityLifeStage == EntityLifeStage.Initializing;
public bool EntityDeleted => EntityLifeStage >= EntityLifeStage.Deleted;
public override ComponentState GetComponentState(ICommonSession player)
{
return new MetaDataComponentState(_entityName, _entityDescription, EntityPrototype?.ID);
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
@@ -159,7 +167,7 @@ namespace Robust.Shared.GameObjects
_entityDescription = state.Description;
if(state.PrototypeId != null)
_entityPrototype = _prototypes.Index<EntityPrototype>(state.PrototypeId);
_entityPrototype = IoCManager.Resolve<IPrototypeManager>().Index<EntityPrototype>(state.PrototypeId);
}
internal override void ClearTicks()

View File

@@ -22,9 +22,11 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Should this component be removed when no more timers are running?
/// N.B. This is set to false because https://github.com/space-wizards/RobustToolbox/pull/2091 caused massive issues.
/// Changing this to false won't cause issues but masks the underlying problem, while leaving the option to turn it on for testing available.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool RemoveOnEmpty { get; set; } = true;
public bool RemoveOnEmpty { get; set; } = false;
public void Update(float frameTime)
{

View File

@@ -109,7 +109,7 @@ namespace Robust.Shared.GameObjects
if(_noLocalRotation)
return;
if (_localRotation.EqualsApprox(value, 0.0001f))
if (_localRotation.EqualsApprox(value))
return;
var oldRotation = _localRotation;
@@ -775,7 +775,7 @@ namespace Robust.Shared.GameObjects
rebuildMatrices = true;
}
if (!_localPosition.EqualsApprox(newState.LocalPosition, 0.0001))
if (!_localPosition.EqualsApprox(newState.LocalPosition))
{
var oldPos = Coordinates;
_localPosition = newState.LocalPosition;
@@ -926,7 +926,7 @@ namespace Robust.Shared.GameObjects
_anchored = value;
Dirty();
var anchorStateChangedEvent = new AnchorStateChangedEvent(Owner);
var anchorStateChangedEvent = new AnchorStateChangedEvent(Owner, value);
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, ref anchorStateChangedEvent);
}
}
@@ -985,9 +985,12 @@ namespace Robust.Shared.GameObjects
{
public readonly IEntity Entity;
public AnchorStateChangedEvent(IEntity entity)
public readonly bool Anchored;
public AnchorStateChangedEvent(IEntity entity, bool anchored)
{
Entity = entity;
Anchored = anchored;
}
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
@@ -24,19 +23,11 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public EntityUid Uid { get; }
private EntityLifeStage _lifeStage;
/// <inheritdoc cref="IEntity.LifeStage" />
[ViewVariables]
internal EntityLifeStage LifeStage
{
get => _lifeStage;
set => _lifeStage = value;
}
/// <inheritdoc />
EntityLifeStage IEntity.LifeStage { get => LifeStage; set => LifeStage = value; }
public EntityLifeStage LifeStage { get => MetaData.EntityLifeStage; internal set => MetaData.EntityLifeStage = value; }
/// <inheritdoc />
[ViewVariables]
public EntityPrototype? Prototype
@@ -76,20 +67,7 @@ namespace Robust.Shared.GameObjects
public bool Deleted => LifeStage >= EntityLifeStage.Deleted;
[ViewVariables]
public bool Paused
{
get => _paused;
set
{
if (_paused == value || value && HasComponent<IgnorePauseComponent>())
return;
_paused = value;
EntityManager.EventBus.RaiseLocalEvent(Uid, new EntityPausedEvent(Uid, value));
}
}
private bool _paused;
public bool Paused { get => MetaData.EntityPaused; set => MetaData.EntityPaused = value; }
private ITransformComponent? _transform;
@@ -97,11 +75,15 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public ITransformComponent Transform => _transform ??= GetComponent<ITransformComponent>();
private IMetaDataComponent? _metaData;
private MetaDataComponent? _metaData;
/// <inheritdoc />
[ViewVariables]
public IMetaDataComponent MetaData => _metaData ??= GetComponent<IMetaDataComponent>();
public MetaDataComponent MetaData
{
get => _metaData ??= GetComponent<MetaDataComponent>();
internal set => _metaData = value;
}
#endregion Members
@@ -124,6 +106,8 @@ namespace Robust.Shared.GameObjects
/// </summary>
public void InitializeComponents()
{
// TODO: Move this to EntityManager.
DebugTools.Assert(LifeStage == EntityLifeStage.PreInit);
LifeStage = EntityLifeStage.Initializing;
@@ -166,6 +150,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
public void StartAllComponents()
{
// TODO: Move this to EntityManager.
// Startup() can modify _components
// This code can only handle additions to the list. Is there a better way? Probably not.
var comps = EntityManager.GetComponents(Uid)
@@ -323,9 +308,11 @@ namespace Robust.Shared.GameObjects
/// </summary>
public void Dirty()
{
// TODO: Move this to EntityManager.
LastModifiedTick = EntityManager.CurrentTick;
if (LifeStage >= EntityLifeStage.Initialized && Transform.Anchored)
if (LifeStage >= EntityLifeStage.Initialized && Transform.Anchored && Transform.ParentUid.IsValid())
EntityManager.GetComponent<IMapGridComponent>(Transform.ParentUid).AnchoredEntityDirty(Transform);
}

View File

@@ -485,10 +485,13 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Enumerates all subscriptions for an event on a specific entity, returning the component instances and registrations.
/// </summary>
private bool TryGetSubscriptions(Type eventType, EntityUid euid,
[NotNullWhen(true)] out SubscriptionsEnumerator enumerator)
private bool TryGetSubscriptions(Type eventType, EntityUid euid, [NotNullWhen(true)] out SubscriptionsEnumerator enumerator)
{
var eventTable = _eventTables[euid];
if (!_eventTables.TryGetValue(euid, out var eventTable))
{
enumerator = default!;
return false;
}
// No subscriptions to this event type, return null.
if (!eventTable.TryGetValue(eventType, out var subscribedComps))

View File

@@ -56,8 +56,6 @@ namespace Robust.Shared.GameObjects
if (Initialized)
throw new InvalidOperationException("Already initialized.");
Initialized = true;
FillComponentDict();
_componentFactory.ComponentAdded += OnComponentAdded;
_componentFactory.ComponentReferenceAdded += OnComponentReferenceAdded;
@@ -90,7 +88,6 @@ namespace Robust.Shared.GameObjects
#region Component Management
/// <inheritdoc />
public T AddComponent<T>(IEntity entity) where T : Component, new()
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
@@ -104,18 +101,32 @@ namespace Robust.Shared.GameObjects
return newComponent;
}
/// <inheritdoc />
public T AddComponent<T>(EntityUid uid) where T : Component, new()
{
if (!TryGetEntity(uid, out var entity)) throw new ArgumentException("Entity is not valid or deleted.", nameof(uid));
return AddComponent<T>(entity);
}
public void AddComponent<T>(IEntity entity, T component, bool overwrite = false) where T : Component
{
if (entity == null || !entity.IsValid())
throw new ArgumentException("Entity is not valid.", nameof(entity));
AddComponent(entity.Uid, component, overwrite);
}
public void AddComponent<T>(EntityUid uid, T component, bool overwrite = false) where T : Component
{
if (!uid.IsValid() || !EntityExists(uid))
throw new ArgumentException("Entity is not valid.", nameof(uid));
if (component == null) throw new ArgumentNullException(nameof(component));
if (component.Owner != entity) throw new InvalidOperationException("Component is not owned by entity.");
if (component.Owner.Uid != uid) throw new InvalidOperationException("Component is not owned by entity.");
var uid = entity.Uid;
AddComponentInternal(uid, component, overwrite);
}
private void AddComponentInternal<T>(EntityUid uid, T component, bool overwrite = false) where T : Component
{
// get interface aliases for mapping
var reg = _componentFactory.GetRegistration(component);
@@ -131,10 +142,10 @@ namespace Robust.Shared.GameObjects
$"Component reference type {type} already occupied by {duplicate}");
// these two components are required on all entities and cannot be overwritten.
if (duplicate is ITransformComponent || duplicate is IMetaDataComponent)
if (duplicate is ITransformComponent || duplicate is MetaDataComponent)
throw new InvalidOperationException("Tried to overwrite a protected component.");
RemoveComponentImmediate(duplicate);
RemoveComponentImmediate(duplicate, uid, false);
}
// add the component to the grid
@@ -163,16 +174,18 @@ namespace Robust.Shared.GameObjects
ComponentAdded?.Invoke(this, new AddedComponentEventArgs(component, uid));
_componentDependencyManager.OnComponentAdd(entity.Uid, component);
_componentDependencyManager.OnComponentAdd(uid, component);
component.LifeAddToEntity();
if (!entity.Initialized && !entity.Initializing)
var metadata = GetComponent<MetaDataComponent>(uid);
if (!metadata.EntityInitialized && !metadata.EntityInitializing)
return;
component.LifeInitialize();
if (entity.Initialized)
if (metadata.EntityInitialized)
component.LifeStartup();
}
@@ -214,8 +227,8 @@ namespace Robust.Shared.GameObjects
static int Sequence(IComponent x)
=> x switch
{
ITransformComponent _ => 0,
IMetaDataComponent _ => 1,
MetaDataComponent _ => 0,
ITransformComponent _ => 1,
IPhysBody _ => 2,
_ => int.MaxValue
};
@@ -258,7 +271,7 @@ namespace Robust.Shared.GameObjects
{
#endif
// these two components are required on all entities and cannot be removed normally.
if (!removeProtected && (component is ITransformComponent || component is IMetaDataComponent))
if (!removeProtected && (component is ITransformComponent || component is MetaDataComponent))
{
DebugTools.Assert("Tried to remove a protected component.");
return;
@@ -287,14 +300,18 @@ namespace Robust.Shared.GameObjects
#endif
}
private void RemoveComponentImmediate(Component component)
private void RemoveComponentImmediate(Component component, EntityUid uid, bool removeProtected)
{
if (component == null) throw new ArgumentNullException(nameof(component));
#if EXCEPTION_TOLERANCE
try
{
#endif
if (!component.Deleted)
{
// these two components are required on all entities and cannot be removed.
if (component is ITransformComponent || component is IMetaDataComponent)
if (!removeProtected && component is ITransformComponent || component is MetaDataComponent)
{
DebugTools.Assert("Tried to remove a protected component.");
return;
@@ -306,9 +323,17 @@ namespace Robust.Shared.GameObjects
if (component.LifeStage != ComponentLifeStage.PreAdd)
component.LifeRemoveFromEntity(); // Sets delete
ComponentRemoved?.Invoke(this, new RemovedComponentEventArgs(component, component.Owner.Uid));
_componentDependencyManager.OnComponentRemove(uid, component);
ComponentRemoved?.Invoke(this, new RemovedComponentEventArgs(component, uid));
}
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
_runtimeLog.LogException(e,
$"RemoveComponentImmediate, owner={component.Owner}, type={component.GetType()}");
}
#endif
DeleteComponent(component);
}
@@ -385,6 +410,15 @@ namespace Robust.Shared.GameObjects
return AddComponent<T>(entity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T EnsureComponent<T>(EntityUid uid) where T : Component, new()
{
if (TryGetComponent<T>(uid, out var component))
return component;
return AddComponent<T>(uid);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetComponent<T>(EntityUid uid)

View File

@@ -75,6 +75,8 @@ namespace Robust.Shared.GameObjects
_eventBus = new EntityEventBus(this);
InitializeComponents();
Initialized = true;
}
public virtual void Startup()
@@ -176,11 +178,7 @@ namespace Robust.Shared.GameObjects
throw new InvalidOperationException($"Tried to spawn entity {protoName} on invalid coordinates {coordinates}.");
var entity = CreateEntityUninitialized(protoName, coordinates);
InitializeAndStartEntity((Entity) entity);
if (_pauseManager.IsMapInitialized(coordinates.GetMapId(this))) entity.RunMapInit();
InitializeAndStartEntity((Entity) entity, coordinates.GetMapId(this));
return entity;
}
@@ -188,7 +186,7 @@ namespace Robust.Shared.GameObjects
public virtual IEntity SpawnEntity(string? protoName, MapCoordinates coordinates)
{
var entity = CreateEntityUninitialized(protoName, coordinates);
InitializeAndStartEntity((Entity) entity);
InitializeAndStartEntity((Entity) entity, coordinates.MapId);
return entity;
}
@@ -253,6 +251,7 @@ namespace Robust.Shared.GameObjects
return;
var transform = entity.Transform;
var metadata = entity.MetaData;
entity.LifeStage = EntityLifeStage.Terminating;
EventBus.RaiseLocalEvent(entity.Uid, new EntityTerminatingEvent(), false);
@@ -274,7 +273,7 @@ namespace Robust.Shared.GameObjects
transform.DetachParentToNull();
}
entity.LifeStage = EntityLifeStage.Deleted;
metadata.EntityLifeStage = EntityLifeStage.Deleted;
EntityDeleted?.Invoke(this, entity.Uid);
EventBus.RaiseEvent(EventSource.Local, new EntityDeletedMessage(entity));
Entities.Remove(entity.Uid);
@@ -300,13 +299,7 @@ namespace Robust.Shared.GameObjects
public bool EntityExists(EntityUid uid)
{
if (!TryGetEntity(uid, out var ent))
return false;
if (ent.Deleted)
return false;
return true;
return TryGetEntity(uid, out _);
}
/// <summary>
@@ -363,8 +356,12 @@ namespace Robust.Shared.GameObjects
// We do this after the event, so if the event throws we have not committed
Entities[entity.Uid] = entity;
// allocate the required MetaDataComponent
AddComponent<MetaDataComponent>(entity);
// Create the MetaDataComponent and set it directly on the Entity to avoid a stack overflow in DEBUG.
var metadata = new MetaDataComponent() { Owner = entity };
entity.MetaData = metadata;
// add the required MetaDataComponent directly.
AddComponentInternal(uid.Value, metadata);
// allocate the required TransformComponent
AddComponent<TransformComponent>(entity);
@@ -401,13 +398,16 @@ namespace Robust.Shared.GameObjects
EntityPrototype.LoadEntity(entity.Prototype, entity, ComponentFactory, context);
}
private protected void InitializeAndStartEntity(Entity entity)
private void InitializeAndStartEntity(Entity entity, MapId mapId)
{
try
{
InitializeEntity(entity);
EntityInitialized?.Invoke(this, entity.Uid);
StartEntity(entity);
// If the map we're initializing the entity on is initialized, run map init on it.
if (_pauseManager.IsMapInitialized(mapId))
entity.RunMapInit();
}
catch (Exception e)
{
@@ -416,9 +416,10 @@ namespace Robust.Shared.GameObjects
}
}
private protected static void InitializeEntity(Entity entity)
protected void InitializeEntity(Entity entity)
{
entity.InitializeComponents();
EntityInitialized?.Invoke(this, entity.Uid);
}
protected void StartEntity(Entity entity)

View File

@@ -0,0 +1,14 @@
namespace Robust.Shared.GameObjects
{
public static class EntityManagerExt
{
public static T? GetComponentOrNull<T>(this IEntityManager entityManager, EntityUid entityUid)
where T : class, IComponent
{
if (entityManager.TryGetComponent(entityUid, out T? component))
return component;
return null;
}
}
}

View File

@@ -1,5 +1,6 @@
using Robust.Shared.Serialization;
using System;
using NetSerializer;
namespace Robust.Shared.GameObjects
{
@@ -8,16 +9,15 @@ namespace Robust.Shared.GameObjects
{
public EntityUid Uid { get; }
public ComponentChange[]? ComponentChanges { get; }
public NetListAsArray<ComponentChange> ComponentChanges { get; }
public bool Empty => ComponentChanges is null;
public bool Empty => ComponentChanges.Value is null or { Count: 0 };
public EntityState(EntityUid uid, ComponentChange[]? changedComponents)
public EntityState(EntityUid uid, NetListAsArray<ComponentChange> changedComponents)
{
Uid = uid;
// empty lists are 5 bytes each
ComponentChanges = changedComponents == null || changedComponents.Length == 0 ? null : changedComponents;
ComponentChanges = changedComponents;
}
}

View File

@@ -1,5 +1,8 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
@@ -11,14 +14,24 @@ namespace Robust.Shared.GameObjects
/// </summary>
/// <param name="uid">The entity where to query the components.</param>
/// <param name="component">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="logMissing">Whether to log missing components.</param>
/// <typeparam name="TComp">The component type to resolve.</typeparam>
/// <returns>True if the component is not null or was resolved correctly, false if the component couldn't be resolved.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve<TComp>(EntityUid uid, [NotNullWhen(true)] ref TComp? component)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected bool Resolve<TComp>(EntityUid uid, [NotNullWhen(true)] ref TComp? component, bool logMissing = true)
where TComp : IComponent
{
DebugTools.Assert(component == null || uid == component.Owner.Uid, "Specified Entity is not the component's Owner!");
return component != null || EntityManager.TryGetComponent(uid, out component);
if (component != null)
return true;
var found = EntityManager.TryGetComponent(uid, out component);
if(logMissing && !found)
Logger.ErrorS("resolve", $"Can't resolve \"{typeof(TComp)}\" on entity {uid}!\n{new StackTrace(1, true)}");
return found;
}
/// <summary>
@@ -27,15 +40,16 @@ namespace Robust.Shared.GameObjects
/// <param name="uid">The entity where to query the components.</param>
/// <param name="comp1">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="comp2">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="logMissing">Whether to log missing components.</param>
/// <typeparam name="TComp1">The component type to resolve.</typeparam>
/// <typeparam name="TComp2">The component type to resolve.</typeparam>
/// <returns>True if the components are not null or were resolved correctly, false if any of the component couldn't be resolved.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve<TComp1, TComp2>(EntityUid uid, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected bool Resolve<TComp1, TComp2>(EntityUid uid, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, bool logMissing = true)
where TComp1 : IComponent
where TComp2 : IComponent
{
return Resolve(uid, ref comp1) && Resolve(uid, ref comp2);
return Resolve(uid, ref comp1, logMissing) & Resolve(uid, ref comp2, logMissing);
}
/// <summary>
@@ -45,17 +59,18 @@ namespace Robust.Shared.GameObjects
/// <param name="comp1">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="comp2">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="comp3">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="logMissing">Whether to log missing components.</param>
/// <typeparam name="TComp1">The component type to resolve.</typeparam>
/// <typeparam name="TComp2">The component type to resolve.</typeparam>
/// <typeparam name="TComp3">The component type to resolve.</typeparam>
/// <returns>True if the components are not null or were resolved correctly, false if any of the component couldn't be resolved.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve<TComp1, TComp2, TComp3>(EntityUid uid, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, [NotNullWhen(true)] ref TComp3? comp3)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected bool Resolve<TComp1, TComp2, TComp3>(EntityUid uid, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, [NotNullWhen(true)] ref TComp3? comp3, bool logMissing = true)
where TComp1 : IComponent
where TComp2 : IComponent
where TComp3 : IComponent
{
return Resolve(uid, ref comp1, ref comp2) && Resolve(uid, ref comp3);
return Resolve(uid, ref comp1, ref comp2, logMissing) & Resolve(uid, ref comp3, logMissing);
}
/// <summary>
@@ -66,19 +81,20 @@ namespace Robust.Shared.GameObjects
/// <param name="comp2">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="comp3">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="comp4">A reference to the variable storing the component, or null if it has to be resolved.</param>
/// <param name="logMissing">Whether to log missing components.</param>
/// <typeparam name="TComp1">The component type to resolve.</typeparam>
/// <typeparam name="TComp2">The component type to resolve.</typeparam>
/// <typeparam name="TComp3">The component type to resolve.</typeparam>
/// <typeparam name="TComp4">The component type to resolve.</typeparam>
/// <returns>True if the components are not null or were resolved correctly, false if any of the component couldn't be resolved.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve<TComp1, TComp2, TComp3, TComp4>(EntityUid uid, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, [NotNullWhen(true)] ref TComp3? comp3, [NotNullWhen(true)] ref TComp4? comp4)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected bool Resolve<TComp1, TComp2, TComp3, TComp4>(EntityUid uid, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, [NotNullWhen(true)] ref TComp3? comp3, [NotNullWhen(true)] ref TComp4? comp4, bool logMissing = true)
where TComp1 : IComponent
where TComp2 : IComponent
where TComp3 : IComponent
where TComp4 : IComponent
{
return Resolve(uid, ref comp1, ref comp2) && Resolve(uid, ref comp3, ref comp4);
return Resolve(uid, ref comp1, ref comp2, logMissing) & Resolve(uid, ref comp3, ref comp4, logMissing);
}
}
}

View File

@@ -33,12 +33,14 @@ namespace Robust.Shared.GameObjects
public class PlayAudioPositionalMessage : AudioMessage
{
public EntityCoordinates Coordinates { get; set; }
public EntityCoordinates FallbackCoordinates { get; set; }
}
[Serializable, NetSerializable]
public class PlayAudioEntityMessage : AudioMessage
{
public EntityCoordinates Coordinates { get; set; }
public EntityUid EntityUid { get; set; }
public EntityCoordinates Coordinates { get; set; }
public EntityCoordinates FallbackCoordinates { get; set; }
}
}

View File

@@ -75,7 +75,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// The MetaData Component of this entity.
/// </summary>
IMetaDataComponent MetaData { get; }
MetaDataComponent MetaData { get; }
/// <summary>
/// Public method to add a component to an entity.

View File

@@ -32,6 +32,14 @@ namespace Robust.Shared.GameObjects
/// <returns>The newly added component.</returns>
T AddComponent<T>(IEntity entity) where T : Component, new();
/// <summary>
/// Adds a Component type to an entity. If the entity is already Initialized, the component will
/// automatically be Initialized and Started.
/// </summary>
/// <typeparam name="T">Concrete component type to add.</typeparam>
/// <returns>The newly added component.</returns>
T AddComponent<T>(EntityUid uid) where T : Component, new();
/// <summary>
/// Adds a Component to an entity. If the entity is already Initialized, the component will
/// automatically be Initialized and Started.
@@ -41,6 +49,15 @@ namespace Robust.Shared.GameObjects
/// <param name="overwrite">Should it overwrite existing components?</param>
void AddComponent<T>(IEntity entity, T component, bool overwrite = false) where T : Component;
/// <summary>
/// Adds a Component to an entity. If the entity is already Initialized, the component will
/// automatically be Initialized and Started.
/// </summary>
/// <param name="uid">Entity being modified.</param>
/// <param name="component">Component to add.</param>
/// <param name="overwrite">Should it overwrite existing components?</param>
void AddComponent<T>(EntityUid uid, T component, bool overwrite = false) where T : Component;
/// <summary>
/// Removes the component with the specified reference type,
/// Without needing to have the component itself.
@@ -109,8 +126,22 @@ namespace Robust.Shared.GameObjects
/// <returns>True if the entity has a component with the given network ID, otherwise false.</returns>
bool HasComponent(EntityUid uid, ushort netId);
/// <summary>
/// This method will always return a component for a certain entity, adding it if it's not there already.
/// </summary>
/// <param name="entity">Entity to modify.</param>
/// <typeparam name="T">Component to add.</typeparam>
/// <returns>The component in question</returns>
T EnsureComponent<T>(IEntity entity) where T : Component, new();
/// <summary>
/// This method will always return a component for a certain entity, adding it if it's not there already.
/// </summary>
/// <param name="uid">Entity to modify.</param>
/// <typeparam name="T">Component to add.</typeparam>
/// <returns>The component in question</returns>
T EnsureComponent<T>(EntityUid uid) where T : Component, new();
/// <summary>
/// Returns the component of a specific type.
/// </summary>

View File

@@ -0,0 +1,38 @@
namespace Robust.Shared.GameObjects
{
public sealed class CollideOnAnchorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CollideOnAnchorComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<CollideOnAnchorComponent, AnchorStateChangedEvent>(OnAnchor);
}
private void OnStartup(EntityUid uid, CollideOnAnchorComponent component, ComponentStartup args)
{
if (!EntityManager.TryGetComponent(uid, out TransformComponent? transformComponent)) return;
SetCollide(uid, component, transformComponent.Anchored);
}
private void OnAnchor(EntityUid uid, CollideOnAnchorComponent component, ref AnchorStateChangedEvent args)
{
SetCollide(uid, component, args.Anchored);
}
private void SetCollide(EntityUid uid, CollideOnAnchorComponent component, bool anchored)
{
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? body)) return;
var enabled = component.Enable;
if (!anchored)
{
enabled ^= true;
}
body.CanCollide = enabled;
}
}
}

View File

@@ -61,7 +61,10 @@ namespace Robust.Shared.GameObjects
if (!EntityManager.TryGetComponent<PhysicsComponent>(uid, out var body)) return;
// If we're attached to the map we'll also just never disable collision due to how grid movement works.
body.CanCollide = !component.Enabled || body.Awake || body.Joints.Any() || EntityManager.GetComponent<TransformComponent>(uid).GridID == GridId.Invalid;
body.CanCollide = !component.Enabled ||
body.Awake ||
(EntityManager.TryGetComponent(uid, out JointComponent? jointComponent) && jointComponent.JointCount > 0) ||
EntityManager.GetComponent<TransformComponent>(uid).GridID == GridId.Invalid;
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
@@ -40,7 +41,7 @@ namespace Robust.Shared.GameObjects
IEnumerable<IEntity> GetEntitiesInArc(EntityCoordinates coordinates, float range, Angle direction,
float arcWidth, LookupFlags flags = LookupFlags.IncludeAnchored);
IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 position, LookupFlags flags = LookupFlags.IncludeAnchored);
IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 worldAABB, LookupFlags flags = LookupFlags.IncludeAnchored);
IEnumerable<IEntity> GetEntitiesIntersecting(IEntity entity, LookupFlags flags = LookupFlags.IncludeAnchored);
@@ -50,7 +51,7 @@ namespace Robust.Shared.GameObjects
IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Vector2 position, LookupFlags flags = LookupFlags.IncludeAnchored);
void FastEntitiesIntersecting(in MapId mapId, ref Box2 position, EntityQueryCallback callback, LookupFlags flags = LookupFlags.IncludeAnchored);
void FastEntitiesIntersecting(in MapId mapId, ref Box2 worldAABB, EntityQueryCallback callback, LookupFlags flags = LookupFlags.IncludeAnchored);
IEnumerable<IEntity> GetEntitiesInRange(EntityCoordinates position, float range, LookupFlags flags = LookupFlags.IncludeAnchored);
@@ -263,17 +264,52 @@ namespace Robust.Shared.GameObjects
#region Spatial Queries
private IEnumerable<EntityLookupComponent> GetLookupsIntersecting(MapId mapId, Box2 worldAABB)
private LookupsEnumerator GetLookupsIntersecting(MapId mapId, Box2 worldAABB)
{
if (mapId == MapId.Nullspace) yield break;
_mapManager.FindGridsIntersectingEnumerator(mapId, worldAABB, out var gridEnumerator, true);
// TODO: Recursive and all that.
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB.Enlarged(_lookupEnlargementRange)))
return new LookupsEnumerator(_entityManager, _mapManager, mapId, gridEnumerator);
}
private struct LookupsEnumerator
{
private IEntityManager _entityManager;
private IMapManager _mapManager;
private MapId _mapId;
private FindGridsEnumerator _enumerator;
private bool _final;
public LookupsEnumerator(IEntityManager entityManager, IMapManager mapManager, MapId mapId, FindGridsEnumerator enumerator)
{
yield return _entityManager.GetEntity(grid.GridEntityId).GetComponent<EntityLookupComponent>();
_entityManager = entityManager;
_mapManager = mapManager;
_mapId = mapId;
_enumerator = enumerator;
_final = false;
}
yield return _mapManager.GetMapEntity(mapId).GetComponent<EntityLookupComponent>();
public bool MoveNext([NotNullWhen(true)] out EntityLookupComponent? component)
{
if (!_enumerator.MoveNext(out var grid))
{
if (_final || _mapId == MapId.Nullspace)
{
component = null;
return false;
}
_final = true;
component = _mapManager.GetMapEntity(_mapId).GetComponent<EntityLookupComponent>();
return true;
}
// TODO: Recursive and all that.
component = _entityManager.GetComponent<EntityLookupComponent>(grid.GridEntityId);
return true;
}
}
private IEnumerable<IEntity> GetAnchored(MapId mapId, Box2 worldAABB, LookupFlags flags)
@@ -293,8 +329,9 @@ namespace Robust.Shared.GameObjects
public bool AnyEntitiesIntersecting(MapId mapId, Box2 box, LookupFlags flags = LookupFlags.IncludeAnchored)
{
var found = false;
var enumerator = GetLookupsIntersecting(mapId, box);
foreach (var lookup in GetLookupsIntersecting(mapId, box))
while (enumerator.MoveNext(out var lookup))
{
var offsetBox = lookup.Owner.Transform.InvWorldMatrix.TransformBox(box);
@@ -320,20 +357,21 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void FastEntitiesIntersecting(in MapId mapId, ref Box2 position, EntityQueryCallback callback, LookupFlags flags = LookupFlags.IncludeAnchored)
public void FastEntitiesIntersecting(in MapId mapId, ref Box2 worldAABB, EntityQueryCallback callback, LookupFlags flags = LookupFlags.IncludeAnchored)
{
foreach (var lookup in GetLookupsIntersecting(mapId, position))
var enumerator = GetLookupsIntersecting(mapId, worldAABB);
while (enumerator.MoveNext(out var lookup))
{
var offsetBox = lookup.Owner.Transform.InvWorldMatrix.TransformBox(position);
var offsetBox = lookup.Owner.Transform.InvWorldMatrix.TransformBox(worldAABB);
lookup.Tree._b2Tree.FastQuery(ref offsetBox, (ref IEntity data) => callback(data));
}
if ((flags & LookupFlags.IncludeAnchored) != 0x0)
{
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, position))
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
{
foreach (var uid in grid.GetAnchoredEntities(position))
foreach (var uid in grid.GetAnchoredEntities(worldAABB))
{
if (!_entityManager.TryGetEntity(uid, out var ent)) continue;
callback(ent);
@@ -343,15 +381,16 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 position, LookupFlags flags = LookupFlags.IncludeAnchored)
public IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 worldAABB, LookupFlags flags = LookupFlags.IncludeAnchored)
{
if (mapId == MapId.Nullspace) return Enumerable.Empty<IEntity>();
var list = new List<IEntity>();
var enumerator = GetLookupsIntersecting(mapId, worldAABB);
foreach (var lookup in GetLookupsIntersecting(mapId, position))
while (enumerator.MoveNext(out var lookup))
{
var offsetBox = lookup.Owner.Transform.InvWorldMatrix.TransformBox(position);
var offsetBox = lookup.Owner.Transform.InvWorldMatrix.TransformBox(worldAABB);
lookup.Tree.QueryAabb(ref list, (ref List<IEntity> list, in IEntity ent) =>
{
@@ -363,7 +402,7 @@ namespace Robust.Shared.GameObjects
}, offsetBox, (flags & LookupFlags.Approximate) != 0x0);
}
foreach (var ent in GetAnchored(mapId, position, flags))
foreach (var ent in GetAnchored(mapId, worldAABB, flags))
{
list.Add(ent);
}
@@ -380,7 +419,9 @@ namespace Robust.Shared.GameObjects
var list = new List<IEntity>();
var state = (list, position);
foreach (var lookup in GetLookupsIntersecting(mapId, aabb))
var enumerator = GetLookupsIntersecting(mapId, aabb);
while (enumerator.MoveNext(out var lookup))
{
var localPoint = lookup.Owner.Transform.InvWorldMatrix.Transform(position);
@@ -545,8 +586,9 @@ namespace Robust.Shared.GameObjects
var state = (list, position);
var aabb = new Box2(position, position).Enlarged(PointEnlargeRange);
var enumerator = GetLookupsIntersecting(mapId, aabb);
foreach (var lookup in GetLookupsIntersecting(mapId, aabb))
while (enumerator.MoveNext(out var lookup))
{
var offsetPos = lookup.Owner.Transform.InvWorldMatrix.Transform(position);
@@ -669,9 +711,10 @@ namespace Robust.Shared.GameObjects
{
// TODO: Need to fix ordering issues and then we can just directly remove it from the tree
// rather than this O(n) legacy garbage.
// Also we can't early returns because somehow it gets added to multiple trees!!!
foreach (var lookup in _entityManager.EntityQuery<EntityLookupComponent>(true))
{
if (lookup.Tree.Remove(entity)) return;
lookup.Tree.Remove(entity);
}
}

View File

@@ -0,0 +1,27 @@
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Robust.Shared.GameObjects
{
public class SharedAudioSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
protected EntityCoordinates GetFallbackCoordinates(MapCoordinates mapCoordinates)
{
if (_mapManager.TryFindGridAt(mapCoordinates, out var mapGrid))
{
return new EntityCoordinates(mapGrid.GridEntityId,
mapGrid.WorldToLocal(mapCoordinates.Position));
}
if (_mapManager.HasMapEntity(mapCoordinates.MapId))
{
return new EntityCoordinates(_mapManager.GetMapEntityId(mapCoordinates.MapId),
mapCoordinates.Position);
}
return EntityCoordinates.Invalid;
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Robust.Shared.GameObjects
/// Only reason this exists is so we can avoid the message allocation when not needed.
/// It's only useful for debugging and is kind of a perf sink.
/// </summary>
public class SharedDebugPhysicsSystem : EntitySystem
public abstract class SharedDebugPhysicsSystem : EntitySystem
{
public virtual void HandlePreSolve(Contact contact, in Manifold oldManifold) {}
}

View File

@@ -0,0 +1,348 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
namespace Robust.Shared.GameObjects
{
/*
* Handles all of the public query methods for physics.
*/
public partial class SharedPhysicsSystem
{
[Dependency] private readonly SharedBroadphaseSystem _broadphaseSystem = default!;
/// <summary>
/// Get the percentage that 2 bodies overlap. Ignores whether collision is turned on for either body.
/// </summary>
/// <param name="bodyA"></param>
/// <param name="bodyB"></param>
/// <returns> 0 -> 1.0f based on WorldAABB overlap</returns>
public float IntersectionPercent(PhysicsComponent bodyA, PhysicsComponent bodyB)
{
// TODO: Use actual shapes and not just the AABB?
return bodyA.GetWorldAABB().IntersectPercentage(bodyB.GetWorldAABB());
}
/// <summary>
/// Checks to see if the specified collision rectangle collides with any of the physBodies under management.
/// Also fires the OnCollide event of the first managed physBody to intersect with the collider.
/// </summary>
/// <param name="collider">Collision rectangle to check</param>
/// <param name="mapId">Map to check on</param>
/// <param name="approximate"></param>
/// <returns>true if collides, false if not</returns>
public bool TryCollideRect(Box2 collider, MapId mapId, bool approximate = true)
{
var state = (collider, mapId, found: false);
foreach (var broadphase in _broadphaseSystem.GetBroadphases(mapId, collider))
{
var gridCollider = broadphase.Owner.Transform.InvWorldMatrix.TransformBox(collider);
broadphase.Tree.QueryAabb(ref state, (ref (Box2 collider, MapId map, bool found) state, in FixtureProxy proxy) =>
{
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
return true;
}, gridCollider, approximate);
}
return state.found;
}
public IEnumerable<PhysicsComponent> GetCollidingEntities(PhysicsComponent body, Vector2 offset, bool approximate = true)
{
var broadphase = body.Broadphase;
if (broadphase == null)
{
return Array.Empty<PhysicsComponent>();
}
var entities = new List<PhysicsComponent>();
var state = (body, entities);
foreach (var fixture in body._fixtures)
{
foreach (var proxy in fixture.Proxies)
{
broadphase.Tree.QueryAabb(ref state,
(ref (PhysicsComponent body, List<PhysicsComponent> entities) state,
in FixtureProxy other) =>
{
if (other.Fixture.Body.Deleted || other.Fixture.Body == body) return true;
if ((proxy.Fixture.CollisionMask & other.Fixture.CollisionLayer) == 0x0) return true;
if (!body.ShouldCollide(other.Fixture.Body)) return true;
state.entities.Add(other.Fixture.Body);
return true;
}, proxy.AABB, approximate);
}
}
return entities;
}
/// <summary>
/// Get all entities colliding with a certain body.
/// </summary>
public IEnumerable<PhysicsComponent> GetCollidingEntities(MapId mapId, in Box2 worldAABB)
{
if (mapId == MapId.Nullspace) return Array.Empty<PhysicsComponent>();
var bodies = new HashSet<PhysicsComponent>();
foreach (var broadphase in _broadphaseSystem.GetBroadphases(mapId, worldAABB))
{
var gridAABB = broadphase.Owner.Transform.InvWorldMatrix.TransformBox(worldAABB);
foreach (var proxy in broadphase.Tree.QueryAabb(gridAABB, false))
{
bodies.Add(proxy.Fixture.Body);
}
}
return bodies;
}
/// <summary>
/// Get all entities colliding with a certain body.
/// </summary>
public IEnumerable<PhysicsComponent> GetCollidingEntities(MapId mapId, in Box2Rotated worldBounds)
{
if (mapId == MapId.Nullspace) return Array.Empty<PhysicsComponent>();
var bodies = new HashSet<PhysicsComponent>();
foreach (var broadphase in _broadphaseSystem.GetBroadphases(mapId, worldBounds.CalcBoundingBox()))
{
var gridAABB = broadphase.Owner.Transform.InvWorldMatrix.TransformBox(worldBounds);
foreach (var proxy in broadphase.Tree.QueryAabb(gridAABB, false))
{
bodies.Add(proxy.Fixture.Body);
}
}
return bodies;
}
// TODO: This is mainly just a hack to get rotated grid doors working in SS14 but whenever we do querysystem we'll clean this shit up
/// <summary>
/// Returns all enabled physics bodies intersecting this body.
/// </summary>
/// <remarks>
/// Does not consider CanCollide on the provided body.
/// </remarks>
/// <param name="body">The body to check for intersection</param>
/// <param name="enlarge">How much to enlarge / shrink the bounds by given we often extend them slightly.</param>
/// <returns></returns>
public IEnumerable<PhysicsComponent> GetCollidingEntities(PhysicsComponent body, float enlarge = 0f)
{
// TODO: Should use the collisionmanager test for overlap instead (once I optimise and check it actually works).
var mapId = body.Owner.Transform.MapID;
if (mapId == MapId.Nullspace || body.FixtureCount == 0) return Array.Empty<PhysicsComponent>();
var bodies = new HashSet<PhysicsComponent>();
var transform = body.GetTransform();
var worldAABB = body.GetWorldAABB(transform.Position, transform.Quaternion2D.Angle);
foreach (var broadphase in _broadphaseSystem.GetBroadphases(mapId, worldAABB))
{
var invMatrix = broadphase.Owner.Transform.InvWorldMatrix;
var localTransform = new Transform(invMatrix.Transform(transform.Position), transform.Quaternion2D.Angle - broadphase.Owner.Transform.WorldRotation);
foreach (var fixture in body._fixtures)
{
var collisionMask = fixture.CollisionMask;
var collisionLayer = fixture.CollisionLayer;
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
var aabb = fixture.Shape.ComputeAABB(localTransform, i).Enlarged(enlarge);
foreach (var proxy in broadphase.Tree.QueryAabb(aabb, false))
{
var proxyFixture = proxy.Fixture;
var proxyBody = proxyFixture.Body;
if (proxyBody == body ||
(proxyFixture.CollisionMask & collisionLayer) == 0x0 &&
(collisionMask & proxyFixture.CollisionLayer) == 0x0) continue;
bodies.Add(proxyBody);
}
}
}
}
return bodies;
}
// TODO: This will get every body but we don't need to do that
/// <summary>
/// Checks whether a body is colliding
/// </summary>
/// <param name="body"></param>
/// <param name="offset"></param>
/// <returns></returns>
public bool IsColliding(PhysicsComponent body, Vector2 offset, bool approximate)
{
return GetCollidingEntities(body, offset, approximate).Any();
}
#region RayCast
/// <summary>
/// Casts a ray in the world, returning the first entity it hits (or all entities it hits, if so specified)
/// </summary>
/// <param name="mapId"></param>
/// <param name="ray">Ray to cast in the world.</param>
/// <param name="maxLength">Maximum length of the ray in meters.</param>
/// <param name="predicate">A predicate to check whether to ignore an entity or not. If it returns true, it will be ignored.</param>
/// <param name="returnOnFirstHit">If true, will only include the first hit entity in results. Otherwise, returns all of them.</param>
/// <returns>A result object describing the hit, if any.</returns>
public IEnumerable<RayCastResults> IntersectRayWithPredicate(MapId mapId, CollisionRay ray,
float maxLength = 50F,
Func<IEntity, bool>? predicate = null, bool returnOnFirstHit = true)
{
List<RayCastResults> results = new();
var endPoint = ray.Position + ray.Direction.Normalized * maxLength;
var rayBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint),
Vector2.ComponentMax(ray.Position, endPoint));
foreach (var broadphase in _broadphaseSystem.GetBroadphases(mapId, rayBox))
{
var invMatrix = broadphase.Owner.Transform.InvWorldMatrix;
var matrix = broadphase.Owner.Transform.WorldMatrix;
var position = invMatrix.Transform(ray.Position);
var gridRot = new Angle(-broadphase.Owner.Transform.WorldRotation.Theta);
var direction = gridRot.RotateVec(ray.Direction);
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
broadphase.Tree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0) return true;
if (distFromOrigin > maxLength)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
{
return true;
}
if (predicate?.Invoke(proxy.Fixture.Body.Owner) == true)
{
return true;
}
// TODO: Shape raycast here
// Need to convert it back to world-space.
var result = new RayCastResults(distFromOrigin, matrix.Transform(point), proxy.Fixture.Body.Owner);
results.Add(result);
#if DEBUG
EntityManager.EventBus.QueueEvent(EventSource.Local,
new DebugDrawRayMessage(
new DebugRayData(ray, maxLength, result)));
#endif
return true;
}, gridRay);
}
#if DEBUG
if (results.Count == 0)
{
EntityManager.EventBus.QueueEvent(EventSource.Local,
new DebugDrawRayMessage(
new DebugRayData(ray, maxLength, null)));
}
#endif
results.Sort((a, b) => a.Distance.CompareTo(b.Distance));
return results;
}
/// <summary>
/// Casts a ray in the world and returns the first entity it hits, or a list of all entities it hits.
/// </summary>
/// <param name="mapId"></param>
/// <param name="ray">Ray to cast in the world.</param>
/// <param name="maxLength">Maximum length of the ray in meters.</param>
/// <param name="ignoredEnt">A single entity that can be ignored by the RayCast. Useful if the ray starts inside the body of an entity.</param>
/// <param name="returnOnFirstHit">If false, will return a list of everything it hits, otherwise will just return a list of the first entity hit</param>
/// <returns>An enumerable of either the first entity hit or everything hit</returns>
public IEnumerable<RayCastResults> IntersectRay(MapId mapId, CollisionRay ray, float maxLength = 50, IEntity? ignoredEnt = null, bool returnOnFirstHit = true)
=> IntersectRayWithPredicate(mapId, ray, maxLength, entity => entity == ignoredEnt, returnOnFirstHit);
/// <summary>
/// Casts a ray in the world and returns the distance the ray traveled while colliding with entities
/// </summary>
/// <param name="mapId"></param>
/// <param name="ray">Ray to cast in the world.</param>
/// <param name="maxLength">Maximum length of the ray in meters.</param>
/// <param name="ignoredEnt">A single entity that can be ignored by the RayCast. Useful if the ray starts inside the body of an entity.</param>
/// <returns>The distance the ray traveled while colliding with entities</returns>
public float IntersectRayPenetration(MapId mapId, CollisionRay ray, float maxLength, IEntity? ignoredEnt = null)
{
var penetration = 0f;
var endPoint = ray.Position + ray.Direction.Normalized * maxLength;
var rayBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint),
Vector2.ComponentMax(ray.Position, endPoint));
foreach (var broadphase in _broadphaseSystem.GetBroadphases(mapId, rayBox))
{
var offset = broadphase.Owner.Transform.InvWorldMatrix.Transform(ray.Position);
var gridRot = new Angle(-broadphase.Owner.Transform.WorldRotation.Theta);
var direction = gridRot.RotateVec(ray.Direction);
var gridRay = new CollisionRay(offset, direction, ray.CollisionMask);
broadphase.Tree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (distFromOrigin > maxLength || proxy.Fixture.Body.Owner == ignoredEnt) return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
{
return true;
}
if (new Ray(point + ray.Direction * proxy.AABB.Size.Length * 2, -ray.Direction).Intersects(
proxy.AABB, out _, out var exitPoint))
{
penetration += (point - exitPoint).Length;
}
return true;
}, gridRay);
}
#if DEBUG
if (penetration > 0f)
{
EntityManager.EventBus.QueueEvent(EventSource.Local,
new DebugDrawRayMessage(
new DebugRayData(ray, maxLength, null)));
}
#endif
return penetration;
}
#endregion
}
}

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