Merge remote-tracking branch 'upstream/stable' into ed-23-06-2025-upstream-sync

# Conflicts:
#	.github/workflows/check-trailing-whitespace.yml
#	Content.IntegrationTests/Tests/Access/AccessReaderTest.cs
#	Content.IntegrationTests/Tests/Chameleon/ChameleonJobLoadoutTest.cs
#	Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
#	Content.IntegrationTests/Tests/PostMapInitTest.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
#	Content.Shared/Light/Components/SunShadowCycleComponent.cs
#	Resources/Prototypes/Damage/modifier_sets.yml
#	Resources/Prototypes/Maps/Pools/default.yml
This commit is contained in:
Ed
2025-06-23 16:37:22 +03:00
1455 changed files with 168869 additions and 1121592 deletions

View File

@@ -1,59 +0,0 @@
name: Trailing Whitespace Check
on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
jobs:
build:
name: Trailing Whitespace Check
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- name: Get changed text files
id: changed-files
uses: tj-actions/changed-files@v46.0.5
with:
files: |
**.cs
**.yml
**.swsl
**.json
**.py
- name: Check for trailing whitespace and EOF newline
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
run: |
has_trailing_whitespace=0
has_missing_eof_newline=0
for file in ${ALL_CHANGED_FILES}; do
# Ignore vanilla not CrystallEdge files
if [[ "$file" != *CP14* ]]; then
continue
fi
echo "Checking $file"
# Check for trailing whitespace
if grep -qP '[ \t]+$' "$file"; then
echo "::error file=$file::Trailing whitespace found"
has_trailing_whitespace=1
fi
# Check for missing EOF newline
if [ -f "$file" ] && [ -s "$file" ]; then
last_char=$(tail -c 1 "$file")
if [ "$last_char" != "" ] && [ "$last_char" != $'\n' ]; then
echo "::error file=$file::Missing newline at end of file"
has_missing_eof_newline=1
fi
fi
done
if [ "$has_trailing_whitespace" -eq 1 ] || [ "$has_missing_eof_newline" -eq 1 ]; then
echo "Issues found: trailing whitespace or missing EOF newline."
echo "We recommend using an IDE to prevent this from happening."
exit 1
fi

View File

@@ -4,39 +4,20 @@ using Robust.Shared.Console;
namespace Content.Client.Access.Commands;
public sealed class ShowAccessReadersCommand : IConsoleCommand
public sealed class ShowAccessReadersCommand : LocalizedEntityCommands
{
public string Command => "showaccessreaders";
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
public string Description => "Toggles showing access reader permissions on the map";
public string Help => """
Overlay Info:
-Disabled | The access reader is disabled
+Unrestricted | The access reader has no restrictions
+Set [Index]: [Tag Name]| A tag in an access set (accessor needs all tags in the set to be allowed by the set)
+Key [StationUid]: [StationRecordKeyId] | A StationRecordKey that is allowed
-Tag [Tag Name] | A tag that is not allowed (takes priority over other allows)
""";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "showaccessreaders";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var collection = IoCManager.Instance;
var existing = _overlay.RemoveOverlay<AccessOverlay>();
if (!existing)
_overlay.AddOverlay(new AccessOverlay(EntityManager, _cache, _xform));
if (collection == null)
return;
var overlay = collection.Resolve<IOverlayManager>();
if (overlay.RemoveOverlay<AccessOverlay>())
{
shell.WriteLine($"Set access reader debug overlay to false");
return;
}
var entManager = collection.Resolve<IEntityManager>();
var cache = collection.Resolve<IResourceCache>();
var xform = entManager.System<SharedTransformSystem>();
overlay.AddOverlay(new AccessOverlay(entManager, cache, xform));
shell.WriteLine($"Set access reader debug overlay to true");
shell.WriteLine(Loc.GetString($"cmd-showaccessreaders-status", ("status", !existing)));
}
}

View File

@@ -32,7 +32,6 @@ namespace Content.Client.Actions
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public event Action<EntityUid>? OnActionAdded;

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

