Compare commits

..

102 Commits

Author SHA1 Message Date
metalgearsloth
eb46b04c0e Version: 0.14.2.1 2022-04-24 00:58:38 +10:00
metalgearsloth
442de12b99 Grid splitting (#2743)
Co-authored-by: Vera Aguilera Puerto <gradientvera@outlook.com>
2022-04-24 00:57:56 +10:00
Vera Aguilera Puerto
712c1f3559 Adds "ressys" and IEntitySystemManager dependency to scripting globals. (#2762)
"ressys" resolves an `EntitySystem`.
2022-04-24 00:55:31 +10:00
Vera Aguilera Puerto
5a5cfa1eba Adds GetGridOrMapTilePosition to SharedTransformSystem. (#2759)
A little helper method that would be VERY useful for many content things... Such as atmos.
2022-04-24 00:55:11 +10:00
metalgearsloth
7988386bc3 Version: 0.14.1.1 2022-04-24 00:53:41 +10:00
Leon Friedrich
76bfe2905d Add support for float[] and vec2[] in shaders (#2751)
* huh it works.

* vector arrays
2022-04-22 22:43:17 +02:00
ShadowCommander
23893bcacd Change LineEdit to keep cursor position (#2752)
* Change LineEdit to keep cursor position

* Revert console changes
2022-04-22 22:42:24 +02:00
Pieter-Jan Briers
c1da159e8f Rely on SharpZstd -Bsymbolic 2022-04-22 22:39:59 +02:00
20kdc
13889acb37 Fix crash caused by passing uninitialized structure to ZSTD_inBuffer (#2760) 2022-04-22 14:10:20 +02:00
Vera Aguilera Puerto
ffb3de3f2c Fix loading soundfonts from filesystem on Linux 2022-04-21 17:52:27 +02:00
Flipp Syder
87cd45fc64 Adds custom midi soundfont support (sandboxed) (#2746) 2022-04-21 10:05:57 +02:00
Leon Friedrich
1cf914813c Add PVSOverrideSystem (#2747)
* Add PVSOverrideSystem

* add session and clear functions
2022-04-20 16:00:18 +02:00
Pieter-Jan Briers
86d61f8d03 Version: 0.14.0.1 2022-04-20 15:39:32 +02:00
Pieter-Jan Briers
8f638fbf9e HOTFIX ParallelManager out of bounds
I could have sworn I made this -1.

Completely untested
2022-04-20 15:21:17 +02:00
metalgearsloth
95ac134a07 Comment out msgstate logger (#2754)
Getting spammed and I don't think we want to log here long-term anyway.
2022-04-20 13:13:15 +02:00
metalgearsloth
4e65f9b2a1 Version: 0.14.0.0 2022-04-20 18:48:06 +10:00
Paul
d60bbe9fe9 misc client optimizations 2022-04-18 22:34:49 +02:00
Pieter-Jan Briers
dec1495e1e Remove reference to ManagedHttpListener.
How did this ever compile?
2022-04-17 19:13:24 +02:00
Paul
dc72c6fe22 fix typo 2022-04-17 18:15:41 +02:00
Paul
141b1205c6 removes some todos 2022-04-17 18:09:46 +02:00
Pieter-Jan Briers
65f4a09ad5 Try to fix ZStd stuff for servers. 2022-04-17 18:02:35 +02:00
Pieter-Jan Briers
3d1545c0b9 Fix incorrect unsubscription to AssemblyLoadContext.Default.Resolving.
+= instead of -=.
2022-04-17 18:02:35 +02:00
metalgearsloth
ec26dd622b DetachParentToNull tweaks (#2741)
* DetachParentToNull tweaks

The other parent change message already has the mapid and gridid updated when issuing the event.

We'll also guard the event by checking if they're already in nullspace.

* woops
2022-04-17 17:28:07 +10:00
metalgearsloth
0ab3131964 ECS transform states (#2710)
* ECS transform states

Turns out it also saves us a GetComponent on the parent too which is nice.

* Fix test
2022-04-17 14:55:29 +10:00
Pieter-Jan Briers
588a9e9f63 ZSTD game states + other improvements (#2737) 2022-04-16 14:36:59 +02:00
Pieter-Jan Briers
f2fa930edd Block explicit layout types in sandboxing. 2022-04-16 14:17:37 +02:00
Kara D
ec47229a37 Version: 0.13.0.0 2022-04-15 14:36:50 -07:00
mirrorcult
bf5d1d58a8 Merge pull request #2715 from moonheart08/better-map-loading 2022-04-15 14:34:48 -07:00
mirrorcult
8b4da24ee7 Merge pull request #2722 from PaulRitter/2022_04_12_abstract_validation 2022-04-15 14:34:22 -07:00
mirrorcult
3fba108d70 Merge pull request #2734 from PJB3005/22-04-14-spacewizards-http 2022-04-15 14:34:01 -07:00
mirrorcult
35029f0eed Merge pull request #2692 from vulppine/color-sliders 2022-04-15 14:33:21 -07:00
mirrorcult
b66ab9d7c6 Merge pull request #2727 from ElectroJr/update-mapping-except 2022-04-15 14:31:30 -07:00
mirrorcult
5e0b745ba9 Merge pull request #2739 from PJB3005/22-04-15-remove-createnetmsg 2022-04-15 14:30:32 -07:00
Pieter-Jan Briers
45d906ba7e Deprecate CreateNetMessage<T>. 2022-04-15 18:47:42 +02:00
Pieter-Jan Briers
24b124fb17 Fix client reconnect 2022-04-15 15:28:45 +02:00
Vera Aguilera Puerto
7cb0978468 Revert "Adds custom MIDI soundfont support (#2720)"
This reverts commit 9ff46b9aad.

Sandbox violation, oops.
2022-04-15 12:46:30 +02:00
mirrorcult
44cb135a1d Even shorter pretty-print type abbreviations for VV (#2735) 2022-04-15 09:15:10 +02:00
Pieter-Jan Briers
146b673203 Move string serializer to ZStd and BLAKE2b.
Faster and smaller
2022-04-15 01:16:35 +02:00
Pieter-Jan Briers
b0d23c5665 Remove zstd & libsodium natives from client publishes.
Save a good megabyte.
2022-04-15 00:39:56 +02:00
Pieter-Jan Briers
68f89c8958 SkipIfSandboxedAttribute
Should allow something like OpenDream to provide unsandboxed-only DLLs easily.
2022-04-15 00:31:17 +02:00
Pieter-Jan Briers
1327d6bf25 Replace ManagedHttpListener submodule in favor of NuGet package.
It's SpaceWizards.HttpListener now!
2022-04-14 23:53:57 +02:00
Pieter-Jan Briers
237e37ff30 Update Lidgren submodule 2022-04-14 23:47:17 +02:00
Pieter-Jan Briers
4d707c86cb Version: 0.12.1.0 2022-04-14 17:17:17 +02:00
Pieter-Jan Briers
c7027c6e00 ACZ manifest delta downloads (#2698) 2022-04-14 17:15:54 +02:00
Paul Ritter
81ec61bcc8 overflowqueue (#2721)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2022-04-14 10:57:28 +02:00
DrSmugleaf
649178fa54 Rename engine prototypes folder to EnginePrototypes (#2723) 2022-04-14 05:46:29 +02:00
Flipp Syder
9ff46b9aad Adds custom MIDI soundfont support (#2720)
Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
2022-04-13 11:59:43 +02:00
ElectroJr
cdcbb60ca7 more comments 2022-04-13 21:46:40 +12:00
ElectroJr
d8cdb2b312 comments 2022-04-13 21:43:47 +12:00
ElectroJr
fa1c1b8e6e better equality 2022-04-13 21:39:48 +12:00
ElectroJr
2ded835602 Make MappingDataNode.Except() not recursive 2022-04-13 21:08:59 +12:00
moonheart08
a43f04818d fix map commands. 2022-04-12 09:48:45 -05:00
moonheart08
278dc60119 use a query for xform. 2022-04-12 09:43:59 -05:00
moonheart08
8a202be0cf naming. 2022-04-12 09:34:18 -05:00
moonheart08
58940a3cd7 bp/map load now take arguments. 2022-04-11 23:54:15 -05:00
moonheart08
aa9721d146 fix tests 2022-04-11 23:15:37 -05:00
Paul
79f114ad5b work 2022-04-12 02:04:41 +02:00
Leon Friedrich
197a6ddd9d Fix sprite bb calculation (#2714) 2022-04-11 18:13:32 +02:00
metalgearsloth
4a4fb15e06 Add xform comp ref to parent change message (#2709) 2022-04-11 17:01:07 +10:00
metalgearsloth
e7e83ce6e8 Optimise fixture init slightly (#2707) 2022-04-11 16:58:48 +10:00
Leon Friedrich
a82b293452 Make RemoveComp not error on missing component (#2689) 2022-04-11 16:57:35 +10:00
Vera Aguilera Puerto
bb483a3a38 Add "midipanic" command. (#2701) 2022-04-11 16:55:12 +10:00
moonheart08
2ca3d0e395 Fix map loading once and for all. 2022-04-11 00:44:28 -05:00
metalgearsloth
bda79b1e82 Cache serialized types (#2706) 2022-04-11 12:50:34 +10:00
metalgearsloth
4f4b754e2d Don't allow velocity to be set where an obj can't collide (#2691) 2022-04-11 12:49:35 +10:00
mirrorcult
214cabac43 Merge pull request #2713 from oappiah/minor_config_update 2022-04-10 13:35:00 -07:00
Appiah
7b6229c222 Corrected the map example 2022-04-10 22:24:21 +02:00
Paul
1a14a75897 nullspace viewer crash fix 2022-04-10 17:52:12 +02:00
metalgearsloth
76b75fd9b3 Remove LINQ from sprite bounds (#2708) 2022-04-10 17:48:45 +10:00
Pieter-Jan Briers
b969fd22f7 Version: 0.12.0.1 2022-04-09 21:02:56 +02:00
Profane McBane
8d27d091af Hotfix - change PVS pools to use MaxVisPoolSize (#2704) 2022-04-09 20:54:41 +02:00
DrSmugleaf
1eb7393a60 Revert "pvsrange vec2 + eyezoom (#2676)" (#2703)
This reverts commit 582d8a5587.
2022-04-09 19:41:11 +02:00
Vera Aguilera Puerto
4cf88507c2 Version: 0.11.0.1 2022-04-09 13:21:38 +02:00
Vera Aguilera Puerto
3565d8b321 Fix synth state getting reset every MIDI player loop. 2022-04-09 13:21:09 +02:00
Vera Aguilera Puerto
7094c29b2e Version: 0.11.0.0 2022-04-08 16:07:42 +02:00
Vera Aguilera Puerto
63004b270f Cleans up and improves MIDI code significantly. (#2666) 2022-04-08 15:58:15 +02:00
metalgearsloth
6714a99b38 EntityLookup anchor flag test (#2699) 2022-04-08 13:36:24 +10:00
metalgearsloth
4c3b8df1e7 Fix anchor query (#2697) 2022-04-08 09:56:30 +10:00
metalgearsloth
4bb695121f Version: 0.10.0.0 2022-04-06 19:34:47 +10:00
metalgearsloth
09fd47c421 Don't store contained entities on entitylookup (#2662) 2022-04-06 19:31:34 +10:00
vulppine
d201d9c688 adds color sliders, fixes issues with RGB->HSL/V 2022-04-05 12:21:59 -07:00
Paul
fd1e25c584 Version: 0.9.3.2 2022-04-05 18:51:03 +02:00
Paul Ritter
6bb66ae70e fixes loc (#2685) 2022-04-05 18:50:27 +02:00
Paul
cc82d6b1d9 Version: 0.9.3.1 2022-04-05 18:36:19 +02:00
Paul
956be749b6 fix prototype reload 2022-04-05 18:36:09 +02:00
Paul Ritter
6585a00608 readds expandpvsevent (#2684) 2022-04-05 19:47:14 +10:00
metalgearsloth
c0525f710f Sprite subscription shutdown (#2688) 2022-04-05 19:45:23 +10:00
Vera Aguilera Puerto
d3672807d2 Improves SpriteSystem.GetPrototypeIcon method significantly. (#2680) 2022-04-05 16:36:16 +10:00
ElectroJr
60f18d5f36 Version: 0.9.3.0 2022-04-05 18:06:43 +12:00
mirrorcult
e72d3de256 Local event for BUI opening (#2687) 2022-04-05 15:52:02 +10:00
Moony
ba9846b9c4 Fix vector2 CVars (#2686) 2022-04-05 15:03:10 +10:00
Paul
09586284dc Version: 0.9.2.0 2022-04-04 20:28:53 +02:00
Paul
a1ee4374b2 add a check for major version == 0 2022-04-04 20:28:26 +02:00
Paul Ritter
4de6f25f11 updates version-script to use the new version format (#2683) 2022-04-04 20:25:12 +02:00
Paul Ritter
582d8a5587 pvsrange vec2 + eyezoom (#2676)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
2022-04-04 20:20:13 +02:00
Leon Friedrich
ec53b04f99 Add NotNullWhen attribute to TryComp (#2681) 2022-04-04 11:29:02 +02:00
metalgearsloth
950fc94408 Physicsmap tweaks (#2663) 2022-04-04 17:10:15 +10:00
Leon Friedrich
58d12e6e09 Remove Component.OnAdd() (#2660) 2022-04-04 16:11:47 +10:00
ElectroJr
94323005c4 Version: 0.9.1 2022-04-04 17:43:01 +12:00
metalgearsloth
4989842057 Remove IgnorePause (#2649) 2022-04-04 15:41:38 +10:00
Paul
80172636a8 version 0.9 - serv3 refactor 2022-04-03 02:00:41 +02:00
Paul Ritter
8491f7be24 New Serv3 api just dropped (#2605)
Co-authored-by: Paul Ritter <ritter.paul1@gmail.com>
2022-04-03 01:59:48 +02:00
301 changed files with 10320 additions and 4908 deletions

3
.gitmodules vendored
View File

@@ -10,9 +10,6 @@
[submodule "Robust.LoaderApi"]
path = Robust.LoaderApi
url = https://github.com/space-wizards/Robust.LoaderApi.git
[submodule "ManagedHttpListener"]
path = ManagedHttpListener
url = https://github.com/space-wizards/ManagedHttpListener.git
[submodule "cefglue"]
path = cefglue
url = https://github.com/space-wizards/cefglue.git

View File

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

View File

@@ -0,0 +1,10 @@
color-selector-sliders-red = R
color-selector-sliders-green = G
color-selector-sliders-blue = B
color-selector-sliders-hue = H
color-selector-sliders-saturation = S
color-selector-sliders-value = V
color-selector-sliders-alpha = A
color-selector-sliders-rgb = RGB
color-selector-sliders-hsv = HSV

View File

@@ -0,0 +1 @@
midi-panic-command-description = Turns off every note for every active MIDI renderer.

View File

@@ -1,7 +1,6 @@
using System.Globalization;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
@@ -19,10 +18,10 @@ namespace Robust.Benchmarks.Serialization
: new ErrorNode(node, $"Failed parsing int value: {node.Value}");
}
public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null)
public int Read(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null, int _ = default)
{
return new DeserializedValue<int>(int.Parse(node.Value, CultureInfo.InvariantCulture));
return int.Parse(node.Value, CultureInfo.InvariantCulture);
}
public DataNode Write(ISerializationManager serializationManager, int value, bool alwaysWrite = false,

View File

@@ -28,7 +28,7 @@ namespace Robust.Benchmarks.Serialization.Copy
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
Seed = SerializationManager.Read<SeedDataDefinition>(seedMapping);
}
private const string String = "ABC";

View File

@@ -36,7 +36,7 @@ namespace Robust.Benchmarks.Serialization.Definitions
Max: 10
PotencyDivisor: 10";
[DataField("id", required: true)] public string ID { get; set; } = default!;
[IdDataFieldAttribute] public string ID { get; set; } = default!;
#region Tracking
[DataField("name")] public string Name { get; set; } = string.Empty;

View File

@@ -2,7 +2,6 @@
using BenchmarkDotNet.Attributes;
using Robust.Benchmarks.Serialization.Definitions;
using Robust.Shared.Analyzers;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
@@ -42,32 +41,32 @@ namespace Robust.Benchmarks.Serialization.Read
private ValueDataNode FlagThirtyOne { get; } = new("ThirtyOne");
[Benchmark]
public string? ReadString()
public string ReadString()
{
return SerializationManager.ReadValue<string>(StringNode);
return SerializationManager.Read<string>(StringNode);
}
[Benchmark]
public int? ReadInteger()
public int ReadInteger()
{
return SerializationManager.ReadValue<int>(IntNode);
return SerializationManager.Read<int>(IntNode);
}
[Benchmark]
public DataDefinitionWithString? ReadDataDefinitionWithString()
public DataDefinitionWithString ReadDataDefinitionWithString()
{
return SerializationManager.ReadValue<DataDefinitionWithString>(StringDataDefNode);
return SerializationManager.Read<DataDefinitionWithString>(StringDataDefNode);
}
[Benchmark]
public SeedDataDefinition? ReadSeedDataDefinition()
public SeedDataDefinition ReadSeedDataDefinition()
{
return SerializationManager.ReadValue<SeedDataDefinition>(SeedNode);
return SerializationManager.Read<SeedDataDefinition>(SeedNode);
}
[Benchmark]
[BenchmarkCategory("flag")]
public DeserializationResult ReadFlagZero()
public object? ReadFlagZero()
{
return SerializationManager.ReadWithTypeSerializer(
typeof(int),
@@ -77,7 +76,7 @@ namespace Robust.Benchmarks.Serialization.Read
[Benchmark]
[BenchmarkCategory("flag")]
public DeserializationResult ReadThirtyOne()
public object? ReadThirtyOne()
{
return SerializationManager.ReadWithTypeSerializer(
typeof(int),
@@ -87,7 +86,7 @@ namespace Robust.Benchmarks.Serialization.Read
[Benchmark]
[BenchmarkCategory("customTypeSerializer")]
public DeserializationResult ReadIntegerCustomSerializer()
public object? ReadIntegerCustomSerializer()
{
return SerializationManager.ReadWithTypeSerializer(
typeof(int),

View File

@@ -46,84 +46,84 @@ namespace Robust.Benchmarks.Serialization
[BenchmarkCategory("read")]
public string[]? ReadEmptyString()
{
return SerializationManager.ReadValue<string[]>(EmptyNode);
return SerializationManager.Read<string[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public string[]? ReadOneString()
{
return SerializationManager.ReadValue<string[]>(OneIntNode);
return SerializationManager.Read<string[]>(OneIntNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public string[]? ReadTenStrings()
{
return SerializationManager.ReadValue<string[]>(TenIntsNode);
return SerializationManager.Read<string[]>(TenIntsNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public int[]? ReadEmptyInt()
{
return SerializationManager.ReadValue<int[]>(EmptyNode);
return SerializationManager.Read<int[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public int[]? ReadOneInt()
{
return SerializationManager.ReadValue<int[]>(OneIntNode);
return SerializationManager.Read<int[]>(OneIntNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public int[]? ReadTenInts()
{
return SerializationManager.ReadValue<int[]>(TenIntsNode);
return SerializationManager.Read<int[]>(TenIntsNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public DataDefinitionWithString[]? ReadEmptyStringDataDef()
{
return SerializationManager.ReadValue<DataDefinitionWithString[]>(EmptyNode);
return SerializationManager.Read<DataDefinitionWithString[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public DataDefinitionWithString[]? ReadOneStringDataDef()
{
return SerializationManager.ReadValue<DataDefinitionWithString[]>(OneStringDefNode);
return SerializationManager.Read<DataDefinitionWithString[]>(OneStringDefNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public DataDefinitionWithString[]? ReadTenStringDataDefs()
{
return SerializationManager.ReadValue<DataDefinitionWithString[]>(TenStringDefsNode);
return SerializationManager.Read<DataDefinitionWithString[]>(TenStringDefsNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public SealedDataDefinitionWithString[]? ReadEmptySealedStringDataDef()
{
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(EmptyNode);
return SerializationManager.Read<SealedDataDefinitionWithString[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public SealedDataDefinitionWithString[]? ReadOneSealedStringDataDef()
{
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(OneStringDefNode);
return SerializationManager.Read<SealedDataDefinitionWithString[]>(OneStringDefNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public SealedDataDefinitionWithString[]? ReadTenSealedStringDataDefs()
{
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(TenStringDefsNode);
return SerializationManager.Read<SealedDataDefinitionWithString[]>(TenStringDefsNode);
}
}
}

View File

@@ -28,7 +28,7 @@ namespace Robust.Benchmarks.Serialization.Write
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
Seed = SerializationManager.Read<SeedDataDefinition>(seedMapping);
}
private const string String = "ABC";

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Robust.Client.Audio.Midi.Commands;
public sealed class MidiPanicCommand : IConsoleCommand
{
[Dependency] private readonly IMidiManager _midiManager = default!;
public string Command => "midipanic";
public string Description => Loc.GetString("midi-panic-command-description");
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
foreach (var renderer in _midiManager.Renderers)
{
renderer.StopAllNotes();
}
}
}

View File

@@ -0,0 +1,62 @@
using System.Collections.Generic;
using NFluidsynth;
using Robust.Shared.Audio.Midi;
namespace Robust.Client.Audio.Midi;
public interface IMidiManager
{
/// <summary>
/// A read-only list of all existing MIDI Renderers.
/// </summary>
IReadOnlyList<IMidiRenderer> Renderers { get; }
/// <summary>
/// If true, MIDI support is available.
/// </summary>
bool IsAvailable { get; }
/// <summary>
/// Volume, in db.
/// </summary>
float Volume { get; set; }
public int OcclusionCollisionMask { get; set; }
/// <summary>
/// This method tries to return a midi renderer ready to be used.
/// You only need to set the <see cref="IMidiRenderer.MidiProgram"/> afterwards.
/// </summary>
/// <remarks>
/// This method can fail if MIDI support is not available.
/// </remarks>
/// <returns>
/// <c>null</c> if MIDI support is not available.
/// </returns>
IMidiRenderer? GetNewRenderer(bool mono = true);
/// <summary>
/// Creates a <see cref="RobustMidiEvent"/> given a <see cref="MidiEvent"/> and a sequencer tick.
/// </summary>
RobustMidiEvent FromFluidEvent(MidiEvent midiEvent, uint tick);
/// <summary>
/// Creates a <see cref="SequencerEvent"/> given a <see cref="RobustMidiEvent"/>.
/// Be sure to dispose of the result after you've used it.
/// </summary>
SequencerEvent ToSequencerEvent(RobustMidiEvent midiEvent);
/// <summary>
/// Creates a <see cref="RobustMidiEvent"/> given a <see cref="SequencerEvent"/> and a sequencer tick.
/// </summary>
RobustMidiEvent FromSequencerEvent(SequencerEvent midiEvent, uint tick);
/// <summary>
/// Method called every frame.
/// Should be used to update positional audio.
/// </summary>
/// <param name="frameTime"></param>
void FrameUpdate(float frameTime);
void Shutdown();
}

View File

@@ -0,0 +1,174 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Midi;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Robust.Client.Audio.Midi;
public enum MidiRendererStatus : byte
{
None,
Input,
File,
}
public interface IMidiRenderer : IDisposable
{
/// <summary>
/// The buffered audio source of this renderer.
/// </summary>
internal IClydeBufferedAudioSource Source { get; }
/// <summary>
/// Whether this renderer has been disposed or not.
/// </summary>
bool Disposed { get; }
/// <summary>
/// This controls whether the midi file being played will loop or not.
/// </summary>
bool LoopMidi { get; set; }
/// <summary>
/// This increases all note on velocities to 127.
/// </summary>
bool VolumeBoost { get; set; }
/// <summary>
/// The midi program (instrument) the renderer is using.
/// </summary>
byte MidiProgram { get; set; }
/// <summary>
/// The instrument bank the renderer is using.
/// </summary>
byte MidiBank { get; set; }
/// <summary>
/// The soundfont currently selected by the renderer.
/// </summary>
uint MidiSoundfont { get; set; }
/// <summary>
/// The current status of the renderer.
/// "None" if the renderer isn't playing from input or a midi file.
/// "Input" if the renderer is playing from midi input.
/// "File" if the renderer is playing from a midi file.
/// </summary>
MidiRendererStatus Status { get; }
/// <summary>
/// Whether the sound will play in stereo or mono.
/// </summary>
bool Mono { get; set; }
/// <summary>
/// Whether to drop messages on the percussion channel.
/// </summary>
bool DisablePercussionChannel { get; set; }
/// <summary>
/// Whether to drop messages for program change events.
/// </summary>
bool DisableProgramChangeEvent { get; set; }
/// <summary>
/// Gets the total number of ticks possible for the MIDI player.
/// </summary>
int PlayerTotalTick { get; }
/// <summary>
/// Gets or sets (seeks) the current tick of the MIDI player.
/// </summary>
int PlayerTick { get; set; }
/// <summary>
/// Gets the current tick of the sequencer.
/// </summary>
uint SequencerTick { get; }
/// <summary>
/// Gets the Time Scale of the sequencer in ticks per second. Default is 1000 for 1 tick per millisecond.
/// </summary>
double SequencerTimeScale { get; }
/// <summary>
/// Start listening for midi input.
/// </summary>
bool OpenInput();
/// <summary>
/// Start playing a midi file.
/// </summary>
/// <param name="buffer">Bytes of the midi file</param>
bool OpenMidi(ReadOnlySpan<byte> buffer);
/// <summary>
/// Stops listening for midi input.
/// </summary>
bool CloseInput();
/// <summary>
/// Stops playing midi files.
/// </summary>
bool CloseMidi();
/// <summary>
/// Stops all notes being played currently.
/// </summary>
void StopAllNotes();
/// <summary>
/// Render and play MIDI to the audio source.
/// </summary>
internal void Render();
/// <summary>
/// Loads a new soundfont into the renderer.
/// </summary>
void LoadSoundfont(string filename, bool resetPresets = false);
/// <summary>
/// Invoked whenever a new midi event is registered.
/// </summary>
event Action<RobustMidiEvent> OnMidiEvent;
/// <summary>
/// Invoked when the midi player finishes playing a song.
/// </summary>
event Action OnMidiPlayerFinished;
/// <summary>
/// The entity whose position will be used for positional audio.
/// This is only used if <see cref="Mono"/> is set to True.
/// </summary>
EntityUid? TrackingEntity { get; set; }
/// <summary>
/// The position that will be used for positional audio.
/// This is only used if <see cref="Mono"/> is set to True
/// and <see cref="TrackingEntity"/> is null.
/// </summary>
EntityCoordinates? TrackingCoordinates { get; set; }
/// <summary>
/// Send a midi event for the renderer to play.
/// </summary>
/// <param name="midiEvent">The midi event to be played</param>
void SendMidiEvent(RobustMidiEvent midiEvent);
/// <summary>
/// Schedule a MIDI event to be played at a later time.
/// </summary>
/// <param name="midiEvent">the midi event in question</param>
/// <param name="time"></param>
/// <param name="absolute"></param>
void ScheduleMidiEvent(RobustMidiEvent midiEvent, uint time, bool absolute);
/// <summary>
/// Actually disposes of this renderer. Do NOT use outside the MIDI thread.
/// </summary>
internal void InternalDispose();
}

View File

@@ -0,0 +1,155 @@
using NFluidsynth;
using Robust.Shared.Audio.Midi;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiManager
{
public RobustMidiEvent FromFluidEvent(MidiEvent midiEvent, uint tick)
{
var status = RobustMidiEvent.MakeStatus((byte) midiEvent.Channel, (byte) midiEvent.Type);
// Control is always the first data byte. Value is always the second data byte. Fluidsynth's API ain't great.
var data1 = (byte) midiEvent.Control;
var data2 = (byte) midiEvent.Value;
// PitchBend is handled specially.
if (midiEvent.Type == (int) RobustMidiCommand.PitchBend)
{
// We pack pitch into both data values.
var pitch = (ushort) midiEvent.Pitch;
data1 = (byte) pitch;
data2 = (byte) (pitch >> 8);
}
return new RobustMidiEvent(status, data1, data2, tick);
}
public SequencerEvent ToSequencerEvent(RobustMidiEvent midiEvent)
{
var sequencerEvent = new SequencerEvent();
switch (midiEvent.MidiCommand)
{
case RobustMidiCommand.NoteOff:
sequencerEvent.NoteOff(midiEvent.Channel, midiEvent.Key);
break;
case RobustMidiCommand.NoteOn:
sequencerEvent.NoteOn(midiEvent.Channel, midiEvent.Key, midiEvent.Velocity);
break;
case RobustMidiCommand.AfterTouch:
sequencerEvent.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);
break;
case RobustMidiCommand.ControlChange:
sequencerEvent.ControlChange(midiEvent.Channel, midiEvent.Control, midiEvent.Value);
break;
case RobustMidiCommand.ProgramChange:
sequencerEvent.ProgramChange(midiEvent.Channel, midiEvent.Program);
break;
case RobustMidiCommand.ChannelPressure:
sequencerEvent.ChannelPressure(midiEvent.Channel, midiEvent.Value);
break;
case RobustMidiCommand.PitchBend:
sequencerEvent.PitchBend(midiEvent.Channel, midiEvent.Pitch);
break;
case RobustMidiCommand.SystemMessage:
switch (midiEvent.Control)
{
case 0x0 when midiEvent.Status == 0xFF:
sequencerEvent.SystemReset();
break;
case 0x0B:
sequencerEvent.AllNotesOff(midiEvent.Channel);
break;
default:
_midiSawmill.Warning($"Tried to convert unsupported event to sequencer event:\n{midiEvent}");
break;
}
break;
default:
_midiSawmill.Warning($"Tried to convert unsupported event to sequencer event:\n{midiEvent}");
break;
}
return sequencerEvent;
}
public RobustMidiEvent FromSequencerEvent(SequencerEvent midiEvent, uint tick)
{
byte channel = (byte) midiEvent.Channel;
RobustMidiCommand command = 0x0;
byte data1 = 0;
byte data2 = 0;
switch (midiEvent.Type)
{
case FluidSequencerEventType.NoteOn:
command = RobustMidiCommand.NoteOn;
data1 = (byte) midiEvent.Key;
data2 = (byte) midiEvent.Velocity;
break;
case FluidSequencerEventType.NoteOff:
command = RobustMidiCommand.NoteOff;
data1 = (byte) midiEvent.Key;
break;
case FluidSequencerEventType.PitchBend:
command = RobustMidiCommand.PitchBend;
// We pack pitch into both data values
var pitch = (ushort) midiEvent.Pitch;
data1 = (byte) pitch;
data2 = (byte) (pitch >> 8);
break;
case FluidSequencerEventType.ProgramChange:
command = RobustMidiCommand.ProgramChange;
data1 = (byte) midiEvent.Program;
break;
case FluidSequencerEventType.KeyPressure:
command = RobustMidiCommand.AfterTouch;
data1 = (byte) midiEvent.Key;
data2 = (byte) midiEvent.Value;
break;
case FluidSequencerEventType.ControlChange:
command = RobustMidiCommand.ControlChange;
data1 = (byte) midiEvent.Control;
data2 = (byte) midiEvent.Value;
break;
case FluidSequencerEventType.ChannelPressure:
command = RobustMidiCommand.ChannelPressure;
data1 = (byte) midiEvent.Value;
break;
case FluidSequencerEventType.AllNotesOff:
command = RobustMidiCommand.SystemMessage;
data1 = 0x0B;
break;
case FluidSequencerEventType.SystemReset:
command = RobustMidiCommand.SystemMessage;
channel = 0x0F;
break;
default:
_midiSawmill.Error($"Unsupported Sequencer Event: {tick:D8}: {SequencerEventToString(midiEvent)}");
break;
}
return new RobustMidiEvent(RobustMidiEvent.MakeStatus(channel, (byte)command), data1, data2, tick);
}
}

View File

@@ -7,6 +7,7 @@ using NFluidsynth;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -15,404 +16,435 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using Logger = Robust.Shared.Log.Logger;
namespace Robust.Client.Audio.Midi
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiManager : IMidiManager
{
public interface IMidiManager
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly ILogManager _logger = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
public IReadOnlyList<IMidiRenderer> Renderers
{
/// <summary>
/// This method tries to return a midi renderer ready to be used.
/// You only need to set the <see cref="IMidiRenderer.MidiProgram"/> afterwards.
/// </summary>
/// <remarks>
/// This method can fail if MIDI support is not available.
/// </remarks>
/// <returns>
/// <c>null</c> if MIDI support is not available.
/// </returns>
IMidiRenderer? GetNewRenderer();
/// <summary>
/// Method called every frame.
/// Should be used to update positional audio.
/// </summary>
/// <param name="frameTime"></param>
void FrameUpdate(float frameTime);
/// <summary>
/// Volume, in db.
/// </summary>
float Volume { get; set; }
/// <summary>
/// If true, MIDI support is available.
/// </summary>
bool IsAvailable { get; }
public int OcclusionCollisionMask { get; set; }
void Shutdown();
get
{
lock (_renderers)
{
// Perform a copy. Sadly, we can't return a reference to the original list due to threading concerns.
return _renderers.ToArray();
}
}
}
internal sealed class MidiManager : IMidiManager
[ViewVariables]
public bool IsAvailable
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
[ViewVariables]
public bool IsAvailable
get
{
get
{
InitializeFluidsynth();
InitializeFluidsynth();
return FluidsynthInitialized;
}
return FluidsynthInitialized;
}
}
[ViewVariables]
private readonly List<IMidiRenderer> _renderers = new();
private bool _alive = true;
private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
{
get => _volume;
set
{
if (MathHelper.CloseToPercent(_volume, value))
return;
_cfgMan.SetCVar(CVars.MidiVolume, value);
_volumeDirty = true;
}
}
private static readonly string[] LinuxSoundfonts =
{
"/usr/share/soundfonts/default.sf2",
"/usr/share/soundfonts/FluidR3_GM.sf2",
"/usr/share/soundfonts/freepats-general-midi.sf2",
"/usr/share/sounds/sf2/default.sf2",
"/usr/share/sounds/sf2/FluidR3_GM.sf2",
"/usr/share/sounds/sf2/TimGM6mb.sf2",
};
private const string WindowsSoundfont = @"C:\WINDOWS\system32\drivers\gm.dls";
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private static ResourcePath CustomSoundfontDirectory = new ResourcePath("/soundfonts/");
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
private bool FluidsynthInitialized;
private bool _failedInitialize;
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
private ISawmill _sawmill = default!;
[ViewVariables(VVAccess.ReadWrite)]
public int OcclusionCollisionMask { get; set; }
private void InitializeFluidsynth()
{
if (FluidsynthInitialized || _failedInitialize) return;
_volume = _cfgMan.GetCVar(CVars.MidiVolume);
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
{
_volume = value;
_volumeDirty = true;
}, true);
_midiSawmill = _logger.GetSawmill("midi");
_midiSawmill.Level = LogLevel.Info;
_sawmill = _logger.GetSawmill("midi.fluidsynth");
_loggerDelegate = LoggerDelegate;
if (!_resourceManager.UserData.Exists(CustomSoundfontDirectory))
{
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
}
// not a directory, preserve the old file and create an actual directory
else if (!_resourceManager.UserData.IsDir(CustomSoundfontDirectory))
{
_resourceManager.UserData.Rename(CustomSoundfontDirectory, CustomSoundfontDirectory.WithName(CustomSoundfontDirectory.Filename + ".old"));
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
}
[ViewVariables]
private readonly List<IMidiRenderer> _renderers = new();
private bool _alive = true;
private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
try
{
get => _volume;
set
{
if (MathHelper.CloseToPercent(_volume, value))
return;
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
_cfgMan.SetCVar(CVars.MidiVolume, value);
_volumeDirty = true;
}
_settings = new Settings();
_settings["synth.sample-rate"].DoubleValue = 44100;
_settings["player.timing-source"].StringValue = "sample";
_settings["synth.lock-memory"].IntValue = 0;
_settings["synth.threadsafe-api"].IntValue = 1;
_settings["synth.gain"].DoubleValue = 1.0d;
_settings["synth.polyphony"].IntValue = 1024;
_settings["synth.cpu-cores"].IntValue = 2;
_settings["synth.midi-channels"].IntValue = 16;
_settings["synth.overflow.age"].DoubleValue = 3000;
_settings["audio.driver"].StringValue = "file";
_settings["audio.periods"].IntValue = 8;
_settings["audio.period-size"].IntValue = 4096;
_settings["midi.autoconnect"].IntValue = 1;
_settings["player.reset-synth"].IntValue = 0;
_settings["synth.midi-bank-select"].StringValue = "gm";
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
}
catch (Exception e)
{
_midiSawmill.Warning(
"Failed to initialize fluidsynth due to exception, disabling MIDI support:\n{0}", e);
_failedInitialize = true;
return;
}
private static readonly string[] LinuxSoundfonts =
{
"/usr/share/soundfonts/default.sf2",
"/usr/share/soundfonts/FluidR3_GM.sf2",
"/usr/share/soundfonts/freepats-general-midi.sf2",
"/usr/share/sounds/sf2/default.sf2",
"/usr/share/sounds/sf2/FluidR3_GM.sf2",
"/usr/share/sounds/sf2/TimGM6mb.sf2",
_midiThread = new Thread(ThreadUpdate);
_midiThread.Start();
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
FluidsynthInitialized = true;
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch {
NFluidsynth.Logger.LogLevel.Panic => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Error => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Warning => LogLevel.Warning,
NFluidsynth.Logger.LogLevel.Information => LogLevel.Info,
NFluidsynth.Logger.LogLevel.Debug => LogLevel.Debug,
_ => LogLevel.Debug
};
_sawmill.Log(rLevel, message);
}
private const string WindowsSoundfont = @"C:\WINDOWS\system32\drivers\gm.dls";
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
private bool FluidsynthInitialized;
private bool _failedInitialize;
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
private ISawmill _sawmill = default!;
[ViewVariables(VVAccess.ReadWrite)]
public int OcclusionCollisionMask { get; set; }
private void InitializeFluidsynth()
public IMidiRenderer? GetNewRenderer(bool mono = true)
{
if (!FluidsynthInitialized)
{
if (FluidsynthInitialized || _failedInitialize) return;
InitializeFluidsynth();
_volume = _cfgMan.GetCVar(CVars.MidiVolume);
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
if (!FluidsynthInitialized) // init failed
{
_volume = value;
_volumeDirty = true;
}, true);
_midiSawmill = Logger.GetSawmill("midi");
_sawmill = Logger.GetSawmill("midi.fluidsynth");
_loggerDelegate = LoggerDelegate;
try
{
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
_settings = new Settings();
_settings["synth.sample-rate"].DoubleValue = 44100;
_settings["player.timing-source"].StringValue = "sample";
_settings["synth.lock-memory"].IntValue = 0;
_settings["synth.threadsafe-api"].IntValue = 1;
_settings["synth.gain"].DoubleValue = 1.0d;
_settings["synth.polyphony"].IntValue = 1024;
_settings["synth.cpu-cores"].IntValue = 2;
_settings["synth.overflow.age"].DoubleValue = 3000;
_settings["audio.driver"].StringValue = "file";
_settings["audio.periods"].IntValue = 8;
_settings["audio.period-size"].IntValue = 4096;
_settings["midi.autoconnect"].IntValue = 1;
_settings["player.reset-synth"].IntValue = 0;
_settings["synth.midi-bank-select"].StringValue = "gm";
}
catch (Exception e)
{
Logger.WarningS("midi",
"Failed to initialize fluidsynth due to exception, disabling MIDI support:\n{0}", e);
_failedInitialize = true;
return;
}
_midiThread = new Thread(ThreadUpdate);
_midiThread.Start();
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
FluidsynthInitialized = true;
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch {
NFluidsynth.Logger.LogLevel.Panic => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Error => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Warning => LogLevel.Warning,
NFluidsynth.Logger.LogLevel.Information => LogLevel.Info,
NFluidsynth.Logger.LogLevel.Debug => LogLevel.Debug,
_ => LogLevel.Debug
};
_sawmill.Log(rLevel, message);
}
public IMidiRenderer? GetNewRenderer()
{
if (!FluidsynthInitialized)
{
InitializeFluidsynth();
if (!FluidsynthInitialized) // init failed
{
return null;
}
}
var soundfontLoader = SoundFontLoader.NewDefaultSoundFontLoader(_settings);
// Just making double sure these don't get GC'd.
// They shouldn't, MidiRenderer keeps a ref, but making sure...
var handle = GCHandle.Alloc(soundfontLoader);
try
{
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader);
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
{
if (file.Extension != "sf2" && file.Extension != "dls") continue;
renderer.LoadSoundfont(file.ToString());
}
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
if (OperatingSystem.IsLinux())
{
foreach (var filepath in LinuxSoundfonts)
{
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
try
{
renderer.LoadSoundfont(filepath, true);
}
catch (Exception)
{
continue;
}
break;
}
}
else if (OperatingSystem.IsMacOS())
{
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
renderer.LoadSoundfont(OsxSoundfont, true);
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
renderer.LoadSoundfont(WindowsSoundfont, true);
}
renderer.Source.SetVolume(Volume);
lock (_renderers)
{
_renderers.Add(renderer);
}
return renderer;
}
finally
{
handle.Free();
return null;
}
}
public void FrameUpdate(float frameTime)
var soundfontLoader = SoundFontLoader.NewDefaultSoundFontLoader(_settings);
// Just making double sure these don't get GC'd.
// They shouldn't, MidiRenderer keeps a ref, but making sure...
var handle = GCHandle.Alloc(soundfontLoader);
try
{
if (!FluidsynthInitialized)
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
{
return;
if (file.Extension != "sf2" && file.Extension != "dls") continue;
renderer.LoadSoundfont(file.ToString());
}
// Update positions of streams every frame.
foreach (var renderer in _renderers)
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
if (OperatingSystem.IsLinux())
{
if (renderer.Disposed)
continue;
if(_volumeDirty)
renderer.Source.SetVolume(Volume);
if (!renderer.Mono)
foreach (var filepath in LinuxSoundfonts)
{
renderer.Source.SetGlobal();
continue;
}
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
MapCoordinates? mapPos = null;
var trackingEntity = renderer.TrackingEntity != null && !_entityManager.Deleted(renderer.TrackingEntity);
if (trackingEntity)
{
renderer.TrackingCoordinates = _entityManager.GetComponent<TransformComponent>(renderer.TrackingEntity!.Value).Coordinates;
}
if (renderer.TrackingCoordinates != null)
{
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
}
if (mapPos != null && mapPos.Value.MapId == _eyeManager.CurrentMap)
{
var pos = mapPos.Value;
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
try
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
renderer.TrackingEntity);
renderer.LoadSoundfont(filepath, true);
}
catch (Exception)
{
continue;
}
renderer.Source.SetOcclusion(occlusion);
if (!renderer.Source.SetPosition(pos.Position))
{
return;
}
if (trackingEntity)
{
renderer.Source.SetVelocity(renderer.TrackingEntity!.Value.GlobalLinearVelocity());
}
}
else
{
renderer.Source.SetOcclusion(float.MaxValue);
break;
}
}
_volumeDirty = false;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
private void ThreadUpdate()
{
while (_alive)
else if (OperatingSystem.IsMacOS())
{
lock (_renderers)
{
for (var i = 0; i < _renderers.Count; i++)
{
var renderer = _renderers[i];
if (!renderer.Disposed)
renderer.Render();
else
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
}
}
Thread.Sleep(1);
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
renderer.LoadSoundfont(OsxSoundfont, true);
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
renderer.LoadSoundfont(WindowsSoundfont, true);
}
}
public void Shutdown()
{
_alive = false;
_midiThread?.Join();
_settings?.Dispose();
// load every soundfont from the user data directory
_midiSawmill.Debug($"loading soundfonts from {CustomSoundfontDirectory.ToRelativePath().ToString()}/*");
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath().ToString()}/*").Item1;
foreach (var soundfont in enumerator)
{
if (soundfont.Extension != "sf2" && soundfont.Extension != "dls") continue;
_midiSawmill.Debug($"loading soundfont {soundfont}");
renderer.LoadSoundfont(soundfont.ToString());
}
renderer.Source.SetVolume(Volume);
lock (_renderers)
{
foreach (var renderer in _renderers)
{
renderer?.Dispose();
}
_renderers.Add(renderer);
}
return renderer;
}
finally
{
handle.Free();
}
}
public void FrameUpdate(float frameTime)
{
if (!FluidsynthInitialized)
{
return;
}
// Update positions of streams every frame.
foreach (var renderer in _renderers)
{
if (renderer.Disposed)
continue;
if(_volumeDirty)
renderer.Source.SetVolume(Volume);
if (!renderer.Mono)
{
renderer.Source.SetGlobal();
continue;
}
if (FluidsynthInitialized && !_failedInitialize)
MapCoordinates? mapPos = null;
var trackingEntity = renderer.TrackingEntity != null && !_entityManager.Deleted(renderer.TrackingEntity);
if (trackingEntity)
{
NFluidsynth.Logger.SetLoggerMethod(null);
renderer.TrackingCoordinates = _entityManager.GetComponent<TransformComponent>(renderer.TrackingEntity!.Value).Coordinates;
}
if (renderer.TrackingCoordinates != null)
{
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
}
if (mapPos != null && mapPos.Value.MapId == _eyeManager.CurrentMap)
{
var pos = mapPos.Value;
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
renderer.TrackingEntity);
}
renderer.Source.SetOcclusion(occlusion);
if (!renderer.Source.SetPosition(pos.Position))
{
return;
}
if (trackingEntity)
{
renderer.Source.SetVelocity(renderer.TrackingEntity!.Value.GlobalLinearVelocity());
}
}
else
{
renderer.Source.SetOcclusion(float.MaxValue);
}
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
_volumeDirty = false;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
private void ThreadUpdate()
{
while (_alive)
{
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public override IntPtr Open(string filename)
lock (_renderers)
{
if (string.IsNullOrEmpty(filename))
for (var i = 0; i < _renderers.Count; i++)
{
return IntPtr.Zero;
var renderer = _renderers[i];
if (!renderer.Disposed)
renderer.Render();
else
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
}
}
Stream? stream;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resourcePath = new ResourcePath(filename);
Thread.Sleep(1);
}
}
if (resourcePath.IsRooted && resourceCache.ContentFileExists(filename))
public void Shutdown()
{
_alive = false;
_midiThread?.Join();
_settings?.Dispose();
lock (_renderers)
{
foreach (var renderer in _renderers)
{
renderer?.Dispose();
}
}
if (FluidsynthInitialized && !_failedInitialize)
{
NFluidsynth.Logger.SetLoggerMethod(null);
}
}
/// <summary>
/// Internal method to get a human-readable representation of a <see cref="SequencerEvent"/>.
/// </summary>
internal static string SequencerEventToString(SequencerEvent midiEvent)
{
// ReSharper disable once UseStringInterpolation
return string.Format(
"{0} chan:{1:D2} key:{2:D5} bank:{3:D2} ctrl:{4:D5} dur:{5:D5} pitch:{6:D5} prog:{7:D3} val:{8:D5} vel:{9:D5}",
midiEvent.Type.ToString().PadLeft(22),
midiEvent.Channel,
midiEvent.Key,
midiEvent.Bank,
midiEvent.Control,
midiEvent.Duration,
midiEvent.Pitch,
midiEvent.Program,
midiEvent.Value,
midiEvent.Velocity);
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
{
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public override IntPtr Open(string filename)
{
if (string.IsNullOrEmpty(filename))
{
return IntPtr.Zero;
}
Stream? stream;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resourcePath = new ResourcePath(filename);
if (resourcePath.IsRooted)
{
// is it in content?
if (resourceCache.ContentFileExists(filename))
{
if (!resourceCache.TryContentFileRead(filename, out stream))
return IntPtr.Zero;
}
// is it in userdata?
else if (resourceCache.UserData.Exists(resourcePath))
{
stream = resourceCache.UserData.OpenRead(resourcePath);
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
@@ -421,73 +453,81 @@ namespace Robust.Client.Audio.Midi
{
return IntPtr.Zero;
}
var id = _nextStreamId++;
_openStreams.Add(id, stream);
return (IntPtr) id;
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
}
else
{
return IntPtr.Zero;
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
var id = _nextStreamId++;
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
_openStreams.Add(id, stream);
return (IntPtr) id;
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
stream.ReadExact(buffer);
stream.ReadExact(buffer);
buffer.CopyTo(span);
}
else
{
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
buffer.CopyTo(span);
}
catch (EndOfStreamException)
else
{
return -1;
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
return 0;
}
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
catch (EndOfStreamException)
{
var stream = _openStreams[(int) sfHandle];
stream.Seek(offset, origin);
return 0;
return -1;
}
public override int Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return 0;
}
return (int) stream.Position;
}
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
{
var stream = _openStreams[(int) sfHandle];
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Seek(offset, origin);
stream.Dispose();
return 0;
return 0;
}
public override int Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return (int) stream.Position;
}
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Dispose();
return 0;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,7 @@ namespace Robust.Client
{
if (RunLevel == ClientRunLevel.Connecting)
{
_net.Shutdown("Client mashing that connect button.");
_net.Reset("Client mashing that connect button.");
Reset();
}

View File

@@ -134,7 +134,7 @@ namespace Robust.Client.Console
if (!NetManager.IsConnected) // we don't care about session on client
return;
var msg = NetManager.CreateNetMessage<MsgConCmd>();
var msg = new MsgConCmd();
msg.Text = command;
NetManager.ClientSendMessage(msg);
}
@@ -198,7 +198,7 @@ namespace Robust.Client.Console
if (!NetManager.IsConnected)
return;
var msg = NetManager.CreateNetMessage<MsgConCmdReg>();
var msg = new MsgConCmdReg();
NetManager.ClientSendMessage(msg);
_requestedCommands = true;

View File

@@ -8,12 +8,12 @@ namespace Robust.Client.Console.Commands
{
public string Command => "physics";
public string Description => $"Shows a debug physics overlay. The arg supplied specifies the overlay.";
public string Help => $"{Command} <aabbs / contactnormals / contactpoints / joints / shapeinfo / shapes>";
public string Help => $"{Command} <aabbs / com / contactnormals / contactpoints / joints / shapeinfo / shapes>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine($"Invalid number of args supplied");
shell.WriteError($"Invalid number of args supplied");
return;
}
@@ -43,7 +43,7 @@ namespace Robust.Client.Console.Commands
system.Flags ^= PhysicsDebugFlags.Shapes;
break;
default:
shell.WriteLine($"{args[0]} is not a recognised overlay");
shell.WriteError($"{args[0]} is not a recognised overlay");
return;
}

View File

@@ -15,7 +15,7 @@ namespace Robust.Client.Console.Commands
// MsgStringTableEntries is registered as NetMessageAccept.Client so the server will immediately deny it.
// And kick us.
var net = IoCManager.Resolve<IClientNetManager>();
var msg = net.CreateNetMessage<MsgStringTableEntries>();
var msg = new MsgStringTableEntries();
msg.Entries = new MsgStringTableEntries.Entry[0];
net.ClientSendMessage(msg);
}

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.Console
RunButton.Disabled = true;
var msg = _client._netManager.CreateNetMessage<MsgScriptEval>();
var msg = new MsgScriptEval();
msg.ScriptSession = _session;
msg.Code = _lastEnteredText = InputBar.Text;
@@ -48,7 +48,7 @@ namespace Robust.Client.Console
protected override void Complete()
{
var msg = _client._netManager.CreateNetMessage<MsgScriptCompletion>();
var msg = new MsgScriptCompletion();
msg.ScriptSession = _session;
msg.Code = InputBar.Text;
msg.Cursor = InputBar.CursorPosition;

View File

@@ -65,7 +65,7 @@ namespace Robust.Client.Console
throw new InvalidOperationException("We do not have scripting permission.");
}
var msg = _netManager.CreateNetMessage<MsgScriptStart>();
var msg = new MsgScriptStart();
msg.ScriptSession = _nextSessionId++;
_netManager.ClientSendMessage(msg);
}
@@ -74,7 +74,7 @@ namespace Robust.Client.Console
{
_activeConsoles.Remove(session);
var msg = _netManager.CreateNetMessage<MsgScriptStop>();
var msg = new MsgScriptStop();
msg.ScriptSession = session;
_netManager.ClientSendMessage(msg);
}

View File

@@ -151,10 +151,12 @@ namespace Robust.Client.Debugging
/// Shows the world point for each contact in the viewport.
/// </summary>
ContactPoints = 1 << 0,
/// <summary>
/// Shows the world normal for each contact in the viewport.
/// </summary>
ContactNormals = 1 << 1,
/// <summary>
/// Shows all physics shapes in the viewport.
/// </summary>
@@ -162,6 +164,10 @@ namespace Robust.Client.Debugging
ShapeInfo = 1 << 3,
Joints = 1 << 4,
AABBs = 1 << 5,
/// <summary>
/// Shows Center of Mass for all bodies in the viewport.
/// </summary>
COM = 1 << 6,
}

View File

@@ -21,7 +21,6 @@ using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -29,6 +28,7 @@ using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
@@ -66,6 +66,7 @@ namespace Robust.Client
[Dependency] private readonly IAuthManager _authManager = default!;
[Dependency] private readonly IMidiManager _midiManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IParallelManagerInternal _parallelMgr = default!;
private IWebViewManagerHook? _webViewHook;
@@ -131,8 +132,9 @@ namespace Robust.Client
_inputManager.Initialize();
_console.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDirectory(new ResourcePath("/EnginePrototypes/"));
_prototypeManager.LoadDirectory(Options.PrototypeDirectory);
_prototypeManager.Resync();
_prototypeManager.ResolveResults();
_entityManager.Initialize();
_mapManager.Initialize();
_gameStateManager.Initialize();
@@ -326,6 +328,8 @@ namespace Robust.Client
ProfileOptSetup.Setup(_configurationManager);
_parallelMgr.Initialize();
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
var mountOptions = _commandLineArgs != null

View File

@@ -86,7 +86,7 @@ namespace Robust.Client.GameObjects
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
{
var msg = _networkManager.CreateNetMessage<MsgEntity>();
var msg = new MsgEntity();
msg.Type = EntityMessageType.SystemMessage;
msg.SystemMessage = message;
msg.SourceTick = _gameTiming.CurTick;

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
@@ -32,11 +33,13 @@ namespace Robust.Client.GameObjects
public const string LogCategory = "go.comp.icon";
const string SerializationCache = "icon";
[Obsolete("Use SpriteSystem instead.")]
private static IRsiStateLike TextureForConfig(IconComponent compData, IResourceCache resourceCache)
{
return compData.Icon?.Default ?? resourceCache.GetFallback<TextureResource>().Texture;
}
[Obsolete("Use SpriteSystem instead.")]
public static IRsiStateLike? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
if (!prototype.Components.TryGetValue("Icon", out var compData))

View File

@@ -601,8 +601,10 @@ namespace Robust.Client.GameObjects
private void RebuildBounds()
{
_bounds = new Box2();
foreach (var layer in Layers.Where(layer => layer.Visible))
foreach (var layer in Layers)
{
if (!layer.Visible) continue;
_bounds = _bounds.Union(layer.CalculateBoundingBox());
}
}
@@ -1520,6 +1522,7 @@ namespace Robust.Client.GameObjects
}
}
[Obsolete("Use SpriteSystem instead.")]
internal static RSI.State GetFallbackState(IResourceCache cache)
{
var rsi = cache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
@@ -1968,7 +1971,7 @@ namespace Robust.Client.GameObjects
/// <inheritdoc/>
public Box2 CalculateBoundingBox()
{
var textureSize = PixelSize / EyeManager.PixelsPerMeter;
var textureSize = (Vector2) PixelSize / EyeManager.PixelsPerMeter;
// If the parent has locked rotation and we don't have any rotation,
// we can take the quick path of just making a box the size of the texture.

View File

@@ -75,6 +75,10 @@ namespace Robust.Client.GameObjects
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[] {this, wrapped.UiKey});
boundInterface.Open();
_openInterfaces[wrapped.UiKey] = boundInterface;
var playerSession = _playerManager.LocalPlayer?.Session;
if(playerSession != null)
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIOpenedEvent(wrapped.UiKey, Owner, playerSession));
}
internal void Close(object uiKey, bool remoteCall)

View File

@@ -1,12 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
@@ -35,7 +33,8 @@ namespace Robust.Client.GameObjects
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(HandleAnchorChanged);
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<ClientOccluderComponent, ReAnchorEvent>(OnReAnchor);
}
public override void FrameUpdate(float frameTime)
@@ -62,7 +61,12 @@ namespace Robust.Client.GameObjects
}
}
private static void HandleAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
private static void OnAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
{
component.AnchorStateChanged();
}
private void OnReAnchor(EntityUid uid, ClientOccluderComponent component, ref ReAnchorEvent args)
{
component.AnchorStateChanged();
}

View File

@@ -0,0 +1,105 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.GameObjects;
public sealed class DebugEntityLookupCommand : IConsoleCommand
{
public string Command => "togglelookup";
public string Description => "Shows / hides entitylookup bounds via an overlay";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugEntityLookupSystem>().Enabled ^= true;
}
}
public sealed class DebugEntityLookupSystem : EntitySystem
{
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value) return;
_enabled = value;
if (_enabled)
{
IoCManager.Resolve<IOverlayManager>().AddOverlay(
new EntityLookupOverlay(
EntityManager,
Get<EntityLookupSystem>()));
}
else
{
IoCManager.Resolve<IOverlayManager>().RemoveOverlay<EntityLookupOverlay>();
}
}
}
private bool _enabled;
}
public sealed class EntityLookupOverlay : Overlay
{
private IEntityManager _entityManager = default!;
private EntityLookupSystem _lookup = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityLookupOverlay(IEntityManager entManager, EntityLookupSystem lookup)
{
_entityManager = entManager;
_lookup = lookup;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var worldHandle = args.WorldHandle;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var lookup in _lookup.FindLookupsIntersecting(args.MapId, args.WorldBounds))
{
var lookupXform = xformQuery.GetComponent(lookup.Owner);
var (_, rotation, matrix, invMatrix) = lookupXform.GetWorldPositionRotationMatrixWithInv();
worldHandle.SetTransform(matrix);
var lookupAABB = invMatrix.TransformBox(args.WorldBounds);
var ents = new List<EntityUid>();
// Gonna allocate a lot but debug overlay sooo
lookup.Tree._b2Tree.FastQuery(ref lookupAABB, (ref EntityUid data) =>
{
ents.Add(data);
});
foreach (var ent in ents)
{
if (_entityManager.Deleted(ent)) continue;
var xform = xformQuery.GetComponent(ent);
//DebugTools.Assert(!ent.IsInContainer(_entityManager));
var (entPos, entRot) = xform.GetWorldPositionRotation();
var lookupPos = invMatrix.Transform(entPos);
var lookupRot = entRot - rotation;
var aabb = _lookup.GetAABB(ent, lookupPos, lookupRot, xform, xformQuery);
worldHandle.DrawRect(aabb, Color.Blue.WithAlpha(0.2f));
}
}
}
}

View File

@@ -1,9 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -17,13 +18,13 @@ public sealed partial class SpriteSystem
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Pure]
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
[Pure]
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
@@ -35,38 +36,71 @@ public sealed partial class SpriteSystem
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
if (!_proto.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
return SpriteComponent.GetFallbackState(_resourceCache);
}
return SpriteComponent.GetPrototypeIcon(prototype, _resourceCache);
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
default:
throw new NotSupportedException();
}
}
[Pure]
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method caches the result based on the prototype identifier.
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
var icon = IconComponent.GetPrototypeIcon(prototype, _resourceCache);
if (icon != null) return icon;
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!prototype.Components.ContainsKey("Sprite"))
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
return SpriteComponent.GetFallbackState(resourceCache);
// The specified prototype doesn't exist, return the fallback "error" sprite.
Logger.Error("Failed to load PrototypeIcon {0}", prototype);
return GetFallbackState();
}
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData is IconComponent {Icon: {} icon})
{
return icon.Default;
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
{
return GetFallbackState();
}
// Finally, we use spawn a dummy entity to get its icon.
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? SpriteComponent.GetFallbackState(resourceCache);
var result = spriteComponent.Icon ?? GetFallbackState();
Del(dummy);
return result;
}
[Pure]
public RSI.State GetFallbackState()
{
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
@@ -79,6 +113,20 @@ public sealed partial class SpriteSystem
}
Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return SpriteComponent.GetFallbackState(_resourceCache);
return GetFallbackState();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs protoReloaded)
{
// Check if any EntityPrototype has been changed.
if (!protoReloaded.ByType.TryGetValue(typeof(EntityPrototype), out var changedSet))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var (prototype, _) in changedSet.Modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);
}
}
}

View File

@@ -23,9 +23,16 @@ namespace Robust.Client.GameObjects
{
base.Initialize();
_proto.PrototypesReloaded += OnPrototypesReloaded;
SubscribeLocalEvent<SpriteUpdateInertEvent>(QueueUpdateInert);
}
public override void Shutdown()
{
base.Shutdown();
_proto.PrototypesReloaded -= OnPrototypesReloaded;
}
private void QueueUpdateInert(SpriteUpdateInertEvent ev)
{
_inertUpdateQueue.Enqueue(ev.Sprite);

View File

@@ -409,7 +409,7 @@ namespace Robust.Client.GameStates
private void AckGameState(GameTick sequence)
{
var msg = _network.CreateNetMessage<MsgStateAck>();
var msg = new MsgStateAck();
msg.Sequence = sequence;
_network.ClientSendMessage(msg);
}
@@ -429,7 +429,7 @@ namespace Robust.Client.GameStates
private List<EntityUid> ApplyEntityStates(ReadOnlySpan<EntityState> curEntStates, ReadOnlySpan<EntityUid> deletions,
ReadOnlySpan<EntityState> nextEntStates)
{
var toApply = new Dictionary<EntityUid, (EntityState?, EntityState?)>();
var toApply = new Dictionary<EntityUid, (EntityState?, EntityState?)>(curEntStates.Length);
var toInitialize = new List<EntityUid>();
var created = new List<EntityUid>();
@@ -548,6 +548,8 @@ namespace Robust.Client.GameStates
if (curState != null)
{
compStateWork.EnsureCapacity(curState.ComponentChanges.Span.Length);
foreach (var compChange in curState.ComponentChanges.Span)
{
if (compChange.Deleted)
@@ -580,6 +582,8 @@ namespace Robust.Client.GameStates
if (nextState != null)
{
compStateWork.EnsureCapacity(compStateWork.Count + nextState.ComponentChanges.Span.Length);
foreach (var compState in nextState.ComponentChanges.Span)
{
if (compStateWork.TryGetValue(compState.NetID, out var state))

View File

@@ -144,7 +144,7 @@ namespace Robust.Client.Graphics.Clyde
var worldAABB = CalcWorldAABB(vp);
var worldBounds = CalcWorldBounds(vp);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, worldAABB, worldBounds);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, vp.Eye!.Position.MapId, worldAABB, worldBounds);
foreach (var overlay in list)
{

View File

@@ -415,9 +415,15 @@ namespace Robust.Client.Graphics.Clyde
case float f:
program.SetUniform(name, f);
break;
case float[] fArr:
program.SetUniform(name, fArr);
break;
case Vector2 vector2:
program.SetUniform(name, vector2);
break;
case Vector2[] vector2Arr:
program.SetUniform(name, vector2Arr);
break;
case Vector3 vector3:
program.SetUniform(name, vector3);
break;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -395,12 +395,24 @@ namespace Robust.Client.Graphics.Clyde
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, float[] value)
{
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Vector2 value)
{
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Vector2[] value)
{
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Vector3 value)
{
var data = Parent._shaderInstances[Handle];

View File

@@ -412,10 +412,18 @@ namespace Robust.Client.Graphics.Clyde
{
}
private protected override void SetParameterImpl(string name, float[] value)
{
}
private protected override void SetParameterImpl(string name, Vector2 value)
{
}
private protected override void SetParameterImpl(string name, Vector2[] value)
{
}
private protected override void SetParameterImpl(string name, Vector3 value)
{
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;
@@ -234,6 +234,19 @@ namespace Robust.Client.Graphics.Clyde
GL.Uniform1(uniformId, single);
}
public void SetUniform(string uniformName, float[] singles)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, singles.Length, singles);
_clyde.CheckGlError();
}
public void SetUniform(int uniformName, float[] singles)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, singles.Length, singles);
}
public void SetUniform(string uniformName, in Matrix3 matrix)
{
var uniformId = GetUniform(uniformName);
@@ -375,6 +388,31 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniform(string uniformName, Vector2[] vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
public void SetUniform(int uniformName, Vector2[] vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, Vector2[] vectors)
{
unsafe
{
fixed (Vector2* ptr = &vectors[0])
{
GL.Uniform2(slot, vectors.Length, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniformTexture(string uniformName, TextureUnit textureUnit)
{
var uniformId = GetUniform(uniformName);

View File

@@ -98,7 +98,7 @@ namespace Robust.Client.Graphics
handle = renderHandle.DrawingHandleWorld;
}
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, worldBox, worldBounds);
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, vp.Eye!.Position.MapId, worldBox, worldBounds);
Draw(args);
}

View File

@@ -1,6 +1,7 @@
using JetBrains.Annotations;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -38,6 +39,11 @@ namespace Robust.Client.Graphics
/// </summary>
public readonly UIBox2i ViewportBounds;
/// <summary>
/// <see cref="MapId"/> of the viewport's eye.
/// </summary>
public readonly MapId MapId;
/// <summary>
/// AABB enclosing the area visible in the viewport.
/// </summary>
@@ -57,6 +63,7 @@ namespace Robust.Client.Graphics
IClydeViewport viewport,
DrawingHandleBase drawingHandle,
in UIBox2i viewportBounds,
in MapId mapId,
in Box2 worldAabb,
in Box2Rotated worldBounds)
{
@@ -65,6 +72,7 @@ namespace Robust.Client.Graphics
Viewport = viewport;
DrawingHandle = drawingHandle;
ViewportBounds = viewportBounds;
MapId = mapId;
WorldAABB = worldAabb;
WorldBounds = worldBounds;
}

View File

@@ -135,36 +135,42 @@ namespace Robust.Client.Graphics
internal sealed class ShaderDataTypeFull
{
public ShaderDataTypeFull(ShaderDataType type, ShaderPrecisionQualifier prec)
public ShaderDataTypeFull(ShaderDataType type, ShaderPrecisionQualifier prec, int? count = null)
{
Type = type;
Precision = prec;
Count = count;
}
public ShaderDataType Type { get; }
public ShaderPrecisionQualifier Precision { get; }
public int? Count;
public bool IsArray => Count != null;
public string GetNativeType()
{
if (Precision == ShaderPrecisionQualifier.Low)
string? precision = Precision switch
{
return "lowp " + Type.GetNativeType();
}
else if (Precision == ShaderPrecisionQualifier.Medium)
{
return "mediump " + Type.GetNativeType();
}
else if (Precision == ShaderPrecisionQualifier.High)
{
return "highp " + Type.GetNativeType();
}
return Type.GetNativeType();
ShaderPrecisionQualifier.Low => "lowp ",
ShaderPrecisionQualifier.Medium => "mediump ",
ShaderPrecisionQualifier.High => "highp ",
_ => null,
};
return IsArray ? $"{precision}{Type.GetNativeType()}[{Count}]" : $"{precision}{Type.GetNativeType()}";
}
public bool TypePrecisionConsistent()
{
return Type.TypeHasPrecision() == (Precision != ShaderPrecisionQualifier.None);
}
public bool TypeCountConsistent()
{
return Count == null || Type.TypeSupportsArrays();
}
}
internal static class ShaderEnumExt
@@ -203,6 +209,14 @@ namespace Robust.Client.Graphics
(type == ShaderDataType.Mat4);
}
public static bool TypeSupportsArrays(this ShaderDataType type)
{
// TODO: add support for int, and vec3/4 arrays
return
(type == ShaderDataType.Float) ||
(type == ShaderDataType.Vec2);
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]
private static readonly Dictionary<ShaderDataType, string> _nativeTypes = new()
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -119,6 +119,13 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, float[] value)
{
EnsureAlive();
EnsureMutable();
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector2 value)
{
EnsureAlive();
@@ -126,6 +133,13 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector2[] value)
{
EnsureAlive();
EnsureMutable();
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector3 value)
{
EnsureAlive();
@@ -244,7 +258,9 @@ namespace Robust.Client.Graphics
private protected abstract ShaderInstance DuplicateImpl();
private protected abstract void SetParameterImpl(string name, float value);
private protected abstract void SetParameterImpl(string name, float[] value);
private protected abstract void SetParameterImpl(string name, Vector2 value);
private protected abstract void SetParameterImpl(string name, Vector2[] value);
private protected abstract void SetParameterImpl(string name, Vector3 value);
private protected abstract void SetParameterImpl(string name, Vector4 value);
private protected abstract void SetParameterImpl(string name, Color value);

View File

@@ -504,17 +504,33 @@ namespace Robust.Client.Graphics
continue;
}
if (_shaderTypeMap.TryGetValue(wordType.Word, out var ret))
if (!_shaderTypeMap.TryGetValue(wordType.Word, out var ret))
throw new ShaderParseException("Expected type", wordType.Position);
var result = new ShaderDataTypeFull(ret, precision);
if (!result.TypePrecisionConsistent())
{
var result = new ShaderDataTypeFull(ret, precision);
if (!result.TypePrecisionConsistent())
{
throw new ShaderParseException($"Type {ret} cannot accept precision {precision}", wordType.Position);
}
return result;
throw new ShaderParseException($"Type {ret} cannot accept precision {precision}", wordType.Position);
}
throw new ShaderParseException("Expected type or precision", wordType.Position);
// is this type meant to be an array?
if (_peekToken() is TokenSymbol bracketOpen && bracketOpen.Symbol == Symbols.BracketOpen)
{
_takeToken();
if (_takeToken() is not TokenNumber number || !int.TryParse(number.Number, out var count))
throw new ShaderParseException($"Failed to parse array length", bracketOpen.Position);
if (_takeToken() is not TokenSymbol bracketClose || bracketClose.Symbol != Symbols.BracketClosed)
throw new ShaderParseException($"Array length definition missing closing bracket", number.Position);
result.Count = count;
// are arrays supported by this type?
if (!result.TypeCountConsistent())
throw new ShaderParseException($"Type {ret} does not support arrays", wordType.Position);
}
return result;
}
}

View File

@@ -19,7 +19,7 @@ namespace Robust.Client.Graphics
[Dependency] private readonly IResourceCache _resourceCache = default!;
[ViewVariables]
[DataField("id", required: true)]
[IdDataFieldAttribute]
public string ID { get; } = default!;
private ShaderKind Kind;

View File

@@ -497,7 +497,7 @@ namespace Robust.Client.Input
if (robustMapping.TryGet("binds", out var BaseKeyRegsNode))
{
var baseKeyRegs = serializationManager.ReadValueOrThrow<KeyBindingRegistration[]>(BaseKeyRegsNode);
var baseKeyRegs = serializationManager.Read<KeyBindingRegistration[]>(BaseKeyRegsNode);
foreach (var reg in baseKeyRegs)
{
@@ -526,7 +526,7 @@ namespace Robust.Client.Input
if (userData && robustMapping.TryGet("leaveEmpty", out var node))
{
var leaveEmpty = serializationManager.ReadValueOrThrow<BoundKeyFunction[]>(node);
var leaveEmpty = serializationManager.Read<BoundKeyFunction[]>(node);
if (leaveEmpty.Length > 0)
{

View File

@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Physics
{
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
public bool EnableDebug
{
get => _enableDebug;
set
{
if (_enableDebug == value) return;
_enableDebug = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_enableDebug)
{
var overlay = new GridSplitNodeOverlay(EntityManager, IoCManager.Resolve<IMapManager>(), this);
overlayManager.AddOverlay(overlay);
RaiseNetworkEvent(new RequestGridNodesMessage());
}
else
{
overlayManager.RemoveOverlay<GridSplitNodeOverlay>();
RaiseNetworkEvent(new StopGridNodesMessage());
}
}
}
private bool _enableDebug = false;
private readonly Dictionary<EntityUid, Dictionary<Vector2i, List<List<Vector2i>>>> _nodes = new();
private readonly Dictionary<EntityUid, List<(Vector2, Vector2)>> _connections = new();
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<ChunkSplitDebugMessage>(OnDebugMessage);
}
public override void Shutdown()
{
base.Shutdown();
_nodes.Clear();
_connections.Clear();
}
private void OnDebugMessage(ChunkSplitDebugMessage ev)
{
if (!_enableDebug) return;
_nodes[ev.Grid] = ev.Nodes;
_connections[ev.Grid] = ev.Connections;
}
private sealed class GridSplitNodeOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private IEntityManager _entManager;
private IMapManager _mapManager;
private GridFixtureSystem _system;
public GridSplitNodeOverlay(IEntityManager entManager, IMapManager mapManager, GridFixtureSystem system)
{
_entManager = entManager;
_mapManager = mapManager;
_system = system;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var worldHandle = args.WorldHandle;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
foreach (var iGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
{
// May not have received nodes yet.
if (!_system._nodes.TryGetValue(iGrid.GridEntityId, out var nodes)) continue;
var gridXform = xformQuery.GetComponent(iGrid.GridEntityId);
worldHandle.SetTransform(gridXform.WorldMatrix);
var grid = (MapGrid)iGrid;
grid.GetMapChunks(args.WorldBounds, out var chunkEnumerator);
while (chunkEnumerator.MoveNext(out var chunk))
{
if (!nodes.TryGetValue(chunk.Indices, out var chunkNodes)) continue;
for (var i = 0; i < chunkNodes.Count; i++)
{
var group = chunkNodes[i];
var offset = chunk.Indices * chunk.ChunkSize;
var color = GetColor(chunk, i);
foreach (var index in group)
{
worldHandle.DrawRect(new Box2(offset + index, offset + index + 1).Enlarged(-0.1f), color);
}
}
}
var connections = _system._connections[iGrid.GridEntityId];
foreach (var (start, end) in connections)
{
worldHandle.DrawLine(start, end, Color.Aquamarine);
}
}
}
private Color GetColor(MapChunk chunk, int index)
{
var red = Math.Abs(chunk.Indices.X * 30 % 255);
var green = Math.Abs(chunk.Indices.Y * 30 % 255);
var blue = index * 30 % 255;
return new Color(red, green, blue, 0.3f);
}
}
}
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Physics;
public sealed class GridSplitVisualCommand : IConsoleCommand
{
public string Command => SharedGridFixtureSystem.ShowGridNodesCommand;
public string Description => "Shows the nodes for grid split purposes";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GridFixtureSystem>();
system.EnableDebug ^= true;
shell.WriteLine($"Toggled gridsplit node visuals");
}
}

View File

@@ -49,7 +49,7 @@ namespace Robust.Client.Physics
// Also need to suss out having the client build the island anyway and just... not solving it?
foreach (var body in AwakeBodies)
{
if (body.Island || body.LinearVelocity.Length > _linSleepTolerance / 2f || body.AngularVelocity > _angSleepTolerance / 2f) continue;
if (body.LinearVelocity.Length > _linSleepTolerance / 2f || body.AngularVelocity > _angSleepTolerance / 2f) continue;
body.SleepTime += frameTime;
if (body.SleepTime > _timeToSleep)
{

View File

@@ -418,7 +418,7 @@ namespace Robust.Client.Placement
if (!IsActive || !Eraser) return;
if (Hijack != null && Hijack.HijackDeletion(entity)) return;
var msg = NetworkManager.CreateNetMessage<MsgPlacement>();
var msg = new MsgPlacement();
msg.PlaceType = PlacementManagerMessage.RequestEntRemove;
msg.EntityUid = entity;
NetworkManager.ClientSendMessage(msg);
@@ -426,7 +426,7 @@ namespace Robust.Client.Placement
public void HandleRectDeletion(EntityCoordinates start, Box2 rect)
{
var msg = NetworkManager.CreateNetMessage<MsgPlacement>();
var msg = new MsgPlacement();
msg.PlaceType = PlacementManagerMessage.RequestRectRemove;
msg.EntityCoordinates = new EntityCoordinates(StartPoint.EntityId, rect.BottomLeft);
msg.RectSize = rect.Size;
@@ -748,7 +748,7 @@ namespace Robust.Client.Placement
_pendingTileChanges.Add(tuple);
}
var message = NetworkManager.CreateNetMessage<MsgPlacement>();
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.RequestPlacement;
message.Align = CurrentMode.ModeName;

View File

@@ -170,10 +170,9 @@ namespace Robust.Client.Placement
/// </summary>
public TileRef GetTileRef(EntityCoordinates coordinates)
{
var mapCoords = coordinates.ToMap(pManager.EntityManager);
var gridId = coordinates.GetGridId(pManager.EntityManager);
return gridId.IsValid() ? pManager.MapManager.GetGrid(gridId).GetTileRef(MouseCoords)
: new TileRef(mapCoords.MapId, gridId,
: new TileRef(gridId,
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager), Tile.Empty);
}

View File

@@ -89,7 +89,7 @@ namespace Robust.Client.Player
{
LocalPlayer = new LocalPlayer();
var msgList = _network.CreateNetMessage<MsgPlayerListReq>();
var msgList = new MsgPlayerListReq();
// message is empty
_network.ClientSendMessage(msgList);
}

View File

@@ -57,7 +57,7 @@ namespace Robust.Client.Prototypes
#if !FULL_RELEASE
var sw = Stopwatch.StartNew();
var msg = _netManager.CreateNetMessage<MsgReloadPrototypes>();
var msg = new MsgReloadPrototypes();
msg.Paths = _reloadQueue.ToArray();
_netManager.ClientSendMessage(msg);

View File

@@ -22,6 +22,8 @@ namespace Robust.Client.ResourceManagement
{
private static readonly float[] OneArray = {1};
public override ResourcePath? Fallback => new("/Textures/error.rsi");
private static readonly JsonSerializerOptions SerializerOptions =
new JsonSerializerOptions(JsonSerializerDefaults.Web)
{

View File

@@ -1,10 +1,10 @@
using System;
using Robust.Client.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
@@ -16,26 +16,40 @@ namespace Robust.Client.Serialization
[TypeSerializer]
public sealed class AppearanceVisualizerSerializer : ITypeSerializer<AppearanceVisualizer, MappingDataNode>
{
public DeserializationResult Read(ISerializationManager serializationManager, MappingDataNode node,
public AppearanceVisualizer Read(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context = null)
ISerializationContext? context = null, AppearanceVisualizer? value = null)
{
Type? type = null;
if (!node.TryGet("type", out var typeNode))
throw new InvalidMappingException("No type specified for AppearanceVisualizer!");
{
if (value == null)
throw new InvalidMappingException("No type specified for AppearanceVisualizer!");
if (typeNode is not ValueDataNode typeValueDataNode)
throw new InvalidMappingException("Type node not a value node for AppearanceVisualizer!");
type = value.GetType();
}
else
{
if (typeNode is not ValueDataNode typeValueDataNode)
throw new InvalidMappingException("Type node not a value node for AppearanceVisualizer!");
var type = IoCManager.Resolve<IReflectionManager>()
.YamlTypeTagLookup(typeof(AppearanceVisualizer), typeValueDataNode.Value);
if (type == null)
throw new InvalidMappingException(
$"Invalid type {typeValueDataNode.Value} specified for AppearanceVisualizer!");
type = IoCManager.Resolve<IReflectionManager>()
.YamlTypeTagLookup(typeof(AppearanceVisualizer), typeValueDataNode.Value);
if (type == null)
throw new InvalidMappingException(
$"Invalid type {typeValueDataNode.Value} specified for AppearanceVisualizer!");
if(value != null && !type.IsInstanceOfType(value))
{
throw new InvalidMappingException(
$"Specified Type does not match type of provided Value for AppearanceVisualizer! (TypeOfValue: {value.GetType()}, ProvidedValue: {type})");
}
}
var newNode = node.Copy();
newNode.Remove("type");
return serializationManager.Read(type, newNode, context, skipHook);
return (AppearanceVisualizer) serializationManager.Read(type, newNode, context, skipHook, value)!;
}
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,

View File

@@ -0,0 +1,395 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface.Controls;
// condensed version of the original ColorSlider set
public sealed class ColorSelectorSliders : Control
{
public Color Color
{
get => _currentColor;
set
{
_updating = true;
_currentColor = value;
Update();
_updating = false;
}
}
public ColorSelectorType SelectorType
{
get => _currentType;
set
{
_updating = true;
_currentType = value;
UpdateType();
Update();
_updating = false;
}
}
public bool IsAlphaVisible
{
get => _isAlphaVisible;
set
{
_isAlphaVisible = value;
_alphaSliderBox.Visible = _isAlphaVisible;
}
}
public Action<Color>? OnColorChanged;
private bool _updating = false;
private Color _currentColor = Color.White;
private ColorSelectorType _currentType = ColorSelectorType.Rgb;
private bool _isAlphaVisible = false;
private ColorableSlider _topColorSlider;
private ColorableSlider _middleColorSlider;
private ColorableSlider _bottomColorSlider;
private Slider _alphaSlider;
private BoxContainer _alphaSliderBox = new();
private FloatSpinBox _topInputBox;
private FloatSpinBox _middleInputBox;
private FloatSpinBox _bottomInputBox;
private FloatSpinBox _alphaInputBox;
private Label _topSliderLabel = new();
private Label _middleSliderLabel = new();
private Label _bottomSliderLabel = new();
private Label _alphaSliderLabel = new();
private OptionButton _typeSelector;
private List<ColorSelectorType> _types = new();
public ColorSelectorSliders()
{
_topColorSlider = new ColorableSlider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f
};
_middleColorSlider = new ColorableSlider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f
};
_bottomColorSlider = new ColorableSlider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f
};
_alphaSlider = new Slider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f,
};
_topColorSlider.OnValueChanged += _ => { OnColorSet(); };
_middleColorSlider.OnValueChanged += _ => { OnColorSet(); };
_bottomColorSlider.OnValueChanged += _ => { OnColorSet(); };
_alphaSlider.OnValueChanged += _ => { OnColorSet(); };
_topInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Top)
};
_middleInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Middle)
};
_bottomInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Bottom)
};
_alphaInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Alpha)
};
_topInputBox.OnValueChanged += value =>
{
_topColorSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Top);
};
_middleInputBox.OnValueChanged += value =>
{
_middleColorSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Middle);
};
_bottomInputBox.OnValueChanged += value =>
{
_bottomColorSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Bottom);
};
_alphaInputBox.OnValueChanged += value =>
{
_alphaSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Alpha);
};
_alphaSliderLabel.Text = Loc.GetString("color-selector-sliders-alpha");
_typeSelector = new OptionButton();
foreach (var ty in Enum.GetValues<ColorSelectorType>())
{
_typeSelector.AddItem(Loc.GetString($"color-selector-sliders-{ty.ToString().ToLower()}"));
_types.Add(ty);
}
_typeSelector.OnItemSelected += args =>
{
SelectorType = _types[args.Id];
_typeSelector.Select(args.Id);
};
// TODO: Maybe some engine widgets could be laid out in XAML?
var rootBox = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical
};
AddChild(rootBox);
var headerBox = new BoxContainer();
rootBox.AddChild(headerBox);
headerBox.AddChild(_typeSelector);
var bodyBox = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical
};
// pita
var topSliderBox = new BoxContainer();
topSliderBox.AddChild(_topSliderLabel);
topSliderBox.AddChild(_topColorSlider);
topSliderBox.AddChild(_topInputBox);
var middleSliderBox = new BoxContainer();
middleSliderBox.AddChild(_middleSliderLabel);
middleSliderBox.AddChild(_middleColorSlider);
middleSliderBox.AddChild(_middleInputBox);
var bottomSliderBox = new BoxContainer();
bottomSliderBox.AddChild(_bottomSliderLabel);
bottomSliderBox.AddChild(_bottomColorSlider);
bottomSliderBox.AddChild(_bottomInputBox);
_alphaSliderBox.Visible = IsAlphaVisible;
_alphaSliderBox.AddChild(_alphaSliderLabel);
_alphaSliderBox.AddChild(_alphaSlider);
_alphaSliderBox.AddChild(_alphaInputBox);
bodyBox.AddChild(topSliderBox);
bodyBox.AddChild(middleSliderBox);
bodyBox.AddChild(bottomSliderBox);
bodyBox.AddChild(_alphaSliderBox);
rootBox.AddChild(bodyBox);
_updating = true;
UpdateType();
Update();
_updating = false;
}
private void UpdateType()
{
(string topLabel, string middleLabel, string bottomLabel) labels = GetSliderLabels();
_topSliderLabel.Text = labels.topLabel;
_middleSliderLabel.Text = labels.middleLabel;
_bottomSliderLabel.Text = labels.bottomLabel;
}
private void Update()
{
_topColorSlider.SetColor(_currentColor);
_middleColorSlider.SetColor(_currentColor);
_bottomColorSlider.SetColor(_currentColor);
switch (SelectorType)
{
case ColorSelectorType.Rgb:
_topColorSlider.Value = Color.R;
_middleColorSlider.Value = Color.G;
_bottomColorSlider.Value = Color.B;
_topInputBox.Value = Color.R * 255.0f;
_middleInputBox.Value = Color.G * 255.0f;
_bottomInputBox.Value = Color.B * 255.0f;
break;
case ColorSelectorType.Hsv:
Vector4 color = Color.ToHsv(Color);
// dumb workaround because the formula for
// HSV calculation results in a negative
// number in any value past 300 degrees
if (color.X > 0)
{
_topColorSlider.Value = color.X;
_topInputBox.Value = color.X * 360.0f;
}
else
{
_topInputBox.Value = _topColorSlider.Value * 360.0f;
}
_middleColorSlider.Value = color.Y;
_bottomColorSlider.Value = color.Z;
_middleInputBox.Value = color.Y * 100.0f;
_bottomInputBox.Value = color.Z * 100.0f;
break;
}
_alphaSlider.Value = Color.A;
_alphaInputBox.Value = Color.A * 100.0f;
}
private bool IsSpinBoxValid(float value, ColorSliderOrder ordering)
{
if (value < 0)
{
return false;
}
if (ordering == ColorSliderOrder.Alpha)
{
return value <= 100.0f;
}
switch (SelectorType)
{
case ColorSelectorType.Rgb:
return value <= byte.MaxValue;
case ColorSelectorType.Hsv:
switch (ordering)
{
case ColorSliderOrder.Top:
return value <= 360.0f;
default:
return value <= 100.0f;
}
}
return false;
}
private (string, string, string) GetSliderLabels()
{
switch (SelectorType)
{
case ColorSelectorType.Rgb:
return (
Loc.GetString("color-selector-sliders-red"),
Loc.GetString("color-selector-sliders-green"),
Loc.GetString("color-selector-sliders-blue")
);
case ColorSelectorType.Hsv:
return (
Loc.GetString("color-selector-sliders-hue"),
Loc.GetString("color-selector-sliders-saturation"),
Loc.GetString("color-selector-sliders-value")
);
}
return ("ERR", "ERR", "ERR");
}
private float GetColorValueDivisor(ColorSliderOrder order)
{
if (order == ColorSliderOrder.Alpha)
{
return 100.0f;
}
switch (SelectorType)
{
case ColorSelectorType.Rgb:
return 255.0f;
case ColorSelectorType.Hsv:
switch (order)
{
case ColorSliderOrder.Top:
return 360.0f;
default:
return 100.0f;
}
}
return 0.0f;
}
private void OnColorSet()
{
// stack overflow otherwise due to value sets
if (_updating)
{
return;
}
switch (SelectorType)
{
case ColorSelectorType.Rgb:
Color rgbColor = new Color(_topColorSlider.Value, _middleColorSlider.Value, _bottomColorSlider.Value, _alphaSlider.Value);
_currentColor = rgbColor;
Update();
OnColorChanged!(rgbColor);
break;
case ColorSelectorType.Hsv:
Color hsvColor = Color.FromHsv(new Vector4(_topColorSlider.Value, _middleColorSlider.Value, _bottomColorSlider.Value, _alphaSlider.Value));
_currentColor = hsvColor;
Update();
OnColorChanged!(hsvColor);
break;
}
}
private enum ColorSliderOrder
{
Top,
Middle,
Bottom,
Alpha
}
public enum ColorSelectorType
{
Rgb,
Hsv,
}
}

View File

@@ -0,0 +1,81 @@
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface.Controls;
public sealed class ColorableSlider : Slider
{
public const string StylePropertyFillWhite = "fillWhite"; // needs to be filled with white
public const string StylePropertyBackgroundWhite = "backgroundWhite"; // also needs to be filled with white
public Color Color { get; private set; } = Color.White;
public PartSelector Part
{
get => _currentPartSelector;
set
{
_currentPartSelector = value;
UpdateStyleBoxes();
}
}
private PartSelector _currentPartSelector = PartSelector.Background;
public void SetColor(Color color)
{
Color = color;
switch (Part)
{
case PartSelector.Fill:
_fillPanel.Modulate = Color;
break;
case PartSelector.Background:
_backgroundPanel.Modulate = Color;
break;
}
}
protected override void UpdateStyleBoxes()
{
StyleBox? GetStyleBox(string name)
{
if (TryGetStyleProperty<StyleBox>(name, out var box))
{
return box;
}
return null;
}
string backBox = StylePropertyBackground;
string fillBox = StylePropertyFill;
switch (Part)
{
case PartSelector.Fill:
fillBox = StylePropertyFillWhite;
_fillPanel.Modulate = Color;
break;
case PartSelector.Background:
backBox = StylePropertyBackgroundWhite;
_fillPanel.Modulate = Color.Transparent; // make this transparent
_backgroundPanel.Modulate = Color;
break;
}
_backgroundPanel.PanelOverride = BackgroundStyleBoxOverride ?? GetStyleBox(backBox);
_foregroundPanel.PanelOverride = ForegroundStyleBoxOverride ?? GetStyleBox(StylePropertyForeground);
_fillPanel.PanelOverride = FillStyleBoxOverride ?? GetStyleBox(fillBox);
_grabber.PanelOverride = GrabberStyleBoxOverride ?? GetStyleBox(StylePropertyGrabber);
}
public enum PartSelector
{
Fill,
Background
}
}

View File

@@ -65,18 +65,22 @@ namespace Robust.Client.UserInterface.Controls
get => _text;
set
{
if (value == null)
{
value = "";
}
// Save cursor position or -1 for end
var cursorTarget = CursorPosition == Text.Length ? -1 : CursorPosition;
if (!SetText(value))
if (!InternalSetText(value))
{
return;
}
_cursorPosition = 0;
_selectionStart = 0;
var clamped = MathHelper.Clamp(cursorTarget == -1 ? _text.Length : cursorTarget, 0, _text.Length);
while (clamped < _text.Length && !Rune.TryGetRuneAt(_text, clamped, out _))
{
clamped++;
}
_cursorPosition = clamped;
_selectionStart = _cursorPosition;
_updatePseudoClass();
}
}
@@ -203,7 +207,7 @@ namespace Robust.Client.UserInterface.Controls
var lower = SelectionLower;
var newContents = Text[..lower] + text + Text[SelectionUpper..];
if (!SetText(newContents))
if (!InternalSetText(newContents))
{
return;
}
@@ -216,7 +220,7 @@ namespace Robust.Client.UserInterface.Controls
/// <remarks>
/// Does not fix cursor positions, those will have to be adjusted manually.
/// </remarks>>
protected bool SetText(string newText)
private bool InternalSetText(string newText)
{
if (IsValid != null && !IsValid(newText))
{

View File

@@ -13,10 +13,10 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertyFill = "fill";
public const string StylePropertyGrabber = "grabber";
private readonly PanelContainer _foregroundPanel;
private readonly PanelContainer _backgroundPanel;
private readonly PanelContainer _fillPanel;
private readonly PanelContainer _grabber;
protected readonly PanelContainer _foregroundPanel;
protected readonly PanelContainer _backgroundPanel;
protected readonly PanelContainer _fillPanel;
protected readonly PanelContainer _grabber;
private bool _grabbed;
@@ -169,7 +169,7 @@ namespace Robust.Client.UserInterface.Controls
UpdateStyleBoxes();
}
private void UpdateStyleBoxes()
protected virtual void UpdateStyleBoxes()
{
StyleBox? GetStyleBox(string name)
{
@@ -182,7 +182,7 @@ namespace Robust.Client.UserInterface.Controls
}
_backgroundPanel.PanelOverride = BackgroundStyleBoxOverride ?? GetStyleBox(StylePropertyBackground);
_foregroundPanel.PanelOverride = BackgroundStyleBoxOverride ?? GetStyleBox(StylePropertyForeground);
_foregroundPanel.PanelOverride = ForegroundStyleBoxOverride ?? GetStyleBox(StylePropertyForeground);
_fillPanel.PanelOverride = FillStyleBoxOverride ?? GetStyleBox(StylePropertyFill);
_grabber.PanelOverride = GrabberStyleBoxOverride ?? GetStyleBox(StylePropertyGrabber);
}

View File

@@ -73,7 +73,7 @@ namespace Robust.Client.UserInterface.CustomControls
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
tile = new TileRef(mouseWorldMap.MapId, GridId.Invalid,
tile = new TileRef(GridId.Invalid,
mouseGridPos.ToVector2i(_entityManager, _mapManager), Tile.Empty);
}

View File

@@ -228,7 +228,7 @@ namespace Robust.Client.UserInterface.CustomControls
foreach (var prototype in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
if (prototype.Abstract)
if (prototype.NoSpawn || prototype.Abstract)
{
continue;
}

View File

@@ -142,7 +142,7 @@ namespace Robust.Client.ViewVariables.Instances
{
ViewVariablesTraitMembers.CreateMemberGroupHeader(
ref first,
TypeAbbreviation.Abbreviate(group.Key),
PrettyPrint.PrintUserFacingTypeShort(group.Key, 2),
clientVBox);
foreach (var control in group)
@@ -206,7 +206,7 @@ namespace Robust.Client.ViewVariables.Instances
foreach (var component in componentList)
{
var button = new Button {Text = TypeAbbreviation.Abbreviate(component.GetType()), TextAlign = Label.AlignMode.Left};
var button = new Button {Text = PrettyPrint.PrintUserFacingTypeShort(component.GetType(), 2), TextAlign = Label.AlignMode.Left};
var removeButton = new TextureButton()
{
StyleClasses = { DefaultWindow.StyleClassWindowCloseButton },

View File

@@ -45,7 +45,7 @@ namespace Robust.Client.ViewVariables.Traits
{
CreateMemberGroupHeader(
ref first,
TypeAbbreviation.Abbreviate(group.Key),
PrettyPrint.PrintUserFacingTypeShort(group.Key, 2),
_memberList);
foreach (var control in group)

View File

@@ -98,7 +98,7 @@ namespace Robust.Client.ViewVariables
Editable = access == VVAccess.ReadWrite,
Name = memberInfo.Name,
Type = memberType.AssemblyQualifiedName,
TypePretty = TypeAbbreviation.Abbreviate(memberType),
TypePretty = PrettyPrint.PrintUserFacingTypeShort(memberType, 2),
Value = value
};

View File

@@ -277,7 +277,7 @@ namespace Robust.Client.ViewVariables
public Task<ViewVariablesRemoteSession> RequestSession(ViewVariablesObjectSelector selector)
{
var msg = _netManager.CreateNetMessage<MsgViewVariablesReqSession>();
var msg = new MsgViewVariablesReqSession();
msg.Selector = selector;
msg.RequestId = _nextReqId++;
_netManager.ClientSendMessage(msg);
@@ -293,7 +293,7 @@ namespace Robust.Client.ViewVariables
throw new ArgumentException("Session is closed", nameof(session));
}
var msg = _netManager.CreateNetMessage<MsgViewVariablesReqData>();
var msg = new MsgViewVariablesReqData();
var reqId = msg.RequestId = _nextReqId++;
msg.RequestMeta = meta;
msg.SessionId = session.SessionId;
@@ -315,7 +315,7 @@ namespace Robust.Client.ViewVariables
throw new ArgumentException();
}
var closeMsg = _netManager.CreateNetMessage<MsgViewVariablesCloseSession>();
var closeMsg = new MsgViewVariablesCloseSession();
closeMsg.SessionId = session.SessionId;
_netManager.ClientSendMessage(closeMsg);
}
@@ -332,7 +332,7 @@ namespace Robust.Client.ViewVariables
throw new ArgumentException();
}
var msg = _netManager.CreateNetMessage<MsgViewVariablesModifyRemote>();
var msg = new MsgViewVariablesModifyRemote();
msg.SessionId = session.SessionId;
msg.ReinterpretValue = reinterpretValue;
msg.PropertyIndex = propertyIndex;

View File

@@ -31,6 +31,7 @@ using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Serilog.Debugging;
@@ -87,6 +88,7 @@ namespace Robust.Server
[Dependency] private readonly ILocalizationManagerInternal _loc = default!;
[Dependency] private readonly INetConfigurationManager _netCfgMan = default!;
[Dependency] private readonly IServerConsoleHost _consoleHost = default!;
[Dependency] private readonly IParallelManagerInternal _parallelMgr = default!;
private readonly Stopwatch _uptimeStopwatch = new();
@@ -184,6 +186,8 @@ namespace Robust.Server
ProfileOptSetup.Setup(_config);
_parallelMgr.Initialize();
//Sets up Logging
_logHandlerFactory = logHandlerFactory;
@@ -338,7 +342,7 @@ namespace Robust.Server
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.Initialize();
prototypeManager.LoadDirectory(Options.PrototypeDirectory);
prototypeManager.Resync();
prototypeManager.ResolveResults();
_consoleHost.Initialize();
_entityManager.Startup();

View File

@@ -312,15 +312,20 @@ namespace Robust.Server.Bql
{
var radius = (float)(double)arguments[0];
var entityLookup = EntitySystem.Get<EntityLookupSystem>();
var xformQuery = IoCManager.Resolve<IEntityManager>().GetEntityQuery<TransformComponent>();
var distinct = new HashSet<EntityUid>();
foreach (var uid in input)
{
foreach (var near in entityLookup.GetEntitiesInRange(xformQuery.GetComponent(uid).Coordinates,
radius))
{
if (!distinct.Add(near)) continue;
yield return near;
}
}
// TODO: Make this a foreach and reduce LINQ chain because it'll allocate a LOT
//BUG: GetEntitiesInRange effectively uses manhattan distance. This is not intended, near is supposed to be circular.
return input.Where(entityManager.HasComponent<TransformComponent>)
.SelectMany(e =>
entityLookup.GetEntitiesInRange(entityManager.GetComponent<TransformComponent>(e).Coordinates,
radius))
.Select(x => x) // Sloth's fault.
.Distinct();
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
@@ -114,17 +115,19 @@ namespace Robust.Server.Console.Commands
{
public string Command => "loadbp";
public string Description => "Loads a blueprint from disk into the game.";
public string Help => "loadbp <MapID> <Path> [storeUids]";
public string Help => "loadbp <MapID> <Path> [storeUids] [x y] [rotation]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
if (args.Length != 2 && args.Length != 3 && args.Length != 5 && args.Length != 6)
{
shell.WriteError("Must have either 2, 3, 5, or 6 arguments.");
return;
}
if (!int.TryParse(args[0], out var intMapId))
{
shell.WriteError($"{args[0]} is not a valid integer.");
return;
}
@@ -147,7 +150,41 @@ namespace Robust.Server.Console.Commands
var loadOptions = new MapLoadOptions();
if (args.Length > 2)
{
loadOptions.StoreMapUids = bool.Parse(args[2]);
if (!Boolean.TryParse(args[2], out var storeUids))
{
shell.WriteError($"{args[2]} is not a valid boolean..");
return;
}
loadOptions.StoreMapUids = storeUids;
}
if (args.Length >= 5)
{
if (!int.TryParse(args[3], out var x))
{
shell.WriteError($"{args[3]} is not a valid integer.");
return;
}
if (!int.TryParse(args[4], out var y))
{
shell.WriteError($"{args[4]} is not a valid integer.");
return;
}
loadOptions.Offset = new Vector2(x, y);
}
if (args.Length == 6)
{
if (!float.TryParse(args[5], out var rotation))
{
shell.WriteError($"{args[5]} is not a valid integer.");
return;
}
loadOptions.Rotation = new Angle(rotation);
}
var mapLoader = IoCManager.Resolve<IMapLoader>();
@@ -191,15 +228,20 @@ namespace Robust.Server.Console.Commands
{
public string Command => "loadmap";
public string Description => "Loads a map from disk into the game.";
public string Help => "loadmap <MapID> <Path>";
public string Help => "loadmap <MapID> <Path> [x y] [rotation]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 1)
return;
if (args.Length != 2 && args.Length != 4 && args.Length != 5)
{
shell.WriteError($"Must have either 2, 4, or 5 arguments.");
}
if (!int.TryParse(args[0], out var intMapId))
{
shell.WriteError($"{args[0]} is not a valid integer.");
return;
}
var mapId = new MapId(intMapId);
@@ -217,7 +259,36 @@ namespace Robust.Server.Console.Commands
return;
}
IoCManager.Resolve<IMapLoader>().LoadMap(mapId, args[1]);
var loadOptions = new MapLoadOptions();
if (args.Length >= 3)
{
if (!int.TryParse(args[2], out var x))
{
shell.WriteError($"{args[2]} is not a valid integer.");
return;
}
if (!int.TryParse(args[3], out var y))
{
shell.WriteError($"{args[3]} is not a valid integer.");
return;
}
loadOptions.Offset = new Vector2(x, y);
}
if (args.Length == 4)
{
if (!float.TryParse(args[4], out var rotation))
{
shell.WriteError($"{args[4]} is not a valid integer.");
return;
}
loadOptions.Rotation = new Angle(rotation);
}
IoCManager.Resolve<IMapLoader>().LoadMap(mapId, args[1], loadOptions);
if (mapManager.MapExists(mapId))
shell.WriteLine($"Map {mapId} has been loaded from {args[1]}.");

View File

@@ -33,7 +33,7 @@ namespace Robust.Server.Console
if (!NetManager.IsConnected || session is null)
return;
var msg = NetManager.CreateNetMessage<MsgConCmd>();
var msg = new MsgConCmd();
msg.Text = command;
NetManager.ServerSendMessage(msg, ((IPlayerSession)session).ConnectedClient);
}
@@ -121,7 +121,7 @@ namespace Robust.Server.Console
private void HandleRegistrationRequest(INetChannel senderConnection)
{
var netMgr = IoCManager.Resolve<IServerNetManager>();
var message = netMgr.CreateNetMessage<MsgConCmdReg>();
var message = new MsgConCmdReg();
var counter = 0;
message.Commands = new MsgConCmdReg.Command[RegisteredCommands.Count];
@@ -154,7 +154,7 @@ namespace Robust.Server.Console
{
if (session != null)
{
var replyMsg = NetManager.CreateNetMessage<MsgConCmdAck>();
var replyMsg = new MsgConCmdAck();
replyMsg.Error = error;
replyMsg.Text = text;
NetManager.ServerSendMessage(replyMsg, session.ConnectedClient);

View File

@@ -3,7 +3,7 @@ using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
using ManagedHttpListener;
using SpaceWizards.HttpListener;
using Prometheus;
using Robust.Shared.Log;

View File

@@ -184,6 +184,7 @@ namespace Robust.Server.GameObjects
}
_subscribedSessions.Add(session);
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner.Owner, new BoundUIOpenedEvent(UiKey, Owner.Owner, session));
SendMessage(new OpenBoundInterfaceMessage(), session);
if (_lastState != null)
{
@@ -236,11 +237,11 @@ namespace Robust.Server.GameObjects
public void CloseShared(IPlayerSession session)
{
var owner = Owner.Owner;
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(owner, new BoundUIClosedEvent(UiKey, owner, session));
OnClosed?.Invoke(session);
_subscribedSessions.Remove(session);
_playerStateOverrides.Remove(session);
session.PlayerStatusChanged -= OnSessionOnPlayerStatusChanged;
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(owner, new BoundUIClosedEvent(UiKey, owner, session));
if (_subscribedSessions.Count == 0)
{

View File

@@ -206,7 +206,7 @@ namespace Robust.Server.GameObjects
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message)
{
var newMsg = _networkManager.CreateNetMessage<MsgEntity>();
var newMsg = new MsgEntity();
newMsg.Type = EntityMessageType.SystemMessage;
newMsg.SystemMessage = message;
newMsg.SourceTick = _gameTiming.CurTick;
@@ -217,7 +217,7 @@ namespace Robust.Server.GameObjects
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel targetConnection)
{
var newMsg = _networkManager.CreateNetMessage<MsgEntity>();
var newMsg = new MsgEntity();
newMsg.Type = EntityMessageType.SystemMessage;
newMsg.SystemMessage = message;
newMsg.SourceTick = _gameTiming.CurTick;

View File

@@ -0,0 +1,41 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
namespace Robust.Server.GameStates;
/// <summary>
/// Placeholder system to expose some parts of the internal <see cref="PVSSystem"/> that allows entities to ignore
/// normal PVS rules, such that they are always sent to clients.
/// </summary>
public sealed partial class PVSOverrideSystem : EntitySystem
{
[Shared.IoC.Dependency] private readonly PVSSystem _pvs = default!;
/// <summary>
/// Used to ensure that an entity is always sent to every client. Overrides any client-specific overrides.
/// </summary>
public void AddGlobalOverride(EntityUid uid)
{
_pvs.EntityPVSCollection.UpdateIndex(uid, true);
}
/// <summary>
/// Used to ensure that an entity is always sent to a specific client. Overrides any global or pre-existing
/// client-specific overrides.
/// </summary>
public void AddSessionOverride(EntityUid uid, ICommonSession session)
{
_pvs.EntityPVSCollection.UpdateIndex(uid, session, true);
}
/// <summary>
/// Removes any global or client-specific overrides.
/// </summary>
public void ClearOverride(EntityUid uid, TransformComponent? xform = null)
{
if (!Resolve(uid, ref xform))
return;
_pvs.EntityPVSCollection.UpdateIndex(uid, xform.Coordinates, true);
}
}

View File

@@ -10,6 +10,7 @@ using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
@@ -83,11 +84,11 @@ internal sealed partial class PVSSystem : EntitySystem
private readonly ObjectPool<Dictionary<MapChunkLocation, int>> _mapChunkPool =
new DefaultObjectPool<Dictionary<MapChunkLocation, int>>(
new ChunkPoolPolicy<MapChunkLocation>(), 256);
new ChunkPoolPolicy<MapChunkLocation>(), MaxVisPoolSize);
private readonly ObjectPool<Dictionary<GridChunkLocation, int>> _gridChunkPool =
new DefaultObjectPool<Dictionary<GridChunkLocation, int>>(
new ChunkPoolPolicy<GridChunkLocation>(), 256);
new ChunkPoolPolicy<GridChunkLocation>(), MaxVisPoolSize);
private readonly Dictionary<uint, Dictionary<MapChunkLocation, int>> _mapIndices = new(4);
private readonly Dictionary<uint, Dictionary<GridChunkLocation, int>> _gridIndices = new(4);
@@ -220,7 +221,7 @@ internal sealed partial class PVSSystem : EntitySystem
private void OnEntityMove(ref MoveEvent ev)
{
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var coordinates = _transform.GetMoverCoordinates(ev.Component);
UpdateEntityRecursive(ev.Sender, ev.Component, coordinates, xformQuery, false);
}
@@ -228,7 +229,7 @@ internal sealed partial class PVSSystem : EntitySystem
private void OnTransformStartup(EntityUid uid, TransformComponent component, ComponentStartup args)
{
// use Startup because GridId is not set during the eventbus init yet!
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var coordinates = _transform.GetMoverCoordinates(component);
UpdateEntityRecursive(uid, component, coordinates, xformQuery, false);
}
@@ -352,6 +353,8 @@ internal sealed partial class PVSSystem : EntitySystem
{
var (viewPos, range, mapId) = CalcViewBounds(in eyeEuid, transformQuery);
if(mapId == MapId.Nullspace) continue;
uint visMask = EyeComponent.DefaultVisibilityMask;
if (eyeQuery.TryGetComponent(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
@@ -544,7 +547,7 @@ internal sealed partial class PVSSystem : EntitySystem
!AddToChunkSetRecursively(in parent, visMask, tree, set, transform, metadata)) //did we just fail to add the parent?
return false; //we failed? suppose we dont get added either
//todo paul i want it to crash here if it gets added double bc that shouldnt happen and will add alot of unneeded cycles, make this a simpl assignment at some point maybe idk
//i want it to crash here if it gets added double bc that shouldnt happen and will add alot of unneeded cycles
tree.Set(uid, parent);
set.Add(uid, mComp);
return true;
@@ -601,6 +604,14 @@ internal sealed partial class PVSSystem : EntitySystem
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget, in newEntityBudget);
}
var expandEvent = new ExpandPvsEvent(session, new List<EntityUid>());
RaiseLocalEvent(ref expandEvent);
foreach (var entityUid in expandEvent.Entities)
{
RecursivelyAddOverride(in entityUid, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget, in newEntityBudget);
}
var entityStates = new List<EntityState>();
foreach (var (entityUid, visiblity) in visibleEnts)
@@ -629,7 +640,7 @@ internal sealed partial class PVSSystem : EntitySystem
entityStates.Add(new EntityState(entityUid, new NetListAsArray<ComponentChange>(new []
{
ComponentChange.Changed(_stateManager.TransformNetId, new TransformComponent.TransformComponentState(Vector2.Zero, Angle.Zero, EntityUid.Invalid, false, false)),
ComponentChange.Changed(_stateManager.TransformNetId, new TransformComponentState(Vector2.Zero, Angle.Zero, EntityUid.Invalid, false, false)),
}), true));
}

View File

@@ -1,15 +1,14 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -18,8 +17,10 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SharpZstd.Interop;
namespace Robust.Server.GameStates
{
@@ -40,9 +41,13 @@ namespace Robust.Server.GameStates
[Dependency] private readonly INetworkedMapManager _mapManager = default!;
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IServerEntityNetworkManager _entityNetworkManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IParallelManager _parallelMgr = default!;
private ISawmill _logger = default!;
private PvsThreadResources[] _threadResourcesPool = Array.Empty<PvsThreadResources>();
public ushort TransformNetId { get; set; }
public void PostInject()
@@ -60,6 +65,43 @@ namespace Robust.Server.GameStates
_networkManager.Disconnect += HandleClientDisconnect;
_pvs = EntitySystem.Get<PVSSystem>();
_parallelMgr.AddAndInvokeParallelCountChanged(ParallelChanged);
_cfg.OnValueChanged(CVars.NetPVSCompressLevel, _ => UpdateZStdParams(), true);
}
private void ParallelChanged()
{
foreach (var resource in _threadResourcesPool)
{
resource.CompressionContext.Dispose();
}
_threadResourcesPool = new PvsThreadResources[_parallelMgr.ParallelProcessCount];
for (var i = 0; i < _threadResourcesPool.Length; i++)
{
ref var res = ref _threadResourcesPool[i];
res.CompressionContext = new ZStdCompressionContext();
}
UpdateZStdParams();
}
private void UpdateZStdParams()
{
var compressionLevel = _cfg.GetCVar(CVars.NetPVSCompressLevel);
for (var i = 0; i < _threadResourcesPool.Length; i++)
{
ref var res = ref _threadResourcesPool[i];
res.CompressionContext.SetParameter(ZSTD_cParameter.ZSTD_c_compressionLevel, compressionLevel);
}
}
private struct PvsThreadResources
{
public ZStdCompressionContext CompressionContext;
}
private void HandleClientConnected(object? sender, NetChannelArgs e)
@@ -142,7 +184,7 @@ namespace Robust.Server.GameStates
(chunks, playerChunks, viewerEntities) = _pvs.GetChunks(players);
const int ChunkBatchSize = 2;
var chunksCount = chunks.Count;
var chunkBatches = (int) MathF.Ceiling((float) chunksCount / ChunkBatchSize);
var chunkBatches = (int)MathF.Ceiling((float)chunksCount / ChunkBatchSize);
chunkCache =
new (Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[chunksCount];
@@ -159,7 +201,8 @@ namespace Robust.Server.GameStates
for (var j = start; j < end; ++j)
{
var (visMask, chunkIndexLocation) = chunks[j];
reuse[j] = _pvs.TryCalculateChunk(chunkIndexLocation, visMask, transformQuery, metadataQuery, out var chunk);
reuse[j] = _pvs.TryCalculateChunk(chunkIndexLocation, visMask, transformQuery, metadataQuery,
out var chunk);
chunkCache[j] = chunk;
}
});
@@ -168,33 +211,27 @@ namespace Robust.Server.GameStates
ArrayPool<bool>.Shared.Return(reuse);
}
const int BatchSize = 2;
var batches = (int) MathF.Ceiling((float) players.Length / BatchSize);
Parallel.For(0, batches, i =>
{
var start = i * BatchSize;
var end = Math.Min(start + BatchSize, players.Length);
for (var j = start; j < end; ++j)
_parallelMgr.ParallelForWithResources(
0, players.Length,
_threadResourcesPool,
(int i, ref PvsThreadResources resource) =>
{
try
{
SendStateUpdate(j);
SendStateUpdate(i, ref resource);
}
catch (Exception e) // Catch EVERY exception
{
_logger.Log(LogLevel.Error, e, "Caught exception while generating mail.");
}
}
});
});
void SendStateUpdate(int sessionIndex)
void SendStateUpdate(int sessionIndex, ref PvsThreadResources resources)
{
var session = players[sessionIndex];
// KILL IT WITH FIRE
if(mainThread != Thread.CurrentThread)
if (mainThread != Thread.CurrentThread)
IoCManager.InitThread(new DependencyCollection(parentDeps), true);
var channel = session.ConnectedClient;
@@ -214,13 +251,15 @@ namespace Robust.Server.GameStates
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
var lastInputCommand = inputSystem.GetLastInputCommand(session);
var lastSystemMessage = _entityNetworkManager.GetLastMessageSequence(session);
var state = new GameState(lastAck, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage), entStates, playerStates, deletions, mapData);
var state = new GameState(lastAck, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage),
entStates, playerStates, deletions, mapData);
InterlockedHelper.Min(ref oldestAckValue, lastAck.Value);
// actually send the state
var stateUpdateMessage = _networkManager.CreateNetMessage<MsgState>();
var stateUpdateMessage = new MsgState();
stateUpdateMessage.State = state;
stateUpdateMessage.CompressionContext = resources.CompressionContext;
// If the state is too big we let Lidgren send it reliably.
// This is to avoid a situation where a state is so large that it consistently gets dropped
@@ -238,7 +277,7 @@ namespace Robust.Server.GameStates
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
}
if(_pvs.CullingEnabled)
if (_pvs.CullingEnabled)
_pvs.ReturnToPool(playerChunks);
_pvs.Cleanup(_playerManager.ServerSessions);
var oldestAck = new GameTick(oldestAckValue);

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Server.Maps
{
[TypeSerializer]
internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingDataNode>
{
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
throw new NotImplementedException();
}
public MapChunk Read(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null, MapChunk? chunk = null)
{
var tileNode = (ValueDataNode)node["tiles"];
var tileBytes = Convert.FromBase64String(tileNode.Value);
using var stream = new MemoryStream(tileBytes);
using var reader = new BinaryReader(stream);
var mapManager = dependencies.Resolve<IMapManager>();
mapManager.SuppressOnTileChanged = true;
if (chunk == null)
{
throw new InvalidOperationException(
$"Someone tried deserializing a gridchunk without passing a value.");
}
if (context is not MapLoader.MapContext mapContext)
{
throw new InvalidOperationException(
$"Someone tried serializing a gridchunk without passing {nameof(MapLoader.MapContext)} as context.");
}
var tileMap = mapContext.TileMap;
if (tileMap == null)
{
throw new InvalidOperationException(
$"Someone tried deserializing a gridchunk before deserializing the tileMap.");
}
chunk.SuppressCollisionRegeneration = true;
var tileDefinitionManager = dependencies.Resolve<ITileDefinitionManager>();
for (ushort y = 0; y < chunk.ChunkSize; y++)
{
for (ushort x = 0; x < chunk.ChunkSize; x++)
{
var id = reader.ReadUInt16();
var flags = (TileRenderFlag)reader.ReadByte();
var variant = reader.ReadByte();
var defName = tileMap[id];
id = tileDefinitionManager[defName].TileId;
var tile = new Tile(id, flags, variant);
chunk.SetTile(x, y, tile);
}
}
chunk.SuppressCollisionRegeneration = false;
mapManager.SuppressOnTileChanged = false;
return chunk;
}
public DataNode Write(ISerializationManager serializationManager, MapChunk value, bool alwaysWrite = false,
ISerializationContext? context = null)
{
var root = new MappingDataNode();
var ind = new ValueDataNode($"{value.X},{value.Y}");
root.Add("ind", ind);
var gridNode = new ValueDataNode();
root.Add("tiles", gridNode);
gridNode.Value = SerializeTiles(value);
return root;
}
private static string SerializeTiles(MapChunk chunk)
{
// number of bytes written per tile, because sizeof(Tile) is useless.
const int structSize = 4;
var nTiles = chunk.ChunkSize * chunk.ChunkSize * structSize;
var barr = new byte[nTiles];
using (var stream = new MemoryStream(barr))
using (var writer = new BinaryWriter(stream))
{
for (ushort y = 0; y < chunk.ChunkSize; y++)
{
for (ushort x = 0; x < chunk.ChunkSize; x++)
{
var tile = chunk.GetTile(x, y);
writer.Write(tile.TypeId);
writer.Write((byte)tile.Flags);
writer.Write(tile.Variant);
}
}
}
return Convert.ToBase64String(barr);
}
public MapChunk Copy(ISerializationManager serializationManager, MapChunk source, MapChunk target, bool skipHook,
ISerializationContext? context = null)
{
throw new NotImplementedException();
}
}
//todo paul make this be used
[TypeSerializer]
internal sealed class GridSerializer : ITypeSerializer<MapGrid, MappingDataNode>
{
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
throw new NotImplementedException();
}
public MapGrid Read(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null, MapGrid? grid = null)
{
var info = node.Get<MappingDataNode>("settings");
var chunks = node.Get<SequenceDataNode>("chunks");
ushort csz = 0;
ushort tsz = 0;
float sgsz = 0.0f;
foreach (var kvInfo in info.Cast<KeyValuePair<ValueDataNode, ValueDataNode>>())
{
var key = kvInfo.Key.Value;
var val = kvInfo.Value.Value;
if (key == "chunksize")
csz = ushort.Parse(val);
else if (key == "tilesize")
tsz = ushort.Parse(val);
else if (key == "snapsize")
sgsz = float.Parse(val, CultureInfo.InvariantCulture);
}
//TODO: Pass in options
if (context is not MapLoader.MapContext mapContext)
{
throw new InvalidOperationException(
$"Someone tried serializing a mapgrid without passing {nameof(MapLoader.MapContext)} as context.");
}
if (grid == null) throw new NotImplementedException();
//todo paul grid ??= dependencies.Resolve<MapManager>().CreateUnboundGrid(mapContext.TargetMap);
foreach (var chunkNode in chunks.Cast<MappingDataNode>())
{
var (chunkOffsetX, chunkOffsetY) =
serializationManager.Read<Vector2i>(chunkNode["ind"], context, skipHook);
var chunk = grid.GetChunk(chunkOffsetX, chunkOffsetY);
serializationManager.Read(typeof(MapChunkSerializer), chunkNode, context, skipHook, chunk);
}
return grid;
}
public DataNode Write(ISerializationManager serializationManager, MapGrid value, bool alwaysWrite = false,
ISerializationContext? context = null)
{
var gridn = new MappingDataNode();
var info = new MappingDataNode();
var chunkSeq = new SequenceDataNode();
gridn.Add("settings", info);
gridn.Add("chunks", chunkSeq);
info.Add("chunksize", value.ChunkSize.ToString(CultureInfo.InvariantCulture));
info.Add("tilesize", value.TileSize.ToString(CultureInfo.InvariantCulture));
var chunks = value.GetMapChunks();
foreach (var chunk in chunks)
{
var chunkNode = serializationManager.WriteValue(chunk.Value);
chunkSeq.Add(chunkNode);
}
return gridn;
}
public MapGrid Copy(ISerializationManager serializationManager, MapGrid source, MapGrid target, bool skipHook,
ISerializationContext? context = null)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using YamlDotNet.RepresentationModel;
@@ -6,12 +8,12 @@ namespace Robust.Server.Maps
{
public interface IMapLoader
{
IMapGrid? LoadBlueprint(MapId mapId, string path);
IMapGrid? LoadBlueprint(MapId mapId, string path, MapLoadOptions options);
(IReadOnlyList<EntityUid> entities, GridId? gridId) LoadBlueprint(MapId mapId, string path);
(IReadOnlyList<EntityUid> entities, GridId? gridId) LoadBlueprint(MapId mapId, string path, MapLoadOptions options);
void SaveBlueprint(GridId gridId, string yamlPath);
void LoadMap(MapId mapId, string path);
void LoadMap(MapId mapId, string path, MapLoadOptions options);
(IReadOnlyList<EntityUid> entities, IReadOnlyList<GridId> gridIds) LoadMap(MapId mapId, string path);
(IReadOnlyList<EntityUid> entities, IReadOnlyList<GridId> gridIds) LoadMap(MapId mapId, string path, MapLoadOptions options);
void SaveMap(MapId mapId, string yamlPath);
event Action<YamlStream, string> LoadedMapData;

View File

@@ -1,5 +1,9 @@
namespace Robust.Server.Maps
using JetBrains.Annotations;
using Robust.Shared.Maths;
namespace Robust.Server.Maps
{
[PublicAPI]
public sealed class MapLoadOptions
{
/// <summary>
@@ -7,5 +11,38 @@
/// to maintain consistency upon subsequent savings.
/// </summary>
public bool StoreMapUids { get; set; }
/// <summary>
/// Offset to apply to the loaded objects.
/// </summary>
public Vector2 Offset
{
get => _offset;
set
{
TransformMatrix = Matrix3.CreateTransform(value, Rotation);
_offset = value;
}
}
private Vector2 _offset = Vector2.Zero;
/// <summary>
/// Rotation to apply to the loaded objects as a collective, around 0, 0.
/// </summary>
/// <remarks>Setting this overrides <</remarks>
public Angle Rotation
{
get => _rotation;
set
{
TransformMatrix = Matrix3.CreateTransform(Offset, value);
_rotation = value;
}
}
private Angle _rotation = Angle.Zero;
public Matrix3 TransformMatrix { get; set; } = Matrix3.Identity;
}
}

View File

@@ -13,14 +13,15 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
@@ -44,6 +45,7 @@ namespace Robust.Server.Maps
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IServerEntityManagerInternal _serverEntityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
public event Action<YamlStream, string>? LoadedMapData;
@@ -52,7 +54,7 @@ namespace Robust.Server.Maps
{
var grid = _mapManager.GetGrid(gridId);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager, _serializationManager);
context.RegisterGrid(grid);
var root = context.Serialize();
var document = new YamlDocument(root);
@@ -68,7 +70,7 @@ namespace Robust.Server.Maps
}
/// <inheritdoc />
public IMapGrid? LoadBlueprint(MapId mapId, string path)
public (IReadOnlyList<EntityUid> entities, GridId? gridId) LoadBlueprint(MapId mapId, string path)
{
return LoadBlueprint(mapId, path, DefaultLoadOptions);
}
@@ -78,13 +80,14 @@ namespace Robust.Server.Maps
return new ResourcePath(path).ToRootedPath();
}
public IMapGrid? LoadBlueprint(MapId mapId, string path, MapLoadOptions options)
public (IReadOnlyList<EntityUid> entities, GridId? gridId) LoadBlueprint(MapId mapId, string path, MapLoadOptions options)
{
var resPath = Rooted(path);
if (!TryGetReader(resPath, out var reader)) return null;
if (!TryGetReader(resPath, out var reader)) return (Array.Empty<EntityUid>(), null);
IMapGrid grid;
IMapGrid? grid;
IReadOnlyList<EntityUid> entities;
using (reader)
{
Logger.InfoS("map", $"Loading Grid: {resPath}");
@@ -99,14 +102,15 @@ namespace Robust.Server.Maps
}
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager,
_prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
_prototypeManager, _serializationManager, data.RootNode.ToDataNodeCast<MappingDataNode>(), mapId, options);
context.Deserialize();
grid = context.Grids[0];
grid = context.Grids.FirstOrDefault();
entities = context.Entities;
PostDeserialize(mapId, context);
}
return grid;
return (entities, grid?.Index);
}
private void PostDeserialize(MapId mapId, MapContext context)
@@ -139,7 +143,7 @@ namespace Robust.Server.Maps
public void SaveMap(MapId mapId, string yamlPath)
{
Logger.InfoS("map", $"Saving map {mapId} to {yamlPath}");
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager, _serializationManager);
foreach (var grid in _mapManager.GetAllMapGrids(mapId))
{
context.RegisterGrid(grid);
@@ -159,9 +163,9 @@ namespace Robust.Server.Maps
Logger.InfoS("map", "Save completed!");
}
public void LoadMap(MapId mapId, string path)
public (IReadOnlyList<EntityUid> entities, IReadOnlyList<GridId> gridIds) LoadMap(MapId mapId, string path)
{
LoadMap(mapId, path, DefaultLoadOptions);
return LoadMap(mapId, path, DefaultLoadOptions);
}
private bool TryGetReader(ResourcePath resPath, [NotNullWhen(true)] out TextReader? reader)
@@ -191,12 +195,14 @@ namespace Robust.Server.Maps
return true;
}
public void LoadMap(MapId mapId, string path, MapLoadOptions options)
public (IReadOnlyList<EntityUid> entities, IReadOnlyList<GridId> gridIds) LoadMap(MapId mapId, string path, MapLoadOptions options)
{
var resPath = Rooted(path);
if (!TryGetReader(resPath, out var reader)) return;
if (!TryGetReader(resPath, out var reader)) return (Array.Empty<EntityUid>(), Array.Empty<GridId>());
IReadOnlyList<GridId> grids;
IReadOnlyList<EntityUid> entities;
using (reader)
{
Logger.InfoS("map", $"Loading Map: {resPath}");
@@ -206,17 +212,21 @@ namespace Robust.Server.Maps
LoadedMapData?.Invoke(data.Stream, resPath.ToString());
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager,
_prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
_prototypeManager, _serializationManager, data.RootNode.ToDataNodeCast<MappingDataNode>(), mapId, options);
context.Deserialize();
grids = context.Grids.Select(x => x.Index).ToArray(); // TODO: make context use grid IDs.
entities = context.Entities;
PostDeserialize(mapId, context);
}
return (entities, grids);
}
/// <summary>
/// Handles the primary bulk of state during the map serialization process.
/// </summary>
private sealed class MapContext : ISerializationContext, IEntityLoadContext,
internal sealed class MapContext : ISerializationContext, IEntityLoadContext,
ITypeSerializer<GridId, ValueDataNode>,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeReaderWriter<EntityUid, ValueDataNode>
@@ -225,29 +235,32 @@ namespace Robust.Server.Maps
private readonly ITileDefinitionManager _tileDefinitionManager;
private readonly IServerEntityManagerInternal _serverEntityManager;
private readonly IPrototypeManager _prototypeManager;
private readonly ISerializationManager _serializationManager;
private readonly MapLoadOptions? _loadOptions;
private readonly Dictionary<GridId, int> GridIDMap = new();
public readonly List<MapGrid> Grids = new();
private readonly List<GridId> _readGridIndices = new();
private EntityQuery<TransformComponent>? _xformQuery = null;
private readonly Dictionary<EntityUid, int> EntityUidMap = new();
private readonly Dictionary<int, EntityUid> UidEntityMap = new();
public readonly List<EntityUid> Entities = new();
private readonly List<(EntityUid, YamlMappingNode)> _entitiesToDeserialize
private readonly List<(EntityUid, MappingDataNode)> _entitiesToDeserialize
= new();
private bool IsBlueprintMode => GridIDMap.Count == 1;
private readonly YamlMappingNode RootNode;
private readonly MapId TargetMap;
private readonly MappingDataNode RootNode;
public readonly MapId TargetMap;
private Dictionary<string, YamlMappingNode>? CurrentReadingEntityComponents;
private Dictionary<string, MappingDataNode>? CurrentReadingEntityComponents;
private string? CurrentWritingComponent;
private EntityUid? CurrentWritingEntity;
public IReadOnlyDictionary<ushort, string>? TileMap => _tileMap;
private Dictionary<ushort, string>? _tileMap;
public Dictionary<(Type, Type), object> TypeReaders { get; }
@@ -258,14 +271,16 @@ namespace Robust.Server.Maps
public bool MapIsPostInit { get; private set; }
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities, IPrototypeManager prototypeManager)
IServerEntityManagerInternal entities, IPrototypeManager prototypeManager,
ISerializationManager serializationManager)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_prototypeManager = prototypeManager;
_serializationManager = serializationManager;
RootNode = new YamlMappingNode();
RootNode = new MappingDataNode();
TypeWriters = new Dictionary<Type, object>()
{
{typeof(GridId), this},
@@ -281,12 +296,14 @@ namespace Robust.Server.Maps
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities,
IPrototypeManager prototypeManager,
YamlMappingNode node, MapId targetMapId, MapLoadOptions options)
ISerializationManager serializationManager,
MappingDataNode node, MapId targetMapId, MapLoadOptions options)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_loadOptions = options;
_serializationManager = serializationManager;
RootNode = node;
TargetMap = targetMapId;
@@ -339,29 +356,36 @@ namespace Robust.Server.Maps
// We have to fix the created grids up with the grid entities deserialized from the map.
FixMapEntities();
_xformQuery = _serverEntityManager.GetEntityQuery<TransformComponent>();
// We have to attach grids to the target map here.
// If we don't, initialization & startup can fail for some entities.
AttachMapEntities();
ApplyGridFixtures();
AdjustEntityTransforms();
// Run Initialize on all components.
FinishEntitiesInitialization();
// Run Startup on all components.
FinishEntitiesStartup();
// Do this last so any entity transforms are fixed first and that they go to the new grids correctly.
CheckGridSplits();
}
private void VerifyEntitiesExist()
{
var fail = false;
var entities = RootNode.GetNode<YamlSequenceNode>("entities");
var entities = RootNode.Get<SequenceDataNode>("entities");
var reportedError = new HashSet<string>();
foreach (var entityDef in entities.Cast<YamlMappingNode>())
foreach (var entityDef in entities.Cast<MappingDataNode>())
{
if (entityDef.TryGetNode("type", out var typeNode))
if (entityDef.TryGet<ValueDataNode>("type", out var typeNode))
{
var type = typeNode.AsString();
var type = typeNode.Value;
if (!_prototypeManager.HasIndex<EntityPrototype>(type) && !reportedError.Contains(type))
{
Logger.Error("Missing prototype for map: {0}", type);
@@ -384,7 +408,7 @@ namespace Robust.Server.Maps
foreach (var (entity, data) in _entitiesToDeserialize)
{
if (!data.TryGetNode("components", out YamlSequenceNode? componentList))
if (!data.TryGet("components", out SequenceDataNode? componentList))
{
continue;
}
@@ -399,7 +423,7 @@ namespace Robust.Server.Maps
var castComp = (Component) component;
var compName = compFactory.GetComponentName(castComp.GetType());
if (componentList.Any(p => p["type"].AsString() == compName))
if (componentList.Cast<MappingDataNode>().Any(p => ((ValueDataNode)p["type"]).Value == compName))
{
if (prototype.Components.ContainsKey(compName))
{
@@ -438,6 +462,7 @@ namespace Robust.Server.Maps
body.Broadphase = entManager.GetComponent<BroadphaseComponent>(mapUid);
var fixtures = entManager.EnsureComponent<FixturesComponent>(grid.GridEntityId);
// Regenerate grid collision.
gridFixtures.EnsureGrid(grid.GridEntityId);
gridFixtures.ProcessGrid(gridInternal);
// Avoid duplicating the deserialization in FixtureSystem.
fixtures.SerializedFixtures.Clear();
@@ -487,7 +512,7 @@ namespace Robust.Server.Maps
// Now we need to actually bind the MapGrids to their components so that you can resolve GridId -> EntityUid
// After doing this, it should be 100% safe to use the MapManager API like normal.
var yamlGrids = RootNode.GetNode<YamlSequenceNode>("grids");
var yamlGrids = RootNode.Get<SequenceDataNode>("grids");
// get ents that the grids will bind to
var gridComps = new Dictionary<GridId, MapGridComponent>(_readGridIndices.Count);
@@ -511,28 +536,30 @@ namespace Robust.Server.Maps
{
// Here is where the implicit index pairing magic happens from the yaml.
var gridIndex = _readGridIndices[index];
var yamlGrid = (YamlMappingNode)yamlGrids.Children[index];
var yamlGrid = (MappingDataNode)yamlGrids[index];
// designed to throw if something is broken, every grid must map to an ent
var gridComp = gridComps[gridIndex];
DebugTools.Assert(gridComp.GridIndex == gridIndex);
YamlMappingNode yamlGridInfo = (YamlMappingNode)yamlGrid["settings"];
YamlSequenceNode yamlGridChunks = (YamlSequenceNode)yamlGrid["chunks"];
MappingDataNode yamlGridInfo = (MappingDataNode)yamlGrid["settings"];
SequenceDataNode yamlGridChunks = (SequenceDataNode)yamlGrid["chunks"];
var grid = AllocateMapGrid(gridComp, yamlGridInfo);
foreach (var chunkNode in yamlGridChunks.Cast<YamlMappingNode>())
foreach (var chunkNode in yamlGridChunks.Cast<MappingDataNode>())
{
YamlGridSerializer.DeserializeChunk(_mapManager, grid, chunkNode, _tileMap!, _tileDefinitionManager);
var (chunkOffsetX, chunkOffsetY) = _serializationManager.Read<Vector2i>(chunkNode["ind"]);
var chunk = grid.GetChunk(chunkOffsetX, chunkOffsetY);
_serializationManager.Read(chunkNode, this, value: chunk);
}
Grids.Add(grid); // Grids are kept in index order
}
}
private static MapGrid AllocateMapGrid(MapGridComponent gridComp, YamlMappingNode yamlGridInfo)
private static MapGrid AllocateMapGrid(MapGridComponent gridComp, MappingDataNode yamlGridInfo)
{
// sane defaults
ushort csz = 16;
@@ -540,8 +567,8 @@ namespace Robust.Server.Maps
foreach (var kvInfo in yamlGridInfo)
{
var key = kvInfo.Key.ToString();
var val = kvInfo.Value.ToString();
var key = ((ValueDataNode)kvInfo.Key).Value;
var val = ((ValueDataNode)kvInfo.Value).Value;
if (key == "chunksize")
csz = ushort.Parse(val);
else if (key == "tilesize")
@@ -561,7 +588,7 @@ namespace Robust.Server.Maps
foreach (var grid in Grids)
{
var transform = _serverEntityManager.GetComponent<TransformComponent>(grid.GridEntityId);
var transform = _xformQuery!.Value.GetComponent(grid.GridEntityId);
if (transform.Parent != null)
continue;
@@ -590,14 +617,14 @@ namespace Robust.Server.Maps
private void ReadMetaSection()
{
var meta = RootNode.GetNode<YamlMappingNode>("meta");
var ver = meta.GetNode("format").AsInt();
var meta = RootNode.Get<MappingDataNode>("meta");
var ver = meta.Get<ValueDataNode>("format").AsInt();
if (ver != MapFormatVersion)
{
throw new InvalidDataException("Cannot handle this map file version.");
}
if (meta.TryGetNode("postmapinit", out var mapInitNode))
if (meta.TryGet<ValueDataNode>("postmapinit", out var mapInitNode))
{
MapIsPostInit = mapInitNode.AsBool();
}
@@ -612,11 +639,11 @@ namespace Robust.Server.Maps
// Load tile mapping so that we can map the stored tile IDs into the ones actually used at runtime.
_tileMap = new Dictionary<ushort, string>();
var tileMap = RootNode.GetNode<YamlMappingNode>("tilemap");
foreach (var (key, value) in tileMap)
var tileMap = RootNode.Get<MappingDataNode>("tilemap");
foreach (var (key, value) in tileMap.Children)
{
var tileId = (ushort) key.AsInt();
var tileDefName = value.AsString();
var tileId = (ushort) ((ValueDataNode)key).AsInt();
var tileDefName = ((ValueDataNode)value).Value;
_tileMap.Add(tileId, tileDefName);
}
}
@@ -625,9 +652,9 @@ namespace Robust.Server.Maps
{
// sets up the mapping so the serializer can properly deserialize GridIds.
var yamlGrids = RootNode.GetNode<YamlSequenceNode>("grids");
var yamlGrids = RootNode.Get<SequenceDataNode>("grids");
for (var i = 0; i < yamlGrids.Children.Count; i++)
for (var i = 0; i < yamlGrids.Count; i++)
{
_readGridIndices.Add(_mapManager.GenerateGridId(null));
}
@@ -652,17 +679,17 @@ namespace Robust.Server.Maps
private void AllocEntities()
{
var entities = RootNode.GetNode<YamlSequenceNode>("entities");
foreach (var entityDef in entities.Cast<YamlMappingNode>())
var entities = RootNode.Get<SequenceDataNode>("entities");
foreach (var entityDef in entities.Cast<MappingDataNode>())
{
string? type = null;
if (entityDef.TryGetNode("type", out var typeNode))
if (entityDef.TryGet<ValueDataNode>("type", out var typeNode))
{
type = typeNode.AsString();
type = typeNode.Value;
}
var uid = Entities.Count;
if (entityDef.TryGetNode("uid", out var uidNode))
if (entityDef.TryGet<ValueDataNode>("uid", out var uidNode))
{
uid = uidNode.AsInt();
}
@@ -684,15 +711,14 @@ namespace Robust.Server.Maps
{
foreach (var (entity, data) in _entitiesToDeserialize)
{
CurrentReadingEntityComponents = new Dictionary<string, YamlMappingNode>();
if (data.TryGetNode("components", out YamlSequenceNode? componentList))
CurrentReadingEntityComponents = new Dictionary<string, MappingDataNode>();
if (data.TryGet("components", out SequenceDataNode? componentList))
{
foreach (var compData in componentList)
foreach (var compData in componentList.Cast<MappingDataNode>())
{
var copy = new YamlMappingNode(((YamlMappingNode)compData).AsEnumerable());
copy.Children.Remove(new YamlScalarNode("type"));
//TODO Paul: maybe replace mapping with datanode
CurrentReadingEntityComponents[compData["type"].AsString()] = copy;
var datanode = compData.Copy();
datanode.Remove("type");
CurrentReadingEntityComponents[((ValueDataNode)compData["type"]).Value] = datanode;
}
}
@@ -700,6 +726,25 @@ namespace Robust.Server.Maps
}
}
private void AdjustEntityTransforms()
{
var map = _mapManager.GetMapEntityId(TargetMap);
if (_loadOptions is null || _loadOptions.TransformMatrix.EqualsApprox(Matrix3.Identity))
return;
foreach (var entity in Entities)
{
if (!_xformQuery!.Value.TryGetComponent(entity, out var transform) ||
transform.ParentUid != map) continue;
var off = _loadOptions.TransformMatrix.Transform(transform.Coordinates.Position);
transform.Coordinates = transform.Coordinates.WithPosition(off);
transform.WorldRotation += _loadOptions.Rotation;
}
}
private void FinishEntitiesInitialization()
{
foreach (var entity in Entities)
@@ -716,6 +761,16 @@ namespace Robust.Server.Maps
}
}
private void CheckGridSplits()
{
var gridFixtures = _serverEntityManager.EntitySysManager.GetEntitySystem<GridFixtureSystem>();
foreach (var grid in Grids)
{
if (_serverEntityManager.Deleted(grid.GridEntityId)) continue;
gridFixtures.CheckSplits(grid.GridEntityId);
}
}
// Serialization
public void RegisterGrid(IMapGrid grid)
{
@@ -737,12 +792,12 @@ namespace Robust.Server.Maps
PopulateEntityList();
WriteEntitySection();
return RootNode;
return RootNode.ToYaml();
}
private void WriteMetaSection()
{
var meta = new YamlMappingNode();
var meta = new MappingDataNode();
RootNode.Add("meta", meta);
meta.Add("format", MapFormatVersion.ToString(CultureInfo.InvariantCulture));
// TODO: Make these values configurable.
@@ -764,7 +819,7 @@ namespace Robust.Server.Maps
private void WriteTileMapSection()
{
var tileMap = new YamlMappingNode();
var tileMap = new MappingDataNode();
RootNode.Add("tilemap", tileMap);
foreach (var tileDefinition in _tileDefinitionManager)
{
@@ -774,12 +829,12 @@ namespace Robust.Server.Maps
private void WriteGridSection()
{
var grids = new YamlSequenceNode();
var grids = new SequenceDataNode();
RootNode.Add("grids", grids);
foreach (var grid in Grids)
{
var entry = YamlGridSerializer.SerializeGrid(grid);
var entry = _serializationManager.WriteValue(grid, context: this);
grids.Add(entry);
}
}
@@ -843,34 +898,35 @@ namespace Robust.Server.Maps
var serializationManager = IoCManager.Resolve<ISerializationManager>();
var compFactory = IoCManager.Resolve<IComponentFactory>();
var metaQuery = _serverEntityManager.GetEntityQuery<MetaDataComponent>();
var entities = new YamlSequenceNode();
var entities = new SequenceDataNode();
RootNode.Add("entities", entities);
var prototypeCompCache = new Dictionary<string, Dictionary<string, MappingDataNode>>();
foreach (var (saveId, entityUid) in UidEntityMap.OrderBy(e=>e.Key))
{
CurrentWritingEntity = entityUid;
var mapping = new YamlMappingNode
var mapping = new MappingDataNode
{
{"uid", saveId.ToString(CultureInfo.InvariantCulture)}
};
var md = metaQuery.GetComponent(entityUid);
Dictionary<string, MappingDataNode>? cache = null;
if (md.EntityPrototype is {} prototype)
{
mapping.Add("type", prototype.ID);
if (!prototypeCompCache.ContainsKey(prototype.ID))
if (!prototypeCompCache.TryGetValue(prototype.ID, out cache))
{
prototypeCompCache[prototype.ID] = new Dictionary<string, MappingDataNode>();
prototypeCompCache[prototype.ID] = cache = new Dictionary<string, MappingDataNode>();
foreach (var (compType, comp) in prototype.Components)
{
prototypeCompCache[prototype.ID].Add(compType, serializationManager.WriteValueAs<MappingDataNode>(comp.GetType(), comp));
cache.Add(compType, serializationManager.WriteValueAs<MappingDataNode>(comp.GetType(), comp));
}
}
}
var components = new YamlSequenceNode();
var components = new SequenceDataNode();
// See engine#636 for why the Distinct() call.
foreach (var component in _serverEntityManager.GetComponents(entityUid))
@@ -883,8 +939,10 @@ namespace Robust.Server.Maps
CurrentWritingComponent = compName;
var compMapping = serializationManager.WriteValueAs<MappingDataNode>(compType, component, context: this);
if (md.EntityPrototype != null && prototypeCompCache[md.EntityPrototype.ID].TryGetValue(compName, out var protMapping))
if (cache != null && cache.TryGetValue(compName, out var protMapping))
{
// This will NOT recursively call Except() on the values of the mapping. It will only remove
// key-value pairs if both the keys and values are equal.
compMapping = compMapping.Except(protMapping);
if(compMapping == null) continue;
}
@@ -894,11 +952,11 @@ namespace Robust.Server.Maps
{
compMapping.Add("type", new ValueDataNode(compName));
// Something actually got written!
components.Add(compMapping.ToYamlNode());
components.Add(compMapping);
}
}
if (components.Children.Count != 0)
if (components.Count != 0)
{
mapping.Add("components", components);
}
@@ -908,8 +966,8 @@ namespace Robust.Server.Maps
}
// Create custom object serializers that will correctly allow data to be overriden by the map file.
IComponent IEntityLoadContext.GetComponentData(string componentName,
IComponent? protoData)
MappingDataNode IEntityLoadContext.GetComponentData(string componentName,
MappingDataNode? protoData)
{
if (CurrentReadingEntityComponents == null)
{
@@ -919,20 +977,15 @@ namespace Robust.Server.Maps
var serializationManager = IoCManager.Resolve<ISerializationManager>();
var factory = IoCManager.Resolve<IComponentFactory>();
IComponent data = protoData != null
? serializationManager.CreateCopy(protoData, this)!
: (IComponent) Activator.CreateInstance(factory.GetRegistration(componentName).Type)!;
if (CurrentReadingEntityComponents.TryGetValue(componentName, out var mapping))
{
var mapData = (IDeserializedDefinition) serializationManager.Read(
factory.GetRegistration(componentName).Type,
mapping.ToDataNode(), this);
var newData = serializationManager.PopulateDataDefinition(data, mapData);
data = (IComponent) newData.RawValue!;
if (protoData == null) return mapping.Copy();
return serializationManager.PushCompositionWithGenericNode(
factory.GetRegistration(componentName).Type, new[] { protoData }, mapping, this);
}
return data;
return protoData ?? new MappingDataNode();
}
public IEnumerable<string> GetExtraComponentTypes()
@@ -947,10 +1000,10 @@ namespace Robust.Server.Maps
: base(message) { }
}
public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node,
public GridId Read(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context = null)
ISerializationContext? context = null, GridId _ = default)
{
// This is the code that deserializes the Grids index into the GridId. This has to happen between Grid allocation
// and when grids are bound to their entities.
@@ -966,7 +1019,7 @@ namespace Robust.Server.Maps
throw new MapLoadException($"Error in map file: found local grid ID '{val}' which does not exist.");
}
return new DeserializedValue<GridId>(_readGridIndices[val]);
return _readGridIndices[val];
}
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
@@ -1032,15 +1085,15 @@ namespace Robust.Server.Maps
}
}
DeserializationResult ITypeReader<EntityUid, ValueDataNode>.Read(ISerializationManager serializationManager,
EntityUid ITypeReader<EntityUid, ValueDataNode>.Read(ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context)
ISerializationContext? context, EntityUid _)
{
if (node.Value == "null")
{
return new DeserializedValue<EntityUid>(EntityUid.Invalid);
return EntityUid.Invalid;
}
var val = int.Parse(node.Value);
@@ -1048,11 +1101,11 @@ namespace Robust.Server.Maps
if (val >= Entities.Count || !UidEntityMap.ContainsKey(val) || !Entities.TryFirstOrNull(e => e == UidEntityMap[val], out var entity))
{
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val);
return null!;
return EntityUid.Invalid;
}
else
{
return new DeserializedValue<EntityUid>(entity!.Value);
return entity!.Value;
}
}

View File

@@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Robust.Server.Maps
{
internal static class YamlGridSerializer
{
public static YamlMappingNode SerializeGrid(IMapGrid mapGrid)
{
var grid = (IMapGridInternal) mapGrid;
var gridn = new YamlMappingNode();
var info = new YamlMappingNode();
var chunkSeq = new YamlSequenceNode();
gridn.Add("settings", info);
gridn.Add("chunks", chunkSeq);
info.Add("chunksize", grid.ChunkSize.ToString(CultureInfo.InvariantCulture));
info.Add("tilesize", grid.TileSize.ToString(CultureInfo.InvariantCulture));
var chunks = grid.GetMapChunks();
foreach (var chunk in chunks)
{
var chunkNode = SerializeChunk(chunk.Value);
chunkSeq.Add(chunkNode);
}
return gridn;
}
private static YamlNode SerializeChunk(MapChunk chunk)
{
var root = new YamlMappingNode();
var value = new YamlScalarNode($"{chunk.X},{chunk.Y}");
value.Style = ScalarStyle.DoubleQuoted;
root.Add("ind", value);
var gridNode = new YamlScalarNode();
root.Add("tiles", gridNode);
gridNode.Value = SerializeTiles(chunk);
return root;
}
private static string SerializeTiles(MapChunk chunk)
{
// number of bytes written per tile, because sizeof(Tile) is useless.
const int structSize = 4;
var nTiles = chunk.ChunkSize * chunk.ChunkSize * structSize;
var barr = new byte[nTiles];
using (var stream = new MemoryStream(barr))
using (var writer = new BinaryWriter(stream))
{
for (ushort y = 0; y < chunk.ChunkSize; y++)
{
for (ushort x = 0; x < chunk.ChunkSize; x++)
{
var tile = chunk.GetTile(x, y);
writer.Write(tile.TypeId);
writer.Write((byte)tile.Flags);
writer.Write(tile.Variant);
}
}
}
return Convert.ToBase64String(barr);
}
public static void DeserializeChunk(IMapManager mapMan, IMapGridInternal grid,
YamlMappingNode chunkData,
IReadOnlyDictionary<ushort, string> tileDefMapping,
ITileDefinitionManager tileDefinitionManager)
{
var indNode = chunkData["ind"];
var tileNode = chunkData["tiles"];
var (chunkOffsetX, chunkOffsetY) = indNode.AsVector2i();
var tileBytes = Convert.FromBase64String(tileNode.ToString());
using var stream = new MemoryStream(tileBytes);
using var reader = new BinaryReader(stream);
mapMan.SuppressOnTileChanged = true;
var chunk = grid.GetChunk(chunkOffsetX, chunkOffsetY);
chunk.SuppressCollisionRegeneration = true;
for (ushort y = 0; y < grid.ChunkSize; y++)
{
for (ushort x = 0; x < grid.ChunkSize; x++)
{
var id = reader.ReadUInt16();
var flags = (TileRenderFlag)reader.ReadByte();
var variant = reader.ReadByte();
var defName = tileDefMapping[id];
id = tileDefinitionManager[defName].TileId;
var tile = new Tile(id, flags, variant);
chunk.SetTile(x, y, tile);
}
}
chunk.SuppressCollisionRegeneration = false;
mapMan.SuppressOnTileChanged = false;
}
}
}

View File

@@ -1,4 +1,19 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.Physics
{
@@ -7,5 +22,631 @@ namespace Robust.Server.Physics
/// </summary>
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
private ISawmill _logger = default!;
private readonly Dictionary<EntityUid, Dictionary<Vector2i, ChunkNodeGroup>> _nodes = new();
/// <summary>
/// Sessions to receive nodes for debug purposes.
/// </summary>
private readonly HashSet<ICommonSession> _subscribedSessions = new();
/// <summary>
/// Recursion detection to avoid splitting while handling an existing split
/// </summary>
private bool _isSplitting;
private bool _splitAllowed = true;
public override void Initialize()
{
base.Initialize();
_logger = Logger.GetSawmill("gsplit");
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoval);
SubscribeNetworkEvent<RequestGridNodesMessage>(OnDebugRequest);
SubscribeNetworkEvent<StopGridNodesMessage>(OnDebugStopRequest);
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.GridSplitting, SetSplitAllowed, true);
}
private void SetSplitAllowed(bool value) => _splitAllowed = value;
public override void Shutdown()
{
base.Shutdown();
_subscribedSessions.Clear();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CVars.GridSplitting, SetSplitAllowed);
}
/// <summary>
/// Due to how MapLoader works need to ensure grid exists in dictionary before it's initialised.
/// </summary>
internal void EnsureGrid(EntityUid uid)
{
if (!_nodes.ContainsKey(uid))
_nodes[uid] = new Dictionary<Vector2i, ChunkNodeGroup>();
}
private void OnGridInit(GridInitializeEvent ev)
{
EnsureGrid(ev.EntityUid);
}
private void OnGridRemoval(GridRemovalEvent ev)
{
_nodes.Remove(ev.EntityUid);
}
#region Debug
private void OnDebugRequest(RequestGridNodesMessage msg, EntitySessionEventArgs args)
{
var adminManager = IoCManager.Resolve<IConGroupController>();
var pSession = (PlayerSession) args.SenderSession;
if (!adminManager.CanCommand(pSession, ShowGridNodesCommand)) return;
AddDebugSubscriber(args.SenderSession);
}
private void OnDebugStopRequest(StopGridNodesMessage msg, EntitySessionEventArgs args)
{
RemoveDebugSubscriber(args.SenderSession);
}
public bool IsSubscribed(ICommonSession session)
{
return _subscribedSessions.Contains(session);
}
public void AddDebugSubscriber(ICommonSession session)
{
if (!_subscribedSessions.Add(session)) return;
foreach (var (uid, _) in _nodes)
{
SendNodeDebug(uid);
}
}
public void RemoveDebugSubscriber(ICommonSession session)
{
_subscribedSessions.Remove(session);
}
private void SendNodeDebug(EntityUid uid)
{
if (_subscribedSessions.Count == 0) return;
var msg = new ChunkSplitDebugMessage
{
Grid = uid,
};
foreach (var (index, group) in _nodes[uid])
{
var list = new List<List<Vector2i>>();
// To avoid double-sending connections.
var conns = new HashSet<ChunkSplitNode>();
foreach (var node in group.Nodes)
{
conns.Add(node);
list.Add(node.Indices.ToList());
foreach (var neighbor in node.Neighbors)
{
if (conns.Contains(neighbor)) continue;
msg.Connections.Add((
node.GetCentre() + node.Group.Chunk.Indices * node.Group.Chunk.ChunkSize,
neighbor.GetCentre() + neighbor.Group.Chunk.Indices * neighbor.Group.Chunk.ChunkSize));
}
}
msg.Nodes.Add(index, list);
}
foreach (var session in _subscribedSessions)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
}
}
#endregion
/// <summary>
/// Check for any potential splits after maploading is done.
/// </summary>
internal void CheckSplits(EntityUid uid)
{
var nodes = _nodes[uid];
var dirtyNodes = new HashSet<ChunkSplitNode>(nodes.Count);
foreach (var (_, group) in nodes)
{
foreach (var node in group.Nodes)
{
dirtyNodes.Add(node);
}
}
CheckSplits(uid, dirtyNodes);
}
/// <summary>
/// Check for splits on the specified nodes.
/// </summary>
private void CheckSplits(EntityUid uid, HashSet<ChunkSplitNode> dirtyNodes)
{
if (_isSplitting || !_splitAllowed) return;
_isSplitting = true;
var splitFrontier = new Queue<ChunkSplitNode>(4);
var grids = new List<HashSet<ChunkSplitNode>>(1);
// TODO: At this point detect splits.
while (dirtyNodes.Count > 0)
{
var originEnumerator = dirtyNodes.GetEnumerator();
originEnumerator.MoveNext();
var origin = originEnumerator.Current;
originEnumerator.Dispose();
splitFrontier.Enqueue(origin);
var foundSplits = new HashSet<ChunkSplitNode>
{
origin
};
while (splitFrontier.TryDequeue(out var split))
{
dirtyNodes.Remove(split);
foreach (var neighbor in split.Neighbors)
{
if (!foundSplits.Add(neighbor)) continue;
splitFrontier.Enqueue(neighbor);
}
}
grids.Add(foundSplits);
}
var mapGrid = _mapManager.GetGrid(uid);
// Split time
if (grids.Count > 1)
{
var sw = new Stopwatch();
sw.Start();
// We'll leave the biggest group as the original grid
// anything smaller gets split off.
grids.Sort((x, y) =>
x.Sum(o => o.Indices.Count)
.CompareTo(y.Sum(o => o.Indices.Count)));
var xformQuery = GetEntityQuery<TransformComponent>();
var bodyQuery = GetEntityQuery<PhysicsComponent>();
var oldGridXform = xformQuery.GetComponent(mapGrid.GridEntityId);
var (gridPos, gridRot) = oldGridXform.GetWorldPositionRotation(xformQuery);
var mapBody = bodyQuery.GetComponent(mapGrid.GridEntityId);
var oldGridComp = Comp<MapGridComponent>(mapGrid.GridEntityId);
var newGrids = new GridId[grids.Count - 1];
for (var i = 0; i < grids.Count - 1; i++)
{
var group = grids[i];
var splitGrid = _mapManager.CreateGrid(mapGrid.ParentMapId);
newGrids[i] = splitGrid.Index;
// Keep same origin / velocity etc; this makes updating a lot faster and easier.
splitGrid.WorldPosition = gridPos;
splitGrid.WorldRotation = gridRot;
var splitBody = bodyQuery.GetComponent(splitGrid.GridEntityId);
var splitXform = xformQuery.GetComponent(splitGrid.GridEntityId);
splitBody.LinearVelocity = mapBody.LinearVelocity;
splitBody.AngularVelocity = mapBody.AngularVelocity;
var gridComp = Comp<MapGridComponent>(splitGrid.GridEntityId);
var tileData = new List<(Vector2i GridIndices, Tile Tile)>(group.Sum(o => o.Indices.Count));
// Gather all tiles up front and set once to minimise fixture change events
foreach (var node in group)
{
var offset = node.Group.Chunk.Indices * node.Group.Chunk.ChunkSize;
foreach (var index in node.Indices)
{
var tilePos = offset + index;
tileData.Add((tilePos, mapGrid.GetTileRef(tilePos).Tile));
}
}
splitGrid.SetTiles(tileData);
// Set tiles on new grid + update anchored entities
foreach (var node in group)
{
var offset = node.Group.Chunk.Indices * node.Group.Chunk.ChunkSize;
foreach (var tile in node.Indices)
{
var tilePos = offset + tile;
// Access it directly because we're gonna be hammering it and want to keep allocs down.
var snapgrid = node.Group.Chunk.GetSnapGrid((ushort) tile.X, (ushort) tile.Y);
if (snapgrid == null || snapgrid.Count == 0) continue;
for (var j = snapgrid.Count - 1; j >= 0; j--)
{
var ent = snapgrid[j];
var xform = xformQuery.GetComponent(ent);
_xformSystem.ReAnchor(xform, oldGridComp, gridComp, tilePos, oldGridXform, splitXform, xformQuery);
DebugTools.Assert(xform.Anchored);
}
}
// Update lookup ents
// Needs to be done before setting old tiles as they will be re-parented to the map.
// TODO: Combine tiles into larger rectangles or something; this is gonna be the killer bit.
foreach (var tile in node.Indices)
{
var tilePos = offset + tile;
var bounds = _lookup.GetLocalBounds(tilePos, mapGrid.TileSize);
foreach (var ent in _lookup.GetEntitiesIntersecting(mapGrid.Index, tilePos, LookupFlags.None))
{
// Consider centre of entity position maybe?
var entXform = xformQuery.GetComponent(ent);
if (entXform.ParentUid != mapGrid.GridEntityId ||
!bounds.Contains(entXform.LocalPosition)) continue;
entXform.AttachParent(splitXform);
}
}
_nodes[mapGrid.GridEntityId][node.Group.Chunk.Indices].Nodes.Remove(node);
}
for (var j = 0; j < tileData.Count; j++)
{
var (index, _) = tileData[j];
tileData[j] = (index, Tile.Empty);
}
// Set tiles on old grid
mapGrid.SetTiles(tileData);
GenerateSplitNodes((IMapGridInternal) splitGrid);
SendNodeDebug(splitGrid.GridEntityId);
}
// Cull all of the old chunk nodes.
var toRemove = new RemQueue<ChunkNodeGroup>();
foreach (var (_, group) in _nodes[mapGrid.GridEntityId])
{
if (group.Nodes.Count > 0) continue;
toRemove.Add(group);
}
foreach (var group in toRemove)
{
_nodes[mapGrid.GridEntityId].Remove(group.Chunk.Indices);
}
// Allow content to react to the grid being split...
var ev = new GridSplitEvent(newGrids, mapGrid.Index);
RaiseLocalEvent(uid, ref ev);
_logger.Debug($"Split {grids.Count} grids in {sw.Elapsed}");
}
_isSplitting = false;
SendNodeDebug(mapGrid.GridEntityId);
}
private void GenerateSplitNodes(IMapGridInternal grid)
{
foreach (var (_, chunk) in grid.GetMapChunks())
{
var group = CreateNodes(grid.GridEntityId, grid, chunk);
_nodes[grid.GridEntityId].Add(chunk.Indices, group);
}
}
/// <summary>
/// Creates all of the splitting nodes within this chunk; also consider neighbor chunks.
/// </summary>
private ChunkNodeGroup CreateNodes(EntityUid gridEuid, IMapGridInternal grid, MapChunk chunk)
{
var group = new ChunkNodeGroup
{
Chunk = chunk,
};
var tiles = new HashSet<Vector2i>(chunk.ChunkSize * chunk.ChunkSize);
for (var x = 0; x < chunk.ChunkSize; x++)
{
for (var y = 0; y < chunk.ChunkSize; y++)
{
tiles.Add(new Vector2i(x, y));
}
}
var frontier = new Queue<Vector2i>();
var node = new ChunkSplitNode
{
Group = group,
};
// Simple BFS search to get all of the nodes in the chunk.
while (tiles.Count > 0)
{
var originEnumerator = tiles.GetEnumerator();
originEnumerator.MoveNext();
var origin = originEnumerator.Current;
frontier.Enqueue(origin);
originEnumerator.Dispose();
// Just reuse the node if we couldn't use it last time.
// This is in case weh ave 1 chunk with 255 empty tiles and 1 valid tile.
if (node.Indices.Count > 0)
{
node = new ChunkSplitNode
{
Group = group,
};
}
tiles.Remove(origin);
// Check for valid neighbours and add them to the frontier.
while (frontier.TryDequeue(out var index))
{
var tile = chunk.GetTile((ushort) index.X, (ushort) index.Y);
if (tile.IsEmpty) continue;
node.Indices.Add(index);
var enumerator = new NeighborEnumerator(chunk, index);
while (enumerator.MoveNext(out var neighbor))
{
// Already iterated this tile before so just ignore it.
if (!tiles.Remove(neighbor.Value)) continue;
frontier.Enqueue(neighbor.Value);
}
}
if (node.Indices.Count == 0) continue;
group.Nodes.Add(node);
}
// Build neighbors
ChunkSplitNode? neighborNode;
MapChunk? neighborChunk;
// Check each tile for node neighbours on other chunks (not possible for us to have neighbours on the same chunk
// as they would already be in our node).
// TODO: This could be better (maybe only check edges of the chunk or something).
foreach (var chunkNode in group.Nodes)
{
foreach (var index in chunkNode.Indices)
{
// Check for edge tiles.
if (index.X == 0)
{
// Check West
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X - 1, chunk.Indices.Y), out neighborChunk) &&
TryGetNode(gridEuid, neighborChunk, new Vector2i(chunk.ChunkSize - 1, index.Y), out neighborNode))
{
chunkNode.Neighbors.Add(neighborNode);
neighborNode.Neighbors.Add(chunkNode);
}
}
if (index.Y == 0)
{
// Check South
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X, chunk.Indices.Y - 1), out neighborChunk) &&
TryGetNode(gridEuid, neighborChunk, new Vector2i(index.X, chunk.ChunkSize - 1), out neighborNode))
{
chunkNode.Neighbors.Add(neighborNode);
neighborNode.Neighbors.Add(chunkNode);
}
}
if (index.X == chunk.ChunkSize - 1)
{
// Check East
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X + 1, chunk.Indices.Y), out neighborChunk) &&
TryGetNode(gridEuid, neighborChunk, new Vector2i(0, index.Y), out neighborNode))
{
chunkNode.Neighbors.Add(neighborNode);
neighborNode.Neighbors.Add(chunkNode);
}
}
if (index.Y == chunk.ChunkSize - 1)
{
// Check North
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X, chunk.Indices.Y + 1), out neighborChunk) &&
TryGetNode(gridEuid, neighborChunk, new Vector2i(index.X, 0), out neighborNode))
{
chunkNode.Neighbors.Add(neighborNode);
neighborNode.Neighbors.Add(chunkNode);
}
}
}
}
return group;
}
internal override void GenerateSplitNode(EntityUid gridEuid, MapChunk chunk, bool checkSplit = true)
{
if (_isSplitting) return;
var grid = (IMapGridInternal) _mapManager.GetGrid(gridEuid);
var dirtyNodes = new HashSet<ChunkSplitNode>();
Cleanup(gridEuid, chunk, dirtyNodes);
var group = CreateNodes(gridEuid, grid, chunk);
_nodes[grid.GridEntityId][chunk.Indices] = group;
foreach (var chunkNode in group.Nodes)
{
dirtyNodes.Add(chunkNode);
}
if (checkSplit)
CheckSplits(gridEuid, dirtyNodes);
}
/// <summary>
/// Tries to get the relevant split node from a neighbor chunk.
/// </summary>
private bool TryGetNode(EntityUid gridEuid, MapChunk chunk, Vector2i index, [NotNullWhen(true)] out ChunkSplitNode? node)
{
if (!_nodes[gridEuid].TryGetValue(chunk.Indices, out var neighborGroup))
{
node = null;
return false;
}
foreach (var neighborNode in neighborGroup.Nodes)
{
if (!neighborNode.Indices.Contains(index)) continue;
node = neighborNode;
return true;
}
node = null;
return false;
}
private void Cleanup(EntityUid gridEuid, MapChunk chunk, HashSet<ChunkSplitNode> dirtyNodes)
{
if (!_nodes[gridEuid].TryGetValue(chunk.Indices, out var group)) return;
foreach (var node in group.Nodes)
{
// Most important thing is updating our neighbor nodes.
foreach (var neighbor in node.Neighbors)
{
neighbor.Neighbors.Remove(node);
// If neighbor is on a different chunk mark it for checking connections later.
if (neighbor.Group.Equals(group)) continue;
dirtyNodes.Add(neighbor);
}
node.Indices.Clear();
node.Neighbors.Clear();
}
_nodes[gridEuid].Remove(chunk.Indices);
}
private sealed class ChunkNodeGroup
{
internal MapChunk Chunk = default!;
public HashSet<ChunkSplitNode> Nodes = new();
}
private sealed class ChunkSplitNode
{
public ChunkNodeGroup Group = default!;
public HashSet<Vector2i> Indices { get; set; } = new();
public HashSet<ChunkSplitNode> Neighbors { get; set; } = new();
public Vector2 GetCentre()
{
var centre = Vector2.Zero;
foreach (var index in Indices)
{
centre += index;
}
centre /= Indices.Count;
return centre;
}
}
private struct NeighborEnumerator
{
private MapChunk _chunk;
private Vector2i _index;
private int _count = -1;
public NeighborEnumerator(MapChunk chunk, Vector2i index)
{
_chunk = chunk;
_index = index;
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? neighbor)
{
_count++;
// Just go through S E N W
switch (_count)
{
case 0:
if (_index.Y == 0) break;
neighbor = new Vector2i(_index.X, _index.Y - 1);
return true;
case 1:
if (_index.X == _chunk.ChunkSize - 1) break;
neighbor = new Vector2i(_index.X + 1, _index.Y);
return true;
case 2:
if (_index.Y == _chunk.ChunkSize + 1) break;
neighbor = new Vector2i(_index.X, _index.Y + 1);
return true;
case 3:
if (_index.X == 0) break;
neighbor = new Vector2i(_index.X - 1, _index.Y);
return true;
default:
neighbor = null;
return false;
}
return MoveNext(out neighbor);
}
}
}
}
/// <summary>
/// Event raised on a grid that has been split into multiple grids.
/// </summary>
[ByRefEvent]
public readonly struct GridSplitEvent
{
/// <summary>
/// Contains the IDs of the newly created grids.
/// </summary>
public readonly GridId[] NewGrids;
/// <summary>
/// The grid that has been split.
/// </summary>
public readonly GridId Grid;
public GridSplitEvent(GridId[] newGrids, GridId grid)
{
NewGrids = newGrids;
Grid = grid;
}
}

View File

@@ -230,7 +230,7 @@ namespace Robust.Server.Placement
if (playerConnection == null)
return;
var message = _networkManager.CreateNetMessage<MsgPlacement>();
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.StartPlacement;
message.Range = range;
message.IsTile = false;
@@ -251,7 +251,7 @@ namespace Robust.Server.Placement
if (playerConnection == null)
return;
var message = _networkManager.CreateNetMessage<MsgPlacement>();
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.StartPlacement;
message.Range = range;
message.IsTile = true;
@@ -272,7 +272,7 @@ namespace Robust.Server.Placement
if (playerConnection == null)
return;
var message = _networkManager.CreateNetMessage<MsgPlacement>();
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.CancelPlacement;
_networkManager.ServerSendMessage(message, playerConnection);
}

View File

@@ -445,7 +445,7 @@ namespace Robust.Server.Player
{
var channel = message.MsgChannel;
var players = Sessions;
var netMsg = channel.CreateNetMessage<MsgPlayerList>();
var netMsg = new MsgPlayerList();
// client session is complete, set their status accordingly.
// This is done before the packet is built, so that the client

View File

@@ -11,6 +11,7 @@
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
<PackageReference Include="SpaceWizards.HttpListener" Version="0.1.0" />
<!-- -->
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" />
<PackageReference Include="prometheus-net" Version="4.1.1" />
@@ -20,7 +21,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\ManagedHttpListener\src\System.Net.HttpListener.csproj" />
<ProjectReference Include="..\Robust.Physics\Robust.Physics.csproj" />
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />

View File

@@ -75,7 +75,7 @@ namespace Robust.Server.Scripting
private void ReceiveScriptStart(MsgScriptStart message)
{
var reply = _netManager.CreateNetMessage<MsgScriptStartAck>();
var reply = new MsgScriptStartAck();
reply.ScriptSession = message.ScriptSession;
reply.WasAccepted = false;
if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session))
@@ -128,7 +128,7 @@ namespace Robust.Server.Scripting
return;
}
var replyMessage = _netManager.CreateNetMessage<MsgScriptResponse>();
var replyMessage = new MsgScriptResponse();
replyMessage.ScriptSession = message.ScriptSession;
var code = message.Code;
@@ -259,7 +259,7 @@ namespace Robust.Server.Scripting
!instances.TryGetValue(message.ScriptSession, out var instance))
return;
var replyMessage = _netManager.CreateNetMessage<MsgScriptCompletionResponse>();
var replyMessage = new MsgScriptCompletionResponse();
replyMessage.ScriptSession = message.ScriptSession;
// Everything below here cribbed from

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
@@ -11,10 +12,18 @@ namespace Robust.Server.ServerStatus
{
HttpMethod RequestMethod { get; }
IPEndPoint RemoteEndPoint { get; }
/// <summary>
/// Stream that reads the request body data,
/// </summary>
Stream RequestBody { get; }
Uri Url { get; }
bool IsGetLike { get; }
IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
IDictionary<string, string> ResponseHeaders { get; }
bool KeepAlive { get; set; }
[Obsolete("Use async versions instead")]
T? RequestBodyJson<T>();
Task<T?> RequestBodyJsonAsync<T>();
@@ -43,6 +52,8 @@ namespace Robust.Server.ServerStatus
int code = 200,
string contentType = "text/plain");
Task RespondNoContentAsync();
Task RespondAsync(
string text,
HttpStatusCode code = HttpStatusCode.OK,
@@ -72,5 +83,7 @@ namespace Robust.Server.ServerStatus
void RespondJson(object jsonData, HttpStatusCode code = HttpStatusCode.OK);
Task RespondJsonAsync(object jsonData, HttpStatusCode code = HttpStatusCode.OK);
Task<Stream> RespondStreamAsync(HttpStatusCode code = HttpStatusCode.OK);
}
}

View File

@@ -0,0 +1,654 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using Robust.Shared;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
using Robust.Shared.Utility.Collections;
using SharpZstd.Interop;
using SpaceWizards.Sodium;
namespace Robust.Server.ServerStatus
{
// Contains primary logic for ACZ (Automatic Client Zip)
// This entails the following:
// * Automatic generation of client zip on development servers.
// * Loading of pre-built client zip on release servers. ("Hybrid ACZ")
// * Distribution of the above two via status host, to facilitate easier server setup.
// * Manifest-based download system from the above.
internal sealed partial class StatusHost
{
// Lock used while working on the ACZ.
private readonly SemaphoreSlim _aczLock = new(1, 1);
// If an attempt has been made to prepare the ACZ.
private bool _aczPrepareAttempted = false;
// Automatic Client Zip
private AutomaticClientZipInfo? _aczPrepared;
private (string binFolder, string[] assemblies)? _aczInfo;
private void AddAczHandlers()
{
AddHandler(HandleAutomaticClientZip);
AddHandler(HandleAczManifest);
AddHandler(HandleAczManifestDownload);
}
private void InitAcz()
{
_cfg.OnValueChanged(CVars.AczStreamCompress, _ => InvalidateAcz());
_cfg.OnValueChanged(CVars.AczStreamCompressLevel, _ => InvalidateAcz());
_cfg.OnValueChanged(CVars.AczBlobCompress, _ => InvalidateAcz());
_cfg.OnValueChanged(CVars.AczBlobCompressLevel, _ => InvalidateAcz());
_cfg.OnValueChanged(CVars.AczBlobCompressSaveThreshold, _ => InvalidateAcz());
_cfg.OnValueChanged(CVars.AczManifestCompress, _ => InvalidateAcz());
_cfg.OnValueChanged(CVars.AczManifestCompressLevel, _ => InvalidateAcz());
}
private void InvalidateAcz()
{
using var _ = _aczLock.WaitGuard();
if (_aczPrepared == null)
return;
_aczSawmill.Info("ACZ CVars changed, invalidating ACZ data.");
_aczPrepared = null;
_aczPrepareAttempted = false;
}
private async Task<bool> HandleAutomaticClientZip(IStatusHandlerContext context)
{
if (!context.IsGetLike || context.Url!.AbsolutePath != "/client.zip")
{
return false;
}
if (!string.IsNullOrEmpty(_cfg.GetCVar(CVars.BuildDownloadUrl)))
{
await context.RespondAsync("This server has a build download URL.", HttpStatusCode.NotFound);
return true;
}
var result = await PrepareACZ();
if (result == null)
{
await context.RespondAsync("Automatic Client Zip was not preparable.",
HttpStatusCode.InternalServerError);
return true;
}
await context.RespondAsync(result.ZipData, HttpStatusCode.OK, "application/zip");
return true;
}
private async Task<bool> HandleAczManifest(IStatusHandlerContext context)
{
if (!context.IsGetLike || context.Url!.AbsolutePath != "/manifest.txt")
return false;
if (!string.IsNullOrEmpty(_cfg.GetCVar(CVars.BuildManifestUrl)))
{
await context.RespondAsync("This server has a build manifest URL.", HttpStatusCode.NotFound);
return true;
}
var result = await PrepareACZ();
if (result == null)
{
await context.RespondAsync("Automatic Client Zip was not preparable.",
HttpStatusCode.InternalServerError);
return true;
}
if (RequestWantsZStd(context) && result.ManifestCompressed)
{
context.ResponseHeaders.Add("Content-Encoding", "zstd");
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK);
}
else
{
if (result.ManifestCompressed)
{
// Manifest is compressed in-memory but client didn't want it compressed.
// Have to decompress ourselves.
var ms = new MemoryStream(result.ManifestData);
await using var stream = await context.RespondStreamAsync();
await using var decompressStream = new ZStdDecompressStream(ms);
await decompressStream.CopyToAsync(stream);
}
else
{
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK);
}
}
return true;
}
private async Task<bool> HandleAczManifestDownload(IStatusHandlerContext context)
{
if (context.Url.AbsolutePath != "/download")
return false;
if (context.RequestHeaders.ContainsKey("Content-Type")
&& context.RequestHeaders["Content-Type"] != "application/octet-stream")
{
await context.RespondAsync(
"Must specify application/octet-stream Content-Type",
HttpStatusCode.BadRequest);
}
if (!string.IsNullOrEmpty(_cfg.GetCVar(CVars.BuildManifestUrl)))
{
await context.RespondAsync("This server has a build manifest URL.", HttpStatusCode.NotFound);
return true;
}
// HTTP OPTIONS
if (context.RequestMethod == HttpMethod.Options)
{
context.ResponseHeaders["X-Robust-Download-Min-Protocol"] = "1";
context.ResponseHeaders["X-Robust-Download-Max-Protocol"] = "1";
await context.RespondNoContentAsync();
return true;
}
if (context.RequestMethod != HttpMethod.Post)
return false;
var aczInfo = await PrepareACZ();
if (aczInfo == null)
{
await context.RespondAsync("Automatic Client Zip was not preparable.",
HttpStatusCode.InternalServerError);
return true;
}
// HTTP POST: main handling system.
// Verify version request header.
// Right now only one version ("1") exists, so...
// Request body not yet read, don't allow keepalive.
context.KeepAlive = false;
if (!context.RequestHeaders.TryGetValue("X-Robust-Download-Protocol", out var versionHeader)
|| versionHeader.Count != 1
|| !Parse.TryInt32(versionHeader[0], out var version))
{
await context.RespondAsync("Expected single X-Robust-Download-Protocol header",
HttpStatusCode.BadRequest);
return true;
}
if (version != 1)
{
await context.RespondAsync("Unsupported download protocol version", HttpStatusCode.NotImplemented);
return true;
}
var fileCount = aczInfo.ManifestEntries.Length;
var requestBufSize = fileCount * 4;
var pool = ArrayPool<byte>.Shared.Rent(requestBufSize);
using var poolGuard = ArrayPool<byte>.Shared.ReturnGuard(pool);
var buffer = new MemoryStream(
pool,
0, requestBufSize,
writable: true,
publiclyVisible: true);
try
{
await context.RequestBody.CopyToAsync(buffer);
}
catch (NotSupportedException)
{
// Thrown by memory stream if full.
await context.RespondAsync("Request too large", HttpStatusCode.RequestEntityTooLarge);
return true;
}
// Request body read, allow keepalive again.
context.KeepAlive = true;
// Request body read. Validate it.
// Do not allow out-of-bounds files or duplicate requests.
var buf = pool.AsMemory(0, (int)buffer.Position);
var manifestLength = aczInfo.ManifestEntries.Length;
var bits = new BitArray(manifestLength);
var offset = 0;
while (offset < buf.Length)
{
var index = BinaryPrimitives.ReadInt32LittleEndian(buf.Slice(offset, 4).Span);
if (index < 0 || index >= manifestLength)
{
await context.RespondAsync("Out of bounds manifest index", HttpStatusCode.BadRequest);
return true;
}
if (bits[index])
{
await context.RespondAsync("Cannot request file twice", HttpStatusCode.BadRequest);
return true;
}
bits[index] = true;
offset += 4;
}
// There is a theoretical tiny race condition here where the main thread may change these parameters
// between us acquiring the ACZ info above and reading them here.
// The worst that could happen here is that the stream is either double-compressed or not compressed at all,
// So I am not too worried and am just gonna leave it as-is.
var cVarStreamCompression = _cfg.GetCVar(CVars.AczStreamCompress);
var cVarStreamCompressionLevel = _cfg.GetCVar(CVars.AczStreamCompressLevel);
// Only do zstd stream compression if the client asks for it and we have it enabled.
var doStreamCompression = RequestWantsZStd(context)
&& cVarStreamCompression;
if (doStreamCompression)
context.ResponseHeaders["Content-Encoding"] = "zstd";
var outStream = await context.RespondStreamAsync();
if (doStreamCompression)
{
var zStdCompressStream = new ZStdCompressStream(outStream);
zStdCompressStream.Context.SetParameter(
ZSTD_cParameter.ZSTD_c_compressionLevel,
cVarStreamCompressionLevel);
outStream = zStdCompressStream;
}
var preCompressed = aczInfo.PreCompressed;
var fileHeaderSize = 4;
if (preCompressed)
fileHeaderSize += 4;
var fileHeader = new byte[fileHeaderSize];
await using (outStream)
{
var streamHeader = new byte[4];
DownloadStreamHeaderFlags streamHeaderFlags = 0;
if (preCompressed)
streamHeaderFlags |= DownloadStreamHeaderFlags.PreCompressed;
BinaryPrimitives.WriteInt32LittleEndian(streamHeader, (int)streamHeaderFlags);
await outStream.WriteAsync(streamHeader);
offset = 0;
while (offset < buf.Length)
{
var index = BinaryPrimitives.ReadInt32LittleEndian(buf.Slice(offset, 4).Span);
var (blobLength, dataOffset, dataLength) = aczInfo.ManifestEntries[index];
// _aczSawmill.Debug($"{index:D5}: {blobLength:D8} {dataOffset:D8} {dataLength:D8}");
BinaryPrimitives.WriteInt32LittleEndian(fileHeader, blobLength);
if (preCompressed)
BinaryPrimitives.WriteInt32LittleEndian(fileHeader.AsSpan(4, 4), dataLength);
var writeLength = dataLength == 0 ? blobLength : dataLength;
await outStream.WriteAsync(fileHeader);
await outStream.WriteAsync(aczInfo.ManifestBlobData.AsMemory(dataOffset, writeLength));
offset += 4;
}
}
return true;
}
private static bool RequestWantsZStd(IStatusHandlerContext context)
{
// Yeah this isn't a good parser for Accept-Encoding but who cares.
return context.RequestHeaders.TryGetValue("Accept-Encoding", out var ac) && ac[0].Contains("zstd");
}
// Only call this if the download URL is not available!
private async Task<AutomaticClientZipInfo?> PrepareACZ()
{
// Take the ACZ lock asynchronously
await _aczLock.WaitAsync();
try
{
// Setting this now ensures that it won't fail repeatedly on exceptions/etc.
if (_aczPrepareAttempted)
return _aczPrepared;
_aczPrepareAttempted = true;
// ACZ hasn't been prepared, prepare it
try
{
// Run actual ACZ generation via Task.Run because it's synchronous
var maybeData = await Task.Run(PrepareACZInnards);
if (maybeData == null)
{
_aczSawmill.Error("StatusHost PrepareACZ failed (server will not be usable from launcher!)");
return null;
}
_aczPrepared = maybeData;
return maybeData;
}
catch (Exception e)
{
_aczSawmill.Error(
$"Exception in StatusHost PrepareACZ (server will not be usable from launcher!): {e}");
return null;
}
}
finally
{
_aczLock.Release();
}
}
// -- All methods from this point forward do not access the ACZ global state --
private AutomaticClientZipInfo? PrepareACZInnards()
{
_aczSawmill.Info("Preparing ACZ...");
// All of these should Info on success and Error on null-return failure
var zipData = PrepareACZViaFile() ?? PrepareACZViaMagic();
if (zipData == null)
return null;
var streamCompression = _cfg.GetCVar(CVars.AczStreamCompress);
var blobCompress = _cfg.GetCVar(CVars.AczBlobCompress);
var blobCompressLevel = _cfg.GetCVar(CVars.AczBlobCompressLevel);
var blobCompressSaveThresh = _cfg.GetCVar(CVars.AczBlobCompressSaveThreshold);
var manifestCompress = _cfg.GetCVar(CVars.AczManifestCompress);
var manifestCompressLevel = _cfg.GetCVar(CVars.AczManifestCompressLevel);
// Stream compression disables individual compression.
blobCompress &= !streamCompression;
_aczSawmill.Debug("Making ACZ manifest...");
var dataHash = Convert.ToHexString(SHA256.HashData(zipData));
using var zip = OpenZip(zipData);
var (manifestData, manifestEntries, manifestBlobData) = CalcManifestData(
zip,
blobCompress,
blobCompressLevel,
blobCompressSaveThresh);
var manifestHash = CryptoGenericHashBlake2B.Hash(32, manifestData, ReadOnlySpan<byte>.Empty);
var manifestHashString = Convert.ToHexString(manifestHash);
_aczSawmill.Debug("ACZ Manifest hash: {ManifestHash}", manifestHashString);
if (manifestCompress)
{
_aczSawmill.Debug("Compressing ACZ manifest at level {ManifestCompressLevel}", manifestCompressLevel);
var beforeSize = manifestData.Length;
var compressBuffer = (int) Zstd.ZSTD_COMPRESSBOUND((nuint) manifestData.Length);
var compressed = ArrayPool<byte>.Shared.Rent(compressBuffer);
var size = ZStd.Compress(compressed, manifestData, manifestCompressLevel);
manifestData = compressed[..size];
ArrayPool<byte>.Shared.Return(compressed);
_aczSawmill.Debug(
"ACZ manifest compression: {ManifestSize} -> {ManifestSizeCompressed} ({ManifestSizeRatio} ratio)",
beforeSize, manifestData.Length, manifestData.Length / (float) beforeSize);
}
return new AutomaticClientZipInfo(
zipData,
dataHash,
manifestData,
manifestCompress,
manifestHashString,
manifestBlobData,
manifestEntries,
blobCompress);
}
private static (byte[] manifestContent, AczManifestEntry[] manifestEntries, byte[] blobData)
CalcManifestData(
ZipArchive zip,
bool blobCompress,
int blobCompressLevel,
int blobCompressSaveThresh)
{
var blobData = new MemoryStream();
ZStdCompressStream? compressStream = null;
if (blobCompress)
{
var zStdCompressStream = new ZStdCompressStream(blobData);
zStdCompressStream.Context.SetParameter(
ZSTD_cParameter.ZSTD_c_compressionLevel,
blobCompressLevel);
compressStream = zStdCompressStream;
}
try
{
var decompressBuffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);
Span<byte> entryHash = stackalloc byte[256 / 8];
var manifestStream = new MemoryStream();
using var manifestWriter = new StreamWriter(manifestStream, EncodingHelpers.UTF8);
manifestWriter.Write("Robust Content Manifest 1\n");
var manifestEntries = new ValueList<AczManifestEntry>();
foreach (var entry in zip.Entries.OrderBy(e => e.FullName, StringComparer.Ordinal))
{
// Ignore directory entries.
if (entry.Name == "")
continue;
var length = (int)entry.Length;
var startPos = (int)blobData.Position;
BufferHelpers.EnsurePooledBuffer(ref decompressBuffer, ArrayPool<byte>.Shared, length);
var data = decompressBuffer.AsSpan(0, length);
using (var stream = entry.Open())
{
stream.ReadExact(data);
}
// Calculate hash.
CryptoGenericHashBlake2B.Hash(entryHash, data, ReadOnlySpan<byte>.Empty);
// Set to 0 to indicate not compressed.
int dataLength;
// Try compression if enabled.
if (blobCompress)
{
// Actually compress.
compressStream!.Write(data);
compressStream.FlushEnd();
// See if compression was worth it.
var endPos = (int)blobData.Position;
var compressedSize = endPos - startPos;
if (compressedSize + blobCompressSaveThresh < length)
{
dataLength = compressedSize;
}
else
{
// Compression not worth it, just send an uncompressed blob instead.
blobData.Position = startPos;
blobData.Write(data);
dataLength = 0;
}
}
else
{
// No compression, just write.
blobData.Write(data);
dataLength = 0;
}
manifestWriter.Write($"{Convert.ToHexString(entryHash)} {entry.FullName}\n");
manifestEntries.Add(new AczManifestEntry(length, startPos, dataLength));
}
manifestWriter.Flush();
ArrayPool<byte>.Shared.Return(decompressBuffer);
return (manifestStream.ToArray(), manifestEntries.ToArray(), blobData.ToArray());
}
finally
{
compressStream?.Dispose();
}
}
private static ZipArchive OpenZip(byte[] data)
{
var ms = new MemoryStream(data, false);
return new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: false);
}
private byte[]? PrepareACZViaFile()
{
var path = PathHelpers.ExecutableRelativeFile("Content.Client.zip");
if (!File.Exists(path)) return null;
_aczSawmill.Info($"StatusHost found client zip: {path}");
return File.ReadAllBytes(path);
}
private byte[]? PrepareACZViaMagic()
{
var sw = Stopwatch.StartNew();
var (binFolderPath, assemblyNames) =
_aczInfo ?? ("Content.Client", new[] { "Content.Client", "Content.Shared" });
var outStream = new MemoryStream();
var archive = new ZipArchive(outStream, ZipArchiveMode.Create);
foreach (var assemblyName in assemblyNames)
{
AttemptPullFromDisk($"Assemblies/{assemblyName}.dll", $"../../bin/{binFolderPath}/{assemblyName}.dll");
AttemptPullFromDisk($"Assemblies/{assemblyName}.pdb", $"../../bin/{binFolderPath}/{assemblyName}.pdb");
}
var prefix = PathHelpers.ExecutableRelativeFile("../../Resources");
foreach (var path in PathHelpers.GetFiles(prefix))
{
var relPath = Path.GetRelativePath(prefix, path);
if (OperatingSystem.IsWindows())
relPath = relPath.Replace('\\', '/');
AttemptPullFromDisk(relPath, path);
}
archive.Dispose();
_aczSawmill.Info("StatusHost synthesized client zip in {Elapsed} ms!", sw.ElapsedMilliseconds);
return outStream.ToArray();
void AttemptPullFromDisk(string pathTo, string pathFrom)
{
// _aczSawmill.Debug($"StatusHost PrepareACZMagic: {pathFrom} -> {pathTo}");
var res = PathHelpers.ExecutableRelativeFile(pathFrom);
if (!File.Exists(res))
return;
var entry = archive.CreateEntry(pathTo);
using var file = File.OpenRead(res);
using var entryStream = entry.Open();
file.CopyTo(entryStream);
}
}
public void SetAczInfo(string clientBinFolder, string[] clientAssemblyNames)
{
_aczLock.Wait();
try
{
if (_aczPrepared != null)
throw new InvalidOperationException("ACZ already prepared");
_aczInfo = (clientBinFolder, clientAssemblyNames);
}
finally
{
_aczLock.Release();
}
}
[Flags]
private enum DownloadStreamHeaderFlags
{
None = 0,
/// <summary>
/// If this flag is set on the download stream, individual files have been pre-compressed by the server.
/// This means each file has a compression header, and the launcher should not attempt to compress files itself.
/// </summary>
PreCompressed = 1 << 0
}
/// <param name="ZipData">Byte array containing the raw zip file data.</param>
/// <param name="ZipHash">Hex SHA256 hash of <see cref="ZipData"/>.</param>
/// <param name="ManifestData">Data for the content manifest</param>
/// <param name="ManifestHash">Hex BLAKE2B 256-bit hash of <see cref="ManifestData"/>.</param>
/// <param name="ManifestEntries">Manifest -> zip entry map.</param>
internal sealed record AutomaticClientZipInfo(
byte[] ZipData,
string ZipHash,
byte[] ManifestData,
bool ManifestCompressed,
string ManifestHash,
byte[] ManifestBlobData,
AczManifestEntry[] ManifestEntries,
bool PreCompressed);
/// <param name="BlobLength">Length of the uncompressed blob.</param>
/// <param name="DataOffset">Offset into <see cref="AutomaticClientZipInfo.ManifestBlobData"/> that this blob's (possibly compressed) data starts at.</param>
/// <param name="DataLength">
/// Length in <see cref="AutomaticClientZipInfo.ManifestBlobData"/> for this blob's (possibly compressed) data.
/// If this is zero, it means the file is not stored uncompressed and you should use <see cref="BlobLength"/>.
/// </param>
internal record struct AczManifestEntry(int BlobLength, int DataOffset, int DataLength);
}
}

View File

@@ -1,177 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Security.Cryptography;
using Robust.Shared;
using Robust.Shared.ContentPack;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
{
// Lock used while working on the ACZ.
private readonly SemaphoreSlim _aczLock = new(1, 1);
// If an attempt has been made to prepare the ACZ.
private bool _aczPrepareAttempted = false;
// Automatic Client Zip
private AutomaticClientZipInfo? _aczPrepared;
private (string binFolder, string[] assemblies)? _aczInfo;
private async Task<bool> HandleAutomaticClientZip(IStatusHandlerContext context)
{
if (!context.IsGetLike || context.Url!.AbsolutePath != "/client.zip")
{
return false;
}
if (!string.IsNullOrEmpty(_configurationManager.GetCVar(CVars.BuildDownloadUrl)))
{
await context.RespondAsync("This server has a build download URL.", HttpStatusCode.NotFound);
return true;
}
var result = await PrepareACZ();
if (result == null)
{
await context.RespondAsync("Automatic Client Zip was not preparable.", HttpStatusCode.InternalServerError);
return true;
}
await context.RespondAsync(result.Value.Data, HttpStatusCode.OK, "application/zip");
return true;
}
// Only call this if the download URL is not available!
private async Task<AutomaticClientZipInfo?> PrepareACZ()
{
// Take the ACZ lock asynchronously
await _aczLock.WaitAsync();
try
{
// Setting this now ensures that it won't fail repeatedly on exceptions/etc.
if (_aczPrepareAttempted) return _aczPrepared;
_aczPrepareAttempted = true;
// ACZ hasn't been prepared, prepare it
byte[] data;
try
{
// Run actual ACZ generation via Task.Run because it's synchronous
var maybeData = await Task.Run(PrepareACZInnards);
if (maybeData == null)
{
_httpSawmill.Error("StatusHost PrepareACZ failed (server will not be usable from launcher!)");
return null;
}
data = maybeData;
}
catch (Exception e)
{
_httpSawmill.Error($"Exception in StatusHost PrepareACZ (server will not be usable from launcher!): {e}");
return null;
}
_aczPrepared = new AutomaticClientZipInfo(data);
return _aczPrepared;
}
finally
{
_aczLock.Release();
}
}
// -- All methods from this point forward do not access the ACZ global state --
private byte[]? PrepareACZInnards()
{
// All of these should Info on success and Error on null-return failure
return PrepareACZViaFile() ?? PrepareACZViaMagic();
}
private byte[]? PrepareACZViaFile()
{
var path = PathHelpers.ExecutableRelativeFile("Content.Client.zip");
if (!File.Exists(path)) return null;
_httpSawmill.Info($"StatusHost found client zip: {path}");
return File.ReadAllBytes(path);
}
private byte[]? PrepareACZViaMagic()
{
var paths = new Dictionary<string, byte[]>();
bool AttemptPullFromDisk(string pathTo, string pathFrom)
{
// _httpSawmill.Debug($"StatusHost PrepareACZMagic: {pathFrom} -> {pathTo}");
var res = PathHelpers.ExecutableRelativeFile(pathFrom);
if (!File.Exists(res)) return false;
paths[pathTo] = File.ReadAllBytes(res);
return true;
}
var (binFolderPath, assemblyNames) =
_aczInfo ?? ("Content.Client", new[] { "Content.Client", "Content.Shared" });
foreach (var assemblyName in assemblyNames)
{
AttemptPullFromDisk($"Assemblies/{assemblyName}.dll", $"../../bin/{binFolderPath}/{assemblyName}.dll");
AttemptPullFromDisk($"Assemblies/{assemblyName}.pdb", $"../../bin/{binFolderPath}/{assemblyName}.pdb");
}
var prefix = PathHelpers.ExecutableRelativeFile("../../Resources");
foreach (var path in PathHelpers.GetFiles(prefix))
{
var relPath = Path.GetRelativePath(prefix, path);
if (OperatingSystem.IsWindows())
relPath = relPath.Replace('\\', '/');
AttemptPullFromDisk(relPath, path);
}
var outStream = new MemoryStream();
var archive = new ZipArchive(outStream, ZipArchiveMode.Create);
foreach (var kvp in paths)
{
var entry = archive.CreateEntry(kvp.Key);
using (var entryStream = entry.Open())
{
entryStream.Write(kvp.Value);
}
}
archive.Dispose();
_httpSawmill.Info($"StatusHost synthesized client zip!");
return outStream.ToArray();
}
public void SetAczInfo(string clientBinFolder, string[] clientAssemblyNames)
{
_aczLock.Wait();
try
{
if (_aczPrepared != null)
throw new InvalidOperationException("ACZ already prepared");
_aczInfo = (clientBinFolder, clientAssemblyNames);
}
finally
{
_aczLock.Release();
}
}
}
internal struct AutomaticClientZipInfo
{
public readonly byte[] Data;
public readonly string Hash;
public AutomaticClientZipInfo(byte[] data)
{
Data = data;
using var sha = SHA256.Create();
Hash = Convert.ToHexString(sha.ComputeHash(data));
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Robust.Server.ServerStatus
AddHandler(HandleTeapot);
AddHandler(HandleStatus);
AddHandler(HandleInfo);
AddHandler(HandleAutomaticClientZip);
AddAczHandlers();
}
private static async Task<bool> HandleTeapot(IStatusHandlerContext context)
@@ -58,7 +58,7 @@ namespace Robust.Server.ServerStatus
return false;
}
var downloadUrl = _configurationManager.GetCVar(CVars.BuildDownloadUrl);
var downloadUrl = _cfg.GetCVar(CVars.BuildDownloadUrl);
JsonObject? buildInfo;
@@ -68,20 +68,7 @@ namespace Robust.Server.ServerStatus
}
else
{
var hash = _configurationManager.GetCVar(CVars.BuildHash);
if (hash == "")
{
hash = null;
}
buildInfo = new JsonObject
{
["engine_version"] = _configurationManager.GetCVar(CVars.BuildEngineVersion),
["fork_id"] = _configurationManager.GetCVar(CVars.BuildForkId),
["version"] = _configurationManager.GetCVar(CVars.BuildVersion),
["download_url"] = downloadUrl,
["hash"] = hash,
};
buildInfo = GetExternalBuildInfo();
}
var authInfo = new JsonObject
@@ -94,7 +81,7 @@ namespace Robust.Server.ServerStatus
var jObject = new JsonObject
{
["connect_address"] = _configurationManager.GetCVar(CVars.StatusConnectAddress),
["connect_address"] = _cfg.GetCVar(CVars.StatusConnectAddress),
["auth"] = authInfo,
["build"] = buildInfo
};
@@ -106,6 +93,54 @@ namespace Robust.Server.ServerStatus
return true;
}
private JsonObject GetExternalBuildInfo()
{
var zipHash = _cfg.GetCVar(CVars.BuildHash);
var manifestHash = _cfg.GetCVar(CVars.BuildManifestHash);
var forkId = _cfg.GetCVar(CVars.BuildForkId);
var forkVersion = _cfg.GetCVar(CVars.BuildVersion);
var manifestDownloadUrl = Interpolate(_cfg.GetCVar(CVars.BuildManifestDownloadUrl));
var manifestUrl = Interpolate(_cfg.GetCVar(CVars.BuildManifestUrl));
var downloadUrl = Interpolate(_cfg.GetCVar(CVars.BuildDownloadUrl));
if (zipHash == "")
zipHash = null;
if (manifestHash == "")
manifestHash = null;
if (manifestDownloadUrl == "")
manifestDownloadUrl = null;
if (manifestUrl == "")
manifestUrl = null;
return new JsonObject
{
["engine_version"] = _cfg.GetCVar(CVars.BuildEngineVersion),
["fork_id"] = forkId,
["version"] = forkVersion,
["download_url"] = downloadUrl,
["hash"] = zipHash,
["acz"] = false,
["manifest_download_url"] = manifestDownloadUrl,
["manifest_url"] = manifestUrl,
["manifest_hash"] = manifestHash
};
string? Interpolate(string? value)
{
// Can't tell if splitting the ?. like this is more cursed than
// failing to align due to putting the full ?. on the next line
return value?
.Replace("{FORK_VERSION}", forkVersion)
.Replace("{FORK_ID}", forkId)
.Replace("{MANIFEST_HASH}", manifestHash)
.Replace("{ZIP_HASH}", zipHash);
}
}
private async Task<JsonObject?> PrepareACZBuildInfo()
{
var acz = await PrepareACZ();
@@ -113,10 +148,10 @@ namespace Robust.Server.ServerStatus
// Automatic - pass to ACZ
// Unfortunately, we still can't divine engine version.
var engineVersion = _configurationManager.GetCVar(CVars.BuildEngineVersion);
var engineVersion = _cfg.GetCVar(CVars.BuildEngineVersion);
// Fork ID is an interesting case, we don't want to cause too many redownloads but we also don't want to pollute disk.
// Call the fork "custom" if there's no explicit ID given.
var fork = _configurationManager.GetCVar(CVars.BuildForkId);
var fork = _cfg.GetCVar(CVars.BuildForkId);
if (string.IsNullOrEmpty(fork))
{
fork = "custom";
@@ -125,10 +160,15 @@ namespace Robust.Server.ServerStatus
{
["engine_version"] = engineVersion,
["fork_id"] = fork,
["version"] = acz.Value.Hash,
["version"] = acz.ManifestHash,
// Don't supply a download URL - like supplying an empty self-address
["download_url"] = "",
["hash"] = acz.Value.Hash,
["manifest_download_url"] = "",
["manifest_url"] = "",
// Pass acz so the launcher knows where to find the downloads.
["acz"] = true,
["hash"] = acz.ZipHash,
["manifest_hash"] = acz.ManifestHash
};
}
}

View File

@@ -18,8 +18,8 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Utility;
using HttpListener = ManagedHttpListener.HttpListener;
using HttpListenerContext = ManagedHttpListener.HttpListenerContext;
using HttpListener = SpaceWizards.HttpListener.HttpListener;
using HttpListenerContext = SpaceWizards.HttpListener.HttpListenerContext;
// This entire file is NIHing a REST server because pulling in libraries is effort.
// Also it was fun to write.
@@ -31,7 +31,7 @@ namespace Robust.Server.ServerStatus
{
private const string Sawmill = "statushost";
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -39,6 +39,7 @@ namespace Robust.Server.ServerStatus
private HttpListener? _listener;
private TaskCompletionSource? _stopSource;
private ISawmill _httpSawmill = default!;
private ISawmill _aczSawmill = default!;
private string? _serverNameCache;
@@ -95,13 +96,14 @@ namespace Robust.Server.ServerStatus
public void Start()
{
_httpSawmill = Logger.GetSawmill($"{Sawmill}.http");
_aczSawmill = Logger.GetSawmill($"{Sawmill}.acz");
RegisterCVars();
// Cache this in a field to avoid thread safety shenanigans.
// Writes/reads of references are atomic in C# so no further synchronization necessary.
_configurationManager.OnValueChanged(CVars.GameHostName, n => _serverNameCache = n, true);
_cfg.OnValueChanged(CVars.GameHostName, n => _serverNameCache = n, true);
if (!_configurationManager.GetCVar(CVars.StatusEnabled))
if (!_cfg.GetCVar(CVars.StatusEnabled))
{
return;
}
@@ -110,7 +112,7 @@ namespace Robust.Server.ServerStatus
_stopSource = new TaskCompletionSource();
_listener = new HttpListener();
_listener.Prefixes.Add($"http://{_configurationManager.GetCVar(CVars.StatusBind)}/");
_listener.Prefixes.Add($"http://{_cfg.GetCVar(CVars.StatusBind)}/");
_listener.Start();
Task.Run(ListenerThread);
@@ -119,7 +121,7 @@ namespace Robust.Server.ServerStatus
// Not a real thread but whatever.
private async Task ListenerThread()
{
var maxConnections = _configurationManager.GetCVar(CVars.StatusMaxConnections);
var maxConnections = _cfg.GetCVar(CVars.StatusMaxConnections);
var connectionsSemaphore = new SemaphoreSlim(maxConnections, maxConnections);
while (true)
{
@@ -157,6 +159,8 @@ namespace Robust.Server.ServerStatus
private void RegisterCVars()
{
InitAcz();
// Set status host binding to match network manager by default
SetCVarIfUnmodified(CVars.StatusBind, $"*:{_netManager.Port}");
@@ -173,19 +177,22 @@ namespace Robust.Server.ServerStatus
SetCVarIfUnmodified(CVars.BuildVersion, info.Version);
SetCVarIfUnmodified(CVars.BuildDownloadUrl, info.Download ?? "");
SetCVarIfUnmodified(CVars.BuildHash, info.Hash ?? "");
SetCVarIfUnmodified(CVars.BuildManifestHash, info.ManifestHash ?? "");
SetCVarIfUnmodified(CVars.BuildManifestDownloadUrl, info.ManifestDownloadUrl ?? "");
SetCVarIfUnmodified(CVars.BuildManifestUrl, info.ManifestUrl ?? "");
}
// Automatically determine engine version if no other source has provided a result
var asmVer = typeof(StatusHost).Assembly.GetName().Version;
if (asmVer != null)
{
SetCVarIfUnmodified(CVars.BuildEngineVersion, asmVer.ToString(3));
SetCVarIfUnmodified(CVars.BuildEngineVersion, asmVer.ToString(4));
}
void SetCVarIfUnmodified(CVarDef<string> cvar, string val)
{
if (_configurationManager.GetCVar(cvar) == "")
_configurationManager.SetCVar(cvar, val);
if (_cfg.GetCVar(cvar) == "")
_cfg.SetCVar(cvar, val);
}
}
@@ -211,17 +218,33 @@ namespace Robust.Server.ServerStatus
[property: JsonPropertyName("fork_id")]
string ForkId,
[property: JsonPropertyName("version")]
string Version);
string Version,
[property: JsonPropertyName("manifest_hash")]
string? ManifestHash,
[property: JsonPropertyName("manifest_url")]
string? ManifestUrl,
[property: JsonPropertyName("manifest_download_url")]
string? ManifestDownloadUrl);
private sealed class ContextImpl : IStatusHandlerContext
{
private readonly HttpListenerContext _context;
private readonly Dictionary<string, string> _responseHeaders;
public HttpMethod RequestMethod { get; }
public IPEndPoint RemoteEndPoint => _context.Request.RemoteEndPoint!;
public Stream RequestBody => _context.Request.InputStream;
public Uri Url => _context.Request.Url!;
public bool IsGetLike => RequestMethod == HttpMethod.Head || RequestMethod == HttpMethod.Get;
public IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
public bool KeepAlive
{
get => _context.Response.KeepAlive;
set => _context.Response.KeepAlive = value;
}
public IDictionary<string, string> ResponseHeaders => _responseHeaders;
public ContextImpl(HttpListenerContext context)
{
_context = context;
@@ -237,16 +260,17 @@ namespace Robust.Server.ServerStatus
}
RequestHeaders = headers;
_responseHeaders = new Dictionary<string, string>();
}
public T? RequestBodyJson<T>()
{
return JsonSerializer.Deserialize<T>(_context.Request.InputStream);
return JsonSerializer.Deserialize<T>(RequestBody);
}
public async Task<T?> RequestBodyJsonAsync<T>()
{
return await JsonSerializer.DeserializeAsync<T>(_context.Request.InputStream);
return await JsonSerializer.DeserializeAsync<T>(RequestBody);
}
public void Respond(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = MediaTypeNames.Text.Plain)
@@ -290,6 +314,16 @@ namespace Robust.Server.ServerStatus
_context.Response.Close();
}
public Task RespondNoContentAsync()
{
RespondShared();
_context.Response.StatusCode = (int) HttpStatusCode.NoContent;
_context.Response.Close();
return Task.CompletedTask;
}
public Task RespondAsync(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = "text/plain")
{
return RespondAsync(text, (int) code, contentType);
@@ -297,6 +331,8 @@ namespace Robust.Server.ServerStatus
public async Task RespondAsync(string text, int code = 200, string contentType = "text/plain")
{
RespondShared();
_context.Response.StatusCode = code;
_context.Response.ContentType = contentType;
@@ -315,6 +351,8 @@ namespace Robust.Server.ServerStatus
public async Task RespondAsync(byte[] data, int code = 200, string contentType = "text/plain")
{
RespondShared();
_context.Response.StatusCode = code;
_context.Response.ContentType = contentType;
_context.Response.ContentLength64 = data.Length;
@@ -341,6 +379,8 @@ namespace Robust.Server.ServerStatus
public void RespondJson(object jsonData, HttpStatusCode code = HttpStatusCode.OK)
{
RespondShared();
_context.Response.ContentType = "application/json";
JsonSerializer.Serialize(_context.Response.OutputStream, jsonData);
@@ -350,12 +390,31 @@ namespace Robust.Server.ServerStatus
public async Task RespondJsonAsync(object jsonData, HttpStatusCode code = HttpStatusCode.OK)
{
RespondShared();
_context.Response.ContentType = "application/json";
await JsonSerializer.SerializeAsync(_context.Response.OutputStream, jsonData);
_context.Response.Close();
}
public Task<Stream> RespondStreamAsync(HttpStatusCode code = HttpStatusCode.OK)
{
RespondShared();
_context.Response.StatusCode = (int) code;
return Task.FromResult(_context.Response.OutputStream);
}
private void RespondShared()
{
foreach (var (header, value) in _responseHeaders)
{
_context.Response.AddHeader(header, value);
}
}
}
}
}

View File

@@ -38,7 +38,7 @@ namespace Robust.Server.ViewVariables.Traits
{
var type = component.GetType();
list.Add(new ViewVariablesBlobEntityComponents.Entry
{Stringified = TypeAbbreviation.Abbreviate(type), FullName = type.FullName, ComponentName = component.Name});
{Stringified = PrettyPrint.PrintUserFacingTypeShort(type, 2), FullName = type.FullName, ComponentName = component.Name});
}
return new ViewVariablesBlobEntityComponents

View File

@@ -44,7 +44,7 @@ namespace Robust.Server.ViewVariables.Traits
Editable = access == VVAccess.ReadWrite,
Name = property.Name,
Type = property.PropertyType.AssemblyQualifiedName,
TypePretty = TypeAbbreviation.Abbreviate(property.PropertyType),
TypePretty = PrettyPrint.PrintUserFacingTypeShort(property.PropertyType, 2),
Value = property.GetValue(Session.Object),
PropertyIndex = _members.Count
}, property));
@@ -63,7 +63,7 @@ namespace Robust.Server.ViewVariables.Traits
Editable = access == VVAccess.ReadWrite,
Name = field.Name,
Type = field.FieldType.AssemblyQualifiedName,
TypePretty = TypeAbbreviation.Abbreviate(field.FieldType),
TypePretty = PrettyPrint.PrintUserFacingTypeShort(field.FieldType, 2),
Value = field.GetValue(Session.Object),
PropertyIndex = _members.Count
}, field));

View File

@@ -26,7 +26,7 @@ namespace Robust.Server.ViewVariables
[Dependency] private readonly IRobustSerializer _robustSerializer = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
private readonly Dictionary<uint, ViewVariablesesSession>
private readonly Dictionary<uint, ViewVariablesSession>
_sessions = new();
private uint _nextSessionId = 1;
@@ -91,7 +91,7 @@ namespace Robust.Server.ViewVariables
var blob = session.DataRequest(message.RequestMeta);
var dataMsg = _netManager.CreateNetMessage<MsgViewVariablesRemoteData>();
var dataMsg = new MsgViewVariablesRemoteData();
dataMsg.RequestId = message.RequestId;
dataMsg.Blob = blob;
_netManager.ServerSendMessage(dataMsg, message.MsgChannel);
@@ -101,7 +101,7 @@ namespace Robust.Server.ViewVariables
{
void Deny(DenyReason reason)
{
var denyMsg = _netManager.CreateNetMessage<MsgViewVariablesDenySession>();
var denyMsg = new MsgViewVariablesDenySession();
denyMsg.RequestId = message.RequestId;
denyMsg.Reason = reason;
_netManager.ServerSendMessage(denyMsg, message.MsgChannel);
@@ -212,12 +212,12 @@ namespace Robust.Server.ViewVariables
}
var sessionId = _nextSessionId++;
var session = new ViewVariablesesSession(message.MsgChannel.UserId, theObject, sessionId, this,
var session = new ViewVariablesSession(message.MsgChannel.UserId, theObject, sessionId, this,
_robustSerializer);
_sessions.Add(sessionId, session);
var allowMsg = _netManager.CreateNetMessage<MsgViewVariablesOpenSession>();
var allowMsg = new MsgViewVariablesOpenSession();
allowMsg.RequestId = message.RequestId;
allowMsg.SessionId = session.SessionId;
_netManager.ServerSendMessage(allowMsg, message.MsgChannel);
@@ -245,7 +245,7 @@ namespace Robust.Server.ViewVariables
return;
}
var closeMsg = _netManager.CreateNetMessage<MsgViewVariablesCloseSession>();
var closeMsg = new MsgViewVariablesCloseSession();
closeMsg.SessionId = session.SessionId;
_netManager.ServerSendMessage(closeMsg, player.ConnectedClient);
}

View File

@@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Server.ViewVariables
{
internal sealed class ViewVariablesesSession : IViewVariablesSession
internal sealed class ViewVariablesSession : IViewVariablesSession
{
private readonly List<ViewVariablesTrait> _traits = new();
public IViewVariablesHost Host { get; }
@@ -24,7 +24,7 @@ namespace Robust.Server.ViewVariables
/// The session ID for this session. This is what the server and client use to talk about this session.
/// </param>
/// <param name="host">The view variables host owning this session.</param>
public ViewVariablesesSession(NetUserId playerUser, object o, uint sessionId, IViewVariablesHost host,
public ViewVariablesSession(NetUserId playerUser, object o, uint sessionId, IViewVariablesHost host,
IRobustSerializer robustSerializer)
{
PlayerUser = playerUser;

View File

@@ -13,7 +13,7 @@ namespace Robust.Server.ViewVariables
/// Traits define what behavior an object can have that VV cares about.
/// So like, is it enumerable, does it have VV accessible members. That kinda deal.
/// These are the "modular" way of extending VV.
/// Server traits are bound to one <see cref="ViewVariablesesSession"/>, AKA one object.
/// Server traits are bound to one <see cref="ViewVariablesSession"/>, AKA one object.
/// </summary>
internal abstract class ViewVariablesTrait
{

View File

@@ -32,7 +32,7 @@ enabled = true
[game]
hostname = "MyServer"
# map = "Maps/saltern.yml"
# map = "saltern"
maxplayers = 64
type = 1
welcomemsg = "Welcome to the server!"

View File

@@ -458,12 +458,15 @@ namespace Robust.Shared.Maths
var c = max - min;
var h = 0.0f;
if (max == rgb.R)
h = (rgb.G - rgb.B) / c;
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)
h = (rgb.R - rgb.G) / c + 4.0f;
if (c != 0)
{
if (max == rgb.R)
h = (rgb.G - rgb.B) / c;
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)
h = (rgb.R - rgb.G) / c + 4.0f;
}
var hue = h / 6.0f;
if (hue < 0.0f)
@@ -566,12 +569,15 @@ namespace Robust.Shared.Maths
var c = max - min;
var h = 0.0f;
if (max == rgb.R)
h = (rgb.G - rgb.B) / c % 6.0f;
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)
h = (rgb.R - rgb.G) / c + 4.0f;
if (c != 0)
{
if (max == rgb.R)
h = (rgb.G - rgb.B) / c % 6.0f;
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)
h = (rgb.R - rgb.G) / c + 4.0f;
}
var hue = h * 60.0f / 360.0f;

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