This commit is contained in:
Zekins3366
2025-07-06 20:51:50 +03:00
1300 changed files with 246643 additions and 99106 deletions

View File

@@ -20,6 +20,7 @@ public sealed class AnomalyScannerBoundUserInterface : BoundUserInterface
_menu = new AnomalyScannerMenu();
_menu.OpenCentered();
_menu.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -1,4 +1,4 @@
using Robust.Client.GameObjects;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
@@ -18,6 +18,7 @@ namespace Content.Client.Atmos.UI
base.Open();
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
_window.OnClose += Close;
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
@@ -29,15 +30,6 @@ namespace Content.Client.Atmos.UI
_window.Populate(cast);
}
/// <summary>
/// Closes UI and tells the server to disable the analyzer
/// </summary>
private void OnClose()
{
SendMessage(new GasAnalyzerDisableMessage());
Close();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@@ -2,16 +2,16 @@ using Robust.Shared.Console;
namespace Content.Client.Audio;
public sealed class AmbientOverlayCommand : IConsoleCommand
public sealed class AmbientOverlayCommand : LocalizedEntityCommands
{
public string Command => "showambient";
public string Description => "Shows all AmbientSoundComponents in the viewport";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AmbientSoundSystem>();
system.OverlayEnabled ^= true;
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
shell.WriteLine($"Ambient sound overlay set to {system.OverlayEnabled}");
public override string Command => "showambient";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_ambient.OverlayEnabled ^= true;
shell.WriteLine(Loc.GetString($"cmd-showambient-status", ("status", _ambient.OverlayEnabled)));
}
}

View File

@@ -15,8 +15,8 @@ namespace Content.Client.Changelog
[GenerateTypedNameReferences]
public sealed partial class ChangelogWindow : FancyWindow
{
[Dependency] private readonly ChangelogManager _changelog = default!;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly ChangelogManager _changelog = default!;
public ChangelogWindow()
{
@@ -112,15 +112,15 @@ namespace Content.Client.Changelog
}
[UsedImplicitly, AnyCommand]
public sealed class ChangelogCommand : IConsoleCommand
public sealed class ChangelogCommand : LocalizedCommands
{
public string Command => "changelog";
public string Description => "Opens the changelog";
public string Help => "Usage: changelog";
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "changelog";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<ChangelogUIController>().OpenWindow();
_uiManager.GetUIController<ChangelogUIController>().OpenWindow();
}
}
}

View File

@@ -14,6 +14,7 @@ namespace Content.Client.Chat.UI
{
public abstract class SpeechBubble : Control
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] protected readonly IConfigurationManager ConfigManager = default!;
@@ -30,12 +31,12 @@ namespace Content.Client.Chat.UI
/// <summary>
/// The total time a speech bubble stays on screen.
/// </summary>
private const float TotalTime = 4;
private static readonly TimeSpan TotalTime = TimeSpan.FromSeconds(4);
/// <summary>
/// The amount of time at the end of the bubble's life at which it starts fading.
/// </summary>
private const float FadeTime = 0.25f;
private static readonly TimeSpan FadeTime = TimeSpan.FromSeconds(0.25f);
/// <summary>
/// The distance in world space to offset the speech bubble from the center of the entity.
@@ -50,7 +51,10 @@ namespace Content.Client.Chat.UI
private readonly EntityUid _senderEntity;
private float _timeLeft = TotalTime;
/// <summary>
/// The time at which this bubble will die.
/// </summary>
private TimeSpan _deathTime;
public float VerticalOffset { get; set; }
private float _verticalOffsetAchieved;
@@ -99,6 +103,7 @@ namespace Content.Client.Chat.UI
bubble.Measure(Vector2Helpers.Infinity);
ContentSize = bubble.DesiredSize;
_verticalOffsetAchieved = -ContentSize.Y;
_deathTime = _timing.RealTime + TotalTime;
}
protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null);
@@ -107,8 +112,8 @@ namespace Content.Client.Chat.UI
{
base.FrameUpdate(args);
_timeLeft -= args.DeltaSeconds;
if (_entityManager.Deleted(_senderEntity) || _timeLeft <= 0)
var timeLeft = (float)(_deathTime - _timing.RealTime).TotalSeconds;
if (_entityManager.Deleted(_senderEntity) || timeLeft <= 0)
{
// Timer spawn to prevent concurrent modification exception.
Timer.Spawn(0, Die);
@@ -131,10 +136,10 @@ namespace Content.Client.Chat.UI
return;
}
if (_timeLeft <= FadeTime)
if (timeLeft <= FadeTime.TotalSeconds)
{
// Update alpha if we're fading.
Modulate = Color.White.WithAlpha(_timeLeft / FadeTime);
Modulate = Color.White.WithAlpha(timeLeft / (float)FadeTime.TotalSeconds);
}
else
{
@@ -144,7 +149,7 @@ namespace Content.Client.Chat.UI
var baseOffset = 0f;
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
baseOffset = speech.SpeechBubbleOffset;
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
@@ -175,9 +180,9 @@ namespace Content.Client.Chat.UI
/// </summary>
public void FadeNow()
{
if (_timeLeft > FadeTime)
if (_deathTime > _timing.RealTime)
{
_timeLeft = FadeTime;
_deathTime = _timing.RealTime + FadeTime;
}
}

View File

@@ -5,8 +5,9 @@ using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class HypospraySystem : SharedHypospraySystem
public sealed class HyposprayStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
public override void Initialize()
{
base.Initialize();

View File

@@ -38,7 +38,7 @@ namespace Content.Client.Construction.UI
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = [];
private Dictionary<string, ContainerButton> _recipeButtons = new();
private readonly Dictionary<string, ContainerButton> _recipeButtons = new();
private string _selectedCategory = string.Empty;
private const string FavoriteCatName = "construction-category-favorites";
@@ -217,8 +217,8 @@ namespace Content.Client.Construction.UI
var itemButton = new ContainerButton()
{
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.TargetPrototype.Name,
ToolTip = recipe.TargetPrototype.Name,
Name = recipe.Prototype.Name,
ToolTip = recipe.Prototype.Name,
ToggleMode = true,
Children = { protoView },
};
@@ -235,7 +235,7 @@ namespace Content.Client.Construction.UI
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton))
_recipeButtons.TryGetValue(_selected.ID, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
@@ -245,7 +245,7 @@ namespace Content.Client.Construction.UI
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Prototype.Name!] = itemButton;
_recipeButtons[recipe.Prototype.ID] = itemButton;
var isCurrentButtonSelected = _selected == recipe.Prototype;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
@@ -307,7 +307,7 @@ namespace Content.Client.Construction.UI
if (button.Parent is not PanelContainer buttonPanel)
return;
button.Modulate = select ? Color.Green : Color.Transparent;
button.Children.Single().Modulate = select ? Color.Green : Color.White;
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}

View File

@@ -1,17 +1,15 @@
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
namespace Content.Client.Decals;
public sealed class ToggleDecalCommand : IConsoleCommand
public sealed class ToggleDecalCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly DecalSystem _decal = default!;
public string Command => "toggledecals";
public string Description => "Toggles decaloverlay";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "toggledecals";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_e.System<DecalSystem>().ToggleOverlay();
_decal.ToggleOverlay();
}
}

View File

@@ -16,6 +16,7 @@ using Content.Client.Lobby;
using Content.Client.MainMenu;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Playtime;
using Content.Client.Radiation.Overlays;
using Content.Client.Replay;
using Content.Client.Screenshot;
@@ -76,6 +77,7 @@ namespace Content.Client.Entry
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!;
public override void Init()
{
@@ -139,6 +141,7 @@ namespace Content.Client.Entry
_extendedDisconnectInformation.Initialize();
_jobRequirements.Initialize();
_playbackMan.Initialize();
_clientsidePlaytimeManager.Initialize();
//AUTOSCALING default Setup!
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080);

View File

@@ -2,25 +2,17 @@
namespace Content.Client.Ghost.Commands;
public sealed class ToggleGhostVisibilityCommand : IConsoleCommand
public sealed class ToggleGhostVisibilityCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entSysMan = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
public string Command => "toggleghostvisibility";
public string Description => "Toggles ghost visibility on the client.";
public string Help => "toggleghostvisibility [bool]";
public override string Command => "toggleghostvisibility";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ghostSystem = _entSysMan.GetEntitySystem<GhostSystem>();
if (args.Length != 0 && bool.TryParse(args[0], out var visibility))
{
ghostSystem.ToggleGhostVisibility(visibility);
}
_ghost.ToggleGhostVisibility(visibility);
else
{
ghostSystem.ToggleGhostVisibility();
}
_ghost.ToggleGhostVisibility();
}
}

View File

@@ -4,28 +4,27 @@ using Robust.Shared.Console;
namespace Content.Client.Ghost;
public sealed class GhostToggleSelfVisibility : IConsoleCommand
public sealed class GhostToggleSelfVisibility : LocalizedEntityCommands
{
public string Command => "toggleselfghost";
public string Description => "Toggles seeing your own ghost.";
public string Help => "toggleselfghost";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly SpriteSystem _sprite = default!;
public override string Command => "toggleselfghost";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var attachedEntity = shell.Player?.AttachedEntity;
if (!attachedEntity.HasValue)
return;
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!entityManager.HasComponent<GhostComponent>(attachedEntity))
if (!EntityManager.HasComponent<GhostComponent>(attachedEntity))
{
shell.WriteError("Entity must be a ghost.");
shell.WriteError(Loc.GetString($"cmd-toggleselfghost-must-be-ghost"));
return;
}
if (!entityManager.TryGetComponent(attachedEntity, out SpriteComponent? spriteComponent))
if (!EntityManager.TryGetComponent(attachedEntity, out SpriteComponent? spriteComponent))
return;
var spriteSys = entityManager.System<SpriteSystem>();
spriteSys.SetVisible((attachedEntity.Value, spriteComponent), !spriteComponent.Visible);
_sprite.SetVisible((attachedEntity.Value, spriteComponent), !spriteComponent.Visible);
}
}

View File

@@ -0,0 +1,54 @@
using System.Linq;
using Content.Shared.Instruments;
using Robust.Shared.Audio.Midi;
namespace Content.Client.Instruments;
public sealed partial class InstrumentSystem
{
/// <summary>
/// Tries to parse the input data as a midi and set the channel names respectively.
/// </summary>
/// <remarks>
/// Thank you to http://www.somascape.org/midi/tech/mfile.html for providing an awesome resource for midi files.
/// </remarks>
/// <remarks>
/// This method has exception tolerance and does not throw, even if the midi file is invalid.
/// </remarks>
private bool TrySetChannels(EntityUid uid, byte[] data)
{
if (!MidiParser.MidiParser.TryGetMidiTracks(data, out var tracks, out var error))
{
Log.Error(error);
return false;
}
var resolvedTracks = new List<MidiTrack?>();
for (var index = 0; index < tracks.Length; index++)
{
var midiTrack = tracks[index];
if (midiTrack is { TrackName: null, ProgramName: null, InstrumentName: null})
continue;
switch (midiTrack)
{
case { TrackName: not null, ProgramName: not null }:
case { TrackName: not null, InstrumentName: not null }:
case { TrackName: not null }:
case { ProgramName: not null }:
resolvedTracks.Add(midiTrack);
break;
default:
resolvedTracks.Add(null); // Used so the channel still displays as MIDI Channel X and doesn't just take the next valid one in the UI
break;
}
Log.Debug($"Channel name: {resolvedTracks.Last()}");
}
RaiseNetworkEvent(new InstrumentSetChannelsEvent(GetNetEntity(uid), resolvedTracks.Take(RobustMidiEvent.MaxChannels).ToArray()));
Log.Debug($"Resolved {resolvedTracks.Count} channels.");
return true;
}
}

View File