@@ -203,6 +203,9 @@ namespace Content.Client.Cargo.UI
/// </summary>
public void PopulateOrders(IEnumerable<CargoOrderData> orders)
{
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
return;
Requests.DisposeAllChildren();
foreach (var order in orders)
@@ -237,6 +240,7 @@ namespace Content.Client.Cargo.UI
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
// TODO: Disable based on access.
row.SetApproveVisible(orderConsole.Mode != CargoOrderConsoleMode.SendToPrimary);
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
Requests.AddChild(row);
}
@@ -290,8 +294,8 @@ namespace Content.Client.Cargo.UI
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
_timing.CurTime < orderConsole.NextAccountActionTime;
OrdersSpacer.Visible = !orderConsole.SlipPrinter;
Orders.Visible = !orderConsole.SlipPrinter;
OrdersSpacer.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
Orders.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
}
}
}

View File

@@ -14,5 +14,15 @@ namespace Content.Client.Cargo.UI
{
RobustXamlLoader.Load(this);
}
public void SetApproveVisible(bool visible)
{
Approve.Visible = visible;
if (visible)
Cancel.AddStyleClass("OpenLeft");
else
Cancel.RemoveStyleClass("OpenLeft");
}
}
}

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

@@ -11,8 +11,6 @@ namespace Content.Client.Clothing.Systems;
// All valid items for chameleon are calculated on client startup and stored in dictionary.
public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
[Dependency] private readonly IPrototypeManager _proto = 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);
@@ -310,7 +310,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

@@ -18,6 +18,7 @@ using Content.Client.MainMenu;
using Content.Client.Overlays;
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;
@@ -85,6 +86,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()
{
@@ -134,6 +136,8 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
_prototypeManager.RegisterIgnore("codewordGenerator");
_prototypeManager.RegisterIgnore("codewordFaction");
_componentFactory.GenerateNetIds();
_adminManager.Initialize();
@@ -145,6 +149,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

@@ -50,6 +50,18 @@ namespace Content.Client.Inventory
[ViewVariables]
private readonly EntityUid _virtualHiddenEntity;
/// <summary>
/// The current amount of added hand buttons.
/// </summary>
[ViewVariables]
private int _handCount;
/// <summary>
/// The current shape of the inventory, needed to calculate the window size.
/// </summary>
[ViewVariables]
private Vector2i _inventoryDimensions;
public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_examine = EntMan.System<ExamineSystem>();
@@ -93,6 +105,8 @@ namespace Content.Client.Inventory
return;
_strippingMenu.ClearButtons();
_handCount = 0;
_inventoryDimensions = Vector2i.Zero;
if (EntMan.TryGetComponent<InventoryComponent>(Owner, out var inv))
{
@@ -152,9 +166,15 @@ namespace Content.Client.Inventory
// TODO allow windows to resize based on content's desired size
// for now: shit-code
// this breaks for drones (too many hands, lots of empty vertical space), and looks shit for monkeys and the like.
// but the window is realizable, so eh.
_strippingMenu.SetSize = new Vector2(220, snare?.IsEnsnared == true ? 550 : 530);
// calculate the window size manually
// +20 horizontally and vertically from the ContentsContainer margin
// +16 vertically from the BoxContainer margin
// +27 vertically from the window header
var horizontalMenuSize = Math.Max(200, Math.Max(_handCount, _inventoryDimensions.X + 1) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 20);
var verticalMenuSize = Math.Max(200, (_inventoryDimensions.Y + (_handCount > 0 ? 2 : 1)) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 53);
if (snare?.IsEnsnared == true)
verticalMenuSize += 20;
_strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize);
}
private void AddHandButton(Hand hand)
@@ -172,6 +192,8 @@ namespace Content.Client.Inventory
UpdateEntityIcon(button, hand.HeldEntity);
_strippingMenu!.HandsContainer.AddChild(button);
LayoutContainer.SetPosition(button, new Vector2i(_handCount, 0) * (SlotControl.DefaultButtonSize + ButtonSeparation));
_handCount++;
}
private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot)
@@ -220,6 +242,10 @@ namespace Content.Client.Inventory
UpdateEntityIcon(button, entity);
LayoutContainer.SetPosition(button, slotDef.StrippingWindowPos * (SlotControl.DefaultButtonSize + ButtonSeparation));
if (slotDef.StrippingWindowPos.X > _inventoryDimensions.X)
_inventoryDimensions = new Vector2i(slotDef.StrippingWindowPos.X, _inventoryDimensions.Y);
if (slotDef.StrippingWindowPos.Y > _inventoryDimensions.Y)
_inventoryDimensions = new Vector2i(_inventoryDimensions.X, slotDef.StrippingWindowPos.Y);
}
private void UpdateEntityIcon(SlotControl button, EntityUid? entity)

