Compare commits

...

77 Commits

Author SHA1 Message Date
ElectroJr
e7c6151310 Version: 193.2.0 2023-12-17 20:39:52 -05:00
Leon Friedrich
005c2e784a Add more PVS logs (#4730) 2023-12-18 12:36:48 +11:00
ElectroJr
1009dd3ea0 Version: 193.1.1 2023-12-16 23:29:50 -05:00
ElectroJr
87b82160b0 Fix #if FULL_RELEASE error 2023-12-16 23:28:58 -05:00
ElectroJr
fc318c9ecd Version: 193.1.0 2023-12-16 22:56:16 -05:00
Leon Friedrich
ac957ca7fc Try fix PVS bugs (#4728)
* Try fix PVS bugs

* change order
2023-12-17 14:55:46 +11:00
Leon Friedrich
1bdd82b0bf Raise ECS event on prototype-reload (#4724)
* Raise system event on prototype-reload

* Oops
2023-12-17 08:30:53 +11:00
Leon Friedrich
0150c5e6ff Add IPrototypeManager.GetInstances() (#4723) 2023-12-17 08:29:31 +11:00
Pieter-Jan Briers
b2cc90d00f Convert static readonly dictionaries to FrozenDictionary (#4722)
* Convert static readonly dictionaries to FrozenDictionary

* Also `IReadOnlyDictionary`
2023-12-17 04:47:01 +11:00
Leon Friedrich
1f8b89e92f Add frozen collections to sandbox and add yaml type serializers (#4721) 2023-12-16 18:36:58 +01:00
ElectroJr
33d394295e Version: 193.0.0 2023-12-16 11:42:37 -05:00
Leon Friedrich
4934a9c5a5 Combine PVS dictionaries (#4693)
* Try combine dictionaries

* Remove last-ack

* Remove lastSent

* Use structs

* Don't clear previous-sent on deletion

* Pass data into ProcessEntry

* Store visibility data in EntityData

* Turn ToSend into hashset

* Add EntityStringRepresentation constructors

* Add MetaDataComponent to EntityData

* Remove metadata cache

* Create GetStates partial class

* add ToSend partial class

* fix debug asserts

* Cleanup

* Rename partial class

* Try to optimize `ProcessLeavePvs`

* Add ProcessLeavePvsBenchmark

* Turn HashSet into List

* Collect expensive asserts into one block

* Revert "Add ProcessLeavePvsBenchmark"

This reverts commit 93d984e929.

* remove comment
2023-12-17 03:37:48 +11:00
Leon Friedrich
91ebc3eb02 Use FrozenDictionary for prototype instances (#4719)
* Use FrozenDictionary for prototype instances

* Add assert

* Change log to verbose

* RStopwatch.StartNew

* Rename TryGetKindInstances
2023-12-17 03:22:18 +11:00
Leon Friedrich
bc84590a33 Make ChildEnumerator out value non-nullable (#4707)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-17 03:10:20 +11:00
Kelrak
e3944dc6fb Should resolve some error spam in non EFX systems (#4704)
* Should resolve some error spam in non EFX systems

* Exit early if not supported
2023-12-16 15:57:59 +11:00
Leon Friedrich
6a77f4c27b Use frozen dictionaries for prototype kinds (#4718)
* Use frozen dictionaries for prototype kinds

* More logs

* AAAA

* Change log to verbose

* RStopwatch.StartNew()
2023-12-16 15:51:55 +11:00
Leon Friedrich
6246ae412e Make RobustMappedStringSerializer use frozen dictionaries (#4717)
* Make RobustMappedStringSerializer use frozen dictionaries

* Update RobustMappedStringSerializer.MappedStringDict.cs

* Change log to verbose

* RStopwatch.StartNew()
2023-12-16 15:48:57 +11:00
ElectroJr
ac86accc20 Version: 192.0.0 2023-12-15 23:43:53 -05:00
Leon Friedrich
19ff7f25ca Slightly speed up IDependencyCollection (#4716) 2023-12-16 00:23:12 +01:00
Pieter-Jan Briers
3f83733a03 Decode ogg files as shorts instead of floats (#4715) 2023-12-15 20:48:07 +01:00
Leon Friedrich
438fed2f0e Make TryGetEntity protected (#4713)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2023-12-15 19:40:43 +01:00
Pieter-Jan Briers
816a535a92 Fix obsoletion warnings for BinaryFormatter stuff
This became an outright warning in .NET 8. Into the trash, never used it.
2023-12-15 19:37:13 +01:00
Pieter-Jan Briers
01df42aa8f Fix ICommonSession.ConnectedClient obsoletion warnings 2023-12-15 19:34:30 +01:00
Pieter-Jan Briers
a200d73ef9 Use FrozenDictionary in DependencyCollection.
Did not test the perf of this at all but it's the perfect use case so...
2023-12-15 19:28:56 +01:00
metalgearsloth
8d30735ffb Run ProcessQueuedAcks asynchronously (#4696) 2023-12-15 19:10:09 +01:00
Pieter-Jan Briers
2686150f9d Version: 191.0.1 2023-12-15 19:08:35 +01:00
Pieter-Jan Briers
d720e9393b Fix .NET 8 sandbox.
Oops.
2023-12-15 19:08:06 +01:00
Pieter-Jan Briers
f5b1c26bec Update Lidgren to fix nullable compilation issue 2023-12-15 18:28:10 +01:00
Pieter-Jan Briers
3204002c72 Version: 191.0.0 2023-12-15 18:09:15 +01:00
Pieter-Jan Briers
4c79d0c6d0 .NET 8 (#4712) 2023-12-15 18:07:41 +01:00
Pieter-Jan Briers
e0d38fb8bd Fix TryGetResource silently eating exceptions
why god
2023-12-15 16:13:17 +01:00
Leon Friedrich
250f6ca7db Obsolete TransformComponent.ChildEntities (#4706)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-14 17:50:29 +11:00
KP
773365c185 Add a workaround for MacOS' bad CoreAudio OpenAL gain behaviour (#4710)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-14 17:41:52 +11:00
metalgearsloth
9a1e6af586 Remove DeferMoveEvent (#4709) 2023-12-14 17:20:02 +11:00
Pieter-Jan Briers
dc96318379 Fix Lidgren compile on GitHub hopefully 2023-12-13 19:08:08 +01:00
Pieter-Jan Briers
31a3f145de Add [ViewVariables] to BaseButton.EnableAllKeybinds
Makes it show up in the devwindow UI inspector.
2023-12-13 00:28:33 +01:00
Pieter-Jan Briers
331e1fcc81 Update Lidgren to v0.2.7
This adds NRT annotations. Fix compatibility with those.
2023-12-13 00:06:00 +01:00
Leon Friedrich
dc7a51e582 Fix showrays networking (#4701) 2023-12-12 20:17:36 +11:00
metalgearsloth
bfe8e687da Fix audio params variation (#4699) 2023-12-12 20:16:43 +11:00
metalgearsloth
04b6d60d76 Fix TileEdgeOverlay drawing over grids (#4698) 2023-12-12 20:13:20 +11:00
Leon Friedrich
5bc5bfd58a Add more implicit Entity<T> casts (#4552) 2023-12-12 13:27:31 +11:00
TemporalOroboros
56899b4e64 Some ContainerManagerComponent ECSing (#4622) 2023-12-12 13:24:18 +11:00
DrSmugleaf
a23915e0dd Make MapLoaderSystem.GetSaveData public (#4702) 2023-12-12 13:09:39 +11:00
metalgearsloth
726d91c5e8 Version: 190.1.1 2023-12-11 21:20:58 +11:00
metalgearsloth
d8a8783680 Revert "Avoid recontruction broadphase job every tick (#4653)" (#4695) 2023-12-11 20:11:57 +11:00
metalgearsloth
8839dd9a3b Version: 190.1.0 2023-12-11 19:56:10 +11:00
metalgearsloth
0296d9635c Fix some grid setting asserts (#4580) 2023-12-11 19:49:55 +11:00
metalgearsloth
f24d9751d4 Add GetRotation for matrices and better precision (#4691) 2023-12-11 18:40:57 +11:00
wixoa
58ac82ae55 Add OnGrabbed and OnReleased events to the Slider control (#4694) 2023-12-11 18:39:09 +11:00
metalgearsloth
b9130bf236 Version: 190.0.0 2023-12-10 21:33:56 +11:00
metalgearsloth
a2cd33afe5 Add better OpenAL logging (#4687) 2023-12-10 21:25:43 +11:00
metalgearsloth
1772651049 Fix color_picker shader compilation (#4690) 2023-12-10 21:21:55 +11:00
Leon Friedrich
826fa4d131 Ensure shaders parameters are updated when swapping instances (#4689) 2023-12-10 20:36:23 +11:00
Leon Friedrich
0b712ae86c Add colour gradients to sliders (#4688) 2023-12-10 20:35:43 +11:00
Łukasz Mędrek
22528fc484 Fix colour space translation feedback loop(#3839) (#4681) 2023-12-10 14:10:43 +11:00
Leon Friedrich
20a411e6ce Remove non-networked comps from NetComponents dictionary (#4686) 2023-12-10 13:56:34 +11:00
Leon Friedrich
6f9ed8a242 Stop terminating entities from being prematurely detached to nullspace (#4675) 2023-12-10 13:18:42 +11:00
Łukasz Mędrek
790f4c1309 Fix HSV and HSL producing black color on hue 360 (#4682) 2023-12-10 13:14:15 +11:00
metalgearsloth
0b62cb6445 Change netsyncenabled to an assert (#4604) 2023-12-10 13:03:25 +11:00
metalgearsloth
9d0f4d8a08 Update release notes (#4684) 2023-12-10 12:49:47 +11:00
metalgearsloth
5069b0ccf9 Version: 189.0.0 2023-12-10 12:44:00 +11:00
chromiumboy
12cfdb2175 Tweaked how ExpandPvsEvent is raised (#4665) 2023-12-10 12:35:00 +11:00
metalgearsloth
b5e079815d Add MIDI semaphore (#4643) 2023-12-10 12:34:24 +11:00
metalgearsloth
ca3a3279c5 Set priorGain to 0 if gain is 0 (#4680) 2023-12-10 12:31:06 +11:00
Leon Friedrich
003752a161 Improve yaml linter error messages (#4683) 2023-12-10 12:18:52 +11:00
Leon Friedrich
55e51cba9c Fix client-side entity error spam (#4673) 2023-12-10 12:17:46 +11:00
Leon Friedrich
2cd829f4f6 SpriteView Modulation (#4584) 2023-12-10 12:02:21 +11:00
metalgearsloth
43138669ec Network base not adjusted audio params (#4679) 2023-12-09 17:12:09 +11:00
metalgearsloth
3fe30bc00f Version: 188.0.0 2023-12-09 15:17:37 +11:00
metalgearsloth
3ccbdeac6a Fix GetDimensions for screenhandle (#4677) 2023-12-09 15:04:45 +11:00
metalgearsloth
9eb9c91da6 Fix predicted audio not using adjust params (#4676) 2023-12-09 14:59:22 +11:00
metalgearsloth
28d2b47a2c Log errors on spawning audio to deleting ents (#4629) 2023-12-09 14:30:18 +11:00
metalgearsloth
049ffa05e4 Remove EntityQuery<T> from MapVelocity API (#4648) 2023-12-09 14:12:04 +11:00
metalgearsloth
2f36a0a5fc Change midi volume to gain (#4639) 2023-12-09 14:03:01 +11:00
Kelrak
41d03db59d Possible fix to some audio issues (#4640) 2023-12-09 13:39:09 +11:00
Łukasz Mędrek
68df887a65 Fix sorting order in entity spawn panel (#3767) (#4671) 2023-12-09 12:46:57 +11:00
metalgearsloth
e0bbcd7b08 Return null buffered audio on exception (#4624) 2023-12-09 12:44:16 +11:00
169 changed files with 3846 additions and 2127 deletions

View File

@@ -14,7 +14,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -22,7 +22,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Package client
run: Tools/package_client_build.py -p windows mac linux

View File

@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Disable submodule autoupdate
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE

View File

@@ -10,6 +10,9 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

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

View File

@@ -1,8 +1,8 @@
<Project>
<!-- Engine-specific properties. Content should not use this file. -->
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>11</LangVersion>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

View File

@@ -3,7 +3,7 @@
<!-- Import this at the end of any project files in Robust and Content. -->
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<Import Project="Robust.Custom.targets" Condition="Exists('Robust.Custom.targets')"/>

View File

@@ -61,18 +61,5 @@ namespace OpenToolkit.GraphicsLibraryFramework
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GLFWException"/> class with the specified context
/// and the serialization information.
/// </summary>
/// <param name="info">The <see cref="SerializationInfo"/> associated with this exception.</param>
/// <param name="context">
/// A <see cref="StreamingContext"/> that represents the context of this exception.
/// </param>
protected GLFWException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@@ -54,6 +54,169 @@ END TEMPLATE-->
*None yet*
## 193.2.0
### Other
* Added more PVS error logs
## 193.1.1
### Bugfixes
* Fixed an exception when building in FULL_RELEASE
## 193.1.0
### New features
* Added FrozenDictionary and FrozenHashSet to sandbox whitelist
* Added yaml type serializers for FrozenDictionary and FrozenHashSet
* Added `IPrototypeManager.GetInstances<T>()`
* `IPrototypeManager` now also raises `PrototypesReloadedEventArgs` as a system event.
### Bugfixes
* Might fix some PVS bugs added in the last version.
### Internal
* Various static dictionaries have been converted into FrozenDictionary.
## 193.0.0
### Breaking changes
* The `TransformChildrenEnumerator`'s out values are now non-nullable
### New features
* Added `IPrototypeManager.TryGetInstances()`, which returns a dictionary of prototype instances for a given prototype kind/type.
### Bugfixes
* Fixed `BaseAudioSource.SetAuxiliary()` throwing errors on non-EFX systems
### Internal
* The internals of PVS system have been reworked to reduce the number of dictionary lookups.
* `RobustMappedStringSerializer` now uses frozen dictionaries
* `IPrototypeManager` now uses frozen dictionaries
## 192.0.0
### Breaking changes
* `EntitySystem.TryGetEntity` is now `protected`.
### Internal
* PVS message ack processing now happens asynchronously
* Dependency collections now use a `FrozenDictionary`
## 191.0.1
### Bugfixes
.* Fix sandbox being broken thanks to .NET 8.
## 191.0.0
### Breaking changes
* Robust now uses **.NET 8**. Nyoom.
### Bugfixes
* `IResourceCache.TryGetResource<T>` won't silently eat all exceptions anymore.
## 190.1.1
### Bugfixes
* Revert broadphase job to prevent OOM from logs.
## 190.1.0
### New features
* Add OnGrabbed / OnReleased to slider controls.
* Add Rotation method for matrices and also make the precision slightly better when angles are passed in by taking double-precision not single-precision floats.
### Bugfixes
* Fix some grid setting asserts when adding gridcomponent to existing maps.
## 190.0.0
### New features
* Add color gradients to sliders.
### Bugfixes
* Fix HSV / HSL producing black colors on 360 hue.
* Stop terminating entities from prematurely detaching to nullspace.
* Ensure shader parameters update when swapping instances.
### Other
* Add more verbose logging to OpenAL errors.
### Internal
* Change NetSyncEnabled to an assert and fix instances where it slips through to PVS.
## 189.0.0
### Breaking changes
* Use the base AudioParams for networking not the z-offset adjusted ones.
* Modulate SpriteView sprites by the control's color modulation.
### New features
* Improve YAML linter error messages for parent nodes.
* ExpandPvsEvent will also be raised directed to the session's attached entity.
### Bugfixes
* Client clientside entity error spam.
### Internal
* Set priorGain to 0 where no EFX is supported for audio rather than 0.5.
* Try to hotfix MIDI lock contention more via a semaphore.
## 188.0.0
### Breaking changes
* Return null buffered audio if there's an exception and use the dummy instance internally.
* Use entity name then suffix for entity spawn window ordering.
* Change MidiManager volume to gain.
* Remove EntityQuery from the MapVelocity API.
### Bugfixes
* Potentially fix some audio issues by setting gain to half where EFX not found and the prior gain was 0.
* Log errors upon trying to spawn audio attached to deleted entities instead of trying to spawn them and erroring later.
* Fixed predicted audio spawns not applying the adjusted audio params.
* Fix GetDimensions for the screenhandle where the text is only a single line.
## 187.2.0
### New features
@@ -322,7 +485,7 @@ END TEMPLATE-->
### Breaking changes
* Most methods in ActorSystem have been moved to ISharedPlayerManager.
* Most methods in ActorSystem have been moved to ISharedPlayerManager.
* Several actor/player related components and events have been moved to shared.
### New features

View File

@@ -12,3 +12,8 @@
id: bgra
kind: source
path: "/Shaders/Internal/bgra.swsl"
- type: shader
id: ColorPicker
kind: source
path: "/Shaders/color_picker.swsl"

View File

@@ -0,0 +1,46 @@
// Simple shader for creating a box with colours varying along the x and y axes.
uniform highp vec2 size;
uniform highp vec2 offset;
uniform highp vec4 xAxis;
uniform highp vec4 yAxis;
uniform highp vec4 baseColor;
uniform bool hsv;
void fragment()
{
// Calculate local uv coordinates.
// I.e., if using this shader to draw a box to the screen, (0,0) is the bottom left of the box.
highp float yCoords = 1.0/SCREEN_PIXEL_SIZE.y - FRAGCOORD.y;
highp vec2 uv = vec2(FRAGCOORD.x - offset.x, yCoords - offset.y);
uv /= size;
uv.y = 1.0 - uv.y;
highp vec4 modulate = baseColor + uv.x * xAxis + uv.y * yAxis;
if (hsv)
{
modulate.xyz = hsv2rgb(modulate.xyz);
}
// The UV used for the texture lookup is the TEXTURE UV coordinate, which is different from the coordinates computed above.
COLOR = zTexture(UV) * modulate;
}
// hsv to RGB conversion taken from www.shadertoy.com/view/MsS3Wc
// The MIT License
// Copyright © 2014 Inigo Quilez
// 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.
// https://www.youtube.com/c/InigoQuilez
// https://iquilezles.org
highp vec3 hsv2rgb( in highp vec3 c )
{
highp vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 );
return c.z * mix( vec3(1.0), rgb, c.y);
}

View File

@@ -97,11 +97,11 @@ internal partial class AudioManager
// check the git history, I originally used libvorbisfile which worked and loaded 16 bit LPCM.
if (vorbis.Channels == 1)
{
format = ALFormat.MonoFloat32Ext;
format = ALFormat.Mono16;
}
else if (vorbis.Channels == 2)
{
format = ALFormat.StereoFloat32Ext;
format = ALFormat.Stereo16;
}
else
{
@@ -110,9 +110,9 @@ internal partial class AudioManager
unsafe
{
fixed (float* ptr = vorbis.Data.Span)
fixed (short* ptr = vorbis.Data.Span)
{
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(float),
AL.BufferData(buffer, format, (IntPtr) ptr, vorbis.Data.Length * sizeof(short),
(int) vorbis.SampleRate);
}
}
@@ -214,9 +214,28 @@ internal partial class AudioManager
return new AudioStream(handle, length, channels, name);
}
public void SetMasterVolume(float newVolume)
public void SetMasterGain(float newGain)
{
AL.Listener(ALListenerf.Gain, newVolume);
if (newGain < 0f)
{
OpenALSawmill.Error("Tried to set master gain below 0, clamping to 0");
AL.Listener(ALListenerf.Gain, 0f);
return;
}
#region Platform hack for MacOS
// HACK/BUG: Apple's OpenAL implementation has a bug where values of 0f for listener gain don't actually
// HACK/BUG: prevent sound playback. Workaround is to cap the minimum gain at a value just above 0.
if (OperatingSystem.IsMacOS() && newGain == 0f)
{
OpenALSawmill.Verbose("Not setting gain to 0 because Apple can't write an OpenAL implementation");
AL.Listener(ALListenerf.Gain, float.Epsilon);
return;
}
#endregion Platform hack for MacOS
AL.Listener(ALListenerf.Gain, newGain);
}
public void SetAttenuation(Attenuation attenuation)
@@ -262,7 +281,7 @@ internal partial class AudioManager
_bufferedAudioSources.Remove(handle);
}
public IAudioSource? CreateAudioSource(AudioStream stream)
IAudioSource? IAudioInternal.CreateAudioSource(AudioStream stream)
{
var source = AL.GenSource();
@@ -282,13 +301,15 @@ internal partial class AudioManager
return audioSource;
}
public IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
/// <inheritdoc/>
IBufferedAudioSource? IAudioInternal.CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// ReSharper disable once PossibleInvalidOperationException

View File

@@ -115,7 +115,7 @@ internal sealed partial class AudioManager : IAudioInternal
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
}
internal bool IsMainThread()
@@ -140,6 +140,18 @@ internal sealed partial class AudioManager : IAudioInternal
}
}
/// <summary>
/// Like _checkAlError but allows custom data to be passed in as relevant.
/// </summary>
internal void LogALError(string message, [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();
if (error != ALError.NoError)
{
OpenALSawmill.Error("[{0}:{1}] AL error: {2}, {3}", callerMember, callerLineNumber, error, message);
}
}
public void _checkAlError([CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
{
var error = AL.GetError();

View File

@@ -62,6 +62,18 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
_zOffset = value;
_audio.SetZOffset(value);
var query = AllEntityQuery<AudioComponent>();
while (query.MoveNext(out var audio))
{
// Pythagoras back to normal then adjust.
var maxDistance = GetAudioDistance(audio.Params.MaxDistance);
var refDistance = GetAudioDistance(audio.Params.ReferenceDistance);
audio.MaxDistance = maxDistance;
audio.ReferenceDistance = refDistance;
}
}
}
@@ -118,7 +130,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
public void SetMasterVolume(float value)
{
_audio.SetMasterVolume(value);
_audio.SetMasterGain(value);
}
public override void Shutdown()
@@ -325,13 +337,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
var delta = worldPos - listener.Position;
var distance = delta.Length();
if (distance > 0f && distance < 0.01f)
{
worldPos = listener.Position;
delta = Vector2.Zero;
distance = 0f;
}
// Out of range so just clip it for us.
if (distance > component.MaxDistance)
{
@@ -340,6 +345,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
return;
}
if (distance > 0f && distance < 0.01f)
{
worldPos = listener.Position;
delta = Vector2.Zero;
distance = 0f;
}
// Update audio occlusion
var occlusion = GetOcclusion(listener, delta, distance, entity);
component.Occlusion = occlusion;
@@ -411,7 +423,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (Timing.IsFirstTimePredicted || sound == null)
if (Timing.IsFirstTimePredicted && sound != null)
return PlayEntity(sound, Filter.Local(), source, false, audioParams);
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
@@ -419,7 +431,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
{
if (Timing.IsFirstTimePredicted || sound == null)
if (Timing.IsFirstTimePredicted && sound != null)
return PlayStatic(sound, Filter.Local(), coordinates, false, audioParams);
return null;
@@ -486,6 +498,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(entity))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
@@ -521,6 +539,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
return playing;
@@ -593,8 +617,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
source.PlaybackPosition = offset;
ApplyAudioParams(audioP, comp);
comp.Params = audioP;
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
ApplyAudioParams(comp.Params, comp);
source.StartPlaying();
return (entity, comp);
}
@@ -607,8 +631,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
source.Pitch = audioParams.Pitch;
source.Volume = audioParams.Volume;
source.RolloffFactor = audioParams.RolloffFactor;
source.MaxDistance = audioParams.MaxDistance;
source.ReferenceDistance = audioParams.ReferenceDistance;
source.MaxDistance = GetAudioDistance(audioParams.MaxDistance);
source.ReferenceDistance = GetAudioDistance(audioParams.ReferenceDistance);
source.Looping = audioParams.Loop;
}

View File

@@ -35,7 +35,7 @@ internal sealed class HeadlessAudioManager : IAudioInternal
}
/// <inheritdoc />
public IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
public IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
@@ -56,7 +56,7 @@ internal sealed class HeadlessAudioManager : IAudioInternal
}
/// <inheritdoc />
public void SetMasterVolume(float value)
public void SetMasterGain(float newGain)
{
}

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Audio;
/// <summary>
/// Handles clientside audio.
/// </summary>
internal interface IAudioInternal
internal interface IAudioInternal : IAudioManager
{
void InitializePostWindowing();
void Shutdown();
@@ -23,7 +23,11 @@ internal interface IAudioInternal
IAudioSource? CreateAudioSource(AudioStream stream);
IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false);
/// <summary>
/// Returns a buffered audio source.
/// </summary>
/// <returns>null if unable to create the source.</returns>
IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio=false);
/// <summary>
/// Sets the velocity for the audio listener.
@@ -40,7 +44,6 @@ internal interface IAudioInternal
/// </summary>
void SetRotation(Angle angle);
void SetMasterVolume(float value);
void SetAttenuation(Attenuation attenuation);
/// <summary>

View File

@@ -0,0 +1,9 @@
namespace Robust.Client.Audio;
/// <summary>
/// Public audio API for stuff that can't go through <see cref="AudioSystem"/>
/// </summary>
public interface IAudioManager
{
void SetMasterGain(float gain);
}

View File

@@ -17,9 +17,9 @@ public interface IMidiManager
bool IsAvailable { get; }
/// <summary>
/// Volume, in db.
/// Gain of audio.
/// </summary>
float Volume { get; set; }
float Gain { get; set; }
/// <summary>
/// This method tries to return a midi renderer ready to be used.

View File

@@ -9,6 +9,7 @@ using NFluidsynth;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
@@ -69,28 +70,32 @@ internal sealed partial class MidiManager : IMidiManager
[ViewVariables] private readonly List<IMidiRenderer> _renderers = new();
// To avoid lock contention for now just don't update that much fam.
// To avoid lock contention until some kind of MIDI refactor.
private TimeSpan _nextUpdate;
private TimeSpan _updateFrequency = TimeSpan.FromSeconds(0.1f);
private SemaphoreSlim _updateSemaphore = new(1);
private bool _alive = true;
[ViewVariables] private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private float _gain = 0f;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
public float Gain
{
get => _volume;
get => _gain;
set
{
if (MathHelper.CloseToPercent(_volume, value))
var clamped = Math.Clamp(value, 0f, 1f);
if (MathHelper.CloseToPercent(_gain, clamped))
return;
_cfgMan.SetCVar(CVars.MidiVolume, value);
_cfgMan.SetCVar(CVars.MidiVolume, clamped);
_volumeDirty = true;
}
}
@@ -142,7 +147,7 @@ internal sealed partial class MidiManager : IMidiManager
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
{
_volume = value;
_gain = value;
_volumeDirty = true;
}, true);
@@ -340,7 +345,7 @@ internal sealed partial class MidiManager : IMidiManager
renderer.LoadSoundfont(file.ToString());
}
renderer.Source.Volume = _volume;
renderer.Source.Gain = _gain;
lock (_renderers)
{
@@ -364,21 +369,23 @@ internal sealed partial class MidiManager : IMidiManager
if (_nextUpdate > _timing.RealTime)
return;
// I don't care for accuracy we only have this for performance for now.
_nextUpdate = _timing.RealTime + _updateFrequency;
// Update positions of streams occasionally.
// This has a lot of code duplication with AudioSystem.FrameUpdate(), and they should probably be combined somehow.
// so TRUE
lock (_renderers)
{
if (_renderers.Count == 0)
return;
_updateJob.OurPosition = _audioSys.GetListenerCoordinates();
_updateJob.OurPosition = _audioSys.GetListenerCoordinates();
_parallel.ProcessNow(_updateJob, _renderers.Count);
}
// This semaphore is here to avoid lock contention as much as possible.
_updateSemaphore.Wait();
// The ONLY time this should be contested is with ThreadUpdate.
// If that becomes NOT the case then just lock this, remove the semaphore, and drop the update frequency even harder.
// ReSharper disable once InconsistentlySynchronizedField
_parallel.ProcessNow(_updateJob, _renderers.Count);
_updateSemaphore.Release();
_volumeDirty = false;
}
@@ -393,7 +400,7 @@ internal sealed partial class MidiManager : IMidiManager
if (_volumeDirty)
{
renderer.Source.Volume = Volume;
renderer.Source.Gain = Gain;
}
if (!renderer.Mono)
@@ -433,7 +440,7 @@ internal sealed partial class MidiManager : IMidiManager
// Was previously muted maybe so try unmuting it?
if (renderer.Source.Gain == 0f)
{
renderer.Source.Volume = Volume;
renderer.Source.Gain = Gain;
}
var worldPos = mapPos.Position;
@@ -489,21 +496,39 @@ internal sealed partial class MidiManager : IMidiManager
{
lock (_renderers)
{
var toRemove = new ValueList<IMidiRenderer>();
for (var i = 0; i < _renderers.Count; i++)
{
var renderer = _renderers[i];
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
renderer.Master = null;
renderer.Render();
lock (renderer)
{
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
renderer.Master = null;
renderer.Render();
}
else
{
toRemove.Add(renderer);
}
}
else
}
if (toRemove.Count > 0)
{
_updateSemaphore.Wait();
foreach (var renderer in toRemove)
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
_updateSemaphore.Release();
}
}
@@ -681,7 +706,7 @@ internal sealed partial class MidiManager : IMidiManager
{
public int MinimumBatchParallel => 2;
public int BatchSize => 2;
public int BatchSize => 1;
public MidiManager Manager;
@@ -690,7 +715,13 @@ internal sealed partial class MidiManager : IMidiManager
public void Execute(int index)
{
Manager.UpdateRenderer(Renderers[index], OurPosition);
// The indices shouldn't be able to be touched while this job is running, just the renderer itself getting locked.
var renderer = Renderers[index];
lock (renderer)
{
Manager.UpdateRenderer(renderer, OurPosition);
}
}
}

View File

@@ -255,7 +255,7 @@ internal sealed class MidiRenderer : IMidiRenderer
_taskManager = taskManager;
_midiSawmill = midiSawmill;
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true);
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true) ?? DummyBufferedAudioSource.Instance;
Source.SampleRate = SampleRate;
_settings = settings;
_soundFontLoader = soundFontLoader;

View File

@@ -157,7 +157,22 @@ internal abstract class BaseAudioSource : IAudioSource
}
/// <inheritdoc />
public float Pitch { get; set; }
public float Pitch
{
get
{
_checkDisposed();
AL.GetSource(SourceHandle, ALSourcef.Pitch, out var value);
Master._checkAlError();
return value;
}
set
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.Pitch, value);
Master._checkAlError();
}
}
/// <inheritdoc />
public float Volume
@@ -188,12 +203,13 @@ internal abstract class BaseAudioSource : IAudioSource
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
// Default to 0 to avoid spiking audio, just means it will be muted for a frame in this case.
priorOcclusion = _gain == 0 ? 1f : priorGain / _gain;
}
_gain = value;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
Master._checkAlError();
Master.LogALError($"Gain is {_gain:0.00} and priorOcclusion is {priorOcclusion:0.00}. EFX supported: {IsEfxSupported}");
}
}
@@ -211,7 +227,7 @@ internal abstract class BaseAudioSource : IAudioSource
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.MaxDistance, value);
Master._checkAlError();
Master.LogALError($"MaxDistance is {value:0.00}");
}
}
@@ -229,7 +245,7 @@ internal abstract class BaseAudioSource : IAudioSource
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.RolloffFactor, value);
Master._checkAlError();
Master.LogALError($"RolloffFactor is {value:0.00}");
}
}
@@ -247,7 +263,7 @@ internal abstract class BaseAudioSource : IAudioSource
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.ReferenceDistance, value);
Master._checkAlError();
Master.LogALError($"ReferenceDistance is {value:0.00}");
}
}
@@ -293,7 +309,7 @@ internal abstract class BaseAudioSource : IAudioSource
{
_checkDisposed();
AL.Source(SourceHandle, ALSourcef.SecOffset, value);
Master._checkAlError();
Master._checkAlError($"Tried to set invalid playback position of {value:0.00}");
}
}
@@ -327,6 +343,8 @@ internal abstract class BaseAudioSource : IAudioSource
public void SetAuxiliary(IAuxiliaryAudio? audio)
{
_checkDisposed();
if (!IsEfxSupported)
return;
if (audio is AuxiliaryAudio impAudio)
{

View File

@@ -20,8 +20,6 @@ internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSourc
private bool _float = false;
private int FilterHandle;
private float _gain;
public int SampleRate { get; set; } = 44100;
private bool IsEfxSupported => _master.IsEfxSupported;
@@ -37,7 +35,6 @@ internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSourc
BufferMap[bufferHandle] = i;
}
_float = floatAudio;
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
}
/// <inheritdoc />

View File

@@ -107,6 +107,7 @@ namespace Robust.Client
deps.Register<IClyde, ClydeHeadless>();
deps.Register<IClipboardManager, ClydeHeadless>();
deps.Register<IClydeInternal, ClydeHeadless>();
deps.Register<IAudioManager, HeadlessAudioManager>();
deps.Register<IAudioInternal, HeadlessAudioManager>();
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
@@ -116,6 +117,7 @@ namespace Robust.Client
deps.Register<IClyde, Clyde>();
deps.Register<IClipboardManager, Clyde>();
deps.Register<IClydeInternal, Clyde>();
deps.Register<IAudioManager, AudioManager>();
deps.Register<IAudioInternal, AudioManager>();
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();

View File

@@ -198,6 +198,7 @@ namespace Robust.Client.Console.Commands
}
}
#if DEBUG
internal sealed class ShowRayCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
@@ -224,6 +225,7 @@ namespace Robust.Client.Console.Commands
mgr.DebugRayLifetime = TimeSpan.FromSeconds(duration);
}
}
#endif
internal sealed class DisconnectCommand : LocalizedCommands
{

View File

@@ -4,18 +4,17 @@ using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Debugging;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
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 sealed class DebugRayDrawingSystem : SharedDebugRayDrawingSystem
{
#if DEBUG
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IGameTiming _gameTimer = default!;
@@ -28,6 +27,8 @@ namespace Robust.Client.Debugging
public Vector2 RayHit;
public TimeSpan LifeTime;
public bool DidActuallyHit;
public bool Server;
public MapId Map;
}
public bool DebugDrawRays
@@ -73,7 +74,8 @@ namespace Robust.Client.Debugging
DidActuallyHit = ev.Results != null,
RayOrigin = ev.Ray.Position,
RayHit = ev.Results?.HitPos ?? ev.Ray.Direction * ev.MaxLength + ev.Ray.Position,
LifeTime = _gameTimer.RealTime + DebugRayLifetime
LifeTime = _gameTimer.RealTime + DebugRayLifetime,
Map = ev.Map
};
_raysWithLifeTime.Add(newRayWithLifetime);
@@ -93,7 +95,9 @@ namespace Robust.Client.Debugging
DidActuallyHit = msg.DidHit,
RayOrigin = msg.RayOrigin,
RayHit = msg.RayHit,
LifeTime = _gameTimer.RealTime + DebugRayLifetime
LifeTime = _gameTimer.RealTime + DebugRayLifetime,
Server = true,
Map = msg.Map
};
_raysWithLifeTime.Add(newRayWithLifetime);
@@ -114,10 +118,20 @@ namespace Robust.Client.Debugging
var handle = args.WorldHandle;
foreach (var ray in _owner._raysWithLifeTime)
{
if (args.MapId != ray.Map)
continue;
Color color;
if (ray.Server)
color = ray.DidActuallyHit ? Color.Cyan : Color.Orange;
else
color = ray.DidActuallyHit ? Color.Blue : Color.Red;
handle.DrawLine(
ray.RayOrigin,
ray.RayHit,
ray.DidActuallyHit ? Color.Yellow : Color.Magenta);
color
);
}
}
@@ -128,5 +142,6 @@ namespace Robust.Client.Debugging
_owner._raysWithLifeTime.RemoveAll(r => r.LifeTime < _owner._gameTimer.RealTime);
}
}
#endif
}
}

View File

@@ -18,11 +18,5 @@ namespace Robust.Client.GameObjects
public ComponentStateApplyException(string message, Exception inner) : base(message, inner)
{
}
protected ComponentStateApplyException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -59,7 +59,7 @@ namespace Robust.Client.GameObjects
if (!RemoveExpectedEntity(GetNetEntity(uid), out var container))
return;
container.Insert(uid, EntityManager, transform: TransformQuery.GetComponent(uid), meta: MetaQuery.GetComponent(uid));
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
}
private void HandleComponentState(EntityUid uid, ContainerManagerComponent component, ref ComponentHandleState args)
@@ -81,17 +81,17 @@ namespace Robust.Client.GameObjects
foreach (var entity in container.ContainedEntities.ToArray())
{
container.Remove(entity,
EntityManager,
TransformQuery.GetComponent(entity),
MetaQuery.GetComponent(entity),
Remove(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
container,
force: true,
reparent: false);
reparent: false
);
DebugTools.Assert(!container.Contains(entity));
}
container.Shutdown(EntityManager, _netMan);
ShutdownContainer(container);
toDelete.Add(id);
}
@@ -108,7 +108,7 @@ namespace Robust.Client.GameObjects
{
var type = _serializer.FindSerializedType(typeof(BaseContainer), data.ContainerType);
container = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type!, inject:false);
container.Init(id, uid, component);
InitContainer(container, (uid, component), id);
component.Containers.Add(id, container);
}
@@ -132,13 +132,12 @@ namespace Robust.Client.GameObjects
foreach (var entity in toRemove)
{
container.Remove(
entity,
EntityManager,
TransformQuery.GetComponent(entity),
MetaQuery.GetComponent(entity),
Remove(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
container,
force: true,
reparent: false);
reparent: false
);
DebugTools.Assert(!container.Contains(entity));
}
@@ -188,11 +187,12 @@ namespace Robust.Client.GameObjects
continue;
RemoveExpectedEntity(netEnt, out _);
container.Insert(entity, EntityManager,
TransformQuery.GetComponent(entity),
Insert(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity), null),
container,
xform,
MetaQuery.GetComponent(entity),
force: true);
force: true
);
DebugTools.Assert(container.Contains(entity));
}
@@ -222,7 +222,7 @@ namespace Robust.Client.GameObjects
return;
}
container.Insert(message.Entity, EntityManager);
Insert(message.Entity, container);
}
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
@@ -324,32 +324,30 @@ namespace Robust.Client.GameObjects
if (_pointLightQuery.TryGetComponent(entity, out var light))
_lightSys.SetContainerOccluded(entity, lightOccluded, light);
var childEnumerator = xform.ChildEnumerator;
// Try to avoid TryComp if we already know stuff is occluded.
if ((!spriteOccluded || !lightOccluded) && TryComp<ContainerManagerComponent>(entity, out var manager))
{
while (childEnumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
// Thank god it's by value and not by ref.
var childSpriteOccluded = spriteOccluded;
var childLightOccluded = lightOccluded;
// We already know either sprite or light is not occluding so need to check container.
if (manager.TryGetContainer(child.Value, out var container))
if (manager.TryGetContainer(child, out var container))
{
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
childLightOccluded = childLightOccluded || container.OccludesLight;
}
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), childSpriteOccluded, childLightOccluded);
UpdateEntity(child, TransformQuery.GetComponent(child), childSpriteOccluded, childLightOccluded);
}
}
else
{
while (childEnumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
UpdateEntity(child.Value, TransformQuery.GetComponent(child.Value), spriteOccluded, lightOccluded);
UpdateEntity(child, TransformQuery.GetComponent(child), spriteOccluded, lightOccluded);
}
}
}

View File

@@ -8,30 +8,28 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Dynamics;
namespace Robust.Client.GameObjects
namespace Robust.Client.GameObjects;
public sealed class MapSystem : SharedMapSystem
{
public sealed class MapSystem : SharedMapSystem
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
public override void Initialize()
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
base.Initialize();
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _resource, _tileDefinitionManager));
}
public override void Initialize()
{
base.Initialize();
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _mapManager, _resource, _tileDefinitionManager));
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
}