@@ -1,3 +1,4 @@
using System.IO;
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Instruments;
@@ -12,7 +13,7 @@ using Robust.Shared.Timing;
namespace Content.Client.Instruments;
public sealed class InstrumentSystem : SharedInstrumentSystem
public sealed partial class InstrumentSystem : SharedInstrumentSystem
{
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IMidiManager _midiManager = default!;
@@ -23,6 +24,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
public int MaxMidiEventsPerBatch { get; private set; }
public int MaxMidiEventsPerSecond { get; private set; }
public event Action? OnChannelsUpdated;
public override void Initialize()
{
base.Initialize();
@@ -38,6 +41,26 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<ActiveInstrumentComponent, AfterAutoHandleStateEvent>(OnActiveInstrumentAfterHandleState);
}
private bool _isUpdateQueued = false;
private void OnActiveInstrumentAfterHandleState(Entity<ActiveInstrumentComponent> ent, ref AfterAutoHandleStateEvent args)
{
// Called in the update loop so that the components update client side for resolving them in TryComps.
_isUpdateQueued = true;
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!_isUpdateQueued)
return;
_isUpdateQueued = false;
OnChannelsUpdated?.Invoke();
}
private void OnHandleState(EntityUid uid, SharedInstrumentComponent component, ref ComponentHandleState args)
@@ -252,7 +275,13 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
}
[Obsolete("Use overload that takes in byte[] instead.")]
public bool OpenMidi(EntityUid uid, ReadOnlySpan<byte> data, InstrumentComponent? instrument = null)
{
return OpenMidi(uid, data.ToArray(), instrument);
}
public bool OpenMidi(EntityUid uid, byte[] data, InstrumentComponent? instrument = null)
{
if (!Resolve(uid, ref instrument))
return false;
@@ -263,6 +292,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
return false;
SetMaster(uid, null);
TrySetChannels(uid, data);
instrument.MidiEventBuffer.Clear();
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
return true;

View File

@@ -0,0 +1,147 @@
using Robust.Shared.Utility;
namespace Content.Client.Instruments.MidiParser;
// This file was autogenerated. Based on https://www.ccarh.org/courses/253/handout/gminstruments/
public enum MidiInstrument : byte
{
AcousticGrandPiano = 0,
BrightAcousticPiano = 1,
ElectricGrandPiano = 2,
HonkyTonkPiano = 3,
RhodesPiano = 4,
ChorusedPiano = 5,
Harpsichord = 6,
Clavinet = 7,
Celesta = 8,
Glockenspiel = 9,
MusicBox = 10,
Vibraphone = 11,
Marimba = 12,
Xylophone = 13,
TubularBells = 14,
Dulcimer = 15,
HammondOrgan = 16,
PercussiveOrgan = 17,
RockOrgan = 18,
ChurchOrgan = 19,
ReedOrgan = 20,
Accordion = 21,
Harmonica = 22,
TangoAccordion = 23,
AcousticNylonGuitar = 24,
AcousticSteelGuitar = 25,
ElectricJazzGuitar = 26,
ElectricCleanGuitar = 27,
ElectricMutedGuitar = 28,
OverdrivenGuitar = 29,
DistortionGuitar = 30,
GuitarHarmonics = 31,
AcousticBass = 32,
FingeredElectricBass = 33,
PluckedElectricBass = 34,
FretlessBass = 35,
SlapBass1 = 36,
SlapBass2 = 37,
SynthBass1 = 38,
SynthBass2 = 39,
Violin = 40,
Viola = 41,
Cello = 42,
Contrabass = 43,
TremoloStrings = 44,
PizzicatoStrings = 45,
OrchestralHarp = 46,
Timpani = 47,
StringEnsemble1 = 48,
StringEnsemble2 = 49,
SynthStrings1 = 50,
SynthStrings2 = 51,
ChoirAah = 52,
VoiceOoh = 53,
SynthChoir = 54,
OrchestraHit = 55,
Trumpet = 56,
Trombone = 57,
Tuba = 58,
MutedTrumpet = 59,
FrenchHorn = 60,
BrassSection = 61,
SynthBrass1 = 62,
SynthBrass2 = 63,
SopranoSax = 64,
AltoSax = 65,
TenorSax = 66,
BaritoneSax = 67,
Oboe = 68,
EnglishHorn = 69,
Bassoon = 70,
Clarinet = 71,
Piccolo = 72,
Flute = 73,
Recorder = 74,
PanFlute = 75,
BottleBlow = 76,
Shakuhachi = 77,
Whistle = 78,
Ocarina = 79,
SquareWaveLead = 80,
SawtoothWaveLead = 81,
CalliopeLead = 82,
ChiffLead = 83,
CharangLead = 84,
VoiceLead = 85,
FithsLead = 86,
BassLead = 87,
NewAgePad = 88,
WarmPad = 89,
PolysynthPad = 90,
ChoirPad = 91,
BowedPad = 92,
MetallicPad = 93,
HaloPad = 94,
SweepPad = 95,
RainEffect = 96,
SoundtrackEffect = 97,
CrystalEffect = 98,
AtmosphereEffect = 99,
BrightnessEffect = 100,
GoblinsEffect = 101,
EchoesEffect = 102,
SciFiEffect = 103,
Sitar = 104,
Banjo = 105,
Shamisen = 106,
Koto = 107,
Kalimba = 108,
Bagpipe = 109,
Fiddle = 110,
Shanai = 111,
TinkleBell = 112,
Agogo = 113,
SteelDrums = 114,
Woodblock = 115,
TaikoDrum = 116,
MelodicTom = 117,
SynthDrum = 118,
ReverseCymbal = 119,
GuitarFretNoise = 120,
BreathNoise = 121,
Seashore = 122,
BirdTweet = 123,
TelephoneRing = 124,
Helicopter = 125,
Applause = 126,
Gunshot = 127,
}
public static class MidiInstrumentExt
{
/// <summary>
/// Turns the given enum value into it's string representation to be used in localization.
/// </summary>
public static string GetStringRep(this MidiInstrument instrument)
{
return CaseConversion.PascalToKebab(instrument.ToString());
}
}

View File

@@ -0,0 +1,184 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Content.Shared.Instruments;
namespace Content.Client.Instruments.MidiParser;
public static class MidiParser
{
// Thanks again to http://www.somascape.org/midi/tech/mfile.html
public static bool TryGetMidiTracks(
byte[] data,
[NotNullWhen(true)] out MidiTrack[]? tracks,
[NotNullWhen(false)] out string? error)
{
tracks = null;
error = null;
var stream = new MidiStreamWrapper(data);
if (stream.ReadString(4) != "MThd")
{
error = "Invalid file header";
return false;
}
var headerLength = stream.ReadUInt32();
// MIDI specs define that the header is 6 bytes, we only look at the 6 bytes, if its more, we skip ahead.
stream.Skip(2); // format
var trackCount = stream.ReadUInt16();
stream.Skip(2); // time div
// We now skip ahead if we still have any header length left
stream.Skip((int)(headerLength - 6));
var parsedTracks = new List<MidiTrack>();
for (var i = 0; i < trackCount; i++)
{
if (stream.ReadString(4) != "MTrk")
{
tracks = null;
error = "Track contains invalid header";
return false;
}
var track = new MidiTrack();
var trackLength = stream.ReadUInt32();
var trackEnd = stream.StreamPosition + trackLength;
var hasMidiEvent = false;
byte? lastStatusByte = null;
while (stream.StreamPosition < trackEnd)
{
stream.ReadVariableLengthQuantity();
/*
* If the first (status) byte is less than 128 (hex 80), this implies that running status is in effect,
* and that this byte is actually the first data byte (the status carrying over from the previous MIDI event).
* This can only be the case if the immediately previous event was also a MIDI event,
* i.e. SysEx and Meta events interrupt (clear) running status.
* See http://www.somascape.org/midi/tech/mfile.html#events
*/
var firstByte = stream.ReadByte();
if (firstByte >= 0x80)
{
lastStatusByte = firstByte;
}
else
{
// Running status: push byte back for reading as data
stream.Skip(-1);
}
// The first event in each MTrk chunk must specify status.
if (lastStatusByte == null)
{
tracks = null;
error = "Track data not valid, expected status byte, got nothing.";
return false;
}
var eventType = (byte)(lastStatusByte & 0xF0);
switch (lastStatusByte)
{
// Meta events
case 0xFF:
{
var metaType = stream.ReadByte();
var metaLength = stream.ReadVariableLengthQuantity();
var metaData = stream.ReadBytes((int)metaLength);
if (metaType == 0x00) // SequenceNumber event
continue;
// Meta event types 01 through 0F are reserved for text and all follow the basic FF 01 len text format
if (metaType is < 0x01 or > 0x0F)
break;
// 0x03 is TrackName,
// 0x04 is InstrumentName
var text = Encoding.ASCII.GetString(metaData, 0, (int)metaLength);
switch (metaType)
{
case 0x03 when track.TrackName == null:
track.TrackName = text;
break;
case 0x04 when track.InstrumentName == null:
track.InstrumentName = text;
break;
}
// still here? then we dont care about the event
break;
}
// SysEx events
case 0xF0:
case 0xF7:
{
var sysexLength = stream.ReadVariableLengthQuantity();
stream.Skip((int)sysexLength);
// Sysex events and meta-events cancel any running status which was in effect.
// Running status does not apply to and may not be used for these messages.
lastStatusByte = null;
break;
}
default:
switch (eventType)
{
// Program Change
case 0xC0:
{
var programNumber = stream.ReadByte();
if (track.ProgramName == null)
{
if (programNumber < Enum.GetValues<MidiInstrument>().Length)
track.ProgramName = Loc.GetString($"instruments-component-menu-midi-channel-{((MidiInstrument)programNumber).GetStringRep()}");
}
break;
}
case 0x80: // Note Off
case 0x90: // Note On
case 0xA0: // Polyphonic Key Pressure
case 0xB0: // Control Change
case 0xE0: // Pitch Bend
{
hasMidiEvent = true;
stream.Skip(2);
break;
}
case 0xD0: // Channel Pressure
{
hasMidiEvent = true;
stream.Skip(1);
break;
}
default:
error = $"Unknown MIDI event type {lastStatusByte:X2}";
tracks = null;
return false;
}
break;
}
}
if (hasMidiEvent)
parsedTracks.Add(track);
}
tracks = parsedTracks.ToArray();
return true;
}
}

View File

@@ -0,0 +1,103 @@
using System.IO;
using System.Text;
namespace Content.Client.Instruments.MidiParser;
public sealed class MidiStreamWrapper
{
private readonly MemoryStream _stream;
private byte[] _buffer;
public long StreamPosition => _stream.Position;
public MidiStreamWrapper(byte[] data)
{
_stream = new MemoryStream(data, writable: false);
_buffer = new byte[4];
}
/// <summary>
/// Skips X number of bytes in the stream.
/// </summary>
/// <param name="count">The number of bytes to skip. If 0, no operations on the stream are performed.</param>
public void Skip(int count)
{
if (count == 0)
return;
_stream.Seek(count, SeekOrigin.Current);
}
public byte ReadByte()
{
var b = _stream.ReadByte();
if (b == -1)
throw new Exception("Unexpected end of stream");
return (byte)b;
}
/// <summary>
/// Reads N bytes using the buffer.
/// </summary>
public byte[] ReadBytes(int count)
{
if (_buffer.Length < count)
{
Array.Resize(ref _buffer, count);
}
var read = _stream.Read(_buffer, 0, count);
if (read != count)
throw new Exception("Unexpected end of stream");
return _buffer;
}
/// <summary>
/// Reads a 4 byte big-endian uint.
/// </summary>
public uint ReadUInt32()
{
var bytes = ReadBytes(4);
return (uint)((bytes[0] << 24) |
(bytes[1] << 16) |
(bytes[2] << 8) |
(bytes[3]));
}
/// <summary>
/// Reads a 2 byte big-endian ushort.
/// </summary>
public ushort ReadUInt16()
{
var bytes = ReadBytes(2);
return (ushort)((bytes[0] << 8) | bytes[1]);
}
public string ReadString(int count)
{
var bytes = ReadBytes(count);
return Encoding.UTF8.GetString(bytes, 0, count);
}
public uint ReadVariableLengthQuantity()
{
uint value = 0;
// variable-length-quantities encode ints using 7 bits per byte
// the highest bit (7) is used for a continuation flag. We read until the high bit is 0
while (true)
{
var b = ReadByte();
value = (value << 7) | (uint)(b & 0x7f); // Shift current value and add 7 bits
// value << 7, make room for the next 7 bits
// b & 0x7F mask out the high bit to just get the 7 bit payload
if ((b & 0x80) == 0)
break; // This was the last bit.
}
return value;
}
}

View File

@@ -7,5 +7,7 @@
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
</BoxContainer>
<CheckButton Name="DisplayTrackNames"
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
</BoxContainer>
</DefaultWindow>

View File

@@ -1,26 +1,56 @@
using Content.Shared.Instruments;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Instruments.UI;
[GenerateTypedNameReferences]
public sealed partial class ChannelsMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = null!;
private readonly InstrumentBoundUserInterface _owner;
public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_owner = owner;
ChannelList.OnItemSelected += OnItemSelected;
ChannelList.OnItemDeselected += OnItemDeselected;
AllButton.OnPressed += OnAllPressed;
ClearButton.OnPressed += OnClearPressed;
DisplayTrackNames.OnPressed += OnDisplayTrackNamesPressed;
}
protected override void EnteredTree()
{
base.EnteredTree();
_owner.Instruments.OnChannelsUpdated += UpdateChannelList;
}
private void OnDisplayTrackNamesPressed(BaseButton.ButtonEventArgs obj)
{
DisplayTrackNames.SetClickPressed(!DisplayTrackNames.Pressed);
Populate();
}
private void UpdateChannelList()
{
Populate(); // This is kind of in-efficent because we don't filter for which instrument updated its channels, but idc
}
protected override void ExitedTree()
{
base.ExitedTree();
_owner.Instruments.OnChannelsUpdated -= UpdateChannelList;
}
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
@@ -51,15 +81,71 @@ public sealed partial class ChannelsMenu : DefaultWindow
}
}
public void Populate(InstrumentComponent? instrument)
/// <summary>
/// Walks up the tree of instrument masters to find the truest master of them all.
/// </summary>
private ActiveInstrumentComponent ResolveActiveInstrument(InstrumentComponent? comp)
{
comp ??= _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
var instrument = new Entity<InstrumentComponent>(_owner.Owner, comp);
while (true)
{
if (instrument.Comp.Master == null)
break;
instrument = new Entity<InstrumentComponent>((EntityUid)instrument.Comp.Master,
_entityManager.GetComponent<InstrumentComponent>((EntityUid)instrument.Comp.Master));
}
return _entityManager.GetComponent<ActiveInstrumentComponent>(instrument.Owner);
}
public void Populate()
{
ChannelList.Clear();
var instrument = _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
var activeInstrument = ResolveActiveInstrument(instrument);
for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
{
var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name",
("number", i)), null, true, i);
var label = _owner.Loc.GetString("instrument-component-channel-name",
("number", i));
if (activeInstrument != null
&& activeInstrument.Tracks.TryGetValue(i, out var resolvedMidiChannel)
&& resolvedMidiChannel != null)
{
if (DisplayTrackNames.Pressed)
{
label = resolvedMidiChannel switch
{
{ TrackName: not null, InstrumentName: not null } =>
Loc.GetString("instruments-component-channels-multi",
("channel", i),
("name", resolvedMidiChannel.TrackName),
("other", resolvedMidiChannel.InstrumentName)),
{ TrackName: not null } =>
Loc.GetString("instruments-component-channels-single",
("channel", i),
("name", resolvedMidiChannel.TrackName)),
_ => label,
};
}
else
{
label = resolvedMidiChannel switch
{
{ ProgramName: not null } =>
Loc.GetString("instruments-component-channels-single",
("channel", i),
("name", resolvedMidiChannel.ProgramName)),
_ => label,
};
}
}
var item = ChannelList.AddItem(label, null, true, i);
item.Selected = !instrument?.FilteredChannels[i] ?? false;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Instruments;
using Content.Shared.Instruments.UI;
using Content.Shared.Interaction;
using Robust.Client.Audio.Midi;
@@ -101,9 +102,7 @@ namespace Content.Client.Instruments.UI
public void OpenChannelsMenu()
{
_channelsMenu ??= new ChannelsMenu(this);
EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument);
_channelsMenu.Populate(instrument);
_channelsMenu.Populate();
_channelsMenu.OpenCenteredRight();
}

View File

@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.Containers;
using Robust.Shared.Input;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BaseButton;
using Range = Robust.Client.UserInterface.Controls.Range;
@@ -145,10 +146,6 @@ namespace Content.Client.Instruments.UI
if (!PlayCheck())
return;
await using var memStream = new MemoryStream((int) file.Length);
await file.CopyToAsync(memStream);
if (!_entManager.TryGetComponent<InstrumentComponent>(Entity, out var instrument))
{
return;
@@ -156,7 +153,7 @@ namespace Content.Client.Instruments.UI
if (!_entManager.System<InstrumentSystem>()
.OpenMidi(Entity,
memStream.GetBuffer().AsSpan(0, (int) memStream.Length),
file.CopyToArray(),
instrument))
{
return;

View File

@@ -13,6 +13,7 @@ using Content.Client.Launcher;
using Content.Client.Mapping;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Playtime;
using Content.Client.Replay;
using Content.Client.Screenshot;
using Content.Client.Stylesheets;
@@ -62,6 +63,7 @@ namespace Content.Client.IoC
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
collection.Register<ClientsidePlaytimeTrackingManager>();
collection.Register<InteractionPanelManager>(); // Corvax-Wega
}
}

View File

@@ -3,6 +3,7 @@ using Content.Client.GameTicking.Managers;
using Content.Client.LateJoin;
using Content.Client.Lobby.UI;
using Content.Client.Message;
using Content.Client.Playtime;
using Content.Client.UserInterface.Systems.Chat;
using Content.Client.Voting;
using Content.Shared.CCVar;
@@ -26,6 +27,7 @@ namespace Content.Client.Lobby
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly ClientsidePlaytimeTrackingManager _playtimeTracking = default!;
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
@@ -195,6 +197,26 @@ namespace Content.Client.Lobby
{
Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
}
var minutesToday = _playtimeTracking.PlaytimeMinutesToday;
if (minutesToday > 60)
{
Lobby!.PlaytimeComment.Visible = true;
var hoursToday = Math.Round(minutesToday / 60f, 1);
var chosenString = minutesToday switch
{
< 180 => "lobby-state-playtime-comment-normal",
< 360 => "lobby-state-playtime-comment-concerning",
< 720 => "lobby-state-playtime-comment-grasstouchless",
_ => "lobby-state-playtime-comment-selfdestructive"
};
Lobby.PlaytimeComment.SetMarkup(Loc.GetString(chosenString, ("hours", hoursToday)));
}
else
Lobby!.PlaytimeComment.Visible = false;
}
private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev)

View File

@@ -20,6 +20,12 @@ public sealed partial class LoadoutContainer : BoxContainer
public Button Select => SelectButton;
public string? Text
{
get => SelectButton.Text;
set => SelectButton.Text = value;
}
public LoadoutContainer(ProtoId<LoadoutPrototype> proto, bool disabled, FormattedMessage? reason)
{
RobustXamlLoader.Load(this);
@@ -54,22 +60,9 @@ public sealed partial class LoadoutContainer : BoxContainer
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_entManager.DeleteEntity(_entity);
}
public bool Pressed
{
get => SelectButton.Pressed;
set => SelectButton.Pressed = value;
}
public string? Text
{
get => SelectButton.Text;
set => SelectButton.Text = value;
}
}

View File

@@ -1,10 +1,14 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical"/>
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True" Margin="5">
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True"/>
</PanelContainer>
<!-- Buffer space so we have 10 margin between controls but also 10 to the borders -->
<Label Text="{Loc 'loadout-restrictions'}" Margin="5 0 5 5"/>
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True" Margin="5">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'loadout-restrictions'}"/>
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Corvax.Interfaces.Shared;
using Content.Shared.Clothing;
using Content.Shared.Preferences;
@@ -8,12 +7,21 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Client.Lobby.UI.Loadouts;
[GenerateTypedNameReferences]
public sealed partial class LoadoutGroupContainer : BoxContainer
{
private const string ClosedGroupMark = "▶";
private const string OpenedGroupMark = "▼";
/// <summary>
/// A dictionary that stores open groups
/// </summary>
private Dictionary<string, bool> _openedGroups = new();
private readonly LoadoutGroupPrototype _groupProto;
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
@@ -22,6 +30,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
public LoadoutGroupContainer(HumanoidCharacterProfile profile, RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_groupProto = groupProto;
RefreshLoadouts(profile, loadout, session, collection);
@@ -64,9 +73,6 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
}
LoadoutsContainer.DisposeAllChildren();
// Didn't use options because this is more robust in future.
var selected = loadout.SelectedLoadouts[_groupProto.ID];
// Corvax-Loadouts-Start
var groupLoadouts = _groupProto.Loadouts;
@@ -76,28 +82,167 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
}
// Corvax-Loadouts-End
foreach (var loadoutProto in groupLoadouts) // Corvax-Loadouts
// Get all loadout prototypes for this group.
var validProtos = groupLoadouts.Select(id => protoMan.Index(id)); // Corvax-Loadouts-Edit
// Get all loadout prototypes for this group.
//var validProtos = _groupProto.Loadouts.Select(id => protoMan.Index(id));
/*
* Group the prototypes based on their GroupBy field.
* - If GroupBy is null or empty, fallback to grouping by the prototype ID itself.
* - The result is a dictionary where:
* - The key is either GroupBy or ID (if GroupBy is not set).
* - The value is the list of prototypes that belong to that group.
*
* This allows grouping loadouts into sub-categories within the group.
*/
var groups = validProtos
.GroupBy(p => string.IsNullOrEmpty(p.GroupBy)
? p.ID
: p.GroupBy)
.ToDictionary(g => g.Key, g => g.ToList());
foreach (var kvp in groups)
{
if (!protoMan.TryIndex(loadoutProto, out var loadProto))
continue;
var protos = kvp.Value;
var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
var pressed = matchingLoadout != null;
var enabled = loadout.IsValid(profile, session, loadoutProto, collection, out var reason);
var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
loadoutContainer.Select.Pressed = pressed;
loadoutContainer.Text = loadoutSystem.GetName(loadProto);
loadoutContainer.Select.OnPressed += args =>
if (protos.Count > 1)
{
if (args.Button.Pressed)
OnLoadoutPressed?.Invoke(loadoutProto);
else
OnLoadoutUnpressed?.Invoke(loadoutProto);
};
/*
* Build the list of UI elements for each loadout prototype:
* - For each prototype, create its corresponding LoadoutContainer UI element.
* - Set HorizontalExpand to true so elements properly stretch in layout.
* - Collect all UI elements into a list for further processing.
*/
var uiElements = protos
.Select(proto =>
{
var elem = CreateLoadoutUI(proto, profile, loadout, session, collection, loadoutSystem);
elem.HorizontalExpand = true;
return elem;
})
.ToList();
LoadoutsContainer.AddChild(loadoutContainer);
/*
* Determine which element should be displayed first:
* - If any element is currently selected (its button is pressed), use it.
* - Otherwise, fallback to the first element in the list.
*
* This moves the selected item outside of the sublist for better usability,
* making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
* without having to search inside expanded subgroups.
*/
var firstElement = uiElements.FirstOrDefault(e => e.Select.Pressed) ?? uiElements[0];
/*
* Get all remaining elements except the first one:
* - Use ReferenceEquals to ensure we exclude the exact instance used as firstElement.
*/
var otherElements = uiElements.Where(e => !ReferenceEquals(e, firstElement)).ToList();
firstElement.HorizontalExpand = true;
var subContainer = new SubLoadoutContainer()
{
Visible = _openedGroups.GetValueOrDefault(kvp.Key, false)
};
var toggle = CreateToggleButton(kvp, firstElement, subContainer);
LoadoutsContainer.AddChild(firstElement);
LoadoutsContainer.AddChild(subContainer);
var subList = subContainer.Grid;
foreach (var proto in otherElements)
{
subList.AddChild(proto);
}
UpdateToggleColor(toggle, subList);
}
else
{
LoadoutsContainer.AddChild(
CreateLoadoutUI(protos[0], profile, loadout, session, collection, loadoutSystem)
);
}
}
}
private ToggleLoadoutButton CreateToggleButton(KeyValuePair<string, List<LoadoutPrototype>> kvp, LoadoutContainer firstElement, SubLoadoutContainer subContainer)
{
var toggle = new ToggleLoadoutButton
{
Text = ClosedGroupMark
};
toggle.Text = subContainer.Visible ? OpenedGroupMark : ClosedGroupMark;
toggle.OnPressed += _ =>
{
var willOpen = !subContainer.Visible;
subContainer.Visible = willOpen;
toggle.Text = willOpen ? OpenedGroupMark : ClosedGroupMark;
_openedGroups[kvp.Key] = willOpen;
};
firstElement.AddChild(toggle);
toggle.SetPositionFirst();
return toggle;
}
private void UpdateToggleColor(Button toggle, BoxContainer subList)
{
var anyActive = subList.Children
.OfType<LoadoutContainer>()
.Any(c => c.Select.Pressed);
toggle.Modulate = anyActive
? Color.Green
: Color.White;
}
/// <summary>
/// Creates a UI container for a single Loadout item.
///
/// This method was extracted from RefreshLoadouts because the logic for creating
/// individual loadout items is used multiple times inside that method, and duplicating
/// the code made it harder to maintain.
///
/// Logic:
/// - Checks if the item is currently selected in the loadout.
/// - Checks if the item is valid for selection (IsValid).
/// - Creates a LoadoutContainer with the appropriate status (disabled / active).
/// - Subscribes to button press events to handle selection and deselection.
/// </summary>
/// <param name="proto">The loadout item prototype.</param>
/// <param name="profile">The humanoid character profile.</param>
/// <param name="loadout">The current role loadout for the user.</param>
/// <param name="session">The user's session.</param>
/// <param name="collection">The dependency injection container.</param>
/// <param name="loadoutSystem">The loadout system instance.</param>
/// <returns>A fully initialized LoadoutContainer for UI display.</returns>
private LoadoutContainer CreateLoadoutUI(LoadoutPrototype proto, HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, LoadoutSystem loadoutSystem)
{
var selected = loadout.SelectedLoadouts[_groupProto.ID];
var pressed = selected.Any(e => e.Prototype == proto.ID);
var enabled = loadout.IsValid(profile, session, proto.ID, collection, out var reason);
var cont = new LoadoutContainer(proto, !enabled, reason);
cont.Text = loadoutSystem.GetName(proto);
cont.Select.Pressed = pressed;
cont.Select.OnPressed += args =>
{
if (args.Button.Pressed)
OnLoadoutPressed?.Invoke(proto.ID);
else
OnLoadoutUnpressed?.Invoke(proto.ID);
};
return cont;
}
}

