mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
87 Commits
reactjs-su
...
v0.14.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e65f9b2a1 | ||
|
|
d60bbe9fe9 | ||
|
|
dec1495e1e | ||
|
|
dc72c6fe22 | ||
|
|
141b1205c6 | ||
|
|
65f4a09ad5 | ||
|
|
3d1545c0b9 | ||
|
|
ec26dd622b | ||
|
|
0ab3131964 | ||
|
|
588a9e9f63 | ||
|
|
f2fa930edd | ||
|
|
ec47229a37 | ||
|
|
bf5d1d58a8 | ||
|
|
8b4da24ee7 | ||
|
|
3fba108d70 | ||
|
|
35029f0eed | ||
|
|
b66ab9d7c6 | ||
|
|
5e0b745ba9 | ||
|
|
45d906ba7e | ||
|
|
24b124fb17 | ||
|
|
7cb0978468 | ||
|
|
44cb135a1d | ||
|
|
146b673203 | ||
|
|
b0d23c5665 | ||
|
|
68f89c8958 | ||
|
|
1327d6bf25 | ||
|
|
237e37ff30 | ||
|
|
4d707c86cb | ||
|
|
c7027c6e00 | ||
|
|
81ec61bcc8 | ||
|
|
649178fa54 | ||
|
|
9ff46b9aad | ||
|
|
cdcbb60ca7 | ||
|
|
d8cdb2b312 | ||
|
|
fa1c1b8e6e | ||
|
|
2ded835602 | ||
|
|
a43f04818d | ||
|
|
278dc60119 | ||
|
|
8a202be0cf | ||
|
|
58940a3cd7 | ||
|
|
aa9721d146 | ||
|
|
79f114ad5b | ||
|
|
197a6ddd9d | ||
|
|
4a4fb15e06 | ||
|
|
e7e83ce6e8 | ||
|
|
a82b293452 | ||
|
|
bb483a3a38 | ||
|
|
2ca3d0e395 | ||
|
|
bda79b1e82 | ||
|
|
4f4b754e2d | ||
|
|
214cabac43 | ||
|
|
7b6229c222 | ||
|
|
1a14a75897 | ||
|
|
76b75fd9b3 | ||
|
|
b969fd22f7 | ||
|
|
8d27d091af | ||
|
|
1eb7393a60 | ||
|
|
4cf88507c2 | ||
|
|
3565d8b321 | ||
|
|
7094c29b2e | ||
|
|
63004b270f | ||
|
|
6714a99b38 | ||
|
|
4c3b8df1e7 | ||
|
|
4bb695121f | ||
|
|
09fd47c421 | ||
|
|
d201d9c688 | ||
|
|
fd1e25c584 | ||
|
|
6bb66ae70e | ||
|
|
cc82d6b1d9 | ||
|
|
956be749b6 | ||
|
|
6585a00608 | ||
|
|
c0525f710f | ||
|
|
d3672807d2 | ||
|
|
60f18d5f36 | ||
|
|
e72d3de256 | ||
|
|
ba9846b9c4 | ||
|
|
09586284dc | ||
|
|
a1ee4374b2 | ||
|
|
4de6f25f11 | ||
|
|
582d8a5587 | ||
|
|
ec53b04f99 | ||
|
|
950fc94408 | ||
|
|
58d12e6e09 | ||
|
|
94323005c4 | ||
|
|
4989842057 | ||
|
|
80172636a8 | ||
|
|
8491f7be24 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
Submodule Lidgren.Network/Lidgren.Network updated: b723fc532e...45e45e61ac
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<PropertyGroup><Version>0.8.86</Version></PropertyGroup>
|
||||
<PropertyGroup><Version>0.14.0.0</Version></PropertyGroup>
|
||||
</Project>
|
||||
|
||||
Submodule ManagedHttpListener deleted from ae0539e66f
10
Resources/Locale/en-US/controls.ftl
Normal file
10
Resources/Locale/en-US/controls.ftl
Normal 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
|
||||
1
Resources/Locale/en-US/midi-commands.ftl
Normal file
1
Resources/Locale/en-US/midi-commands.ftl
Normal file
@@ -0,0 +1 @@
|
||||
midi-panic-command-description = Turns off every note for every active MIDI renderer.
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
22
Robust.Client/Audio/Midi/Commands/MidiPanicCommand.cs
Normal file
22
Robust.Client/Audio/Midi/Commands/MidiPanicCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Robust.Client/Audio/Midi/IMidiManager.cs
Normal file
62
Robust.Client/Audio/Midi/IMidiManager.cs
Normal 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();
|
||||
}
|
||||
174
Robust.Client/Audio/Midi/IMidiRenderer.cs
Normal file
174
Robust.Client/Audio/Midi/IMidiRenderer.cs
Normal 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();
|
||||
}
|
||||
155
Robust.Client/Audio/Midi/MidiManager.Conversion.cs
Normal file
155
Robust.Client/Audio/Midi/MidiManager.Conversion.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,479 +16,478 @@ 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 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;
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
private readonly List<IMidiRenderer> _renderers = new();
|
||||
_midiThread = new Thread(ThreadUpdate);
|
||||
_midiThread.Start();
|
||||
|
||||
private bool _alive = true;
|
||||
private Settings? _settings;
|
||||
private Thread? _midiThread;
|
||||
private ISawmill _midiSawmill = default!;
|
||||
private float _volume = 0f;
|
||||
private bool _volumeDirty = true;
|
||||
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
|
||||
FluidsynthInitialized = 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 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();
|
||||
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++)
|
||||
{
|
||||
var renderer = _renderers[i];
|
||||
if (!renderer.Disposed)
|
||||
renderer.Render();
|
||||
else
|
||||
{
|
||||
renderer.InternalDispose();
|
||||
_renderers.Remove(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
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 && resourceCache.ContentFileExists(filename))
|
||||
{
|
||||
if (!resourceCache.TryContentFileRead(filename, out stream))
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
Stream? stream;
|
||||
var resourceCache = IoCManager.Resolve<IResourceCache>();
|
||||
var resourcePath = new ResourcePath(filename);
|
||||
var id = _nextStreamId++;
|
||||
|
||||
if (resourcePath.IsRooted && resourceCache.ContentFileExists(filename))
|
||||
_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)
|
||||
{
|
||||
if (!resourceCache.TryContentFileRead(filename, out stream))
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
|
||||
stream.ReadExact(buffer);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
|
||||
var id = _nextStreamId++;
|
||||
|
||||
_openStreams.Add(id, stream);
|
||||
|
||||
return (IntPtr) id;
|
||||
}
|
||||
|
||||
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
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)
|
||||
{
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
|
||||
stream.ReadExact(buffer);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream.Seek(offset, origin);
|
||||
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
return 0;
|
||||
}
|
||||
stream.Seek(offset, origin);
|
||||
|
||||
public override int Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) stream.Position;
|
||||
}
|
||||
public override int Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
return (int) stream.Position;
|
||||
}
|
||||
|
||||
stream.Dispose();
|
||||
return 0;
|
||||
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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
395
Robust.Client/UserInterface/Controls/ColorSelectorSliders.cs
Normal file
395
Robust.Client/UserInterface/Controls/ColorSelectorSliders.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
81
Robust.Client/UserInterface/Controls/ColorableSlider.cs
Normal file
81
Robust.Client/UserInterface/Controls/ColorableSlider.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]}.");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -83,11 +83,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);
|
||||
@@ -352,6 +352,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 +546,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 +603,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 +639,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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
218
Robust.Server/Maps/GridSerializer.cs
Normal file
218
Robust.Server/Maps/GridSerializer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,12 +356,16 @@ 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();
|
||||
|
||||
@@ -355,13 +376,13 @@ namespace Robust.Server.Maps
|
||||
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 +405,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 +420,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))
|
||||
{
|
||||
@@ -487,7 +508,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 +532,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 +563,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 +584,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 +613,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 +635,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 +648,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 +675,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 +707,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 +722,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)
|
||||
@@ -737,12 +778,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 +805,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 +815,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 +884,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 +925,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 +938,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 +952,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 +963,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 +986,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 +1005,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 +1071,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 +1087,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
654
Robust.Server/ServerStatus/StatusHost.Acz.cs
Normal file
654
Robust.Server/ServerStatus/StatusHost.Acz.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ enabled = true
|
||||
|
||||
[game]
|
||||
hostname = "MyServer"
|
||||
# map = "Maps/saltern.yml"
|
||||
# map = "saltern"
|
||||
maxplayers = 64
|
||||
type = 1
|
||||
welcomemsg = "Welcome to the server!"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
using System;
|
||||
using NFluidsynth;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Robust.Shared.Audio.Midi
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is a data representation of a Midi Event.
|
||||
/// It's 'compatible' with NFluidsynth's own MidiEvent class.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public struct MidiEvent
|
||||
{
|
||||
public byte Type { get; set; }
|
||||
|
||||
public byte Channel { get; set; }
|
||||
|
||||
public byte Key { get; set; }
|
||||
|
||||
public byte Velocity { get; set; }
|
||||
|
||||
public int Control { get; set; }
|
||||
|
||||
public byte Value { get; set; }
|
||||
|
||||
public byte Program { get; set; }
|
||||
|
||||
public short Pitch { get; set; } // needs range from 0 to 16383
|
||||
|
||||
public uint Tick { get; set; }
|
||||
|
||||
public static explicit operator MidiEvent(NFluidsynth.MidiEvent midiEvent)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Type = (byte) midiEvent.Type,
|
||||
Channel = (byte) midiEvent.Channel,
|
||||
Control = midiEvent.Control,
|
||||
Key = (byte) midiEvent.Key,
|
||||
Pitch = (short) midiEvent.Pitch,
|
||||
Program = (byte) midiEvent.Program,
|
||||
Value = (byte) midiEvent.Value,
|
||||
Velocity = (byte) midiEvent.Velocity,
|
||||
};
|
||||
}
|
||||
|
||||
public static implicit operator NFluidsynth.MidiEvent(MidiEvent midiEvent)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Type = midiEvent.Type,
|
||||
Channel = midiEvent.Channel,
|
||||
Control = midiEvent.Control,
|
||||
Key = midiEvent.Key,
|
||||
Pitch = midiEvent.Pitch,
|
||||
Program = midiEvent.Program,
|
||||
Value = midiEvent.Value,
|
||||
Velocity = midiEvent.Velocity,
|
||||
};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} >> TYPE: {Type} || CHANNEL: {Channel} || CONTROL: {Control} || KEY: {Key} || VELOCITY: {Velocity} || PITCH: {Pitch} || PROGRAM: {Program} || VALUE: {Value} <<";
|
||||
}
|
||||
|
||||
public static implicit operator SequencerEvent(MidiEvent midiEvent)
|
||||
{
|
||||
var @event = new SequencerEvent();
|
||||
|
||||
switch (midiEvent.Type)
|
||||
{
|
||||
// NoteOff
|
||||
case 128:
|
||||
@event.NoteOff(midiEvent.Channel, midiEvent.Key);
|
||||
break;
|
||||
|
||||
// NoteOn
|
||||
case 144:
|
||||
if(midiEvent.Velocity != 0)
|
||||
@event.NoteOn(midiEvent.Channel, midiEvent.Key, midiEvent.Velocity);
|
||||
else
|
||||
@event.NoteOff(midiEvent.Channel, midiEvent.Key);
|
||||
break;
|
||||
|
||||
// After Touch
|
||||
case 160:
|
||||
@event.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);
|
||||
break;
|
||||
|
||||
// CC
|
||||
case 176:
|
||||
@event.ControlChange(midiEvent.Channel, (short)midiEvent.Control, midiEvent.Value);
|
||||
break;
|
||||
|
||||
// Program Change
|
||||
case 192:
|
||||
@event.ProgramChange(midiEvent.Channel, midiEvent.Program);
|
||||
break;
|
||||
|
||||
// Channel Pressure
|
||||
case 208:
|
||||
@event.ChannelPressure(midiEvent.Channel, midiEvent.Value);
|
||||
break;
|
||||
|
||||
// Pitch Bend
|
||||
case 224:
|
||||
@event.PitchBend(midiEvent.Channel, midiEvent.Pitch);
|
||||
break;
|
||||
}
|
||||
|
||||
return @event;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
64
Robust.Shared/Audio/Midi/RobustMidiCommand.cs
Normal file
64
Robust.Shared/Audio/Midi/RobustMidiCommand.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace Robust.Shared.Audio.Midi;
|
||||
|
||||
/// <summary>
|
||||
/// Helper enum that keeps track of all MIDI commands that Robust currently supports.
|
||||
/// </summary>
|
||||
public enum RobustMidiCommand : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// NoteOff event. <br/>
|
||||
/// data1 - Key, <br/>
|
||||
/// data2 - undefined
|
||||
/// </summary>
|
||||
NoteOff = 0x80,
|
||||
|
||||
/// <summary>
|
||||
/// NoteOn event. <br/>
|
||||
/// data1 - Key, <br/>
|
||||
/// data2 - Velocity
|
||||
/// </summary>
|
||||
NoteOn = 0x90,
|
||||
|
||||
/// <summary>
|
||||
/// AfterTouch event. <br/>
|
||||
/// data1 - Key, <br/>
|
||||
/// data2 - Value
|
||||
/// </summary>
|
||||
/// <remarks>Also known as "KeyPressure".</remarks>
|
||||
AfterTouch = 0xA0,
|
||||
|
||||
/// <summary>
|
||||
/// ControlChange (CC) event. <br/>
|
||||
/// data1 - Control, <br/>
|
||||
/// data2 - Value
|
||||
/// </summary>
|
||||
ControlChange = 0xB0,
|
||||
|
||||
/// <summary>
|
||||
/// ProgramChange event. <br/>
|
||||
/// data1 - Program, <br/>
|
||||
/// data2 - undefined
|
||||
/// </summary>
|
||||
ProgramChange = 0xC0,
|
||||
|
||||
/// <summary>
|
||||
/// ChannelPressure event. <br/>
|
||||
/// data1 - Value, <br/>
|
||||
/// data2 - undefined
|
||||
/// </summary>
|
||||
ChannelPressure = 0xD0,
|
||||
|
||||
/// <summary>
|
||||
/// PitchBend event. <br/>
|
||||
/// data1 - Lower Pitch Nibble, <br/>
|
||||
/// data2 - Higher Pitch Nibble
|
||||
/// </summary>
|
||||
PitchBend = 0xE0,
|
||||
|
||||
/// <summary>
|
||||
/// SystemMessage event. <br/>
|
||||
/// data1 - Control <br/>
|
||||
/// data2 - undefined
|
||||
/// </summary>
|
||||
SystemMessage = 0xF0,
|
||||
}
|
||||
94
Robust.Shared/Audio/Midi/RobustMidiEvent.cs
Normal file
94
Robust.Shared/Audio/Midi/RobustMidiEvent.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Robust.Shared.Audio.Midi
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is a lightweight data representation of a Midi Event.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public readonly struct RobustMidiEvent
|
||||
{
|
||||
#region Data
|
||||
|
||||
/// <summary>
|
||||
/// Byte that stores both Command Type and Channel.
|
||||
/// </summary>
|
||||
public readonly byte Status;
|
||||
|
||||
public readonly byte Data1;
|
||||
public readonly byte Data2;
|
||||
|
||||
/// <summary>
|
||||
/// Sequencer tick to schedule this event at.
|
||||
/// </summary>
|
||||
public readonly uint Tick;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public int Channel => Status & 0x0F; // Low nibble.
|
||||
public int Command => Status & 0xF0; // High nibble.
|
||||
public RobustMidiCommand MidiCommand => (RobustMidiCommand) Command;
|
||||
public byte Key => Data1;
|
||||
public byte Velocity => Data2;
|
||||
public byte Control => Data1;
|
||||
public byte Value => Data2;
|
||||
public int Pitch => (Data2 << 8) | Data1;
|
||||
public byte Pressure => Data1;
|
||||
public byte Program => Data1;
|
||||
|
||||
#endregion
|
||||
|
||||
public RobustMidiEvent(byte status, byte data1, byte data2, uint tick)
|
||||
{
|
||||
Status = status;
|
||||
Data1 = data1;
|
||||
Data2 = data2;
|
||||
Tick = tick;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} >> CHANNEL: 0x{Channel:X} || COMMAND: 0x{Command:X} {MidiCommand} || DATA1: 0x{Data1:X} || DATA2: 0x{Data2:X} || TICK: {Tick} <<";
|
||||
}
|
||||
|
||||
#region Static Methods
|
||||
|
||||
/// <summary>
|
||||
/// Returns a status byte given a channel byte and a command type byte.
|
||||
/// </summary>
|
||||
public static byte MakeStatus(byte channel, byte command)
|
||||
{
|
||||
return (byte) (command | channel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and returns an event to turn all notes off on a given channel.
|
||||
/// </summary>
|
||||
public static RobustMidiEvent AllNotesOff(byte channel, uint tick)
|
||||
{
|
||||
return new RobustMidiEvent(MakeStatus(channel, (byte)RobustMidiCommand.SystemMessage), 0x0B, 0x0, tick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and returns an event to reset all controllers.
|
||||
/// </summary>
|
||||
public static RobustMidiEvent ResetAllControllers(uint tick)
|
||||
{
|
||||
return new RobustMidiEvent((byte)RobustMidiCommand.ControlChange, 0x79, 0x0, tick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and returns a system reset event.
|
||||
/// </summary>
|
||||
public static RobustMidiEvent SystemReset(uint tick)
|
||||
{
|
||||
return new RobustMidiEvent((byte) RobustMidiCommand.SystemMessage | 0x0F, 0x0, 0x0, tick);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,12 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> NetPVSEntityBudget =
|
||||
CVarDef.Create("net.pvs_budget", 50, CVar.ARCHIVE | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// ZSTD compression level to use when compressing game states.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> NetPVSCompressLevel =
|
||||
CVarDef.Create("net.pvs_compress_level", 3, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Log late input messages from clients.
|
||||
/// </summary>
|
||||
@@ -188,6 +194,33 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<string> NetLidgrenAppIdentifier =
|
||||
CVarDef.Create("net.lidgren_app_identifier", "RobustToolbox");
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// Add random fake network loss to all outgoing UDP network packets, as a ratio of how many packets to drop.
|
||||
/// 0 = no packet loss, 1 = all packets dropped
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetFakeLoss = CVarDef.Create("net.fakeloss", 0f, CVar.CHEAT);
|
||||
|
||||
/// <summary>
|
||||
/// Add fake extra delay to all outgoing UDP network packets, in seconds.
|
||||
/// </summary>
|
||||
/// <seealso cref="NetFakeLagRand"/>
|
||||
public static readonly CVarDef<float> NetFakeLagMin = CVarDef.Create("net.fakelagmin", 0f, CVar.CHEAT);
|
||||
|
||||
/// <summary>
|
||||
/// Add fake extra random delay to all outgoing UDP network packets, in seconds.
|
||||
/// The actual delay added for each packet is random between 0 and the specified value.
|
||||
/// </summary>
|
||||
/// <seealso cref="NetFakeLagMin"/>
|
||||
public static readonly CVarDef<float> NetFakeLagRand = CVarDef.Create("net.fakelagrand", 0f, CVar.CHEAT);
|
||||
|
||||
/// <summary>
|
||||
/// Add random fake duplicates to all outgoing UDP network packets, as a ratio of how many packets to duplicate.
|
||||
/// 0 = no packets duplicated, 1 = all packets duplicated.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetFakeDuplicates = CVarDef.Create("net.fakeduplicates", 0f, CVar.CHEAT);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* SUS
|
||||
*/
|
||||
@@ -218,33 +251,6 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> SysGameThreadPriority =
|
||||
CVarDef.Create("sys.game_thread_priority", (int) ThreadPriority.AboveNormal);
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// Add random fake network loss to all outgoing UDP network packets, as a ratio of how many packets to drop.
|
||||
/// 0 = no packet loss, 1 = all packets dropped
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetFakeLoss = CVarDef.Create("net.fakeloss", 0f, CVar.CHEAT);
|
||||
|
||||
/// <summary>
|
||||
/// Add fake extra delay to all outgoing UDP network packets, in seconds.
|
||||
/// </summary>
|
||||
/// <seealso cref="NetFakeLagRand"/>
|
||||
public static readonly CVarDef<float> NetFakeLagMin = CVarDef.Create("net.fakelagmin", 0f, CVar.CHEAT);
|
||||
|
||||
/// <summary>
|
||||
/// Add fake extra random delay to all outgoing UDP network packets, in seconds.
|
||||
/// The actual delay added for each packet is random between 0 and the specified value.
|
||||
/// </summary>
|
||||
/// <seealso cref="NetFakeLagMin"/>
|
||||
public static readonly CVarDef<float> NetFakeLagRand = CVarDef.Create("net.fakelagrand", 0f, CVar.CHEAT);
|
||||
|
||||
/// <summary>
|
||||
/// Add random fake duplicates to all outgoing UDP network packets, as a ratio of how many packets to duplicate.
|
||||
/// 0 = no packets duplicated, 1 = all packets duplicated.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetFakeDuplicates = CVarDef.Create("net.fakeduplicates", 0f, CVar.CHEAT);
|
||||
#endif
|
||||
|
||||
/*
|
||||
* METRICS
|
||||
*/
|
||||
@@ -426,12 +432,30 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<string> BuildDownloadUrl =
|
||||
CVarDef.Create("build.download_url", string.Empty, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// URL of the content manifest the launcher should download to connect to this server.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> BuildManifestUrl =
|
||||
CVarDef.Create("build.manifest_url", string.Empty, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// URL at which the launcher can download the manifest game files.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> BuildManifestDownloadUrl =
|
||||
CVarDef.Create("build.manifest_download_url", string.Empty, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of the content pack hosted at <c>build.download_url</c>
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> BuildHash =
|
||||
CVarDef.Create("build.hash", "", CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of the manifest hosted at <c>build.manifest_url</c>
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> BuildManifestHash =
|
||||
CVarDef.Create("build.manifest_hash", "", CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* WATCHDOG
|
||||
*/
|
||||
@@ -1072,5 +1096,67 @@ namespace Robust.Shared
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> HubAdvertiseInterval =
|
||||
CVarDef.Create("hub.advertise_interval", 120, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* ACZ
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use stream compression instead of per-file compression when transmitting ACZ data.
|
||||
/// Enabling stream compression significantly reduces bandwidth usage of downloads,
|
||||
/// but increases server and launcher CPU load. It also makes final files stored on the client compressed less.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> AczStreamCompress =
|
||||
CVarDef.Create("acz.stream_compress", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// ZSTD Compression level to use when doing ACZ stream compressed.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> AczStreamCompressLevel =
|
||||
CVarDef.Create("acz.stream_compress_level", 3, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to do compression on individual files for ACZ downloads.
|
||||
/// Automatically forced off if stream compression is enabled.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> AczBlobCompress =
|
||||
CVarDef.Create("acz.blob_compress", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// ZSTD Compression level to use for individual file compression.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> AczBlobCompressLevel =
|
||||
CVarDef.Create("acz.blob_compress_level", 14, CVar.SERVERONLY);
|
||||
|
||||
// Could consider using a ratio for this?
|
||||
/// <summary>
|
||||
/// Amount of bytes that need to be saved by compression for the compression to be "worth it".
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> AczBlobCompressSaveThreshold =
|
||||
CVarDef.Create("acz.blob_compress_save_threshold", 14, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to ZSTD compress the ACZ manifest.
|
||||
/// If this is enabled (the default) then non-compressed manifest requests will be decompressed live.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> AczManifestCompress =
|
||||
CVarDef.Create("acz.manifest_compress", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Compression level for ACZ manifest compression.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> AczManifestCompressLevel =
|
||||
CVarDef.Create("acz.manifest_compress_level", 14, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* THREAD
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// The nominal parallel processing count to use for parallelized operations.
|
||||
/// The default of 0 automatically selects the system's processor count.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ThreadParallelCount =
|
||||
CVarDef.Create("thread.parallel_count", 0);
|
||||
}
|
||||
}
|
||||
|
||||
148
Robust.Shared/Collections/OverflowQueue.cs
Normal file
148
Robust.Shared/Collections/OverflowQueue.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Robust.Shared.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// A fixed-size queue that discards the oldest entry if a new entry is enqueued when the queue is full.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public sealed class OverflowQueue<T>
|
||||
{
|
||||
private readonly T[] _queue;
|
||||
private int _currentIdx = 0;
|
||||
private int _length = 0;
|
||||
/// <summary>
|
||||
/// The size of the queue-buffer.
|
||||
/// </summary>
|
||||
public int Size => _queue.Length;
|
||||
|
||||
/// <param name="size">size of the queue-buffer</param>
|
||||
public OverflowQueue(int size)
|
||||
{
|
||||
_queue = new T[size];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues the <paramref name="item"/>. Overrides the oldest item if the queue is full.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to enqueue</param>
|
||||
public void Enqueue(T item)
|
||||
{
|
||||
_queue[_currentIdx++] = item;
|
||||
if(_length < Size)
|
||||
_length++;
|
||||
|
||||
if (_currentIdx == Size)
|
||||
{
|
||||
_currentIdx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the item at the head of the queue and returns it. If the queue is empty, this method throws an InvalidOperationException.
|
||||
/// </summary>
|
||||
/// <returns>The dequeued item.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the queue is empty.</exception>
|
||||
public T Dequeue()
|
||||
{
|
||||
if (!TryDequeue(out var item))
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(OverflowQueue<T>)} has no more items to dequeue.");
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to dequeue an item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item which got dequeued. Null if the queue was empty</param>
|
||||
/// <returns>True if an item was dequeued, false if not.</returns>
|
||||
public bool TryDequeue([NotNullWhen(true)] out T? item)
|
||||
{
|
||||
if (_length == 0)
|
||||
{
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
item = _queue[GetCurrentIndex()]!;
|
||||
_length--;
|
||||
return true;
|
||||
}
|
||||
|
||||
private int GetCurrentIndex()
|
||||
{
|
||||
Debug.Assert(_length > 0);
|
||||
var idx = _currentIdx - _length;
|
||||
if (idx < 0)
|
||||
{
|
||||
return idx + Size;
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the item at the head of the queue. The object remains in the queue. If the queue is empty, this method throws an InvalidOperationException.
|
||||
/// </summary>
|
||||
/// <returns>The item at the head of the queue.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the queue is empty.</exception>
|
||||
public T Peek()
|
||||
{
|
||||
if (_length == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(OverflowQueue<T>)} has no more items to dequeue.");
|
||||
}
|
||||
|
||||
return _queue[GetCurrentIndex()];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the queue contains at least one object equal to item. Equality is determined using EqualityComparer<T>.Default.Equals().
|
||||
/// </summary>
|
||||
/// <param name="item">Item to look for.</param>
|
||||
/// <returns>True if the queue contains the item, false if not.</returns>
|
||||
public bool Contains(T item)
|
||||
{
|
||||
for (int i = 0; i < _length; i++)
|
||||
{
|
||||
var actualIndex = _currentIdx + i;
|
||||
if (actualIndex >= _length)
|
||||
actualIndex -= _length;
|
||||
if (EqualityComparer<T>.Default.Equals(item, _queue[actualIndex])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the queue contents first to last as an array.
|
||||
/// </summary>
|
||||
/// <returns>The array containing the queue contents.</returns>
|
||||
public T[] ToArray()
|
||||
{
|
||||
if (_length == 0)
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
var res = new T[_length];
|
||||
|
||||
var startIdx = _currentIdx - _length;
|
||||
if (startIdx < 0)
|
||||
{
|
||||
Array.Copy(_queue, startIdx + Size, res, 0, -1 * startIdx);
|
||||
Array.Copy(_queue, 0, res, -1 * startIdx, startIdx + Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_queue, startIdx, res, 0, _length);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Nett;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Utility.Collections;
|
||||
|
||||
namespace Robust.Shared.Configuration
|
||||
{
|
||||
@@ -21,6 +23,8 @@ namespace Robust.Shared.Configuration
|
||||
private string? _configFile;
|
||||
protected bool _isServer;
|
||||
|
||||
protected readonly ReaderWriterLockSlim Lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new ConfigurationManager.
|
||||
/// </summary>
|
||||
@@ -35,6 +39,8 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
public virtual void Shutdown()
|
||||
{
|
||||
using var _ = Lock.WriteGuard();
|
||||
|
||||
_configVars.Clear();
|
||||
_configFile = null;
|
||||
}
|
||||
@@ -46,7 +52,18 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
var tblRoot = Toml.ReadFile(configFile);
|
||||
|
||||
ProcessTomlObject(tblRoot);
|
||||
var callbackEvents = new ValueList<ValueChangedInvoke>();
|
||||
|
||||
// Ensure callbacks are raised OUTSIDE the write lock.
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
ProcessTomlObject(tblRoot, ref callbackEvents);
|
||||
}
|
||||
|
||||
foreach (var callback in callbackEvents)
|
||||
{
|
||||
InvokeValueChanged(callback);
|
||||
}
|
||||
|
||||
_configFile = configFile;
|
||||
Logger.InfoS("cfg", $"Configuration Loaded from '{Path.GetFullPath(configFile)}'");
|
||||
@@ -66,8 +83,12 @@ namespace Robust.Shared.Configuration
|
||||
/// A recursive function that walks over the config tree, transforming all key nodes into CVars.
|
||||
/// </summary>
|
||||
/// <param name="obj">The root table of the TOML document.</param>
|
||||
/// <param name="changedInvokes">List of CVars that will need to have their InvokeValueChanged ran.</param>
|
||||
/// <param name="tablePath">For internal use only, the current path to the node.</param>
|
||||
private void ProcessTomlObject(TomlObject obj, string tablePath = "")
|
||||
private void ProcessTomlObject(
|
||||
TomlObject obj,
|
||||
ref ValueList<ValueChangedInvoke> changedInvokes,
|
||||
string tablePath = "")
|
||||
{
|
||||
if (obj is TomlTable table) // this is a table
|
||||
{
|
||||
@@ -80,7 +101,7 @@ namespace Robust.Shared.Configuration
|
||||
else
|
||||
newPath = tablePath + kvTml.Key;
|
||||
|
||||
ProcessTomlObject(kvTml.Value, newPath);
|
||||
ProcessTomlObject(kvTml.Value, ref changedInvokes, newPath);
|
||||
}
|
||||
}
|
||||
else // this is a key, add CVar
|
||||
@@ -91,7 +112,8 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
// overwrite the value with the saved one
|
||||
cfgVar.Value = tomlValue;
|
||||
InvokeValueChanged(cfgVar, cfgVar.Value);
|
||||
if (SetupInvokeValueChanged(cfgVar, tomlValue) is { } invoke)
|
||||
changedInvokes.Add(invoke);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -118,73 +140,76 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
var tblRoot = Toml.Create();
|
||||
|
||||
foreach (var (name, cVar) in _configVars)
|
||||
using (Lock.ReadGuard())
|
||||
{
|
||||
var value = cVar.Value;
|
||||
if (value == null && cVar.Registered)
|
||||
foreach (var (name, cVar) in _configVars)
|
||||
{
|
||||
value = cVar.DefaultValue;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
Logger.ErrorS("cfg",
|
||||
$"CVar {name} has no value or default value, was the default value registered as null?");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't write if Archive flag is not set.
|
||||
// Don't write if the cVar is the default value.
|
||||
if (!cVar.ConfigModified &&
|
||||
(cVar.Flags & CVar.ARCHIVE) == 0 || value.Equals(cVar.DefaultValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var keyIndex = name.LastIndexOf(TABLE_DELIMITER);
|
||||
var tblPath = name.Substring(0, keyIndex).Split(TABLE_DELIMITER);
|
||||
var keyName = name.Substring(keyIndex + 1);
|
||||
|
||||
// locate the Table in the config tree
|
||||
var table = tblRoot;
|
||||
foreach (var curTblName in tblPath)
|
||||
{
|
||||
if (!table.TryGetValue(curTblName, out TomlObject tblObject))
|
||||
var value = cVar.Value;
|
||||
if (value == null && cVar.Registered)
|
||||
{
|
||||
tblObject = table.Add(curTblName, new Dictionary<string, TomlObject>()).Added;
|
||||
value = cVar.DefaultValue;
|
||||
}
|
||||
|
||||
table = tblObject as TomlTable ?? throw new InvalidConfigurationException(
|
||||
$"[CFG] Object {curTblName} is being used like a table, but it is a {tblObject}. Are your CVar names formed properly?");
|
||||
}
|
||||
if (value == null)
|
||||
{
|
||||
Logger.ErrorS("cfg",
|
||||
$"CVar {name} has no value or default value, was the default value registered as null?");
|
||||
continue;
|
||||
}
|
||||
|
||||
//runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯
|
||||
switch (value)
|
||||
{
|
||||
case Enum val:
|
||||
table.Add(keyName, (int) (object) val); // asserts Enum value != (ulong || long)
|
||||
break;
|
||||
case int val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case long val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case bool val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case string val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case float val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case double val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
default:
|
||||
Logger.WarningS("cfg", $"Cannot serialize '{name}', unsupported type.");
|
||||
break;
|
||||
// Don't write if Archive flag is not set.
|
||||
// Don't write if the cVar is the default value.
|
||||
if (!cVar.ConfigModified &&
|
||||
(cVar.Flags & CVar.ARCHIVE) == 0 || value.Equals(cVar.DefaultValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var keyIndex = name.LastIndexOf(TABLE_DELIMITER);
|
||||
var tblPath = name.Substring(0, keyIndex).Split(TABLE_DELIMITER);
|
||||
var keyName = name.Substring(keyIndex + 1);
|
||||
|
||||
// locate the Table in the config tree
|
||||
var table = tblRoot;
|
||||
foreach (var curTblName in tblPath)
|
||||
{
|
||||
if (!table.TryGetValue(curTblName, out TomlObject tblObject))
|
||||
{
|
||||
tblObject = table.Add(curTblName, new Dictionary<string, TomlObject>()).Added;
|
||||
}
|
||||
|
||||
table = tblObject as TomlTable ?? throw new InvalidConfigurationException(
|
||||
$"[CFG] Object {curTblName} is being used like a table, but it is a {tblObject}. Are your CVar names formed properly?");
|
||||
}
|
||||
|
||||
//runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯
|
||||
switch (value)
|
||||
{
|
||||
case Enum val:
|
||||
table.Add(keyName, (int)(object)val); // asserts Enum value != (ulong || long)
|
||||
break;
|
||||
case int val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case long val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case bool val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case string val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case float val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
case double val:
|
||||
table.Add(keyName, val);
|
||||
break;
|
||||
default:
|
||||
Logger.WarningS("cfg", $"Cannot serialize '{name}', unsupported type.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +245,8 @@ namespace Robust.Shared.Configuration
|
||||
return;
|
||||
}
|
||||
|
||||
using var _ = Lock.WriteGuard();
|
||||
|
||||
if (_configVars.TryGetValue(name, out var cVar))
|
||||
{
|
||||
if (cVar.Registered)
|
||||
@@ -253,12 +280,16 @@ namespace Robust.Shared.Configuration
|
||||
public void OnValueChanged<T>(string name, Action<T> onValueChanged, bool invokeImmediately = false)
|
||||
where T : notnull
|
||||
{
|
||||
var reg = _configVars[name];
|
||||
var exDel = (Action<T>?) reg.ValueChanged;
|
||||
exDel += onValueChanged;
|
||||
reg.ValueChanged = exDel;
|
||||
|
||||
reg.ValueChangedInvoker ??= (del, v) => ((Action<T>) del)((T) v);
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
var reg = _configVars[name];
|
||||
var exDel = (Action<T>?) reg.ValueChanged;
|
||||
exDel += onValueChanged;
|
||||
reg.ValueChanged = exDel;
|
||||
|
||||
reg.ValueChangedInvoker ??= (del, v) => ((Action<T>) del)((T) v);
|
||||
}
|
||||
|
||||
if (invokeImmediately)
|
||||
{
|
||||
@@ -273,6 +304,8 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
public void UnsubValueChanged<T>(string name, Action<T> onValueChanged) where T : notnull
|
||||
{
|
||||
using var _ = Lock.WriteGuard();
|
||||
|
||||
var reg = _configVars[name];
|
||||
var exDel = (Action<T>?) reg.ValueChanged;
|
||||
exDel -= onValueChanged;
|
||||
@@ -315,13 +348,17 @@ namespace Robust.Shared.Configuration
|
||||
/// <inheritdoc />
|
||||
public bool IsCVarRegistered(string name)
|
||||
{
|
||||
using var _ = Lock.ReadGuard();
|
||||
return _configVars.TryGetValue(name, out var cVar) && cVar.Registered;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetRegisteredCVars()
|
||||
{
|
||||
return _configVars.Select(p => p.Key);
|
||||
using var _ = Lock.ReadGuard();
|
||||
// Have to .ToArray() so the lock is held for the whole iteration operation.
|
||||
// This function is only currently used for the cvar ? command so I'm not too worried.
|
||||
return _configVars.Select(p => p.Key).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -332,21 +369,29 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
private void SetCVarInternal(string name, object value)
|
||||
{
|
||||
//TODO: Make flags work, required non-derpy net system.
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
|
||||
{
|
||||
if (!Equals(cVar.OverrideValueParsed ?? cVar.Value, value))
|
||||
{
|
||||
// Setting an overriden var just turns off the override, basically.
|
||||
cVar.OverrideValue = null;
|
||||
cVar.OverrideValueParsed = null;
|
||||
ValueChangedInvoke? invoke = null;
|
||||
|
||||
cVar.Value = value;
|
||||
InvokeValueChanged(cVar, value);
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
//TODO: Make flags work, required non-derpy net system.
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
|
||||
{
|
||||
if (!Equals(cVar.OverrideValueParsed ?? cVar.Value, value))
|
||||
{
|
||||
// Setting an overriden var just turns off the override, basically.
|
||||
cVar.OverrideValue = null;
|
||||
cVar.OverrideValueParsed = null;
|
||||
|
||||
cVar.Value = value;
|
||||
invoke = SetupInvokeValueChanged(cVar, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
|
||||
}
|
||||
else
|
||||
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
|
||||
|
||||
if (invoke != null)
|
||||
InvokeValueChanged(invoke.Value);
|
||||
}
|
||||
|
||||
public void SetCVar<T>(CVarDef<T> def, T value) where T : notnull
|
||||
@@ -357,6 +402,7 @@ namespace Robust.Shared.Configuration
|
||||
/// <inheritdoc />
|
||||
public T GetCVar<T>(string name)
|
||||
{
|
||||
using var _ = Lock.ReadGuard();
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
|
||||
//TODO: Make flags work, required non-derpy net system.
|
||||
return (T) (GetConfigVarValue(cVar))!;
|
||||
@@ -371,6 +417,7 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
public Type GetCVarType(string name)
|
||||
{
|
||||
using var _ = Lock.ReadGuard();
|
||||
if (!_configVars.TryGetValue(name, out var cVar) || !cVar.Registered)
|
||||
{
|
||||
throw new InvalidConfigurationException($"Trying to get type of unregistered variable '{name}'");
|
||||
@@ -387,24 +434,35 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
public void OverrideConVars(IEnumerable<(string key, string value)> cVars)
|
||||
{
|
||||
foreach (var (key, value) in cVars)
|
||||
var invokes = new ValueList<ValueChangedInvoke>();
|
||||
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
if (_configVars.TryGetValue(key, out var cfgVar))
|
||||
foreach (var (key, value) in cVars)
|
||||
{
|
||||
cfgVar.OverrideValue = value;
|
||||
if (cfgVar.Registered)
|
||||
if (_configVars.TryGetValue(key, out var cfgVar))
|
||||
{
|
||||
cfgVar.OverrideValueParsed = ParseOverrideValue(value, cfgVar.DefaultValue?.GetType());
|
||||
InvokeValueChanged(cfgVar, cfgVar.OverrideValueParsed);
|
||||
cfgVar.OverrideValue = value;
|
||||
if (cfgVar.Registered)
|
||||
{
|
||||
cfgVar.OverrideValueParsed = ParseOverrideValue(value, cfgVar.DefaultValue?.GetType());
|
||||
if (SetupInvokeValueChanged(cfgVar, cfgVar.OverrideValueParsed) is { } invoke)
|
||||
invokes.Add(invoke);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
var cVar = new ConfigVar(key, 0, CVar.NONE) {OverrideValue = value};
|
||||
_configVars.Add(key, cVar);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
//Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered.
|
||||
var cVar = new ConfigVar(key, 0, CVar.NONE) {OverrideValue = value};
|
||||
_configVars.Add(key, cVar);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var invoke in invokes)
|
||||
{
|
||||
InvokeValueChanged(invoke);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,9 +519,17 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
private static void InvokeValueChanged(ConfigVar var, object value)
|
||||
private static void InvokeValueChanged(ValueChangedInvoke invoke)
|
||||
{
|
||||
var.ValueChangedInvoker?.Invoke(var.ValueChanged!, value);
|
||||
invoke.Invoker.Invoke(invoke.ValueChanged, invoke.Value);
|
||||
}
|
||||
|
||||
private static ValueChangedInvoke? SetupInvokeValueChanged(ConfigVar var, object value)
|
||||
{
|
||||
if (var.ValueChangedInvoker == null)
|
||||
return null;
|
||||
|
||||
return new ValueChangedInvoke(var.ValueChangedInvoker, var.ValueChanged!, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -530,6 +596,14 @@ namespace Robust.Shared.Configuration
|
||||
public string? OverrideValue { get; set; }
|
||||
public object? OverrideValueParsed { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All data we need to invoke a deferred ValueChanged handler outside of a write lock.
|
||||
/// </summary>
|
||||
private record struct ValueChangedInvoke(
|
||||
Action<Delegate, object> Invoker,
|
||||
Delegate ValueChanged,
|
||||
object Value);
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -6,6 +6,13 @@ namespace Robust.Shared.Configuration
|
||||
/// <summary>
|
||||
/// Stores and manages global configuration variables.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Accessing (getting/setting) main CVars is thread safe.
|
||||
/// Note that value-changed callbacks are ran synchronously from the thread using <see cref="SetCVar"/>,
|
||||
/// so it is not recommended to modify CVars from other threads.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IConfigurationManager
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -190,6 +190,8 @@ namespace Robust.Shared.Configuration
|
||||
return;
|
||||
}
|
||||
|
||||
using var _ = Lock.ReadGuard();
|
||||
|
||||
foreach (var (name, value) in networkedVars)
|
||||
{
|
||||
if (!_configVars.TryGetValue(name, out var cVar))
|
||||
@@ -219,6 +221,8 @@ namespace Robust.Shared.Configuration
|
||||
/// <inheritdoc />
|
||||
public T GetClientCVar<T>(INetChannel channel, string name)
|
||||
{
|
||||
using var _ = Lock.ReadGuard();
|
||||
|
||||
if (!_configVars.TryGetValue(name, out var cVar) || !cVar.Registered)
|
||||
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
|
||||
|
||||
@@ -233,43 +237,46 @@ namespace Robust.Shared.Configuration
|
||||
/// <inheritdoc />
|
||||
public override void SetCVar(string name, object value)
|
||||
{
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
|
||||
CVar flags;
|
||||
using (Lock.ReadGuard())
|
||||
{
|
||||
if (_netManager.IsClient)
|
||||
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
|
||||
{
|
||||
if (_netManager.IsConnected)
|
||||
flags = cVar.Flags;
|
||||
if (_netManager.IsClient)
|
||||
{
|
||||
if ((cVar.Flags & CVar.NOT_CONNECTED) != 0)
|
||||
if (_netManager.IsConnected)
|
||||
{
|
||||
Logger.WarningS("cfg", $"'{name}' can only be changed when not connected to a server.");
|
||||
if ((cVar.Flags & CVar.NOT_CONNECTED) != 0)
|
||||
{
|
||||
Logger.WarningS("cfg", $"'{name}' can only be changed when not connected to a server.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((cVar.Flags & CVar.SERVER) != 0)
|
||||
{
|
||||
Logger.WarningS("cfg", $"Only the server can change '{name}'.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((cVar.Flags & CVar.SERVER) != 0)
|
||||
{
|
||||
Logger.WarningS("cfg", $"Only the server can change '{name}'.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
|
||||
else
|
||||
{
|
||||
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Actually set the CVar
|
||||
base.SetCVar(name, value);
|
||||
|
||||
var cvar = _configVars[name];
|
||||
if ((flags & CVar.REPLICATED) == 0)
|
||||
return;
|
||||
|
||||
// replicate if needed
|
||||
if (_netManager.IsClient)
|
||||
{
|
||||
if ((cvar.Flags & CVar.REPLICATED) == 0)
|
||||
return;
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
var msg = new MsgConVars();
|
||||
msg.Tick = _timing.CurTick;
|
||||
msg.NetworkedVars = new List<(string name, object value)>
|
||||
{
|
||||
@@ -279,10 +286,7 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
else // Server
|
||||
{
|
||||
if ((cvar.Flags & CVar.REPLICATED) == 0)
|
||||
return;
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
var msg = new MsgConVars();
|
||||
msg.Tick = _timing.CurTick;
|
||||
msg.NetworkedVars = new List<(string name, object value)>
|
||||
{
|
||||
@@ -300,7 +304,7 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
Logger.InfoS("cfg", $"{client}: Sending server info...");
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
var msg = new MsgConVars();
|
||||
msg.Tick = _timing.CurTick;
|
||||
msg.NetworkedVars = GetReplicatedVars();
|
||||
_netManager.ServerSendMessage(msg, client);
|
||||
@@ -314,7 +318,7 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
Logger.InfoS("cfg", "Sending client info...");
|
||||
|
||||
var msg = _netManager.CreateNetMessage<MsgConVars>();
|
||||
var msg = new MsgConVars();
|
||||
msg.Tick = default;
|
||||
msg.NetworkedVars = GetReplicatedVars();
|
||||
_netManager.ClientSendMessage(msg);
|
||||
@@ -327,6 +331,8 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
private List<(string name, object value)> GetReplicatedVars()
|
||||
{
|
||||
using var _ = Lock.ReadGuard();
|
||||
|
||||
var nwVars = new List<(string name, object value)>();
|
||||
|
||||
foreach (var cVar in _configVars.Values)
|
||||
|
||||
@@ -193,6 +193,10 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
_sawmill.Debug($"Unmanaged methods... {fullStopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
CheckNoTypeAbuse(reader, errors);
|
||||
|
||||
_sawmill.Debug($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
CheckMemberReferences(loadedConfig, members, errors);
|
||||
|
||||
foreach (var error in errors)
|
||||
@@ -311,6 +315,27 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag<SandboxError> errors)
|
||||
{
|
||||
foreach (var typeDefHandle in reader.TypeDefinitions)
|
||||
{
|
||||
var typeDef = reader.GetTypeDefinition(typeDefHandle);
|
||||
if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) != 0)
|
||||
{
|
||||
// The C# compiler emits explicit layout types for some array init logic. These have no fields.
|
||||
// Only ban explicit layout if it has fields.
|
||||
|
||||
var type = GetTypeFromDefinition(reader, typeDefHandle);
|
||||
|
||||
if (typeDef.GetFields().Count > 0)
|
||||
{
|
||||
var err = $"Explicit layout type {type} may not have fields.";
|
||||
errors.Add(new SandboxError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckMemberReferences(
|
||||
SandboxConfig sandboxConfig,
|
||||
List<MMemberRef> members,
|
||||
@@ -746,7 +771,7 @@ namespace Robust.Shared.ContentPack
|
||||
/// <exception href="UnsupportedMetadataException">
|
||||
/// Thrown if the metadata does something funny we don't "support" like type forwarding.
|
||||
/// </exception>
|
||||
private static MTypeReferenced ParseTypeReference(MetadataReader reader, TypeReferenceHandle handle)
|
||||
internal static MTypeReferenced ParseTypeReference(MetadataReader reader, TypeReferenceHandle handle)
|
||||
{
|
||||
var typeRef = reader.GetTypeReference(handle);
|
||||
var name = reader.GetString(typeRef.Name);
|
||||
@@ -883,7 +908,7 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TypeProvider : ISignatureTypeProvider<MType, int>
|
||||
internal sealed class TypeProvider : ISignatureTypeProvider<MType, int>
|
||||
{
|
||||
public MType GetSZArrayType(MType elementType)
|
||||
{
|
||||
|
||||
@@ -90,7 +90,11 @@ namespace Robust.Shared.ContentPack
|
||||
Logger.DebugS("res.mod", $"Found module '{fullPath}'");
|
||||
|
||||
using var asmFile = _res.ContentFileRead(fullPath);
|
||||
var (asmRefs, asmName) = GetAssemblyReferenceData(asmFile);
|
||||
var refData = GetAssemblyReferenceData(asmFile);
|
||||
if (refData == null)
|
||||
continue;
|
||||
|
||||
var (asmRefs, asmName) = refData.Value;
|
||||
|
||||
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
|
||||
{
|
||||
@@ -158,18 +162,48 @@ namespace Robust.Shared.ContentPack
|
||||
return true;
|
||||
}
|
||||
|
||||
private static (string[] refs, string name) GetAssemblyReferenceData(Stream stream)
|
||||
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
|
||||
{
|
||||
using var reader = ModLoader.MakePEReader(stream);
|
||||
var metaReader = reader.GetMetadataReader();
|
||||
|
||||
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);
|
||||
|
||||
// Try to find SkipIfSandboxedAttribute.
|
||||
|
||||
if (_sandboxingEnabled && TryFindSkipIfSandboxed(metaReader))
|
||||
{
|
||||
Logger.DebugS("res.mod", "Module {ModuleName} has SkipIfSandboxedAttribute, ignoring.", name);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (metaReader.AssemblyReferences
|
||||
.Select(a => metaReader.GetAssemblyReference(a))
|
||||
.Select(a => metaReader.GetString(a.Name)).ToArray(), name);
|
||||
}
|
||||
|
||||
private static bool TryFindSkipIfSandboxed(MetadataReader reader)
|
||||
{
|
||||
foreach (var attribHandle in reader.CustomAttributes)
|
||||
{
|
||||
var attrib = reader.GetCustomAttribute(attribHandle);
|
||||
if (attrib.Parent.Kind != HandleKind.AssemblyDefinition)
|
||||
continue;
|
||||
|
||||
var ctor = attrib.Constructor;
|
||||
if (ctor.Kind != HandleKind.MemberReference)
|
||||
continue;
|
||||
|
||||
var memberRef = reader.GetMemberReference((MemberReferenceHandle) ctor);
|
||||
var typeRef = AssemblyTypeChecker.ParseTypeReference(reader, (TypeReferenceHandle)memberRef.Parent);
|
||||
|
||||
if (typeRef.Namespace == "Robust.Shared.ContentPack" && typeRef.Name == "SkipIfSandboxedAttribute")
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LoadGameAssembly(Stream assembly, Stream? symbols = null, bool skipVerify = false)
|
||||
{
|
||||
if (!skipVerify && !MakeTypeChecker().CheckAssembly(assembly))
|
||||
@@ -329,7 +363,7 @@ namespace Robust.Shared.ContentPack
|
||||
public void Dispose()
|
||||
{
|
||||
_loadContext.Unload();
|
||||
AssemblyLoadContext.Default.Resolving += DefaultOnResolving;
|
||||
AssemblyLoadContext.Default.Resolving -= DefaultOnResolving;
|
||||
}
|
||||
|
||||
private Assembly? DefaultOnResolving(AssemblyLoadContext ctx, AssemblyName name)
|
||||
|
||||
18
Robust.Shared/ContentPack/SkipIfSandboxedAttribute.cs
Normal file
18
Robust.Shared/ContentPack/SkipIfSandboxedAttribute.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.ContentPack;
|
||||
|
||||
/// <summary>
|
||||
/// If defined on an assembly, causes Robust to skip trying to load this assembly if running on sandboxed mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you have some sandbox-unsafe code you want to optionally enable for your project,
|
||||
/// you can put the code in a leaf project,
|
||||
/// tag the assembly with this attribute,
|
||||
/// and then detect at runtime whether the assembly was loaded to enable these features.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class SkipIfSandboxedAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
/// <summary>
|
||||
/// Increases the life stage from <see cref="ComponentLifeStage.PreAdd" /> to <see cref="ComponentLifeStage.Added" />,
|
||||
/// calling <see cref="OnAdd" />.
|
||||
/// after raising a <see cref="ComponentAdd"/> event.
|
||||
/// </summary>
|
||||
internal void LifeAddToEntity(IEntityManager entManager)
|
||||
{
|
||||
@@ -44,14 +44,7 @@ namespace Robust.Shared.GameObjects
|
||||
LifeStage = ComponentLifeStage.Adding;
|
||||
CreationTick = entManager.CurrentTick;
|
||||
entManager.EventBus.RaiseComponentEvent(this, CompAddInstance);
|
||||
OnAdd();
|
||||
|
||||
#if DEBUG
|
||||
if (LifeStage != ComponentLifeStage.Added)
|
||||
{
|
||||
DebugTools.Assert($"Component {this.GetType().Name} did not call base {nameof(OnAdd)} in derived method.");
|
||||
}
|
||||
#endif
|
||||
LifeStage = ComponentLifeStage.Added;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -166,14 +159,6 @@ namespace Robust.Shared.GameObjects
|
||||
private static readonly ComponentShutdown CompShutdownInstance = new();
|
||||
private static readonly ComponentRemove CompRemoveInstance = new();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the component gets added to an entity.
|
||||
/// </summary>
|
||||
protected virtual void OnAdd()
|
||||
{
|
||||
LifeStage = ComponentLifeStage.Added;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when all of the entity's other components have been added and are available,
|
||||
/// But are not necessarily initialized yet. DO NOT depend on the values of other components to be correct.
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
internal LinkedList<Contact> Contacts = new();
|
||||
|
||||
[DataField("ignorePaused"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool IgnorePaused { get; set; }
|
||||
|
||||
internal SharedPhysicsMapComponent? PhysicsMap { get; set; }
|
||||
@@ -113,7 +114,8 @@ namespace Robust.Shared.GameObjects
|
||||
_angularVelocity = 0.0f;
|
||||
// SynchronizeFixtures(); TODO: When CCD
|
||||
}
|
||||
else
|
||||
// Even if it's dynamic if it can't collide then don't force it awake.
|
||||
else if (_canCollide)
|
||||
{
|
||||
SetAwake(true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
// If you wanna use these, add it to some random prototype.
|
||||
@@ -11,24 +9,15 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Throws an exception in <see cref="OnAdd" />.
|
||||
/// </summary>
|
||||
public sealed class DebugExceptionOnAddComponent : Component
|
||||
{
|
||||
protected override void OnAdd() => throw new NotSupportedException();
|
||||
}
|
||||
public sealed class DebugExceptionOnAddComponent : Component { }
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception in <see cref="Initialize" />.
|
||||
/// </summary>
|
||||
public sealed class DebugExceptionInitializeComponent : Component
|
||||
{
|
||||
protected override void Initialize() => throw new NotSupportedException();
|
||||
}
|
||||
public sealed class DebugExceptionInitializeComponent : Component { }
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception in <see cref="Startup" />.
|
||||
/// </summary>
|
||||
public sealed class DebugExceptionStartupComponent : Component
|
||||
{
|
||||
protected override void Startup() => throw new NotSupportedException();
|
||||
}
|
||||
public sealed class DebugExceptionStartupComponent : Component { }
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class IgnorePauseComponent : Component
|
||||
{
|
||||
protected override void OnAdd()
|
||||
{
|
||||
base.OnAdd();
|
||||
IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(Owner).EntityPaused = false;
|
||||
}
|
||||
|
||||
protected override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
if (IoCManager.Resolve<IMapManager>().IsMapPaused(entMan.GetComponent<TransformComponent>(Owner).MapID))
|
||||
{
|
||||
entMan.GetComponent<MetaDataComponent>(Owner).EntityPaused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,12 +151,8 @@ namespace Robust.Shared.GameObjects
|
||||
if (_entityPaused == value)
|
||||
return;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
if (value && entMan.HasComponent<IgnorePauseComponent>(Owner))
|
||||
return;
|
||||
|
||||
_entityPaused = value;
|
||||
entMan.EventBus.RaiseLocalEvent(Owner, new EntityPausedEvent(Owner, value));
|
||||
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner, new EntityPausedEvent(Owner, value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,23 +26,20 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
[DataField("parent")]
|
||||
private EntityUid _parent;
|
||||
[DataField("pos")]
|
||||
private Vector2 _localPosition = Vector2.Zero; // holds offset from grid, or offset from parent
|
||||
[DataField("rot")]
|
||||
private Angle _localRotation; // local rotation
|
||||
[DataField("noRot")]
|
||||
private bool _noLocalRotation;
|
||||
[DataField("pos")] internal Vector2 _localPosition = Vector2.Zero; // holds offset from grid, or offset from parent
|
||||
[DataField("rot")] internal Angle _localRotation; // local rotation
|
||||
[DataField("noRot")] internal bool _noLocalRotation;
|
||||
[DataField("anchored")]
|
||||
private bool _anchored;
|
||||
|
||||
private Matrix3 _localMatrix = Matrix3.Identity;
|
||||
private Matrix3 _invLocalMatrix = Matrix3.Identity;
|
||||
|
||||
private Vector2? _nextPosition;
|
||||
private Angle? _nextRotation;
|
||||
internal Vector2? _nextPosition;
|
||||
internal Angle? _nextRotation;
|
||||
|
||||
private Vector2 _prevPosition;
|
||||
private Angle _prevRotation;
|
||||
internal Vector2 _prevPosition;
|
||||
internal Angle _prevRotation;
|
||||
|
||||
// Cache changes so we can distribute them after physics is done (better cache)
|
||||
private EntityCoordinates? _oldCoords;
|
||||
@@ -365,7 +362,7 @@ namespace Robust.Shared.GameObjects
|
||||
// Cache new GridID before raising the event.
|
||||
GridID = GetGridIndex(xformQuery);
|
||||
|
||||
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent?.Owner, oldMapId);
|
||||
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent?.Owner, oldMapId, this);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref entParentChangedMessage);
|
||||
}
|
||||
|
||||
@@ -531,7 +528,7 @@ namespace Robust.Shared.GameObjects
|
||||
[ViewVariables] internal Vector2 LerpSource => _prevPosition;
|
||||
[ViewVariables] internal Angle LerpSourceAngle => _prevRotation;
|
||||
|
||||
[ViewVariables] internal EntityUid LerpParent { get; private set; }
|
||||
[ViewVariables] internal EntityUid LerpParent { get; set; }
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
@@ -710,8 +707,11 @@ namespace Robust.Shared.GameObjects
|
||||
public void DetachParentToNull()
|
||||
{
|
||||
var oldParent = _parent;
|
||||
|
||||
// Even though they may already be in nullspace we may want to deparent them anyway
|
||||
if (!oldParent.IsValid())
|
||||
{
|
||||
DebugTools.Assert(!Anchored);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -734,8 +734,11 @@ namespace Robust.Shared.GameObjects
|
||||
oldConcrete._children.Remove(uid);
|
||||
|
||||
_parent = EntityUid.Invalid;
|
||||
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent, MapID);
|
||||
var oldMap = MapID;
|
||||
MapID = MapId.Nullspace;
|
||||
GridID = GridId.Invalid;
|
||||
|
||||
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent, oldMap, this);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref entParentChangedMessage);
|
||||
|
||||
// Does it even make sense to call these since this is called purely from OnRemove right now?
|
||||
@@ -808,19 +811,6 @@ namespace Robust.Shared.GameObjects
|
||||
AttachParent(transform);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the transform of the entity located on the map itself
|
||||
/// </summary>
|
||||
public TransformComponent GetMapTransform()
|
||||
{
|
||||
if (Parent != null) //If we are not the final transform, query up the chain of parents
|
||||
{
|
||||
return Parent.GetMapTransform();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the WorldPosition and WorldRotation of this entity faster than each individually.
|
||||
/// </summary>
|
||||
@@ -953,104 +943,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new TransformComponentState(_localPosition, LocalRotation, _parent, _noLocalRotation, _anchored);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
if (curState != null)
|
||||
{
|
||||
var newState = (TransformComponentState) curState;
|
||||
|
||||
var newParentId = newState.ParentID;
|
||||
var rebuildMatrices = false;
|
||||
if (Parent?.Owner != newParentId)
|
||||
{
|
||||
if (newParentId != _parent)
|
||||
{
|
||||
if (!newParentId.IsValid())
|
||||
{
|
||||
DetachParentToNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
var entManager = _entMan;
|
||||
if (!entManager.EntityExists(newParentId))
|
||||
{
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw new InvalidOperationException($"Unable to find new parent {newParentId}! This probably means the server never sent it.");
|
||||
#else
|
||||
Logger.ErrorS("transform", $"Unable to find new parent {newParentId}! Deleting {Owner}");
|
||||
entManager.QueueDeleteEntity(Owner);
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
AttachParent(entManager.GetComponent<TransformComponent>(newParentId));
|
||||
}
|
||||
}
|
||||
|
||||
rebuildMatrices = true;
|
||||
}
|
||||
|
||||
if (LocalRotation != newState.Rotation)
|
||||
{
|
||||
_localRotation = newState.Rotation;
|
||||
rebuildMatrices = true;
|
||||
}
|
||||
|
||||
if (!_localPosition.EqualsApprox(newState.LocalPosition))
|
||||
{
|
||||
var oldPos = Coordinates;
|
||||
_localPosition = newState.LocalPosition;
|
||||
|
||||
var ev = new MoveEvent(Owner, oldPos, Coordinates, this);
|
||||
EntitySystem.Get<SharedTransformSystem>().DeferMoveEvent(ref ev);
|
||||
|
||||
rebuildMatrices = true;
|
||||
}
|
||||
|
||||
_prevPosition = newState.LocalPosition;
|
||||
_prevRotation = newState.Rotation;
|
||||
|
||||
Anchored = newState.Anchored;
|
||||
_noLocalRotation = newState.NoLocalRotation;
|
||||
|
||||
// This is not possible, because client entities don't exist on the server, so the parent HAS to be a shared entity.
|
||||
// If this assert fails, the code above that sets the parent is broken.
|
||||
DebugTools.Assert(!_parent.IsClientSide(), "Transform received a state, but is still parented to a client entity.");
|
||||
|
||||
// Whatever happened on the client, these should still be correct
|
||||
DebugTools.Assert(ParentUid == newState.ParentID);
|
||||
DebugTools.Assert(Anchored == newState.Anchored);
|
||||
|
||||
if (rebuildMatrices)
|
||||
{
|
||||
RebuildMatrices();
|
||||
}
|
||||
|
||||
Dirty(_entMan);
|
||||
}
|
||||
|
||||
if (nextState is TransformComponentState nextTransform)
|
||||
{
|
||||
_nextPosition = nextTransform.LocalPosition;
|
||||
_nextRotation = nextTransform.Rotation;
|
||||
LerpParent = nextTransform.ParentID;
|
||||
ActivateLerp();
|
||||
}
|
||||
else
|
||||
{
|
||||
// this should cause the lerp to do nothing
|
||||
_nextPosition = null;
|
||||
_nextRotation = null;
|
||||
LerpParent = EntityUid.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildMatrices()
|
||||
internal void RebuildMatrices()
|
||||
{
|
||||
var pos = _localPosition;
|
||||
|
||||
@@ -1068,65 +961,6 @@ namespace Robust.Shared.GameObjects
|
||||
return $"pos/rot/wpos/wrot: {Coordinates}/{LocalRotation}/{WorldPosition}/{WorldRotation}";
|
||||
}
|
||||
|
||||
private void ActivateLerp()
|
||||
{
|
||||
if (ActivelyLerping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivelyLerping = true;
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new TransformStartLerpMessage(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialized state of a TransformComponent.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
internal sealed class TransformComponentState : ComponentState
|
||||
{
|
||||
/// <summary>
|
||||
/// Current parent entity of this entity.
|
||||
/// </summary>
|
||||
public readonly EntityUid ParentID;
|
||||
|
||||
/// <summary>
|
||||
/// Current position offset of the entity.
|
||||
/// </summary>
|
||||
public readonly Vector2 LocalPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Current rotation offset of the entity.
|
||||
/// </summary>
|
||||
public readonly Angle Rotation;
|
||||
|
||||
/// <summary>
|
||||
/// Is the transform able to be locally rotated?
|
||||
/// </summary>
|
||||
public readonly bool NoLocalRotation;
|
||||
|
||||
/// <summary>
|
||||
/// True if the transform is anchored to a tile.
|
||||
/// </summary>
|
||||
public readonly bool Anchored;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new state snapshot of a TransformComponent.
|
||||
/// </summary>
|
||||
/// <param name="localPosition">Current position offset of this entity.</param>
|
||||
/// <param name="rotation">Current direction offset of this entity.</param>
|
||||
/// <param name="parentId">Current parent transform of this entity.</param>
|
||||
/// <param name="noLocalRotation"></param>
|
||||
public TransformComponentState(Vector2 localPosition, Angle rotation, EntityUid parentId, bool noLocalRotation, bool anchored)
|
||||
{
|
||||
LocalPosition = localPosition;
|
||||
Rotation = rotation;
|
||||
ParentID = parentId;
|
||||
NoLocalRotation = noLocalRotation;
|
||||
Anchored = anchored;
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetAnchored(bool value)
|
||||
{
|
||||
_anchored = value;
|
||||
|
||||
@@ -135,6 +135,16 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BoundUIOpenedEvent : BoundUserInterfaceMessage
|
||||
{
|
||||
public BoundUIOpenedEvent(object uiKey, EntityUid uid, ICommonSession session)
|
||||
{
|
||||
UiKey = uiKey;
|
||||
Entity = uid;
|
||||
Session = session;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BoundUIClosedEvent : BoundUserInterfaceMessage
|
||||
{
|
||||
public BoundUIClosedEvent(object uiKey, EntityUid uid, ICommonSession session)
|
||||
|
||||
@@ -148,9 +148,9 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
#if DEBUG
|
||||
// Second integrity check in case of.
|
||||
foreach (var t in GetComponents(uid))
|
||||
foreach (var t in _entCompIndex[uid])
|
||||
{
|
||||
if (!t.Initialized)
|
||||
if (!t.Deleted && !t.Initialized)
|
||||
{
|
||||
DebugTools.Assert(
|
||||
$"Component {t.GetType()} was not initialized at the end of {nameof(InitializeComponents)}.");
|
||||
@@ -325,35 +325,50 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RemoveComponent<T>(EntityUid uid)
|
||||
public bool RemoveComponent<T>(EntityUid uid)
|
||||
{
|
||||
RemoveComponent(uid, typeof(T));
|
||||
return RemoveComponent(uid, typeof(T));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RemoveComponent(EntityUid uid, Type type)
|
||||
public bool RemoveComponent(EntityUid uid, Type type)
|
||||
{
|
||||
RemoveComponentImmediate((Component)GetComponent(uid, type), uid, false);
|
||||
if (!TryGetComponent(uid, type, out var comp))
|
||||
return false;
|
||||
|
||||
RemoveComponentImmediate((Component)comp, uid, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RemoveComponent(EntityUid uid, ushort netId)
|
||||
public bool RemoveComponent(EntityUid uid, ushort netId)
|
||||
{
|
||||
RemoveComponentImmediate((Component)GetComponent(uid, netId), uid, false);
|
||||
if (!TryGetComponent(uid, netId, out var comp))
|
||||
return false;
|
||||
|
||||
RemoveComponentImmediate((Component)comp, uid, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RemoveComponent(EntityUid uid, IComponent component)
|
||||
{
|
||||
RemoveComponent(uid, (Component)component);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RemoveComponent(EntityUid uid, Component component)
|
||||
{
|
||||
if (component == null) throw new ArgumentNullException(nameof(component));
|
||||
|
||||
if (component.Owner != uid)
|
||||
throw new InvalidOperationException("Component is not owned by entity.");
|
||||
|
||||
RemoveComponentImmediate((Component)component, uid, false);
|
||||
RemoveComponentImmediate(component, uid, false);
|
||||
}
|
||||
|
||||
private static IEnumerable<Component> InSafeOrder(IEnumerable<Component> comps, bool forCreation = false)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user