View File

@@ -122,14 +122,13 @@ public sealed partial class SpriteSystem
return GetFallbackState();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs protoReloaded)
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
// Check if any EntityPrototype has been changed.
if (!protoReloaded.ByType.TryGetValue(typeof(EntityPrototype), out var changedSet))
if (!args.TryGetModified<EntityPrototype>(out var modified))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var (prototype, _) in changedSet.Modified)
foreach (var prototype in modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);

View File

@@ -58,7 +58,7 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(SpriteTreeSystem));
_proto.PrototypesReloaded += OnPrototypesReloaded;
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
@@ -75,7 +75,6 @@ namespace Robust.Client.GameObjects
public override void Shutdown()
{
base.Shutdown();
_proto.PrototypesReloaded -= OnPrototypesReloaded;
_cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged);
}

View File

@@ -174,19 +174,21 @@ namespace Robust.Client.GameStates
private void OnComponentAdded(AddedComponentEventArgs args)
{
if (_resettingPredictedEntities)
{
var comp = args.ComponentType;
if (!_resettingPredictedEntities)
return;
if (comp.NetID == null)
return;
var comp = args.ComponentType;
if (comp.NetID == null)
return;
_sawmill.Error($"""
Added component {comp.Name} with net id {comp.NetID} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
}
if (_entityManager.IsClientSide(args.BaseArgs.Owner))
return;
_sawmill.Error($"""
Added component {comp.Name} to entity {_entityManager.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
}
/// <inheritdoc />
@@ -663,14 +665,13 @@ namespace Robust.Client.GameStates
foreach (var netEntity in createdEntities)
{
var (createdEntity, meta) = _entityManager.GetEntityData(netEntity);
var (_, meta) = _entityManager.GetEntityData(netEntity);
var compData = _compDataPool.Get();
_outputData.Add(netEntity, compData);
foreach (var (netId, component) in meta.NetComponents)
{
if (!component.NetSyncEnabled)
continue;
DebugTools.Assert(component.NetSyncEnabled);
var state = _entityManager.GetComponentState(bus, component, null, GameTick.Zero);
DebugTools.Assert(state is not IComponentDeltaState delta || delta.FullState);
@@ -981,16 +982,15 @@ namespace Robust.Client.GameStates
xformSys.DetachParentToNull(ent, xform);
// Then detach all children.
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
xformSys.DetachParentToNull(child.Value, xforms.GetComponent(child.Value), xform);
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
if (deleteClientChildren
&& !deleteClientEntities // don't add duplicates
&& _entities.IsClientSide(child.Value))
&& _entities.IsClientSide(child))
{
_toDelete.Add(child.Value);
_toDelete.Add(child);
}
}
@@ -1038,7 +1038,7 @@ namespace Robust.Client.GameStates
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
xformSys.DetachParentToNull(child.Value, xforms.GetComponent(child.Value), xform);
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
}
// Finally, delete the entity.
@@ -1129,7 +1129,7 @@ namespace Robust.Client.GameStates
(containerMeta.Flags & MetaDataFlags.Detached) == 0 &&
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container, null, true))
{
container.Remove(ent.Value, _entities, xform, meta, false, true);
containerSys.Remove((ent.Value, xform, meta), container, false, true);
}
meta._flags |= MetaDataFlags.Detached;
@@ -1217,7 +1217,9 @@ namespace Robust.Client.GameStates
foreach (var (id, comp) in meta.NetComponents)
{
if (comp.NetSyncEnabled && !curState.NetComponents.Contains(id))
DebugTools.Assert(comp.NetSyncEnabled);
if (!curState.NetComponents.Contains(id))
_toRemove.Add(comp);
}
@@ -1427,7 +1429,7 @@ namespace Robust.Client.GameStates
void _recursiveRemoveState(NetEntity netEntity, TransformComponent xform, EntityQuery<MetaDataComponent> metaQuery, EntityQuery<TransformComponent> xformQuery)
{
_processor._lastStateFullRep.Remove(netEntity);
foreach (var child in xform.ChildEntities)
foreach (var child in xform._children)
{
if (xformQuery.TryGetComponent(child, out var childXform) &&
metaQuery.TryGetComponent(child, out var childMeta))

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
@@ -18,10 +20,15 @@ namespace Robust.Client.Graphics.Clyde
private readonly Dictionary<EntityUid, Dictionary<Vector2i, MapChunkData>> _mapChunkData =
new();
/// <summary>
/// To avoid spamming errors we'll just log it once and move on.
/// </summary>
private HashSet<Type> _erroredGridOverlays = new();
private int _verticesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private void _drawGrids(Viewport viewport, Box2Rotated worldBounds, IEye eye)
private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye)
{
var mapId = eye.Position.MapId;
if (!_mapManager.MapExists(mapId))
@@ -30,27 +37,35 @@ namespace Robust.Client.Graphics.Clyde
mapId = MapId.Nullspace;
}
SetTexture(TextureUnit.Texture0, _tileDefinitionManager.TileTextureAtlas);
SetTexture(TextureUnit.Texture1, _lightingReady ? viewport.LightRenderTarget.Texture : _stockTextureWhite);
var gridProgram = ActivateShaderInstance(_defaultShader.Handle).Item1;
SetupGlobalUniformsImmediate(gridProgram, (ClydeTexture) _tileDefinitionManager.TileTextureAtlas);
gridProgram.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
var grids = new List<Entity<MapGridComponent>>();
_mapManager.FindGridsIntersecting(mapId, worldBounds, ref grids);
var requiresFlush = true;
GLShaderProgram gridProgram = default!;
var gridOverlays = GetOverlaysForSpace(OverlaySpace.WorldSpaceGrids);
foreach (var mapGrid in grids)
{
if (!_mapChunkData.ContainsKey(mapGrid))
if (!_mapChunkData.TryGetValue(mapGrid, out var data))
{
continue;
}
if (requiresFlush)
{
SetTexture(TextureUnit.Texture0, _tileDefinitionManager.TileTextureAtlas);
SetTexture(TextureUnit.Texture1, _lightingReady ? viewport.LightRenderTarget.Texture : _stockTextureWhite);
gridProgram = ActivateShaderInstance(_defaultShader.Handle).Item1;
SetupGlobalUniformsImmediate(gridProgram, (ClydeTexture) _tileDefinitionManager.TileTextureAtlas);
gridProgram.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
}
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
var enumerator = mapGrid.Comp.GetMapChunks(worldBounds);
var data = _mapChunkData[mapGrid];
while (enumerator.MoveNext(out var chunk))
{
@@ -72,6 +87,31 @@ namespace Robust.Client.Graphics.Clyde
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
requiresFlush = false;
foreach (var overlay in gridOverlays)
{
if (overlay is not IGridOverlay iGrid)
{
if (!_erroredGridOverlays.Add(overlay.GetType()))
{
_clydeSawmill.Error($"Tried to render grid overlay {overlay.GetType()} that doesn't implement {nameof(IGridOverlay)}");
}
continue;
}
iGrid.Grid = mapGrid;
iGrid.RequiresFlush = false;
RenderSingleWorldOverlay(overlay, viewport, OverlaySpace.WorldSpaceGrids, worldAABB, worldBounds);
requiresFlush |= iGrid.RequiresFlush;
}
if (requiresFlush)
{
FlushRenderQueue();
}
}
CullEmptyChunks();

View File

@@ -497,7 +497,7 @@ namespace Robust.Client.Graphics.Clyde
using (DebugGroup("Grids"))
using (_prof.Group("Grids"))
{
_drawGrids(viewport, worldBounds, eye);
_drawGrids(viewport, worldAABB, worldBounds, eye);
}
// We will also render worldspace overlays here so we can do them under / above entities as necessary

View File

@@ -38,6 +38,11 @@ namespace Robust.Client.Graphics.Clyde
_clyde.DrawSetModelTransform(matrix);
}
public Matrix3 GetModelTransform()
{
return _clyde.DrawGetModelTransform();
}
public void SetProjView(in Matrix3 proj, in Matrix3 view)
{
_clyde.DrawSetProjViewTransform(proj, view);
@@ -222,7 +227,14 @@ namespace Robust.Client.Graphics.Clyde
var clydeShader = (ClydeShaderInstance?) shader;
_clyde.DrawUseShader(clydeShader?.Handle ?? _clyde._defaultShader.Handle);
_clyde.DrawUseShader(clydeShader ?? _clyde._defaultShader);
}
public ShaderInstance? GetShader()
{
return _clyde._queuedShaderInstance == _clyde._defaultShader
? null
: _clyde._queuedShaderInstance;
}
public void Viewport(Box2i viewport)
@@ -285,11 +297,21 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.SetModelTransform(matrix);
}
public override Matrix3 GetTransform()
{
return _renderHandle.GetModelTransform();
}
public override void UseShader(ShaderInstance? shader)
{
_renderHandle.UseShader(shader);
}
public override ShaderInstance? GetShader()
{
return _renderHandle.GetShader();
}
public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture,
ReadOnlySpan<DrawVertexUV2DColor> vertices)
{
@@ -380,11 +402,21 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.SetModelTransform(matrix);
}
public override Matrix3 GetTransform()
{
return _renderHandle.GetModelTransform();
}
public override void UseShader(ShaderInstance? shader)
{
_renderHandle.UseShader(shader);
}
public override ShaderInstance? GetShader()
{
return _renderHandle.GetShader();
}
public override void DrawCircle(Vector2 position, float radius, Color color, bool filled = true)
{
int divisions = Math.Max(16,(int)(radius * 16));

View File

@@ -78,7 +78,9 @@ namespace Robust.Client.Graphics.Clyde
// private LoadedTexture? _batchLoadedTexture;
// Contains the shader instance that's currently being used by the (queue) stage for new commands.
private ClydeHandle _queuedShader;
private ClydeHandle _queuedShader => _queuedShaderInstance.Handle;
private ClydeShaderInstance _queuedShaderInstance = default!;
// Current projection & view matrices that are being used ot render.
// This gets updated to keep track during (queue) and (misc), but not during (submit).
@@ -314,7 +316,7 @@ namespace Robust.Client.Graphics.Clyde
// Reset renderer state.
_currentMatrixModel = Matrix3.Identity;
_queuedShader = _defaultShader.Handle;
_queuedShaderInstance = _defaultShader;
SetScissorFull(null);
}
@@ -443,9 +445,13 @@ namespace Robust.Client.Graphics.Clyde
_isStencilling = false;
}
if (!instance.ParametersDirty)
if (instance.Parameters.Count == 0)
return (program, instance);
if (shader.LastInstance == instance && !instance.ParametersDirty)
return (program, instance);
shader.LastInstance = instance;
instance.ParametersDirty = false;
int textureUnitVal = 0;
@@ -531,6 +537,11 @@ namespace Robust.Client.Graphics.Clyde
_currentMatrixModel = matrix;
}
private Matrix3 DrawGetModelTransform()
{
return _currentMatrixModel;
}
private void DrawSetProjViewTransform(in Matrix3 proj, in Matrix3 view)
{
BreakBatch();
@@ -700,9 +711,9 @@ namespace Robust.Client.Graphics.Clyde
_currentScissorState = scissorBox;
}
private void DrawUseShader(ClydeHandle handle)
private void DrawUseShader(ClydeShaderInstance instance)
{
_queuedShader = handle;
_queuedShaderInstance = instance;
}
private void DrawClear(Color color, int stencil, ClearBufferMask mask)
@@ -875,7 +886,7 @@ namespace Robust.Client.Graphics.Clyde
SetScissorFull(null);
BindRenderTargetFull(_mainWindow!.RenderTarget);
_batchMetaData = null;
_queuedShader = _defaultShader.Handle;
_queuedShaderInstance = _defaultShader;
GL.Viewport(0, 0, _mainWindow!.FramebufferSize.X, _mainWindow!.FramebufferSize.Y);
}