View File

@@ -0,0 +1,8 @@
<PanelContainer Name="SubContainer"
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<BoxContainer Name="SubGridContainer"
Orientation="Vertical"
HorizontalExpand="true"/>
</PanelContainer>

View File

@@ -0,0 +1,21 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Lobby.UI.Loadouts;
/// <summary>
/// A simple container used to group additional loadout UI elements
/// that are part of the same GroupBy subgroup.
///
/// - Used when a loadout group contains multiple prototypes.
/// - The first prototype is shown directly; the remaining ones are placed inside this container.
/// - Allows toggling visibility of the subgroup via expandable UI (collapsible behavior).
///
/// Internally inherits from PanelContainer to allow for border/background styling if needed.
/// Exposes its internal BoxContainer (SubGridContainer) via the <see cref="Grid"/> property for adding children.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SubLoadoutContainer : PanelContainer
{
public BoxContainer Grid => SubGridContainer;
}

View File

@@ -0,0 +1,9 @@
<Button Name="ToggleButton"
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
VerticalExpand="False"
HorizontalExpand="False"
SetSize="64 64"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0 0 5 0"/>

View File

@@ -0,0 +1,10 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Lobby.UI.Loadouts;
/// <summary>
/// A button that toggles the loadout groups. Needs for override default styles.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class ToggleLoadoutButton : Button;

View File

@@ -41,6 +41,7 @@
StyleClasses="ButtonBig" MinWidth="137" />
</BoxContainer>
</controls:StripeBack>
<RichTextLabel Name="PlaytimeComment" Visible="False" Access="Public" HorizontalAlignment="Center" />
</BoxContainer>
</PanelContainer>
<!-- Voting Popups -->

View File

@@ -454,7 +454,7 @@ public sealed class MappingState : GameplayStateBase
switch (prototype)
{
case EntityPrototype entity:
textures.AddRange(SpriteComponent.GetPrototypeTextures(entity, _resources).Select(t => t.Default));
textures.AddRange(_sprite.GetPrototypeTextures(entity).Select(t => t.Default));
break;
case DecalPrototype decal:
textures.Add(_sprite.Frame0(decal.Sprite));

View File

@@ -3,14 +3,14 @@ using Robust.Shared.Console;
namespace Content.Client.NPC;
public sealed class ShowHTNCommand : IConsoleCommand
public sealed class ShowHtnCommand : LocalizedEntityCommands
{
public string Command => "showhtn";
public string Description => "Shows the current status for HTN NPCs";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly HTNSystem _htnSystem = default!;
public override string Command => "showhtn";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var npcs = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<HTNSystem>();
npcs.EnableOverlay ^= true;
_htnSystem.EnableOverlay ^= true;
}
}

View File

@@ -137,15 +137,14 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
}
}
public sealed class ClearAllNetworkLinkOverlays : IConsoleCommand
public sealed class ClearAllNetworkLinkOverlays : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly NetworkConfiguratorSystem _network = default!;
public string Command => "clearnetworklinkoverlays";
public string Description => "Clear all network link overlays.";
public string Help => Command;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "clearnetworklinkoverlays";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_e.System<NetworkConfiguratorSystem>().ClearAllOverlays();
_network.ClearAllOverlays();
}
}

View File

@@ -1,48 +1,39 @@
using Content.Client.Administration.Managers;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Client.NodeContainer
{
public sealed class NodeVisCommand : IConsoleCommand
public sealed class NodeVisCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly NodeGroupSystem _nodeSystem = default!;
public string Command => "nodevis";
public string Description => "Toggles node group visualization";
public string Help => "";
public override string Command => "nodevis";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var adminMan = IoCManager.Resolve<IClientAdminManager>();
if (!adminMan.HasFlag(AdminFlags.Debug))
if (!_adminManager.HasFlag(AdminFlags.Debug))
{
shell.WriteError("You need +DEBUG for this command");
shell.WriteError(Loc.GetString($"shell-missing-required-permission", ("perm", "+DEBUG")));
return;
}
var sys = _e.System<NodeGroupSystem>();
sys.SetVisEnabled(!sys.VisEnabled);
_nodeSystem.SetVisEnabled(!_nodeSystem.VisEnabled);
}
}
public sealed class NodeVisFilterCommand : IConsoleCommand
public sealed class NodeVisFilterCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly NodeGroupSystem _nodeSystem = default!;
public string Command => "nodevisfilter";
public string Description => "Toggles showing a specific group on nodevis";
public string Help => "Usage: nodevis [filter]\nOmit filter to list currently masked-off";
public override string Command => "nodevisfilter";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var sys = _e.System<NodeGroupSystem>();
if (args.Length == 0)
{
foreach (var filtered in sys.Filtered)
foreach (var filtered in _nodeSystem.Filtered)
{
shell.WriteLine(filtered);
}
@@ -50,10 +41,8 @@ namespace Content.Client.NodeContainer
else
{
var filter = args[0];
if (!sys.Filtered.Add(filter))
{
sys.Filtered.Remove(filter);
}
if (!_nodeSystem.Filtered.Add(filter))
_nodeSystem.Filtered.Remove(filter);
}
}
}

View File

@@ -0,0 +1,108 @@
using Content.Shared.CCVar;
using Robust.Client.Player;
using Robust.Shared.Network;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Client.Playtime;
/// <summary>
/// Keeps track of how long the player has played today.
/// </summary>
/// <remarks>
/// <para>
/// Playtime is treated as any time in which the player is attached to an entity.
/// This notably excludes scenarios like the lobby.
/// </para>
/// </remarks>
public sealed class ClientsidePlaytimeTrackingManager
{
[Dependency] private readonly IClientNetManager _clientNetManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private ISawmill _sawmill = default!;
private const string InternalDateFormat = "yyyy-MM-dd";
[ViewVariables]
private TimeSpan? _mobAttachmentTime;
/// <summary>
/// The total amount of time played today, in minutes.
/// </summary>
[ViewVariables]
public float PlaytimeMinutesToday
{
get
{
var cvarValue = _configurationManager.GetCVar(CCVars.PlaytimeMinutesToday);
if (_mobAttachmentTime == null)
return cvarValue;
return cvarValue + (float)(_gameTiming.RealTime - _mobAttachmentTime.Value).TotalMinutes;
}
}
public void Initialize()
{
_sawmill = _logManager.GetSawmill("clientplaytime");
_clientNetManager.Connected += OnConnected;
// The downside to relying on playerattached and playerdetached is that unsaved playtime won't be saved in the event of a crash
// But then again, the config doesn't get saved in the event of a crash, either, so /shrug
// Playerdetached gets called on quit, though, so at least that's covered.
_playerManager.LocalPlayerAttached += OnPlayerAttached;
_playerManager.LocalPlayerDetached += OnPlayerDetached;
}
private void OnConnected(object? sender, NetChannelArgs args)
{
var datatimey = DateTime.Now;
_sawmill.Info($"Current day: {datatimey.Day} Current Date: {datatimey.Date.ToString(InternalDateFormat)}");
var recordedDateString = _configurationManager.GetCVar(CCVars.PlaytimeLastConnectDate);
var formattedDate = datatimey.Date.ToString(InternalDateFormat);
if (formattedDate == recordedDateString)
return;
_configurationManager.SetCVar(CCVars.PlaytimeMinutesToday, 0);
_configurationManager.SetCVar(CCVars.PlaytimeLastConnectDate, formattedDate);
}
private void OnPlayerAttached(EntityUid entity)
{
_mobAttachmentTime = _gameTiming.RealTime;
}
private void OnPlayerDetached(EntityUid entity)
{
if (_mobAttachmentTime == null)
return;
var newTimeValue = PlaytimeMinutesToday;
_mobAttachmentTime = null;
var timeDiffMinutes = newTimeValue - _configurationManager.GetCVar(CCVars.PlaytimeMinutesToday);
if (timeDiffMinutes < 0)
{
_sawmill.Error("Time differential on player detachment somehow less than zero!");
return;
}
// At less than 1 minute of time diff, there's not much point, and saving regardless will brick tests
// The reason this isn't checking for 0 is because TotalMinutes is fractional, rather than solely whole minutes
if (timeDiffMinutes < 1)
return;
_configurationManager.SetCVar(CCVars.PlaytimeMinutesToday, newTimeValue);
_sawmill.Info($"Recorded {timeDiffMinutes} minutes of living playtime!");
_configurationManager.SaveToFile(); // We don't like that we have to save the entire config just to store playtime stats '^'
}
}

View File

@@ -14,12 +14,17 @@ namespace Content.Client.UserInterface.Systems.Chat;
/// </summary>
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
{
[Dependency] private readonly ILocalizationManager _loc = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
private static readonly Regex StartDoubleQuote = new("\"$");
private static readonly Regex EndDoubleQuote = new("^\"|(?<=^@)\"");
private static readonly Regex StartAtSign = new("^@");
/// <summary>
/// The list of words to be highlighted in the chatbox.
/// </summary>
private List<string> _highlights = new();
private readonly List<string> _highlights = new();
/// <summary>
/// The string holding the hex color used to highlight words.
@@ -42,7 +47,7 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
_config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }, true);
// Load highlights if any were saved.
string highlights = _config.GetCVar(CCVars.ChatHighlights);
var highlights = _config.GetCVar(CCVars.ChatHighlights);
if (!string.IsNullOrEmpty(highlights))
{
@@ -84,12 +89,12 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
// We first subdivide the highlights based on newlines to prevent replacing
// a valid "\n" tag and adding it to the final regex.
string[] splittedHighlights = newHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var splittedHighlights = newHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
for (int i = 0; i < splittedHighlights.Length; i++)
for (var i = 0; i < splittedHighlights.Length; i++)
{
// Replace every "\" character with a "\\" to prevent "\n", "\0", etc...
string keyword = splittedHighlights[i].Replace(@"\", @"\\");
var keyword = splittedHighlights[i].Replace(@"\", @"\\");
// Escape the keyword to prevent special characters like "(" and ")" to be considered valid regex.
keyword = Regex.Escape(keyword);
@@ -102,17 +107,17 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
// that make sure the words to match are separated by spaces or punctuation.
// NOTE: The reason why we don't use \b tags is that \b doesn't match reverse slash characters "\" so
// a pre-sanitized (see 1.) string like "\[test]" wouldn't get picked up by the \b.
if (keyword.Count(c => (c == '"')) > 0)
if (keyword.Any(c => c == '"'))
{
// Matches the last double quote character.
keyword = Regex.Replace(keyword, "\"$", "(?!\\w)");
keyword = StartDoubleQuote.Replace(keyword, "(?!\\w)");
// When matching for the first double quote character we also consider the possibility
// of the double quote being preceded by a @ character.
keyword = Regex.Replace(keyword, "^\"|(?<=^@)\"", "(?<!\\w)");
keyword = EndDoubleQuote.Replace(keyword, "(?<!\\w)");
}
// Make sure any name tagged as ours gets highlighted only when others say it.
keyword = Regex.Replace(keyword, "^@", "(?<=(?<=/name.*)|(?<=,.*\"\".*))");
keyword = StartAtSign.Replace(keyword, "(?<=(?<=/name.*)|(?<=,.*\"\".*))");
_highlights.Add(keyword);
}
@@ -132,7 +137,7 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
var (_, job, _, _, entityName) = data;
// Mark this entity's name as our character name for the "UpdateHighlights" function.
string newHighlights = "@" + entityName;
var newHighlights = "@" + entityName;
// Subdivide the character's name based on spaces or hyphens so that every word gets highlighted.
if (newHighlights.Count(c => (c == ' ' || c == '-')) == 1)
@@ -144,9 +149,9 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
newHighlights = newHighlights.Split('-')[0] + "\n@" + newHighlights.Split('-')[^1];
// Convert the job title to kebab-case and use it as a key for the loc file.
string jobKey = job.Replace(' ', '-').ToLower();
var jobKey = job.Replace(' ', '-').ToLower();
if (Loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
if (_loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
newHighlights += '\n' + jobMatches.Replace(", ", "\n");
UpdateHighlights(newHighlights);

View File

@@ -19,8 +19,11 @@ namespace Content.Client.UserInterface.Systems.Chat.Widgets;
[Virtual]
public partial class ChatBox : UIWidget
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ILogManager _log = default!;
private readonly ISawmill _sawmill;
private readonly ChatUIController _controller;
private readonly IEntityManager _entManager;
public bool Main { get; set; }
@@ -29,7 +32,7 @@ public partial class ChatBox : UIWidget
public ChatBox()
{
RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>();
_sawmill = _log.GetSawmill("chat");
ChatInput.Input.OnTextEntered += OnTextEntered;
ChatInput.Input.OnKeyBindDown += OnInputKeyBindDown;
@@ -52,7 +55,7 @@ public partial class ChatBox : UIWidget
private void OnMessageAdded(ChatMessage msg)
{
Logger.DebugS("chat", $"{msg.Channel}: {msg.Message}");
_sawmill.Debug($"{msg.Channel}: {msg.Message}");
if (!ChatInput.FilterButton.Popup.IsActive(msg.Channel))
{
return;

View File

@@ -225,6 +225,10 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
if (!IsDragging && EntityManager.System<HandsSystem>().GetActiveHandEntity() == null)
return;
// Do not rotate items unless we are either dragging them or hovering over a storage window.
if (DraggingGhost is null && UIManager.CurrentlyHovered is not StorageWindow)
return;
//clamp it to a cardinal.
DraggingRotation = (DraggingRotation + Math.PI / 2f).GetCardinalDir().ToAngle();
if (DraggingGhost != null)

View File

@@ -274,13 +274,11 @@ namespace Content.Client.Voting.UI
}
[UsedImplicitly, AnyCommand]
public sealed class VoteMenuCommand : IConsoleCommand
public sealed class VoteMenuCommand : LocalizedCommands
{
public string Command => "votemenu";
public string Description => Loc.GetString("ui-vote-menu-command-description");
public string Help => Loc.GetString("ui-vote-menu-command-help-text");
public override string Command => "votemenu";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
new VoteCallMenu().OpenCentered();
}

View File

@@ -1,18 +1,18 @@
using Content.Client.Weapons.Ranged.Systems;
using Robust.Shared.Console;
namespace Content.Client.Weapons.Ranged;
namespace Content.Client.Weapons.Ranged.Commands;
public sealed class ShowSpreadCommand : IConsoleCommand
public sealed class ShowSpreadCommand : LocalizedEntityCommands
{
public string Command => "showgunspread";
public string Description => $"Shows gun spread overlay for debugging";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GunSystem>();
system.SpreadOverlay ^= true;
[Dependency] private readonly GunSystem _gunSystem = default!;
shell.WriteLine($"Set spread overlay to {system.SpreadOverlay}");
public override string Command => "showgunspread";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_gunSystem.SpreadOverlay ^= true;
shell.WriteLine(Loc.GetString($"cmd-showgunspread-status", ("status", _gunSystem.SpreadOverlay)));
}
}

View File

@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Text;
using Content.Client.Implants;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Clothing;
using Content.Shared.Implants;
@@ -11,17 +9,17 @@ using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Chameleon;
/// <summary>
/// Ensures all round <see cref="IsProbablyRoundStartJob">"round start jobs"</see> have an associated chameleon loadout.
/// Ensures all <see cref="IsProbablyRoundStartJob">"round start jobs"</see> have an associated chameleon loadout.
/// </summary>
public sealed class ChameleonJobLoadoutTest : InteractionTest
{
private readonly List<ProtoId<JobPrototype>> JobBlacklist =
private static readonly List<ProtoId<JobPrototype>> JobBlacklist =
[
];
[Test]
public async Task CheckAllJobs()
public Task CheckAllJobs()
{
var alljobs = ProtoMan.EnumeratePrototypes<JobPrototype>();
@@ -47,24 +45,16 @@ public sealed class ChameleonJobLoadoutTest : InteractionTest
validJobs[chameleon.Job.Value] += 1;
}
var errorMessage = new StringBuilder();
errorMessage.AppendLine("The following job(s) have no chameleon prototype(s):");
var invalid = false;
// All round start jobs have a chameleon loadout
foreach (var job in validJobs)
Assert.Multiple(() =>
{
if (job.Value != 0)
continue;
foreach (var job in validJobs)
{
Assert.That(job.Value, Is.Not.Zero,
$"{job.Key} has no chameleonOutfit prototype.");
}
});
errorMessage.AppendLine(job.Key + " has no chameleonOutfit prototype.");
invalid = true;
}
if (!invalid)
return;
Assert.Fail(errorMessage.ToString());
return Task.CompletedTask;
}
/// <summary>

View File

@@ -16,6 +16,7 @@ namespace Content.IntegrationTests.Tests
var client = pair.Client;
var prototypeManager = client.ResolveDependency<IPrototypeManager>();
var resourceCache = client.ResolveDependency<IResourceCache>();
var spriteSys = client.System<SpriteSystem>();
await client.WaitAssertion(() =>
{
@@ -26,7 +27,7 @@ namespace Content.IntegrationTests.Tests
Assert.DoesNotThrow(() =>
{
var _ = SpriteComponent.GetPrototypeTextures(proto, resourceCache).ToList();
var _ = spriteSys.GetPrototypeTextures(proto).ToList();
}, "Prototype {0} threw an exception when getting its textures.",
proto.ID);
}

View File

@@ -0,0 +1,50 @@
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Localization;
public sealed class EntityPrototypeLocalizationTest
{
/// <summary>
/// An explanation of why LocIds should not be used for entity prototype names/descriptions.
/// Appended to the error message when the test is failed.
/// </summary>
private const string NoLocIdExplanation = "Entity prototypes should not use LocIds for names/descriptions, as localization IDs are automated for entity prototypes. See https://docs.spacestation14.com/en/ss14-by-example/fluent-and-localization.html#localizing-prototypes for more information.";
/// <summary>
/// Checks that no entity prototypes have a LocId as their name or description.
/// See <see href="https://docs.spacestation14.com/en/ss14-by-example/fluent-and-localization.html#localizing-prototypes"/> for why this is important.
/// </summary>
[Test]
public async Task TestNoManualEntityLocStrings()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoMan = server.ProtoMan;
var locMan = server.ResolveDependency<ILocalizationManager>();
var protos = protoMan.EnumeratePrototypes<EntityPrototype>();
Assert.Multiple(() =>
{
foreach (var proto in protos)
{
// Check name
if (!string.IsNullOrEmpty(proto.SetName))
{
Assert.That(locMan.HasString(proto.SetName), Is.False,
$"Entity prototype {proto.ID} has a LocId ({proto.SetName}) as a name. {NoLocIdExplanation}");
}
// Check description
if (!string.IsNullOrEmpty(proto.SetDesc))
{
Assert.That(locMan.HasString(proto.SetDesc), Is.False,
$"Entity prototype {proto.ID} has a LocId ({proto.SetDesc}) as a description. {NoLocIdExplanation}");
}
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -91,7 +91,7 @@ namespace Content.IntegrationTests.Tests
"Elkridge",
"Relic",
"dm01-entryway",
"Exo",
};
/// <summary>

View File

@@ -51,7 +51,7 @@ public static class ServerPackaging
// Python script had Npgsql. though we want Npgsql.dll as well soooo
"Npgsql",
"Microsoft",
"Discord",
"NetCord",
};
private static readonly List<string> ServerNotExtraAssemblies = new()

View File

@@ -0,0 +1,74 @@
using System.Linq;
using Content.Server.Administration;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Administration;
using Content.Shared.Prototypes;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Actions.Commands;
[AdminCommand(AdminFlags.Debug)]
public sealed class AddActionCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Command => "addaction";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString(Loc.GetString("cmd-addaction-invalid-args")));
return;
}
if (!NetEntity.TryParse(args[0], out var targetUidNet) || !EntityManager.TryGetEntity(targetUidNet, out var targetEntity))
{
shell.WriteLine(Loc.GetString("shell-entity-uid-must-be-number"));
return;
}
if (!EntityManager.HasComponent<ActionsComponent>(targetEntity))
{
shell.WriteError(Loc.GetString("cmd-addaction-actions-not-found"));
return;
}
if (!_prototypeManager.TryIndex<EntityPrototype>(args[1], out var proto) ||
!proto.HasComponent<ActionComponent>())
{
shell.WriteError(Loc.GetString("cmd-addaction-action-not-found", ("action", args[1])));
return;
}
if (_actions.AddAction(targetEntity.Value, args[1]) == null)
{
shell.WriteError(Loc.GetString("cmd-addaction-adding-failed"));
}
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.Components<ActionsComponent>(args[0]),
Loc.GetString("cmd-addaction-player-completion"));
}
if (args.Length != 2)
return CompletionResult.Empty;
var actionPrototypes = _prototypeManager.EnumeratePrototypes<EntityPrototype>()
.Where(p => p.HasComponent<ActionComponent>())
.Select(p => p.ID)
.Order();
return CompletionResult.FromHintOptions(
actionPrototypes,
Loc.GetString("cmd-addaction-action-completion"));
}
}

View File

@@ -0,0 +1,84 @@
using Content.Server.Administration;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Actions.Commands;
[AdminCommand(AdminFlags.Debug)]
public sealed class RemoveActionCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
public override string Command => "rmaction";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString(Loc.GetString("cmd-rmaction-invalid-args")));
return;
}
if (!NetEntity.TryParse(args[0], out var targetUidNet) || !EntityManager.TryGetEntity(targetUidNet, out var targetEntity))
{
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
return;
}
if (!NetEntity.TryParse(args[1], out var targetActionUidNet) || !EntityManager.TryGetEntity(targetActionUidNet, out var targetActionEntity))
{
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[1])));
return;
}
if (!EntityManager.HasComponent<ActionsComponent>(targetEntity))
{
shell.WriteError(Loc.GetString("cmd-rmaction-actions-not-found"));
return;
}
if (_actions.GetAction(targetActionEntity) is not { } ent)
{
shell.WriteError(Loc.GetString("cmd-rmaction-not-an-action"));
return;
}
_actions.SetTemporary(ent.Owner, true);
_actions.RemoveAction(ent.Owner);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.Components<ActionsComponent>(args[0]),
Loc.GetString("cmd-rmaction-player-completion"));
}
if (args.Length == 2)
{
if (!NetEntity.TryParse(args[0], out var targetUidNet) || !EntityManager.TryGetEntity(targetUidNet, out var targetEntity))
return CompletionResult.Empty;
if (!EntityManager.HasComponent<ActionsComponent>(targetEntity))
return CompletionResult.Empty;
var actions = _actions.GetActions(targetEntity.Value);
var options = new List<CompletionOption>();
foreach (var action in actions)
{
var hint = Loc.GetString("cmd-rmaction-action-info", ("action", action));
options.Add(new CompletionOption(action.Owner.ToString(), hint));
}
return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-rmaction-action-completion"));
}
return CompletionResult.Empty;
}
}