View File

@@ -15,6 +15,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;
@@ -69,6 +70,7 @@ namespace Content.Client.IoC
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
collection.Register<ClientsidePlaytimeTrackingManager>();
}
}
}

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.Shared.Clothing;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
@@ -7,12 +6,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;
@@ -21,6 +29,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);
@@ -63,32 +72,165 @@ 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];
// Get all loadout prototypes for this group.
var validProtos = _groupProto.Loadouts.Select(id => protoMan.Index(id));
foreach (var loadoutProto in _groupProto.Loadouts)
/*
* 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

@@ -16,7 +16,6 @@ public sealed class JetpackSystem : SharedJetpackSystem
[Dependency] private readonly ClothingSystem _clothing = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{

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

@@ -3,15 +3,15 @@ using Robust.Shared.Console;
namespace Content.Client.Shuttles.Commands;
public sealed class ShowEmergencyShuttleCommand : IConsoleCommand
public sealed class ShowEmergencyShuttleCommand : LocalizedEntityCommands
{
public string Command => "showemergencyshuttle";
public string Description => "Shows the expected position of the emergency shuttle";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly ShuttleSystem _shuttle = default!;
public override string Command => "showemergencyshuttle";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var tstalker = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ShuttleSystem>();
tstalker.EnableShuttlePosition ^= true;
shell.WriteLine($"Set emergency shuttle debug to {tstalker.EnableShuttlePosition}");
_shuttle.EnableShuttlePosition ^= true;
shell.WriteLine(Loc.GetString($"cmd-showemergencyshuttle-status", ("status", _shuttle.EnableShuttlePosition)));
}
}

View File

@@ -8,7 +8,7 @@ namespace Content.Client.Strip
public sealed class StrippingMenu : DefaultWindow
{
public LayoutContainer InventoryContainer = new();
public BoxContainer HandsContainer = new() { Orientation = LayoutOrientation.Horizontal };
public LayoutContainer HandsContainer = new();
public BoxContainer SnareContainer = new();
public bool Dirty = true;

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

@@ -1,7 +1,7 @@
using Content.Client.Eui;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost.Roles.Raffles;
using JetBrains.Annotations;
using Robust.Client.Console;
using Robust.Client.Player;

View File

@@ -1,6 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Server.Ghost.Roles.Raffles;
using System.Numerics;
using Content.Shared.Ghost.Roles.Raffles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;

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

@@ -3,39 +3,33 @@ using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared.Console;
using Robust.Shared.Map;
namespace Content.Client.Weapons.Melee;
public sealed class MeleeSpreadCommand : IConsoleCommand
public sealed class MeleeSpreadCommand : LocalizedEntityCommands
{
public string Command => "showmeleespread";
public string Description => "Shows the current weapon's range and arc for debugging";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly MeleeWeaponSystem _meleeSystem = default!;
[Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public override string Command => "showmeleespread";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var collection = IoCManager.Instance;
if (collection == null)
if (_overlay.RemoveOverlay<MeleeArcOverlay>())
return;
var overlayManager = collection.Resolve<IOverlayManager>();
if (overlayManager.RemoveOverlay<MeleeArcOverlay>())
{
return;
}
var sysManager = collection.Resolve<IEntitySystemManager>();
overlayManager.AddOverlay(new MeleeArcOverlay(
collection.Resolve<IEntityManager>(),
collection.Resolve<IEyeManager>(),
collection.Resolve<IInputManager>(),
collection.Resolve<IPlayerManager>(),
sysManager.GetEntitySystem<MeleeWeaponSystem>(),
sysManager.GetEntitySystem<SharedCombatModeSystem>(),
sysManager.GetEntitySystem<SharedTransformSystem>()));
_overlay.AddOverlay(new MeleeArcOverlay(
EntityManager,
_eyeManager,
_inputManager,
_playerManager,
_meleeSystem,
_combatSystem,
_transformSystem));
}
}

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,9 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Access
@@ -13,6 +13,15 @@ namespace Content.IntegrationTests.Tests.Access
public sealed class AccessReaderTest
{
/*
[TestPrototypes]
private const string Prototypes = @"
- type: entity
id: TestAccessReader
name: access reader
components:
- type: AccessReader
";
[Test]
public async Task TestTags()
{
@@ -20,13 +29,13 @@ namespace Content.IntegrationTests.Tests.Access
var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
await server.WaitAssertion(() =>
{
var system = entityManager.System<AccessReaderSystem>();
var ent = entityManager.SpawnEntity("TestAccessReader", MapCoordinates.Nullspace);
var reader = new Entity<AccessReaderComponent>(ent, entityManager.GetComponent<AccessReaderComponent>(ent));
// test empty
var reader = new AccessReaderComponent();
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
@@ -35,8 +44,7 @@ namespace Content.IntegrationTests.Tests.Access
});
// test deny
reader = new AccessReaderComponent();
reader.DenyTags.Add("A");
system.AddDenyTag(reader, "A");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
@@ -44,10 +52,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "Foo" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.True);
});
system.ClearDenyTags(reader);
// test one list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
system.AddAccess(reader, "A");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -55,10 +63,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
// test one list - two items
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
@@ -66,11 +74,14 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
// test two list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "B", "C" });
var accesses = new List<HashSet<ProtoId<AccessLevelPrototype>>>() {
new HashSet<ProtoId<AccessLevelPrototype>> () { "A" },
new HashSet<ProtoId<AccessLevelPrototype>> () { "B", "C" }
};
system.AddAccesses(reader, accesses);
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -80,11 +91,11 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B", "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
// test deny list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
reader.DenyTags.Add("B");
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
system.AddDenyTag(reader, "B");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -92,6 +103,8 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
system.ClearDenyTags(reader);
});
await pair.CleanReturnAsync();
}

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
{/* //CP14 we dont wanna chameleons, disabled test
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

@@ -16,6 +16,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.NukeOps;
using Content.Shared.Pinpointer;
@@ -23,12 +24,16 @@ using Content.Shared.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.GameRules;
[TestFixture]
public sealed class NukeOpsTest
{/*
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
/// <summary>
/// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded.
/// </summary>
@@ -119,8 +124,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(player));
Assert.That(roleSys.MindIsAntagonist(mind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
var roles = roleSys.MindGetAllRoleInfo(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
@@ -130,8 +135,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(dummyEnts[1]));
Assert.That(roleSys.MindIsAntagonist(dummyMind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
Assert.That(factionSys.IsMember(dummyEnts[1], SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], NanotrasenFaction), Is.False);
roles = roleSys.MindGetAllRoleInfo(dummyMind);
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
@@ -146,8 +151,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
Assert.That(factionSys.IsMember(ent, SyndicateFaction), Is.False);
Assert.That(factionSys.IsMember(ent, NanotrasenFaction), Is.True);
var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
}

View File

@@ -8,6 +8,7 @@ using Content.Server.Roles;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components;
using Robust.Shared.GameObjects;
@@ -20,6 +21,8 @@ public sealed class TraitorRuleTest
{/*
private const string TraitorGameRuleProtoId = "Traitor";
private const string TraitorAntagRoleName = "Traitor";
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
[Test]
public async Task TestTraitorObjectives()
@@ -108,8 +111,8 @@ public sealed class TraitorRuleTest
// Make sure the player is a traitor.
var mind = mindSys.GetMind(player)!.Value;
Assert.That(roleSys.MindIsAntagonist(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
Assert.That(traitorRule.TotalTraitors, Is.EqualTo(1));
Assert.That(traitorRule.TraitorMinds[0], Is.EqualTo(mind));

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

@@ -55,8 +55,6 @@ namespace Content.IntegrationTests.Tests
//CrystallEdge Maps end
"/Maps/centcomm.yml",
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
"/Maps/gate.yml", // Contains positronic brain and LSE-1200c "Perforator"
"/Maps/meta.yml", // Contains warden's rubber stamp
"/Maps/reach.yml", // Contains handheld crew monitor
"/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
"/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp

View File

@@ -47,7 +47,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

@@ -1,6 +1,7 @@
using Content.Server.Wires;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Wires;
namespace Content.Server.Access;
@@ -23,23 +24,21 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
public override bool Cut(EntityUid user, Wire wire, AccessReaderComponent comp)
{
WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
comp.Enabled = false;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
{
comp.Enabled = true;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, AccessReaderComponent comp)
{
comp.Enabled = false;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), false);
WiresSystem.StartWireAction(wire.Owner, _pulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire));
}
@@ -57,8 +56,7 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
{
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
{
access.Enabled = true;
EntityManager.Dirty(wire.Owner, access);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, access), true);
}
}
}

View File

@@ -1,8 +1,8 @@
using Content.Server.Administration;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
namespace Content.Server.Access;
@@ -19,7 +19,7 @@ public sealed class AddAccessLogCommand : ToolshedCommand
ctx.WriteLine($"WARNING: Surpassing the limit of the log by {accessLogCount - accessReader.AccessLogLimit+1} entries!");
var accessTime = TimeSpan.FromSeconds(seconds);
accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessor));
EntityManager.System<AccessReaderSystem>().LogAccess((input, accessReader), accessor, accessTime, true);
ctx.WriteLine($"Successfully added access log to {input} with this information inside:\n " +
$"Time of access: {accessTime}\n " +
$"Accessed by: {accessor}");

View File

@@ -37,21 +37,21 @@ public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComp
public override bool Cut(EntityUid user, Wire wire, AccessReaderComponent comp)
{
WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
comp.LoggingDisabled = true;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
{
comp.LoggingDisabled = false;
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, AccessReaderComponent comp)
{
_access.LogAccess((wire.Owner, comp), Loc.GetString(PulseLog));
comp.LoggingDisabled = true;
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), false);
WiresSystem.StartWireAction(wire.Owner, PulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire));
}
@@ -64,7 +64,7 @@ public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComp
private void AwaitPulseCancel(Wire wire)
{
if (!wire.IsCut && EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var comp))
comp.LoggingDisabled = false;
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), true);
}
private enum PulseTimeoutKey : byte

View File

@@ -168,21 +168,6 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
return accessList;
}
private List<HashSet<ProtoId<AccessLevelPrototype>>> ConvertAccessListToHashSet(List<ProtoId<AccessLevelPrototype>> accessList)
{
List<HashSet<ProtoId<AccessLevelPrototype>>> accessHashsets = new List<HashSet<ProtoId<AccessLevelPrototype>>>();
if (accessList != null && accessList.Any())
{
foreach (ProtoId<AccessLevelPrototype> access in accessList)
{
accessHashsets.Add(new HashSet<ProtoId<AccessLevelPrototype>>() { access });
}
}
return accessHashsets;
}
/// <summary>
/// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader.
/// </summary>
@@ -244,12 +229,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
_adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
_accessReader.SetAccesses(accessReaderEnt.Value, newAccessList);
var ev = new OnAccessOverriderAccessUpdatedEvent(player);
RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
Dirty(accessReaderEnt.Value);
}
/// <summary>

View File

@@ -15,7 +15,6 @@ namespace Content.Server.Actions;
public sealed class ActionOnInteractSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;

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

@@ -8,32 +8,30 @@ using Robust.Shared.Utility;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.AdminWho)]
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

@@ -12,12 +12,10 @@ namespace Content.Server.Administration.Commands;
public sealed class PersistenceSave : LocalizedEntityCommands
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
public override string Command => "persistencesave";
public override string Description => "Saves server data to a persistence file to be loaded later.";
public override string Help => "persistencesave [mapId] [filePath - default: game.map (CCVar) ]";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
@@ -47,8 +45,7 @@ public sealed class PersistenceSave : LocalizedEntityCommands
return;
}
var mapLoader = _system.GetEntitySystem<MapLoaderSystem>();
mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath));
_mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath));
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
}
}

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

@@ -5,6 +5,7 @@ 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;
@@ -21,6 +22,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";
@@ -148,7 +150,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,10 +1,10 @@
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.Clothing.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
using Content.Server.GhostKick;
@@ -42,7 +42,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;
@@ -80,6 +79,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!;
// All smite verbs have names so invokeverb works.
private void AddSmiteVerbs(GetVerbsEvent<Verb> args)
@@ -416,7 +416,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);
@@ -587,7 +587,7 @@ public sealed partial class AdminVerbSystem
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);
@@ -685,7 +685,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");

View File

@@ -38,18 +38,21 @@ public sealed class SolutionCommand : ToolshedCommand
public SolutionRef AdjReagent(
[PipedArgument] SolutionRef input,
ProtoId<ReagentPrototype> proto,
FixedPoint2 amount
float amount
)
{
_solutionContainer ??= GetSys<SharedSolutionContainerSystem>();
if (amount > 0)
// Convert float to FixedPoint2
var amountFixed = FixedPoint2.New(amount);
if (amountFixed > 0)
{
_solutionContainer.TryAddReagent(input.Solution, proto, amount, out _);
_solutionContainer.TryAddReagent(input.Solution, proto, amountFixed, out _);
}
else if (amount < 0)
else if (amountFixed < 0)
{
_solutionContainer.RemoveReagent(input.Solution, proto, -amount);
_solutionContainer.RemoveReagent(input.Solution, proto, -amountFixed);
}
return input;
@@ -59,7 +62,7 @@ public sealed class SolutionCommand : ToolshedCommand
public IEnumerable<SolutionRef> AdjReagent(
[PipedArgument] IEnumerable<SolutionRef> input,
ProtoId<ReagentPrototype> name,
FixedPoint2 amount
float amount
)
=> input.Select(x => AdjReagent(x, name, amount));
}

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

@@ -7,6 +7,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;
@@ -14,12 +15,14 @@ namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
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);
@@ -105,10 +108,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

@@ -1,11 +1,10 @@
using Content.Server.Actions;
using Content.Server.Bed.Components;
using Content.Server.Body.Systems;
using Content.Server.Power.EntitySystems;
using Content.Shared.Bed;
using Content.Shared.Bed.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Emag.Systems;

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

@@ -1,8 +1,8 @@
using Content.Server.Body.Components;
using Content.Shared.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
@@ -10,6 +10,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Drunk;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;
@@ -204,10 +205,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;
@@ -226,7 +230,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);
}
@@ -247,18 +251,30 @@ public sealed class BloodstreamSystem : EntitySystem
/// </summary>
private void OnHealthBeingExamined(Entity<BloodstreamComponent> ent, ref HealthBeingExaminedEvent args)
{
// Shows profusely bleeding at half the max bleed rate.
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2)
// Shows massively bleeding at 0.75x the max bleed rate.
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.75f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-massive-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding, but less than profusely.
else if (ent.Comp.BleedAmount > 0)
// Shows bleeding message when bleeding above half the max rate, but less than massively.
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.5f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-strong-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding above 0.25x the max rate, but less than half the max.
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.25f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding below 0.25x the max cap
else if (ent.Comp.BleedAmount > 0)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-slight-bleeding", ("target", ent.Owner)));
}
// If the mob's blood level is below the damage threshhold, the pale message is added.
if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)

View File

@@ -1,9 +1,12 @@
using System.Numerics;
using Content.Server.Body.Components;
using Content.Server.Ghost;
using Content.Server.Humanoid;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
using Content.Shared.Damage.Components;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
@@ -11,8 +14,6 @@ using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Timing;
using System.Numerics;
using Content.Shared.Damage.Components;
namespace Content.Server.Body.Systems;

View File

@@ -1,9 +1,10 @@
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
@@ -231,29 +232,4 @@ namespace Content.Server.Body.Systems
_solutionContainerSystem.UpdateChemicals(soln.Value);
}
}
// TODO REFACTOR THIS
// This will cause rates to slowly drift over time due to floating point errors.
// Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,
float Multiplier,
bool Apply)
{
/// <summary>
/// The entity whose metabolism is being modified.
/// </summary>
public readonly EntityUid Uid = Uid;
/// <summary>
/// What the metabolism's update rate will be multiplied by.
/// </summary>
public readonly float Multiplier = Multiplier;
/// <summary>
/// If true, apply the multiplier. If false, revert it.
/// </summary>
public readonly bool Apply = Apply;
}
}

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