View File

@@ -48,6 +48,10 @@ namespace Robust.Client.Graphics.Clyde
[ViewVariables]
public string? Name;
// Last instance that used this shader.
// Used to ensure that shader uniforms get updated.
public LoadedShaderInstance? LastInstance;
}
private sealed class LoadedShaderInstance
@@ -158,7 +162,7 @@ namespace Robust.Client.Graphics.Clyde
_defaultShader = (ClydeShaderInstance) InstanceShader(defaultLoadedShader);
_queuedShader = _defaultShader.Handle;
_queuedShaderInstance = _defaultShader;
}
private string ReadEmbeddedShader(string fileName)

View File

@@ -76,6 +76,7 @@ namespace Robust.Client.Graphics.Clyde
private Thread? _gameThread;
private ISawmill _clydeSawmill = default!;
private ISawmill _sawmillOgl = default!;
private IBindingsContext _glBindingsContext = default!;
@@ -91,6 +92,7 @@ namespace Robust.Client.Graphics.Clyde
public bool InitializePreWindowing()
{
_clydeSawmill = _logManager.GetSawmill("clyde");
_sawmillOgl = _logManager.GetSawmill("clyde.ogl");
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);

View File

@@ -20,11 +20,5 @@ namespace Robust.Client.Graphics.Clyde
public ShaderCompilationException(string message, Exception inner) : base(message, inner)
{
}
protected ShaderCompilationException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Robust.Shared;
@@ -65,7 +66,7 @@ namespace Robust.Client.Graphics.Clyde
return MouseButtonMap[button];
}
private static readonly Dictionary<GlfwButton, Button> MouseButtonMap = new()
private static readonly FrozenDictionary<GlfwButton, Button> MouseButtonMap = new Dictionary<GlfwButton, Button>()
{
{GlfwButton.Left, Button.Left},
{GlfwButton.Middle, Button.Middle},
@@ -75,10 +76,10 @@ namespace Robust.Client.Graphics.Clyde
{GlfwButton.Button6, Button.Button6},
{GlfwButton.Button7, Button.Button7},
{GlfwButton.Button8, Button.Button8},
};
}.ToFrozenDictionary();
private static readonly Dictionary<GlfwKey, Key> KeyMap;
private static readonly Dictionary<Key, GlfwKey> KeyMapReverse;
private static readonly FrozenDictionary<GlfwKey, Key> KeyMap;
private static readonly FrozenDictionary<Key, GlfwKey> KeyMapReverse;
internal static Key ConvertGlfwKey(GlfwKey key)
@@ -218,14 +219,16 @@ namespace Robust.Client.Graphics.Clyde
{GlfwKey.F24, Key.F24},
{GlfwKey.Pause, Key.Pause},
{GlfwKey.World1, Key.World1},
};
}.ToFrozenDictionary();
KeyMapReverse = new Dictionary<Key, GlfwKey>();
var keyMapReverse = new Dictionary<Key, GlfwKey>();
foreach (var (key, value) in KeyMap)
{
KeyMapReverse[value] = key;
keyMapReverse[value] = key;
}
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
}
}
}

View File

@@ -125,12 +125,6 @@ namespace Robust.Client.Graphics.Clyde
public GlfwException(string message, Exception inner) : base(message, inner)
{
}
protected GlfwException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Robust.Client.Input;
@@ -16,7 +17,7 @@ internal partial class Clyde
{
// Indices are values of SDL_Scancode
private static readonly Key[] KeyMap;
private static readonly Dictionary<Key, SDL_Scancode> KeyMapReverse;
private static readonly FrozenDictionary<Key, SDL_Scancode> KeyMapReverse;
private static readonly Button[] MouseButtonMap;
// TODO: to avoid having to ask the windowing thread, key names are cached.
@@ -202,15 +203,17 @@ internal partial class Clyde
MapKey(SDL_SCANCODE_F24, Key.F24);
MapKey(SDL_SCANCODE_PAUSE, Key.Pause);
KeyMapReverse = new Dictionary<Key, SDL_Scancode>();
var keyMapReverse = new Dictionary<Key, SDL_Scancode>();
for (var code = 0; code < KeyMap.Length; code++)
{
var key = KeyMap[code];
if (key != Key.Unknown)
KeyMapReverse[key] = (SDL_Scancode) code;
keyMapReverse[key] = (SDL_Scancode) code;
}
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void MapKey(SDL_Scancode code, Key key)
{

View File

@@ -55,8 +55,12 @@ namespace Robust.Client.Graphics
public abstract void SetTransform(in Matrix3 matrix);
public abstract Matrix3 GetTransform();
public abstract void UseShader(ShaderInstance? shader);
public abstract ShaderInstance? GetShader();
// ---- DrawPrimitives: Vector2 API ----
/// <summary>

View File

@@ -116,7 +116,6 @@ namespace Robust.Client.Graphics
{
baseLine.Y += lineHeight;
advanceTotal.Y += lineHeight;
advanceTotal.X = Math.Max(advanceTotal.X, baseLine.X);
baseLine.X = 0f;
continue;
}
@@ -128,6 +127,7 @@ namespace Robust.Client.Graphics
var advance = metrics.Value.Advance;
baseLine += new Vector2(advance, 0);
advanceTotal.X = MathF.Max(baseLine.X, advanceTotal.X);
}
return advanceTotal;

View File

@@ -15,7 +15,8 @@ namespace Robust.Client.Graphics
/// which can be either stretched or tiled to fill up
/// the space the box is being drawn in.
/// </summary>
public sealed class StyleBoxTexture : StyleBox
[Virtual]
public class StyleBoxTexture : StyleBox
{
public StyleBoxTexture()
{

View File

@@ -37,7 +37,7 @@ namespace Robust.Client.Graphics
Vector2 scale,
Angle? worldRot,
Angle eyeRotation = default,
Shared.Maths.Direction? overrideDirection = null,
Direction? overrideDirection = null,
SpriteComponent? sprite = null,
TransformComponent? xform = null,
SharedTransformSystem? xformSystem = null);

View File

@@ -0,0 +1,14 @@
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
namespace Robust.Client.Graphics;
public abstract class GridOverlay : Overlay, IGridOverlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpaceGrids;
public Entity<MapGridComponent> Grid { get; set; }
public bool RequiresFlush { get; set; }
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
namespace Robust.Client.Graphics;
/// <summary>
/// Marks this overlay as implementing per-grid rendering.
/// </summary>
public interface IGridOverlay
{
Entity<MapGridComponent> Grid { get; set; }
/// <summary>
/// Should we flush the render or can we keep going.
/// </summary>
public bool RequiresFlush { get; set; }
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
@@ -227,7 +228,7 @@ namespace Robust.Client.Graphics
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]
private static readonly Dictionary<ShaderDataType, string> _nativeTypes = new()
private static readonly FrozenDictionary<ShaderDataType, string> _nativeTypes = new Dictionary<ShaderDataType, string>()
{
{ShaderDataType.Void, "void"},
{ShaderDataType.Bool, "bool"},
@@ -252,7 +253,7 @@ namespace Robust.Client.Graphics
{ShaderDataType.Sampler2D, "sampler2D"},
{ShaderDataType.ISampler2D, "isampler2D"},
{ShaderDataType.USampler2D, "usampler2D"},
};
}.ToFrozenDictionary();
}
internal enum ShaderLightMode : byte

View File

@@ -1,3 +1,4 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -637,7 +638,7 @@ namespace Robust.Client.Graphics
Colon,
}
private static readonly Dictionary<Symbols, string> _symbolStringMap = new()
private static readonly FrozenDictionary<Symbols, string> _symbolStringMap = new Dictionary<Symbols, string>()
{
{Symbols.Semicolon, ";\n"},
{Symbols.Comma, ","},
@@ -679,6 +680,6 @@ namespace Robust.Client.Graphics
{Symbols.GreaterOrEq, ">="},
{Symbols.QuestionMark, "?"},
{Symbols.Colon, ":"},
};
}.ToFrozenDictionary();
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -536,17 +537,17 @@ namespace Robust.Client.Graphics
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]
private static readonly Dictionary<string, ShaderPrecisionQualifier> _shaderTypePrecisionMap =
new()
private static readonly FrozenDictionary<string, ShaderPrecisionQualifier> _shaderTypePrecisionMap =
new Dictionary<string, ShaderPrecisionQualifier>()
{
{"lowp", ShaderPrecisionQualifier.Low},
{"mediump", ShaderPrecisionQualifier.Medium},
{"highp", ShaderPrecisionQualifier.High}
};
}.ToFrozenDictionary();
[SuppressMessage("ReSharper", "StringLiteralTypo")]
private static readonly Dictionary<string, ShaderDataType> _shaderTypeMap =
new()
private static readonly FrozenDictionary<string, ShaderDataType> _shaderTypeMap =
new Dictionary<string, ShaderDataType>()
{
{"void", ShaderDataType.Void},
{"bool", ShaderDataType.Bool},
@@ -571,7 +572,7 @@ namespace Robust.Client.Graphics
{"sampler2D", ShaderDataType.Sampler2D},
{"isampler2D", ShaderDataType.ISampler2D},
{"usampler2D", ShaderDataType.USampler2D},
};
}.ToFrozenDictionary();
[PublicAPI]
internal readonly struct TextFileRange

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using Robust.Shared.Localization;
@@ -28,7 +29,7 @@ namespace Robust.Client.Input
return _mouseKeyMap[button];
}
private static readonly Dictionary<Button, Keyboard.Key> _mouseKeyMap = new()
private static readonly FrozenDictionary<Button, Keyboard.Key> _mouseKeyMap = new Dictionary<Button, Keyboard.Key>()
{
{Button.Left, Keyboard.Key.MouseLeft},
{Button.Middle, Keyboard.Key.MouseMiddle},
@@ -40,7 +41,7 @@ namespace Robust.Client.Input
{Button.Button8, Keyboard.Key.MouseButton8},
{Button.Button9, Keyboard.Key.MouseButton9},
{Button.LastButton, Keyboard.Key.Unknown},
};
}.ToFrozenDictionary();
}