View File

@@ -1,4 +1,4 @@
using System.Text;
using System.Text;
using Content.Server.Administration.Managers;
using Content.Server.Afk;
using Content.Shared.Administration;
@@ -8,32 +8,30 @@ using Robust.Shared.Utility;
namespace Content.Server.Administration.Commands;
[AnyCommand] // Corvax: Allow use to everyone
public sealed class AdminWhoCommand : IConsoleCommand
public sealed class AdminWhoCommand : LocalizedCommands
{
public string Command => "adminwho";
public string Description => "Returns a list of all admins on the server";
public string Help => "Usage: adminwho";
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "adminwho";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var adminMgr = IoCManager.Resolve<IAdminManager>();
var afk = IoCManager.Resolve<IAfkManager>();
var seeStealth = true;
// If null it (hopefully) means it is being called from the console.
if (shell.Player != null)
{
var playerData = adminMgr.GetAdminData(shell.Player);
var playerData = _adminManager.GetAdminData(shell.Player);
seeStealth = playerData != null && playerData.CanStealth();
}
var sb = new StringBuilder();
var first = true;
foreach (var admin in adminMgr.ActiveAdmins)
foreach (var admin in _adminManager.ActiveAdmins)
{
var adminData = adminMgr.GetAdminData(admin)!;
var adminData = _adminManager.GetAdminData(admin)!;
DebugTools.AssertNotNull(adminData);
if (adminData.Stealth && !seeStealth)
@@ -50,9 +48,9 @@ public sealed class AdminWhoCommand : IConsoleCommand
if (adminData.Stealth)
sb.Append(" (S)");
if (shell.Player is { } player && adminMgr.HasAdminFlag(player, AdminFlags.Admin))
if (shell.Player is { } player && _adminManager.HasAdminFlag(player, AdminFlags.Admin))
{
if (afk.IsAfk(admin))
if (_afkManager.IsAfk(admin))
sb.Append(" [AFK]");
}
}

View File

@@ -6,26 +6,23 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Moderator)]
public sealed class AnnounceUiCommand : IConsoleCommand
public sealed class AnnounceUiCommand : LocalizedEntityCommands
{
public string Command => "announceui";
[Dependency] private readonly EuiManager _euiManager = default!;
public string Description => "Opens the announcement UI";
public override string Command => "announceui";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("This does not work from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new AdminAnnounceEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}
}

View File

@@ -7,23 +7,22 @@ namespace Content.Server.Administration.Commands
{
[UsedImplicitly]
[AdminCommand(AdminFlags.None)]
public sealed class DeAdminCommand : IConsoleCommand
public sealed class DeAdminCommand : LocalizedCommands
{
public string Command => "deadmin";
public string Description => "Temporarily de-admins you so you can experience the round as a normal player.";
public string Help => "Usage: deadmin\nUse readmin to re-admin after using this.";
[Dependency] private readonly IAdminManager _admin = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "deadmin";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("You cannot use this command from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
mgr.DeAdmin(player);
_admin.DeAdmin(player);
}
}
}

View File

@@ -4,42 +4,40 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Spawn)]
public sealed class DeleteComponent : IConsoleCommand
public sealed class DeleteComponent : LocalizedEntityCommands
{
public string Command => "deletecomponent";
public string Description => "Deletes all instances of the specified component.";
public string Help => $"Usage: {Command} <name>";
[Dependency] private readonly IComponentFactory _compFactory = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "deletecomponent";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
shell.WriteLine($"Not enough arguments.\n{Help}");
shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument"));
break;
default:
var name = string.Join(" ", args);
var componentFactory = IoCManager.Resolve<IComponentFactory>();
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!componentFactory.TryGetRegistration(name, out var registration))
if (!_compFactory.TryGetRegistration(name, out var registration))
{
shell.WriteLine($"No component exists with name {name}.");
shell.WriteLine(Loc.GetString($"cmd-deletecomponent-no-component-exists", ("name", name)));
break;
}
var componentType = registration.Type;
var components = entityManager.GetAllComponents(componentType, true);
var components = EntityManager.GetAllComponents(componentType, true);
var i = 0;
foreach (var (uid, component) in components)
{
entityManager.RemoveComponent(uid, component);
EntityManager.RemoveComponent(uid, component);
i++;
}
shell.WriteLine($"Removed {i} components with name {name}.");
shell.WriteLine(Loc.GetString($"cmd-deletecomponent-success", ("count", i), ("name", name)));
break;
}

View File

@@ -13,72 +13,73 @@ using Robust.Server.GameObjects;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
public sealed class OpenExplosionEui : IConsoleCommand
public sealed class OpenExplosionEui : LocalizedEntityCommands
{
public string Command => "explosionui";
public string Description => "Opens a window for easy access to station destruction";
public string Help => $"Usage: {Command}";
[Dependency] private readonly EuiManager _euiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "explosionui";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteError("This does not work from the server console.");
shell.WriteError(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new SpawnExplosionEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}
[AdminCommand(AdminFlags.Fun)] // for the admin. Not so much for anyone else.
public sealed class ExplosionCommand : IConsoleCommand
public sealed class ExplosionCommand : LocalizedEntityCommands
{
public string Command => "explosion";
public string Description => "Train go boom";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ExplosionSystem _explosion = default!;
[Dependency] private readonly TransformSystem _transform = default!;
public override string Command => "explosion";
// Note that if you change the arguments, you should also update the client-side SpawnExplosionWindow, as that just
// uses this command.
public string Help => "Usage: explosion [intensity] [slope] [maxIntensity] [x y] [mapId] [prototypeId]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0 || args.Length == 4 || args.Length > 7)
{
shell.WriteError("Wrong number of arguments.");
shell.WriteError(Loc.GetString($"shell-wrong-arguments-number"));
return;
}
if (!float.TryParse(args[0], out var intensity))
{
shell.WriteError($"Failed to parse intensity: {args[0]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-intensity", ("value", args[0])));
return;
}
float slope = 5;
if (args.Length > 1 && !float.TryParse(args[1], out slope))
{
shell.WriteError($"Failed to parse float: {args[1]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-float", ("value", args[1])));
return;
}
float maxIntensity = 100;
if (args.Length > 2 && !float.TryParse(args[2], out maxIntensity))
{
shell.WriteError($"Failed to parse float: {args[2]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-float", ("value", args[2])));
return;
}
float x = 0, y = 0;
if (args.Length > 4)
{
if (!float.TryParse(args[3], out x) ||
!float.TryParse(args[4], out y))
if (!float.TryParse(args[3], out x) || !float.TryParse(args[4], out y))
{
shell.WriteError($"Failed to parse coordinates: {(args[3], args[4])}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-coords",
("value1", args[3]),
("value2", args[4])));
return;
}
}
@@ -88,7 +89,7 @@ public sealed class ExplosionCommand : IConsoleCommand
{
if (!int.TryParse(args[5], out var parsed))
{
shell.WriteError($"Failed to parse map ID: {args[5]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-map-id", ("value", args[5])));
return;
}
coords = new MapCoordinates(new Vector2(x, y), new(parsed));
@@ -96,42 +97,39 @@ public sealed class ExplosionCommand : IConsoleCommand
else
{
// attempt to find the player's current position
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(shell.Player?.AttachedEntity, out TransformComponent? xform))
if (!EntityManager.TryGetComponent(shell.Player?.AttachedEntity, out TransformComponent? xform))
{
shell.WriteError($"Failed get default coordinates/map via player's transform. Need to specify explicitly.");
shell.WriteError(Loc.GetString($"cmd-explosion-need-coords-explicit"));
return;
}
if (args.Length > 4)
coords = new MapCoordinates(new Vector2(x, y), xform.MapID);
else
coords = entMan.System<TransformSystem>().GetMapCoordinates(shell.Player.AttachedEntity.Value, xform: xform);
coords = _transform.GetMapCoordinates(shell.Player.AttachedEntity.Value, xform: xform);
}
ExplosionPrototype? type;
var protoMan = IoCManager.Resolve<IPrototypeManager>();
if (args.Length > 6)
{
if (!protoMan.TryIndex(args[6], out type))
if (!_prototypeManager.TryIndex(args[6], out type))
{
shell.WriteError($"Unknown explosion prototype: {args[6]}");
shell.WriteError(Loc.GetString($"cmd-explosion-unknown-prototype", ("value", args[6])));
return;
}
}
else if (!protoMan.TryIndex(ExplosionSystem.DefaultExplosionPrototypeId, out type))
else if (!_prototypeManager.TryIndex(ExplosionSystem.DefaultExplosionPrototypeId, out type))
{
// no prototype was specified, so lets default to whichever one was defined first
type = protoMan.EnumeratePrototypes<ExplosionPrototype>().FirstOrDefault();
type = _prototypeManager.EnumeratePrototypes<ExplosionPrototype>().FirstOrDefault();
if (type == null)
{
shell.WriteError($"Prototype manager has no explosion prototypes?");
shell.WriteError(Loc.GetString($"cmd-explosion-no-prototypes"));
return;
}
}
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
sysMan.GetEntitySystem<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity, null);
_explosion.QueueExplosion(coords, type.ID, intensity, slope, maxIntensity, null);
}
}

View File

@@ -6,14 +6,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
public sealed class FaxUiCommand : IConsoleCommand
public sealed class FaxUiCommand : LocalizedEntityCommands
{
public string Command => "faxui";
[Dependency] private readonly EuiManager _euiManager = default!;
public string Description => Loc.GetString("cmd-faxui-desc");
public string Help => Loc.GetString("cmd-faxui-help");
public override string Command => "faxui";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
@@ -21,8 +20,7 @@ public sealed class FaxUiCommand : IConsoleCommand
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new AdminFaxEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}

View File

@@ -3,42 +3,36 @@ using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.EntitySerialization;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Round | AdminFlags.Spawn)]
public sealed class LoadGameMapCommand : IConsoleCommand
public sealed class LoadGameMapCommand : LocalizedEntityCommands
{
public string Command => "loadgamemap";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public string Description => "Loads the given game map at the given coordinates.";
public override string Command => "loadgamemap";
public string Help => "loadgamemap <mapid> <gamemap> [<x> <y> [<name>]] ";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var gameTicker = entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
var mapSys = entityManager.EntitySysManager.GetEntitySystem<SharedMapSystem>();
if (args.Length is not (2 or 4 or 5))
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
if (!prototypeManager.TryIndex<GameMapPrototype>(args[1], out var gameMap))
if (!_prototypeManager.TryIndex<GameMapPrototype>(args[1], out var gameMap))
{
shell.WriteError($"The given map prototype {args[0]} is invalid.");
return;
}
if (!int.TryParse(args[0], out var mapId))
return;
return;
var stationName = args.Length == 5 ? args[4] : null;
@@ -48,14 +42,14 @@ namespace Content.Server.Administration.Commands
var id = new MapId(mapId);
var grids = mapSys.MapExists(id)
? gameTicker.MergeGameMap(gameMap, id, stationName: stationName, offset: offset)
: gameTicker.LoadGameMapWithId(gameMap, id, stationName: stationName, offset: offset);
var grids = _mapSystem.MapExists(id)
? _gameTicker.MergeGameMap(gameMap, id, stationName: stationName, offset: offset)
: _gameTicker.LoadGameMapWithId(gameMap, id, stationName: stationName, offset: offset);
shell.WriteLine($"Loaded {grids.Count} grids.");
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
@@ -79,26 +73,20 @@ namespace Content.Server.Administration.Commands
}
[AdminCommand(AdminFlags.Round | AdminFlags.Spawn)]
public sealed class ListGameMaps : IConsoleCommand
public sealed class ListGameMaps : LocalizedCommands
{
public string Command => "listgamemaps";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Description => "Lists the game maps that can be used by loadgamemap";
public override string Command => "listgamemaps";
public string Help => "listgamemaps";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var gameTicker = entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
if (args.Length != 0)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
foreach (var prototype in prototypeManager.EnumeratePrototypes<GameMapPrototype>())
foreach (var prototype in _prototypeManager.EnumeratePrototypes<GameMapPrototype>())
{
shell.WriteLine($"{prototype.ID} - {prototype.MapName}");
}

View File

@@ -6,13 +6,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Logs)]
public sealed class OpenAdminLogsCommand : IConsoleCommand
public sealed class OpenAdminLogsCommand : LocalizedEntityCommands
{
public string Command => "adminlogs";
public string Description => "Opens the admin logs panel.";
public string Help => $"Usage: {Command}";
[Dependency] private readonly EuiManager _euiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "adminlogs";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
@@ -20,8 +20,7 @@ public sealed class OpenAdminLogsCommand : IConsoleCommand
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new AdminLogsEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}

View File

@@ -6,24 +6,23 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Permissions)]
public sealed class OpenPermissionsCommand : IConsoleCommand
public sealed class OpenPermissionsCommand : LocalizedEntityCommands
{
public string Command => "permissions";
public string Description => "Opens the admin permissions panel.";
public string Help => "Usage: permissions";
[Dependency] private readonly EuiManager _euiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "permissions";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("This does not work from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new PermissionsEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}
}

View File

@@ -1,21 +1,19 @@
using System.Text;
using Content.Server.Database;
using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Ban)]
public sealed class PardonCommand : IConsoleCommand
public sealed class PardonCommand : LocalizedCommands
{
public string Command => "pardon";
public string Description => "Pardons somebody's ban";
public string Help => $"Usage: {Command} <ban id>";
[Dependency] private readonly IServerDbManager _dbManager = default!;
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "pardon";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
var dbMan = IoCManager.Resolve<IServerDbManager>();
if (args.Length != 1)
{
@@ -25,11 +23,11 @@ namespace Content.Server.Administration.Commands
if (!int.TryParse(args[0], out var banId))
{
shell.WriteLine($"Unable to parse {args[0]} as a ban id integer.\n{Help}");
shell.WriteLine(Loc.GetString($"cmd-pardon-unable-to-parse", ("id", args[0]), ("help", Help)));
return;
}
var ban = await dbMan.GetServerBanAsync(banId);
var ban = await _dbManager.GetServerBanAsync(banId);
if (ban == null)
{
@@ -39,22 +37,22 @@ namespace Content.Server.Administration.Commands
if (ban.Unban != null)
{
var response = new StringBuilder("This ban has already been pardoned");
if (ban.Unban.UnbanningAdmin != null)
{
response.Append($" by {ban.Unban.UnbanningAdmin.Value}");
shell.WriteLine(Loc.GetString($"cmd-pardon-already-pardoned-specific",
("admin", ban.Unban.UnbanningAdmin.Value),
("time", ban.Unban.UnbanTime)));
}
response.Append($" in {ban.Unban.UnbanTime}.");
else
shell.WriteLine(Loc.GetString($"cmd-pardon-already-pardoned"));
shell.WriteLine(response.ToString());
return;
}
await dbMan.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now));
await _dbManager.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now));
shell.WriteLine($"Pardoned ban with id {banId}");
shell.WriteLine(Loc.GetString($"cmd-pardon-success", ("id", banId)));
}
}
}

