mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c89c529ba4 | ||
|
|
dd56de70b7 | ||
|
|
710408c613 | ||
|
|
2461cd94dd | ||
|
|
721408bb37 | ||
|
|
8b42c1dd46 | ||
|
|
688b0b0458 | ||
|
|
5fa49b5689 | ||
|
|
1f7a9bdf0a | ||
|
|
796abe1230 | ||
|
|
6c2cf26250 | ||
|
|
ce262d5ff8 | ||
|
|
c250010dad | ||
|
|
709c7bc808 | ||
|
|
37918da73c | ||
|
|
6cc2083b09 | ||
|
|
6a6bfe33ca | ||
|
|
9737a4249c | ||
|
|
a48a353939 | ||
|
|
f0b45d95cb | ||
|
|
d69c5500f2 | ||
|
|
cf133ca341 | ||
|
|
b0922b8e0e | ||
|
|
512ebd8422 | ||
|
|
85f74c3ba3 | ||
|
|
da7abc6580 | ||
|
|
b1329d30bf | ||
|
|
12808d073e | ||
|
|
ec794ce4e4 | ||
|
|
6b13475842 | ||
|
|
b48ee22800 | ||
|
|
0b95a4edeb | ||
|
|
ed359481b4 | ||
|
|
1189613908 | ||
|
|
30907d8415 | ||
|
|
7f2da4d4f3 | ||
|
|
e30e963623 | ||
|
|
b056caeed7 | ||
|
|
fbc8086335 | ||
|
|
799702b814 | ||
|
|
63df90f86f | ||
|
|
51f0c60bd3 | ||
|
|
a9ed53f47b | ||
|
|
41c40f1a94 | ||
|
|
6e61c35d35 | ||
|
|
aae0a8bc51 | ||
|
|
cb543240c6 | ||
|
|
1654ab06f5 | ||
|
|
211245215e | ||
|
|
10aaaa65c5 | ||
|
|
d2a2afe82e | ||
|
|
025d90d281 | ||
|
|
c229f2e312 | ||
|
|
fe051a3577 | ||
|
|
51a0ef1e60 | ||
|
|
702dfef5fc | ||
|
|
a0c1ad246f | ||
|
|
1153888bd1 | ||
|
|
ccbb6ddec7 | ||
|
|
970da5f717 | ||
|
|
4d528dd577 | ||
|
|
c83720b163 | ||
|
|
bd87a805d4 | ||
|
|
fff42fb2b4 | ||
|
|
4500669f65 | ||
|
|
7d19ea9338 | ||
|
|
2dc610907d | ||
|
|
beb1c4b1fb | ||
|
|
7e331eaa75 | ||
|
|
caf9e45ad9 | ||
|
|
7cb3aeccc2 | ||
|
|
ae83e606d6 | ||
|
|
d9d5ef7471 | ||
|
|
0f97f366a6 | ||
|
|
35ab0b8cc8 | ||
|
|
5a14e939bf | ||
|
|
ccba6b5d1c | ||
|
|
254a5987c7 | ||
|
|
8550056e68 | ||
|
|
25211e3781 | ||
|
|
3500abfd47 | ||
|
|
7d1915096a | ||
|
|
4504731588 | ||
|
|
701fa95a82 | ||
|
|
40a9048704 | ||
|
|
cee8d42776 | ||
|
|
3330d96177 | ||
|
|
4033d96327 | ||
|
|
6e0205d1a8 | ||
|
|
7cd95351c3 | ||
|
|
2a102f048f | ||
|
|
16bab1bc03 | ||
|
|
123d0ae6ac | ||
|
|
d72de032fa | ||
|
|
0fdba836ee |
@@ -7,6 +7,18 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
|
||||
max_line_length = 120
|
||||
|
||||
# ReSharper properties
|
||||
resharper_csharp_max_line_length = 120
|
||||
resharper_csharp_wrap_after_declaration_lpar = true
|
||||
resharper_csharp_wrap_arguments_style = chop_if_long
|
||||
resharper_csharp_wrap_parameters_style = chop_if_long
|
||||
resharper_keep_existing_attribute_arrangement = true
|
||||
resharper_place_field_attribute_on_same_line = if_owner_is_single_line
|
||||
resharper_wrap_chained_binary_patterns = chop_if_long
|
||||
resharper_wrap_chained_method_calls = chop_if_long
|
||||
|
||||
[*.{csproj,xml,yml,dll.config,targets,props}]
|
||||
indent_size = 2
|
||||
|
||||
|
||||
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
os: [ubuntu-latest, windows-latest ] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
||||
6
.github/workflows/publish-client.yml
vendored
6
.github/workflows/publish-client.yml
vendored
@@ -33,10 +33,10 @@ jobs:
|
||||
mkdir "release/${{ steps.parse_version.outputs.version }}"
|
||||
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
|
||||
|
||||
- name: Upload files to centcomm
|
||||
- name: Upload files to Suns
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: centcomm.spacestation14.io
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
source: "release/${{ steps.parse_version.outputs.version }}"
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Update manifest JSON
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: centcomm.spacestation14.io
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
We actually set ManagePackageVersionsCentrally manually in another import file.
|
||||
Since .NET SDK 8.0.300, ManagePackageVersionsCentrally is automatically set if Directory.Packages.props exists.
|
||||
https://github.com/NuGet/NuGet.Client/pull/5572
|
||||
We actively negate this here, as we have some packages in tree we don't want such automatic behavior for.
|
||||
We use Directory.Build.props to get copy the state *after* our MSBuild config but before Nuget's config.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
|
||||
Submodule Lidgren.Network/Lidgren.Network updated: 61a56c60bd...1d85b82e05
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
224
RELEASE-NOTES.md
224
RELEASE-NOTES.md
@@ -54,6 +54,230 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 223.3.0
|
||||
|
||||
### New features
|
||||
|
||||
* Better exception logging for IRobustJob.
|
||||
* Add SetGridAudio helper for SharedAudioSystem.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix placement manager not setting entity rotation correctly.
|
||||
* Fix grid-based audio not playing correctly.
|
||||
|
||||
|
||||
## 223.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added several new `FormattedMessage` methods for better exception tolerance when parsing markup. Several existing methods have been marked as obsolete, with new renamed methods taking their place.
|
||||
|
||||
|
||||
## 223.1.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* `MapGridComponent.LastTileModifiedTick` is now actually networked to clients.
|
||||
|
||||
|
||||
## 223.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed an exception caused by enum cvars using integer type values instead of enum values
|
||||
|
||||
|
||||
## 223.1.0
|
||||
|
||||
### Other
|
||||
|
||||
* Various `ContainerSystem` methods have been obsoleted in favour of overrides that take in an `Entity` struct instead of `EntityUid`
|
||||
* Various `EntityCoordinates` methods have been obsoleted with replacements added to `SharedTransformSystem`
|
||||
|
||||
|
||||
## 223.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The `ComponentState` class is now abstract. Networked components that don't have state information now just return a null state.
|
||||
* The way that delta component states work has changed. It now expects there to be two different state classes, only one of which should implement `IComponentDeltaState<TFullState>`
|
||||
|
||||
### New features
|
||||
|
||||
* A new `replay.checkpoint_min_interval` cvar has been added. It can be used to limit the frequency at which checkpoints are generated when loading a replay.
|
||||
* Added `IConfigurationManager.OnCVarValueChanged`. This is a c# event that gets invoked whenever any cvar value changes.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* `IEyeManager.GetWorldViewbounds()` and `IEyeManager.GetWorldViewbounds()` should now return the correct bounds if the main viewport does not take up the whole screen.
|
||||
|
||||
### Other
|
||||
|
||||
* The default values of various replay related cvars have been changed to try and reduce memory usage.
|
||||
|
||||
|
||||
## 222.4.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added the following types from `System.Numerics` to the sandbox: `Complex`, `Matrix3x2`, `Matrix4x4`, `Plane`, `Quaternion`, `Vector3`, `Vector4`.
|
||||
|
||||
|
||||
## 222.3.0
|
||||
|
||||
### New features
|
||||
|
||||
* `ITileDefinition.EditorHidden` allows hiding a tile from the tile spawn panel.
|
||||
* Ordered event subscriptions now take child types into account, so ordering based on a shared type will work.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Cross-map BUI range checks now work.
|
||||
* Paused entities update on prototype reload.
|
||||
|
||||
### Other
|
||||
|
||||
* Fixed build compatibility with .NET 8.0.300 SDK, due to changes in how Central Package Management behaves.
|
||||
* Physics component has delta states to reduce network usage.
|
||||
|
||||
|
||||
## 222.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added `EntityQuery.Comp()` (abbreviation of `GetComponent()`)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `SerializationManager.TryGetVariableType` checking the wrong property.
|
||||
* Fixed GrammarSystem mispredicting a character's gender
|
||||
|
||||
### Other
|
||||
|
||||
* User interface system now performs range checks in parallel
|
||||
|
||||
|
||||
## 222.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed never setting BoundUserInterface.State.
|
||||
|
||||
### Other
|
||||
|
||||
* Add truncate for filesaving.
|
||||
* Add method for getting the type of a data field by name from ISerializationManager.
|
||||
|
||||
|
||||
## 222.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added `BoundKeyEventArgs.IsRepeat`.
|
||||
* Added `net.lidgren_log_warning` and `net.lidgren_log_error` CVars.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix assert trip when holding repeatable keybinds.
|
||||
|
||||
### Other
|
||||
|
||||
* Updated Lidgren to v0.3.1. This should provide performance improvements if warning/error logs are disabled.
|
||||
|
||||
|
||||
## 222.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Mark IComponentFactory argument in EntityPrototype as mandatory.
|
||||
|
||||
### New features
|
||||
|
||||
* Add `EntProtoId<T>` to check for components on the attached entity as well.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix PVS iterating duplicate chunks for multiple viewsubscriptions.
|
||||
|
||||
### Other
|
||||
|
||||
* Defer clientside BUI opens if it's the first state that comes in.
|
||||
|
||||
|
||||
## 221.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add SetMapAudio helper to SharedAudioSystem to setup map-wide audio entities.
|
||||
* Add SetWorldRotNoLerp method to SharedTransformSystem to avoid client lerping.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* `SpriteComponent.CopyFrom` now copies `CopyToShaderParameters` configuration.
|
||||
|
||||
|
||||
## 221.1.0
|
||||
|
||||
|
||||
## 221.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `EntParentChangedMessage.OldMapId` is now an `EntityUid` instead of `MapId`
|
||||
* `TransformSystem.DetachParentToNull()` is being renamed to `DetachEntity`
|
||||
* The order in which `MoveEvent` handlers are invoked has been changed to prioritise engine subscriptions
|
||||
|
||||
### New features
|
||||
|
||||
* Added `UpdateHovered()` and `SetHovered()` to `IUserInterfaceManager`, for updating or modifying the currently hovered control.
|
||||
* Add SwapPositions to TransformSystem to swap two entity's transforms.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Improve client gamestate exception tolerance.
|
||||
|
||||
### Other
|
||||
|
||||
* If the currently hovered control is disposed, `UserInterfaceManager` will now look for a new control, rather than just setting the hovered control to null.
|
||||
|
||||
### Internal
|
||||
|
||||
* Use more `EntityQuery<T>` internally in EntityManager and PhysicsSystem.
|
||||
|
||||
|
||||
## 220.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* RSIs can now specify load parameters, mimicking the ones from `.png.yml`. Currently only disabling sRGB is supported.
|
||||
* Added a second UV channel to Clyde's vertex format. On regular batched sprite draws, this goes 0 -> 1 across the sprite quad.
|
||||
* Added a new `CopyToShaderParameters` system for `SpriteComponent` layers.
|
||||
|
||||
|
||||
## 220.1.0
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix client-side replay exceptions due to dropped states when recording.
|
||||
|
||||
### Other
|
||||
|
||||
* Remove IP + HWId from ViewVariables.
|
||||
* Close BUIs upon disconnect.
|
||||
|
||||
|
||||
## 220.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Refactor UserInterfaceSystem.
|
||||
- The API has been significantly cleaned up and standardised, most noticeably callers don't need to worry about TryGetUi and can rely on either HasUi, SetUiState, CloseUi, or OpenUi to handle their code as appropriate.
|
||||
- Interface data is now stored via key rather than as a flat list which is a breaking change for YAML.
|
||||
- BoundUserInterfaces can now be completely handled via Shared code. Existing Server-side callers will behave similarly to before.
|
||||
- BoundUserInterfaces now properly close in many more situations, additionally they are now attached to the entity so reconnecting can re-open them and they can be serialized properly.
|
||||
|
||||
|
||||
## 219.2.0
|
||||
|
||||
### New features
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
id: Audio
|
||||
name: Audio
|
||||
description: Audio entity used by engine
|
||||
save: false
|
||||
save: false
|
||||
components:
|
||||
- type: Transform
|
||||
gridTraversal: false
|
||||
|
||||
57
Robust.Analyzers.Tests/NoUncachedRegexAnalyzerTest.cs
Normal file
57
Robust.Analyzers.Tests/NoUncachedRegexAnalyzerTest.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing.Verifiers;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.NoUncachedRegexAnalyzer>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class NoUncachedRegexAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, NUnitVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
public static class Foo
|
||||
{
|
||||
public static void Bad()
|
||||
{
|
||||
Regex.Replace("foo", "bar", "baz");
|
||||
}
|
||||
|
||||
public static void Good()
|
||||
{
|
||||
var r = new Regex("bar");
|
||||
r.Replace("foo", "baz");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(7,9): warning RA0026: Usage of a static Regex function that takes in a pattern string. This can cause constant re-parsing of the pattern.
|
||||
VerifyCS.Diagnostic().WithSpan(7, 9, 7, 43)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
using Robust.Shared.Serialization.Manager.Definition;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
@@ -56,8 +57,18 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to add a setter."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
|
||||
Diagnostics.IdDataFieldRedundantTag,
|
||||
"Data field has redundant tag specified",
|
||||
"Data field {0} in data definition {1} has an explicitly set tag that matches autogenerated tag",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Info,
|
||||
true,
|
||||
"Make sure to remove the tag string from the data field attribute."
|
||||
);
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule
|
||||
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
|
||||
DataFieldRedundantTagRule
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
@@ -125,6 +136,11 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasRedundantTag(fieldSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +165,11 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasRedundantTag(propertySymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
|
||||
@@ -248,6 +269,29 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasRedundantTag(ISymbol symbol)
|
||||
{
|
||||
if (!IsDataField(symbol, out var _, out var attribute))
|
||||
return false;
|
||||
|
||||
// No args, no problem
|
||||
if (attribute.ConstructorArguments.Length == 0)
|
||||
return false;
|
||||
|
||||
// If a tag is explicitly specified, it will be the first argument...
|
||||
var tagArgument = attribute.ConstructorArguments[0];
|
||||
// ...but the first arg could also something else, since tag is optional
|
||||
// so we make sure that it's a string
|
||||
if (tagArgument.Value is not string explicitName)
|
||||
return false;
|
||||
|
||||
// Get the name that sourcegen would provide
|
||||
var automaticName = DataDefinitionUtility.AutoGenerateTag(symbol.Name);
|
||||
|
||||
// If the explicit name matches the sourcegen name, we have a redundancy
|
||||
return explicitName == automaticName;
|
||||
}
|
||||
|
||||
private static bool IsImplicitDataDefinition(ITypeSymbol type)
|
||||
{
|
||||
if (HasAttribute(type, ImplicitDataDefinitionNamespace))
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
@@ -16,8 +13,11 @@ namespace Robust.Analyzers;
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp)]
|
||||
public sealed class DefinitionFixer : CodeFixProvider
|
||||
{
|
||||
private const string DataFieldAttributeName = "DataField";
|
||||
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
|
||||
IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldWritable, IdDataFieldPropertyWritable
|
||||
IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldWritable, IdDataFieldPropertyWritable,
|
||||
IdDataFieldRedundantTag
|
||||
);
|
||||
|
||||
public override Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
@@ -34,6 +34,8 @@ public sealed class DefinitionFixer : CodeFixProvider
|
||||
return RegisterDataFieldFix(context, diagnostic);
|
||||
case IdDataFieldPropertyWritable:
|
||||
return RegisterDataFieldPropertyFix(context, diagnostic);
|
||||
case IdDataFieldRedundantTag:
|
||||
return RegisterRedundantTagFix(context, diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +74,68 @@ public sealed class DefinitionFixer : CodeFixProvider
|
||||
return document.WithSyntaxRoot(root);
|
||||
}
|
||||
|
||||
private static async Task RegisterRedundantTagFix(CodeFixContext context, Diagnostic diagnostic)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
var span = diagnostic.Location.SourceSpan;
|
||||
var token = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType<MemberDeclarationSyntax>().First();
|
||||
|
||||
if (token == null)
|
||||
return;
|
||||
|
||||
// Find the DataField attribute
|
||||
AttributeSyntax? dataFieldAttribute = null;
|
||||
foreach (var attributeList in token.AttributeLists)
|
||||
{
|
||||
foreach (var attribute in attributeList.Attributes)
|
||||
{
|
||||
if (attribute.Name.ToString() == DataFieldAttributeName)
|
||||
{
|
||||
dataFieldAttribute = attribute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dataFieldAttribute != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (dataFieldAttribute == null)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(CodeAction.Create(
|
||||
"Remove explicitly set tag",
|
||||
c => RemoveRedundantTag(context.Document, dataFieldAttribute, c),
|
||||
"Remove explicitly set tag"
|
||||
), diagnostic);
|
||||
}
|
||||
|
||||
private static async Task<Document> RemoveRedundantTag(Document document, AttributeSyntax syntax, CancellationToken cancellation)
|
||||
{
|
||||
var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation);
|
||||
|
||||
if (syntax.ArgumentList == null)
|
||||
return document;
|
||||
|
||||
AttributeSyntax? newSyntax;
|
||||
if (syntax.ArgumentList.Arguments.Count == 1)
|
||||
{
|
||||
// If this is the only argument, delete the ArgumentList so we don't leave empty parentheses
|
||||
newSyntax = syntax.RemoveNode(syntax.ArgumentList, SyntaxRemoveOptions.KeepNoTrivia);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove the first argument, which is the tag
|
||||
var newArgs = syntax.ArgumentList.Arguments.RemoveAt(0);
|
||||
var newArgList = syntax.ArgumentList.WithArguments(newArgs);
|
||||
// Construct a new attribute with the tag removed
|
||||
newSyntax = syntax.WithArgumentList(newArgList);
|
||||
}
|
||||
|
||||
root = root!.ReplaceNode(syntax, newSyntax!);
|
||||
|
||||
return document.WithSyntaxRoot(root);
|
||||
}
|
||||
|
||||
private static async Task RegisterDataFieldFix(CodeFixContext context, Diagnostic diagnostic)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
|
||||
66
Robust.Analyzers/NoUncachedRegexAnalyzer.cs
Normal file
66
Robust.Analyzers/NoUncachedRegexAnalyzer.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class NoUncachedRegexAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string RegexTypeName = "Regex";
|
||||
private const string RegexType = $"System.Text.RegularExpressions.{RegexTypeName}";
|
||||
|
||||
private static readonly DiagnosticDescriptor Rule = new (
|
||||
Diagnostics.IdUncachedRegex,
|
||||
"Use of uncached static Regex function",
|
||||
"Usage of a static Regex function that takes in a pattern string. This can cause constant re-parsing of the pattern.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||
|
||||
public static readonly HashSet<string> BadFunctions =
|
||||
[
|
||||
"Count",
|
||||
"EnumerateMatches",
|
||||
"IsMatch",
|
||||
"Match",
|
||||
"Matches",
|
||||
"Replace",
|
||||
"Split"
|
||||
];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.RegisterOperationAction(CheckInvocation, OperationKind.Invocation);
|
||||
}
|
||||
|
||||
private static void CheckInvocation(OperationAnalysisContext context)
|
||||
{
|
||||
if (context.Operation is not IInvocationOperation invocation)
|
||||
return;
|
||||
|
||||
// All Regex functions we care about are static.
|
||||
var targetMethod = invocation.TargetMethod;
|
||||
if (!targetMethod.IsStatic)
|
||||
return;
|
||||
|
||||
// Bail early.
|
||||
if (targetMethod.ContainingType.Name != "Regex")
|
||||
return;
|
||||
|
||||
var regexType = context.Compilation.GetTypeByMetadataName(RegexType);
|
||||
if (!SymbolEqualityComparer.Default.Equals(regexType, targetMethod.ContainingType))
|
||||
return;
|
||||
|
||||
if (!BadFunctions.Contains(targetMethod.Name))
|
||||
return;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.Syntax.GetLocation()));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@
|
||||
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for DataDefinitionAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Serialization\Manager\Definition\DataDefinitionUtility.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -239,6 +239,7 @@ namespace Robust.Build.Tasks
|
||||
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
|
||||
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
|
||||
}
|
||||
res.Remove();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
[Dependency] private readonly IParallelManager _parMan = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
[Dependency] private readonly IAudioInternal _audio = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly SharedMapSystem _maps = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
@@ -216,11 +216,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
}
|
||||
|
||||
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
|
||||
{
|
||||
_metadata.SetFlag(entity.Owner, MetaDataFlags.Undetachable, true);
|
||||
}
|
||||
|
||||
// Need to set all initial data for first frame.
|
||||
ApplyAudioParams(component.Params, component);
|
||||
component.Source.Global = component.Global;
|
||||
@@ -336,55 +331,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
Vector2 worldPos;
|
||||
var gridUid = xform.ParentUid;
|
||||
component.Volume = component.Params.Volume;
|
||||
Vector2 delta;
|
||||
|
||||
// Handle grid audio differently by using nearest-edge instead of entity centre.
|
||||
// Handle grid audio differently by using grid position.
|
||||
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
|
||||
{
|
||||
// It's our grid so max volume.
|
||||
if (_listenerGrid == gridUid)
|
||||
{
|
||||
component.Volume = component.Params.Volume;
|
||||
component.Occlusion = 0f;
|
||||
component.Position = listener.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Need a grid-optimised version because this is gonna be expensive.
|
||||
// Just to avoid clipping on and off grid or nearestPoint changing we'll
|
||||
// always set the sound to listener's pos, we'll just manually do gain ourselves.
|
||||
if (_physics.TryGetNearest(gridUid, listener, out _, out var gridDistance))
|
||||
{
|
||||
// Out of range
|
||||
if (gridDistance > component.MaxDistance)
|
||||
{
|
||||
component.Gain = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
var paramsGain = VolumeToGain(component.Params.Volume);
|
||||
|
||||
// Thought I'd never have to manually calculate gain again but this is the least
|
||||
// unpleasant audio I could get at the moment.
|
||||
component.Gain = paramsGain * _audio.GetAttenuationGain(
|
||||
gridDistance,
|
||||
component.Params.RolloffFactor,
|
||||
component.Params.ReferenceDistance,
|
||||
component.Params.MaxDistance);
|
||||
component.Position = listener.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't get nearest point so don't play anymore.
|
||||
component.Gain = 0f;
|
||||
return;
|
||||
var parentUid = xform.ParentUid;
|
||||
worldPos = _maps.GetGridPosition(parentUid);
|
||||
}
|
||||
else
|
||||
{
|
||||
worldPos = _xformSys.GetWorldPosition(entity);
|
||||
}
|
||||
|
||||
worldPos = _xformSys.GetWorldPosition(entity);
|
||||
component.Volume = component.Params.Volume;
|
||||
|
||||
// Max distance check
|
||||
var delta = worldPos - listener.Position;
|
||||
delta = worldPos - listener.Position;
|
||||
var distance = delta.Length();
|
||||
|
||||
// Out of range so just clip it for us.
|
||||
@@ -403,8 +365,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
// Update audio occlusion
|
||||
var occlusion = GetOcclusion(listener, delta, distance, entity);
|
||||
component.Occlusion = occlusion;
|
||||
if ((component.Flags & AudioFlags.NoOcclusion) == AudioFlags.NoOcclusion)
|
||||
{
|
||||
component.Occlusion = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
var occlusion = GetOcclusion(listener, delta, distance, entity);
|
||||
component.Occlusion = occlusion;
|
||||
}
|
||||
|
||||
// Update audio positions.
|
||||
component.Position = worldPos;
|
||||
@@ -671,10 +640,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
|
||||
{
|
||||
var audioP = audioParams ?? AudioParams.Default;
|
||||
var entity = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
|
||||
var comp = SetupAudio(entity, null, audioP, stream.Length);
|
||||
LoadStream((entity, comp), stream);
|
||||
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
|
||||
LoadStream(entity, stream);
|
||||
EntityManager.InitializeAndStartEntity(entity);
|
||||
var comp = entity.Comp;
|
||||
var source = comp.Source;
|
||||
|
||||
// TODO clamp the offset inside of SetPlaybackPosition() itself.
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace Robust.Client.Debugging
|
||||
{
|
||||
var viewBounds = args.WorldBounds;
|
||||
var viewAABB = args.WorldAABB;
|
||||
var mapId = _eyeManager.CurrentMap;
|
||||
var mapId = args.MapId;
|
||||
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.Shapes) != 0)
|
||||
{
|
||||
@@ -373,7 +373,7 @@ namespace Robust.Client.Debugging
|
||||
|
||||
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
|
||||
{
|
||||
var mapId = _eyeManager.CurrentMap;
|
||||
var mapId = args.MapId;
|
||||
var mousePos = _inputManager.MouseScreenPosition;
|
||||
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ShapeInfo) != 0x0)
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Animations;
|
||||
@@ -28,6 +29,7 @@ using static Robust.Client.ComponentTrees.SpriteTreeSystem;
|
||||
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
|
||||
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
|
||||
using Direction = Robust.Shared.Maths.Direction;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -770,15 +772,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
foreach (var keyString in layerDatum.MapKeys)
|
||||
{
|
||||
object key;
|
||||
if (reflection.TryParseEnumReference(keyString, out var @enum))
|
||||
{
|
||||
key = @enum;
|
||||
}
|
||||
else
|
||||
{
|
||||
key = keyString;
|
||||
}
|
||||
var key = ParseKey(keyString);
|
||||
|
||||
if (LayerMap.TryGetValue(key, out var mappedIndex))
|
||||
{
|
||||
@@ -804,9 +798,30 @@ namespace Robust.Client.GameObjects
|
||||
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
|
||||
layer.Visible = layerDatum.Visible ?? layer.Visible;
|
||||
|
||||
if (layerDatum.CopyToShaderParameters is { } copyParameters)
|
||||
{
|
||||
layer.CopyToShaderParameters = new CopyToShaderParameters(ParseKey(copyParameters.LayerKey))
|
||||
{
|
||||
ParameterTexture = copyParameters.ParameterTexture,
|
||||
ParameterUV = copyParameters.ParameterUV
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
layer.CopyToShaderParameters = null;
|
||||
}
|
||||
|
||||
RebuildBounds();
|
||||
}
|
||||
|
||||
private object ParseKey(string keyString)
|
||||
{
|
||||
if (reflection.TryParseEnumReference(keyString, out var @enum))
|
||||
return @enum;
|
||||
|
||||
return keyString;
|
||||
}
|
||||
|
||||
public void LayerSetData(object layerKey, PrototypeLayerData data)
|
||||
{
|
||||
if (!LayerMapTryGet(layerKey, out var layer, true))
|
||||
@@ -1635,6 +1650,9 @@ namespace Robust.Client.GameObjects
|
||||
[ViewVariables]
|
||||
public LayerRenderingStrategy RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public CopyToShaderParameters? CopyToShaderParameters;
|
||||
|
||||
public Layer(SpriteComponent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
@@ -1663,6 +1681,8 @@ namespace Robust.Client.GameObjects
|
||||
DirOffset = toClone.DirOffset;
|
||||
_autoAnimated = toClone._autoAnimated;
|
||||
RenderingStrategy = toClone.RenderingStrategy;
|
||||
if (toClone.CopyToShaderParameters is { } copyToShaderParameters)
|
||||
CopyToShaderParameters = new CopyToShaderParameters(copyToShaderParameters);
|
||||
}
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
@@ -2007,8 +2027,6 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// Set the drawing transform for this layer
|
||||
GetLayerDrawMatrix(dir, out var layerMatrix);
|
||||
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
|
||||
drawingHandle.SetTransform(in transformMatrix);
|
||||
|
||||
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
|
||||
// due to direction overrides or offsets.
|
||||
@@ -2018,7 +2036,41 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// Get the correct directional texture from the state, and draw it!
|
||||
var texture = GetRenderTexture(_actualState, dir);
|
||||
RenderTexture(drawingHandle, texture);
|
||||
|
||||
if (CopyToShaderParameters == null)
|
||||
{
|
||||
// Set the drawing transform for this layer
|
||||
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
|
||||
drawingHandle.SetTransform(in transformMatrix);
|
||||
|
||||
RenderTexture(drawingHandle, texture);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multiple atrocities to god being committed right here.
|
||||
var otherLayerIdx = _parent.LayerMap[CopyToShaderParameters.LayerKey!];
|
||||
var otherLayer = _parent.Layers[otherLayerIdx];
|
||||
if (otherLayer.Shader is not { } shader)
|
||||
{
|
||||
// No shader set apparently..?
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shader.Mutable)
|
||||
otherLayer.Shader = shader = shader.Duplicate();
|
||||
|
||||
var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
|
||||
var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);
|
||||
|
||||
if (CopyToShaderParameters.ParameterTexture is { } paramTexture)
|
||||
shader.SetParameter(paramTexture, clydeTexture);
|
||||
|
||||
if (CopyToShaderParameters.ParameterUV is { } paramUV)
|
||||
{
|
||||
var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
|
||||
shader.SetParameter(paramUV, uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture)
|
||||
@@ -2096,6 +2148,23 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiated version of <see cref="PrototypeCopyToShaderParameters"/>.
|
||||
/// Has <see cref="LayerKey"/> actually resolved to a a real key.
|
||||
/// </summary>
|
||||
public sealed class CopyToShaderParameters(object layerKey)
|
||||
{
|
||||
public object LayerKey = layerKey;
|
||||
public string? ParameterTexture;
|
||||
public string? ParameterUV;
|
||||
|
||||
public CopyToShaderParameters(CopyToShaderParameters toClone) : this(toClone.LayerKey)
|
||||
{
|
||||
ParameterTexture = toClone.ParameterTexture;
|
||||
ParameterUV = toClone.ParameterUV;
|
||||
}
|
||||
}
|
||||
|
||||
void IAnimationProperties.SetAnimatableProperty(string name, object value)
|
||||
{
|
||||
if (!name.StartsWith("layer/"))
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
var type = _serializer.FindSerializedType(typeof(BaseContainer), data.ContainerType);
|
||||
container = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type!, inject:false);
|
||||
InitContainer(container, (uid, component), id);
|
||||
container.Init(this, id, (uid, component));
|
||||
component.Containers.Add(id, container);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var map = _eyeManager.CurrentMap;
|
||||
var map = args.MapId;
|
||||
if (map == MapId.Nullspace) return;
|
||||
|
||||
foreach (var (_, treeComp) in _trees.GetIntersectingTrees(map, args.WorldBounds))
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var currentMap = _eyeManager.CurrentMap;
|
||||
var currentMap = args.MapId;
|
||||
var viewport = args.WorldBounds;
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
|
||||
@@ -1,84 +1,8 @@
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using System;
|
||||
using UserInterfaceComponent = Robust.Shared.GameObjects.UserInterfaceComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<BoundUIWrapMessage>(MessageReceived);
|
||||
}
|
||||
|
||||
private void MessageReceived(BoundUIWrapMessage ev)
|
||||
{
|
||||
var uid = GetEntity(ev.Entity);
|
||||
|
||||
if (!TryComp<UserInterfaceComponent>(uid, out var cmp))
|
||||
return;
|
||||
|
||||
var uiKey = ev.UiKey;
|
||||
var message = ev.Message;
|
||||
message.Session = _playerManager.LocalSession!;
|
||||
message.Entity = GetNetEntity(uid);
|
||||
message.UiKey = uiKey;
|
||||
|
||||
// Raise as object so the correct type is used.
|
||||
RaiseLocalEvent(uid, (object)message, true);
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case OpenBoundInterfaceMessage _:
|
||||
TryOpenUi(uid, uiKey, cmp);
|
||||
break;
|
||||
|
||||
case CloseBoundInterfaceMessage _:
|
||||
TryCloseUi(message.Session, uid, uiKey, remoteCall: true, uiComp: cmp);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (cmp.OpenInterfaces.TryGetValue(uiKey, out var bui))
|
||||
bui.InternalReceiveMessage(message);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryOpenUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? uiComp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref uiComp))
|
||||
return false;
|
||||
|
||||
if (uiComp.OpenInterfaces.ContainsKey(uiKey))
|
||||
return false;
|
||||
|
||||
var data = uiComp.MappedInterfaceData[uiKey];
|
||||
|
||||
// TODO: This type should be cached, but I'm too lazy.
|
||||
var type = _reflectionManager.LooseGetType(data.ClientType);
|
||||
var boundInterface =
|
||||
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {uid, uiKey});
|
||||
|
||||
boundInterface.Open();
|
||||
uiComp.OpenInterfaces[uiKey] = boundInterface;
|
||||
|
||||
if (_playerManager.LocalSession is { } playerSession)
|
||||
{
|
||||
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
|
||||
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ namespace Robust.Client.GameStates
|
||||
private readonly HashSet<NetEntity> _stateEnts = new();
|
||||
private readonly List<EntityUid> _toDelete = new();
|
||||
private readonly List<IComponent> _toRemove = new();
|
||||
private readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState>> _outputData = new();
|
||||
private readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> _outputData = new();
|
||||
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
|
||||
|
||||
private readonly ObjectPool<Dictionary<ushort, IComponentState>> _compDataPool =
|
||||
new DefaultObjectPool<Dictionary<ushort, IComponentState>>(new DictPolicy<ushort, IComponentState>(), 256);
|
||||
private readonly ObjectPool<Dictionary<ushort, IComponentState?>> _compDataPool =
|
||||
new DefaultObjectPool<Dictionary<ushort, IComponentState?>>(new DictPolicy<ushort, IComponentState?>(), 256);
|
||||
|
||||
private uint _metaCompNetId;
|
||||
|
||||
@@ -125,6 +125,8 @@ namespace Robust.Client.GameStates
|
||||
#endif
|
||||
|
||||
private bool _resettingPredictedEntities;
|
||||
private readonly List<EntityUid> _brokenEnts = new();
|
||||
private readonly List<(EntityUid, NetEntity)> _toStart = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
@@ -257,7 +259,7 @@ namespace Robust.Client.GameStates
|
||||
public void UpdateFullRep(GameState state, bool cloneDelta = false)
|
||||
=> _processor.UpdateFullRep(state, cloneDelta);
|
||||
|
||||
public Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep()
|
||||
public Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> GetFullRep()
|
||||
=> _processor.GetFullRep();
|
||||
|
||||
private void HandlePvsLeaveMessage(MsgStateLeavePvs message)
|
||||
@@ -598,8 +600,12 @@ namespace Robust.Client.GameStates
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
|
||||
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
if (compState != null)
|
||||
{
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
}
|
||||
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
}
|
||||
@@ -631,8 +637,12 @@ namespace Robust.Client.GameStates
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was removed: {comp.GetType()}");
|
||||
|
||||
var stateEv = new ComponentHandleState(state, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
|
||||
if (state != null)
|
||||
{
|
||||
var stateEv = new ComponentHandleState(state, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
|
||||
}
|
||||
|
||||
comp.ClearCreationTick(); // don't undo the re-adding.
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
@@ -667,7 +677,16 @@ namespace Robust.Client.GameStates
|
||||
|
||||
foreach (var netEntity in createdEntities)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
|
||||
{
|
||||
_sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
var (_, meta) = _entityManager.GetEntityData(netEntity);
|
||||
#endif
|
||||
|
||||
var compData = _compDataPool.Get();
|
||||
_outputData.Add(netEntity, compData);
|
||||
|
||||
@@ -676,7 +695,7 @@ namespace Robust.Client.GameStates
|
||||
DebugTools.Assert(component.NetSyncEnabled);
|
||||
|
||||
var state = _entityManager.GetComponentState(bus, component, null, GameTick.Zero);
|
||||
DebugTools.Assert(state is not IComponentDeltaState delta || delta.FullState);
|
||||
DebugTools.Assert(state is not IComponentDeltaState);
|
||||
compData.Add(netId, state);
|
||||
}
|
||||
}
|
||||
@@ -876,9 +895,22 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
foreach (var (entity, data) in _toApply)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
HandleEntityState(entity, data.NetEntity, data.Meta, _entities.EventBus, data.curState,
|
||||
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
|
||||
|
||||
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(entity)}. Exception: {e}");
|
||||
_entityManager.DeleteEntity(entity);
|
||||
RequestFullState();
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
if (!data.EnteringPvs)
|
||||
continue;
|
||||
|
||||
@@ -917,7 +949,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessDeletions(delSpan, xforms, xformSys);
|
||||
ProcessDeletions(delSpan, xforms, metas, xformSys);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -962,6 +994,7 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
var xforms = _entities.GetEntityQuery<TransformComponent>();
|
||||
var metas = _entities.GetEntityQuery<MetaDataComponent>();
|
||||
var xformSys = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
|
||||
|
||||
_toDelete.Clear();
|
||||
@@ -990,12 +1023,12 @@ namespace Robust.Client.GameStates
|
||||
|
||||
// This entity is going to get deleted, but maybe some if its children won't be, so lets detach them to
|
||||
// null. First we will detach the parent in order to reduce the number of broadphase/lookup updates.
|
||||
xformSys.DetachParentToNull(ent, xform);
|
||||
xformSys.DetachEntity(ent, xform);
|
||||
|
||||
// Then detach all children.
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
|
||||
xformSys.DetachEntity(child, xforms.Get(child), metas.Get(child), xform);
|
||||
|
||||
if (deleteClientChildren
|
||||
&& !deleteClientEntities // don't add duplicates
|
||||
@@ -1014,9 +1047,9 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDeletions(
|
||||
ReadOnlySpan<NetEntity> delSpan,
|
||||
private void ProcessDeletions(ReadOnlySpan<NetEntity> delSpan,
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
SharedTransformSystem xformSys)
|
||||
{
|
||||
// Processing deletions is non-trivial, because by default deletions will also delete all child entities.
|
||||
@@ -1043,13 +1076,13 @@ namespace Robust.Client.GameStates
|
||||
continue; // Already deleted? or never sent to us?
|
||||
|
||||
// First, a single recursive map change
|
||||
xformSys.DetachParentToNull(id.Value, xform);
|
||||
xformSys.DetachEntity(id.Value, xform);
|
||||
|
||||
// Then detach all children.
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
{
|
||||
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
|
||||
xformSys.DetachEntity(child, xforms.Get(child), metas.Get(child), xform);
|
||||
}
|
||||
|
||||
// Finally, delete the entity.
|
||||
@@ -1144,7 +1177,7 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
meta._flags |= MetaDataFlags.Detached;
|
||||
xformSys.DetachParentToNull(ent.Value, xform);
|
||||
xformSys.DetachEntity(ent.Value, xform);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
|
||||
|
||||
if (container != null)
|
||||
@@ -1157,63 +1190,58 @@ namespace Robust.Client.GameStates
|
||||
|
||||
private void InitializeAndStart(Dictionary<NetEntity, EntityState> toCreate)
|
||||
{
|
||||
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
|
||||
_toStart.Clear();
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
var brokenEnts = new List<EntityUid>();
|
||||
#endif
|
||||
using (_prof.Group("Initialize Entity"))
|
||||
{
|
||||
EntityUid entity = default;
|
||||
foreach (var netEntity in toCreate.Keys)
|
||||
{
|
||||
var entity = _entityManager.GetEntity(netEntity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
_entities.InitializeEntity(entity, metaQuery.GetComponent(entity));
|
||||
#if EXCEPTION_TOLERANCE
|
||||
(entity, var meta) = _entityManager.GetEntityData(netEntity);
|
||||
_entities.InitializeEntity(entity, meta);
|
||||
_toStart.Add((entity, netEntity));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Init: ent={_entities.ToPrettyString(entity)}");
|
||||
_sawmill.Error($"Server entity threw in Init: nent={netEntity}, ent={_entities.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
brokenEnts.Add(entity);
|
||||
toCreate.Remove(netEntity);
|
||||
}
|
||||
_toCreate.Remove(netEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (_prof.Group("Start Entity"))
|
||||
{
|
||||
foreach (var netEntity in toCreate.Keys)
|
||||
foreach (var (entity, netEntity) in _toStart)
|
||||
{
|
||||
var entity = _entityManager.GetEntity(netEntity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
_entities.StartEntity(entity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
_entities.StartEntity(entity);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Start: ent={_entityManager.ToPrettyString(entity)}");
|
||||
_sawmill.Error($"Server entity threw in Start: nent={netEntity}, ent={_entityManager.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
brokenEnts.Add(entity);
|
||||
toCreate.Remove(netEntity);
|
||||
}
|
||||
_toCreate.Remove(netEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
foreach (var entity in brokenEnts)
|
||||
foreach (var entity in _brokenEnts)
|
||||
{
|
||||
_entityManager.DeleteEntity(entity);
|
||||
}
|
||||
#endif
|
||||
_brokenEnts.Clear();
|
||||
}
|
||||
|
||||
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
|
||||
@@ -1329,6 +1357,9 @@ namespace Robust.Client.GameStates
|
||||
|
||||
foreach (var (comp, cur, next) in _compStateWork.Values)
|
||||
{
|
||||
if (cur == null && next == null)
|
||||
continue;
|
||||
|
||||
var handleState = new ComponentHandleState(cur, next);
|
||||
bus.RaiseComponentEvent(comp, ref handleState);
|
||||
}
|
||||
@@ -1402,7 +1433,7 @@ namespace Robust.Client.GameStates
|
||||
containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container);
|
||||
}
|
||||
|
||||
_entities.EntitySysManager.GetEntitySystem<TransformSystem>().DetachParentToNull(uid, xform);
|
||||
_entities.EntitySysManager.GetEntitySystem<TransformSystem>().DetachEntity(uid, xform);
|
||||
|
||||
if (container != null)
|
||||
containerSys.AddExpectedEntity(_entities.GetNetEntity(uid), container);
|
||||
@@ -1481,6 +1512,9 @@ namespace Robust.Client.GameStates
|
||||
_entityManager.AddComponent(uid, comp, true, meta);
|
||||
}
|
||||
|
||||
if (state == null)
|
||||
continue;
|
||||
|
||||
var handleState = new ComponentHandleState(state, null);
|
||||
_entityManager.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Robust.Client.GameStates
|
||||
/// <summary>
|
||||
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
|
||||
/// </summary>
|
||||
internal readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState>> _lastStateFullRep
|
||||
internal readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> _lastStateFullRep
|
||||
= new();
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -212,7 +212,7 @@ Had full state: {LastFullState != null}"
|
||||
{
|
||||
if (!_lastStateFullRep.TryGetValue(entityState.NetEntity, out var compData))
|
||||
{
|
||||
compData = new Dictionary<ushort, IComponentState>();
|
||||
compData = new();
|
||||
_lastStateFullRep.Add(entityState.NetEntity, compData);
|
||||
}
|
||||
|
||||
@@ -221,21 +221,20 @@ Had full state: {LastFullState != null}"
|
||||
var compState = change.State;
|
||||
|
||||
if (compState is IComponentDeltaState delta
|
||||
&& !delta.FullState
|
||||
&& compData.TryGetValue(change.NetID, out var old)) // May fail if relying on implicit data
|
||||
{
|
||||
DebugTools.Assert(old is IComponentDeltaState oldDelta && oldDelta.FullState, "last state is not a full state");
|
||||
DebugTools.Assert(old is not IComponentDeltaState, "last state is not a full state");
|
||||
|
||||
if (cloneDelta)
|
||||
{
|
||||
compState = delta.CreateNewFullState(old);
|
||||
compState = delta.CreateNewFullState(old!);
|
||||
}
|
||||
else
|
||||
{
|
||||
delta.ApplyToFullState(old);
|
||||
delta.ApplyToFullState(old!);
|
||||
compState = old;
|
||||
}
|
||||
DebugTools.Assert(compState is IComponentDeltaState newState && newState.FullState, "newly constructed state is not a full state");
|
||||
DebugTools.Assert(compState is not IComponentDeltaState, "newly constructed state is not a full state");
|
||||
}
|
||||
|
||||
compData[change.NetID] = compState;
|
||||
@@ -391,7 +390,7 @@ Had full state: {LastFullState != null}"
|
||||
LastFullStateRequested = null;
|
||||
}
|
||||
|
||||
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState>> implicitData)
|
||||
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> implicitData)
|
||||
{
|
||||
foreach (var (netEntity, implicitEntState) in implicitData)
|
||||
{
|
||||
@@ -399,6 +398,7 @@ Had full state: {LastFullState != null}"
|
||||
|
||||
foreach (var (netId, implicitCompState) in implicitEntState)
|
||||
{
|
||||
DebugTools.Assert(implicitCompState is not IComponentDeltaState);
|
||||
ref var serverState = ref CollectionsMarshal.GetValueRefOrAddDefault(fullRep, netId, out var exists);
|
||||
|
||||
if (!exists)
|
||||
@@ -407,36 +407,32 @@ Had full state: {LastFullState != null}"
|
||||
continue;
|
||||
}
|
||||
|
||||
if (serverState is not IComponentDeltaState serverDelta || serverDelta.FullState)
|
||||
if (serverState is not IComponentDeltaState serverDelta)
|
||||
continue;
|
||||
|
||||
DebugTools.AssertNotNull(implicitCompState);
|
||||
|
||||
// Server sent an initial delta state. This is fine as long as the client can infer an initial full
|
||||
// state from the entity prototype.
|
||||
if (implicitCompState is not IComponentDeltaState implicitDelta || !implicitDelta.FullState)
|
||||
{
|
||||
_logger.Error($"Server sent delta state and client failed to construct an implicit full state for entity {netEntity}");
|
||||
continue;
|
||||
}
|
||||
|
||||
serverDelta.ApplyToFullState(implicitCompState);
|
||||
serverDelta.ApplyToFullState(implicitCompState!);
|
||||
serverState = implicitCompState;
|
||||
DebugTools.Assert(implicitCompState is IComponentDeltaState d && d.FullState);
|
||||
DebugTools.Assert(serverState is not IComponentDeltaState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<ushort, IComponentState> GetLastServerStates(NetEntity netEntity)
|
||||
public Dictionary<ushort, IComponentState?> GetLastServerStates(NetEntity netEntity)
|
||||
{
|
||||
return _lastStateFullRep[netEntity];
|
||||
}
|
||||
|
||||
public Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep()
|
||||
public Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> GetFullRep()
|
||||
{
|
||||
return _lastStateFullRep;
|
||||
}
|
||||
|
||||
public bool TryGetLastServerStates(NetEntity entity,
|
||||
[NotNullWhen(true)] out Dictionary<ushort, IComponentState>? dictionary)
|
||||
[NotNullWhen(true)] out Dictionary<ushort, IComponentState?>? dictionary)
|
||||
{
|
||||
return _lastStateFullRep.TryGetValue(entity, out dictionary);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace Robust.Client.GameStates
|
||||
/// <summary>
|
||||
/// Returns the full collection of cached game states that are used to reset predicted entities.
|
||||
/// </summary>
|
||||
Dictionary<NetEntity, Dictionary<ushort, IComponentState>> GetFullRep();
|
||||
Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> GetFullRep();
|
||||
|
||||
/// <summary>
|
||||
/// This will perform some setup in order to reset the game to an earlier state. To fully reset the state
|
||||
|
||||
@@ -83,13 +83,13 @@ namespace Robust.Client.GameStates
|
||||
/// The data to merge.
|
||||
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
|
||||
/// </param>
|
||||
void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState>> data);
|
||||
void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> data);
|
||||
|
||||
/// <summary>
|
||||
/// Get the last state data from the server for an entity.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary (net ID -> ComponentState)</returns>
|
||||
Dictionary<ushort, IComponentState> GetLastServerStates(NetEntity entity);
|
||||
Dictionary<ushort, IComponentState?> GetLastServerStates(NetEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the number of applicable states in the game state buffer from a given tick.
|
||||
@@ -99,6 +99,6 @@ namespace Robust.Client.GameStates
|
||||
int GetApplicableStateCount(GameTick? fromTick);
|
||||
|
||||
bool TryGetLastServerStates(NetEntity entity,
|
||||
[NotNullWhen(true)] out Dictionary<ushort, IComponentState>? dictionary);
|
||||
[NotNullWhen(true)] out Dictionary<ushort, IComponentState?>? dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Robust.Client.GameStates
|
||||
while (query.MoveNext(out var uid, out var transform))
|
||||
{
|
||||
// if not on the same map, continue
|
||||
if (transform.MapID != _eyeManager.CurrentMap || _container.IsEntityInContainer(uid))
|
||||
if (transform.MapID != args.MapId || _container.IsEntityInContainer(uid))
|
||||
continue;
|
||||
|
||||
if (transform.GridUid == uid)
|
||||
|
||||
@@ -64,29 +64,22 @@ namespace Robust.Client.Graphics
|
||||
/// <inheritdoc />
|
||||
public Box2 GetWorldViewport()
|
||||
{
|
||||
var vpSize = _displayManager.ScreenSize;
|
||||
|
||||
var topLeft = ScreenToMap(Vector2.Zero);
|
||||
var topRight = ScreenToMap(new Vector2(vpSize.X, 0));
|
||||
var bottomRight = ScreenToMap(vpSize);
|
||||
var bottomLeft = ScreenToMap(new Vector2(0, vpSize.Y));
|
||||
|
||||
var left = MathHelper.Min(topLeft.X, topRight.X, bottomRight.X, bottomLeft.X);
|
||||
var bottom = MathHelper.Min(topLeft.Y, topRight.Y, bottomRight.Y, bottomLeft.Y);
|
||||
var right = MathHelper.Max(topLeft.X, topRight.X, bottomRight.X, bottomLeft.X);
|
||||
var top = MathHelper.Max(topLeft.Y, topRight.Y, bottomRight.Y, bottomLeft.Y);
|
||||
|
||||
return new Box2(left, bottom, right, top);
|
||||
return GetWorldViewbounds().CalcBoundingBox();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Box2Rotated GetWorldViewbounds()
|
||||
{
|
||||
var vpSize = _displayManager.ScreenSize;
|
||||
// This is an inefficient and roundabout way of geting the viewport.
|
||||
// But its a method that shouldn't get used much.
|
||||
|
||||
var vp = MainViewport as Control;
|
||||
var vpSize = vp?.PixelSize ?? _displayManager.ScreenSize;
|
||||
|
||||
var topRight = ScreenToMap(new Vector2(vpSize.X, 0)).Position;
|
||||
var bottomLeft = ScreenToMap(new Vector2(0, vpSize.Y)).Position;
|
||||
|
||||
// This assumes the main viewports eye and the main eye are the same.
|
||||
var rotation = new Angle(CurrentEye.Rotation);
|
||||
var center = (bottomLeft + topRight) / 2;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Map;
|
||||
@@ -13,26 +14,29 @@ namespace Robust.Client.Graphics
|
||||
public interface IEyeManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The current eye that is being used to render the game.
|
||||
/// The primary eye, which is usually the eye associated with the main viewport.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Generally, you should avoid using this whenever possible. E.g., when rendering overlays should use the
|
||||
/// eye & viewbounds that gets passed to the draw method.
|
||||
/// Setting this property to null will use the default eye.
|
||||
/// </remarks>
|
||||
IEye CurrentEye { get; set; }
|
||||
|
||||
IViewportControl MainViewport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the map on which the current eye is "placed".
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
MapId CurrentMap { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A world-space box that is at LEAST the area covered by the viewport.
|
||||
/// A world-space box that is at LEAST the area covered by the main viewport.
|
||||
/// May be larger due to say rotation.
|
||||
/// </summary>
|
||||
Box2 GetWorldViewport();
|
||||
|
||||
/// <summary>
|
||||
/// A world-space box of the area visible in the main viewport.
|
||||
/// </summary>
|
||||
Box2Rotated GetWorldViewbounds();
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +47,7 @@ namespace Robust.Client.Graphics
|
||||
void GetScreenProjectionMatrix(out Matrix3 projMatrix);
|
||||
|
||||
/// <summary>
|
||||
/// Projects a point from world space to UI screen space using the current camera.
|
||||
/// Projects a point from world space to UI screen space using the main viewport.
|
||||
/// </summary>
|
||||
/// <param name="point">Point in world to transform.</param>
|
||||
/// <returns>Corresponding point in UI screen space.</returns>
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
("aPos", 0),
|
||||
("tCoord", 1),
|
||||
("modulate", 2)
|
||||
("tCoord2", 2),
|
||||
("modulate", 3)
|
||||
};
|
||||
|
||||
private const int UniIModUV = 0;
|
||||
|
||||
@@ -480,7 +480,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var worldBounds = CalcWorldBounds(viewport);
|
||||
var worldAABB = worldBounds.CalcBoundingBox();
|
||||
|
||||
if (_eyeManager.CurrentMap != MapId.Nullspace)
|
||||
if (eye.Position.MapId != MapId.Nullspace)
|
||||
{
|
||||
using (DebugGroup("Lights"))
|
||||
using (_prof.Group("Lights"))
|
||||
@@ -544,9 +544,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
UIBox2.FromDimensions(Vector2.Zero, viewport.Size), new Color(1, 1, 1, 0.5f));
|
||||
}
|
||||
|
||||
using (_prof.Group("Overlays WS"))
|
||||
if (eye.Position.MapId != MapId.Nullspace)
|
||||
{
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB, worldBounds);
|
||||
using (_prof.Group("Overlays WS"))
|
||||
{
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB, worldBounds);
|
||||
}
|
||||
}
|
||||
|
||||
_currentViewport = oldVp;
|
||||
|
||||
@@ -23,9 +23,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Texture Coords.
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(1);
|
||||
// Colour Modulation.
|
||||
GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
|
||||
// Texture Coords (2).
|
||||
GL.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(2);
|
||||
// Colour Modulation.
|
||||
GL.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 6 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(3);
|
||||
}
|
||||
|
||||
// NOTE: This is:
|
||||
@@ -37,6 +40,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
public readonly Vector2 Position;
|
||||
public readonly Vector2 TextureCoordinates;
|
||||
public readonly Vector2 TextureCoordinates2;
|
||||
// Note that this color is in linear space.
|
||||
public readonly Color Modulate;
|
||||
|
||||
@@ -48,6 +52,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Modulate = modulate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(Vector2 position, Vector2 textureCoordinates, Vector2 textureCoordinates2, Color modulate)
|
||||
{
|
||||
Position = position;
|
||||
TextureCoordinates = textureCoordinates;
|
||||
TextureCoordinates2 = textureCoordinates2;
|
||||
Modulate = modulate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(float x, float y, float u, float v, float r, float g, float b, float a)
|
||||
: this(new Vector2(x, y), new Vector2(u, v), new Color(r, g, b, a))
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
private RenderHandle _renderHandle = default!;
|
||||
|
||||
private sealed class RenderHandle : IRenderHandle
|
||||
internal sealed class RenderHandle : IRenderHandle
|
||||
{
|
||||
private readonly Clyde _clyde;
|
||||
private readonly IEntityManager _entities;
|
||||
@@ -88,16 +88,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
|
||||
|
||||
var (w, h) = clydeTexture.Size;
|
||||
var sr = new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
|
||||
var sr = WorldTextureBoundsToUV(clydeTexture, csr);
|
||||
|
||||
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
|
||||
}
|
||||
|
||||
internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr)
|
||||
{
|
||||
var (w, h) = texture.Size;
|
||||
return new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a subRegion (px) into texture coords (0-1) of a given texture (cells of the textureAtlas).
|
||||
/// </summary>
|
||||
private static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
|
||||
internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
|
||||
{
|
||||
if (texture is AtlasTexture atlas)
|
||||
{
|
||||
|
||||
@@ -578,10 +578,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// TODO: split batch if necessary.
|
||||
var vIdx = BatchVertexIndex;
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, modulate);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, modulate);
|
||||
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, modulate);
|
||||
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, modulate);
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate);
|
||||
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate);
|
||||
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate);
|
||||
BatchVertexIndex += 4;
|
||||
QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx);
|
||||
|
||||
|
||||
@@ -601,7 +601,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ClydeTexture : OwnedTexture
|
||||
internal sealed class ClydeTexture : OwnedTexture
|
||||
{
|
||||
private readonly Clyde _clyde;
|
||||
public readonly bool IsSrgb;
|
||||
@@ -664,7 +664,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
GL.GetnTexImage(TextureTarget.Texture2D, 0, PF.Rgba, PT.UnsignedByte, bufSize, (IntPtr) p);
|
||||
GL.GetTexImage(TextureTarget.Texture2D, 0, PF.Rgba, PT.UnsignedByte, (IntPtr) p);
|
||||
}
|
||||
|
||||
GL.BindTexture(TextureTarget.Texture2D, curTexture2D);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
varying highp vec2 UV;
|
||||
varying highp vec2 UV2;
|
||||
varying highp vec2 Pos;
|
||||
varying highp vec4 VtxModulate;
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
/*layout (location = 0)*/ attribute vec2 aPos;
|
||||
// Texture coordinates.
|
||||
/*layout (location = 1)*/ attribute vec2 tCoord;
|
||||
/*layout (location = 2)*/ attribute vec2 tCoord2;
|
||||
// Colour modulation.
|
||||
/*layout (location = 2)*/ attribute vec4 modulate;
|
||||
/*layout (location = 3)*/ attribute vec4 modulate;
|
||||
|
||||
varying vec2 UV;
|
||||
varying vec2 UV2;
|
||||
varying vec2 Pos;
|
||||
varying vec4 VtxModulate;
|
||||
|
||||
@@ -36,5 +38,6 @@ void main()
|
||||
gl_Position = vec4(VERTEX, 0.0, 1.0);
|
||||
Pos = (VERTEX + 1.0) / 2.0;
|
||||
UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
|
||||
UV2 = tCoord2;
|
||||
VtxModulate = zFromSrgb(modulate);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
varying highp vec2 UV;
|
||||
varying highp vec2 UV2;
|
||||
|
||||
uniform sampler2D lightMap;
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
/*layout (location = 0)*/ attribute vec2 aPos;
|
||||
// Texture coordinates.
|
||||
/*layout (location = 1)*/ attribute vec2 tCoord;
|
||||
/*layout (location = 2)*/ attribute vec2 tCoord2;
|
||||
// Colour modulation.
|
||||
/*layout (location = 2)*/ attribute vec4 modulate;
|
||||
/*layout (location = 3)*/ attribute vec4 modulate;
|
||||
|
||||
varying vec2 UV;
|
||||
varying vec2 UV2;
|
||||
|
||||
// Maybe we should merge these CPU side.
|
||||
// idk yet.
|
||||
@@ -40,6 +42,7 @@ void main()
|
||||
vec2 VERTEX = aPos;
|
||||
|
||||
UV = tCoord;
|
||||
UV2 = tCoord2;
|
||||
|
||||
// [SHADER_CODE]
|
||||
|
||||
|
||||
@@ -114,43 +114,12 @@ namespace Robust.Client.Graphics
|
||||
DrawPrimitives(primitiveTopology, White, indices, drawVertices);
|
||||
}
|
||||
|
||||
private static void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
|
||||
private void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
|
||||
{
|
||||
if (input.Length == 0)
|
||||
return;
|
||||
|
||||
if (input.Length != output.Length)
|
||||
Color colorLinear = Color.FromSrgb(color);
|
||||
for (var i = 0; i < output.Length; i++)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid lengths!");
|
||||
}
|
||||
|
||||
var colorLinear = Color.FromSrgb(color);
|
||||
var colorVec = Unsafe.As<Color, Vector128<float>>(ref colorLinear);
|
||||
var uvVec = Vector128.Create(0, 0, 0.5f, 0.5f);
|
||||
var maskVec = Vector128.Create(0xFFFFFFFF, 0xFFFFFFFF, 0, 0).AsSingle();
|
||||
|
||||
var simdVectors = (nuint)(input.Length / 2);
|
||||
ref readonly var srcBase = ref Unsafe.As<Vector2, float>(ref Unsafe.AsRef(in input[0]));
|
||||
ref var dstBase = ref Unsafe.As<DrawVertexUV2DColor, float>(ref output[0]);
|
||||
|
||||
for (nuint i = 0; i < simdVectors; i++)
|
||||
{
|
||||
var positions = Vector128.LoadUnsafe(in srcBase, i * 4);
|
||||
|
||||
var posColorLower = (positions & maskVec) | uvVec;
|
||||
var posColorUpper = (Vector128.Shuffle(positions, Vector128.Create(2, 3, 0, 0)) & maskVec) | uvVec;
|
||||
|
||||
posColorLower.StoreUnsafe(ref dstBase, i * 16);
|
||||
colorVec.StoreUnsafe(ref dstBase, i * 16 + 4);
|
||||
posColorUpper.StoreUnsafe(ref dstBase, i * 16 + 8);
|
||||
colorVec.StoreUnsafe(ref dstBase, i * 16 + 12);
|
||||
}
|
||||
|
||||
var lastPos = (int)simdVectors * 2;
|
||||
if (lastPos != output.Length)
|
||||
{
|
||||
// Odd number of vertices. Handle the last manually.
|
||||
output[lastPos] = new DrawVertexUV2DColor(input[lastPos], new Vector2(0.5f, 0.5f), colorLinear);
|
||||
output[i] = new DrawVertexUV2DColor(input[i], new Vector2(0.5f, 0.5f), colorLinear);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +237,8 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
public Vector2 Position;
|
||||
public Vector2 UV;
|
||||
public Vector2 UV2;
|
||||
|
||||
/// <summary>
|
||||
/// Modulation colour for this vertex.
|
||||
/// Note that this color is in linear space.
|
||||
|
||||
@@ -346,7 +346,7 @@ namespace Robust.Client.Input
|
||||
{
|
||||
if (binding.CanRepeat)
|
||||
{
|
||||
return SetBindState(binding, BoundKeyState.Down, uiOnly);
|
||||
return SetBindState(binding, BoundKeyState.Down, uiOnly, isRepeat);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -375,7 +375,7 @@ namespace Robust.Client.Input
|
||||
SetBindState(binding, BoundKeyState.Up);
|
||||
}
|
||||
|
||||
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false)
|
||||
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false, bool isRepeat = false)
|
||||
{
|
||||
if (binding.BindingType == KeyBindingType.Command && state == BoundKeyState.Down)
|
||||
{
|
||||
@@ -387,6 +387,7 @@ namespace Robust.Client.Input
|
||||
// I honestly have no idea what the best solution here is.
|
||||
// note from the future: context switches won't cause re-entrancy anymore because InputContextContainer defers context switches
|
||||
DebugTools.Assert(!_currentlyFindingViewport, "Re-entrant key events??");
|
||||
DebugTools.Assert(!isRepeat || binding.CanRepeat);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -399,7 +400,7 @@ namespace Robust.Client.Input
|
||||
binding.State = state;
|
||||
|
||||
var eventArgs = new BoundKeyEventArgs(binding.Function, binding.State,
|
||||
MouseScreenPosition, binding.CanFocus);
|
||||
MouseScreenPosition, binding.CanFocus, isRepeat);
|
||||
|
||||
// UI returns true here into blockPass if it wants to prevent us from giving input events
|
||||
// to the viewport, but doesn't want it hard-handled so we keep processing possible key actions.
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Robust.Client.Placement.Modes
|
||||
|
||||
public SnapgridCenter(PlacementManager pMan) : base(pMan) { }
|
||||
|
||||
public override void Render(DrawingHandleWorld handle)
|
||||
public override void Render(in OverlayDrawArgs args)
|
||||
{
|
||||
if (Grid != null)
|
||||
{
|
||||
@@ -34,18 +34,18 @@ namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
var from = ScreenToWorld(new Vector2(a, 0));
|
||||
var to = ScreenToWorld(new Vector2(a, viewportSize.Y));
|
||||
handle.DrawLine(from, to, new Color(0, 0, 1f));
|
||||
args.WorldHandle.DrawLine(from, to, new Color(0, 0, 1f));
|
||||
}
|
||||
for (var a = gridstart.Y; a < viewportSize.Y; a += SnapSize * EyeManager.PixelsPerMeter)
|
||||
{
|
||||
var from = ScreenToWorld(new Vector2(0, a));
|
||||
var to = ScreenToWorld(new Vector2(viewportSize.X, a));
|
||||
handle.DrawLine(from, to, new Color(0, 0, 1f));
|
||||
args.WorldHandle.DrawLine(from, to, new Color(0, 0, 1f));
|
||||
}
|
||||
}
|
||||
|
||||
// Draw grid BELOW the ghost thing.
|
||||
base.Render(handle);
|
||||
base.Render(args);
|
||||
}
|
||||
|
||||
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
|
||||
|
||||
@@ -628,20 +628,20 @@ namespace Robust.Client.Placement
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Render(DrawingHandleWorld handle)
|
||||
private void Render(in OverlayDrawArgs args)
|
||||
{
|
||||
if (CurrentMode == null || !IsActive)
|
||||
{
|
||||
if (EraserRect.HasValue)
|
||||
{
|
||||
handle.UseShader(_drawingShader);
|
||||
handle.DrawRect(EraserRect.Value, new Color(255, 0, 0, 50));
|
||||
handle.UseShader(null);
|
||||
args.WorldHandle.UseShader(_drawingShader);
|
||||
args.WorldHandle.DrawRect(EraserRect.Value, new Color(255, 0, 0, 50));
|
||||
args.WorldHandle.UseShader(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentMode.Render(handle);
|
||||
CurrentMode.Render(args);
|
||||
|
||||
if (CurrentPermission is not {Range: > 0} ||
|
||||
!CurrentMode.RangeRequired ||
|
||||
@@ -650,7 +650,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
var worldPos = EntityManager.GetComponent<TransformComponent>(controlled).WorldPosition;
|
||||
|
||||
handle.DrawCircle(worldPos, CurrentPermission.Range, new Color(1, 1, 1, 0.25f));
|
||||
args.WorldHandle.DrawCircle(worldPos, CurrentPermission.Range, new Color(1, 1, 1, 0.25f));
|
||||
}
|
||||
|
||||
private void HandleStartPlacement(MsgPlacement msg)
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Robust.Client.Placement
|
||||
/// <returns></returns>
|
||||
public abstract bool IsValidPosition(EntityCoordinates position);
|
||||
|
||||
public virtual void Render(DrawingHandleWorld handle)
|
||||
public virtual void Render(in OverlayDrawArgs args)
|
||||
{
|
||||
var uid = pManager.CurrentPlacementOverlayEntity;
|
||||
if (!pManager.EntityManager.TryGetComponent(uid, out SpriteComponent? sprite) || !sprite.Visible)
|
||||
@@ -125,7 +125,8 @@ namespace Robust.Client.Placement
|
||||
var worldRot = pManager.EntityManager.GetComponent<TransformComponent>(coordinate.EntityId).WorldRotation + dirAng;
|
||||
|
||||
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
|
||||
spriteSys.Render(uid.Value, sprite, handle, pManager.EyeManager.CurrentEye.Rotation, worldRot, worldPos);
|
||||
var rot = args.Viewport.Eye?.Rotation ?? default;
|
||||
spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
_manager.Render(args.WorldHandle);
|
||||
_manager.Render(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ public sealed partial class ReplayLoadManager
|
||||
if (initMessages != null)
|
||||
UpdateMessages(initMessages, uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
|
||||
UpdateMessages(messages[0], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
|
||||
ProcessQueue(GameTick.MaxValue, detachQueue, detached);
|
||||
|
||||
var entSpan = state0.EntityStates.Value;
|
||||
Dictionary<NetEntity, EntityState> entStates = new(entSpan.Count);
|
||||
@@ -98,6 +97,8 @@ public sealed partial class ReplayLoadManager
|
||||
entStates.Add(entState.NetEntity, modifiedState);
|
||||
}
|
||||
|
||||
ProcessQueue(GameTick.MaxValue, detachQueue, detached, entStates);
|
||||
|
||||
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
|
||||
var playerSpan = state0.PlayerStates.Value;
|
||||
Dictionary<NetUserId, SessionState> playerStates = new(playerSpan.Count);
|
||||
@@ -132,6 +133,11 @@ public sealed partial class ReplayLoadManager
|
||||
var spawnedTracker = 0;
|
||||
var stateTracker = 0;
|
||||
var curState = state0;
|
||||
|
||||
var stats_due_ticks = 0;
|
||||
var stats_due_spawned = 0;
|
||||
var stats_due_state = 0;
|
||||
|
||||
for (var i = 1; i < states.Count; i++)
|
||||
{
|
||||
if (i % 10 == 0)
|
||||
@@ -144,16 +150,33 @@ public sealed partial class ReplayLoadManager
|
||||
UpdatePlayerStates(curState.PlayerStates.Span, playerStates);
|
||||
UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached);
|
||||
UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase);
|
||||
ProcessQueue(curState.ToSequence, detachQueue, detached);
|
||||
ProcessQueue(curState.ToSequence, detachQueue, detached, entStates);
|
||||
UpdateDeletions(curState.EntityDeletions, entStates, detached);
|
||||
serverTime[i] = GetTime(curState.ToSequence) - initialTime;
|
||||
ticksSinceLastCheckpoint++;
|
||||
|
||||
// Don't create checkpoints too frequently no matter the circumstance
|
||||
if (ticksSinceLastCheckpoint < _checkpointMinInterval)
|
||||
continue;
|
||||
|
||||
// Check if enough time, spawned entities or changed states have occurred.
|
||||
if (ticksSinceLastCheckpoint < _checkpointInterval
|
||||
&& spawnedTracker < _checkpointEntitySpawnThreshold
|
||||
&& stateTracker < _checkpointEntityStateThreshold)
|
||||
{
|
||||
continue;
|
||||
|
||||
// Track and update statistics about why checkpoints are getting created:
|
||||
if (ticksSinceLastCheckpoint >= _checkpointInterval)
|
||||
{
|
||||
stats_due_ticks += 1;
|
||||
}
|
||||
else if (spawnedTracker >= _checkpointEntitySpawnThreshold)
|
||||
{
|
||||
stats_due_spawned += 1;
|
||||
}
|
||||
else if (stateTracker >= _checkpointEntityStateThreshold)
|
||||
{
|
||||
stats_due_state += 1;
|
||||
}
|
||||
|
||||
ticksSinceLastCheckpoint = 0;
|
||||
@@ -168,7 +191,8 @@ public sealed partial class ReplayLoadManager
|
||||
checkPoints.Add(new CheckpointState(newState, timeBase, cvars, i, detached));
|
||||
}
|
||||
|
||||
_sawmill.Info($"Finished generating checkpoints. Elapsed time: {st.Elapsed}");
|
||||
_sawmill.Info($"Finished generating {checkPoints.Count} checkpoints. Elapsed time: {st.Elapsed}. Checkpoint every {(float)states.Count / checkPoints.Count} ticks on average");
|
||||
_sawmill.Info($"Checkpoint stats - Spawning: {stats_due_spawned} StateChanges: {stats_due_state} Ticks: {stats_due_ticks}. ");
|
||||
await callback(states.Count, states.Count, LoadingState.ProcessingFiles, false);
|
||||
return (checkPoints.ToArray(), serverTime);
|
||||
}
|
||||
@@ -176,14 +200,28 @@ public sealed partial class ReplayLoadManager
|
||||
private void ProcessQueue(
|
||||
GameTick curTick,
|
||||
Dictionary<GameTick, List<NetEntity>> detachQueue,
|
||||
HashSet<NetEntity> detached)
|
||||
HashSet<NetEntity> detached,
|
||||
Dictionary<NetEntity, EntityState> entStates)
|
||||
{
|
||||
foreach (var (tick, ents) in detachQueue)
|
||||
{
|
||||
if (tick > curTick)
|
||||
continue;
|
||||
detachQueue.Remove(tick);
|
||||
detached.UnionWith(ents);
|
||||
|
||||
foreach (var e in ents)
|
||||
{
|
||||
if (entStates.ContainsKey(e))
|
||||
detached.Add(e);
|
||||
else
|
||||
{
|
||||
// AFAIK this should only happen if the client skipped over some ticks, probably due to packet loss
|
||||
// I.e., entity was created on tick n, then leaves PVS range on the tick n+1
|
||||
// If the n-th tick gets dropped, the client only ever receives the pvs-leave message.
|
||||
// In that case we should just ignore it.
|
||||
_sawmill.Debug($"Received a PVS detach msg for entity {e} before it was received?");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +363,7 @@ public sealed partial class ReplayLoadManager
|
||||
#if DEBUG
|
||||
foreach (var state in modifiedState.ComponentChanges.Value)
|
||||
{
|
||||
DebugTools.Assert(state.State is not IComponentDeltaState delta || delta.FullState);
|
||||
DebugTools.Assert(state.State is not IComponentDeltaState delta);
|
||||
}
|
||||
#endif
|
||||
continue;
|
||||
@@ -338,7 +376,7 @@ public sealed partial class ReplayLoadManager
|
||||
#if DEBUG
|
||||
foreach (var state in entStates[entState.NetEntity].ComponentChanges.Span)
|
||||
{
|
||||
DebugTools.Assert(state.State is not IComponentDeltaState delta || delta.FullState);
|
||||
DebugTools.Assert(state.State is not IComponentDeltaState delta);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -369,20 +407,20 @@ public sealed partial class ReplayLoadManager
|
||||
if (!newCompStates.Remove(existing.NetID, out var newCompState))
|
||||
continue;
|
||||
|
||||
if (newCompState.State is not IComponentDeltaState delta || delta.FullState)
|
||||
if (newCompState.State is not IComponentDeltaState delta)
|
||||
{
|
||||
combined[index] = newCompState;
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(existing.State is IComponentDeltaState fullDelta && fullDelta.FullState);
|
||||
combined[index] = new ComponentChange(existing.NetID, delta.CreateNewFullState(existing.State), newCompState.LastModifiedTick);
|
||||
DebugTools.Assert(existing.State != null && existing.State is not IComponentDeltaState);
|
||||
combined[index] = new ComponentChange(existing.NetID, delta.CreateNewFullState(existing.State!), newCompState.LastModifiedTick);
|
||||
}
|
||||
|
||||
foreach (var compChange in newCompStates.Values)
|
||||
{
|
||||
// I'm not 100% sure about this, but I think delta states should always be full states here?
|
||||
DebugTools.Assert(compChange.State is not IComponentDeltaState delta || delta.FullState);
|
||||
DebugTools.Assert(compChange.State is not IComponentDeltaState delta);
|
||||
combined.Add(compChange);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed partial class ReplayLoadManager
|
||||
continue;
|
||||
|
||||
var state = _entMan.GetComponentState(_entMan.EventBus, component, null, GameTick.Zero);
|
||||
DebugTools.Assert(state is not IComponentDeltaState delta || delta.FullState);
|
||||
DebugTools.Assert(state is not IComponentDeltaState);
|
||||
list.Add(new ComponentChange(netId, state, GameTick.Zero));
|
||||
set.Add(netId);
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public sealed partial class ReplayLoadManager
|
||||
{
|
||||
if (comp.NetID == _metaId)
|
||||
{
|
||||
var state = (MetaDataComponentState) comp.State;
|
||||
var state = (MetaDataComponentState) comp.State!;
|
||||
return state.PrototypeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed partial class ReplayLoadManager : IReplayLoadManager
|
||||
private ushort _metaId;
|
||||
private bool _initialized;
|
||||
private int _checkpointInterval;
|
||||
private int _checkpointMinInterval;
|
||||
private int _checkpointEntitySpawnThreshold;
|
||||
private int _checkpointEntityStateThreshold;
|
||||
private ISawmill _sawmill = default!;
|
||||
@@ -45,6 +46,7 @@ public sealed partial class ReplayLoadManager : IReplayLoadManager
|
||||
|
||||
_initialized = true;
|
||||
_confMan.OnValueChanged(CVars.CheckpointInterval, value => _checkpointInterval = value, true);
|
||||
_confMan.OnValueChanged(CVars.CheckpointMinInterval, value => _checkpointMinInterval = value, true);
|
||||
_confMan.OnValueChanged(CVars.CheckpointEntitySpawnThreshold, value => _checkpointEntitySpawnThreshold = value,
|
||||
true);
|
||||
_confMan.OnValueChanged(CVars.CheckpointEntityStateThreshold, value => _checkpointEntityStateThreshold = value,
|
||||
|
||||
@@ -79,13 +79,14 @@ internal sealed partial class ReplayPlaybackManager
|
||||
if (checkpoint.DetachedStates == null)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length); ;
|
||||
var metas = _entMan.GetEntityQuery<MetaDataComponent>();
|
||||
DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length);
|
||||
foreach (var es in checkpoint.DetachedStates)
|
||||
{
|
||||
var uid = _entMan.GetEntity(es.NetEntity);
|
||||
if (metas.TryGetComponent(uid, out var meta) && !meta.EntityDeleted)
|
||||
if (_entMan.TryGetEntityData(es.NetEntity, out var uid, out var meta))
|
||||
{
|
||||
DebugTools.Assert(!meta.EntityDeleted);
|
||||
continue;
|
||||
}
|
||||
|
||||
var metaState = (MetaDataComponentState?)es.ComponentChanges.Value?
|
||||
.FirstOrDefault(c => c.NetID == _metaId).State;
|
||||
@@ -93,18 +94,16 @@ internal sealed partial class ReplayPlaybackManager
|
||||
if (metaState == null)
|
||||
throw new MissingMetadataException(es.NetEntity);
|
||||
|
||||
_entMan.CreateEntityUninitialized(metaState.PrototypeId, uid);
|
||||
meta = metas.GetComponent(uid);
|
||||
uid = _entMan.CreateEntity(metaState.PrototypeId, out meta);
|
||||
|
||||
// Client creates a client-side net entity for the newly created entity.
|
||||
// We need to clear this mapping before assigning the real net id.
|
||||
// TODO NetEntity Jank: prevent the client from creating this in the first place.
|
||||
_entMan.ClearNetEntity(meta.NetEntity);
|
||||
_entMan.SetNetEntity(uid.Value, es.NetEntity, meta);
|
||||
|
||||
_entMan.SetNetEntity(uid, es.NetEntity, meta);
|
||||
|
||||
_entMan.InitializeEntity(uid, meta);
|
||||
_entMan.StartEntity(uid);
|
||||
_entMan.InitializeEntity(uid.Value, meta);
|
||||
_entMan.StartEntity(uid.Value);
|
||||
meta.LastStateApplied = checkpoint.Tick;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ internal sealed partial class ReplayPlaybackManager
|
||||
skipEffectEvents = true;
|
||||
ResetToNearestCheckpoint(value, false);
|
||||
}
|
||||
else if (value > Replay.CurrentIndex + _checkpointInterval)
|
||||
else if (value > Replay.CurrentIndex + _checkpointMinInterval)
|
||||
{
|
||||
// If we are skipping many ticks into the future, we try to skip directly to a checkpoint instead of
|
||||
// applying every tick.
|
||||
var nextCheckpoint = GetNextCheckpoint(Replay, Replay.CurrentIndex);
|
||||
if (nextCheckpoint.Index < value)
|
||||
if (nextCheckpoint.Index < value && nextCheckpoint.Index > Replay.CurrentIndex)
|
||||
ResetToNearestCheckpoint(value, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
|
||||
|
||||
public ReplayData? Replay { get; private set; }
|
||||
public NetUserId? Recorder => Replay?.Recorder;
|
||||
private int _checkpointInterval;
|
||||
private int _checkpointMinInterval;
|
||||
private int _visualEventThreshold;
|
||||
public uint? AutoPauseCountdown { get; set; }
|
||||
public int? ScrubbingTarget { get; set; }
|
||||
@@ -93,7 +93,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
|
||||
_initialized = true;
|
||||
_sawmill = _logMan.GetSawmill("replay");
|
||||
_metaId = _factory.GetRegistration(typeof(MetaDataComponent)).NetID!.Value;
|
||||
_confMan.OnValueChanged(CVars.CheckpointInterval, (value) => _checkpointInterval = value, true);
|
||||
_confMan.OnValueChanged(CVars.CheckpointMinInterval, (value) => _checkpointMinInterval = value, true);
|
||||
_confMan.OnValueChanged(CVars.ReplaySkipThreshold, (value) => _visualEventThreshold = value, true);
|
||||
_client.RunLevelChanged += OnRunLevelChanged;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -142,6 +143,26 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
});
|
||||
|
||||
// Do not meta-atlas RSIs with custom load parameters.
|
||||
var atlasList = rsiList.Where(x => x.LoadParameters == TextureLoadParameters.Default).ToArray();
|
||||
var nonAtlasList = rsiList.Where(x => x.LoadParameters != TextureLoadParameters.Default).ToArray();
|
||||
|
||||
foreach (var data in nonAtlasList)
|
||||
{
|
||||
if (data.Bad)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
RSIResource.LoadTexture(Clyde, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This combines individual RSI atlases into larger atlases to reduce draw batches. currently this is a VERY
|
||||
// lazy bundling and is not at all compact, its basically an atlas of RSI atlases. Really what this should
|
||||
// try to do is to have each RSI write directly to the atlas, rather than having each RSI write to its own
|
||||
@@ -155,7 +176,7 @@ namespace Robust.Client.ResourceManagement
|
||||
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
|
||||
// TODO combine with (non-rsi) texture atlas?
|
||||
|
||||
Array.Sort(rsiList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
|
||||
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
|
||||
|
||||
// Each RSI sub atlas has a different size.
|
||||
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
|
||||
@@ -167,9 +188,9 @@ namespace Robust.Client.ResourceManagement
|
||||
Vector2i offset = default;
|
||||
int finalized = -1;
|
||||
int atlasCount = 0;
|
||||
for (int i = 0; i < rsiList.Length; i++)
|
||||
for (int i = 0; i < atlasList.Length; i++)
|
||||
{
|
||||
var rsi = rsiList[i];
|
||||
var rsi = atlasList[i];
|
||||
if (rsi.Bad)
|
||||
continue;
|
||||
|
||||
@@ -200,14 +221,14 @@ namespace Robust.Client.ResourceManagement
|
||||
var height = offset.Y + deltaY;
|
||||
var croppedSheet = new Image<Rgba32>(maxSize, height);
|
||||
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
|
||||
FinalizeMetaAtlas(rsiList.Length - 1, croppedSheet);
|
||||
FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
|
||||
|
||||
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
|
||||
{
|
||||
var atlas = Clyde.LoadTextureFromImage(sheet);
|
||||
for (int i = finalized + 1; i <= toIndex; i++)
|
||||
{
|
||||
var rsi = rsiList[i];
|
||||
var rsi = atlasList[i];
|
||||
rsi.AtlasTexture = atlas;
|
||||
}
|
||||
|
||||
@@ -255,9 +276,10 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountErrored} errored) in {LoadTime}",
|
||||
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
|
||||
rsiList.Length,
|
||||
atlasCount,
|
||||
nonAtlasList.Length,
|
||||
errors,
|
||||
sw.Elapsed);
|
||||
|
||||
|
||||
@@ -40,17 +40,21 @@ namespace Robust.Client.ResourceManagement
|
||||
var loadStepData = new LoadStepData {Path = path};
|
||||
var manager = dependencies.Resolve<IResourceManager>();
|
||||
LoadPreTexture(manager, loadStepData);
|
||||
|
||||
loadStepData.AtlasTexture = dependencies.Resolve<IClyde>().LoadTextureFromImage(
|
||||
loadStepData.AtlasSheet,
|
||||
loadStepData.Path.ToString());
|
||||
|
||||
LoadTexture(dependencies.Resolve<IClyde>(), loadStepData);
|
||||
LoadPostTexture(loadStepData);
|
||||
LoadFinish(dependencies.Resolve<IResourceCacheInternal>(), loadStepData);
|
||||
|
||||
loadStepData.AtlasSheet.Dispose();
|
||||
}
|
||||
|
||||
internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData)
|
||||
{
|
||||
loadStepData.AtlasTexture = clyde.LoadTextureFromImage(
|
||||
loadStepData.AtlasSheet,
|
||||
loadStepData.Path.ToString(),
|
||||
loadStepData.LoadParameters);
|
||||
}
|
||||
|
||||
internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
|
||||
{
|
||||
var manifestPath = data.Path / "meta.json";
|
||||
@@ -178,6 +182,7 @@ namespace Robust.Client.ResourceManagement
|
||||
data.FrameSize = frameSize;
|
||||
data.DimX = dimensionX;
|
||||
data.CallbackOffsets = callbackOffsets;
|
||||
data.LoadParameters = metadata.LoadParameters;
|
||||
}
|
||||
|
||||
internal static void LoadPostTexture(LoadStepData data)
|
||||
@@ -380,6 +385,7 @@ namespace Robust.Client.ResourceManagement
|
||||
public Texture AtlasTexture = default!;
|
||||
public Vector2i AtlasOffset;
|
||||
public RSI Rsi = default!;
|
||||
public TextureLoadParameters LoadParameters;
|
||||
}
|
||||
|
||||
internal struct StateReg
|
||||
|
||||
@@ -52,6 +52,10 @@ namespace Robust.Client.UserInterface
|
||||
[ViewVariables] public bool IsMeasureValid { get; private set; }
|
||||
[ViewVariables] public bool IsArrangeValid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls the amount of empty space in virtual pixels around the control.
|
||||
/// </summary>
|
||||
/// <remarks>Values can be provided as "All" or "Horizontal, Vertical" or "Left, Top, Right, Bottom"</remarks>
|
||||
[ViewVariables]
|
||||
public Thickness Margin
|
||||
{
|
||||
|
||||
@@ -130,7 +130,7 @@ public sealed class TileSpawningUIController : UIController
|
||||
|
||||
_window.TileList.Clear();
|
||||
|
||||
IEnumerable<ITileDefinition> tileDefs = _tiles;
|
||||
IEnumerable<ITileDefinition> tileDefs = _tiles.Where(def => !def.EditorHidden);
|
||||
|
||||
if (!string.IsNullOrEmpty(searchStr))
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private TimeSpan? _lastClickTime;
|
||||
private Vector2? _lastClickPosition;
|
||||
|
||||
private bool IsPlaceHolderVisible => string.IsNullOrEmpty(_text) && _placeHolder != null;
|
||||
private bool IsPlaceHolderVisible => !(HidePlaceHolderOnFocus && HasKeyboardFocus()) && string.IsNullOrEmpty(_text) && _placeHolder != null;
|
||||
|
||||
public event Action<LineEditEventArgs>? OnTextChanged;
|
||||
public event Action<LineEditEventArgs>? OnTextEntered;
|
||||
@@ -186,6 +186,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public int SelectionLower => Math.Min(_selectionStart, _cursorPosition);
|
||||
public int SelectionUpper => Math.Max(_selectionStart, _cursorPosition);
|
||||
|
||||
public bool HidePlaceHolderOnFocus { get; set; }
|
||||
|
||||
public bool IgnoreNext { get; set; }
|
||||
|
||||
private (int start, int length)? _imeData;
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Robust.Client.UserInterface
|
||||
return Task.FromResult<Stream?>(null);
|
||||
}
|
||||
|
||||
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null)
|
||||
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
{
|
||||
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Robust.Client.UserInterface
|
||||
return await OpenFileNfd(filters);
|
||||
}
|
||||
|
||||
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters)
|
||||
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
|
||||
{
|
||||
var name = await GetSaveFileName(filters);
|
||||
if (name == null)
|
||||
@@ -61,7 +61,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
try
|
||||
{
|
||||
return (File.Open(name, FileMode.Open), true);
|
||||
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Robust.Client.UserInterface
|
||||
/// The file stream the user chose to save to, and whether the file already existed.
|
||||
/// Null if the user cancelled the action.
|
||||
/// </returns>
|
||||
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null);
|
||||
/// <param name="truncate">Should we truncate an existing file to 0-size then write or append.</param>
|
||||
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,17 @@ namespace Robust.Client.UserInterface
|
||||
/// Plays the UI hover sound if relevant.
|
||||
/// </summary>
|
||||
void HoverSound();
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="CurrentlyHovered"/> to the given control.
|
||||
/// </summary>
|
||||
void SetHovered(Control? control);
|
||||
|
||||
/// <summary>
|
||||
/// Forces <see cref="CurrentlyHovered"/> to get updated. This is done automatically when the mouse is moved,
|
||||
/// but not necessarily a new or existing control is rearranged.
|
||||
/// </summary>
|
||||
void UpdateHovered();
|
||||
}
|
||||
|
||||
public readonly struct PostDrawUIRootEventArgs
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
@@ -20,9 +21,10 @@ internal partial class UserInterfaceManager
|
||||
private bool _needUpdateActiveCursor;
|
||||
[ViewVariables] public Control? KeyboardFocused { get; private set; }
|
||||
|
||||
[ViewVariables] public Control? CurrentlyHovered { get; private set; } = default!;
|
||||
[ViewVariables] public Control? CurrentlyHovered { get; private set; }
|
||||
|
||||
private Control? _controlFocused;
|
||||
|
||||
[ViewVariables]
|
||||
public Control? ControlFocused
|
||||
{
|
||||
@@ -100,6 +102,7 @@ internal partial class UserInterfaceManager
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
args.PointerLocation.Position - control.GlobalPixelPosition);
|
||||
@@ -111,16 +114,20 @@ internal partial class UserInterfaceManager
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
// Attempt to ensure that keybind-up events get raised after a keybind-down.
|
||||
DebugTools.Assert(!_focusedControls.TryGetValue(args.Function, out var existing)
|
||||
|| !existing.VisibleInTree
|
||||
|| args.IsRepeat && existing == control);
|
||||
_focusedControls[args.Function] = control;
|
||||
|
||||
OnKeyBindDown?.Invoke(control);
|
||||
}
|
||||
|
||||
public void KeyBindUp(BoundKeyEventArgs args)
|
||||
{
|
||||
if (!_focusedControls.TryGetValue(args.Function, out var control))
|
||||
{
|
||||
// Only raise keybind-up for the control on which we previously raised keybind-down
|
||||
if (!_focusedControls.Remove(args.Function, out var control) || !control.VisibleInTree)
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
@@ -131,7 +138,6 @@ internal partial class UserInterfaceManager
|
||||
// Always mark this as handled.
|
||||
// The only case it should not be is if we do not have a control to click on,
|
||||
// in which case we never reach this.
|
||||
_focusedControls.Remove(args.Function);
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
@@ -140,23 +146,7 @@ internal partial class UserInterfaceManager
|
||||
_resetTooltipTimer();
|
||||
// Update which control is considered hovered.
|
||||
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
|
||||
if (newHovered != CurrentlyHovered)
|
||||
{
|
||||
_clearTooltip();
|
||||
CurrentlyHovered?.MouseExited();
|
||||
CurrentlyHovered = newHovered;
|
||||
CurrentlyHovered?.MouseEntered();
|
||||
if (CurrentlyHovered != null)
|
||||
{
|
||||
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tooltipDelay = null;
|
||||
}
|
||||
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
SetHovered(newHovered);
|
||||
|
||||
var target = ControlFocused ?? newHovered;
|
||||
if (target != null)
|
||||
@@ -172,6 +162,33 @@ internal partial class UserInterfaceManager
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateHovered()
|
||||
{
|
||||
var ctrl = MouseGetControl(_inputManager.MouseScreenPosition);
|
||||
SetHovered(ctrl);
|
||||
}
|
||||
|
||||
public void SetHovered(Control? control)
|
||||
{
|
||||
if (control == CurrentlyHovered)
|
||||
return;
|
||||
|
||||
_clearTooltip();
|
||||
CurrentlyHovered?.MouseExited();
|
||||
CurrentlyHovered = control;
|
||||
CurrentlyHovered?.MouseEntered();
|
||||
if (CurrentlyHovered != null)
|
||||
{
|
||||
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tooltipDelay = null;
|
||||
}
|
||||
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
|
||||
private void UpdateActiveCursor()
|
||||
{
|
||||
// Consider mouse input focus first so that dragging windows don't act up etc.
|
||||
|
||||
@@ -77,15 +77,12 @@ internal sealed partial class UserInterfaceManager
|
||||
|
||||
ReleaseKeyboardFocus(control);
|
||||
RemoveModal(control);
|
||||
if (control == CurrentlyHovered)
|
||||
{
|
||||
control.MouseExited();
|
||||
CurrentlyHovered = null;
|
||||
_clearTooltip();
|
||||
}
|
||||
|
||||
if (control != ControlFocused) return;
|
||||
ControlFocused = null;
|
||||
if (control == ControlFocused)
|
||||
ControlFocused = null;
|
||||
|
||||
if (control == CurrentlyHovered)
|
||||
UpdateHovered();
|
||||
}
|
||||
|
||||
public void PushModal(Control modal)
|
||||
|
||||
@@ -29,6 +29,8 @@ public static class Diagnostics
|
||||
public const string IdComponentPauseNoParentAttribute = "RA0023";
|
||||
public const string IdComponentPauseWrongTypeAttribute = "RA0024";
|
||||
public const string IdDependencyFieldAssigned = "RA0025";
|
||||
public const string IdUncachedRegex = "RA0026";
|
||||
public const string IdDataFieldRedundantTag = "RA0027";
|
||||
|
||||
public static SuppressionDescriptor MeansImplicitAssignment =>
|
||||
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
@@ -36,6 +34,7 @@ public class Generator : IIncrementalGenerator
|
||||
var (compilation, declarations) = source;
|
||||
var builder = new StringBuilder();
|
||||
var containingTypes = new Stack<INamedTypeSymbol>();
|
||||
var declarationsGenerated = new HashSet<string>();
|
||||
|
||||
foreach (var declaration in declarations)
|
||||
{
|
||||
@@ -44,6 +43,14 @@ public class Generator : IIncrementalGenerator
|
||||
|
||||
var type = compilation.GetSemanticModel(declaration.SyntaxTree).GetDeclaredSymbol(declaration)!;
|
||||
|
||||
var symbolName = type
|
||||
.ToDisplayString()
|
||||
.Replace('<', '{')
|
||||
.Replace('>', '}');
|
||||
|
||||
if (!declarationsGenerated.Add(symbolName))
|
||||
continue;
|
||||
|
||||
var nonPartial = !IsPartial(declaration);
|
||||
|
||||
var namespaceString = type.ContainingNamespace.IsGlobalNamespace
|
||||
@@ -107,11 +114,6 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
{{containingTypesEnd}}
|
||||
""");
|
||||
|
||||
var symbolName = type
|
||||
.ToDisplayString()
|
||||
.Replace('<', '{')
|
||||
.Replace('>', '}');
|
||||
|
||||
var sourceText = CSharpSyntaxTree
|
||||
.ParseText(builder.ToString())
|
||||
.GetRoot()
|
||||
|
||||
@@ -42,6 +42,28 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
component.Source = new DummyAudioSource();
|
||||
}
|
||||
|
||||
public override void SetGridAudio(Entity<AudioComponent>? entity)
|
||||
{
|
||||
if (entity == null)
|
||||
return;
|
||||
|
||||
base.SetGridAudio(entity);
|
||||
|
||||
// Need to global override so everyone can hear it.
|
||||
_pvs.AddGlobalOverride(entity.Value.Owner);
|
||||
}
|
||||
|
||||
public override void SetMapAudio(Entity<AudioComponent>? audio)
|
||||
{
|
||||
if (audio == null)
|
||||
return;
|
||||
|
||||
base.SetMapAudio(audio);
|
||||
|
||||
// Also need a global override because clients not near 0,0 won't get the audio.
|
||||
_pvs.AddGlobalOverride(audio.Value);
|
||||
}
|
||||
|
||||
private void AddAudioFilter(EntityUid uid, AudioComponent component, Filter filter)
|
||||
{
|
||||
var count = filter.Count;
|
||||
@@ -70,11 +92,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
var entity = Spawn("Audio", MapCoordinates.Nullspace);
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
AddAudioFilter(entity, audio, playerFilter);
|
||||
audio.Global = true;
|
||||
return (entity, audio);
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
AddAudioFilter(entity, entity.Comp, playerFilter);
|
||||
entity.Comp.Global = true;
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -89,11 +110,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
AddAudioFilter(entity, audio, playerFilter);
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
// Move it after setting it up
|
||||
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
|
||||
AddAudioFilter(entity, entity.Comp, playerFilter);
|
||||
|
||||
return (entity, audio);
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -108,10 +130,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
|
||||
|
||||
return (entity, audio);
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -129,11 +151,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
if (!coordinates.IsValid(EntityManager))
|
||||
return null;
|
||||
|
||||
var entity = Spawn("Audio", coordinates);
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
AddAudioFilter(entity, audio, playerFilter);
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
XformSystem.SetCoordinates(entity, coordinates);
|
||||
AddAudioFilter(entity, entity.Comp, playerFilter);
|
||||
|
||||
return (entity, audio);
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -152,10 +174,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
if (!coordinates.IsValid(EntityManager))
|
||||
return null;
|
||||
|
||||
var entity = Spawn("Audio", coordinates);
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
// TODO: Transform TryFindGridAt mess + optimisation required.
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
XformSystem.SetCoordinates(entity, coordinates);
|
||||
|
||||
return (entity, audio);
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,416 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using System.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
||||
[Dependency] private readonly TransformSystem _xformSys = default!;
|
||||
|
||||
private EntityQuery<IgnoreUIRangeComponent> _ignoreUIRangeQuery;
|
||||
|
||||
private readonly List<ICommonSession> _sessionCache = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<BoundUIWrapMessage>(OnMessageReceived);
|
||||
_playerMan.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
_ignoreUIRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_playerMan.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
|
||||
{
|
||||
if (args.NewStatus != SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
|
||||
return;
|
||||
|
||||
foreach (var bui in buis.ToArray())
|
||||
{
|
||||
CloseShared(bui, args.Session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var query = AllEntityQuery<ActiveUserInterfaceComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var activeUis, out var xform))
|
||||
{
|
||||
foreach (var ui in activeUis.Interfaces)
|
||||
{
|
||||
CheckRange(uid, activeUis, ui, xform, xformQuery);
|
||||
|
||||
if (!ui.StateDirty)
|
||||
continue;
|
||||
|
||||
ui.StateDirty = false;
|
||||
|
||||
foreach (var (player, state) in ui.PlayerStateOverrides)
|
||||
{
|
||||
RaiseNetworkEvent(state, player.Channel);
|
||||
}
|
||||
|
||||
if (ui.LastStateMsg == null)
|
||||
continue;
|
||||
|
||||
foreach (var session in ui.SubscribedSessions)
|
||||
{
|
||||
if (!ui.PlayerStateOverrides.ContainsKey(session))
|
||||
RaiseNetworkEvent(ui.LastStateMsg, session.Channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that the subscribed clients are still in range of the interface.
|
||||
/// </summary>
|
||||
private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
|
||||
{
|
||||
if (ui.InteractionRange <= 0)
|
||||
return;
|
||||
|
||||
// We have to cache the set of sessions because Unsubscribe modifies the original.
|
||||
_sessionCache.Clear();
|
||||
_sessionCache.AddRange(ui.SubscribedSessions);
|
||||
|
||||
var uiPos = _xformSys.GetWorldPosition(transform, query);
|
||||
var uiMap = transform.MapID;
|
||||
|
||||
foreach (var session in _sessionCache)
|
||||
{
|
||||
// The component manages the set of sessions, so this invalid session should be removed soon.
|
||||
if (!query.TryGetComponent(session.AttachedEntity, out var xform))
|
||||
continue;
|
||||
|
||||
if (_ignoreUIRangeQuery.HasComponent(session.AttachedEntity))
|
||||
continue;
|
||||
|
||||
// Handle pluggable BoundUserInterfaceCheckRangeEvent
|
||||
var checkRangeEvent = new BoundUserInterfaceCheckRangeEvent(uid, ui, session);
|
||||
RaiseLocalEvent(uid, ref checkRangeEvent, broadcast: true);
|
||||
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Pass)
|
||||
continue;
|
||||
|
||||
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Fail)
|
||||
{
|
||||
CloseUi(ui, session, activeUis);
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default);
|
||||
|
||||
if (uiMap != xform.MapID)
|
||||
{
|
||||
CloseUi(ui, session, activeUis);
|
||||
continue;
|
||||
}
|
||||
|
||||
var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared();
|
||||
if (distanceSquared > ui.InteractionRangeSqrd)
|
||||
CloseUi(ui, session, activeUis);
|
||||
}
|
||||
}
|
||||
|
||||
#region Get BUI
|
||||
|
||||
public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!Resolve(uid, ref ui))
|
||||
return false;
|
||||
|
||||
return ui.Interfaces.ContainsKey(uiKey);
|
||||
}
|
||||
|
||||
public PlayerBoundUserInterface GetUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!Resolve(uid, ref ui))
|
||||
throw new InvalidOperationException($"Cannot get {typeof(PlayerBoundUserInterface)} from an entity without {typeof(UserInterfaceComponent)}!");
|
||||
|
||||
return ui.Interfaces[uiKey];
|
||||
}
|
||||
|
||||
public PlayerBoundUserInterface? GetUiOrNull(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
return TryGetUi(uid, uiKey, out var bui, ui)
|
||||
? bui
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return UIs a session has open.
|
||||
/// Null if empty.
|
||||
/// </summary>
|
||||
public List<PlayerBoundUserInterface>? GetAllUIsForSession(ICommonSession session)
|
||||
{
|
||||
OpenInterfaces.TryGetValue(session, out var value);
|
||||
return value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public bool IsUiOpen(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return bui.SubscribedSessions.Count > 0;
|
||||
}
|
||||
|
||||
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return bui.SubscribedSessions.Contains(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a state. This can be used for stateful UI updating.
|
||||
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
|
||||
/// Pretty much how NanoUI did it back in ye olde BYOND.
|
||||
/// </summary>
|
||||
/// <param name="state">
|
||||
/// The state object that will be sent to all current and future client.
|
||||
/// This can be null.
|
||||
/// </param>
|
||||
/// <param name="session">
|
||||
/// The player session to send this new state to.
|
||||
/// Set to null for sending it to every subscribed player session.
|
||||
/// </param>
|
||||
public bool TrySetUiState(EntityUid uid,
|
||||
Enum uiKey,
|
||||
BoundUserInterfaceState state,
|
||||
ICommonSession? session = null,
|
||||
UserInterfaceComponent? ui = null,
|
||||
bool clearOverrides = true)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
SetUiState(bui, state, session, clearOverrides);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a state. This can be used for stateful UI updating.
|
||||
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
|
||||
/// Pretty much how NanoUI did it back in ye olde BYOND.
|
||||
/// </summary>
|
||||
/// <param name="state">
|
||||
/// The state object that will be sent to all current and future client.
|
||||
/// This can be null.
|
||||
/// </param>
|
||||
/// <param name="session">
|
||||
/// The player session to send this new state to.
|
||||
/// Set to null for sending it to every subscribed player session.
|
||||
/// </param>
|
||||
public void SetUiState(PlayerBoundUserInterface bui, BoundUserInterfaceState state, ICommonSession? session = null, bool clearOverrides = true)
|
||||
{
|
||||
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), new UpdateBoundStateMessage(state), bui.UiKey);
|
||||
if (session == null)
|
||||
{
|
||||
bui.LastStateMsg = msg;
|
||||
if (clearOverrides)
|
||||
bui.PlayerStateOverrides.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
bui.PlayerStateOverrides[session] = msg;
|
||||
}
|
||||
|
||||
bui.StateDirty = true;
|
||||
}
|
||||
|
||||
#region Close
|
||||
protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
|
||||
{
|
||||
var owner = bui.Owner;
|
||||
bui._subscribedSessions.Remove(session);
|
||||
bui.PlayerStateOverrides.Remove(session);
|
||||
|
||||
if (OpenInterfaces.TryGetValue(session, out var buis))
|
||||
buis.Remove(bui);
|
||||
|
||||
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
|
||||
|
||||
if (bui._subscribedSessions.Count == 0)
|
||||
DeactivateInterface(bui.Owner, bui, activeUis);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this all interface for any clients that have any open.
|
||||
/// </summary>
|
||||
public bool TryCloseAll(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent? aui = null)
|
||||
{
|
||||
if (!Resolve(uid, ref aui, false))
|
||||
return false;
|
||||
|
||||
foreach (var ui in aui.Interfaces)
|
||||
{
|
||||
CloseAll(ui);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this specific interface for any clients that have it open.
|
||||
/// </summary>
|
||||
public bool TryCloseAll(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
CloseAll(bui);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this interface for any clients that have it open.
|
||||
/// </summary>
|
||||
public void CloseAll(PlayerBoundUserInterface bui)
|
||||
{
|
||||
foreach (var session in bui.SubscribedSessions.ToArray())
|
||||
{
|
||||
CloseUi(bui, session);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SendMessage
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to all connected player sessions.
|
||||
/// </summary>
|
||||
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
SendUiMessage(bui, message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to all connected player sessions.
|
||||
/// </summary>
|
||||
public void SendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message)
|
||||
{
|
||||
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey);
|
||||
foreach (var session in bui.SubscribedSessions)
|
||||
{
|
||||
RaiseNetworkEvent(msg, session.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to a specific player session.
|
||||
/// </summary>
|
||||
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, ICommonSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return TrySendUiMessage(bui, message, session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to a specific player session.
|
||||
/// </summary>
|
||||
public bool TrySendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message, ICommonSession session)
|
||||
{
|
||||
if (!bui.SubscribedSessions.Contains(session))
|
||||
return false;
|
||||
|
||||
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey), session.Channel);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised by <see cref="UserInterfaceSystem"/> to check whether an interface is still accessible by its user.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
[PublicAPI]
|
||||
public struct BoundUserInterfaceCheckRangeEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity owning the UI being checked for.
|
||||
/// </summary>
|
||||
public readonly EntityUid Target;
|
||||
|
||||
/// <summary>
|
||||
/// The UI itself.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public readonly PlayerBoundUserInterface UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// The player for which the UI is being checked.
|
||||
/// </summary>
|
||||
public readonly ICommonSession Player;
|
||||
|
||||
/// <summary>
|
||||
/// The result of the range check.
|
||||
/// </summary>
|
||||
public BoundUserInterfaceRangeResult Result;
|
||||
|
||||
public BoundUserInterfaceCheckRangeEvent(
|
||||
EntityUid target,
|
||||
PlayerBoundUserInterface userInterface,
|
||||
ICommonSession player)
|
||||
{
|
||||
Target = target;
|
||||
UserInterface = userInterface;
|
||||
Player = player;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible results for a <see cref="BoundUserInterfaceCheckRangeEvent"/>.
|
||||
/// </summary>
|
||||
public enum BoundUserInterfaceRangeResult : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Run built-in range check.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Range check passed, UI is accessible.
|
||||
/// </summary>
|
||||
Pass,
|
||||
|
||||
/// <summary>
|
||||
/// Range check failed, UI is inaccessible.
|
||||
/// </summary>
|
||||
Fail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace Robust.Server.GameObjects
|
||||
StartEntity(entity);
|
||||
}
|
||||
|
||||
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
|
||||
internal override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
|
||||
{
|
||||
if (prototypeName == null)
|
||||
return base.CreateEntity(prototypeName, out metadata, context);
|
||||
|
||||
@@ -51,10 +51,15 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
|
||||
public (GameTick ToTick, List<PvsIndex> PreviouslySent)? LastSent;
|
||||
|
||||
/// <summary>
|
||||
/// Visible chunks, sorted by proximity to the clients's viewers;
|
||||
/// Visible chunks, sorted by proximity to the client's viewers.
|
||||
/// </summary>
|
||||
public readonly List<(PvsChunk Chunk, float ChebyshevDistance)> Chunks = new();
|
||||
|
||||
/// <summary>
|
||||
/// Unsorted set of visible chunks. Used to construct the <see cref="Chunks"/> list.
|
||||
/// </summary>
|
||||
public readonly HashSet<PvsChunk> ChunkSet = new();
|
||||
|
||||
/// <summary>
|
||||
/// Squared distance ta all of the visible chunks.
|
||||
/// </summary>
|
||||
@@ -117,6 +122,7 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
|
||||
{
|
||||
PlayerStates.Clear();
|
||||
Chunks.Clear();
|
||||
ChunkSet.Clear();
|
||||
States.Clear();
|
||||
State = null;
|
||||
}
|
||||
|
||||
@@ -90,15 +90,13 @@ internal sealed partial class PvsSystem
|
||||
foreach (var session in _sessions)
|
||||
{
|
||||
session.Chunks.Clear();
|
||||
session.ChunkSet.Clear();
|
||||
GetSessionViewers(session);
|
||||
|
||||
foreach (var eye in session.Viewers)
|
||||
{
|
||||
GetVisibleChunks(eye, session.Chunks);
|
||||
GetVisibleChunks(eye, session.ChunkSet);
|
||||
}
|
||||
|
||||
// The list of visible chunks should be unique.
|
||||
DebugTools.Assert(session.Chunks.Select(x => x.Chunk).ToHashSet().Count == session.Chunks.Count);
|
||||
}
|
||||
DebugTools.Assert(_dirtyChunks.ToHashSet().Count == _dirtyChunks.Count);
|
||||
DebugTools.Assert(_cleanChunks.ToHashSet().Count == _cleanChunks.Count);
|
||||
@@ -108,7 +106,7 @@ internal sealed partial class PvsSystem
|
||||
/// Get the chunks visible to a single entity and add them to a player's set of visible chunks.
|
||||
/// </summary>
|
||||
private void GetVisibleChunks(Entity<TransformComponent, EyeComponent?> eye,
|
||||
List<(PvsChunk Chunk, float ChebyshevDistance)> playerChunks)
|
||||
HashSet<PvsChunk> chunks)
|
||||
{
|
||||
var (viewPos, range, mapUid) = CalcViewBounds(eye);
|
||||
if (mapUid is not {} map)
|
||||
@@ -121,7 +119,7 @@ internal sealed partial class PvsSystem
|
||||
if (!_chunks.TryGetValue(loc, out var chunk))
|
||||
continue;
|
||||
|
||||
playerChunks.Add((chunk, default));
|
||||
chunks.Add(chunk);
|
||||
if (chunk.UpdateQueued)
|
||||
continue;
|
||||
|
||||
@@ -147,7 +145,7 @@ internal sealed partial class PvsSystem
|
||||
if (!_chunks.TryGetValue(loc, out var chunk))
|
||||
continue;
|
||||
|
||||
playerChunks.Add((chunk, default));
|
||||
chunks.Add(chunk);
|
||||
if (chunk.UpdateQueued)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -55,13 +55,10 @@ internal sealed partial class PvsSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var root = (xform.GridUid ?? xform.MapUid);
|
||||
DebugTools.AssertNotNull(root);
|
||||
|
||||
if (xform.ParentUid != root)
|
||||
if (xform.ParentUid != xform.GridUid && xform.ParentUid != xform.MapUid)
|
||||
return;
|
||||
|
||||
var location = new PvsChunkLocation(root.Value, GetChunkIndices(xform._localPosition));
|
||||
var location = new PvsChunkLocation(xform.ParentUid, GetChunkIndices(xform._localPosition));
|
||||
if (meta.LastPvsLocation == location)
|
||||
return;
|
||||
|
||||
|
||||
@@ -50,9 +50,11 @@ internal sealed partial class PvsSystem
|
||||
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 (state != null)
|
||||
DebugTools.Assert(fromTick > component.CreationTick || state is not IComponentDeltaState);
|
||||
|
||||
if (sendCompList)
|
||||
netComps!.Add(netId);
|
||||
}
|
||||
@@ -85,7 +87,7 @@ internal sealed partial class PvsSystem
|
||||
continue;
|
||||
|
||||
var state = EntityManager.GetComponentState(bus, component, player, GameTick.Zero);
|
||||
DebugTools.Assert(state is not IComponentDeltaState delta || delta.FullState);
|
||||
DebugTools.Assert(state is not IComponentDeltaState);
|
||||
changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
|
||||
netComps.Add(netId);
|
||||
}
|
||||
|
||||
@@ -137,15 +137,19 @@ internal sealed partial class PvsSystem
|
||||
if (!CullingEnabled || session.DisableCulling)
|
||||
return;
|
||||
|
||||
var chunkSet = session.ChunkSet;
|
||||
var chunks = session.Chunks;
|
||||
var distances = session.ChunkDistanceSq;
|
||||
|
||||
DebugTools.AssertEqual(chunks.Count, 0);
|
||||
|
||||
distances.Clear();
|
||||
distances.EnsureCapacity(chunks.Count);
|
||||
distances.EnsureCapacity(chunkSet.Count);
|
||||
chunks.EnsureCapacity(chunkSet.Count);
|
||||
|
||||
// Assemble list of chunks and their distances to the nearest eye.
|
||||
foreach (ref var tuple in CollectionsMarshal.AsSpan(chunks))
|
||||
foreach(var chunk in chunkSet)
|
||||
{
|
||||
var chunk = tuple.Chunk;
|
||||
var dist = float.MaxValue;
|
||||
var chebDist = float.MaxValue;
|
||||
|
||||
@@ -165,7 +169,7 @@ internal sealed partial class PvsSystem
|
||||
}
|
||||
|
||||
distances.Add(dist);
|
||||
tuple.ChebyshevDistance = chebDist;
|
||||
chunks.Add((chunk, chebDist));
|
||||
}
|
||||
|
||||
// Sort chunks based on distances
|
||||
|
||||
@@ -134,7 +134,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
SubscribeLocalEvent<TransformComponent, TransformStartupEvent>(OnTransformStartup);
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
_transform.OnGlobalMoveEvent += OnEntityMove;
|
||||
_transform.OnBeforeMoveEvent += OnEntityMove;
|
||||
EntityManager.EntityAdded += OnEntityAdded;
|
||||
EntityManager.EntityDeleted += OnEntityDeleted;
|
||||
EntityManager.AfterEntityFlush += AfterEntityFlush;
|
||||
@@ -159,7 +159,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
base.Shutdown();
|
||||
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
_transform.OnGlobalMoveEvent -= OnEntityMove;
|
||||
_transform.OnBeforeMoveEvent -= OnEntityMove;
|
||||
EntityManager.EntityAdded -= OnEntityAdded;
|
||||
EntityManager.EntityDeleted -= OnEntityDeleted;
|
||||
EntityManager.AfterEntityFlush -= AfterEntityFlush;
|
||||
|
||||
@@ -172,16 +172,10 @@ namespace Robust.Server.Placement
|
||||
}
|
||||
}
|
||||
|
||||
var created = _entityManager.SpawnEntity(entityTemplateName, coordinates);
|
||||
var created = _entityManager.Spawn(entityTemplateName, _xformSystem.ToMapCoordinates(coordinates), rotation: dirRcv.ToAngle());
|
||||
|
||||
var placementCreateEvent = new PlacementEntityEvent(created, coordinates, PlacementEventAction.Create, msg.MsgChannel.UserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementCreateEvent);
|
||||
|
||||
// Some entities immediately delete themselves
|
||||
if (_entityManager.EntityExists(created))
|
||||
{
|
||||
_entityManager.GetComponent<TransformComponent>(created).LocalRotation = dirRcv.ToAngle();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
<Content Include="server_config.toml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="run_server.bat">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<EmbeddedResource Include="ExtraMappedSerializerStrings.txt">
|
||||
<LogicalName>Robust.Server.ExtraMappedSerializerStrings.txt</LogicalName>
|
||||
</EmbeddedResource>
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Robust.Server.ServerStatus
|
||||
if (auth != _watchdogToken)
|
||||
{
|
||||
// Holy shit nobody read these logs please.
|
||||
_sawmill.Info(@"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
|
||||
_sawmill.Verbose(@"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
|
||||
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||
return true;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ namespace Robust.Server.ServerStatus
|
||||
|
||||
if (auth != _watchdogToken)
|
||||
{
|
||||
_sawmill.Warning(
|
||||
_sawmill.Verbose(
|
||||
"received POST /shutdown with invalid authentication token. Ignoring {0}, {1}", auth,
|
||||
_watchdogToken);
|
||||
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||
|
||||
2
Robust.Server/run_server.bat
Normal file
2
Robust.Server/run_server.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
Robust.Server.exe
|
||||
pause
|
||||
@@ -52,7 +52,7 @@ tags = ""
|
||||
# Must be in the form of an ss14:// or ss14s:// URI pointing to the status API.
|
||||
server_url = ""
|
||||
# Comma-separated list of URLs of hub servers to advertise to.
|
||||
hub_urls = "https://central.spacestation14.io/hub/"
|
||||
hub_urls = "https://hub.spacestation14.com/"
|
||||
|
||||
[build]
|
||||
# *Absolutely all of these can be supplied using a "build.json" file*
|
||||
@@ -98,5 +98,5 @@ hub_urls = "https://central.spacestation14.io/hub/"
|
||||
|
||||
# You should probably never EVER need to touch this, but if you need a custom auth server,
|
||||
# (the auth server being the one which manages Space Station 14 accounts), you change it here.
|
||||
# server = https://central.spacestation14.io/auth/
|
||||
# server = https://auth.spacestation14.com/
|
||||
|
||||
|
||||
@@ -272,4 +272,6 @@ public enum AudioFlags : byte
|
||||
/// Should the audio act as if attached to a grid?
|
||||
/// </summary>
|
||||
GridAudio = 1 << 0,
|
||||
|
||||
NoOcclusion = 1 << 1,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
@@ -32,11 +34,13 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
|
||||
[Dependency] protected readonly IRobustRandom RandMan = default!;
|
||||
[Dependency] protected readonly MetaDataSystem MetadataSys = default!;
|
||||
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Default max range at which the sound can be heard.
|
||||
/// </summary>
|
||||
public const float DefaultSoundRange = 20;
|
||||
public const float DefaultSoundRange = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Used in the PAS to designate the physics collision mask of occluders.
|
||||
@@ -90,7 +94,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
var currentPos = (entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart;
|
||||
var timeOffset = TimeSpan.FromSeconds(position - currentPos.TotalSeconds);
|
||||
|
||||
DebugTools.Assert(currentPos > TimeSpan.Zero);
|
||||
DebugTools.Assert(currentPos >= TimeSpan.Zero);
|
||||
|
||||
// Rounding.
|
||||
if (Math.Abs(timeOffset.TotalSeconds) <= 0.01)
|
||||
@@ -131,6 +135,45 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
return (float) (Timing.CurTime - (component.PauseTime ?? TimeSpan.Zero) - component.AudioStart).TotalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this audio as being map-based.
|
||||
/// </summary>
|
||||
public virtual void SetMapAudio(Entity<AudioComponent>? audio)
|
||||
{
|
||||
if (audio == null)
|
||||
return;
|
||||
|
||||
audio.Value.Comp.Global = true;
|
||||
MetadataSys.AddFlag(audio.Value.Owner, MetaDataFlags.Undetachable);
|
||||
}
|
||||
|
||||
public virtual void SetGridAudio(Entity<AudioComponent>? entity)
|
||||
{
|
||||
if (entity == null)
|
||||
return;
|
||||
|
||||
entity.Value.Comp.Flags |= AudioFlags.GridAudio;
|
||||
var gridUid = Transform(entity.Value).GridUid;
|
||||
|
||||
if (TryComp(gridUid, out PhysicsComponent? gridPhysics))
|
||||
{
|
||||
XformSystem.SetLocalPosition(entity.Value.Owner, gridPhysics.LocalCenter);
|
||||
}
|
||||
|
||||
if (TryComp(gridUid, out MapGridComponent? mapGrid))
|
||||
{
|
||||
var extents = mapGrid.LocalAABB.Extents;
|
||||
var minDistance = MathF.Max(extents.X, extents.Y);
|
||||
|
||||
entity.Value.Comp.Params = entity.Value.Comp.Params
|
||||
.WithMaxDistance(minDistance + DefaultSoundRange)
|
||||
.WithReferenceDistance(minDistance);
|
||||
}
|
||||
|
||||
entity.Value.Comp.Flags |= AudioFlags.NoOcclusion;
|
||||
Dirty(entity.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the shared state for an audio entity.
|
||||
/// </summary>
|
||||
@@ -252,16 +295,16 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
|
||||
#region AudioParams
|
||||
|
||||
protected AudioComponent SetupAudio(EntityUid uid, string? fileName, AudioParams? audioParams, TimeSpan? length = null)
|
||||
protected Entity<AudioComponent> SetupAudio(string? fileName, AudioParams? audioParams, bool initialize = true, TimeSpan? length = null)
|
||||
{
|
||||
DebugTools.Assert((!string.IsNullOrEmpty(fileName) || length is not null));
|
||||
var uid = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
|
||||
DebugTools.Assert(!string.IsNullOrEmpty(fileName) || length is not null);
|
||||
audioParams ??= AudioParams.Default;
|
||||
var comp = AddComp<AudioComponent>(uid);
|
||||
comp.FileName = fileName ?? string.Empty;
|
||||
comp.Params = audioParams.Value;
|
||||
comp.AudioStart = Timing.CurTime;
|
||||
|
||||
|
||||
if (!audioParams.Value.Loop)
|
||||
{
|
||||
length ??= GetAudioLength(fileName!);
|
||||
@@ -276,7 +319,12 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
comp.Params.Pitch *= (float) RandMan.NextGaussian(1, comp.Params.Variation.Value);
|
||||
}
|
||||
|
||||
return comp;
|
||||
if (initialize)
|
||||
{
|
||||
EntityManager.InitializeAndStartEntity(uid);
|
||||
}
|
||||
|
||||
return new Entity<AudioComponent>(uid, comp);
|
||||
}
|
||||
|
||||
public static float GainToVolume(float value)
|
||||
@@ -583,7 +631,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
[return: NotNullIfNotNull("sound")]
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayStatic(GetSound(sound), playerFilter, coordinates, recordReplay);
|
||||
return sound == null ? null : PlayStatic(GetSound(sound), playerFilter, coordinates, recordReplay, audioParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -368,6 +368,21 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<float> NetHappyEyeballsDelay =
|
||||
CVarDef.Create("net.happy_eyeballs_delay", 0.025f, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the networking library will log warning messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Disabling this should make the networking layer more resilient against some DDoS attacks.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<bool> NetLidgrenLogWarning =
|
||||
CVarDef.Create("net.lidgren_log_warning", true);
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the networking library will log error messages.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> NetLidgrenLogError =
|
||||
CVarDef.Create("net.lidgren_log_error", true);
|
||||
|
||||
/**
|
||||
* SUS
|
||||
*/
|
||||
@@ -884,7 +899,7 @@ namespace Robust.Shared
|
||||
CVarDef.Create("render.sprite_direction_bias", -0.05, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
public static readonly CVarDef<string> RenderFOVColor =
|
||||
CVarDef.Create("render.fov_color", Color.Black.ToHex(), CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
CVarDef.Create("render.fov_color", Color.Black.ToHex(), CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/*
|
||||
* CONTROLS
|
||||
@@ -1632,15 +1647,20 @@ namespace Robust.Shared
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ReplaySkipThreshold = CVarDef.Create("replay.skip_threshold", 30);
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of ticks before a new checkpoint tick is generated (overrides SpawnThreshold and StateThreshold)
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> CheckpointMinInterval = CVarDef.Create("replay.checkpoint_min_interval", 60);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of ticks before a new checkpoint tick is generated.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> CheckpointInterval = CVarDef.Create("replay.checkpoint_interval", 200);
|
||||
public static readonly CVarDef<int> CheckpointInterval = CVarDef.Create("replay.checkpoint_interval", 500);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of entities that can be spawned before a new checkpoint tick is generated.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> CheckpointEntitySpawnThreshold = CVarDef.Create("replay.checkpoint_entity_spawn_threshold", 100);
|
||||
public static readonly CVarDef<int> CheckpointEntitySpawnThreshold = CVarDef.Create("replay.checkpoint_entity_spawn_threshold", 1000);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of entity states that can be applied before a new checkpoint tick is generated.
|
||||
|
||||
@@ -294,6 +294,9 @@ public struct ValueList<T> : IEnumerable<T>
|
||||
if (Capacity < capacity)
|
||||
Grow(capacity);
|
||||
|
||||
if (capacity == 0)
|
||||
return capacity;
|
||||
|
||||
return _items!.Length;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO EXCEPTION TOLERANCE
|
||||
// Ensure lookup trees update before content code handles move events.
|
||||
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ internal sealed class RecursiveMoveSystem : EntitySystem
|
||||
public override void Shutdown()
|
||||
{
|
||||
if (_subscribed)
|
||||
_transform.OnGlobalMoveEvent -= AnythingMoved;
|
||||
_transform.OnBeforeMoveEvent -= AnythingMoved;
|
||||
|
||||
_subscribed = false;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ internal sealed class RecursiveMoveSystem : EntitySystem
|
||||
return;
|
||||
|
||||
_subscribed = true;
|
||||
_transform.OnGlobalMoveEvent += AnythingMoved;
|
||||
_transform.OnBeforeMoveEvent += AnythingMoved;
|
||||
}
|
||||
|
||||
private void AnythingMoved(ref MoveEvent args)
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public event Action<CVarChangeInfo>? OnCVarValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new ConfigurationManager.
|
||||
/// </summary>
|
||||
@@ -94,15 +96,15 @@ namespace Robust.Shared.Configuration
|
||||
if (_configVars.TryGetValue(cvar, out var cfgVar))
|
||||
{
|
||||
// overwrite the value with the saved one
|
||||
var oldValue = GetConfigVarValue(cfgVar);
|
||||
changedInvokes.Add(SetupInvokeValueChanged(cfgVar, value, oldValue));
|
||||
cfgVar.Value = value;
|
||||
if (SetupInvokeValueChanged(cfgVar, value) is { } invoke)
|
||||
changedInvokes.Add(invoke);
|
||||
}
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
cfgVar = new ConfigVar(cvar, 0, CVar.NONE) { Value = value };
|
||||
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
|
||||
cfgVar = new ConfigVar(cvar, null!, CVar.NONE) { Value = value };
|
||||
_configVars.Add(cvar, cfgVar);
|
||||
}
|
||||
|
||||
@@ -127,26 +129,26 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
|
||||
var convertedValue = value;
|
||||
if (!cVar.DefaultValue.GetType().IsEnum && cVar.DefaultValue.GetType() != value.GetType())
|
||||
if (cVar.Type != value.GetType())
|
||||
{
|
||||
try
|
||||
{
|
||||
convertedValue = ConvertToCVarType(value, cVar.DefaultValue.GetType());
|
||||
convertedValue = ConvertToCVarType(value, cVar.Type!);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_sawmill.Error($"Override TOML parsed cvar does not match registered cvar type. Name: {cVarName}. Code Type: {cVar.DefaultValue.GetType()}. Toml type: {value.GetType()}");
|
||||
_sawmill.Error($"Override TOML parsed cvar does not match registered cvar type. Name: {cVarName}. Code Type: {cVar.Type}. Toml type: {value.GetType()}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cVar.DefaultValue = value;
|
||||
|
||||
if (cVar.OverrideValue == null && cVar.Value == null)
|
||||
{
|
||||
if (SetupInvokeValueChanged(cVar, convertedValue) is { } invoke)
|
||||
callbackEvents.Add(invoke);
|
||||
var oldValue = GetConfigVarValue(cVar);
|
||||
callbackEvents.Add(SetupInvokeValueChanged(cVar, convertedValue, oldValue));
|
||||
}
|
||||
|
||||
cVar.DefaultValue = convertedValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +319,7 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
private void RegisterCVar(string name, Type type, object defaultValue, CVar flags)
|
||||
{
|
||||
DebugTools.AssertEqual(defaultValue.GetType(), type);
|
||||
DebugTools.Assert(!type.IsEnum || type.GetEnumUnderlyingType() == typeof(int),
|
||||
$"{name}: Enum cvars must have int as underlying type.");
|
||||
|
||||
@@ -335,7 +338,7 @@ namespace Robust.Shared.Configuration
|
||||
if (cVar.Registered)
|
||||
_sawmill.Error($"The variable '{name}' has already been registered.");
|
||||
|
||||
if (!type.IsEnum && cVar.Value != null && !type.IsAssignableFrom(cVar.Value.GetType()))
|
||||
if (cVar.Value != null && type != cVar.Value.GetType())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -350,7 +353,7 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
cVar.DefaultValue = defaultValue;
|
||||
cVar.Flags = flags;
|
||||
cVar.Registered = true;
|
||||
cVar.Register();
|
||||
|
||||
if (cVar.OverrideValue != null)
|
||||
{
|
||||
@@ -360,10 +363,9 @@ namespace Robust.Shared.Configuration
|
||||
return;
|
||||
}
|
||||
|
||||
_configVars.Add(name, new ConfigVar(name, defaultValue, flags)
|
||||
{
|
||||
Registered = true
|
||||
});
|
||||
var cvar = new ConfigVar(name, defaultValue, flags);
|
||||
cvar.Register();
|
||||
_configVars.Add(name, cvar);
|
||||
}
|
||||
|
||||
public void OnValueChanged<T>(CVarDef<T> cVar, Action<T> onValueChanged, bool invokeImmediately = false)
|
||||
@@ -412,10 +414,11 @@ namespace Robust.Shared.Configuration
|
||||
public void OnValueChanged<T>(string name, CVarChanged<T> onValueChanged, bool invokeImmediately = false)
|
||||
where T : notnull
|
||||
{
|
||||
object value;
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
var reg = _configVars[name];
|
||||
|
||||
value = GetConfigVarValue(reg);
|
||||
reg.ValueChanged.AddInPlace(
|
||||
(object value, in CVarChangeInfo info) => onValueChanged((T)value, info),
|
||||
onValueChanged);
|
||||
@@ -423,7 +426,7 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
if (invokeImmediately)
|
||||
{
|
||||
onValueChanged(GetCVar<T>(name), new CVarChangeInfo(_gameTiming.CurTick));
|
||||
onValueChanged(GetCVar<T>(name), new CVarChangeInfo(name, _gameTiming.CurTick, value, value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,12 +515,13 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
if (!Equals(cVar.OverrideValueParsed ?? cVar.Value, value))
|
||||
{
|
||||
var oldValue = GetConfigVarValue(cVar);
|
||||
invoke = SetupInvokeValueChanged(cVar, value, oldValue, intendedTick);
|
||||
|
||||
// Setting an overriden var just turns off the override, basically.
|
||||
cVar.OverrideValue = null;
|
||||
cVar.OverrideValueParsed = null;
|
||||
|
||||
cVar.Value = value;
|
||||
invoke = SetupInvokeValueChanged(cVar, value, intendedTick);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -543,10 +547,14 @@ namespace Robust.Shared.Configuration
|
||||
if (!_configVars.TryGetValue(name, out var cVar) || !cVar.Registered)
|
||||
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
|
||||
|
||||
if (cVar.OverrideValue == null && cVar.Value == null)
|
||||
{
|
||||
var oldValue = GetConfigVarValue(cVar);
|
||||
invoke = SetupInvokeValueChanged(cVar, value, oldValue);
|
||||
}
|
||||
|
||||
cVar.DefaultValue = value;
|
||||
|
||||
if (cVar.OverrideValue == null && cVar.Value == null)
|
||||
invoke = SetupInvokeValueChanged(cVar, value);
|
||||
}
|
||||
|
||||
if (invoke != null)
|
||||
@@ -587,7 +595,7 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
|
||||
// If it's null it's a string, since the rest is primitives which aren't null.
|
||||
return cVar.DefaultValue.GetType();
|
||||
return cVar.Type!;
|
||||
}
|
||||
|
||||
protected static object GetConfigVarValue(ConfigVar cVar)
|
||||
@@ -606,18 +614,19 @@ namespace Robust.Shared.Configuration
|
||||
if (_configVars.TryGetValue(key, out var cfgVar))
|
||||
{
|
||||
cfgVar.OverrideValue = value;
|
||||
if (cfgVar.Registered)
|
||||
{
|
||||
cfgVar.OverrideValueParsed = ParseOverrideValue(value, cfgVar.DefaultValue?.GetType());
|
||||
if (SetupInvokeValueChanged(cfgVar, cfgVar.OverrideValueParsed) is { } invoke)
|
||||
invokes.Add(invoke);
|
||||
}
|
||||
if (!cfgVar.Registered)
|
||||
continue;
|
||||
|
||||
var newValue = ParseOverrideValue(value, cfgVar.Type!);
|
||||
var oldValue = GetConfigVarValue(cfgVar);
|
||||
invokes.Add(SetupInvokeValueChanged(cfgVar, newValue, oldValue));
|
||||
cfgVar.OverrideValueParsed = newValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
var cVar = new ConfigVar(key, 0, CVar.NONE) { OverrideValue = value };
|
||||
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
|
||||
var cVar = new ConfigVar(key, null!, CVar.NONE) { OverrideValue = value };
|
||||
_configVars.Add(key, cVar);
|
||||
}
|
||||
}
|
||||
@@ -696,25 +705,20 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
private static void InvokeValueChanged(in ValueChangedInvoke invoke)
|
||||
private void InvokeValueChanged(in ValueChangedInvoke invoke)
|
||||
{
|
||||
OnCVarValueChanged?.Invoke(invoke.Info);
|
||||
foreach (var entry in invoke.Invoke.Entries)
|
||||
{
|
||||
entry.Value!.Invoke(invoke.Value, in invoke.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private ValueChangedInvoke? SetupInvokeValueChanged(ConfigVar var, object value, GameTick? tick = null)
|
||||
private ValueChangedInvoke SetupInvokeValueChanged(ConfigVar var, object newValue, object oldValue, GameTick? tick = null)
|
||||
{
|
||||
if (var.ValueChanged.Count == 0)
|
||||
return null;
|
||||
|
||||
return new ValueChangedInvoke
|
||||
{
|
||||
Info = new CVarChangeInfo(tick ?? _gameTiming.CurTick),
|
||||
Invoke = var.ValueChanged,
|
||||
Value = value
|
||||
};
|
||||
tick ??= _gameTiming.CurTick;
|
||||
var info = new CVarChangeInfo(var.Name, tick.Value, newValue, oldValue);
|
||||
return new ValueChangedInvoke(info, var.ValueChanged);
|
||||
}
|
||||
|
||||
private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(Stream stream)
|
||||
@@ -767,6 +771,9 @@ namespace Robust.Shared.Configuration
|
||||
/// <returns></returns>
|
||||
private static object ConvertToCVarType(object value, Type cVar)
|
||||
{
|
||||
if (cVar.IsEnum)
|
||||
return Enum.Parse(cVar, value.ToString() ?? string.Empty);
|
||||
|
||||
return Convert.ChangeType(value, cVar);
|
||||
}
|
||||
|
||||
@@ -786,10 +793,15 @@ namespace Robust.Shared.Configuration
|
||||
public ConfigVar(string name, object defaultValue, CVar flags)
|
||||
{
|
||||
Name = name;
|
||||
DefaultValue = defaultValue;
|
||||
Flags = flags;
|
||||
_defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of the cvar's value. This may be null until the cvar is registered.
|
||||
/// </summary>
|
||||
public Type? Type { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the CVar.
|
||||
/// </summary>
|
||||
@@ -798,7 +810,16 @@ namespace Robust.Shared.Configuration
|
||||
/// <summary>
|
||||
/// The default value of this CVar.
|
||||
/// </summary>
|
||||
public object DefaultValue { get; set; }
|
||||
public object DefaultValue
|
||||
{
|
||||
get => _defaultValue;
|
||||
set
|
||||
{
|
||||
if (Registered)
|
||||
DebugTools.AssertEqual(value.GetType(), Type);
|
||||
_defaultValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional flags to modify the behavior of this CVar.
|
||||
@@ -808,12 +829,45 @@ namespace Robust.Shared.Configuration
|
||||
/// <summary>
|
||||
/// The current value of this CVar.
|
||||
/// </summary>
|
||||
public object? Value { get; set; }
|
||||
public object? Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (value != null && Registered)
|
||||
DebugTools.AssertEqual(value.GetType(), Type);
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Has this CVar been registered in code?
|
||||
/// </summary>
|
||||
public bool Registered { get; set; }
|
||||
public bool Registered { get; private set; }
|
||||
|
||||
public void Register()
|
||||
{
|
||||
if (Registered)
|
||||
{
|
||||
DebugTools.AssertNotNull(DefaultValue);
|
||||
DebugTools.AssertEqual(DefaultValue.GetType(), Type);
|
||||
DebugTools.Assert(Value == null || Value.GetType() == Type);
|
||||
DebugTools.Assert(OverrideValueParsed == null || OverrideValueParsed.GetType() == Type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_defaultValue == null)
|
||||
throw new NullReferenceException("Must specify default value before registering");
|
||||
|
||||
if (Value != null && DefaultValue.GetType() != Value.GetType())
|
||||
throw new Exception($"The cvar value & default value must be of the same type");
|
||||
|
||||
if (OverrideValueParsed != null && DefaultValue.GetType() != OverrideValueParsed.GetType())
|
||||
throw new Exception($"The cvar override value & default value must be of the same type");
|
||||
|
||||
Type = DefaultValue.GetType();
|
||||
Registered = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Was the CVar present in the config file?
|
||||
@@ -822,12 +876,25 @@ namespace Robust.Shared.Configuration
|
||||
public bool ConfigModified;
|
||||
|
||||
public InvokeList<ValueChangedDelegate> ValueChanged;
|
||||
private object _defaultValue;
|
||||
private object? _value;
|
||||
private object? _overrideValueParsed;
|
||||
|
||||
// We don't know what the type of the var is until it's registered.
|
||||
// So we can't actually parse them until then.
|
||||
// So we keep the raw string around.
|
||||
public string? OverrideValue { get; set; }
|
||||
public object? OverrideValueParsed { get; set; }
|
||||
|
||||
public object? OverrideValueParsed
|
||||
{
|
||||
get => _overrideValueParsed;
|
||||
set
|
||||
{
|
||||
if (value != null && Registered)
|
||||
DebugTools.AssertEqual(value.GetType(), Type);
|
||||
_overrideValueParsed = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -836,8 +903,14 @@ namespace Robust.Shared.Configuration
|
||||
private struct ValueChangedInvoke
|
||||
{
|
||||
public InvokeList<ValueChangedDelegate> Invoke;
|
||||
public object Value;
|
||||
public object Value => Info.NewValue;
|
||||
public CVarChangeInfo Info;
|
||||
|
||||
public ValueChangedInvoke(CVarChangeInfo info, InvokeList<ValueChangedDelegate> invoke) : this()
|
||||
{
|
||||
Info = info;
|
||||
Invoke = invoke;
|
||||
}
|
||||
}
|
||||
|
||||
protected delegate void ValueChangedDelegate(object value, in CVarChangeInfo info);
|
||||
|
||||
@@ -10,6 +10,11 @@ namespace Robust.Shared.Configuration
|
||||
/// </summary>
|
||||
public readonly struct CVarChangeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the cvar that was changed.
|
||||
/// </summary>
|
||||
public readonly string Name;
|
||||
|
||||
/// <summary>
|
||||
/// The tick this CVar changed at.
|
||||
/// </summary>
|
||||
@@ -20,9 +25,22 @@ namespace Robust.Shared.Configuration
|
||||
/// </remarks>
|
||||
public readonly GameTick TickChanged;
|
||||
|
||||
internal CVarChangeInfo(GameTick tickChanged)
|
||||
/// <summary>
|
||||
/// The new value.
|
||||
/// </summary>
|
||||
public readonly object NewValue;
|
||||
|
||||
/// <summary>
|
||||
/// The previous value.
|
||||
/// </summary>
|
||||
public readonly object OldValue;
|
||||
|
||||
internal CVarChangeInfo(string name, GameTick tickChanged, object newValue, object oldValue)
|
||||
{
|
||||
Name = name;
|
||||
TickChanged = tickChanged;
|
||||
NewValue = newValue;
|
||||
OldValue = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,5 +270,7 @@ namespace Robust.Shared.Configuration
|
||||
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
|
||||
void UnsubValueChanged<T>(string name, CVarChanged<T> onValueChanged)
|
||||
where T : notnull;
|
||||
|
||||
public event Action<CVarChangeInfo>? OnCVarValueChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
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.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -23,6 +15,19 @@ namespace Robust.Shared.Containers
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract partial class BaseContainer
|
||||
{
|
||||
// Will be null until after the component has been initialized.
|
||||
protected SharedContainerSystem? System;
|
||||
|
||||
[Access(typeof(SharedContainerSystem), typeof(ContainerManagerComponent))]
|
||||
internal void Init(SharedContainerSystem system, string id, Entity<ContainerManagerComponent> owner)
|
||||
{
|
||||
DebugTools.Assert(ID == null || ID == id);
|
||||
ID = id;
|
||||
Owner = owner;
|
||||
Manager = owner;
|
||||
System = system;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Readonly collection of all the entities contained within this specific container
|
||||
/// </summary>
|
||||
|
||||
@@ -2,10 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Containers
|
||||
@@ -51,14 +49,7 @@ namespace Robust.Shared.Containers
|
||||
if (!_containerList.Contains(contained))
|
||||
return false;
|
||||
|
||||
#if DEBUG
|
||||
if (IoCManager.Resolve<IGameTiming>().ApplyingState)
|
||||
return true;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var flags = entMan.GetComponent<MetaDataComponent>(contained).Flags;
|
||||
DebugTools.Assert((flags & MetaDataFlags.InContainer) != 0, $"Entity has bad container flags. Ent: {entMan.ToPrettyString(contained)}. Container: {ID}, Owner: {entMan.ToPrettyString(Owner)}");
|
||||
#endif
|
||||
System?.AssertInContainer(contained, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,7 @@ namespace Robust.Shared.Containers
|
||||
{
|
||||
foreach (var (id, container) in Containers)
|
||||
{
|
||||
container.ID = id;
|
||||
container.Owner = Owner;
|
||||
container.Manager = this;
|
||||
container.Init(default!, id, (Owner, this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Containers
|
||||
@@ -59,14 +57,7 @@ namespace Robust.Shared.Containers
|
||||
if (contained != ContainedEntity)
|
||||
return false;
|
||||
|
||||
#if DEBUG
|
||||
if (IoCManager.Resolve<IGameTiming>().ApplyingState)
|
||||
return true;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var flags = entMan.GetComponent<MetaDataComponent>(contained).Flags;
|
||||
DebugTools.Assert((flags & MetaDataFlags.InContainer) != 0, $"Entity has bad container flags. Ent: {entMan.ToPrettyString(contained)}. Container: {ID}, Owner: {entMan.ToPrettyString(Owner)}");
|
||||
#endif
|
||||
System?.AssertInContainer(contained, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -12,6 +13,7 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Containers
|
||||
@@ -24,6 +26,7 @@ namespace Robust.Shared.Containers
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedJointSystem _joint = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private EntityQuery<ContainerManagerComponent> _managerQuery;
|
||||
private EntityQuery<MapGridComponent> _gridQuery;
|
||||
@@ -39,6 +42,7 @@ namespace Robust.Shared.Containers
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<EntParentChangedMessage>(OnParentChanged);
|
||||
SubscribeLocalEvent<ContainerManagerComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<ContainerManagerComponent, ComponentStartup>(OnStartupValidation);
|
||||
SubscribeLocalEvent<ContainerManagerComponent, ComponentGetState>(OnContainerGetState);
|
||||
SubscribeLocalEvent<ContainerManagerComponent, ComponentRemove>(OnContainerManagerRemove);
|
||||
@@ -52,9 +56,18 @@ namespace Robust.Shared.Containers
|
||||
TransformQuery = GetEntityQuery<TransformComponent>();
|
||||
}
|
||||
|
||||
private void OnInit(Entity<ContainerManagerComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
foreach (var (id, container) in ent.Comp.Containers)
|
||||
{
|
||||
container.Init(this, id, ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnContainerGetState(EntityUid uid, ContainerManagerComponent component, ref ComponentGetState args)
|
||||
{
|
||||
Dictionary<string, ContainerManagerComponent.ContainerManagerComponentState.ContainerData> containerSet = new(component.Containers.Count);
|
||||
Dictionary<string, ContainerManagerComponent.ContainerManagerComponentState.ContainerData> containerSet =
|
||||
new(component.Containers.Count);
|
||||
|
||||
foreach (var container in component.Containers.Values)
|
||||
{
|
||||
@@ -65,7 +78,11 @@ namespace Robust.Shared.Containers
|
||||
uidArr[index] = GetNetEntity(container.ContainedEntities[index]);
|
||||
}
|
||||
|
||||
var sContainer = new ContainerManagerComponent.ContainerManagerComponentState.ContainerData(container.GetType().Name, container.ShowContents, container.OccludesLight, uidArr);
|
||||
var sContainer =
|
||||
new ContainerManagerComponent.ContainerManagerComponentState.ContainerData(container.GetType().Name,
|
||||
container.ShowContents,
|
||||
container.OccludesLight,
|
||||
uidArr);
|
||||
containerSet.Add(container.ID, sContainer);
|
||||
}
|
||||
|
||||
@@ -96,18 +113,12 @@ namespace Robust.Shared.Containers
|
||||
throw new ArgumentException($"Container with specified ID already exists: '{id}'");
|
||||
|
||||
var container = _dynFactory.CreateInstanceUnchecked<T>(typeof(T), inject: false);
|
||||
InitContainer(container, (uid, containerManager), id);
|
||||
container.Init(this, id, (uid, containerManager));
|
||||
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 virtual void ShutdownContainer(BaseContainer container)
|
||||
{
|
||||
container.InternalShutdown(EntityManager, this, _net.IsClient);
|
||||
@@ -115,7 +126,11 @@ namespace Robust.Shared.Containers
|
||||
container.ExpectedEntities.Clear();
|
||||
}
|
||||
|
||||
public T EnsureContainer<T>(EntityUid uid, string id, out bool alreadyExisted, ContainerManagerComponent? containerManager = null)
|
||||
public T EnsureContainer<T>(
|
||||
EntityUid uid,
|
||||
string id,
|
||||
out bool alreadyExisted,
|
||||
ContainerManagerComponent? containerManager = null)
|
||||
where T : BaseContainer
|
||||
{
|
||||
if (!Resolve(uid, ref containerManager, false))
|
||||
@@ -136,7 +151,7 @@ namespace Robust.Shared.Containers
|
||||
}
|
||||
|
||||
public T EnsureContainer<T>(EntityUid uid, string id, ContainerManagerComponent? containerManager = null)
|
||||
where T : BaseContainer
|
||||
where T : BaseContainer
|
||||
{
|
||||
return EnsureContainer<T>(uid, id, out _, containerManager);
|
||||
}
|
||||
@@ -157,7 +172,11 @@ namespace Robust.Shared.Containers
|
||||
return containerManager.Containers.ContainsKey(id);
|
||||
}
|
||||
|
||||
public bool TryGetContainer(EntityUid uid, string id, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null)
|
||||
public bool TryGetContainer(
|
||||
EntityUid uid,
|
||||
string id,
|
||||
[NotNullWhen(true)] out BaseContainer? container,
|
||||
ContainerManagerComponent? containerManager = null)
|
||||
{
|
||||
if (!Resolve(uid, ref containerManager, false))
|
||||
{
|
||||
@@ -165,16 +184,30 @@ namespace Robust.Shared.Containers
|
||||
return false;
|
||||
}
|
||||
|
||||
return containerManager.Containers.TryGetValue(id, out container);
|
||||
if (!containerManager.Containers.TryGetValue(id, out container))
|
||||
return false;
|
||||
|
||||
DebugTools.AssertEqual(container.ID, id);
|
||||
DebugTools.AssertNotNull(container.Manager);
|
||||
DebugTools.AssertNotEqual(container.Owner, EntityUid.Invalid);
|
||||
return true;
|
||||
}
|
||||
|
||||
[Obsolete("Use variant without skipExistCheck argument")]
|
||||
public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, bool skipExistCheck)
|
||||
public bool TryGetContainingContainer(
|
||||
EntityUid uid,
|
||||
EntityUid containedUid,
|
||||
[NotNullWhen(true)] out BaseContainer? container,
|
||||
bool skipExistCheck)
|
||||
{
|
||||
return TryGetContainingContainer(uid, containedUid, out container);
|
||||
}
|
||||
|
||||
public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null)
|
||||
public bool TryGetContainingContainer(
|
||||
EntityUid uid,
|
||||
EntityUid containedUid,
|
||||
[NotNullWhen(true)] out BaseContainer? container,
|
||||
ContainerManagerComponent? containerManager = null)
|
||||
{
|
||||
DebugTools.Assert(Exists(containedUid));
|
||||
if (!Resolve(uid, ref containerManager, false))
|
||||
@@ -196,7 +229,10 @@ namespace Robust.Shared.Containers
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsEntity(EntityUid uid, EntityUid containedUid, ContainerManagerComponent? containerManager = null)
|
||||
public bool ContainsEntity(
|
||||
EntityUid uid,
|
||||
EntityUid containedUid,
|
||||
ContainerManagerComponent? containerManager = null)
|
||||
{
|
||||
DebugTools.Assert(Exists(containedUid));
|
||||
if (!Resolve(uid, ref containerManager, false))
|
||||
@@ -228,13 +264,20 @@ namespace Robust.Shared.Containers
|
||||
foreach (var containers in containerManager.Containers.Values)
|
||||
{
|
||||
if (containers.Contains(toremove))
|
||||
return Remove((toremove, containedXform, containedMeta), containers, reparent, force, destination, localRotation);
|
||||
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)
|
||||
public ContainerManagerComponent.AllContainersEnumerable GetAllContainers(
|
||||
EntityUid uid,
|
||||
ContainerManagerComponent? containerManager = null)
|
||||
{
|
||||
if (!Resolve(uid, ref containerManager))
|
||||
return new ContainerManagerComponent.AllContainersEnumerable();
|
||||
@@ -246,20 +289,32 @@ namespace Robust.Shared.Containers
|
||||
|
||||
#region Container Helpers
|
||||
|
||||
public bool TryGetContainingContainer(EntityUid uid, [NotNullWhen(true)] out BaseContainer? container, MetaDataComponent? meta = null, TransformComponent? transform = null)
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool TryGetContainingContainer(
|
||||
EntityUid uid,
|
||||
[NotNullWhen(true)] out BaseContainer? container,
|
||||
MetaDataComponent? meta = null,
|
||||
TransformComponent? transform = null)
|
||||
{
|
||||
return TryGetContainingContainer((uid, transform, meta), out container);
|
||||
}
|
||||
|
||||
public bool TryGetContainingContainer(
|
||||
Entity<TransformComponent?, MetaDataComponent?> ent,
|
||||
[NotNullWhen(true)] out BaseContainer? container)
|
||||
{
|
||||
container = null;
|
||||
|
||||
if (!Resolve(uid, ref meta, false))
|
||||
if (!Resolve(ent, ref ent.Comp2, false))
|
||||
return false;
|
||||
|
||||
if ((meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.None)
|
||||
if ((ent.Comp2.Flags & MetaDataFlags.InContainer) == MetaDataFlags.None)
|
||||
return false;
|
||||
|
||||
if (!Resolve(uid, ref transform, false))
|
||||
if (!Resolve(ent, ref ent.Comp1, false))
|
||||
return false;
|
||||
|
||||
return TryGetContainingContainer(transform.ParentUid, uid, out container);
|
||||
return TryGetContainingContainer(ent.Comp1.ParentUid, ent, out container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -356,10 +411,19 @@ namespace Robust.Shared.Containers
|
||||
return TryFindComponentsOnEntityContainerOrParent(xform.ParentUid, entityQuery, foundComponents);
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool IsInSameOrNoContainer(EntityUid user, EntityUid other)
|
||||
{
|
||||
return IsInSameOrNoContainer((user, null, null), (other, null, null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the two entities are not contained, or are contained in the same container.
|
||||
/// </summary>
|
||||
public bool IsInSameOrNoContainer(EntityUid user, EntityUid other)
|
||||
public bool IsInSameOrNoContainer(
|
||||
Entity<TransformComponent?, MetaDataComponent?> user,
|
||||
Entity<TransformComponent?, MetaDataComponent?> other)
|
||||
{
|
||||
var isUserContained = TryGetContainingContainer(user, out var userContainer);
|
||||
var isOtherContained = TryGetContainingContainer(other, out var otherContainer);
|
||||
@@ -374,14 +438,33 @@ namespace Robust.Shared.Containers
|
||||
return userContainer == otherContainer;
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool IsInSameOrParentContainer(EntityUid user, EntityUid other)
|
||||
{
|
||||
return IsInSameOrParentContainer((user, null), other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the two entities are not contained, or are contained in the same container, or if one
|
||||
/// entity contains the other (i.e., is the parent).
|
||||
/// </summary>
|
||||
public bool IsInSameOrParentContainer(EntityUid user, EntityUid other)
|
||||
public bool IsInSameOrParentContainer(
|
||||
Entity<TransformComponent?, MetaDataComponent?> user,
|
||||
Entity<TransformComponent?, MetaDataComponent?> other)
|
||||
{
|
||||
var isUserContained = TryGetContainingContainer(user, out var userContainer);
|
||||
var isOtherContained = TryGetContainingContainer(other, out var otherContainer);
|
||||
return IsInSameOrParentContainer(user, other, out _, out _);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IsInSameOrParentContainer(Robust.Shared.GameObjects.Entity{Robust.Shared.GameObjects.TransformComponent?},Robust.Shared.GameObjects.Entity{Robust.Shared.GameObjects.TransformComponent?})"/>
|
||||
public bool IsInSameOrParentContainer(
|
||||
Entity<TransformComponent?, MetaDataComponent?> user,
|
||||
Entity<TransformComponent?, MetaDataComponent?> other,
|
||||
out BaseContainer? userContainer,
|
||||
out BaseContainer? otherContainer)
|
||||
{
|
||||
var isUserContained = TryGetContainingContainer(user, out userContainer);
|
||||
var isOtherContained = TryGetContainingContainer(other, out otherContainer);
|
||||
|
||||
// Both entities are not in a container
|
||||
if (!isUserContained && !isOtherContained) return true;
|
||||
@@ -396,6 +479,21 @@ namespace Robust.Shared.Containers
|
||||
return userContainer == otherContainer;
|
||||
}
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool IsInSameOrTransparentContainer(
|
||||
EntityUid user,
|
||||
EntityUid other,
|
||||
BaseContainer? userContainer = null,
|
||||
BaseContainer? otherContainer = null,
|
||||
bool userSeeInsideSelf = false)
|
||||
{
|
||||
return IsInSameOrTransparentContainer((user, null),
|
||||
other,
|
||||
userContainer,
|
||||
otherContainer,
|
||||
userSeeInsideSelf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a given entity can see another entity despite whatever containers they may be in.
|
||||
/// </summary>
|
||||
@@ -407,8 +505,8 @@ namespace Robust.Shared.Containers
|
||||
/// this means that the two entity arguments are NOT interchangeable.
|
||||
/// </remarks>
|
||||
public bool IsInSameOrTransparentContainer(
|
||||
EntityUid user,
|
||||
EntityUid other,
|
||||
Entity<TransformComponent?, MetaDataComponent?> user,
|
||||
Entity<TransformComponent?, MetaDataComponent?> other,
|
||||
BaseContainer? userContainer = null,
|
||||
BaseContainer? otherContainer = null,
|
||||
bool userSeeInsideSelf = false)
|
||||
@@ -433,11 +531,11 @@ namespace Robust.Shared.Containers
|
||||
|
||||
// Is the user in a see-through container?
|
||||
if (userContainer?.ShowContents ?? false)
|
||||
return IsInSameOrTransparentContainer(userContainer.Owner, other, otherContainer: otherContainer);
|
||||
return IsInSameOrTransparentContainer((userContainer.Owner, null, null), other, otherContainer: otherContainer);
|
||||
|
||||
// Is the other entity in a see-through container?
|
||||
if (otherContainer?.ShowContents ?? false)
|
||||
return IsInSameOrTransparentContainer(user, otherContainer.Owner, userContainer: userContainer, userSeeInsideSelf: userSeeInsideSelf);
|
||||
return IsInSameOrTransparentContainer(user, (otherContainer.Owner, null, null), userContainer: userContainer, userSeeInsideSelf: userSeeInsideSelf);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -603,5 +701,16 @@ namespace Robust.Shared.Containers
|
||||
if (TryComp(message.OldParent, out ContainerManagerComponent? containerManager))
|
||||
RemoveEntity(message.OldParent.Value, message.Entity, containerManager, message.Transform, meta, reparent: false, force: true);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG"), Access(typeof(BaseContainer))]
|
||||
public void AssertInContainer(EntityUid uid, BaseContainer container)
|
||||
{
|
||||
if (_timing.ApplyingState)
|
||||
return; // Entity might not yet have had its state updated.
|
||||
|
||||
var flags = MetaData(uid).Flags;
|
||||
DebugTools.Assert((flags & MetaDataFlags.InContainer) != 0,
|
||||
$"Entity has bad container flags. Ent: {ToPrettyString(uid)}. Container: {container.ID}, Owner: {ToPrettyString(container.Owner)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +442,14 @@ Types:
|
||||
AddressFamily: { }
|
||||
System.Numerics:
|
||||
BitOperations: { All: True }
|
||||
Complex: { All: True }
|
||||
Matrix3x2: { All: True }
|
||||
Matrix4x4: { All: True }
|
||||
Plane: { All: True }
|
||||
Quaternion: { All: True }
|
||||
Vector2: { All: True }
|
||||
Vector3: { All: True }
|
||||
Vector4: { All: True }
|
||||
System.Reflection:
|
||||
Assembly:
|
||||
Methods:
|
||||
@@ -770,7 +777,7 @@ Types:
|
||||
Array:
|
||||
Methods:
|
||||
- "!!0 Find<>(!!0[], System.Predicate`1<!!0>)"
|
||||
- "!!0 Resize<>(!!0[], int)"
|
||||
- "void Resize<>(ref !!0[], int)"
|
||||
- "!!1 ConvertAll<,>(!!0[], System.Converter`2<!!0, !!1>)"
|
||||
- "!!0[] Empty<>()"
|
||||
- "!!0[] FindAll<>(!!0[], System.Predicate`1<!!0>)"
|
||||
|
||||
@@ -289,6 +289,12 @@ namespace Robust.Shared.GameObjects
|
||||
return GetRegistration(componentType).Name;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public string GetComponentName<T>() where T : IComponent, new()
|
||||
{
|
||||
return GetRegistration<T>().Name;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public string GetComponentName(ushort netID)
|
||||
{
|
||||
@@ -324,7 +330,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public ComponentRegistration GetRegistration<T>() where T : IComponent, new()
|
||||
{
|
||||
return GetRegistration(typeof(T));
|
||||
return GetRegistration(CompIdx.Index<T>());
|
||||
}
|
||||
|
||||
public ComponentRegistration GetRegistration(IComponent component)
|
||||
|
||||
@@ -1,43 +1,56 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
[RequiresSerializable]
|
||||
[Serializable, NetSerializable]
|
||||
[Virtual]
|
||||
public abstract class ComponentState : IComponentState;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of a component for networking purposes.
|
||||
/// </summary>
|
||||
public interface IComponentState;
|
||||
|
||||
public interface IComponentDeltaState : IComponentState
|
||||
{
|
||||
[RequiresSerializable]
|
||||
[Serializable, NetSerializable]
|
||||
[Virtual]
|
||||
public class ComponentState : IComponentState
|
||||
{
|
||||
public void ApplyToFullState(IComponentState fullState);
|
||||
|
||||
}
|
||||
public IComponentState CreateNewFullState(IComponentState fullState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for component states that only contain partial state data. The actual delta state class should be a
|
||||
/// separate class from the full component states.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">The full-state class associated with this partial state</typeparam>
|
||||
public interface IComponentDeltaState<TState> : IComponentDeltaState where TState: IComponentState
|
||||
{
|
||||
/// <summary>
|
||||
/// This function will apply the current delta state to the provided full state, modifying it in the process.
|
||||
/// </summary>
|
||||
public void ApplyToFullState(TState fullState);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of a component for networking purposes.
|
||||
/// This function should take in a full state and return a new full state with the current delta applied, WITHOUT
|
||||
/// modifying the original input state.
|
||||
/// </summary>
|
||||
public interface IComponentState
|
||||
{
|
||||
public TState CreateNewFullState(TState fullState);
|
||||
|
||||
void IComponentDeltaState.ApplyToFullState(IComponentState fullState)
|
||||
{
|
||||
if (fullState is TState state)
|
||||
ApplyToFullState(state);
|
||||
else
|
||||
throw new Exception($"Unexpected type. Expected {nameof(TState)} but got {fullState.GetType().Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for components that support delta-states.
|
||||
/// </summary>
|
||||
public interface IComponentDeltaState
|
||||
IComponentState IComponentDeltaState.CreateNewFullState(IComponentState fullState)
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this state is a delta or full state.
|
||||
/// </summary>
|
||||
bool FullState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This function will apply the current delta state to the provided full state, modifying it in the process.
|
||||
/// </summary>
|
||||
public void ApplyToFullState(IComponentState fullState);
|
||||
|
||||
/// <summary>
|
||||
/// This function should take in a full state and return a new full state with the current delta applied,
|
||||
/// WITHOUT modifying the original input state.
|
||||
/// </summary>
|
||||
public IComponentState CreateNewFullState(IComponentState fullState);
|
||||
if (fullState is TState state)
|
||||
return CreateNewFullState(state);
|
||||
else
|
||||
throw new Exception($"Unexpected type. Expected {nameof(TState)} but got {fullState.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,45 +2,35 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.GameObjects.Components.Localization
|
||||
namespace Robust.Shared.GameObjects.Components.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Overrides grammar attributes specified in prototypes or localization files.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
// [Access(typeof(GrammarSystem))] TODO access
|
||||
public sealed partial class GrammarComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Overrides grammar attributes specified in prototypes or localization files.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent()]
|
||||
public sealed partial class GrammarComponent : Component
|
||||
[DataField, AutoNetworkedField]
|
||||
public Dictionary<string, string> Attributes = new();
|
||||
|
||||
[ViewVariables]
|
||||
public Gender? Gender
|
||||
{
|
||||
[DataField("attributes")]
|
||||
public Dictionary<string, string> Attributes { get; private set; } = new();
|
||||
get => Attributes.TryGetValue("gender", out var g) ? Enum.Parse<Gender>(g, true) : null;
|
||||
[Obsolete("Use GrammarSystem.SetGender instead")]
|
||||
set => IoCManager.Resolve<IEntityManager>().System<GrammarSystem>().SetGender((Owner, this), value);
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
public Gender? Gender
|
||||
{
|
||||
get => Attributes.TryGetValue("gender", out var g) ? Enum.Parse<Gender>(g, true) : null;
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
Attributes["gender"] = value.Value.ToString();
|
||||
else
|
||||
Attributes.Remove("gender");
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
public bool? ProperNoun
|
||||
{
|
||||
get => Attributes.TryGetValue("proper", out var g) ? bool.Parse(g) : null;
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
Attributes["proper"] = value.Value.ToString();
|
||||
else
|
||||
Attributes.Remove("proper");
|
||||
}
|
||||
}
|
||||
[ViewVariables]
|
||||
public bool? ProperNoun
|
||||
{
|
||||
get => Attributes.TryGetValue("proper", out var g) ? bool.Parse(g) : null;
|
||||
[Obsolete("Use GrammarSystem.SetProperNoun instead")]
|
||||
set => IoCManager.Resolve<IEntityManager>().System<GrammarSystem>().SetProperNoun((Owner, this), value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public sealed class GrammarSystem : EntitySystem
|
||||
{
|
||||
public void Clear(Entity<GrammarComponent> grammar)
|
||||
{
|
||||
grammar.Comp.Attributes.Clear();
|
||||
Dirty(grammar);
|
||||
}
|
||||
|
||||
public bool TryGet(Entity<GrammarComponent> grammar, string key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
return grammar.Comp.Attributes.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
public void Set(Entity<GrammarComponent> grammar, string key, string? value)
|
||||
{
|
||||
if (value == null)
|
||||
grammar.Comp.Attributes.Remove(key);
|
||||
else
|
||||
grammar.Comp.Attributes[key] = value;
|
||||
|
||||
Dirty(grammar);
|
||||
}
|
||||
|
||||
public void SetGender(Entity<GrammarComponent> grammar, Gender? gender)
|
||||
{
|
||||
Set(grammar, "gender", gender?.ToString());
|
||||
}
|
||||
|
||||
public void SetProperNoun(Entity<GrammarComponent> grammar, bool? proper)
|
||||
{
|
||||
Set(grammar, "proper", proper?.ToString());
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,50 @@ public sealed partial class PrototypeLayerData
|
||||
[DataField("map")] public HashSet<string>? MapKeys;
|
||||
[DataField("renderingStrategy")] public LayerRenderingStrategy? RenderingStrategy;
|
||||
|
||||
/// <summary>
|
||||
/// If set, indicates that this sprite layer should instead be used to copy into shader parameters on another layer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If set, this sprite layer is not rendered. Instead, the "result" of rendering it (exact sprite layer and such)
|
||||
/// are copied into the shader parameters of another object,
|
||||
/// specified by the <see cref="PrototypeCopyToShaderParameters"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The specified layer must have a shader set. When it does, the shader's
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note that sprite layers are processed in-order, so to avoid 1-frame delays,
|
||||
/// the layer doing the copying should occur BEFORE the layer being copied into.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[DataField] public PrototypeCopyToShaderParameters? CopyToShaderParameters;
|
||||
|
||||
[DataField] public bool Cycle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores parameters for <see cref="PrototypeLayerData.CopyToShaderParameters"/>.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable, DataDefinition]
|
||||
public sealed partial class PrototypeCopyToShaderParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// The map key of the layer that will have its shader modified.
|
||||
/// </summary>
|
||||
[DataField(required: true)] public string LayerKey;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the shader parameter that will receive the actual selected texture.
|
||||
/// </summary>
|
||||
[DataField] public string? ParameterTexture;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the shader parameter that will receive UVs to select the sprite in <see cref="ParameterTexture"/>.
|
||||
/// </summary>
|
||||
[DataField] public string? ParameterUV;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum LayerRenderingStrategy
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -25,16 +26,26 @@ namespace Robust.Shared.GameObjects
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
// Currently this field just exists for VV. In future, it might become a real field
|
||||
[ViewVariables]
|
||||
[ViewVariables, PublicAPI]
|
||||
private NetEntity NetParent => _entMan.GetNetEntity(_parent);
|
||||
|
||||
[DataField("parent")] internal EntityUid _parent;
|
||||
[DataField("pos")] internal Vector2 _localPosition = Vector2.Zero; // holds offset from grid, or offset from parent
|
||||
|
||||
[DataField("pos")] internal Vector2 _localPosition = Vector2.Zero; // holds offset from parent
|
||||
|
||||
[DataField("rot")] internal Angle _localRotation; // local rotation
|
||||
|
||||
[DataField("noRot")] internal bool _noLocalRotation;
|
||||
|
||||
[DataField("anchored")]
|
||||
internal bool _anchored;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates this entity can traverse grids.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GridTraversal = true;
|
||||
|
||||
/// <summary>
|
||||
/// The broadphase that this entity is currently stored on, if any.
|
||||
/// </summary>
|
||||
@@ -157,9 +168,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
var moveEvent = new MoveEvent((Owner, this, meta), Coordinates, Coordinates, oldRotation, _localRotation, _gameTiming.ApplyingState);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
|
||||
_entMan.System<SharedTransformSystem>().InvokeGlobalMoveEvent(ref moveEvent);
|
||||
_entMan.System<SharedTransformSystem>().RaiseMoveEvent((Owner, this, meta), _parent, _localPosition, oldRotation, MapUid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +343,9 @@ namespace Robust.Shared.GameObjects
|
||||
if (_localPosition.EqualsApprox(value))
|
||||
return;
|
||||
|
||||
var oldGridPos = Coordinates;
|
||||
var oldParent = _parent;
|
||||
var oldPos = _localPosition;
|
||||
|
||||
_localPosition = value;
|
||||
var meta = _entMan.GetComponent<MetaDataComponent>(Owner);
|
||||
_entMan.Dirty(Owner, this, meta);
|
||||
@@ -343,9 +354,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
var moveEvent = new MoveEvent((Owner, this, meta), oldGridPos, Coordinates, _localRotation, _localRotation, _gameTiming.ApplyingState);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
|
||||
_entMan.System<SharedTransformSystem>().InvokeGlobalMoveEvent(ref moveEvent);
|
||||
_entMan.System<SharedTransformSystem>().RaiseMoveEvent((Owner, this, meta), oldParent, oldPos, _localRotation, MapUid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,8 +611,12 @@ namespace Robust.Shared.GameObjects
|
||||
/// move events, subscribe to the <see cref="SharedTransformSystem.OnGlobalMoveEvent"/>.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly struct MoveEvent(Entity<TransformComponent, MetaDataComponent> entity, EntityCoordinates oldPos,
|
||||
EntityCoordinates newPos, Angle oldRotation, Angle newRotation, bool stateHandling = false)
|
||||
public readonly struct MoveEvent(
|
||||
Entity<TransformComponent, MetaDataComponent> entity,
|
||||
EntityCoordinates oldPos,
|
||||
EntityCoordinates newPos,
|
||||
Angle oldRotation,
|
||||
Angle newRotation)
|
||||
{
|
||||
public readonly Entity<TransformComponent, MetaDataComponent> Entity = entity;
|
||||
public readonly EntityCoordinates OldPosition = oldPos;
|
||||
@@ -615,15 +628,6 @@ namespace Robust.Shared.GameObjects
|
||||
public TransformComponent Component => Entity.Comp1;
|
||||
|
||||
public bool ParentChanged => NewPosition.EntityId != OldPosition.EntityId;
|
||||
|
||||
[Obsolete("Check IGameTiming.ApplyingState")]
|
||||
public readonly bool FromStateHandling = stateHandling;
|
||||
|
||||
[Obsolete]
|
||||
public MoveEvent(EntityUid uid, EntityCoordinates oldPos, EntityCoordinates newPos, Angle oldRot, Angle newRot, TransformComponent xform, bool state)
|
||||
: this((uid, xform, default!), oldPos, newPos, oldRot, newRot)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct TransformChildrenEnumerator : IDisposable
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// The last received state object sent from the server.
|
||||
/// </summary>
|
||||
protected BoundUserInterfaceState? State { get; private set; }
|
||||
protected internal BoundUserInterfaceState? State { get; internal set; }
|
||||
|
||||
protected BoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
{
|
||||
@@ -41,14 +41,14 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Invoked when the server uses <c>SetState</c>.
|
||||
/// </summary>
|
||||
protected virtual void UpdateState(BoundUserInterfaceState state)
|
||||
protected internal virtual void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the server sends an arbitrary message.
|
||||
/// </summary>
|
||||
protected virtual void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
protected internal virtual void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
UiSystem.TryCloseUi(_playerManager.LocalSession, Owner, UiKey);
|
||||
UiSystem.CloseUi(Owner, UiKey, _playerManager.LocalEntity, predicted: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +65,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
public void SendMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
UiSystem.SendUiMessage(this, message);
|
||||
UiSystem.ClientSendUiMessage(Owner, UiKey, message);
|
||||
}
|
||||
|
||||
public void SendPredictedMessage(BoundUserInterfaceMessage message)
|
||||
@@ -73,20 +73,6 @@ namespace Robust.Shared.GameObjects
|
||||
UiSystem.SendPredictedUiMessage(this, message);
|
||||
}
|
||||
|
||||
internal void InternalReceiveMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case UpdateBoundStateMessage updateBoundStateMessage:
|
||||
State = updateBoundStateMessage.State;
|
||||
UpdateState(State);
|
||||
break;
|
||||
default:
|
||||
ReceiveMessage(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
~BoundUserInterface()
|
||||
{
|
||||
Dispose(false);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Lets any entities with this component ignore user interface range checks that would normally
|
||||
/// close the UI automatically.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class IgnoreUIRangeComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entity-bound interface that can be opened by multiple players at once.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class PlayerBoundUserInterface
|
||||
{
|
||||
[ViewVariables]
|
||||
public float InteractionRange;
|
||||
|
||||
[ViewVariables]
|
||||
public float InteractionRangeSqrd => InteractionRange * InteractionRange;
|
||||
|
||||
[ViewVariables]
|
||||
public Enum UiKey { get; }
|
||||
[ViewVariables]
|
||||
public EntityUid Owner { get; }
|
||||
|
||||
internal readonly HashSet<ICommonSession> _subscribedSessions = new();
|
||||
[ViewVariables]
|
||||
internal BoundUIWrapMessage? LastStateMsg;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool RequireInputValidation;
|
||||
|
||||
[ViewVariables]
|
||||
internal bool StateDirty;
|
||||
|
||||
[ViewVariables]
|
||||
internal readonly Dictionary<ICommonSession, BoundUIWrapMessage> PlayerStateOverrides =
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// All of the sessions currently subscribed to this UserInterface.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public IReadOnlySet<ICommonSession> SubscribedSessions => _subscribedSessions;
|
||||
|
||||
public PlayerBoundUserInterface(PrototypeData data, EntityUid owner)
|
||||
{
|
||||
RequireInputValidation = data.RequireInputValidation;
|
||||
UiKey = data.UiKey;
|
||||
Owner = owner;
|
||||
|
||||
InteractionRange = data.InteractionRange;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user