View File

@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Client.Map;
@@ -14,21 +11,15 @@ namespace Robust.Client.Map;
/// <summary>
/// Draws border sprites for tiles that support them.
/// </summary>
public sealed class TileEdgeOverlay : Overlay
public sealed class TileEdgeOverlay : GridOverlay
{
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly IResourceCache _resource;
private readonly ITileDefinitionManager _tileDefManager;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private List<Entity<MapGridComponent>> _grids = new();
public TileEdgeOverlay(IEntityManager entManager, IMapManager mapManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
public TileEdgeOverlay(IEntityManager entManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
{
_entManager = entManager;
_mapManager = mapManager;
_resource = resource;
_tileDefManager = tileDefManager;
ZIndex = -1;
@@ -39,94 +30,90 @@ public sealed class TileEdgeOverlay : Overlay
if (args.MapId == MapId.Nullspace)
return;
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
var mapSystem = _entManager.System<SharedMapSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
foreach (var grid in _grids)
var tileSize = Grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(Grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var localAABB = invMatrix.TransformBox(args.WorldBounds);
var enumerator = mapSystem.GetLocalTilesEnumerator(Grid.Owner, Grid, localAABB, false);
while (enumerator.MoveNext(out var tileRef))
{
var tileSize = grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var localAABB = invMatrix.TransformBox(args.WorldBounds);
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
var enumerator = mapSystem.GetLocalTilesEnumerator(grid.Owner, grid.Comp, localAABB, false);
if (tileDef.EdgeSprites.Count == 0)
continue;
while (enumerator.MoveNext(out var tileRef))
// Get what tiles border us to determine what sprites we need to draw.
for (var x = -1; x <= 1; x++)
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
if (tileDef.EdgeSprites.Count == 0)
continue;
// Get what tiles border us to determine what sprites we need to draw.
for (var x = -1; x <= 1; x++)
for (var y = -1; y <= 1; y++)
{
for (var y = -1; y <= 1; y++)
if (x == 0 && y == 0)
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = mapSystem.GetTileRef(Grid.Owner, Grid, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tileRef.Tile.TypeId == neighborTile.Tile.TypeId)
continue;
// Don't draw if the the neighbor tile edges should draw over us (or if we have the same priority)
if (neighborDef.EdgeSprites.Count != 0 && neighborDef.EdgeSpritePriority >= tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(x, y).AsDirection();
// No edge tile
if (!tileDef.EdgeSprites.TryGetValue(direction, out var edgePath))
continue;
var texture = _resource.GetResource<TextureResource>(edgePath);
var box = Box2.FromDimensions(neighborIndices, tileDimensions);
var angle = Angle.Zero;
// If we ever need one for both cardinals and corners then update this.
switch (direction)
{
if (x == 0 && y == 0)
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = mapSystem.GetTileRef(grid.Owner, grid.Comp, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tileRef.Tile.TypeId == neighborTile.Tile.TypeId)
continue;
// Don't draw if the the neighbor tile edges should draw over us (or if we have the same priority)
if (neighborDef.EdgeSprites.Count != 0 && neighborDef.EdgeSpritePriority >= tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(x, y).AsDirection();
// No edge tile
if (!tileDef.EdgeSprites.TryGetValue(direction, out var edgePath))
continue;
var texture = _resource.GetResource<TextureResource>(edgePath);
var box = Box2.FromDimensions(neighborIndices, tileDimensions);
var angle = Angle.Zero;
// If we ever need one for both cardinals and corners then update this.
switch (direction)
{
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle == Angle.Zero)
args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle == Angle.Zero)
args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
RequiresFlush = true;
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.Shared.ContentPack;
@@ -45,13 +46,13 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
{
if (useFallback && resource.Fallback != null)
{
Logger.Error(
Sawmill.Error(
$"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}");
return GetResource<T>(resource.Fallback.Value, false);
}
else
{
Logger.Error(
Sawmill.Error(
$"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}");
throw;
}
@@ -81,11 +82,17 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
cache[path] = resource;
return true;
}
catch
catch (FileNotFoundException)
{
resource = null;
return false;
}
catch (Exception e)
{
Sawmill.Error($"Exception while loading resource {typeof(T)} at '{path}'\n{e}");
resource = null;
return false;
}
}
public void ReloadResource<T>(string path) where T : BaseResource, new()
@@ -109,7 +116,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
}
catch (Exception e)
{
Logger.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
Sawmill.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
throw;
}
}

View File

@@ -25,6 +25,7 @@
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-rc2" PrivateAssets="compile" />
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" Version="1.2.0" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.Sodium" Version="0.2.1" PrivateAssets="compile" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.0.1" PrivateAssets="compile" />

View File

@@ -213,7 +213,12 @@ public sealed class EntitySpawningUIController : UIController
_shownEntities.Add(prototype);
}
_shownEntities.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
_shownEntities.Sort((a, b) => {
var namesComparation = string.Compare(a.Name, b.Name, StringComparison.Ordinal);
if (namesComparation == 0)
return string.Compare(a.EditorSuffix, b.EditorSuffix, StringComparison.Ordinal);
return namesComparation;
});
_window.PrototypeList.TotalItemCount = _shownEntities.Count;
_window.PrototypeScrollContainer.SetScrollValue(new Vector2(0, 0));

View File

@@ -109,6 +109,7 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// Whether key functions other than <see cref="EngineKeyFunctions.UIClick"/> trigger the button.
/// </summary>
[ViewVariables]
public bool EnableAllKeybinds
{
get => _enableAllKeybinds;

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface.Controls;
@@ -15,6 +17,15 @@ public sealed class ColorSelectorSliders : Control
set
{
_currentColor = value;
switch (SelectorType)
{
case ColorSelectorType.Rgb:
_colorData = new Vector4(_currentColor.R, _currentColor.G, _currentColor.B, _currentColor.A);
break;
case ColorSelectorType.Hsv:
_colorData = Color.ToHsv(value);
break;
}
Update();
}
}
@@ -24,6 +35,15 @@ public sealed class ColorSelectorSliders : Control
get => _currentType;
set
{
switch ((_currentType, value))
{
case (ColorSelectorType.Rgb, ColorSelectorType.Hsv):
_colorData = Color.ToHsv(Color);
break;
case (ColorSelectorType.Hsv, ColorSelectorType.Rgb):
_colorData = new Vector4(_currentColor.R, _currentColor.G, _currentColor.B, _currentColor.A);
break;
}
_currentType = value;
UpdateType();
Update();
@@ -45,6 +65,7 @@ public sealed class ColorSelectorSliders : Control
private bool _updating = false;
private Color _currentColor = Color.White;
private Vector4 _colorData;
private ColorSelectorType _currentType = ColorSelectorType.Rgb;
private bool _isAlphaVisible = false;
@@ -68,12 +89,19 @@ public sealed class ColorSelectorSliders : Control
private OptionButton _typeSelector;
private List<ColorSelectorType> _types = new();
private static ShaderInstance _shader = default!;
private ColorSelectorStyleBox _topStyle;
private ColorSelectorStyleBox _middleStyle;
private ColorSelectorStyleBox _bottomStyle;
public ColorSelectorSliders()
{
_topColorSlider = new ColorableSlider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
BackgroundStyleBoxOverride = _topStyle = new(),
MaxValue = 1.0f
};
@@ -81,6 +109,7 @@ public sealed class ColorSelectorSliders : Control
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
BackgroundStyleBoxOverride = _middleStyle = new(),
MaxValue = 1.0f
};
@@ -88,6 +117,7 @@ public sealed class ColorSelectorSliders : Control
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
BackgroundStyleBoxOverride = _bottomStyle = new(),
MaxValue = 1.0f
};
@@ -212,7 +242,7 @@ public sealed class ColorSelectorSliders : Control
rootBox.AddChild(bodyBox);
UpdateType();
Update();
Color = _currentColor;
}
private void UpdateType()
@@ -222,6 +252,11 @@ public sealed class ColorSelectorSliders : Control
_topSliderLabel.Text = labels.topLabel;
_middleSliderLabel.Text = labels.middleLabel;
_bottomSliderLabel.Text = labels.bottomLabel;
bool hsv = SelectorType == ColorSelectorType.Hsv;
_topStyle.ConfigureSlider( hsv ? ColorSelectorStyleBox.ColorSliderPreset.Hue : ColorSelectorStyleBox.ColorSliderPreset.Red);
_middleStyle.ConfigureSlider( hsv ? ColorSelectorStyleBox.ColorSliderPreset.Saturation : ColorSelectorStyleBox.ColorSliderPreset.Green);
_bottomStyle.ConfigureSlider( hsv ? ColorSelectorStyleBox.ColorSliderPreset.Value : ColorSelectorStyleBox.ColorSliderPreset.Blue);
}
private void Update()
@@ -232,43 +267,41 @@ public sealed class ColorSelectorSliders : Control
return;
_updating = true;
_topColorSlider.SetColor(_currentColor);
_middleColorSlider.SetColor(_currentColor);
_bottomColorSlider.SetColor(_currentColor);
_topStyle.SetBaseColor(_colorData);
_middleStyle.SetBaseColor(_colorData);
_bottomStyle.SetBaseColor(_colorData);
switch (SelectorType)
{
case ColorSelectorType.Rgb:
_topColorSlider.Value = Color.R;
_middleColorSlider.Value = Color.G;
_bottomColorSlider.Value = Color.B;
_topColorSlider.Value = _colorData.X;
_middleColorSlider.Value = _colorData.Y;
_bottomColorSlider.Value = _colorData.Z;
_topInputBox.Value = (int)(Color.R * 255.0f);
_middleInputBox.Value = (int)(Color.G * 255.0f);
_bottomInputBox.Value = (int)(Color.B * 255.0f);
_topInputBox.Value = (int)(_colorData.X * 255.0f);
_middleInputBox.Value = (int)(_colorData.Y * 255.0f);
_bottomInputBox.Value = (int)(_colorData.Z * 255.0f);
break;
case ColorSelectorType.Hsv:
Vector4 color = Color.ToHsv(Color);
// dumb workaround because the formula for
// HSV calculation results in a negative
// number in any value past 300 degrees
if (color.X > 0)
if (_colorData.X > 0)
{
_topColorSlider.Value = color.X;
_topInputBox.Value = (int)(color.X * 360.0f);
_topColorSlider.Value = _colorData.X;
_topInputBox.Value = (int)(_colorData.X * 360.0f);
}
else
{
_topInputBox.Value = (int)(_topColorSlider.Value * 360.0f);
}
_middleColorSlider.Value = color.Y;
_bottomColorSlider.Value = color.Z;
_middleColorSlider.Value = _colorData.Y;
_bottomColorSlider.Value = _colorData.Z;
_middleInputBox.Value = (int)(color.Y * 100.0f);
_bottomInputBox.Value = (int)(color.Z * 100.0f);
_middleInputBox.Value = (int)(_colorData.Y * 100.0f);
_bottomInputBox.Value = (int)(_colorData.Z * 100.0f);
break;
@@ -361,25 +394,16 @@ public sealed class ColorSelectorSliders : Control
return;
}
switch (SelectorType)
_colorData = new Vector4(_topColorSlider.Value, _middleColorSlider.Value, _bottomColorSlider.Value, _alphaSlider.Value);
_currentColor = SelectorType switch
{
case ColorSelectorType.Rgb:
Color rgbColor = new Color(_topColorSlider.Value, _middleColorSlider.Value, _bottomColorSlider.Value, _alphaSlider.Value);
ColorSelectorType.Hsv => Color.FromHsv(_colorData),
_ => new Color(_colorData.X, _colorData.Y, _colorData.Z, _colorData.W)
};
_currentColor = rgbColor;
Update();
OnColorChanged!(rgbColor);
break;
case ColorSelectorType.Hsv:
Color hsvColor = Color.FromHsv(new Vector4(_topColorSlider.Value, _middleColorSlider.Value, _bottomColorSlider.Value, _alphaSlider.Value));
_currentColor = hsvColor;
Update();
OnColorChanged!(hsvColor);
break;
}
Update();
OnColorChanged?.Invoke(_currentColor);
}
private enum ColorSliderOrder

View File

@@ -0,0 +1,130 @@
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Robust.Client.UserInterface.Controls;
/// <summary>
/// Style box for colouring sliders and 2-d colour selectors. E.g., this could be used to draw the typical HSV colour
/// selection rainbow.
/// </summary>
public sealed class ColorSelectorStyleBox : StyleBoxTexture
{
public const string TexturePath = "/Textures/Interface/Nano/slider_fill.svg.96dpi.png";
public static ProtoId<ShaderPrototype> Prototype = "ColorPicker";
private ShaderInstance _shader;
/// <summary>
/// Base background colour.
/// </summary>
public Vector4 BaseColor;
/// <summary>
/// Colour to add to the background colour along the X-axis.
/// I.e., from left to right the background colour will vary from (BaseColour) to (BaseColour + XAxis)
/// </summary>
public Vector4 XAxis;
/// <summary>
/// Colour to add to the background colour along the y-axis.
/// I.e., from left to right the background colour will vary from (BaseColour) to (BaseColour + XAxis)
/// </summary>
public Vector4 YAxis;
/// <summary>
/// If true, then <see cref="BaseColor"/>, <see cref="XAxis"/>, and <see cref="YAxis"/> will be interpreted as HSVa
/// colours.
/// </summary>
public bool Hsv;
public ColorSelectorStyleBox(ColorSliderPreset preset = ColorSliderPreset.Red)
{
Texture = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(TexturePath);
_shader = IoCManager.Resolve<IPrototypeManager>().Index(Prototype).InstanceUnique();
SetPatchMargin(Margin.All, 12);
ConfigureSlider(preset);
}
protected override void DoDraw(DrawingHandleScreen handle, UIBox2 box, float uiScale)
{
var old = handle.GetShader();
handle.UseShader(_shader);
var globalPixelPos = handle.GetTransform().Transform(default);
_shader.SetParameter("size", box.Size);
_shader.SetParameter("offset", globalPixelPos);
_shader.SetParameter("xAxis", XAxis);
_shader.SetParameter("yAxis", YAxis);
_shader.SetParameter("baseColor", BaseColor);
_shader.SetParameter("hsv", Hsv);
base.DoDraw(handle, box, uiScale);
handle.UseShader(old);
}
public void ConfigureSlider(ColorSliderPreset preset)
{
Hsv = preset > ColorSliderPreset.Blue;
if (preset == ColorSliderPreset.HueValue)
{
XAxis = new(1, 0, 0, 0); // Hue;
YAxis = new(0, 0, 1, 0); // value;
return;
}
YAxis = default;
XAxis = preset switch
{
ColorSliderPreset.Red or ColorSliderPreset.Hue => new(1, 0, 0, 0),
ColorSliderPreset.Green or ColorSliderPreset.Saturation => new(0, 1, 0, 0),
_ => new(0, 0, 1, 0),
};
}
/// <summary>
/// Helper method that sets the base color by taking in some color and removing the components that are controlled by the x and y axes.
/// </summary>
public void SetBaseColor(Color color)
{
var colorData = Hsv
? Color.ToHsv(color)
: new Vector4(color.R, color.G, color.B, color.A);
SetBaseColor(colorData);
}
/// <summary>
/// Helper method that sets the base color by taking in some color and removing the components that are controlled by the x and y axes.
/// </summary>
public void SetBaseColor(Vector4 colorData)
{
BaseColor = colorData - colorData * XAxis - colorData * YAxis;
}
public enum ColorSliderPreset : byte
{
// Horizontal red slider
Red = 1,
// Horizontal green slider
Green = 2,
// Horizontal blue slider
Blue = 3,
// Horizontal hue slider
Hue = 4,
// Horizontal situation slider
Saturation = 5,
// Horizontal saturation slider
Value = 6,
// 2-D hue-value box
HueValue = 7
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Maths;
@@ -14,6 +15,9 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertyFill = "fill";
public const string StylePropertyGrabber = "grabber";
public event Action<Slider>? OnGrabbed;
public event Action<Slider>? OnReleased;
protected readonly PanelContainer _foregroundPanel;
protected readonly PanelContainer _backgroundPanel;
protected readonly PanelContainer _fillPanel;
@@ -135,6 +139,7 @@ namespace Robust.Client.UserInterface.Controls
HandlePositionChange(args.RelativePosition);
_grabbed = true;
OnGrabbed?.Invoke(this);
}
protected internal override void KeyBindUp(GUIBoundKeyEventArgs args)
@@ -144,6 +149,7 @@ namespace Robust.Client.UserInterface.Controls
if (args.Function != EngineKeyFunctions.UIClick) return;
_grabbed = false;
OnReleased?.Invoke(this);
}
protected internal override void MouseMove(GUIMouseMoveEventArgs args)

View File

@@ -13,17 +13,15 @@ namespace Robust.Client.UserInterface.Controls
[Virtual]
public class SpriteView : Control
{
private SpriteSystem? _spriteSystem;
private SpriteSystem? _sprite;
private SharedTransformSystem? _transform;
IEntityManager _entMan;
[ViewVariables]
public SpriteComponent? Sprite { get; private set; }
public SpriteComponent? Sprite => Entity?.Comp1;
[ViewVariables]
public EntityUid? Entity { get; private set; }
public Entity<SpriteComponent>? Ent => Entity == null || Sprite == null ? null : (Entity.Value, Sprite);
public Entity<SpriteComponent, TransformComponent>? Entity { get; private set; }
/// <summary>
/// This field configures automatic scaling of the sprite. This automatic scaling is done before
@@ -118,23 +116,30 @@ namespace Robust.Client.UserInterface.Controls
public SpriteView()
{
_entMan = IoCManager.Resolve<IEntityManager>();
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
{
Sprite = sprite;
}
IoCManager.Resolve(ref _entMan);
RectClipContent = true;
}
public SpriteView(EntityUid uid, IEntityManager entMan)
{
_entMan = entMan;
RectClipContent = true;
SetEntity(uid);
}
public void SetEntity(EntityUid? uid)
{
Entity = uid;
if (Entity?.Owner == uid)
return;
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
if (!_entMan.TryGetComponent(uid, out SpriteComponent? sprite)
|| !_entMan.TryGetComponent(uid, out TransformComponent? xform))
{
Sprite = sprite;
Entity = null;
return;
}
Entity = new(uid.Value, sprite, xform);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
@@ -146,13 +151,13 @@ namespace Robust.Client.UserInterface.Controls
private void UpdateSize()
{
if (Entity == null || Sprite == null)
if (Entity is not { } ent)
{
_spriteSize = default;
return;
}
var spriteBox = Sprite.CalculateRotatedBoundingBox(default, _worldRotation ?? Angle.Zero, _eyeRotation)
var spriteBox = ent.Comp1.CalculateRotatedBoundingBox(default, _worldRotation ?? Angle.Zero, _eyeRotation)
.CalcBoundingBox();
if (!SpriteOffset)
@@ -194,18 +199,22 @@ namespace Robust.Client.UserInterface.Controls
internal override void DrawInternal(IRenderHandle renderHandle)
{
if (Entity is not {} uid || Sprite == null)
if (Entity == null)
return;
if (Sprite.Deleted)
var (uid, sprite, xform) = Entity.Value;
if (sprite.Deleted)
{
SetEntity(null);
return;
}
_sprite ??= _entMan.System<SpriteSystem>();
_transform ??= _entMan.System<TransformSystem>();
// Ensure the sprite is animated despite possible not being visible in any viewport.
_spriteSystem ??= _entMan.System<SpriteSystem>();
_spriteSystem.ForceUpdate(uid);
_sprite.ForceUpdate(uid);
var stretchVec = Stretch switch
{
@@ -217,11 +226,18 @@ namespace Robust.Client.UserInterface.Controls
var offset = SpriteOffset
? Vector2.Zero
: - (-_eyeRotation).RotateVec(Sprite.Offset) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
: - (-_eyeRotation).RotateVec(sprite.Offset) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
var position = PixelSize / 2 + offset * stretch * UIScale;
var scale = Scale * UIScale * stretch;
renderHandle.DrawEntity(uid, position, scale, _worldRotation, _eyeRotation, OverrideDirection, Sprite);
// control modulation is applied automatically to the screen handle, but here we need to use the world handle
var world = renderHandle.DrawingHandleWorld;
var oldModulate = world.Modulate;
world.Modulate *= Modulate * ActualModulateSelf;
renderHandle.DrawEntity(uid, position, scale, _worldRotation, _eyeRotation, OverrideDirection, sprite, xform, _transform);
world.Modulate = oldModulate;
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Numerics;
@@ -228,7 +229,7 @@ public sealed class TextEdit : Control
private bool IsPlaceholderVisible => Rope.IsNullOrEmpty(_textRope) && _placeholder != null;
// Table used by cursor movement system, see below.
private static readonly Dictionary<BoundKeyFunction, MoveType> MoveTypeMap = new()
private static readonly FrozenDictionary<BoundKeyFunction, MoveType> MoveTypeMap = new Dictionary<BoundKeyFunction, MoveType>()
{
// @formatter:off
{ EngineKeyFunctions.TextCursorLeft, MoveType.Left },
@@ -249,7 +250,7 @@ public sealed class TextEdit : Control
{ EngineKeyFunctions.TextCursorSelectBegin, MoveType.BeginOfLine | MoveType.SelectFlag },
{ EngineKeyFunctions.TextCursorSelectEnd, MoveType.EndOfLine | MoveType.SelectFlag },
// @formatter:on
};
}.ToFrozenDictionary();
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -35,7 +36,7 @@ public sealed class UITheme : IPrototype
private ResPath _path;
[DataField("colors", readOnly: true)] // This is a prototype, why is this readonly??
public Dictionary<string, Color>? Colors { get; }
public FrozenDictionary<string, Color>? Colors { get; }
public ResPath Path => _path == default ? new ResPath(DefaultPath+"/"+ID) : _path;
private void ValidateFilePath(IResourceManager manager)

View File

@@ -81,8 +81,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
if (!Exists(uid))
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}");
return null;
}
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
@@ -94,8 +97,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
{
if (!Exists(uid))
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}");
return null;
}
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
@@ -106,6 +112,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
if (!coordinates.IsValid(EntityManager))
return null;
@@ -120,6 +132,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
if (!coordinates.IsValid(EntityManager))
return null;

View File

@@ -28,10 +28,10 @@ namespace Robust.Server.Console.Commands
foreach (var p in players)
{
sb.AppendLine(string.Format("{4,20} {1,12} {2,14:hh\\:mm\\:ss} {3,9} {0,20}",
p.ConnectedClient.RemoteEndPoint,
p.Channel.RemoteEndPoint,
p.Status.ToString(),
DateTime.UtcNow - p.ConnectedTime,
p.ConnectedClient.Ping + "ms",
p.Channel.Ping + "ms",
p.Name));
}
@@ -71,7 +71,7 @@ namespace Robust.Server.Console.Commands
else
reason = "Kicked by console";
_netManager.DisconnectChannel(target.ConnectedClient, reason);
_netManager.DisconnectChannel(target.Channel, reason);
}
}

View File

@@ -211,7 +211,7 @@ namespace Robust.Server.Console
var replyMsg = new MsgConCmdAck();
replyMsg.Error = error;
replyMsg.Text = text;
NetManager.ServerSendMessage(replyMsg, session.ConnectedClient);
NetManager.ServerSendMessage(replyMsg, session.Channel);
}
else
_systemConsole.Print(text + "\n");