View File

@@ -6,29 +6,28 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[UsedImplicitly]
public sealed class PromoteHostCommand : IConsoleCommand
public sealed class PromoteHostCommand : LocalizedCommands
{
public string Command => "promotehost";
public string Description => "Grants client temporary full host admin privileges. Use this to bootstrap admins.";
public string Help => "Usage promotehost <player>";
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "promotehost";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine("Expected exactly one argument.");
shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
var plyMgr = IoCManager.Resolve<IPlayerManager>();
if (!plyMgr.TryGetSessionByUsername(args[0], out var targetPlayer))
if (!_playerManager.TryGetSessionByUsername(args[0], out var targetPlayer))
{
shell.WriteLine("Unable to find a player by that name.");
shell.WriteLine(Loc.GetString($"shell-target-player-does-not-exist"));
return;
}
var adminMgr = IoCManager.Resolve<IAdminManager>();
adminMgr.PromoteHost(targetPlayer);
_adminManager.PromoteHost(targetPlayer);
}
}
}

View File

@@ -5,30 +5,28 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AnyCommand]
public sealed class ReAdminCommand : IConsoleCommand
public sealed class ReAdminCommand : LocalizedCommands
{
public string Command => "readmin";
public string Description => "Re-admins you if you previously de-adminned.";
public string Help => "Usage: readmin";
[Dependency] private readonly IAdminManager _adminManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "readmin";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("You cannot use this command from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
if (mgr.GetAdminData(player, includeDeAdmin: true) == null)
if (_adminManager.GetAdminData(player, includeDeAdmin: true) == null)
{
shell.WriteLine("You're not an admin.");
shell.WriteLine(Loc.GetString($"cmd-readmin-not-an-admin"));
return;
}
mgr.ReAdmin(player);
_adminManager.ReAdmin(player);
}
}
}

View File

@@ -5,46 +5,43 @@ using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Mapping)]
public sealed class RemoveExtraComponents : IConsoleCommand
public sealed class RemoveExtraComponents : LocalizedEntityCommands
{
public string Command => "removeextracomponents";
public string Description => "Removes all components from all entities of the specified id if that component is not in its prototype.\nIf no id is specified, it matches all entities.";
public string Help => $"{Command} <entityId> / {Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Command => "removeextracomponents";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var id = args.Length == 0 ? null : string.Join(" ", args);
var entityManager = IoCManager.Resolve<IEntityManager>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var fac = IoCManager.Resolve<IComponentFactory>();
EntityPrototype? prototype = null;
var checkPrototype = !string.IsNullOrEmpty(id);
if (checkPrototype && !prototypeManager.TryIndex(id!, out prototype))
if (checkPrototype && !_prototypeManager.TryIndex(id!, out prototype))
{
shell.WriteError($"Can't find entity prototype with id \"{id}\"!");
shell.WriteError(Loc.GetString($"cmd-removeextracomponents-invalid-prototype-id", ("id", $"{id}")));
return;
}
var entities = 0;
var components = 0;
foreach (var entity in entityManager.GetEntities())
foreach (var entity in EntityManager.GetEntities())
{
var metaData = entityManager.GetComponent<MetaDataComponent>(entity);
var metaData = EntityManager.GetComponent<MetaDataComponent>(entity);
if (checkPrototype && metaData.EntityPrototype != prototype || metaData.EntityPrototype == null)
{
continue;
}
var modified = false;
foreach (var component in entityManager.GetComponents(entity))
foreach (var component in EntityManager.GetComponents(entity))
{
if (metaData.EntityPrototype.Components.ContainsKey(fac.GetComponentName(component.GetType())))
if (metaData.EntityPrototype.Components.ContainsKey(_compFactory.GetComponentName(component.GetType())))
continue;
entityManager.RemoveComponent(entity, component);
EntityManager.RemoveComponent(entity, component);
components++;
modified = true;
@@ -54,7 +51,18 @@ namespace Content.Server.Administration.Commands
entities++;
}
shell.WriteLine($"Removed {components} components from {entities} entities{(id == null ? "." : $" with id {id}")}");
if (id != null)
{
shell.WriteLine(Loc.GetString($"cmd-removeextracomponents-success-with-id",
("count", components),
("entities", entities),
("id", id)));
return;
}
shell.WriteLine(Loc.GetString($"cmd-removeextracomponents-success",
("count", components),
("entities", entities)));
}
}
}

View File

@@ -5,13 +5,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
public sealed class RoleUnbanCommand : IConsoleCommand
public sealed class RoleUnbanCommand : LocalizedCommands
{
public string Command => "roleunban";
public string Description => Loc.GetString("cmd-roleunban-desc");
public string Help => Loc.GetString("cmd-roleunban-help");
[Dependency] private readonly IBanManager _banManager = default!;
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "roleunban";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
@@ -21,16 +21,15 @@ public sealed class RoleUnbanCommand : IConsoleCommand
if (!int.TryParse(args[0], out var banId))
{
shell.WriteLine($"Unable to parse {args[0]} as a ban id integer.\n{Help}");
shell.WriteLine(Loc.GetString($"cmd-roleunban-unable-to-parse-id", ("id", args[0]), ("help", Help)));
return;
}
var banManager = IoCManager.Resolve<IBanManager>();
var response = await banManager.PardonRoleBan(banId, shell.Player?.UserId, DateTimeOffset.Now);
var response = await _banManager.PardonRoleBan(banId, shell.Player?.UserId, DateTimeOffset.Now);
shell.WriteLine(response);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
// Can't think of good way to do hint options for this
return args.Length switch

View File

@@ -6,13 +6,14 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.NameColor)]
internal sealed class SetAdminOOC : IConsoleCommand
internal sealed class SetAdminOOC : LocalizedCommands
{
public string Command => "setadminooc";
public string Description => Loc.GetString("set-admin-ooc-command-description", ("command", Command));
public string Help => Loc.GetString("set-admin-ooc-command-help-text", ("command", Command));
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IServerPreferencesManager _preferenceManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "setadminooc";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player == null)
{
@@ -36,11 +37,9 @@ namespace Content.Server.Administration.Commands
var userId = shell.Player.UserId;
// Save the DB
var dbMan = IoCManager.Resolve<IServerDbManager>();
dbMan.SaveAdminOOCColorAsync(userId, color.Value);
_dbManager.SaveAdminOOCColorAsync(userId, color.Value);
// Update the cached preference
var prefManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = prefManager.GetPreferences(userId);
var prefs = _preferenceManager.GetPreferences(userId);
prefs.AdminOOCColor = color.Value;
}
}

View File

@@ -1,4 +1,3 @@
using Content.Server.Players;
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
@@ -9,17 +8,16 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
sealed class SetMindCommand : IConsoleCommand
public sealed class SetMindCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
public string Command => "setmind";
public override string Command => "setmind";
public string Description => Loc.GetString("set-mind-command-description", ("requiredComponent", nameof(MindContainerComponent)));
public override string Description => Loc.GetString("cmd-setmind-desc", ("requiredComponent", nameof(MindContainerComponent)));
public string Help => Loc.GetString("set-mind-command-help-text", ("command", Command));
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
@@ -33,7 +31,7 @@ namespace Content.Server.Administration.Commands
return;
}
bool ghostOverride = true;
var ghostOverride = true;
if (args.Length > 2)
{
ghostOverride = bool.Parse(args[2]);
@@ -41,19 +39,19 @@ namespace Content.Server.Administration.Commands
var nent = new NetEntity(entInt);
if (!_entManager.TryGetEntity(nent, out var eUid))
if (!EntityManager.TryGetEntity(nent, out var eUid))
{
shell.WriteLine(Loc.GetString("shell-invalid-entity-id"));
return;
}
if (!_entManager.HasComponent<MindContainerComponent>(eUid))
if (!EntityManager.HasComponent<MindContainerComponent>(eUid))
{
shell.WriteLine(Loc.GetString("set-mind-command-target-has-no-mind-message"));
shell.WriteLine(Loc.GetString("cmd-setmind-target-has-no-mind-message"));
return;
}
if (!IoCManager.Resolve<IPlayerManager>().TryGetSessionByUsername(args[1], out var session))
if (!_playerManager.TryGetSessionByUsername(args[1], out var session))
{
shell.WriteLine(Loc.GetString("shell-target-player-does-not-exist"));
return;
@@ -63,24 +61,21 @@ namespace Content.Server.Administration.Commands
var playerCData = session.ContentData();
if (playerCData == null)
{
shell.WriteLine(Loc.GetString("set-mind-command-target-has-no-content-data-message"));
shell.WriteLine(Loc.GetString("cmd-setmind-target-has-no-content-data-message"));
return;
}
var mindSystem = _entManager.System<SharedMindSystem>();
var metadata = _entManager.GetComponent<MetaDataComponent>(eUid.Value);
var metadata = EntityManager.GetComponent<MetaDataComponent>(eUid.Value);
var mind = playerCData.Mind ?? mindSystem.CreateMind(session.UserId, metadata.EntityName);
var mind = playerCData.Mind ?? _mindSystem.CreateMind(session.UserId, metadata.EntityName);
mindSystem.TransferTo(mind, eUid, ghostOverride);
_mindSystem.TransferTo(mind, eUid, ghostOverride);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("cmd-mind-command-hint"));
}
return CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Help);
return CompletionResult.Empty;
}

View File

@@ -1,36 +1,22 @@
using Content.Server.Administration.UI;
using Content.Server.Clothing.Systems;
using Content.Server.EUI;
using Content.Server.Hands.Systems;
using Content.Server.Preferences.Managers;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Content.Shared.Clothing;
using Content.Shared.Hands.Components;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
public sealed class SetOutfitCommand : IConsoleCommand
public sealed class SetOutfitCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly OutfitSystem _outfitSystem = default!;
public string Command => "setoutfit";
public override string Command => "setoutfit";
public override string Description => Loc.GetString("cmd-setoutfit-desc", ("requiredComponent", nameof(InventoryComponent)));
public string Description => Loc.GetString("set-outfit-command-description", ("requiredComponent", nameof(InventoryComponent)));
public string Help => Loc.GetString("set-outfit-command-help-text", ("command", Command));
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 1)
{
@@ -46,13 +32,13 @@ namespace Content.Server.Administration.Commands
var nent = new NetEntity(entInt);
if (!_entities.TryGetEntity(nent, out var target))
if (!EntityManager.TryGetEntity(nent, out var target))
{
shell.WriteLine(Loc.GetString("shell-invalid-entity-id"));
return;
}
if (!_entities.HasComponent<InventoryComponent>(target))
if (!EntityManager.HasComponent<InventoryComponent>(target))
{
shell.WriteLine(Loc.GetString("shell-target-entity-does-not-have-message", ("missing", "inventory")));
return;
@@ -62,109 +48,17 @@ namespace Content.Server.Administration.Commands
{
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("set-outfit-command-is-not-player-error"));
shell.WriteError(Loc.GetString("cmd-setoutfit-is-not-player-error"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new SetOutfitEui(nent);
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
return;
}
if (!SetOutfit(target.Value, args[1], _entities))
shell.WriteLine(Loc.GetString("set-outfit-command-invalid-outfit-id-error"));
}
public static bool SetOutfit(EntityUid target, string gear, IEntityManager entityManager, Action<EntityUid, EntityUid>? onEquipped = null)
{
if (!entityManager.TryGetComponent(target, out InventoryComponent? inventoryComponent))
return false;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex<StartingGearPrototype>(gear, out var startingGear))
return false;
HumanoidCharacterProfile? profile = null;
ICommonSession? session = null;
// Check if we are setting the outfit of a player to respect the preferences
if (entityManager.TryGetComponent(target, out ActorComponent? actorComponent))
{
session = actorComponent.PlayerSession;
var userId = actorComponent.PlayerSession.UserId;
var preferencesManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = preferencesManager.GetPreferences(userId);
profile = prefs.SelectedCharacter as HumanoidCharacterProfile;
}
var invSystem = entityManager.System<InventorySystem>();
if (invSystem.TryGetSlots(target, out var slots))
{
foreach (var slot in slots)
{
invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
var gearStr = ((IEquipmentLoadout) startingGear).GetGear(slot.Name);
if (gearStr == string.Empty)
{
continue;
}
var equipmentEntity = entityManager.SpawnEntity(gearStr, entityManager.GetComponent<TransformComponent>(target).Coordinates);
if (slot.Name == "id" &&
entityManager.TryGetComponent(equipmentEntity, out PdaComponent? pdaComponent) &&
entityManager.TryGetComponent<IdCardComponent>(pdaComponent.ContainedId, out var id))
{
id.FullName = entityManager.GetComponent<MetaDataComponent>(target).EntityName;
}
invSystem.TryEquip(target, equipmentEntity, slot.Name, silent: true, force: true, inventory: inventoryComponent);
onEquipped?.Invoke(target, equipmentEntity);
}
}
if (entityManager.TryGetComponent(target, out HandsComponent? handsComponent))
{
var handsSystem = entityManager.System<HandsSystem>();
var coords = entityManager.GetComponent<TransformComponent>(target).Coordinates;
foreach (var prototype in startingGear.Inhand)
{
var inhandEntity = entityManager.SpawnEntity(prototype, coords);
handsSystem.TryPickup(target, inhandEntity, checkActionBlocker: false, handsComp: handsComponent);
}
}
// See if this starting gear is associated with a job
var jobs = prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (job.StartingGear != gear)
continue;
var jobProtoId = LoadoutSystem.GetJobPrototype(job.ID);
if (!prototypeManager.TryIndex<RoleLoadoutPrototype>(jobProtoId, out var jobProto))
break;
// Don't require a player, so this works on Urists
profile ??= entityManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var comp)
? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species)
: new HumanoidCharacterProfile();
// Try to get the user's existing loadout for the role
profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout);
if (roleLoadout == null)
{
// If they don't have a loadout for the role, make a default one
roleLoadout = new RoleLoadout(jobProtoId);
roleLoadout.SetDefault(profile, session, prototypeManager);
}
// Equip the target with the job loadout
var stationSpawning = entityManager.System<SharedStationSpawningSystem>();
stationSpawning.EquipRoleLoadout(target, roleLoadout, jobProto);
}
return true;
if (!_outfitSystem.SetOutfit(target.Value, args[1]))
shell.WriteLine(Loc.GetString("cmd-setoutfit-invalid-outfit-id-error"));
}
}
}

View File

@@ -6,46 +6,36 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Round)]
public sealed class CallShuttleCommand : IConsoleCommand
public sealed class CallShuttleCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
public string Command => "callshuttle";
public string Description => Loc.GetString("call-shuttle-command-description");
public string Help => Loc.GetString("call-shuttle-command-help-text", ("command",Command));
public override string Command => "callshuttle";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var loc = IoCManager.Resolve<ILocalizationManager>();
// ReSharper disable once ConvertIfStatementToSwitchStatement
if (args.Length == 1 && TimeSpan.TryParseExact(args[0], ContentLocalizationManager.TimeSpanMinutesFormats, loc.DefaultCulture, out var timeSpan))
{
_e.System<RoundEndSystem>().RequestRoundEnd(timeSpan, shell.Player?.AttachedEntity, false);
}
if (args.Length == 1 && TimeSpan.TryParseExact(args[0], ContentLocalizationManager.TimeSpanMinutesFormats, LocalizationManager.DefaultCulture, out var timeSpan))
_roundEndSystem.RequestRoundEnd(timeSpan, shell.Player?.AttachedEntity, false);
else if (args.Length == 1)
{
shell.WriteLine(Loc.GetString("shell-timespan-minutes-must-be-correct"));
}
else
{
_e.System<RoundEndSystem>().RequestRoundEnd(shell.Player?.AttachedEntity, false);
}
_roundEndSystem.RequestRoundEnd(shell.Player?.AttachedEntity, false);
}
}
[AdminCommand(AdminFlags.Round)]
public sealed class RecallShuttleCommand : IConsoleCommand
public sealed class RecallShuttleCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
public string Command => "recallshuttle";
public string Description => Loc.GetString("recall-shuttle-command-description");
public string Help => Loc.GetString("recall-shuttle-command-help-text", ("command",Command));
public override string Command => "recallshuttle";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_e.System<RoundEndSystem>().CancelRoundEndCountdown(shell.Player?.AttachedEntity, false);
_roundEndSystem.CancelRoundEndCountdown(shell.Player?.AttachedEntity, false);
}
}
}

View File

@@ -1,9 +1,9 @@
using Content.Server.Administration.Commands;
using Content.Server.Antag;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Server.Clothing.Systems;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
@@ -20,6 +20,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly OutfitSystem _outfit = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
@@ -132,7 +133,7 @@ public sealed partial class AdminVerbSystem
Act = () =>
{
// pirates just get an outfit because they don't really have logic associated with them
SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
_outfit.SetOutfit(args.Target, PirateGearId);
},
Impact = LogImpact.High,
Message = string.Join(": ", pirateName, Loc.GetString("admin-verb-make-pirate")),

View File