View File

@@ -1,8 +1,5 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Debugging;
namespace Robust.Server.Debugging;
@@ -10,11 +7,11 @@ namespace Robust.Server.Debugging;
[UsedImplicitly]
internal sealed class DebugRayDrawingSystem : SharedDebugRayDrawingSystem
{
#if DEBUG
protected override void ReceiveLocalRayAtMainThread(DebugRayData data)
{
// This code won't be called on release - eliminate it anyway for good measure.
#if DEBUG
var msg = new MsgRay {RayOrigin = data.Ray.Position};
var msg = new MsgRay {RayOrigin = data.Ray.Position, Map = data.Map};
if (data.Results != null)
{
msg.DidHit = true;
@@ -25,8 +22,7 @@ internal sealed class DebugRayDrawingSystem : SharedDebugRayDrawingSystem
msg.RayHit = data.Ray.Position + data.Ray.Direction * data.MaxLength;
}
EntityManager.EventBus.RaiseEvent(EventSource.Network, msg);
#endif
RaiseNetworkEvent(msg);
}
#endif
}

View File

@@ -924,7 +924,7 @@ public sealed class MapLoaderSystem : EntitySystem
#region Saving
private MappingDataNode GetSaveData(EntityUid uid)
public MappingDataNode GetSaveData(EntityUid uid)
{
var ev = new BeforeSaveEvent(uid, Transform(uid).MapUid);
RaiseLocalEvent(ev);
@@ -1075,11 +1075,10 @@ public sealed class MapLoaderSystem : EntitySystem
withoutUid.Add(uid);
}
var enumerator = transformQuery.GetComponent(uid).ChildEnumerator;
while (enumerator.MoveNext(out var child))
var xform = transformQuery.GetComponent(uid);
foreach (var child in xform._children)
{
RecursivePopulate(child.Value, entities, uidEntityMap, withoutUid, metaQuery, transformQuery, saveCompQuery);
RecursivePopulate(child, entities, uidEntityMap, withoutUid, metaQuery, transformQuery, saveCompQuery);
}
}

View File

@@ -71,7 +71,7 @@ namespace Robust.Server.GameObjects
foreach (var (player, state) in ui.PlayerStateOverrides)
{
RaiseNetworkEvent(state, player.ConnectedClient);
RaiseNetworkEvent(state, player.Channel);
}
if (ui.LastStateMsg == null)
@@ -80,7 +80,7 @@ namespace Robust.Server.GameObjects
foreach (var session in ui.SubscribedSessions)
{
if (!ui.PlayerStateOverrides.ContainsKey(session))
RaiseNetworkEvent(ui.LastStateMsg, session.ConnectedClient);
RaiseNetworkEvent(ui.LastStateMsg, session.Channel);
}
}
}
@@ -324,7 +324,7 @@ namespace Robust.Server.GameObjects
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey);
foreach (var session in bui.SubscribedSessions)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
RaiseNetworkEvent(msg, session.Channel);
}
}
@@ -347,7 +347,7 @@ namespace Robust.Server.GameObjects
if (!bui.SubscribedSessions.Contains(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey), session.ConnectedClient);
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey), session.Channel);
return true;
}

View File

@@ -105,15 +105,15 @@ namespace Robust.Server.GameObjects
meta.VisibilityMask = mask;
_pvs.MarkDirty(uid, xform);
foreach (var child in xform.ChildEntities)
foreach (var child in xform._children)
{
if (!_metaQuery.TryGetComponent(child, out var childMeta))
continue;
var childMask = mask;
if (_visiblityQuery.TryGetComponent(child, out VisibilityComponent? hildVis))
childMask |= hildVis.Layer;
if (_visiblityQuery.TryGetComponent(child, out var childVis))
childMask |= childVis.Layer;
RecursivelyApplyVisibility(child, childMask, childMeta);
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates;
/// <summary>
/// Class for storing session specific PVS data.
/// </summary>
internal sealed class SessionPvsData
{
/// <summary>
/// All <see cref="EntityUid"/>s that this session saw during the last <see cref="PvsSystem.DirtyBufferSize"/> ticks.
/// </summary>
public readonly OverflowDictionary<GameTick, List<NetEntity>> SentEntities = new(PvsSystem.DirtyBufferSize);
public readonly Dictionary<NetEntity, EntityData> EntityData = new();
/// <summary>
/// <see cref="SentEntities"/> overflow in case a player's last ack is more than
/// <see cref="PvsSystem.DirtyBufferSize"/> ticks behind the current tick.
/// </summary>
public (GameTick Tick, List<NetEntity> SentEnts)? Overflow;
/// <summary>
/// If true, the client has explicitly requested a full state. Unlike the first state, we will send them all data,
/// not just data that cannot be implicitly inferred from entity prototypes.
/// </summary>
public bool RequestedFull = false;
public GameTick LastReceivedAck;
public readonly ICommonSession Session;
public SessionPvsData(ICommonSession session)
{
Session = session;
}
}
/// <summary>
/// Struct for storing session-specific information about when an entity was last sent to a player.
/// </summary>
internal struct EntityData
{
public readonly Entity<MetaDataComponent> Entity;
/// <summary>
/// Tick at which this entity was last sent to a player.
/// </summary>
public GameTick LastSent;
/// <summary>
/// Tick at which an entity last left a player's PVS view.
/// </summary>
public GameTick LastLeftView;
/// <summary>
/// Stores the last tick at which a given entity was acked by a player. Used to avoid re-sending the whole entity
/// state when an item re-enters PVS. This is only the same as the player's last acked tick if the entity was
/// present in that state.
/// </summary>
public GameTick EntityLastAcked;
/// <summary>
/// Entity visibility state when it was last sent to this player.
/// </summary>
public PvsEntityVisibility Visibility;
public EntityData(Entity<MetaDataComponent> entity)
{
Entity = entity;
}
public override string ToString()
{
var rep = new EntityStringRepresentation(Entity);
return $"PVS Entity: {rep} - {LastSent}/{LastLeftView}/{EntityLastAcked}";
}
}

View File

@@ -2,6 +2,7 @@ namespace Robust.Server.GameStates;
public enum PvsEntityVisibility : byte
{
Invalid = 0,
Entered,
StayedUnchanged,
StayedChanged

View File

@@ -1,4 +1,7 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Threading;
@@ -16,6 +19,7 @@ internal sealed partial class PvsSystem
/// </summary>
private void OnClientAck(ICommonSession session, GameTick ackedTick)
{
DebugTools.Assert(ackedTick < _gameTiming.CurTick);
if (!PlayerData.TryGetValue(session, out var sessionData))
return;
@@ -29,10 +33,12 @@ internal sealed partial class PvsSystem
/// <summary>
/// Processes queued client acks in parallel
/// </summary>
internal void ProcessQueuedAcks()
internal WaitHandle ProcessQueuedAcks()
{
if (PendingAcks.Count == 0)
return;
{
return ParallelManager.DummyResetEvent.WaitHandle;
}
_toAck.Clear();
@@ -41,8 +47,8 @@ internal sealed partial class PvsSystem
_toAck.Add(session);
}
_parallelManager.ProcessNow(_ackJob, _toAck.Count);
PendingAcks.Clear();
return _parallelManager.Process(_ackJob, _toAck.Count);
}
private record struct PvsAckJob : IParallelRobustJob
@@ -67,37 +73,43 @@ internal sealed partial class PvsSystem
return;
var ackedTick = sessionData.LastReceivedAck;
Dictionary<NetEntity, PvsEntityVisibility>? ackedData;
List<NetEntity>? ackedEnts;
if (sessionData.Overflow != null && sessionData.Overflow.Value.Tick <= ackedTick)
{
var (overflowTick, overflowEnts) = sessionData.Overflow.Value;
sessionData.Overflow = null;
ackedData = overflowEnts;
ackedEnts = overflowEnts;
// Even though the acked tick might be newer, we have no guarantee that the client received the cached tick,
// so discard it unless they happen to be equal.
if (overflowTick != ackedTick)
{
_visSetPool.Return(overflowEnts);
_netUidListPool.Return(overflowEnts);
DebugTools.Assert(!sessionData.SentEntities.Values.Contains(overflowEnts));
return;
}
}
else if (!sessionData.SentEntities.TryGetValue(ackedTick, out ackedData))
else if (!sessionData.SentEntities.TryGetValue(ackedTick, out ackedEnts))
return;
// return last acked to pool, but only if it is not still in the OverflowDictionary.
if (sessionData.LastAcked != null && !sessionData.SentEntities.ContainsKey(sessionData.LastAcked.Value.Tick))
var entityData = sessionData.EntityData;
foreach (var ent in CollectionsMarshal.AsSpan(ackedEnts))
{
DebugTools.Assert(!sessionData.SentEntities.Values.Contains(sessionData.LastAcked.Value.Data));
_visSetPool.Return(sessionData.LastAcked.Value.Data);
}
ref var data = ref CollectionsMarshal.GetValueRefOrNullRef(entityData, ent);
if (Unsafe.IsNullRef(ref data))
{
// This should only happen if the entity has been deleted.
sessionData.LastAcked = (ackedTick, ackedData);
foreach (var ent in ackedData.Keys)
{
sessionData.LastSeenAt[ent] = ackedTick;
// TODO PVS turn into debug assert
if (TryGetEntity(ent, out _))
Log.Error($"Acked entity {ToPrettyString(ent)} is missing entityData entry");
continue;
}
data.EntityLastAcked = ackedTick;
DebugTools.Assert(data.LastSent >= ackedTick); // LastSent may equal ackedTick if the packet was sent reliably.
}
// The client acked a tick. If they requested a full state, this ack happened some time after that, so we can safely set this to false

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class contains code for turning a list of visible entities into actual entity states.
internal sealed partial class PvsSystem
{
public void GetStateList(
List<EntityState> states,
List<NetEntity> toSend,
SessionPvsData sessionData,
GameTick fromTick)
{
DebugTools.Assert(states.Count == 0);
var entData = sessionData.EntityData;
var session = sessionData.Session;
if (sessionData.RequestedFull)
{
foreach (var netEntity in CollectionsMarshal.AsSpan(toSend))
{
ref var data = ref GetEntityData(entData, netEntity);
DebugTools.Assert(data.LastSent == _gameTiming.CurTick);
states.Add(GetFullEntityState(session, data.Entity.Owner, data.Entity.Comp));
}
return;
}
foreach (var netEntity in CollectionsMarshal.AsSpan(toSend))
{
ref var data = ref GetEntityData(entData, netEntity);
DebugTools.Assert(data.LastSent == _gameTiming.CurTick);
if (data.Visibility == PvsEntityVisibility.StayedUnchanged)
continue;
var (uid, meta) = data.Entity;
var entered = data.Visibility == PvsEntityVisibility.Entered;
var entFromTick = entered ? data.EntityLastAcked : fromTick;
// TODO PVS turn into debug assert
// This is should really be a debug assert, but I want to check for errors on live servers
// If an entity is not marked as "entering" this tick, then it HAS to have been in the last acked state
if (!entered && data.EntityLastAcked < fromTick)
{
Log.Error($"un-acked entity is not marked as entering. Entity{ToPrettyString(uid)}. FromTick: {fromTick}. CurTick: {_gameTiming.CurTick}. Data: {data}");
}
var state = GetEntityState(session, uid, entFromTick, meta);
if (entered || !state.Empty)
states.Add(state);
}
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
/// <param name="player">The player to generate this state for. This may be null if the state is for replay recordings.</param>
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
/// <param name="fromTick">Only provide delta changes from this tick.</param>
/// <param name="meta">The entity's metadata component</param>
/// <returns>New entity State for the given entity.</returns>
private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
bool sendCompList = meta.LastComponentRemoved > fromTick;
HashSet<ushort>? netComps = sendCompList ? new() : null;
foreach (var (netId, component) in meta.NetComponents)
{
DebugTools.Assert(component.NetSyncEnabled);
if (component.Deleted || !component.Initialized)
{
Log.Error("Entity manager returned deleted or uninitialized components while sending entity data");
continue;
}
if (component.SendOnlyToOwner && player != null && player.AttachedEntity != entityUid)
continue;
if (component.LastModifiedTick <= fromTick)
{
if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(bus, component, player)))
netComps!.Add(netId);
continue;
}
if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(bus, component, player))
continue;
var state = EntityManager.GetComponentState(bus, component, player, fromTick);
DebugTools.Assert(fromTick > component.CreationTick || state is not IComponentDeltaState delta || delta.FullState);
changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
if (sendCompList)
netComps!.Add(netId);
}
DebugTools.Assert(meta.EntityLastModifiedTick >= meta.LastComponentRemoved);
DebugTools.Assert(GetEntity(meta.NetEntity) == entityUid);
var entState = new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
return entState;
}
/// <summary>
/// Variant of <see cref="GetEntityState"/> that includes all entity data, including data that can be inferred implicitly from the entity prototype.
/// </summary>
private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
HashSet<ushort> netComps = new();
foreach (var (netId, component) in meta.NetComponents)
{
DebugTools.Assert(component.NetSyncEnabled);
if (component.SendOnlyToOwner && player.AttachedEntity != entityUid)
continue;
if (component.SessionSpecific && !EntityManager.CanGetComponentState(bus, component, player))
continue;
var state = EntityManager.GetComponentState(bus, component, player, GameTick.Zero);
DebugTools.Assert(state is not IComponentDeltaState delta || delta.FullState);
changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
netComps.Add(netId);
}
var entState = new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
return entState;
}
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
public (List<EntityState>?, List<NetEntity>?, GameTick fromTick) GetAllEntityStates(ICommonSession? player, GameTick fromTick, GameTick toTick)
{
List<EntityState>? stateEntities;
var toSend = _uidSetPool.Get();
DebugTools.Assert(toSend.Count == 0);
bool enumerateAll = false;
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
DebugTools.Assert(toTick > fromTick);
if (player == null)
{
enumerateAll = fromTick == GameTick.Zero;
}
else if (!_seenAllEnts.Contains(player))
{
enumerateAll = true;
fromTick = GameTick.Zero;
}
if (toTick.Value - fromTick.Value > DirtyBufferSize)
{
// Fall back to enumerating over all entities.
enumerateAll = true;
}
if (enumerateAll)
{
stateEntities = new List<EntityState>(EntityManager.EntityCount);
var query = EntityManager.AllEntityQueryEnumerator<MetaDataComponent>();
while (query.MoveNext(out var uid, out var md))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
if (md.EntityLastModifiedTick <= fromTick)
continue;
var state = GetEntityState(player, uid, fromTick, md);
if (state.Empty)
{
Log.Error($@"{nameof(GetEntityState)} returned an empty state while enumerating entities.
Tick: {fromTick}--{toTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
}
stateEntities.Add(state);
}
}
else
{
stateEntities = new();
for (var i = fromTick.Value + 1; i <= toTick.Value; i++)
{
if (!TryGetDirtyEntities(new GameTick(i), out var add, out var dirty))
{
// This should be unreachable if `enumerateAll` is false.
throw new Exception($"Failed to get tick dirty data. tick: {i}, from: {fromTick}, to {toTick}, buffer: {DirtyBufferSize}");
}
foreach (var uid in add)
{
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
var state = GetEntityState(player, uid, fromTick, md);
if (state.Empty)
{
Log.Error($@"{nameof(GetEntityState)} returned an empty state for a new entity.
Tick: {fromTick}--{toTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
continue;
}
stateEntities.Add(state);
}
foreach (var uid in dirty)
{
DebugTools.Assert(!add.Contains(uid));
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
var state = GetEntityState(player, uid, fromTick, md);
if (!state.Empty)
stateEntities.Add(state);
}
}
}
_uidSetPool.Return(toSend);
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
if (stateEntities.Count == 0)
stateEntities = null;
return (stateEntities, deletions, fromTick);
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class contains miscellaneous convenience functions
internal sealed partial class PvsSystem
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref EntityData GetOrNewEntityData(Dictionary<NetEntity, EntityData> entityData, NetEntity entity)
{
ref var data = ref CollectionsMarshal.GetValueRefOrAddDefault(entityData, entity, out var exists);
if (!exists)
{
if (TryGetEntityData(entity, out var uid, out var meta))
{
data = new((uid.Value, meta));
}
else
{
Log.Error($"Attempted to add deleted entity. NetUid: {entity}");
}
}
DebugTools.AssertEqual(data.Entity.Comp.NetEntity, entity);
return ref data;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref EntityData GetEntityData(Dictionary<NetEntity, EntityData> entityData, NetEntity entity)
{
DebugTools.Assert(entityData.ContainsKey(entity));
ref var data = ref CollectionsMarshal.GetValueRefOrAddDefault(entityData, entity, out _);
DebugTools.AssertNotNull(data.Entity.Comp);
DebugTools.AssertEqual(data.Entity.Comp.NetEntity, entity);
return ref data;
}
}

View File

@@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class contains contains methods for adding entities to the set of entities that are about to get sent
// to a player.
internal sealed partial class PvsSystem
{
/// <summary>
/// This method adds an entity to the to-send list, updates the last-sent tick, and updates the entity's visibility.
/// </summary>
private void AddToSendList(
NetEntity ent,
ref EntityData data,
List<NetEntity> list,
GameTick fromTick,
GameTick toTick,
bool entered,
ref int dirtyEntityCount)
{
var meta = data.Entity.Comp;
DebugTools.AssertEqual(meta.NetEntity, ent);
DebugTools.Assert(fromTick < toTick);
DebugTools.AssertNotEqual(data.LastSent, toTick);
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (meta == null || meta.EntityLifeStage >= EntityLifeStage.Terminating)
{
var rep = new EntityStringRepresentation(data.Entity);
Log.Error($"Attempted to add a deleted entity to PVS send set: '{rep}'. Deletion queued: {EntityManager.IsQueuedForDeletion(data.Entity)}. Trace:\n{Environment.StackTrace}");
// This can happen if some entity was some removed from it's parent while that parent was being deleted.
// As a result the entity was marked for deletion but was never actually properly deleted.
EntityManager.QueueDeleteEntity(data.Entity);
return;
}
data.LastSent = toTick;
list.Add(ent);
if (entered)
{
data.Visibility = PvsEntityVisibility.Entered;
dirtyEntityCount++;
return;
}
if (meta.EntityLastModifiedTick <= fromTick)
{
//entity has been sent before and hasn't been updated since
data.Visibility = PvsEntityVisibility.StayedUnchanged;
return;
}
//add us
data.Visibility = PvsEntityVisibility.StayedChanged;
dirtyEntityCount++;
}
/// <summary>
/// This method figures out whether a given entity is currently entering a player's PVS range.
/// This method will also check that the player's PVS entry budget is not being exceeded.
/// </summary>
private (bool Entered, bool BudgetExceeded) GetPvsEntryData(ref EntityData entity,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
int newEntityBudget,
int enteredEntityBudget)
{
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
var enteredSinceLastSent = fromTick == GameTick.Zero
|| entity.LastSent == GameTick.Zero
|| entity.LastSent.Value != toTick.Value - 1;
var entered = enteredSinceLastSent
|| entity.EntityLastAcked == GameTick.Zero
|| entity.EntityLastAcked < fromTick // this entity was not in the last acked state.
|| entity.LastLeftView >= fromTick; // entity left and re-entered sometime after the last acked tick
// If the entity is entering, but we already sent this entering entity in the last message, we won't add it to
// the budget. Chances are the packet will arrive in a nice and orderly fashion, and the client will stick to
// their requested budget. However this can cause issues if a packet gets dropped, because a player may create
// 2x or more times the normal entity creation budget.
if (enteredSinceLastSent)
{
if (newEntityCount >= newEntityBudget || enteredEntityCount >= enteredEntityBudget)
return (entered, true);
enteredEntityCount++;
if (entity.EntityLastAcked == GameTick.Zero)
newEntityCount++;
}
return (entered, false);
}
/// <summary>
/// Recursively add an entity and all of its children to the to-send set.
/// </summary>
private void RecursivelyAddTreeNode(in NetEntity nodeIndex,
RobustTree<NetEntity> tree,
List<NetEntity> toSend,
Dictionary<NetEntity, EntityData> entityData,
Stack<NetEntity> stack,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
int newEntityBudget,
int enteredEntityBudget)
{
stack.Push(nodeIndex);
while (stack.TryPop(out var currentNodeIndex))
{
DebugTools.Assert(currentNodeIndex.IsValid());
// As every map is parented to uid 0 in the tree we still need to get their children, plus because we go top-down
// we may find duplicate parents with children we haven't encountered before
// on different chunks (this is especially common with direct grid children)
ref var data = ref GetOrNewEntityData(entityData, currentNodeIndex);
if (data.LastSent != toTick)
{
var (entered, budgetExceeded) = GetPvsEntryData(ref data, fromTick, toTick,
ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
if (budgetExceeded)
{
// should be false for the majority of entities
if (data.LastSent == GameTick.Zero)
entityData.Remove(currentNodeIndex);
// We continue, but do not stop iterating this or other chunks.
// This is to avoid sending bad pvs-leave messages. I.e., other entities may have just stayed in view, and we can send them without exceeding our budget.
continue;
}
AddToSendList(currentNodeIndex, ref data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
if (!tree.TryGet(currentNodeIndex, out var node))
{
Log.Error($"tree is missing the current node! Node: {currentNodeIndex}");
continue;
}
if (node.Children == null)
continue;
foreach (var child in node.Children)
{
stack.Push(child);
}
}
}
/// <summary>
/// Recursively add an entity and all of its parents to the to-send set. This optionally also adds all children.
/// </summary>
public bool RecursivelyAddOverride(in EntityUid uid,
List<NetEntity> toSend,
Dictionary<NetEntity, EntityData> entityData,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
int newEntityBudget,
int enteredEntityBudget,
bool addChildren = false)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid())
return false;
var xform = _xformQuery.GetComponent(uid);
var parent = xform.ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, toSend, entityData, fromTick, toTick,
ref newEntityCount, ref enteredEntityCount, ref dirtyEntityCount, newEntityBudget, enteredEntityBudget))
{
return false;
}
var netEntity = _metaQuery.GetComponent(uid).NetEntity;
// Note that we check this AFTER adding parents. This is because while this entity may already have been added
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
// send the new parents, which may otherwise be delayed because of the PVS budget.
var curTick = _gameTiming.CurTick;
ref var data = ref GetOrNewEntityData(entityData, netEntity);
if (data.LastSent != curTick)
{
var (entered, _) = GetPvsEntryData(ref data, fromTick, toTick, ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendList(netEntity, ref data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
if (addChildren)
{
RecursivelyAddChildren(xform, toSend, entityData, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, ref dirtyEntityCount, in newEntityBudget, in enteredEntityBudget);
}
return true;
}
/// <summary>
/// Recursively add an entity and all of its children to the to-send set.
/// </summary>
private void RecursivelyAddChildren(TransformComponent xform,
List<NetEntity> toSend,
Dictionary<NetEntity, EntityData> entityData,
in GameTick fromTick,
in GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
in int newEntityBudget,
in int enteredEntityBudget)
{
foreach (var child in xform._children)
{
if (!_xformQuery.TryGetComponent(child, out var childXform))
continue;
var metadata = _metaQuery.GetComponent(child);
var netChild = metadata.NetEntity;
ref var data = ref GetOrNewEntityData(entityData, netChild);
if (data.LastSent != toTick)
{
var (entered, _) = GetPvsEntryData(ref data, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendList(netChild, ref data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
RecursivelyAddChildren(childXform, toSend, entityData, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, ref dirtyEntityCount, in newEntityBudget, in enteredEntityBudget);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,8 @@ public sealed class RobustTree<T> where T : notnull
public TreeNode this[T index] => _nodeIndex[index];
public bool TryGet(T index, out TreeNode node) => _nodeIndex.TryGetValue(index, out node);
public void Remove(T value, bool mend = false)
{
if (!_nodeIndex.TryGetValue(value, out var node))

View File

@@ -3,6 +3,7 @@ using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
@@ -173,6 +174,10 @@ Oldest acked clients: {string.Join(", ", players)}
{
var players = _playerManager.Sessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
// Update client acks, which is used to figure out what data needs to be sent to clients
// This only needs SessionData which isn't touched during GetPVSData or ProcessCollections.
var ackJob = _pvs.ProcessQueuedAcks();
// Update entity positions in PVS chunks/collections
// TODO disable processing if culling is disabled? Need to check if toggling PVS breaks anything.
// TODO parallelize?
@@ -189,11 +194,7 @@ Oldest acked clients: {string.Join(", ", players)}
pvsData = GetPVSData(players);
}
// Update client acks, which is used to figure out what data needs to be sent to clients
using (_usageHistogram.WithLabels("Process Acks").NewTimer())
{
_pvs.ProcessQueuedAcks();
}
ackJob.WaitOne();
// Construct & send the game state to each player.
GameTick oldestAck;
@@ -253,7 +254,8 @@ Oldest acked clients: {string.Join(", ", players)}
}
catch (Exception e) // Catch EVERY exception
{
_logger.Log(LogLevel.Error, e, "Caught exception while generating mail.");
var source = i >= 0 ? players[i].ToString() : "replays";
_logger.Log(LogLevel.Error, e, $"Caught exception while generating mail for {source}.");
}
return resource;
}
@@ -265,15 +267,14 @@ Oldest acked clients: {string.Join(", ", players)}
{
public HashSet<int>[] PlayerChunks;
public EntityUid[][] ViewerEntities;
public (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] ChunkCache;
public RobustTree<NetEntity>?[] ChunkCache;
}
private PvsData? GetPVSData(ICommonSession[] players)
{
var chunks = _pvs.GetChunks(players, ref _playerChunks, ref _viewerEntities);
var chunksCount = chunks.Count;
var chunkCache =
new (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[chunksCount];
var chunkCache = new RobustTree<NetEntity>?[chunksCount];
// Update the reused trees sequentially to avoid having to lock the dictionary per chunk.
var reuse = ArrayPool<bool>.Shared.Rent(chunksCount);
@@ -310,7 +311,7 @@ Oldest acked clients: {string.Join(", ", players)}
{
var channel = session.Channel;
var sessionData = _pvs.PlayerData[session];
var lastAck = sessionData.LastReceivedAck;
var from = sessionData.RequestedFull ? GameTick.Zero : sessionData.LastReceivedAck;
List<NetEntity>? leftPvs = null;
List<EntityState>? entStates;
List<NetEntity>? deletions;
@@ -321,7 +322,7 @@ Oldest acked clients: {string.Join(", ", players)}
{
(entStates, deletions, leftPvs, fromTick) = _pvs.CalculateEntityStates(
session,
lastAck,
from,
_gameTiming.CurTick,
pvsData.Value.ChunkCache,
pvsData.Value.PlayerChunks[i],
@@ -329,7 +330,7 @@ Oldest acked clients: {string.Join(", ", players)}
}
else
{
(entStates, deletions, fromTick) = _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
(entStates, deletions, fromTick) = _pvs.GetAllEntityStates(session, from, _gameTiming.CurTick);
}
var playerStates = _playerManager.GetPlayerStates(fromTick);
@@ -346,7 +347,7 @@ Oldest acked clients: {string.Join(", ", players)}
playerStates,
deletions);
InterlockedHelper.Min(ref oldestAckValue, lastAck.Value);
InterlockedHelper.Min(ref oldestAckValue, from.Value);
// actually send the state
var stateUpdateMessage = new MsgState();
@@ -360,13 +361,13 @@ Oldest acked clients: {string.Join(", ", players)}
// We also do this if the client's last ack is too old. This helps prevent things like the entity deletion
// history from becoming too bloated if a bad client fails to send acks for whatever reason.
if (_gameTiming.CurTick.Value > lastAck.Value + _pvs.ForceAckThreshold)
if (_gameTiming.CurTick.Value > from.Value + _pvs.ForceAckThreshold)
{
stateUpdateMessage.ForceSendReliably = true;
#if FULL_RELEASE
var connectedTime = (DateTime.UtcNow - session.ConnectedTime).TotalMinutes;
if (lastAck > GameTick.Zero && connectedTime > 1)
_logger.Warning($"Client {session} exceeded ack-tick threshold. Last ack: {lastAck}. Cur tick: {_gameTiming.CurTick}. Connect time: {connectedTime} minutes");
if (sessionData.LastReceivedAck > GameTick.Zero && connectedTime > 1)
_logger.Warning($"Client {session} exceeded ack-tick threshold. Last ack: {sessionData.LastReceivedAck}. Cur tick: {_gameTiming.CurTick}. Connect time: {connectedTime} minutes");
#endif
}
@@ -436,22 +437,22 @@ Oldest acked clients: {string.Join(", ", players)}
public List<(int, IChunkIndexLocation)> Chunks;
public bool[] Reuse;
public (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] ChunkCache;
public RobustTree<NetEntity>?[] ChunkCache;
public void Execute(int index)
{
var (visMask, chunkIndexLocation) = Chunks[index];
Reuse[index] = Pvs.TryCalculateChunk(chunkIndexLocation, visMask, out var chunk);
ChunkCache[index] = chunk;
Reuse[index] = Pvs.TryCalculateChunk(chunkIndexLocation, visMask, out var tree);
ChunkCache[index] = tree;
#if DEBUG
if (chunk == null)
if (tree == null)
return;
// Each root nodes should simply be a map or a grid entity.
DebugTools.Assert(chunk.Value.tree.RootNodes.Count == 1,
$"Root node count is {chunk.Value.tree.RootNodes.Count} instead of 1.");
var nent = chunk.Value.tree.RootNodes.FirstOrDefault();
DebugTools.Assert(tree.RootNodes.Count == 1,
$"Root node count is {tree.RootNodes.Count} instead of 1.");
var nent = tree.RootNodes.FirstOrDefault();
var ent = EntManager.GetEntity(nent);
DebugTools.Assert(EntManager.EntityExists(ent), $"Root node does not exist. Node {ent}.");
DebugTools.Assert(EntManager.HasComponent<MapComponent>(ent)

View File

@@ -156,7 +156,7 @@ namespace Robust.Server.Physics
foreach (var session in _subscribedSessions)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
RaiseNetworkEvent(msg, session.Channel);
}
}

View File

@@ -26,6 +26,7 @@
<PackageReference Include="SpaceWizards.Sodium" Version="0.2.1" PrivateAssets="compile" />
<PackageReference Include="SharpZstd.Interop" Version="1.5.2-beta2" PrivateAssets="compile" />
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />

View File

@@ -1,7 +1,9 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Text.Json.Nodes;
using System.Web;
using Robust.Shared;
using Robust.Shared.Utility;
@@ -77,6 +79,8 @@ namespace Robust.Server.ServerStatus
if (string.IsNullOrEmpty(downloadUrl))
{
var query = HttpUtility.ParseQueryString(context.Url.Query);
var optional = query.Keys;
buildInfo = await PrepareACZBuildInfo();
}
else

View File

@@ -278,7 +278,7 @@ namespace Robust.Server.ViewVariables
var closeMsg = new MsgViewVariablesCloseSession();
closeMsg.SessionId = session.SessionId;
_netManager.ServerSendMessage(closeMsg, player.ConnectedClient);
_netManager.ServerSendMessage(closeMsg, player.Channel);
}
private bool TryReinterpretValue(object? input, [NotNullWhen(true)] out object? output)

View File

@@ -24,6 +24,7 @@
//
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -403,7 +404,7 @@ namespace Robust.Shared.Maths
/// </param>
public static Color FromHsl(Vector4 hsl)
{
var hue = hsl.X * 360.0f;
var hue = (hsl.X - MathF.Truncate(hsl.X)) * 360.0f;
var saturation = hsl.Y;
var lightness = hsl.Z;
@@ -514,7 +515,7 @@ namespace Robust.Shared.Maths
/// </param>
public static Color FromHsv(Vector4 hsv)
{
var hue = hsv.X * 360.0f;
var hue = (hsv.X - MathF.Truncate(hsv.X)) * 360.0f;
var saturation = hsv.Y;
var value = hsv.Z;
@@ -1777,7 +1778,7 @@ namespace Robust.Shared.Maths
/// </summary>
public static Color YellowGreen => new(154, 205, 50, 255);
private static readonly Dictionary<string, Color> DefaultColors = new()
private static readonly FrozenDictionary<string, Color> DefaultColors = new Dictionary<string, Color>()
{
["transparent"] = Transparent,
["aliceblue"] = AliceBlue,
@@ -1924,12 +1925,12 @@ namespace Robust.Shared.Maths
["whitesmoke"] = WhiteSmoke,
["yellow"] = Yellow,
["yellowgreen"] = YellowGreen,
};
}.ToFrozenDictionary();
#endregion
private static readonly Dictionary<Color, string> DefaultColorsInverted =
DefaultColors.ToLookup(pair => pair.Value).ToDictionary(i => i.Key, i => i.First().Key);
private static readonly FrozenDictionary<Color, string> DefaultColorsInverted =
DefaultColors.ToLookup(pair => pair.Value).ToFrozenDictionary(i => i.Key, i => i.First().Key);
public readonly string? Name()
{

View File

@@ -30,6 +30,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Robust.Shared.Maths
@@ -385,7 +386,7 @@ namespace Robust.Shared.Maths
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3 CreateRotation(Angle angle)
public static Matrix3 CreateRotation(double angle)
{
var cos = (float) Math.Cos(angle);
var sin = (float) Math.Sin(angle);
@@ -426,12 +427,12 @@ namespace Robust.Shared.Maths
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3 CreateTransform(float posX, float posY, float angle, float scaleX = 1, float scaleY = 1)
public static Matrix3 CreateTransform(float posX, float posY, double angle, float scaleX = 1, float scaleY = 1)
{
// returns a matrix that is equivalent to returning CreateScale(scale) * CreateRotation(angle) * CreateTranslation(posX, posY)
var sin = MathF.Sin(angle);
var cos = MathF.Cos(angle);
var sin = (float) Math.Sin(angle);
var cos = (float) Math.Cos(angle);
return new Matrix3
{
@@ -446,12 +447,12 @@ namespace Robust.Shared.Maths
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3 CreateInverseTransform(float posX, float posY, float angle, float scaleX = 1, float scaleY = 1)
public static Matrix3 CreateInverseTransform(float posX, float posY, double angle, float scaleX = 1, float scaleY = 1)
{
// returns a matrix that is equivalent to returning CreateTranslation(-posX, -posY) * CreateRotation(-angle) * CreateScale(1/scaleX, 1/scaleY)
var sin = MathF.Sin(angle);
var cos = MathF.Cos(angle);
var sin = (float) Math.Sin(angle);
var cos = (float) Math.Cos(angle);
return new Matrix3
{
@@ -493,7 +494,17 @@ namespace Robust.Shared.Maths
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3 CreateInverseTransform(in Vector2 position, in Angle angle, in Vector2 scale)
{
return CreateInverseTransform(position.X, position.Y, (float)angle.Theta, scale.X, scale.Y);
return CreateInverseTransform(position.X, position.Y, angle.Theta, scale.X, scale.Y);
}
/// <summary>
/// Gets the rotation of the Matrix. Will have some precision loss.
/// </summary>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Angle Rotation()
{
return new Vector2(R0C0, R1C0).ToAngle();
}
#endregion Constructors

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Numerics;
using NVorbis;
namespace Robust.Shared.Audio.AudioLoading;
@@ -16,7 +17,8 @@ internal static class AudioLoaderOgg
/// <param name="stream">Audio file stream to load.</param>
public static AudioMetadata LoadAudioMetadata(Stream stream)
{
using var reader = new VorbisReader(stream);
using var reader = new VorbisReader(stream, false);
reader.Initialize();
return new AudioMetadata(reader.TotalTime, reader.Channels, reader.Tags.Title, reader.Tags.Artist);
}
@@ -26,39 +28,88 @@ internal static class AudioLoaderOgg
/// <param name="stream">Audio file stream to load.</param>
public static OggVorbisData LoadAudioData(Stream stream)
{
using var vorbis = new NVorbis.VorbisReader(stream, false);
using var vorbis = new VorbisReader(stream, false);
vorbis.Initialize();
var sampleRate = vorbis.SampleRate;
var channels = vorbis.Channels;
var totalSamples = vorbis.TotalSamples;
var readSamples = 0;
var buffer = new float[totalSamples * channels];
var totalValues = totalSamples * channels;
var readValues = 0;
var buffer = new short[totalSamples * channels];
Span<float> readBuffer = stackalloc float[32768];
while (readSamples < totalSamples)
while (readValues < totalValues)
{
var read = vorbis.ReadSamples(buffer, readSamples * channels, buffer.Length - readSamples);
var read = ReadSamples(buffer.AsSpan(readValues), readBuffer, channels, vorbis);
if (read == 0)
{
break;
}
readSamples += read;
readValues += read;
}
return new OggVorbisData(totalSamples, sampleRate, channels, buffer, vorbis.Tags.Title, vorbis.Tags.Artist);
}
private static int ReadSamples(Span<short> dest, Span<float> readBuffer, int channels, VorbisReader reader)
{
var read = reader.ReadSamples(readBuffer);
read *= channels;
ConvertToShort(readBuffer[..read], dest[..read]);
return read;
}
private static void ConvertToShort(ReadOnlySpan<float> src, Span<short> dst)
{
if (src.Length != dst.Length)
throw new InvalidOperationException("Invalid lengths!");
var simdSamples = (src.Length / Vector<short>.Count) * Vector<short>.Count;
// Note: I think according to spec we'd actually need to multiply negative values with 2^15 instead of 2^15-1.
// because it's -32768 -> 32767
// Can't be arsed though
var factor = new Vector<float>(short.MaxValue);
ref readonly var srcBase = ref src[0];
ref var dstBase = ref dst[0];
for (var i = 0; i < simdSamples; i += Vector<short>.Count)
{
var lower = Vector.LoadUnsafe(in srcBase, (nuint) i);
var upper = Vector.LoadUnsafe(in srcBase, (nuint) (i + Vector<float>.Count));
lower *= factor;
upper *= factor;
var lowerInt = Vector.ConvertToInt32(lower);
var upperInt = Vector.ConvertToInt32(upper);
var merged = Vector.Narrow(lowerInt, upperInt);
merged.StoreUnsafe(ref dstBase, (nuint) i);
}
for (var i = simdSamples; i < src.Length; i++)
{
dst[i] = (short)(src[i] * short.MaxValue);
}
}
internal readonly struct OggVorbisData
{
public readonly long TotalSamples;
public readonly long SampleRate;
public readonly long Channels;
public readonly ReadOnlyMemory<float> Data;
public readonly ReadOnlyMemory<short> Data;
public readonly string Title;
public readonly string Artist;
public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory<float> data, string title, string artist)
public OggVorbisData(long totalSamples, long sampleRate, long channels, ReadOnlyMemory<short> data,
string title, string artist)
{
TotalSamples = totalSamples;
SampleRate = sampleRate;

View File

@@ -63,21 +63,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
protected void SetZOffset(float value)
{
var query = AllEntityQuery<AudioComponent>();
var oldZOffset = ZOffset;
ZOffset = value;
while (query.MoveNext(out var uid, out var audio))
{
// Pythagoras back to normal then adjust.
var maxDistance = MathF.Pow(audio.Params.MaxDistance, 2) - oldZOffset;
var refDistance = MathF.Pow(audio.Params.ReferenceDistance, 2) - oldZOffset;
audio.Params.MaxDistance = maxDistance;
audio.Params.ReferenceDistance = refDistance;
audio.Params = GetAdjustedParams(audio.Params);
Dirty(uid, audio);
}
}
protected virtual void OnAudioUnpaused(EntityUid uid, AudioComponent component, ref EntityUnpausedEvent args)
@@ -141,9 +127,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
{
DebugTools.Assert(!string.IsNullOrEmpty(fileName));
audioParams ??= AudioParams.Default;
var comp = AddComp<Components.AudioComponent>(uid);
var comp = AddComp<AudioComponent>(uid);
comp.FileName = fileName;
comp.Params = GetAdjustedParams(audioParams.Value);
comp.Params = audioParams.Value;
comp.AudioStart = Timing.CurTime;
if (!audioParams.Value.Loop)
@@ -155,22 +141,14 @@ public abstract partial class SharedAudioSystem : EntitySystem
despawn.Lifetime = (float) length.TotalSeconds + 0.01f;
}
if (comp.Params.Variation != null && comp.Params.Variation.Value != 0f)
{
comp.Params.Pitch *= (float) RandMan.NextGaussian(1, comp.Params.Variation.Value);
}
return comp;
}
/// <summary>
/// Accounts for ZOffset on audio distance.
/// </summary>
private AudioParams GetAdjustedParams(AudioParams audioParams)
{
var maxDistance = GetAudioDistance(audioParams.MaxDistance);
var refDistance = GetAudioDistance(audioParams.ReferenceDistance);
return audioParams
.WithMaxDistance(maxDistance)
.WithReferenceDistance(refDistance);
}
public static float GainToVolume(float value)
{
return 10f * MathF.Log10(value);
@@ -205,6 +183,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
return;
component.Params.Volume = value;
component.Volume = value;
Dirty(entity.Value, component);
}

View File

@@ -1030,7 +1030,7 @@ namespace Robust.Shared
/// Master volume for audio output.
/// </summary>
public static readonly CVarDef<float> AudioMasterVolume =
CVarDef.Create("audio.mastervolume", 1.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
CVarDef.Create("audio.mastervolume", 0.50f, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Maximum raycast distance for audio occlusion.
@@ -1302,7 +1302,7 @@ namespace Robust.Shared
*/
public static readonly CVarDef<float> MidiVolume =
CVarDef.Create("midi.volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
CVarDef.Create("midi.volume", 0.50f, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* HUB

View File

@@ -58,11 +58,10 @@ internal sealed class RecursiveMoveSystem : EntitySystem
// annoyingly, containers aren't guaranteed to occlude sprites & lights
// but AFAIK thats currently unused???
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
if (_xformQuery.TryGetComponent(child.Value, out var childXform))
AnythingMovedSubHandler(child.Value, childXform);
if (_xformQuery.TryGetComponent(child, out var childXform))
AnythingMovedSubHandler(child, childXform);
}
}
}

View File

@@ -799,11 +799,5 @@ namespace Robust.Shared.Configuration
public InvalidConfigurationException(string message, Exception inner) : base(message, inner)
{
}
protected InvalidConfigurationException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -131,7 +131,7 @@ public sealed class TeleportToCommand : LocalizedCommands
return true;
}
if (_players.Sessions.TryFirstOrDefault(x => x.ConnectedClient.UserName == str, out var session)
if (_players.Sessions.TryFirstOrDefault(x => x.Channel.UserName == str, out var session)
&& _entities.TryGetComponent(session.AttachedEntity, out transform))
{
victimUid = session.AttachedEntity;

View File

@@ -48,7 +48,7 @@ namespace Robust.Shared.Containers
public string ID = default!;
[NonSerialized]
internal ContainerManagerComponent Manager = default!;
protected internal ContainerManagerComponent Manager = default!;
/// <summary>
/// Prevents light from escaping the container, from ex. a flashlight.
@@ -71,14 +71,6 @@ namespace Robust.Shared.Containers
[DataField("showEnts")]
public bool ShowContents { get; set; }
internal void Init(string id, EntityUid owner, ContainerManagerComponent component)
{
DebugTools.AssertNull(ID);
ID = id;
Manager = component;
Owner = owner;
}
[Obsolete("Use container system method")]
public bool Insert(
EntityUid toinsert,
@@ -90,172 +82,7 @@ namespace Robust.Shared.Containers
bool force = false)
{
IoCManager.Resolve(ref entMan);
DebugTools.AssertOwner(toinsert, transform);
DebugTools.AssertOwner(Owner, ownerTransform);
DebugTools.AssertOwner(toinsert, physics);
DebugTools.Assert(!ExpectedEntities.Contains(entMan.GetNetEntity(toinsert)));
DebugTools.Assert(Manager.Containers.ContainsKey(ID));
var physicsQuery = entMan.GetEntityQuery<PhysicsComponent>();
var transformQuery = entMan.GetEntityQuery<TransformComponent>();
var jointQuery = entMan.GetEntityQuery<JointComponent>();
// ECS containers when
var physicsSys = entMan.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
var jointSys = entMan.EntitySysManager.GetEntitySystem<SharedJointSystem>();
var containerSys = entMan.EntitySysManager.GetEntitySystem<SharedContainerSystem>();
// If someone is attempting to insert an entity into a container that is getting deleted, then we will
// automatically delete that entity. I.e., the insertion automatically "succeeds" and both entities get deleted.
// This is consistent with what happens if you attempt to attach an entity to a terminating parent.
if (!entMan.TryGetComponent(Owner, out MetaDataComponent? ownerMeta))
{
Logger.ErrorS("container",
$"Attempted to insert an entity {entMan.ToPrettyString(toinsert)} into a non-existent entity.");
entMan.QueueDeleteEntity(toinsert);
return false;
}
if (ownerMeta.EntityLifeStage >= EntityLifeStage.Terminating)
{
Logger.ErrorS("container",
$"Attempted to insert an entity {entMan.ToPrettyString(toinsert)} into an entity that is terminating. Entity: {entMan.ToPrettyString(Owner)}.");
entMan.QueueDeleteEntity(toinsert);
return false;
}
transform ??= transformQuery.GetComponent(toinsert);
//Verify we can insert into this container
if (!force && !containerSys.CanInsert(toinsert, this, containerXform: ownerTransform))
return false;
// Please somebody ecs containers
var lookupSys = entMan.EntitySysManager.GetEntitySystem<EntityLookupSystem>();
var xformSys = entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
meta ??= entMan.GetComponent<MetaDataComponent>(toinsert);
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
{
Logger.ErrorS("container",
$"Attempted to insert a terminating entity {entMan.ToPrettyString(toinsert)} into a container {ID} in entity: {entMan.ToPrettyString(Owner)}.");
return false;
}
// remove from any old containers.
if ((meta.Flags & MetaDataFlags.InContainer) != 0 &&
entMan.TryGetComponent(transform.ParentUid, out ContainerManagerComponent? oldManager) &&
oldManager.TryGetContainer(toinsert, out var oldContainer) &&
!oldContainer.Remove(toinsert, entMan, transform, meta, false, false))
{
// failed to remove from container --> cannot insert.
return false;
}
// Update metadata first, so that parent change events can check IsInContainer.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
meta.Flags |= MetaDataFlags.InContainer;
// Remove the entity and any children from broadphases.
// This is done before changing can collide to avoid unecceary updates.
// TODO maybe combine with RecursivelyUpdatePhysics to avoid fetching components and iterating parents twice?
lookupSys.RemoveFromEntityTree(toinsert, transform);
DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid());
// Avoid unnecessary broadphase updates while unanchoring, changing physics collision, and re-parenting.
var old = transform.Broadphase;
transform.Broadphase = BroadphaseData.Invalid;
// Unanchor the entity (without changing physics body types).
xformSys.Unanchor(toinsert, transform, false);
// Next, update physics. Note that this cannot just be done in the physics system via parent change events,
// because the insertion may not result in a parent change. This could alternatively be done via a
// got-inserted event, but really that event should run after the entity was actually inserted (so that
// parent/map have updated). But we are better of disabling collision before doing map/parent changes.
physicsQuery.Resolve(toinsert, ref physics, false);
RecursivelyUpdatePhysics(toinsert, transform, physics, physicsSys, physicsQuery, transformQuery);
// Attach to new parent
var oldParent = transform.ParentUid;
xformSys.SetCoordinates(toinsert, transform, new EntityCoordinates(Owner, Vector2.Zero), Angle.Zero);
transform.Broadphase = old;
// the transform.AttachParent() could previously result in the flag being unset, so check that this hasn't happened.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
// Implementation specific insert logic
InternalInsert(toinsert, entMan);
// Update any relevant joint relays
// Can't be done above as the container flag isn't set yet.
RecursivelyUpdateJoints(toinsert, transform, jointSys, jointQuery, transformQuery);
// Raise container events (after re-parenting and internal remove).
entMan.EventBus.RaiseLocalEvent(Owner, new EntInsertedIntoContainerMessage(toinsert, oldParent, this), true);
entMan.EventBus.RaiseLocalEvent(toinsert, new EntGotInsertedIntoContainerMessage(toinsert, this), true);
// The sheer number of asserts tells you about how little I trust container and parenting code.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
DebugTools.Assert(!transform.Anchored);
DebugTools.Assert(transform.LocalPosition == Vector2.Zero);
DebugTools.Assert(transform.LocalRotation == Angle.Zero);
DebugTools.Assert(!physicsQuery.TryGetComponent(toinsert, out var phys) || (!phys.Awake && !phys.CanCollide));
entMan.Dirty(Owner, Manager);
return true;
}
internal void RecursivelyUpdatePhysics(
EntityUid uid,
TransformComponent xform,
PhysicsComponent? physics,
SharedPhysicsSystem physicsSys,
EntityQuery<PhysicsComponent> physicsQuery,
EntityQuery<TransformComponent> transformQuery)
{
if (physics != null)
{
// Here we intentionally don't dirty the physics comp. Client-side state handling will apply these same
// changes. This also ensures that the server doesn't have to send the physics comp state to every
// player for any entity inside of a container during init.
physicsSys.SetLinearVelocity(uid, Vector2.Zero, false, body: physics);
physicsSys.SetAngularVelocity(uid,0, false, body: physics);
physicsSys.SetCanCollide(uid, false, false, body: physics);
}
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
var childXform = transformQuery.GetComponent(child.Value);
physicsQuery.TryGetComponent(child.Value, out var childPhysics);
RecursivelyUpdatePhysics(child.Value, childXform, childPhysics, physicsSys, physicsQuery, transformQuery);
}
}
internal void RecursivelyUpdateJoints(
EntityUid uid,
TransformComponent xform,
SharedJointSystem jointSys,
EntityQuery<JointComponent> jointQuery,
EntityQuery<TransformComponent> transformQuery)
{
if (jointQuery.TryGetComponent(uid, out var jointComp))
{
// TODO: This is going to be going up while joints going down, although these aren't too common
// in SS14 atm.
jointSys.RefreshRelay(uid, jointComp);
}
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
var childXform = transformQuery.GetComponent(child.Value);
RecursivelyUpdateJoints(child.Value, childXform, jointSys, jointQuery, transformQuery);
}
return entMan.System<SharedContainerSystem>().Insert((toinsert, transform, meta, physics), this, ownerTransform, force);
}
/// <summary>
@@ -273,75 +100,11 @@ namespace Robust.Shared.Containers
bool reparent = true,
bool force = false,
EntityCoordinates? destination = null,
Angle? localRotation = null)
Angle? localRotation = null
)
{
IoCManager.Resolve(ref entMan);
DebugTools.AssertNotNull(Manager);
DebugTools.Assert(entMan.EntityExists(toRemove));
DebugTools.AssertOwner(toRemove, xform);
DebugTools.AssertOwner(toRemove, meta);
xform ??= entMan.GetComponent<TransformComponent>(toRemove);
meta ??= entMan.GetComponent<MetaDataComponent>(toRemove);
var sys = entMan.EntitySysManager.GetEntitySystem<SharedContainerSystem>();
if (!force && !sys.CanRemove(toRemove, this))
return false;
if (force && !Contains(toRemove))
{
DebugTools.Assert("Attempted to force remove an entity that was never inside of the container.");
return false;
}
DebugTools.Assert(meta.EntityLifeStage < EntityLifeStage.Terminating || (force && !reparent));
DebugTools.Assert(xform.Broadphase == null || !xform.Broadphase.Value.IsValid());
DebugTools.Assert(!xform.Anchored);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0x0);
DebugTools.Assert(!entMan.TryGetComponent(toRemove, out PhysicsComponent? phys) || (!phys.Awake && !phys.CanCollide));
// Unset flag (before parent change events are raised).
meta.Flags &= ~MetaDataFlags.InContainer;
// Implementation specific remove logic
InternalRemove(toRemove, entMan);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0x0);
var oldParent = xform.ParentUid;
if (destination != null)
{
// Container ECS when.
entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().SetCoordinates(toRemove, xform, destination.Value, localRotation);
}
else if (reparent)
{
// Container ECS when.
sys.AttachParentToContainerOrGrid((toRemove, xform));
if (localRotation != null)
entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().SetLocalRotation(xform, localRotation.Value);
}
// Add to new broadphase
if (xform.ParentUid == oldParent // move event should already have handled it
&& xform.Broadphase == null) // broadphase explicitly invalid?
{
entMan.EntitySysManager.GetEntitySystem<EntityLookupSystem>().FindAndAddToEntityTree(toRemove, xform: xform);
}
if (entMan.TryGetComponent<JointComponent>(toRemove, out var jointComp))
{
entMan.System<SharedJointSystem>().RefreshRelay(toRemove, jointComp);
}
// Raise container events (after re-parenting and internal remove).
entMan.EventBus.RaiseLocalEvent(Owner, new EntRemovedFromContainerMessage(toRemove, this), true);
entMan.EventBus.RaiseLocalEvent(toRemove, new EntGotRemovedFromContainerMessage(toRemove, this), false);
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value));
entMan.Dirty(Owner, Manager);
return true;
return entMan.System<SharedContainerSystem>().Remove((toRemove, xform, meta), this, reparent, force, destination, localRotation);
}
[Obsolete("Use container system method")]
@@ -359,28 +122,31 @@ namespace Robust.Shared.Containers
/// <summary>
/// Clears the container and marks it as deleted.
/// </summary>
public void Shutdown(IEntityManager? entMan = null, INetManager? netMan = null)
[Obsolete("use system method")]
public void Shutdown(IEntityManager? entMan = null, INetManager? _ = null)
{
IoCManager.Resolve(ref entMan, ref netMan);
InternalShutdown(entMan, netMan.IsClient);
Manager.Containers.Remove(ID);
IoCManager.Resolve(ref entMan);
entMan.System<SharedContainerSystem>().ShutdownContainer(this);
}
/// <inheritdoc />
protected abstract void InternalShutdown(IEntityManager entMan, bool isClient);
[Access(typeof(SharedContainerSystem))]
protected internal abstract void InternalShutdown(IEntityManager entMan, SharedContainerSystem system, bool isClient);
/// <summary>
/// Implement to store the reference in whatever form you want
/// </summary>
/// <param name="toInsert"></param>
/// <param name="entMan"></param>
protected abstract void InternalInsert(EntityUid toInsert, IEntityManager entMan);
[Access(typeof(SharedContainerSystem))]
protected internal abstract void InternalInsert(EntityUid toInsert, IEntityManager entMan);
/// <summary>
/// Implement to remove the reference you used to store the entity
/// </summary>
/// <param name="toRemove"></param>
/// <param name="entMan"></param>
protected abstract void InternalRemove(EntityUid toRemove, IEntityManager entMan);
[Access(typeof(SharedContainerSystem))]
protected internal abstract void InternalRemove(EntityUid toRemove, IEntityManager entMan);
}
}

View File

@@ -33,14 +33,14 @@ namespace Robust.Shared.Containers
public override IReadOnlyList<EntityUid> ContainedEntities => _containerList;
/// <inheritdoc />
protected override void InternalInsert(EntityUid toInsert, IEntityManager entMan)
protected internal override void InternalInsert(EntityUid toInsert, IEntityManager entMan)
{
DebugTools.Assert(!_containerList.Contains(toInsert));
_containerList.Add(toInsert);
}
/// <inheritdoc />
protected override void InternalRemove(EntityUid toRemove, IEntityManager entMan)
protected internal override void InternalRemove(EntityUid toRemove, IEntityManager entMan)
{
_containerList.Remove(toRemove);
}
@@ -63,14 +63,14 @@ namespace Robust.Shared.Containers
}
/// <inheritdoc />
protected override void InternalShutdown(IEntityManager entMan, bool isClient)
protected internal override void InternalShutdown(IEntityManager entMan, SharedContainerSystem system, bool isClient)
{
foreach (var entity in _containerList.ToArray())
{
if (!isClient)
entMan.DeleteEntity(entity);
else if (entMan.EntityExists(entity))
Remove(entity, entMan, reparent: false, force: true);
system.Remove(entity, this, reparent: false, force: true);
}
}
}

View File

@@ -25,72 +25,43 @@ namespace Robust.Shared.Containers
[DataField("containers")]
public Dictionary<string, BaseContainer> Containers = new();
// Requires a custom serializer + copier to get rid of. Good luck
void ISerializationHooks.AfterDeserialization()
{
// TODO custom type serializer
foreach (var (id, container) in Containers)
{
container.Manager = this;
container.Owner = Owner;
container.ID = id;
container.Owner = Owner;
container.Manager = this;
}
}
[Obsolete]
public T MakeContainer<T>(EntityUid uid, string id)
where T : BaseContainer
{
if (HasContainer(id))
throw new ArgumentException($"Container with specified ID already exists: '{id}'");
var container = _dynFactory.CreateInstanceUnchecked<T>(typeof(T), inject: false);
container.Init(id, uid, this);
Containers[id] = container;
_entMan.Dirty(uid, this);
return container;
}
=> _entMan.System<SharedContainerSystem>().MakeContainer<T>(uid, id, this);
[Obsolete]
public BaseContainer GetContainer(string id)
{
return Containers[id];
}
=> _entMan.System<SharedContainerSystem>().GetContainer(Owner, id, this);
[Obsolete]
public bool HasContainer(string id)
{
return Containers.ContainsKey(id);
}
=> _entMan.System<SharedContainerSystem>().HasContainer(Owner, id, this);
[Obsolete]
public bool TryGetContainer(string id, [NotNullWhen(true)] out BaseContainer? container)
{
var ret = Containers.TryGetValue(id, out var cont);
container = cont!;
return ret;
}
=> _entMan.System<SharedContainerSystem>().TryGetContainer(Owner, id, out container, this);
[Obsolete]
public bool TryGetContainer(EntityUid entity, [NotNullWhen(true)] out BaseContainer? container)
{
foreach (var contain in Containers.Values)
{
if (contain.Contains(entity))
{
container = contain;
return true;
}
}
container = default;
return false;
}
=> _entMan.System<SharedContainerSystem>().TryGetContainingContainer(Owner, entity, out container, this);
[Obsolete]
public bool ContainsEntity(EntityUid entity)
{
foreach (var container in Containers.Values)
{
if (container.Contains(entity)) return true;
}
return false;
}
=> _entMan.System<SharedContainerSystem>().ContainsEntity(Owner, entity, this);
[Obsolete]
public bool Remove(EntityUid toremove,
TransformComponent? xform = null,
MetaDataComponent? meta = null,
@@ -98,20 +69,11 @@ namespace Robust.Shared.Containers
bool force = false,
EntityCoordinates? destination = null,
Angle? localRotation = null)
{
foreach (var containers in Containers.Values)
{
if (containers.Contains(toremove))
return containers.Remove(toremove, _entMan, xform, meta, reparent, force, destination, localRotation);
}
return true; // If we don't contain the entity, it will always be removed
}
=> _entMan.System<SharedContainerSystem>().RemoveEntity(Owner, toremove, this, xform, meta, reparent, force, destination, localRotation);
[Obsolete]
public AllContainersEnumerable GetAllContainers()
{
return new(this);
}
=> _entMan.System<SharedContainerSystem>().GetAllContainers(Owner, this);
[Serializable, NetSerializable]
internal sealed class ContainerManagerComponentState : ComponentState

View File

@@ -74,7 +74,7 @@ namespace Robust.Shared.Containers
=> ContainedEntity == null || assumeEmpty;
/// <inheritdoc />
protected override void InternalInsert(EntityUid toInsert, IEntityManager entMan)
protected internal override void InternalInsert(EntityUid toInsert, IEntityManager entMan)
{
DebugTools.Assert(ContainedEntity == null);
@@ -87,14 +87,14 @@ namespace Robust.Shared.Containers
}
/// <inheritdoc />
protected override void InternalRemove(EntityUid toRemove, IEntityManager entMan)
protected internal override void InternalRemove(EntityUid toRemove, IEntityManager entMan)
{
DebugTools.Assert(ContainedEntity == toRemove);
ContainedEntity = null;
}
/// <inheritdoc />
protected override void InternalShutdown(IEntityManager entMan, bool isClient)
protected internal override void InternalShutdown(IEntityManager entMan, SharedContainerSystem system, bool isClient)
{
if (ContainedEntity is not { } entity)
return;
@@ -102,7 +102,7 @@ namespace Robust.Shared.Containers
if (!isClient)
entMan.DeleteEntity(entity);
else if (entMan.EntityExists(entity))
Remove(entity, entMan, reparent: false, force: true);
system.Remove(entity, this, reparent: false, force: true);
}
}
}

View File

@@ -1,6 +1,10 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
using System;
using System.Numerics;
namespace Robust.Shared.Containers;
@@ -28,12 +32,107 @@ public abstract partial class SharedContainerSystem
TransformComponent? containerXform = null,
bool force = false)
{
var (uid, transform, meta, physics) = toInsert;
// Cannot Use Resolve(ref toInsert) as the physics component is optional
if (!Resolve(toInsert.Owner, ref toInsert.Comp1, ref toInsert.Comp2))
if (!Resolve(uid, ref transform, ref meta))
return false;
// TODO move logic over to the system.
return container.Insert(toInsert, EntityManager, toInsert, containerXform, toInsert, toInsert, force);
DebugTools.AssertOwner(container.Owner, containerXform);
DebugTools.AssertOwner(toInsert, physics);
DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)));
DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID));
// If someone is attempting to insert an entity into a container that is getting deleted, then we will
// automatically delete that entity. I.e., the insertion automatically "succeeds" and both entities get deleted.
// This is consistent with what happens if you attempt to attach an entity to a terminating parent.
if (!TryComp(container.Owner, out MetaDataComponent? ownerMeta))
{
Log.Error($"Attempted to insert an entity {ToPrettyString(toInsert)} into a non-existent entity.");
QueueDel(toInsert);
return false;
}
if (ownerMeta.EntityLifeStage >= EntityLifeStage.Terminating)
{
Log.Error($"Attempted to insert an entity {ToPrettyString(toInsert)} into an entity that is terminating. Entity: {ToPrettyString(container.Owner)}.");
QueueDel(toInsert);
return false;
}
//Verify we can insert into this container
if (!force && !CanInsert(uid, container, containerXform: containerXform))
return false;
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
{
Log.Error($"Attempted to insert a terminating entity {ToPrettyString(uid)} into a container {container.ID} in entity: {ToPrettyString(container.Owner)}.");
return false;
}
// remove from any old containers.
if ((meta.Flags & MetaDataFlags.InContainer) != 0 &&
TryComp(transform.ParentUid, out ContainerManagerComponent? oldManager) &&
TryGetContainingContainer(transform.ParentUid, toInsert, out var oldContainer, oldManager) &&
!Remove((uid, transform, meta), oldContainer, reparent: false, force: false))
{
// failed to remove from container --> cannot insert.
return false;
}
// Update metadata first, so that parent change events can check IsInContainer.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
meta.Flags |= MetaDataFlags.InContainer;
// Remove the entity and any children from broadphases.
// This is done before changing can collide to avoid unecceary updates.
// TODO maybe combine with RecursivelyUpdatePhysics to avoid fetching components and iterating parents twice?
_lookup.RemoveFromEntityTree(toInsert, transform);
DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid());
// Avoid unnecessary broadphase updates while unanchoring, changing physics collision, and re-parenting.
var old = transform.Broadphase;
transform.Broadphase = BroadphaseData.Invalid;
// Unanchor the entity (without changing physics body types).
_transform.Unanchor(toInsert, transform, false);
// Next, update physics. Note that this cannot just be done in the physics system via parent change events,
// because the insertion may not result in a parent change. This could alternatively be done via a
// got-inserted event, but really that event should run after the entity was actually inserted (so that
// parent/map have updated). But we are better of disabling collision before doing map/parent changes.
PhysicsQuery.Resolve(toInsert, ref physics, logMissing: false);
RecursivelyUpdatePhysics((toInsert, transform, physics));
// Attach to new parent
var oldParent = transform.ParentUid;
_transform.SetCoordinates(toInsert, transform, new EntityCoordinates(container.Owner, Vector2.Zero), Angle.Zero);
transform.Broadphase = old;
// the transform.AttachParent() could previously result in the flag being unset, so check that this hasn't happened.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
// Implementation specific insert logic
container.InternalInsert(toInsert, EntityManager);
// Update any relevant joint relays
// Can't be done above as the container flag isn't set yet.
RecursivelyUpdateJoints((toInsert, transform));
// Raise container events (after re-parenting and internal remove).
RaiseLocalEvent(container.Owner, new EntInsertedIntoContainerMessage(toInsert, oldParent, container), true);
RaiseLocalEvent(toInsert, new EntGotInsertedIntoContainerMessage(toInsert, container), true);
// The sheer number of asserts tells you about how little I trust container and parenting code.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
DebugTools.Assert(!transform.Anchored);
DebugTools.Assert(transform.LocalPosition == Vector2.Zero);
DebugTools.Assert(transform.LocalRotation == Angle.Zero);
DebugTools.Assert(!PhysicsQuery.TryGetComponent(toInsert, out var phys) || (!phys.Awake && !phys.CanCollide));
Dirty(container.Owner, container.Manager);
return true;
}
/// <summary>
@@ -74,4 +173,40 @@ public abstract partial class SharedContainerSystem
return !gettingInsertedAttemptEvent.Cancelled;
}
private void RecursivelyUpdatePhysics(Entity<TransformComponent, PhysicsComponent?> entity)
{
if (entity.Comp2 is { } physics)
{
// Here we intentionally don't dirty the physics comp. Client-side state handling will apply these same
// changes. This also ensures that the server doesn't have to send the physics comp state to every
// player for any entity inside of a container during init.
_physics.SetLinearVelocity(entity, Vector2.Zero, false, body: physics);
_physics.SetAngularVelocity(entity, 0, false, body: physics);
_physics.SetCanCollide(entity, false, false, body: physics);
}
foreach (var child in entity.Comp1._children)
{
var childXform = TransformQuery.GetComponent(child);
PhysicsQuery.TryGetComponent(child, out var childPhysics);
RecursivelyUpdatePhysics((child, childXform, childPhysics));
}
}
internal void RecursivelyUpdateJoints(Entity<TransformComponent> entity)
{
if (JointQuery.TryGetComponent(entity, out var jointComp))
{
// TODO: This is going to be going up while joints going down, although these aren't too common
// in SS14 atm.
_joint.RefreshRelay(entity, jointComp);
}
foreach (var child in entity.Comp._children)
{
var childXform = TransformQuery.GetComponent(child);
RecursivelyUpdateJoints((child, childXform));
}
}
}