@@ -1,11 +1,11 @@
using System.Threading;
using Content.Server.Administration.Commands;
using Content.Server.Administration.Components;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Disease; // Corvax-Wega-Disease
using Content.Server.Clothing.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
using Content.Server.GhostKick;
@@ -45,7 +45,6 @@ using Content.Shared.Slippery;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
@@ -83,6 +82,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SuperBonkSystem _superBonkSystem = default!;
[Dependency] private readonly SlipperySystem _slipperySystem = default!;
[Dependency] private readonly OutfitSystem _outfitSystem = default!;
[Dependency] private readonly DiseaseSystem _diseaseSystem = default!; // Corvax-Wega-Disease
// All smite verbs have names so invokeverb works.
@@ -105,7 +105,7 @@ public sealed partial class AdminVerbSystem
{
Text = explodeName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
Act = () =>
{
var coords = _transformSystem.GetMapCoordinates(args.Target);
@@ -126,7 +126,7 @@ public sealed partial class AdminVerbSystem
{
Text = chessName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
Act = () =>
{
_sharedGodmodeSystem.EnableGodmode(args.Target); // So they don't suffocate.
@@ -155,7 +155,7 @@ public sealed partial class AdminVerbSystem
{
Text = flamesName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Fire/fire.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Alerts/Fire/fire.png")),
Act = () =>
{
// Fuck you. Burn Forever.
@@ -178,7 +178,7 @@ public sealed partial class AdminVerbSystem
{
Text = monkeyName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminMonkeySmite");
@@ -195,7 +195,7 @@ public sealed partial class AdminVerbSystem
{
Text = "Lung Cancer",
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-l"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Species/Human/organs.rsi"), "lung-l"),
Act = () =>
{
_diseaseSystem.TryInfect(carrier, _prototypeManager.Index<DiseasePrototype>("StageIIIALungCancer"),
@@ -213,7 +213,7 @@ public sealed partial class AdminVerbSystem
{
Text = disposalBinName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminDisposalsSmite");
@@ -231,20 +231,21 @@ public sealed partial class AdminVerbSystem
{
Text = hardElectrocuteName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
Act = () =>
{
int damageToDeal;
if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Critical, out var criticalThreshold)) {
if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Critical, out var criticalThreshold))
{
// We can't crit them so try killing them.
if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Dead,
out var deadThreshold))
return;// whelp.
damageToDeal = deadThreshold.Value.Int() - (int) damageable.TotalDamage;
damageToDeal = deadThreshold.Value.Int() - (int)damageable.TotalDamage;
}
else
{
damageToDeal = criticalThreshold.Value.Int() - (int) damageable.TotalDamage;
damageToDeal = criticalThreshold.Value.Int() - (int)damageable.TotalDamage;
}
if (damageToDeal <= 0)
@@ -277,7 +278,7 @@ public sealed partial class AdminVerbSystem
{
Text = creamPieName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
Act = () =>
{
_creamPieSystem.SetCreamPied(args.Target, creamPied, true);
@@ -295,7 +296,7 @@ public sealed partial class AdminVerbSystem
{
Text = bloodRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
Act = () =>
{
_bloodstreamSystem.SpillAllSolutions(args.Target, bloodstream);
@@ -348,7 +349,7 @@ public sealed partial class AdminVerbSystem
{
Text = handsRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hands.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/remove-hands.png")),
Act = () =>
{
var baseXform = Transform(args.Target);
@@ -371,7 +372,7 @@ public sealed partial class AdminVerbSystem
{
Text = handRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hand.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/remove-hand.png")),
Act = () =>
{
var baseXform = Transform(args.Target);
@@ -395,7 +396,7 @@ public sealed partial class AdminVerbSystem
{
Text = stomachRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
Act = () =>
{
foreach (var entity in _bodySystem.GetBodyOrganEntityComps<StomachComponent>((args.Target, body)))
@@ -416,7 +417,7 @@ public sealed partial class AdminVerbSystem
{
Text = lungRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
Act = () =>
{
foreach (var entity in _bodySystem.GetBodyOrganEntityComps<LungComponent>((args.Target, body)))
@@ -440,7 +441,7 @@ public sealed partial class AdminVerbSystem
{
Text = pinballName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "basketball"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Balls/basketball.rsi"), "icon"),
Act = () =>
{
var xform = Transform(args.Target);
@@ -475,7 +476,7 @@ public sealed partial class AdminVerbSystem
{
Text = yeetName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
Act = () =>
{
var xform = Transform(args.Target);
@@ -507,7 +508,7 @@ public sealed partial class AdminVerbSystem
{
Text = breadName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminBreadSmite");
@@ -522,7 +523,7 @@ public sealed partial class AdminVerbSystem
{
Text = mouseName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminMouseSmite");
@@ -539,7 +540,7 @@ public sealed partial class AdminVerbSystem
{
Text = ghostKickName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/gavel.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/gavel.svg.192dpi.png")),
Act = () =>
{
_ghostKickManager.DoDisconnect(actorComponent.PlayerSession.Channel, "Smitten.");
@@ -558,7 +559,7 @@ public sealed partial class AdminVerbSystem
{
Text = nyanifyName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
Act = () =>
{
var ears = Spawn("ClothingHeadHatCatEars", Transform(args.Target).Coordinates);
@@ -576,7 +577,7 @@ public sealed partial class AdminVerbSystem
{
Text = killSignName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Misc/killsign.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/killsign.rsi"), "icon"),
Act = () =>
{
EnsureComp<KillSignComponent>(args.Target);
@@ -592,7 +593,7 @@ public sealed partial class AdminVerbSystem
Text = cluwneName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
Act = () =>
{
@@ -608,10 +609,10 @@ public sealed partial class AdminVerbSystem
{
Text = maidenName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
Act = () =>
{
SetOutfitCommand.SetOutfit(args.Target, "JanitorMaidGear", EntityManager, (_, clothing) =>
_outfitSystem.SetOutfit(args.Target, "JanitorMaidGear", (_, clothing) =>
{
if (HasComp<ClothingComponent>(clothing))
EnsureComp<UnremoveableComponent>(clothing);
@@ -629,7 +630,7 @@ public sealed partial class AdminVerbSystem
{
Text = angerPointingArrowsName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
Act = () =>
{
EnsureComp<PointingArrowAngeringComponent>(args.Target);
@@ -644,7 +645,7 @@ public sealed partial class AdminVerbSystem
{
Text = dustName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Materials/materials.rsi"), "ash"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Materials/materials.rsi"), "ash"),
Act = () =>
{
EntityManager.QueueDeleteEntity(args.Target);
@@ -661,7 +662,7 @@ public sealed partial class AdminVerbSystem
{
Text = youtubeVideoSimulationName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Misc/buffering_smite_icon.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Misc/buffering_smite_icon.png")),
Act = () =>
{
EnsureComp<BufferingComponent>(args.Target);
@@ -676,7 +677,7 @@ public sealed partial class AdminVerbSystem
{
Text = instrumentationName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminInstrumentSmite");
@@ -709,7 +710,7 @@ public sealed partial class AdminVerbSystem
{
Text = reptilianName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "plushie_lizard"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/Plushies/lizard.rsi"), "icon"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");
@@ -724,7 +725,7 @@ public sealed partial class AdminVerbSystem
{
Text = lockerName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Storage/closet.rsi"), "generic"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Storage/closet.rsi"), "generic"),
Act = () =>
{
var xform = Transform(args.Target);
@@ -747,7 +748,7 @@ public sealed partial class AdminVerbSystem
{
Text = headstandName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Act = () =>
{
EnsureComp<HeadstandComponent>(args.Target);
@@ -762,7 +763,7 @@ public sealed partial class AdminVerbSystem
{
Text = zoomInName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/zoom.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/zoom.png")),
Act = () =>
{
var eye = EnsureComp<ContentEyeComponent>(args.Target);
@@ -778,7 +779,7 @@ public sealed partial class AdminVerbSystem
{
Text = flipEyeName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/flip.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/flip.png")),
Act = () =>
{
var eye = EnsureComp<ContentEyeComponent>(args.Target);
@@ -794,7 +795,7 @@ public sealed partial class AdminVerbSystem
{
Text = runWalkSwapName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/run-walk-swap.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/run-walk-swap.png")),
Act = () =>
{
var movementSpeed = EnsureComp<MovementSpeedModifierComponent>(args.Target);
@@ -815,7 +816,7 @@ public sealed partial class AdminVerbSystem
{
Text = backwardsAccentName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/help-backwards.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/help-backwards.png")),
Act = () =>
{
EnsureComp<BackwardsAccentComponent>(args.Target);
@@ -830,7 +831,7 @@ public sealed partial class AdminVerbSystem
{
Text = disarmProneName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Actions/disarm.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/disarm.png")),
Act = () =>
{
EnsureComp<DisarmProneComponent>(args.Target);
@@ -845,7 +846,7 @@ public sealed partial class AdminVerbSystem
{
Text = superSpeedName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/super_speed.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/super_speed.png")),
Act = () =>
{
var movementSpeed = EnsureComp<MovementSpeedModifierComponent>(args.Target);

View File

@@ -6,34 +6,31 @@ using Robust.Shared.Console;
namespace Content.Server.Afk
{
[AdminCommand(AdminFlags.Admin)]
public sealed class IsAfkCommand : IConsoleCommand
public sealed class IsAfkCommand : LocalizedCommands
{
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IPlayerManager _players = default!;
public string Command => "isafk";
public string Description => "Checks if a specified player is AFK";
public string Help => "Usage: isafk <playerName>";
public override string Command => "isafk";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var afkManager = IoCManager.Resolve<IAfkManager>();
if (args.Length == 0)
{
shell.WriteError("Need one argument");
shell.WriteError(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
if (!_players.TryGetSessionByUsername(args[0], out var player))
{
shell.WriteError("Unable to find that player");
shell.WriteError(Loc.GetString($"shell-target-player-does-not-exist"));
return;
}
shell.WriteLine(afkManager.IsAfk(player) ? "They are indeed AFK" : "They are not AFK");
shell.WriteLine(Loc.GetString(_afkManager.IsAfk(player) ? "cmd-isafk-true" : "cmd-isafk-false"));
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{

View File

@@ -38,7 +38,7 @@ public sealed partial class AtmosPipeLayersSystem : SharedAtmosPipeLayersSystem
if (ent.Comp.PipeLayersLocked)
return;
base.SetPipeLayer(ent, layer);
base.SetPipeLayer(ent, layer, user, used);
if (!TryComp<NodeContainerComponent>(ent, out var nodeContainer))
return;

View File

@@ -9,6 +9,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -18,13 +19,14 @@ namespace Content.Server.Atmos.EntitySystems
{
[Dependency] private readonly SharedMagbootsSystem _magboots = default!; // Corvax-Wega-AdvMagboots
[Dependency] private readonly SharedPopupSystem _popup = default!; // Corvax-Wega-AdvMagboots
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpaceWindSounds = "SpaceWind";
private const int SpaceWindSoundCooldownCycles = 75;
private int _spaceWindSoundCooldown = 0;
[ViewVariables(VVAccess.ReadWrite)]
public string? SpaceWindSound { get; private set; } = "/Audio/Effects/space_wind.ogg";
public SoundSpecifier? SpaceWindSound { get; private set; } = new SoundCollectionSpecifier(DefaultSpaceWindSounds, AudioParams.Default.WithVariation(0.125f));
private readonly HashSet<Entity<MovedByPressureComponent>> _activePressures = new(8);
@@ -110,10 +112,10 @@ namespace Content.Server.Atmos.EntitySystems
// Don't play the space wind sound on tiles that are on fire...
if (tile.PressureDifference > 15 && !tile.Hotspot.Valid)
{
if (_spaceWindSoundCooldown == 0 && !string.IsNullOrEmpty(SpaceWindSound))
if (_spaceWindSoundCooldown == 0 && SpaceWindSound != null)
{
var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
_audio.PlayPvs(SpaceWindSound, coordinates, AudioParams.Default.WithVariation(0.125f).WithVolume(MathHelper.Clamp(tile.PressureDifference / 10, 10, 100)));
_audio.PlayPvs(SpaceWindSound, coordinates, SpaceWindSound.Params.WithVolume(MathHelper.Clamp(tile.PressureDifference / 10, 10, 100)));
}
}

View File

@@ -7,12 +7,15 @@ using Content.Shared.Database;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
private static readonly ProtoId<SoundCollectionPrototype> DefaultHotspotSounds = "AtmosHotspot";
[Dependency] private readonly DecalSystem _decalSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -21,7 +24,7 @@ namespace Content.Server.Atmos.EntitySystems
private int _hotspotSoundCooldown = 0;
[ViewVariables(VVAccess.ReadWrite)]
public string? HotspotSound { get; private set; } = "/Audio/Effects/fire.ogg";
public SoundSpecifier? HotspotSound { get; private set; } = new SoundCollectionSpecifier(DefaultHotspotSounds);
private void ProcessHotspot(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
@@ -105,14 +108,14 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained)
tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature;
if (_hotspotSoundCooldown++ == 0 && !string.IsNullOrEmpty(HotspotSound))
if (_hotspotSoundCooldown++ == 0 && HotspotSound != null)
{
var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
// A few details on the audio parameters for fire.
// The greater the fire state, the lesser the pitch variation.
// The greater the fire state, the greater the volume.
_audio.PlayPvs(HotspotSound, coordinates, AudioParams.Default.WithVariation(0.15f/tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State));
_audio.PlayPvs(HotspotSound, coordinates, HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State));
}
if (_hotspotSoundCooldown > HotspotSoundCooldownCycles)

View File

@@ -33,9 +33,12 @@ public sealed class GasAnalyzerSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
Subs.BuiEvents<GasAnalyzerComponent>(GasAnalyzerUiKey.Key, subs =>
{
subs.Event<BoundUIOpenedEvent>(OnBoundUIOpened);
subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
});
}
public override void Update(float frameTime)
@@ -72,21 +75,6 @@ public sealed class GasAnalyzerSystem : EntitySystem
args.Handled = true;
}
/// <summary>
/// Activates the analyzer with no target, so it only scans the tile the user was on when activated
/// </summary>
private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
{
// Not checking for Handled because ActivatableUISystem already marks it as such.
if (!entity.Comp.Enabled)
ActivateAnalyzer(entity, args.User);
else
DisableAnalyzer(entity, args.User);
args.Handled = true;
}
/// <summary>
/// Handles analyzer activation logic
/// </summary>
@@ -104,16 +92,6 @@ public sealed class GasAnalyzerSystem : EntitySystem
UpdateAnalyzer(entity.Owner, entity.Comp);
}
/// <summary>
/// Close the UI, turn the analyzer off, and don't update when it's dropped
/// </summary>
private void OnDropped(Entity<GasAnalyzerComponent> entity, ref DroppedEvent args)
{
if (args.User is var userId && entity.Comp.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
DisableAnalyzer(entity, args.User);
}
/// <summary>
/// Closes the UI, sets the icon to off, and removes it from the update list
/// </summary>
@@ -121,6 +99,9 @@ public sealed class GasAnalyzerSystem : EntitySystem
{
_userInterface.CloseUi(entity.Owner, GasAnalyzerUiKey.Key, user);
if (user.HasValue && entity.Comp.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), user.Value, user.Value);
entity.Comp.Enabled = false;
Dirty(entity);
_appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
@@ -130,9 +111,25 @@ public sealed class GasAnalyzerSystem : EntitySystem
/// <summary>
/// Disables the analyzer when the user closes the UI
/// </summary>
private void OnDisabledMessage(Entity<GasAnalyzerComponent> entity, ref GasAnalyzerDisableMessage message)
private void OnBoundUIClosed(Entity<GasAnalyzerComponent> entity, ref BoundUIClosedEvent args)
{
DisableAnalyzer(entity);
if (HasComp<ActiveGasAnalyzerComponent>(entity.Owner)
&& !_userInterface.IsUiOpen(entity.Owner, args.UiKey))
{
DisableAnalyzer(entity, args.Actor);
}
}
/// <summary>
/// Enables the analyzer when the user opens the UI
/// </summary>
private void OnBoundUIOpened(Entity<GasAnalyzerComponent> entity, ref BoundUIOpenedEvent args)
{
if (!HasComp<ActiveGasAnalyzerComponent>(entity.Owner)
&& _userInterface.IsUiOpen(entity.Owner, args.UiKey))
{
ActivateAnalyzer(entity, args.Actor);
}
}
/// <summary>

View File

@@ -3,23 +3,23 @@ using Content.Server.Body.Systems;
using Content.Shared.Administration;
using Content.Shared.Body.Components;
using Robust.Shared.Console;
using Robust.Shared.Random;
namespace Content.Server.Body.Commands
{
[AdminCommand(AdminFlags.Fun)]
sealed class DestroyMechanismCommand : IConsoleCommand
internal sealed class DestroyMechanismCommand : LocalizedEntityCommands
{
public string Command => "destroymechanism";
public string Description => "Destroys a mechanism from your entity";
public string Help => $"Usage: {Command} <mechanism>";
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "destroymechanism";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("Only a player can run this command.");
shell.WriteLine(Loc.GetString($"shell-only-players-can-run-this-command"));
return;
}
@@ -31,36 +31,29 @@ namespace Content.Server.Body.Commands
if (player.AttachedEntity is not {} attached)
{
shell.WriteLine("You have no entity.");
shell.WriteLine(Loc.GetString($"shell-must-be-attached-to-entity"));
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var fac = IoCManager.Resolve<IComponentFactory>();
if (!entityManager.TryGetComponent(attached, out BodyComponent? body))
if (!EntityManager.TryGetComponent(attached, out BodyComponent? body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.WriteLine(text);
shell.WriteLine(Loc.GetString($"shell-must-have-body"));
return;
}
var mechanismName = string.Join(" ", args).ToLowerInvariant();
var bodySystem = entityManager.System<BodySystem>();
foreach (var organ in bodySystem.GetBodyOrgans(attached, body))
foreach (var organ in _bodySystem.GetBodyOrgans(attached, body))
{
if (fac.GetComponentName(organ.Component.GetType()).ToLowerInvariant() == mechanismName)
if (_compFactory.GetComponentName(organ.Component.GetType()).ToLowerInvariant() == mechanismName)
{
entityManager.QueueDeleteEntity(organ.Id);
shell.WriteLine($"Mechanism with name {mechanismName} has been destroyed.");
EntityManager.QueueDeleteEntity(organ.Id);
shell.WriteLine(Loc.GetString($"cmd-destroymechanism-success", ("name", mechanismName)));
return;
}
}
shell.WriteLine($"No mechanism was found with name {mechanismName}.");
shell.WriteLine(Loc.GetString($"cmd-destroymechanism-no-mechanism-found", ("name", mechanismName)));
}
}
}

View File

@@ -236,10 +236,13 @@ public sealed class BloodstreamSystem : EntitySystem
}
// TODO probably cache this or something. humans get hurt a lot
if (!_prototypeManager.TryIndex<DamageModifierSetPrototype>(ent.Comp.DamageBleedModifiers, out var modifiers))
if (!_prototypeManager.TryIndex(ent.Comp.DamageBleedModifiers, out var modifiers))
return;
var bloodloss = DamageSpecifier.ApplyModifierSet(args.DamageDelta, modifiers);
// some reagents may deal and heal different damage types in the same tick, which means DamageIncreased will be true
// but we only want to consider the dealt damage when causing bleeding
var damage = DamageSpecifier.GetPositive(args.DamageDelta);
var bloodloss = DamageSpecifier.ApplyModifierSet(damage, modifiers);
if (bloodloss.Empty)
return;
@@ -258,7 +261,7 @@ public sealed class BloodstreamSystem : EntitySystem
var prob = Math.Clamp(totalFloat / 25, 0, 1);
if (totalFloat > 0 && _robustRandom.Prob(prob))
{
TryModifyBloodLevel(ent, (-total) / 5, ent);
TryModifyBloodLevel(ent, -total / 5, ent);
_audio.PlayPvs(ent.Comp.InstantBloodSound, ent);
}

View File

@@ -6,19 +6,19 @@ using Robust.Shared.Console;
namespace Content.Server.Chat.Commands
{
[AdminCommand(AdminFlags.Adminchat)]
internal sealed class AdminChatCommand : IConsoleCommand
internal sealed class AdminChatCommand : LocalizedCommands
{
public string Command => "asay";
public string Description => "Send chat messages to the private admin chat channel.";
public string Help => "asay <text>";
[Dependency] private readonly IChatManager _chatManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "asay";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteError("You can't run this command locally.");
shell.WriteError(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
@@ -29,7 +29,7 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
IoCManager.Resolve<IChatManager>().TrySendOOCMessage(player, message, OOCChatType.Admin);
_chatManager.TrySendOOCMessage(player, message, OOCChatType.Admin);
}
}
}

View File

@@ -6,17 +6,17 @@ using Robust.Shared.Enums;
namespace Content.Server.Chat.Commands
{
[AnyCommand]
internal sealed class MeCommand : IConsoleCommand
internal sealed class MeCommand : LocalizedEntityCommands
{
public string Command => "me";
public string Description => "Perform an action.";
public string Help => "me <text>";
[Dependency] private readonly ChatSystem _chatSystem = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "me";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
shell.WriteError("This command cannot be run from the server.");
shell.WriteError(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
@@ -25,7 +25,7 @@ namespace Content.Server.Chat.Commands
if (player.AttachedEntity is not {} playerEntity)
{
shell.WriteError("You don't have an entity!");
shell.WriteError(Loc.GetString($"shell-must-be-attached-to-entity"));
return;
}
@@ -36,8 +36,7 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>()
.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Emote, ChatTransmitRange.Normal, false, shell, player);
_chatSystem.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Emote, ChatTransmitRange.Normal, false, shell, player);
}
}
}

View File

@@ -5,17 +5,17 @@ using Robust.Shared.Console;
namespace Content.Server.Chat.Commands
{
[AnyCommand]
internal sealed class OOCCommand : IConsoleCommand
internal sealed class OOCCommand : LocalizedCommands
{
public string Command => "ooc";
public string Description => "Send Out Of Character chat messages.";
public string Help => "ooc <text>";
[Dependency] private readonly IChatManager _chatManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "ooc";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
shell.WriteError("This command cannot be run from the server.");
shell.WriteError(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
@@ -26,7 +26,7 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
IoCManager.Resolve<IChatManager>().TrySendOOCMessage(player, message, OOCChatType.OOC);
_chatManager.TrySendOOCMessage(player, message, OOCChatType.OOC);
}
}
}

View File

@@ -6,13 +6,12 @@ using Robust.Shared.Enums;
namespace Content.Server.Chat.Commands
{
[AnyCommand]
internal sealed class SayCommand : IConsoleCommand
internal sealed class SayCommand : LocalizedEntityCommands
{
public string Command => "say";
public string Description => "Send chat messages to the local channel or a specified radio channel.";
public string Help => "say <text>";
[Dependency] private readonly ChatSystem _chatSystem = default!;
public override string Command => "say";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
@@ -25,7 +24,7 @@ namespace Content.Server.Chat.Commands
if (player.AttachedEntity is not {} playerEntity)
{
shell.WriteError("You don't have an entity!");
shell.WriteError(Loc.GetString($"shell-must-be-attached-to-entity"));
return;
}
@@ -36,8 +35,7 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>()
.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, ChatTransmitRange.Normal, false, shell, player);
_chatSystem.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, ChatTransmitRange.Normal, false, shell, player);
}
}
}

View File

@@ -7,22 +7,21 @@ using Robust.Shared.Console;
namespace Content.Server.Chat.Commands;
[AdminCommand(AdminFlags.Server)]
public sealed class SetLOOCCommand : IConsoleCommand
public sealed class SetLoocCommand : LocalizedCommands
{
public string Command => "setlooc";
public string Description => Loc.GetString("set-looc-command-description");
public string Help => Loc.GetString("set-looc-command-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var cfg = IoCManager.Resolve<IConfigurationManager>();
[Dependency] private readonly IConfigurationManager _configManager = default!;
public override string Command => "setlooc";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("set-looc-command-too-many-arguments-error"));
shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1)));
return;
}
var looc = cfg.GetCVar(CCVars.LoocEnabled);
var looc = _configManager.GetCVar(CCVars.LoocEnabled);
if (args.Length == 0)
{
@@ -31,12 +30,12 @@ public sealed class SetLOOCCommand : IConsoleCommand
if (args.Length == 1 && !bool.TryParse(args[0], out looc))
{
shell.WriteError(Loc.GetString("set-looc-command-invalid-argument-error"));
shell.WriteError(Loc.GetString("shell-invalid-bool"));
return;
}
cfg.SetCVar(CCVars.LoocEnabled, looc);
_configManager.SetCVar(CCVars.LoocEnabled, looc);
shell.WriteLine(Loc.GetString(looc ? "set-looc-command-looc-enabled" : "set-looc-command-looc-disabled"));
shell.WriteLine(Loc.GetString(looc ? "cmd-setlooc-looc-enabled" : "cmd-setlooc-looc-disabled"));
}
}

View File

@@ -7,22 +7,21 @@ using Robust.Shared.Console;
namespace Content.Server.Chat.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class SetOOCCommand : IConsoleCommand
public sealed class SetOOCCommand : LocalizedCommands
{
public string Command => "setooc";
public string Description => Loc.GetString("set-ooc-command-description");
public string Help => Loc.GetString("set-ooc-command-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var cfg = IoCManager.Resolve<IConfigurationManager>();
[Dependency] private readonly IConfigurationManager _configManager = default!;
public override string Command => "setooc";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("set-ooc-command-too-many-arguments-error"));
shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1)));
return;
}
var ooc = cfg.GetCVar(CCVars.OocEnabled);
var ooc = _configManager.GetCVar(CCVars.OocEnabled);
if (args.Length == 0)
{
@@ -31,12 +30,12 @@ public sealed class SetOOCCommand : IConsoleCommand
if (args.Length == 1 && !bool.TryParse(args[0], out ooc))
{
shell.WriteError(Loc.GetString("set-ooc-command-invalid-argument-error"));
shell.WriteError(Loc.GetString("shell-invalid-bool"));
return;
}
cfg.SetCVar(CCVars.OocEnabled, ooc);
_configManager.SetCVar(CCVars.OocEnabled, ooc);
shell.WriteLine(Loc.GetString(ooc ? "set-ooc-command-ooc-enabled" : "set-ooc-command-ooc-disabled"));
shell.WriteLine(Loc.GetString(ooc ? "cmd-setooc-ooc-enabled" : "cmd-setooc-ooc-disabled"));
}
}

View File

@@ -6,13 +6,12 @@ using Robust.Shared.Enums;
namespace Content.Server.Chat.Commands
{
[AnyCommand]
internal sealed class WhisperCommand : IConsoleCommand
internal sealed class WhisperCommand : LocalizedEntityCommands
{
public string Command => "whisper";
public string Description => "Send chat messages to the local channel as a whisper";
public string Help => "whisper <text>";
[Dependency] private readonly ChatSystem _chatSystem = default!;
public override string Command => "whisper";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
@@ -25,7 +24,7 @@ namespace Content.Server.Chat.Commands
if (player.AttachedEntity is not {} playerEntity)
{
shell.WriteError("You don't have an entity!");
shell.WriteError(Loc.GetString($"shell-must-be-attached-to-entity"));
return;
}
@@ -36,8 +35,7 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>()
.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Whisper, ChatTransmitRange.Normal, false, shell, player);
_chatSystem.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Whisper, ChatTransmitRange.Normal, false, shell, player);
}
}
}

View File

@@ -0,0 +1,109 @@
using Content.Server.Hands.Systems;
using Content.Server.Preferences.Managers;
using Content.Shared.Access.Components;
using Content.Shared.Clothing;
using Content.Shared.Hands.Components;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Clothing.Systems;
public sealed class OutfitSystem : EntitySystem
{
[Dependency] private readonly IServerPreferencesManager _preferenceManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly HandsSystem _handSystem = default!;
[Dependency] private readonly InventorySystem _invSystem = default!;
[Dependency] private readonly SharedStationSpawningSystem _spawningSystem = default!;
public bool SetOutfit(EntityUid target, string gear, Action<EntityUid, EntityUid>? onEquipped = null)
{
if (!EntityManager.TryGetComponent(target, out InventoryComponent? inventoryComponent))
return false;
if (!_prototypeManager.TryIndex<StartingGearPrototype>(gear, out var startingGear))
return false;
HumanoidCharacterProfile? profile = null;
ICommonSession? session = null;
// Check if we are setting the outfit of a player to respect the preferences
if (EntityManager.TryGetComponent(target, out ActorComponent? actorComponent))
{
session = actorComponent.PlayerSession;
var userId = actorComponent.PlayerSession.UserId;
var prefs = _preferenceManager.GetPreferences(userId);
profile = prefs.SelectedCharacter as HumanoidCharacterProfile;
}
if (_invSystem.TryGetSlots(target, out var slots))
{
foreach (var slot in slots)
{
_invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
var gearStr = ((IEquipmentLoadout) startingGear).GetGear(slot.Name);
if (gearStr == string.Empty)
continue;
var equipmentEntity = EntityManager.SpawnEntity(gearStr, EntityManager.GetComponent<TransformComponent>(target).Coordinates);
if (slot.Name == "id" &&
EntityManager.TryGetComponent(equipmentEntity, out PdaComponent? pdaComponent) &&
EntityManager.TryGetComponent<IdCardComponent>(pdaComponent.ContainedId, out var id))
{
id.FullName = EntityManager.GetComponent<MetaDataComponent>(target).EntityName;
}
_invSystem.TryEquip(target, equipmentEntity, slot.Name, silent: true, force: true, inventory: inventoryComponent);
onEquipped?.Invoke(target, equipmentEntity);
}
}
if (EntityManager.TryGetComponent(target, out HandsComponent? handsComponent))
{
var coords = EntityManager.GetComponent<TransformComponent>(target).Coordinates;
foreach (var prototype in startingGear.Inhand)
{
var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
_handSystem.TryPickup(target, inhandEntity, checkActionBlocker: false, handsComp: handsComponent);
}
}
// See if this starting gear is associated with a job
var jobs = _prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (job.StartingGear != gear)
continue;
var jobProtoId = LoadoutSystem.GetJobPrototype(job.ID);
if (!_prototypeManager.TryIndex<RoleLoadoutPrototype>(jobProtoId, out var jobProto))
break;
// Don't require a player, so this works on Urists
profile ??= EntityManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var comp)
? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species)
: new HumanoidCharacterProfile();
// Try to get the user's existing loadout for the role
profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout);
if (roleLoadout == null)
{
// If they don't have a loadout for the role, make a default one
roleLoadout = new RoleLoadout(jobProtoId);
roleLoadout.SetDefault(profile, session, _prototypeManager);
}
// Equip the target with the job loadout
_spawningSystem.EquipRoleLoadout(target, roleLoadout, jobProto);
}
return true;
}
}

View File

@@ -1,9 +1,9 @@
using Content.Server.Administration.Commands;
using Content.Server.Popups;
using Content.Shared.Popups;
using Content.Shared.Mobs;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Clothing.Systems;
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Stunnable;
@@ -13,7 +13,6 @@ using Robust.Shared.Prototypes;
using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Cluwne;
using Content.Shared.Interaction.Components;
using Robust.Shared.Audio.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Clumsy;
@@ -31,6 +30,7 @@ public sealed class CluwneSystem : EntitySystem
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly OutfitSystem _outfitSystem = default!;
public override void Initialize()
{
@@ -78,7 +78,7 @@ public sealed class CluwneSystem : EntitySystem
_nameMod.RefreshNameModifiers(uid);
SetOutfitCommand.SetOutfit(uid, "CluwneGear", EntityManager);
_outfitSystem.SetOutfit(uid, "CluwneGear");
}
/// <summary>

View File

@@ -14,8 +14,8 @@
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="NetCord" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Packaging\Content.Packaging.csproj" />

View File

@@ -1,8 +1,7 @@
using System.Threading.Tasks;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Managers;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Discord.WebSocket;
using NetCord.Gateway;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
@@ -59,18 +58,18 @@ public sealed class DiscordChatLink : IPostInjectInit
_adminChannelId = ulong.Parse(channelId);
}
private void OnMessageReceived(SocketMessage message)
private void OnMessageReceived(Message message)
{
if (message.Author.IsBot)
return;
var contents = message.Content.ReplaceLineEndings(" ");
if (message.Channel.Id == _oocChannelId)
if (message.ChannelId == _oocChannelId)
{
_taskManager.RunOnMainThread(() => _chatManager.SendHookOOC(message.Author.Username, contents));
}
else if (message.Channel.Id == _adminChannelId)
else if (message.ChannelId == _adminChannelId)
{
_taskManager.RunOnMainThread(() => _chatManager.SendHookAdmin(message.Author.Username, contents));
}

View File

@@ -1,12 +1,9 @@
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Content.Shared.CCVar;
using Discord;
using Discord.WebSocket;
using NetCord;
using NetCord.Gateway;
using NetCord.Rest;
using Robust.Shared.Configuration;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using LogMessage = Discord.LogMessage;
namespace Content.Server.Discord.DiscordLink;
@@ -28,7 +25,7 @@ public sealed class CommandReceivedEventArgs
/// Information about the message that the command was received from. This includes the message content, author, etc.
/// Use this to reply to the message, delete it, etc.
/// </summary>
public SocketMessage Message { get; init; } = default!;
public Message Message { get; init; } = default!;
}
/// <summary>
@@ -45,7 +42,7 @@ public sealed class DiscordLink : IPostInjectInit
/// <remarks>
/// This should not be used directly outside of DiscordLink. So please do not make it public. Use the methods in this class instead.
/// </remarks>
private DiscordSocketClient? _client;
private GatewayClient? _client;
private ISawmill _sawmill = default!;
private ISawmill _sawmillLog = default!;
@@ -67,7 +64,7 @@ public sealed class DiscordLink : IPostInjectInit
/// <summary>
/// Event that is raised when a message is received from Discord. This is raised for every message, including commands.
/// </summary>
public event Action<SocketMessage>? OnMessageReceived;
public event Action<Message>? OnMessageReceived;
public void RegisterCommandCallback(Action<CommandReceivedEventArgs> callback, string command)
{
@@ -101,33 +98,34 @@ public sealed class DiscordLink : IPostInjectInit
return;
}
_client = new DiscordSocketClient(new DiscordSocketConfig()
_client = new GatewayClient(new BotToken(token), new GatewayClientConfiguration()
{
GatewayIntents = GatewayIntents.Guilds
| GatewayIntents.GuildMembers
Intents = GatewayIntents.Guilds
| GatewayIntents.GuildUsers
| GatewayIntents.GuildMessages
| GatewayIntents.MessageContent
| GatewayIntents.DirectMessages,
Logger = new DiscordSawmillLogger(_sawmillLog),
});
_client.Log += Log;
_client.MessageReceived += OnCommandReceivedInternal;
_client.MessageReceived += OnMessageReceivedInternal;
_client.MessageCreate += OnCommandReceivedInternal;
_client.MessageCreate += OnMessageReceivedInternal;
_botToken = token;
// Since you cannot change the token while the server is running / the DiscordLink is initialized,
// we can just set the token without updating it every time the cvar changes.
_client.Ready += () =>
_client.Ready += _ =>
{
_sawmill.Info("Discord client ready.");
return Task.CompletedTask;
return default;
};
Task.Run(async () =>
{
try
{
await LoginAsync(token);
await _client.StartAsync();
_sawmill.Info("Connected to Discord.");
}
catch (Exception e)
{
@@ -143,12 +141,11 @@ public sealed class DiscordLink : IPostInjectInit
_sawmill.Info("Disconnecting from Discord.");
// Unsubscribe from the events.
_client.MessageReceived -= OnCommandReceivedInternal;
_client.MessageReceived -= OnMessageReceivedInternal;
_client.MessageCreate -= OnCommandReceivedInternal;
_client.MessageCreate -= OnMessageReceivedInternal;
await _client.LogoutAsync();
await _client.StopAsync();
await _client.DisposeAsync();
await _client.CloseAsync();
_client.Dispose();
_client = null;
}
@@ -172,45 +169,12 @@ public sealed class DiscordLink : IPostInjectInit
BotPrefix = prefix;
}
private async Task LoginAsync(string token)
{
DebugTools.Assert(_client != null);
DebugTools.Assert(_client.LoginState == LoginState.LoggedOut);
await _client.LoginAsync(TokenType.Bot, token);
await _client.StartAsync();
_sawmill.Info("Connected to Discord.");
}
private string FormatLog(LogMessage msg)
{
return msg.Exception is null
? $"{msg.Source}: {msg.Message}"
: $"{msg.Source}: {msg.Message}\n{msg.Exception}";
}
private Task Log(LogMessage msg)
{
var logLevel = msg.Severity switch
{
LogSeverity.Critical => LogLevel.Fatal,
LogSeverity.Error => LogLevel.Error,
LogSeverity.Warning => LogLevel.Warning,
_ => LogLevel.Debug
};
_sawmillLog.Log(logLevel, FormatLog(msg));
return Task.CompletedTask;
}
private Task OnCommandReceivedInternal(SocketMessage message)
private ValueTask OnCommandReceivedInternal(Message message)
{
var content = message.Content;
// If the message doesn't start with the bot prefix, ignore it.
if (!content.StartsWith(BotPrefix))
return Task.CompletedTask;
return ValueTask.CompletedTask;
// Split the message into the command and the arguments.
var trimmedInput = content[BotPrefix.Length..].Trim();
@@ -236,13 +200,13 @@ public sealed class DiscordLink : IPostInjectInit
Arguments = arguments,
Message = message,
});
return Task.CompletedTask;
return ValueTask.CompletedTask;
}
private Task OnMessageReceivedInternal(SocketMessage message)
private ValueTask OnMessageReceivedInternal(Message message)
{
OnMessageReceived?.Invoke(message);
return Task.CompletedTask;
return ValueTask.CompletedTask;
}
#region Proxy methods
@@ -257,14 +221,18 @@ public sealed class DiscordLink : IPostInjectInit
return;
}
var channel = _client.GetChannel(channelId) as IMessageChannel;
var channel = await _client.Rest.GetChannelAsync(channelId) as TextChannel;
if (channel == null)
{
_sawmill.Error("Tried to send a message to Discord but the channel {Channel} was not found.", channel);
return;
}
await channel.SendMessageAsync(message, allowedMentions: AllowedMentions.None);
await channel.SendMessageAsync(new MessageProperties()
{
AllowedMentions = AllowedMentionsProperties.None,
Content = message,
});
}
#endregion

View File

@@ -0,0 +1,34 @@
using NetCord.Logging;
using NLogLevel = NetCord.Logging.LogLevel;
using LogLevel = Robust.Shared.Log.LogLevel;
namespace Content.Server.Discord.DiscordLink;
public sealed class DiscordSawmillLogger(ISawmill sawmill) : IGatewayLogger, IRestLogger, IVoiceLogger
{
private static LogLevel GetLogLevel(NLogLevel logLevel)
{
return logLevel switch
{
NLogLevel.Critical => LogLevel.Fatal,
NLogLevel.Error => LogLevel.Error,
NLogLevel.Warning => LogLevel.Warning,
_ => LogLevel.Debug,
};
}
void IGatewayLogger.Log<TState>(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception));
}
void IRestLogger.Log<TState>(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception));
}
void IVoiceLogger.Log<TState>(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception));
}
}

View File

@@ -7,17 +7,17 @@ using Robust.Shared.Prototypes;
namespace Content.Server.EntityList
{
[AdminCommand(AdminFlags.Spawn)]
public sealed class SpawnEntityListCommand : IConsoleCommand
public sealed class SpawnEntityListCommand : LocalizedEntityCommands
{
public string Command => "spawnentitylist";
public string Description => "Spawns a list of entities around you";
public string Help => $"Usage: {Command} <entityListPrototypeId>";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "spawnentitylist";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError($"Invalid arguments.\n{Help}");
shell.WriteError(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
@@ -33,24 +33,23 @@ namespace Content.Server.EntityList
return;
}
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex(args[0], out EntityListPrototype? prototype))
if (!_prototypeManager.TryIndex(args[0], out EntityListPrototype? prototype))
{
shell.WriteError($"No {nameof(EntityListPrototype)} found with id {args[0]}");
shell.WriteError(Loc.GetString($"cmd-spawnentitylist-failed",
("prototype", nameof(EntityListPrototype)),
("id", args[0])));
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var i = 0;
foreach (var entity in prototype.Entities(prototypeManager))
foreach (var entity in prototype.Entities(_prototypeManager))
{
entityManager.SpawnEntity(entity.ID, entityManager.GetComponent<TransformComponent>(attached).Coordinates);
EntityManager.SpawnEntity(entity.ID, EntityManager.GetComponent<TransformComponent>(attached).Coordinates);
i++;
}
shell.WriteLine($"Spawned {i} entities.");
shell.WriteLine(Loc.GetString($"cmd-spawnentitylist-success", ("count", i)));
}
}
}

View File

@@ -5,29 +5,31 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Tools;
using Content.Shared.UserInterface;
using Content.Shared.Administration.Logs;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Emag.Systems;
using Content.Shared.Fax;
using Content.Shared.Fax.Systems;
using Content.Shared.Fax.Components;
using Content.Shared.Fax.Systems;
using Content.Shared.Interaction;
using Content.Shared.Labels.Components;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.Mobs.Components;
using Content.Shared.NameModifier.Components;
using Content.Shared.Paper;
using Content.Shared.Power;
using Content.Shared.Tools;
using Content.Shared.UserInterface;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Content.Shared.NameModifier.Components;
using Content.Shared.Power;
using Content.Shared.DeviceNetwork.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Fax;
@@ -50,6 +52,8 @@ public sealed class FaxSystem : EntitySystem
[Dependency] private readonly FaxecuteSystem _faxecute = default!;
[Dependency] private readonly EmagSystem _emag = default!;
private static readonly ProtoId<ToolQualityPrototype> ScrewingQuality = "Screwing";
private const string PaperSlotId = "Paper";
public override void Initialize()
@@ -209,7 +213,7 @@ public sealed class FaxSystem : EntitySystem
{
if (args.Handled ||
!TryComp<ActorComponent>(args.User, out var actor) ||
!_toolSystem.HasQuality(args.Used, "Screwing")) // Screwing because Pulsing already used by device linking
!_toolSystem.HasQuality(args.Used, ScrewingQuality)) // Screwing because Pulsing already used by device linking
return;
_quickDialog.OpenDialog(actor.PlayerSession,

View File

@@ -46,7 +46,7 @@ namespace Content.Server.Flash
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FlashImmunityComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<FlashComponent, MeleeHitEvent>(OnFlashMeleeHit);
// ran before toggling light for extra-bright lantern
SubscribeLocalEvent<FlashComponent, UseInHandEvent>(OnFlashUseInHand, before: new[] { typeof(HandheldLightSystem) });
@@ -56,7 +56,13 @@ namespace Content.Server.Flash
SubscribeLocalEvent<PermanentBlindnessComponent, FlashAttemptEvent>(OnPermanentBlindnessFlashAttempt);
SubscribeLocalEvent<TemporaryBlindnessComponent, FlashAttemptEvent>(OnTemporaryBlindnessFlashAttempt);
}
private void OnExamine(Entity<FlashImmunityComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("flash-protection"));
}
private void OnFlashMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent args)
{
if (!args.IsHit ||

View File

@@ -10,50 +10,49 @@ using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Commands
{
[AdminCommand(AdminFlags.Round)]
sealed class ForceMapCommand : IConsoleCommand
public sealed class ForceMapCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Command => "forcemap";
public string Description => Loc.GetString("forcemap-command-description");
public string Help => Loc.GetString("forcemap-command-help");
public override string Command => "forcemap";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine(Loc.GetString("forcemap-command-need-one-argument"));
shell.WriteLine(Loc.GetString(Loc.GetString($"shell-need-exactly-one-argument")));
return;
}
var gameMap = IoCManager.Resolve<IGameMapManager>();
var name = args[0];
// An empty string clears the forced map
if (!string.IsNullOrEmpty(name) && !gameMap.CheckMapExists(name))
if (!string.IsNullOrEmpty(name) && !_gameMapManager.CheckMapExists(name))
{
shell.WriteLine(Loc.GetString("forcemap-command-map-not-found", ("map", name)));
shell.WriteLine(Loc.GetString("cmd-forcemap-map-not-found", ("map", name)));
return;
}
_configurationManager.SetCVar(CCVars.GameMap, name);
if (string.IsNullOrEmpty(name))
shell.WriteLine(Loc.GetString("forcemap-command-cleared"));
shell.WriteLine(Loc.GetString("cmd-forcemap-cleared"));
else
shell.WriteLine(Loc.GetString("forcemap-command-success", ("map", name)));
shell.WriteLine(Loc.GetString("cmd-forcemap-success", ("map", name)));
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var options = IoCManager.Resolve<IPrototypeManager>()
var options = _prototypeManager
.EnumeratePrototypes<GameMapPrototype>()
.Select(p => new CompletionOption(p.ID, p.MapName))
.OrderBy(p => p.Value);
return CompletionResult.FromHintOptions(options, Loc.GetString("forcemap-command-arg-map"));
return CompletionResult.FromHintOptions(options, Loc.GetString($"cmd-forcemap-hint"));
}
return CompletionResult.Empty;

View File

@@ -8,51 +8,49 @@ using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Commands
{
[AdminCommand(AdminFlags.Round)]
sealed class ForcePresetCommand : IConsoleCommand
public sealed class ForcePresetCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly GameTicker _ticker = default!;
public string Command => "forcepreset";
public string Description => "Forces a specific game preset to start for the current lobby.";
public string Help => $"Usage: {Command} <preset>";
public override string Command => "forcepreset";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ticker = _e.System<GameTicker>();
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
if (_ticker.RunLevel != GameRunLevel.PreRoundLobby)
{
shell.WriteLine("This can only be executed while the game is in the pre-round lobby.");
shell.WriteLine(Loc.GetString($"cmd-forcepreset-preround-lobby-only"));
return;
}
if (args.Length != 1)
{
shell.WriteLine("Need exactly one argument.");
shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
var name = args[0];
if (!ticker.TryFindGamePreset(name, out var type))
if (!_ticker.TryFindGamePreset(name, out var type))
{
shell.WriteLine($"No preset exists with name {name}.");
shell.WriteLine(Loc.GetString($"cmd-forcepreset-no-preset-found", ("preset", name)));
return;
}
ticker.SetGamePreset(type, true);
shell.WriteLine($"Forced the game to start with preset {name}.");
ticker.UpdateInfoText();
_ticker.SetGamePreset(type, true);
shell.WriteLine(Loc.GetString($"cmd-forcepreset-success", ("preset", name)));
_ticker.UpdateInfoText();
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var options = IoCManager.Resolve<IPrototypeManager>()
var options = _prototypeManager
.EnumeratePrototypes<GamePresetPrototype>()
.OrderBy(p => p.ID)
.Select(p => p.ID);
return CompletionResult.FromHintOptions(options, "<preset>");
return CompletionResult.FromHintOptions(options, Loc.GetString($"cmd-forcepreset-hint"));
}
return CompletionResult.Empty;

View File

@@ -8,40 +8,35 @@ using Robust.Shared.Console;
namespace Content.Server.GameTicking.Commands
{
[AdminCommand(AdminFlags.Round)]
public sealed class GoLobbyCommand : IConsoleCommand
public sealed class GoLobbyCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
public string Command => "golobby";
public string Description => "Enables the lobby and restarts the round.";
public string Help => $"Usage: {Command} / {Command} <preset>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "golobby";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
GamePresetPrototype? preset = null;
var presetName = string.Join(" ", args);
var ticker = _e.System<GameTicker>();
if (args.Length > 0)
{
if (!ticker.TryFindGamePreset(presetName, out preset))
if (!_gameTicker.TryFindGamePreset(presetName, out preset))
{
shell.WriteLine($"No preset found with name {presetName}");
shell.WriteLine(Loc.GetString($"cmd-forcepreset-no-preset-found", ("preset", presetName)));
return;
}
}
var config = IoCManager.Resolve<IConfigurationManager>();
config.SetCVar(CCVars.GameLobbyEnabled, true);
_configManager.SetCVar(CCVars.GameLobbyEnabled, true);
ticker.RestartRound();
_gameTicker.RestartRound();
if (preset != null)
{
ticker.SetGamePreset(preset);
}
_gameTicker.SetGamePreset(preset);
shell.WriteLine($"Enabling the lobby and restarting the round.{(preset == null ? "" : $"\nPreset set to {presetName}")}");
shell.WriteLine(Loc.GetString(preset == null ? "cmd-golobby-success" : "cmd-golobby-success-with-preset", ("preset", presetName)));
}
}
}

View File

@@ -7,31 +7,27 @@ using Robust.Shared.Console;
namespace Content.Server.GameTicking.Commands
{
[AdminCommand(AdminFlags.Round)]
sealed class ToggleDisallowLateJoinCommand : IConsoleCommand
public sealed class ToggleDisallowLateJoinCommand : LocalizedCommands
{
public string Command => "toggledisallowlatejoin";
public string Description => "Allows or disallows latejoining during mid-game.";
public string Help => $"Usage: {Command} <disallow>";
[Dependency] private readonly IConfigurationManager _configManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "toggledisallowlatejoin";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine("Need exactly one argument.");
shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
var cfgMan = IoCManager.Resolve<IConfigurationManager>();
if (bool.TryParse(args[0], out var result))
{
cfgMan.SetCVar(CCVars.GameDisallowLateJoins, bool.Parse(args[0]));
shell.WriteLine(result ? "Late joining has been disabled." : "Late joining has been enabled.");
_configManager.SetCVar(CCVars.GameDisallowLateJoins, bool.Parse(args[0]));
shell.WriteLine(Loc.GetString(result ? "cmd-toggledisallowlatejoin-disabled" : "cmd-toggledisallowlatejoin-enabled"));
}
else
{
shell.WriteLine("Invalid argument.");
}
shell.WriteLine(Loc.GetString($"shell-invalid-bool"));
}
}
}

View File

@@ -1,5 +1,5 @@
using System.Linq;
using Content.Server.Administration.Commands;
using Content.Server.Clothing.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Server.Mind;
@@ -23,6 +23,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly OutfitSystem _outfitSystem = default!;
[Dependency] private readonly PointSystem _point = default!;
[Dependency] private readonly RespawnRuleSystem _respawn = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
@@ -55,7 +56,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
var mob = mobMaybe!.Value;
_mind.TransferTo(newMind, mob);
SetOutfitCommand.SetOutfit(mob, dm.Gear, EntityManager);
_outfitSystem.SetOutfit(mob, dm.Gear);
EnsureComp<KillTrackerComponent>(mob);
_respawn.AddToTracker(ev.Player.UserId, (uid, tracker));

View File

@@ -10,7 +10,7 @@ namespace Content.Server.GhostKick;
// Handles logic for "ghost kicking".
// Basically we boot the client off the server without telling them, so the game shits itself.
// Hilariously isn't it?
// Hilarious, isn't it?
public sealed class GhostKickManager
{
@@ -45,32 +45,30 @@ public sealed class GhostKickManager
}
[AdminCommand(AdminFlags.Moderator)]
public sealed class GhostKickCommand : IConsoleCommand
public sealed class GhostKickCommand : LocalizedEntityCommands
{
public string Command => "ghostkick";
public string Description => "Kick a client from the server as if their network just dropped.";
public string Help => "Usage: ghostkick <Player> [Reason]";
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly GhostKickManager _ghostKick = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "ghostkick";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 1)
{
shell.WriteError("Need at least one argument");
shell.WriteError(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
var playerName = args[0];
var reason = args.Length > 1 ? args[1] : "Ghost kicked by console";
var reason = args.Length > 1 ? args[1] : Loc.GetString($"cmd-ghostkick-default-reason");
var players = IoCManager.Resolve<IPlayerManager>();
var ghostKick = IoCManager.Resolve<GhostKickManager>();
if (!players.TryGetSessionByUsername(playerName, out var player))
if (!_playerManager.TryGetSessionByUsername(playerName, out var player))
{
shell.WriteError($"Unable to find player: '{playerName}'.");
shell.WriteError(Loc.GetString($"shell-target-player-does-not-exist"));
return;
}
ghostKick.DoDisconnect(player.Channel, reason);
_ghostKick.DoDisconnect(player.Channel, reason);
}
}

View File

@@ -71,7 +71,9 @@ public sealed class GlueSystem : SharedGlueSystem
private bool TryGlue(Entity<GlueComponent> entity, EntityUid target, EntityUid actor)
{
// if item is glued then don't apply glue again so it can be removed for reasonable time
if (HasComp<GluedComponent>(target) || !HasComp<ItemComponent>(target))
// If glue is applied to an unremoveable item, the component will disappear after the duration.
// This effecitvely means any unremoveable item could be removed with a bottle of glue.
if (HasComp<GluedComponent>(target) || !HasComp<ItemComponent>(target) || HasComp<UnremoveableComponent>(target))
{
_popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
return false;

View File

@@ -10,6 +10,7 @@ using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mech.EntitySystems;
using Content.Shared.Mobs;
using Content.Shared.Popups;
using Robust.Shared.Audio.Systems;
@@ -56,6 +57,8 @@ namespace Content.Server.Guardian
SubscribeLocalEvent<GuardianHostComponent, GuardianToggleActionEvent>(OnPerformAction);
SubscribeLocalEvent<GuardianComponent, AttackAttemptEvent>(OnGuardianAttackAttempt);
SubscribeLocalEvent<GuardianHostComponent, MechPilotRelayedEvent<GettingAttackedAttemptEvent>>(OnPilotAttackAttempt);
}
private void OnGuardianShutdown(EntityUid uid, GuardianComponent component, ComponentShutdown args)
@@ -144,6 +147,16 @@ namespace Content.Server.Guardian
args.Cancel();
}
private void OnPilotAttackAttempt(Entity<GuardianHostComponent> uid, ref MechPilotRelayedEvent<GettingAttackedAttemptEvent> args)
{
if (args.Args.Cancelled)
return;
_popupSystem.PopupCursor(Loc.GetString("guardian-attack-host"), args.Args.Attacker, PopupType.LargeCaution);
args.Args.Cancelled = true;
}
public void ToggleGuardian(EntityUid user, GuardianHostComponent hostComponent)
{
if (!TryComp<GuardianComponent>(hostComponent.HostedGuardian, out var guardianComponent))

View File

@@ -1,8 +1,12 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Interaction;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Instruments;
using Content.Shared.Instruments.UI;
@@ -17,6 +21,7 @@ using Robust.Shared.Console;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Instruments;
@@ -31,6 +36,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly IAdminLogManager _admingLogSystem = default!;
private const float MaxInstrumentBandRange = 10f;
@@ -50,6 +56,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster);
SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel);
SubscribeNetworkEvent<InstrumentSetChannelsEvent>(OnMidiSetChannels);
Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs =>
{
@@ -132,6 +139,44 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
Clean(uid, instrument);
}
private void OnMidiSetChannels(InstrumentSetChannelsEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
if (!TryComp(uid, out InstrumentComponent? instrument) || !TryComp(uid, out ActiveInstrumentComponent? activeInstrument))
return;
if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
return;
if (msg.Tracks.Length > RobustMidiEvent.MaxChannels)
{
Log.Warning($"{args.SenderSession.UserId.ToString()} - Tried to send tracks over the limit! Received: {msg.Tracks.Length}; Limit: {RobustMidiEvent.MaxChannels}");
return;
}
var tracksString = string.Join("\n",
msg.Tracks
.Where(t => t != null)
.Select(t => t!.ToString()));
_admingLogSystem.Add(
LogType.Instrument,
LogImpact.Low,
$"{ToPrettyString(args.SenderSession.AttachedEntity)} set the midi channels for {ToPrettyString(uid)} to {tracksString}");
// Truncate any track names too long.
foreach (var t in msg.Tracks)
{
t?.TruncateFields(_cfg.GetCVar(CCVars.MidiMaxChannelNameLength));
}
activeInstrument.Tracks = msg.Tracks;
Dirty(uid, activeInstrument);
}
private void OnMidiSetMaster(InstrumentSetMasterEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);

View File

@@ -5,19 +5,21 @@ using Content.Shared.Maps;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Interaction;
[AdminCommand(AdminFlags.Debug)]
public sealed class TilePryCommand : IConsoleCommand
public sealed class TilePryCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public string Command => "tilepry";
public string Description => "Pries up all tiles in a radius around the user.";
public string Help => $"Usage: {Command} <radius>";
private readonly string _platingId = "Plating";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "tilepry";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player?.AttachedEntity is not { } attached)
@@ -33,39 +35,38 @@ public sealed class TilePryCommand : IConsoleCommand
if (!int.TryParse(args[0], out var radius))
{
shell.WriteError($"{args[0]} isn't a valid integer.");
shell.WriteError(Loc.GetString($"cmd-tilepry-arg-must-be-number", ("arg", args[0])));
return;
}
if (radius < 0)
{
shell.WriteError("Radius must be positive.");
shell.WriteError(Loc.GetString($"cmd-tilepry-radius-must-be-positive"));
return;
}
var mapSystem = _entities.System<SharedMapSystem>();
var xform = _entities.GetComponent<TransformComponent>(attached);
var xform = EntityManager.GetComponent<TransformComponent>(attached);
var playerGrid = xform.GridUid;
if (!_entities.TryGetComponent<MapGridComponent>(playerGrid, out var mapGrid))
if (!EntityManager.TryGetComponent<MapGridComponent>(playerGrid, out var mapGrid))
return;
var playerPosition = xform.Coordinates;
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
for (var i = -radius; i <= radius; i++)
{
for (var j = -radius; j <= radius; j++)
{
var tile = mapSystem.GetTileRef(playerGrid.Value, mapGrid, playerPosition.Offset(new Vector2(i, j)));
var coordinates = mapSystem.GridTileToLocal(playerGrid.Value, mapGrid, tile.GridIndices);
var tileDef = (ContentTileDefinition)tileDefinitionManager[tile.Tile.TypeId];
var tile = _mapSystem.GetTileRef(playerGrid.Value, mapGrid, playerPosition.Offset(new Vector2(i, j)));
var coordinates = _mapSystem.GridTileToLocal(playerGrid.Value, mapGrid, tile.GridIndices);
var tileDef = (ContentTileDefinition)_tileDefinitionManager[tile.Tile.TypeId];
if (!tileDef.CanCrowbar) continue;
if (!tileDef.CanCrowbar)
continue;
var plating = tileDefinitionManager["Plating"];
mapSystem.SetTile(playerGrid.Value, mapGrid, coordinates, new Tile(plating.TileId));
var plating = _tileDefinitionManager[_platingId];
_mapSystem.SetTile(playerGrid.Value, mapGrid, coordinates, new Tile(plating.TileId));
}
}
}

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