View File

@@ -1,6 +1,12 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Robust.Shared.Containers;
@@ -30,12 +36,80 @@ public abstract partial class SharedContainerSystem
EntityCoordinates? destination = null,
Angle? localRotation = null)
{
var (uid, xform, meta) = toRemove;
// Cannot Use Resolve(ref toInsert) as the physics component is optional
if (!Resolve(toRemove.Owner, ref toRemove.Comp1, ref toRemove.Comp2))
if (!Resolve(uid, ref xform, ref meta))
return false;
// TODO move logic over to the system.
return container.Remove(toRemove, EntityManager, toRemove, toRemove, reparent, force, destination, localRotation);
DebugTools.AssertNotNull(container.Manager);
DebugTools.Assert(Exists(toRemove));
if (!force && !CanRemove(toRemove, container))
return false;
if (force && !container.Contains(toRemove))
{
DebugTools.Assert("Attempted to force remove an entity that was never inside of the container.");
return false;
}
// Terminating entities should not get re-parented. However, this removal will still be forced when
// detaching to null-space just before deletion happens.
if (meta.EntityLifeStage >= EntityLifeStage.Terminating && (!force || reparent))
{
Log.Error($"Attempting to remove an entity from a container while it is terminating. Entity: {ToPrettyString(toRemove, meta)}. Container: {ToPrettyString(container.Owner)}. Trace: {Environment.StackTrace}");
return false;
}
DebugTools.Assert(meta.EntityLifeStage < EntityLifeStage.Terminating || (force && !reparent));
DebugTools.Assert(xform.Broadphase == null || !xform.Broadphase.Value.IsValid());
DebugTools.Assert(!xform.Anchored);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0x0);
DebugTools.Assert(!TryComp(toRemove, out PhysicsComponent? phys) || (!phys.Awake && !phys.CanCollide));
// Unset flag (before parent change events are raised).
meta.Flags &= ~MetaDataFlags.InContainer;
// Implementation specific remove logic
container.InternalRemove(toRemove, EntityManager);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0x0);
var oldParent = xform.ParentUid;
if (destination != null)
{
// Container ECS when.
_transform.SetCoordinates(toRemove, xform, destination.Value, localRotation);
}
else if (reparent)
{
// Container ECS when.
AttachParentToContainerOrGrid((toRemove, xform));
if (localRotation != null)
_transform.SetLocalRotation(uid, localRotation.Value, xform);
}
// Add to new broadphase
if (xform.ParentUid == oldParent // move event should already have handled it
&& xform.Broadphase == null) // broadphase explicitly invalid?
{
_lookup.FindAndAddToEntityTree(toRemove, xform: xform);
}
if (TryComp<JointComponent>(toRemove, out var jointComp))
{
_joint.RefreshRelay(toRemove, jointComp);
}
// Raise container events (after re-parenting and internal remove).
RaiseLocalEvent(container.Owner, new EntRemovedFromContainerMessage(toRemove, container), true);
RaiseLocalEvent(toRemove, new EntGotRemovedFromContainerMessage(toRemove, container), false);
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value));
Dirty(container.Owner, container.Manager);
return true;
}
/// <summary>

View File

@@ -42,7 +42,7 @@ public abstract partial class SharedContainerSystem : EntitySystem
// entities in containers without having to "re-insert" them.
meta.Flags |= MetaDataFlags.InContainer;
_lookup.RemoveFromEntityTree(ent, xform);
cont.RecursivelyUpdatePhysics(ent, xform, physics, _physics, PhysicsQuery, TransformQuery);
RecursivelyUpdatePhysics((ent, xform, physics));
// assert children have correct properties
ValidateChildren(xform, TransformQuery, PhysicsQuery);
@@ -54,18 +54,17 @@ public abstract partial class SharedContainerSystem : EntitySystem
private void ValidateChildren(TransformComponent xform, EntityQuery<TransformComponent> xformQuery, EntityQuery<PhysicsComponent> physicsQuery)
{
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
foreach (var child in xform._children)
{
if (!xformQuery.TryGetComponent(child, out var childXform))
continue;
DebugTools.Assert(!xform.Anchored,
$"Child of contained entity is anchored, Entity: {ToPrettyString(child.Value)}");
$"Child of contained entity is anchored, Entity: {ToPrettyString(child)}");
DebugTools.Assert(!physicsQuery.TryGetComponent(child, out var physics) || (!physics.Awake && !physics.CanCollide),
$"Child of contained entity is can collide, Entity: {ToPrettyString(child.Value)}");
$"Child of contained entity is can collide, Entity: {ToPrettyString(child)}");
DebugTools.Assert(xform.Broadphase == null,
$"Child of contained entity is has non-null broadphase, Entity: {ToPrettyString(child.Value)}");
$"Child of contained entity is has non-null broadphase, Entity: {ToPrettyString(child)}");
ValidateChildren(childXform, xformQuery, physicsQuery);
}
}

View File

@@ -9,6 +9,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
@@ -17,15 +18,18 @@ namespace Robust.Shared.Containers
{
public abstract partial class SharedContainerSystem
{
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedJointSystem _joint = default!;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<MapComponent> _mapQuery;
protected EntityQuery<MetaDataComponent> MetaQuery;
protected EntityQuery<PhysicsComponent> PhysicsQuery;
protected EntityQuery<JointComponent> JointQuery;
protected EntityQuery<TransformComponent> TransformQuery;
/// <inheritdoc />
@@ -42,6 +46,7 @@ namespace Robust.Shared.Containers
_mapQuery = GetEntityQuery<MapComponent>();
MetaQuery = GetEntityQuery<MetaDataComponent>();
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
JointQuery = GetEntityQuery<JointComponent>();
TransformQuery = GetEntityQuery<TransformComponent>();
}
@@ -69,7 +74,7 @@ namespace Robust.Shared.Containers
{
foreach (var container in component.Containers.Values)
{
container.Shutdown(EntityManager, _net);
ShutdownContainer(container);
}
component.Containers.Clear();
@@ -85,7 +90,26 @@ namespace Robust.Shared.Containers
if (!Resolve(uid, ref containerManager, false))
containerManager = AddComp<ContainerManagerComponent>(uid); // Happy Vera.
return containerManager.MakeContainer<T>(uid, id);
if (HasContainer(uid, id, containerManager))
throw new ArgumentException($"Container with specified ID already exists: '{id}'");
var container = _dynFactory.CreateInstanceUnchecked<T>(typeof(T), inject: false);
InitContainer(container, (uid, containerManager), id);
containerManager.Containers[id] = container;
Dirty(uid, containerManager);
return container;
}
protected void InitContainer(BaseContainer container, Entity<ContainerManagerComponent> containerEnt, string id)
{
DebugTools.AssertNull(container.ID);
((container.Owner, container.Manager), container.ID) = (containerEnt, id);
}
public void ShutdownContainer(BaseContainer container)
{
container.InternalShutdown(EntityManager, this, _net.IsClient);
container.Manager.Containers.Remove(container.ID);
}
public T EnsureContainer<T>(EntityUid uid, string id, out bool alreadyExisted, ContainerManagerComponent? containerManager = null)
@@ -119,7 +143,7 @@ namespace Robust.Shared.Containers
if (!Resolve(uid, ref containerManager))
throw new ArgumentException("Entity does not have a ContainerManagerComponent!", nameof(uid));
return containerManager.GetContainer(id);
return containerManager.Containers[id];
}
public bool HasContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager)
@@ -127,24 +151,38 @@ namespace Robust.Shared.Containers
if (!Resolve(uid, ref containerManager, false))
return false;
return containerManager.HasContainer(id);
return containerManager.Containers.ContainsKey(id);
}
public bool TryGetContainer(EntityUid uid, string id, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null)
{
if (Resolve(uid, ref containerManager, false))
return containerManager.TryGetContainer(id, out container);
if (!Resolve(uid, ref containerManager, false))
{
container = null;
return false;
}
container = null;
return false;
return containerManager.Containers.TryGetValue(id, out container);
}
public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null, bool skipExistCheck = false)
{
if (Resolve(uid, ref containerManager, false) && (skipExistCheck || Exists(containedUid)))
return containerManager.TryGetContainer(containedUid, out container);
if (!Resolve(uid, ref containerManager, false) || !(skipExistCheck || Exists(containedUid)))
{
container = null;
return false;
}
container = null;
foreach (var contain in containerManager.Containers.Values)
{
if (contain.Contains(containedUid))
{
container = contain;
return true;
}
}
container = default;
return false;
}
@@ -153,10 +191,16 @@ namespace Robust.Shared.Containers
if (!Resolve(uid, ref containerManager, false) || !Exists(containedUid))
return false;
return containerManager.ContainsEntity(containedUid);
foreach (var container in containerManager.Containers.Values)
{
if (container.Contains(containedUid))
return true;
}
return false;
}
public void RemoveEntity(
public bool RemoveEntity(
EntityUid uid,
EntityUid toremove,
ContainerManagerComponent? containerManager = null,
@@ -168,9 +212,15 @@ namespace Robust.Shared.Containers
Angle? localRotation = null)
{
if (!Resolve(uid, ref containerManager) || !Resolve(toremove, ref containedMeta, ref containedXform))
return;
return false;
containerManager.Remove(toremove, containedXform, containedMeta, reparent, force, destination, localRotation);
foreach (var containers in containerManager.Containers.Values)
{
if (containers.Contains(toremove))
return Remove((toremove, containedXform, containedMeta), containers, reparent, force, destination, localRotation);
}
return true; // If we don't contain the entity, it will always be removed
}
public ContainerManagerComponent.AllContainersEnumerable GetAllContainers(EntityUid uid, ContainerManagerComponent? containerManager = null)
@@ -178,7 +228,7 @@ namespace Robust.Shared.Containers
if (!Resolve(uid, ref containerManager))
return new ContainerManagerComponent.AllContainersEnumerable();
return containerManager.GetAllContainers();
return new(containerManager);
}
#endregion
@@ -407,7 +457,7 @@ namespace Robust.Shared.Containers
{
if (((metaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) &&
conQuery.TryGetComponent(parent, out var conManager) &&
conManager.TryGetContainer(child, out var parentContainer))
TryGetContainingContainer(parent, child, out var parentContainer, conManager))
{
container = parentContainer;
}
@@ -436,9 +486,9 @@ namespace Robust.Shared.Containers
wasInContainer = true;
if (!force)
return container.Remove(entity);
return Remove(entity, container);
container.Remove(entity, EntityManager, force: true);
Remove(entity, container, force: true);
return true;
}
@@ -469,7 +519,7 @@ namespace Robust.Shared.Containers
var removed = new List<EntityUid>(container.ContainedEntities);
for (var i = removed.Count - 1; i >= 0; i--)
{
if (container.Remove(removed[i], EntityManager, reparent: reparent, force: force, destination: destination))
if (Remove(removed[i], container, reparent: reparent, force: force, destination: destination))
continue;
// failed to remove entity.
@@ -487,8 +537,10 @@ namespace Robust.Shared.Containers
{
foreach (var ent in container.ContainedEntities.ToArray())
{
if (Deleted(ent)) continue;
container.Remove(ent, EntityManager, force: true);
if (Deleted(ent))
continue;
Remove(ent, container, force: true);
Del(ent);
}
}
@@ -506,7 +558,8 @@ namespace Robust.Shared.Containers
private bool TryInsertIntoContainer(Entity<TransformComponent> transform, BaseContainer container)
{
if (container.Insert(transform)) return true;
if (Insert((transform.Owner, transform.Comp, null, null), container))
return true;
if (Transform(container.Owner).ParentUid.IsValid()
&& TryGetContainingContainer(container.Owner, out var newContainer))
@@ -540,7 +593,7 @@ namespace Robust.Shared.Containers
// Eject entities from their parent container if the parent change is done via setting the transform.
if (TryComp(message.OldParent, out ContainerManagerComponent? containerManager))
containerManager.Remove(message.Entity, message.Transform, meta, reparent: false, force: true);
RemoveEntity(message.OldParent.Value, message.Entity, containerManager, message.Transform, meta, reparent: false, force: true);
}
}
}

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