Merge branch 'master' into fix_highlight

This commit is contained in:
vitopigno
2025-06-12 17:20:50 +02:00
committed by GitHub
571 changed files with 17305 additions and 1111896 deletions

View File

@@ -1,54 +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
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

@@ -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

@@ -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

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

View File

@@ -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

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

View File

@@ -3,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,18 +107,18 @@ 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 the character's name is highlighted only when mentioned directly (eg. it's said by someone),
// for example in 'Name Surname says, "..."' 'Name Surname' won't be highlighted.
keyword = Regex.Replace(keyword, "^@", @"(?<=(?<=,.*"".*)|(?<!\[Name].*))");
keyword = StartAtSign.Replace(keyword, @"(?<=(?<=,.*"".*)|(?<!\[Name].*))");
_highlights.Add(keyword);
}
@@ -133,7 +138,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)
@@ -145,9 +150,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

@@ -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

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

View File

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

View File

@@ -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

@@ -48,8 +48,6 @@ namespace Content.IntegrationTests.Tests
{
"/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
@@ -62,27 +60,20 @@ namespace Content.IntegrationTests.Tests
"Dev",
"TestTeg",
"Fland",
"Meta",
"Packed",
"Omega",
"Bagel",
"CentComm",
"Box",
"Core",
"Marathon",
"MeteorArena",
"Saltern",
"Reach",
"Train",
"Oasis",
"Gate",
"Amber",
"Loop",
"Plasma",
"Elkridge",
"Convex",
"Relic",
"dm01-entryway"
"dm01-entryway",
};

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

@@ -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);
@@ -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

@@ -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

@@ -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

@@ -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;
@@ -247,18 +248,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;
}
}

View File

@@ -3,18 +3,19 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chat.Systems;
using Content.Server.EntityEffects;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;

View File

@@ -100,7 +100,7 @@ namespace Content.Server.Cargo.Systems
{
OnInteractUsingCash(uid, component, ref args);
}
else if (TryComp<CargoSlipComponent>(args.Used, out var slip) && !component.SlipPrinter)
else if (TryComp<CargoSlipComponent>(args.Used, out var slip) && component.Mode == CargoOrderConsoleMode.DirectOrder)
{
OnInteractUsingSlip((uid, component), ref args, slip);
}
@@ -144,7 +144,7 @@ namespace Content.Server.Cargo.Systems
if (args.Actor is not { Valid: true } player)
return;
if (component.SlipPrinter)
if (component.Mode != CargoOrderConsoleMode.DirectOrder)
return;
if (!_accessReaderSystem.IsAllowed(player, uid))
@@ -181,7 +181,7 @@ namespace Content.Server.Cargo.Systems
return;
}
var amount = GetOutstandingOrderCount(orderDatabase, order.Account);
var amount = GetOutstandingOrderCount((station.Value, orderDatabase), order.Account);
var capacity = orderDatabase.Capacity;
// Too many orders, avoid them getting spammed in the UI.
@@ -312,7 +312,7 @@ namespace Content.Server.Cargo.Systems
{
var station = _station.GetOwningStation(uid);
if (component.SlipPrinter)
if (component.Mode != CargoOrderConsoleMode.DirectOrder)
return;
if (!TryGetOrderDatabase(station, out var orderDatabase))
@@ -367,6 +367,9 @@ namespace Content.Server.Cargo.Systems
if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
return;
if (!TryComp<StationBankAccountComponent>(stationUid, out var bank))
return;
if (!_protoMan.TryIndex<CargoProductPrototype>(args.CargoProductId, out var product))
{
Log.Error($"Tried to add invalid cargo product {args.CargoProductId} as order!");
@@ -376,15 +379,17 @@ namespace Content.Server.Cargo.Systems
if (!GetAvailableProducts((uid, component)).Contains(args.CargoProductId))
return;
if (component.SlipPrinter)
if (component.Mode == CargoOrderConsoleMode.PrintSlip)
{
OnAddOrderMessageSlipPrinter(uid, component, args, product);
return;
}
var targetAccount = component.Mode == CargoOrderConsoleMode.SendToPrimary ? bank.PrimaryAccount : component.Account;
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase), component.Account);
if (!TryAddOrder(stationUid.Value, component.Account, data, orderDatabase))
if (!TryAddOrder(stationUid.Value, targetAccount, data, orderDatabase))
{
PlayDenySound(uid, component);
return;
@@ -419,15 +424,33 @@ namespace Content.Server.Cargo.Systems
CargoConsoleUiKey.Orders,
new CargoConsoleInterfaceState(
MetaData(station.Value).EntityName,
GetOutstandingOrderCount(orderDatabase, console.Account),
GetOutstandingOrderCount((station!.Value, orderDatabase), console.Account),
orderDatabase.Capacity,
GetNetEntity(station.Value),
orderDatabase.Orders[console.Account],
RelevantOrders((station!.Value, orderDatabase), (consoleUid, console)),
GetAvailableProducts((consoleUid, console))
));
}
}
/// <summary>
/// Gets orders relevant to this account, i.e. orders on the account directly or orders on behalf of the account in the primary account.
/// </summary>
private List<CargoOrderData> RelevantOrders(Entity<StationCargoOrderDatabaseComponent> station, Entity<CargoOrderConsoleComponent> console)
{
if (!TryComp<StationBankAccountComponent>(station, out var bank))
return [];
var ourOrders = station.Comp.Orders[console.Comp.Account];
if (console.Comp.Account == bank.PrimaryAccount)
return ourOrders;
var otherOrders = station.Comp.Orders[bank.PrimaryAccount].Where(order => order.Account == console.Comp.Account);
return ourOrders.Concat(otherOrders).ToList();
}
private void ConsolePopup(EntityUid actor, string text)
{
_popup.PopupCursor(text, actor);
@@ -447,17 +470,32 @@ namespace Content.Server.Cargo.Systems
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason, account);
}
public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component, ProtoId<CargoAccountPrototype> account)
public int GetOutstandingOrderCount(Entity<StationCargoOrderDatabaseComponent> station, ProtoId<CargoAccountPrototype> account)
{
var amount = 0;
foreach (var order in component.Orders[account])
if (!TryComp<StationBankAccountComponent>(station, out var bank))
return amount;
foreach (var order in station.Comp.Orders[account])
{
if (!order.Approved)
continue;
amount += order.OrderQuantity - order.NumDispatched;
}
if (account == bank.PrimaryAccount)
return amount;
foreach (var order in station.Comp.Orders[bank.PrimaryAccount])
{
if (order.Account != account)
continue;
if (!order.Approved)
continue;
amount += order.OrderQuantity - order.NumDispatched;
}
return amount;
}

View File

@@ -2,18 +2,17 @@ using System.Linq;
using System.Numerics;
using Content.Server.Administration.Logs;
using Content.Server.Decals;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Crayon;
using Content.Shared.Database;
using Content.Shared.Decals;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Crayon;

View File

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

View File

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

View File

@@ -1,21 +1,20 @@
using Content.Server.Body.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Kitchen;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Random;

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Mech.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
@@ -13,16 +14,17 @@ using Content.Shared.Mech.Components;
using Content.Shared.Mech.EntitySystems;
using Content.Shared.Movement.Events;
using Content.Shared.Popups;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Content.Shared.Verbs;
using Content.Shared.Wires;
using Content.Server.Body.Systems;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Content.Shared.Wires;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
namespace Content.Server.Mech.Systems;
@@ -40,6 +42,8 @@ public sealed partial class MechSystem : SharedMechSystem
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
private static readonly ProtoId<ToolQualityPrototype> PryingQuality = "Prying";
/// <inheritdoc/>
public override void Initialize()
{
@@ -91,7 +95,7 @@ public sealed partial class MechSystem : SharedMechSystem
return;
}
if (_toolSystem.HasQuality(args.Used, "Prying") && component.BatterySlot.ContainedEntity != null)
if (_toolSystem.HasQuality(args.Used, PryingQuality) && component.BatterySlot.ContainedEntity != null)
{
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.BatteryRemovalDelay,
new RemoveBatteryEvent(), uid, target: uid, used: args.Target)

View File

@@ -9,12 +9,11 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Temperature.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.UserInterface;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Climbing.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
@@ -26,11 +25,14 @@ using Content.Shared.Interaction;
using Content.Shared.Medical.Cryogenics;
using Content.Shared.MedicalScanner;
using Content.Shared.Power;
using Content.Shared.Tools;
using Content.Shared.Tools.Systems;
using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
namespace Content.Server.Medical;
@@ -51,6 +53,8 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
private static readonly ProtoId<ToolQualityPrototype> PryingQuality = "Prying";
public override void Initialize()
{
base.Initialize();
@@ -211,7 +215,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
if (args.Handled || !entity.Comp.Locked || entity.Comp.BodyContainer.ContainedEntity == null)
return;
args.Handled = _toolSystem.UseTool(args.Used, args.User, entity.Owner, entity.Comp.PryDelay, "Prying", new CryoPodPryFinished());
args.Handled = _toolSystem.UseTool(args.Used, args.User, entity.Owner, entity.Comp.PryDelay, PryingQuality, new CryoPodPryFinished());
}
private void OnExamined(Entity<CryoPodComponent> entity, ref ExaminedEvent args)

View File

@@ -4,8 +4,10 @@ using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition.Components;

View File

@@ -1,6 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Server.Player;
@@ -60,4 +59,12 @@ public sealed class RenameCommand : LocalizedEntityCommands
entityUid = EntityUid.Invalid;
return false;
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromOptions(CompletionHelper.SessionNames());
return CompletionResult.Empty;
}
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Nutrition.Components;

View File

@@ -1,4 +1,4 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Nutrition.Components;

View File

@@ -1,30 +1,25 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Inventory;
using Content.Server.Popups;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry;
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.DoAfter;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
@@ -36,19 +31,15 @@ namespace Content.Server.Nutrition.EntitySystems;
public sealed class DrinkSystem : SharedDrinkSystem
{
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
[Dependency] private readonly FoodSystem _food = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly ReactiveSystem _reaction = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly StomachSystem _stomach = default!;
@@ -65,7 +56,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
// run after openable so its always open -> drink
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]);
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter);
}
@@ -157,76 +147,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
_appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance);
}
/// <summary>
/// Tries to feed the drink item to the target entity
/// </summary>
private bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
{
if (!HasComp<BodyComponent>(target))
return false;
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
return false;
if (_openable.IsClosed(item, user))
return true;
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
{
if (drink.IgnoreEmpty)
return false;
_popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
return true;
}
if (_food.IsMouthBlocked(target, user))
return true;
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
return true;
var forceDrink = user != target;
if (forceDrink)
{
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
// logging
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
else
{
// log voluntary drinking
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
var doAfterEventArgs = new DoAfterArgs(EntityManager,
user,
forceDrink ? drink.ForceFeedDelay : drink.Delay,
new ConsumeDoAfterEvent(drink.Solution, flavors),
eventTarget: item,
target: target,
used: item)
{
BreakOnHandChange = false,
BreakOnMove = forceDrink,
BreakOnDamage = true,
MovementThreshold = 0.01f,
DistanceThreshold = 1.0f,
// do-after will stop if item is dropped when trying to feed someone else
// or if the item started out in the user's own hands
NeedHand = forceDrink || _hands.IsHolding(user, item),
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
return true;
}
/// <summary>
/// Raised directed at a victim when someone has force fed them a drink.
/// </summary>
@@ -241,7 +161,7 @@ public sealed class DrinkSystem : SharedDrinkSystem
if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
return;
if (_openable.IsClosed(args.Used.Value, args.Target.Value))
if (_openable.IsClosed(args.Used.Value, args.Target.Value, predicted: true))
return;
// TODO this should really be checked every tick.
@@ -330,36 +250,4 @@ public sealed class DrinkSystem : SharedDrinkSystem
if (!forceDrink && solution.Volume > 0)
args.Repeat = true;
}
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
{
if (entity.Owner == ev.User ||
!ev.CanInteract ||
!ev.CanAccess ||
!TryComp<BodyComponent>(ev.User, out var body) ||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
return;
// Make sure the solution exists
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
return;
// no drinking from living drinks, have to kill them first.
if (_mobState.IsAlive(entity))
return;
var user = ev.User;
AlternativeVerb verb = new()
{
Act = () =>
{
TryDrink(user, user, entity.Comp, entity);
},
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
Text = Loc.GetString("drink-system-verb-drink"),
Priority = 2
};
ev.Verbs.Add(verb);
}
}

View File

@@ -1,19 +1,16 @@
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.DoAfter;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Nutrition;
using System.Threading;
using Content.Shared.Atmos;
using Content.Shared.Nutrition.EntitySystems;
/// <summary>
/// System for vapes

View File

@@ -1,79 +0,0 @@
using Content.Shared.Containers.ItemSlots;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Server.Nutrition.EntitySystems
{
/// <summary>
/// Handles usage of the utensils on the food items
/// </summary>
internal sealed class UtensilSystem : SharedUtensilSystem
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly FoodSystem _foodSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
}
/// <summary>
/// Clicked with utensil
/// </summary>
private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
{
if (ev.Handled || ev.Target == null || !ev.CanReach)
return;
var result = TryUseUtensil(ev.User, ev.Target.Value, entity);
ev.Handled = result.Handled;
}
public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
{
if (!EntityManager.TryGetComponent(target, out FoodComponent? food))
return (false, false);
//Prevents food usage with a wrong utensil
if ((food.Utensil & utensil.Comp.Types) == 0)
{
_popupSystem.PopupEntity(Loc.GetString("food-system-wrong-utensil", ("food", target), ("utensil", utensil.Owner)), user, user);
return (false, true);
}
if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
return (false, true);
return _foodSystem.TryFeed(user, user, target, food);
}
/// <summary>
/// Attempt to break the utensil after interaction.
/// </summary>
/// <param name="uid">Utensil.</param>
/// <param name="userUid">User of the utensil.</param>
public void TryBreak(EntityUid uid, EntityUid userUid, UtensilComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (_robustRandom.Prob(component.BreakChance))
{
_audio.PlayPvs(component.BreakSound, userUid, AudioParams.Default.WithVolume(-2f));
EntityManager.DeleteEntity(uid);
}
}
}
}

View File

@@ -20,7 +20,6 @@ using Content.Shared.Popups;
using Robust.Server.Audio;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

View File

@@ -1,14 +1,24 @@
using Content.Shared.Security.Components;
using Content.Shared.Security.Systems;
using Content.Shared.Wall;
namespace Content.Server.Security;
public sealed class GenpopSystem : SharedGenpopSystem
{
private const float GenpopIDEjectDistanceFromWall = 1f;
protected override void CreateId(Entity<GenpopLockerComponent> ent, string name, float sentence, string crime)
{
// Default to prisoner locker coordinates for ID spawn
var xform = Transform(ent);
var uid = Spawn(ent.Comp.IdCardProto, xform.Coordinates);
var spawnCoordinates = xform.Coordinates;
// Offset prisoner wall locker coordinates in wallmount direction for ID spawn; avoids spawning ID inside wall
if (TryComp<WallMountComponent>(ent, out var wallMountComponent))
{
var offset = (wallMountComponent.Direction + xform.LocalRotation - Math.PI / 2).ToVec() * GenpopIDEjectDistanceFromWall;
spawnCoordinates = spawnCoordinates.Offset(offset);
}
var uid = Spawn(ent.Comp.IdCardProto, spawnCoordinates);
ent.Comp.LinkedId = uid;
IdCard.TryChangeFullName(uid, name);

View File

@@ -53,7 +53,7 @@ public sealed class CargoGiftsRule : StationEventSystem<CargoGiftsRuleComponent>
}
// Add some presents
var outstanding = CargoSystem.GetOutstandingOrderCount(cargoDb, component.Account);
var outstanding = _cargoSystem.GetOutstandingOrderCount((station.Value, cargoDb), component.Account);
while (outstanding < cargoDb.Capacity - component.OrderSpaceToLeave && component.Gifts.Count > 0)
{
// I wish there was a nice way to pop this

View File

@@ -227,14 +227,13 @@ namespace Content.Server.VendingMachines
}
// Default spawn coordinates
var spawnCoordinates = Transform(uid).Coordinates;
var xform = Transform(uid);
var spawnCoordinates = xform.Coordinates;
//Make sure the wallvends spawn outside of the wall.
if (TryComp<WallMountComponent>(uid, out var wallMountComponent))
{
var offset = wallMountComponent.Direction.ToWorldVec() * WallVendEjectDistanceFromWall;
var offset = (wallMountComponent.Direction + xform.LocalRotation - Math.PI / 2).ToVec() * WallVendEjectDistanceFromWall;
spawnCoordinates = spawnCoordinates.Offset(offset);
}

View File

@@ -10,6 +10,7 @@ using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
@@ -29,6 +30,9 @@ public sealed class WiresSystem : SharedWiresSystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ConstructionSystem _construction = default!;
private static readonly ProtoId<ToolQualityPrototype> CuttingQuality = "Cutting";
private static readonly ProtoId<ToolQualityPrototype> PulsingQuality = "Pulsing";
// This is where all the wire layouts are stored.
[ViewVariables] private readonly Dictionary<string, WireLayout> _layouts = new();
@@ -443,8 +447,8 @@ public sealed class WiresSystem : SharedWiresSystem
if (!IsPanelOpen(uid))
return;
if (Tool.HasQuality(args.Used, "Cutting", tool) ||
Tool.HasQuality(args.Used, "Pulsing", tool))
if (Tool.HasQuality(args.Used, CuttingQuality, tool) ||
Tool.HasQuality(args.Used, PulsingQuality, tool))
{
if (TryComp(args.User, out ActorComponent? actor))
{
@@ -623,7 +627,7 @@ public sealed class WiresSystem : SharedWiresSystem
switch (action)
{
case WiresAction.Cut:
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
{
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
return;
@@ -637,7 +641,7 @@ public sealed class WiresSystem : SharedWiresSystem
break;
case WiresAction.Mend:
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
{
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
return;
@@ -651,7 +655,7 @@ public sealed class WiresSystem : SharedWiresSystem
break;
case WiresAction.Pulse:
if (!Tool.HasQuality(toolEntity, "Pulsing", tool))
if (!Tool.HasQuality(toolEntity, PulsingQuality, tool))
{
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), user);
return;
@@ -710,7 +714,7 @@ public sealed class WiresSystem : SharedWiresSystem
switch (action)
{
case WiresAction.Cut:
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
{
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
break;
@@ -731,7 +735,7 @@ public sealed class WiresSystem : SharedWiresSystem
UpdateUserInterface(used);
break;
case WiresAction.Mend:
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
{
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
break;
@@ -752,7 +756,7 @@ public sealed class WiresSystem : SharedWiresSystem
UpdateUserInterface(used);
break;
case WiresAction.Pulse:
if (!Tool.HasQuality(toolEntity, "Pulsing", tool))
if (!Tool.HasQuality(toolEntity, PulsingQuality, tool))
{
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), user);
break;

View File

@@ -38,6 +38,7 @@ using Content.Shared.Ghost.Roles.Components;
using Content.Shared.Tag;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Shared.NPC.Prototypes;
namespace Content.Server.Zombies;
@@ -66,6 +67,8 @@ public sealed partial class ZombieSystem
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
/// <summary>
/// Handles an entity turning into a zombie when they die or go into crit
/// </summary>
@@ -223,7 +226,7 @@ public sealed partial class ZombieSystem
_mobState.ChangeMobState(target, MobState.Alive);
_faction.ClearFactions(target, dirty: false);
_faction.AddFaction(target, "Zombie");
_faction.AddFaction(target, ZombieFaction);
//gives it the funny "Zombie ___" name.
_nameMod.RefreshNameModifiers(target);

View File

@@ -472,5 +472,10 @@ public enum LogType
/// <summary>
/// Damaging grid collision has occurred.
/// </summary>
ShuttleImpact = 102
ShuttleImpact = 102,
/// <summary>
/// Events relating to midi playback.
/// </summary>
Instrument = 103,
}

View File

@@ -320,8 +320,7 @@ namespace Content.Shared.Atmos
/// (The pressure threshold is so low that it doesn't make sense to do any calculations,
/// so it just applies this flat value).
/// </summary>
// Original value is 4, buff back when we have proper ways for players to deal with breaches.
public const int LowPressureDamage = 1;
public const int LowPressureDamage = 4;
public const float WindowHeatTransferCoefficient = 0.1f;

View File

@@ -119,14 +119,16 @@ public abstract partial class SharedAtmosPipeLayersSystem : EntitySystem
if (ent.Comp.NumberOfPipeLayers <= 1 || ent.Comp.PipeLayersLocked)
return;
if (!TryComp<ToolComponent>(args.Used, out var tool) || !_tool.HasQuality(args.Used, ent.Comp.Tool, tool))
return;
if (TryComp<SubFloorHideComponent>(ent, out var subFloorHide) && subFloorHide.IsUnderCover)
{
_popup.PopupPredicted(Loc.GetString("atmos-pipe-layers-component-cannot-adjust-pipes"), ent, args.User);
_popup.PopupClient(Loc.GetString("atmos-pipe-layers-component-cannot-adjust-pipes"), ent, args.User);
return;
}
if (TryComp<ToolComponent>(args.Used, out var tool) && _tool.HasQuality(args.Used, ent.Comp.Tool, tool))
_tool.UseTool(args.Used, args.User, ent, ent.Comp.Delay, tool.Qualities, new TrySetNextPipeLayerCompletedEvent());
_tool.UseTool(args.Used, args.User, ent, ent.Comp.Delay, tool.Qualities, new TrySetNextPipeLayerCompletedEvent());
}
private void OnUseInHandEvent(Entity<AtmosPipeLayersComponent> ent, ref UseInHandEvent args)
@@ -141,7 +143,7 @@ public abstract partial class SharedAtmosPipeLayersSystem : EntitySystem
var toolName = Loc.GetString(toolProto.ToolName).ToLower();
var message = Loc.GetString("atmos-pipe-layers-component-tool-missing", ("toolName", toolName));
_popup.PopupPredicted(message, ent, args.User);
_popup.PopupClient(message, ent, args.User);
}
return;
@@ -217,7 +219,7 @@ public abstract partial class SharedAtmosPipeLayersSystem : EntitySystem
var layerName = GetPipeLayerName(ent.Comp.CurrentPipeLayer);
var message = Loc.GetString("atmos-pipe-layers-component-change-layer", ("layerName", layerName));
_popup.PopupPredicted(message, ent, user);
_popup.PopupClient(message, ent, user);
}
}

View File

@@ -1,13 +1,14 @@
using Content.Server.Body.Systems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
namespace Content.Shared.Body.Components
{
[RegisterComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
[RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
public sealed partial class StomachComponent : Component
{
/// <summary>
@@ -32,7 +33,7 @@ namespace Content.Server.Body.Components
/// What solution should this stomach push reagents into, on the body?
/// </summary>
[DataField]
public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
public string BodySolutionName = "chemicals";
/// <summary>
/// Time between reagents being ingested and them being

View File

@@ -0,0 +1,26 @@
namespace Content.Shared.Body.Events;
// 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;
}

View File

@@ -1,12 +1,14 @@
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Body.Components;
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 Robust.Shared.Containers;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Body.Systems
namespace Content.Shared.Body.Systems
{
public sealed class StomachSystem : EntitySystem
{
@@ -19,6 +21,7 @@ namespace Content.Server.Body.Systems
{
SubscribeLocalEvent<StomachComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StomachComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<StomachComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<StomachComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
@@ -32,6 +35,16 @@ namespace Content.Server.Body.Systems
ent.Comp.NextUpdate += args.PausedTime;
}
private void OnEntRemoved(Entity<StomachComponent> ent, ref EntRemovedFromContainerMessage args)
{
// Make sure the removed entity was our contained solution
if (ent.Comp.Solution is not { } solution || args.Entity != solution.Owner)
return;
// Cleared our cached reference to the solution entity
ent.Comp.Solution = null;
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<StomachComponent, OrganComponent, SolutionContainerManagerComponent>();

View File

@@ -1,5 +1,4 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Burial;
using Content.Shared.Burial.Components;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
@@ -10,7 +9,7 @@ using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Burial.Systems;
namespace Content.Shared.Burial;
public sealed class BurialSystem : EntitySystem
{

View File

@@ -15,4 +15,10 @@ public sealed partial class CCVars
public static readonly CVarDef<int> MaxMidiLaggedBatches =
CVarDef.Create("midi.max_lagged_batches", 8, CVar.SERVERONLY);
/// <summary>
/// Defines the max amount of characters to allow in the "Midi channel selector".
/// </summary>
public static readonly CVarDef<int> MidiMaxChannelNameLength =
CVarDef.Create("midi.max_channel_name_length", 64, CVar.SERVERONLY);
}

View File

@@ -38,7 +38,7 @@ public sealed partial class CCVars
/// some food object won't spam a user with flavors.
/// </summary>
public static readonly CVarDef<int>
FlavorLimit = CVarDef.Create("flavor.limit", 10, CVar.SERVERONLY);
FlavorLimit = CVarDef.Create("flavor.limit", 10, CVar.SERVER | CVar.REPLICATED);
public static readonly CVarDef<string> DestinationFile =
CVarDef.Create("autogen.destination_file", "", CVar.SERVER | CVar.SERVERONLY);

View File

@@ -104,10 +104,10 @@ public sealed partial class CargoOrderConsoleComponent : Component
public static readonly ProtoId<RadioChannelPrototype> BaseAnnouncementChannel = "Supply";
/// <summary>
/// If set to true, restricts this console from ordering and has it print slips instead
/// The behaviour of the cargo console regarding orders
/// </summary>
[DataField]
public bool SlipPrinter;
public CargoOrderConsoleMode Mode = CargoOrderConsoleMode.DirectOrder;
/// <summary>
/// The time at which the console will be able to print a slip again.
@@ -146,6 +146,26 @@ public sealed partial class CargoOrderConsoleComponent : Component
public TimeSpan DenySoundDelay = TimeSpan.FromSeconds(2);
}
/// <summary>
/// The behaviour of the cargo order console
/// </summary>
[Serializable, NetSerializable]
public enum CargoOrderConsoleMode : byte
{
/// <summary>
/// Place orders directly
/// </summary>
DirectOrder,
/// <summary>
/// Print a slip to be inserted into a DirectOrder console
/// </summary>
PrintSlip,
/// <summary>
/// Transfers the order to the primary account
/// </summary>
SendToPrimary,
}
/// <summary>
/// Withdraw funds from an account
/// </summary>

View File

@@ -170,7 +170,7 @@ public sealed partial class ClimbSystem : VirtualController
private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User))
if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User) || !component.Vaultable)
return;
if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing || !climbingComponent.CanClimb)

View File

@@ -17,33 +17,46 @@ namespace Content.Shared.CombatMode.Pacification;
[Access(typeof(PacificationSystem))]
public sealed partial class PacifiedComponent : Component
{
/// <summary>
/// If true, this will prevent you from disarming opponents in combat.
/// </summary>
[DataField]
public bool DisallowDisarm = false;
/// <summary>
/// If true, this will disable combat entirely instead of only disallowing attacking living creatures and harmful things.
/// If true, this will disable combat entirely instead of only disallowing attacking living creatures and harmful things.
/// </summary>
[DataField]
public bool DisallowAllCombat = false;
/// <summary>
/// When attempting attack against the same entity multiple times,
/// don't spam popups every frame and instead have a cooldown.
/// When attempting attack against the same entity multiple times,
/// don't spam popups every frame and instead have a cooldown.
/// </summary>
[DataField]
public TimeSpan PopupCooldown = TimeSpan.FromSeconds(3.0);
/// <summary>
/// Time at which the next popup can be shown.
/// </summary>
[DataField]
[AutoPausedField]
public TimeSpan? NextPopupTime = null;
/// <summary>
/// The last entity attacked, used for popup purposes (avoid spam)
/// The last entity attacked, used for popup purposes (avoid spam)
/// </summary>
[DataField]
public EntityUid? LastAttackedEntity = null;
/// <summary>
/// The alert to show to owners of this component.
/// </summary>
[DataField]
public ProtoId<AlertPrototype> PacifiedAlert = "Pacified";
// Prevent cheat clients from using this to identify thieves and players that cannot fight back.
// This should not matter for prediction reasons since it only blocks user input.
public override bool SendOnlyToOwner => true;
}

View File

@@ -1,8 +1,7 @@
using Content.Shared.Body.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.EffectConditions;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Condition for if the entity is or isn't wearing internals.

View File

@@ -303,15 +303,7 @@ namespace Content.Shared.FixedPoint
public readonly int CompareTo(FixedPoint2 other)
{
if (other.Value > Value)
{
return -1;
}
if (other.Value < Value)
{
return 1;
}
return 0;
return Value.CompareTo(other.Value);
}
}

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Ghost.Roles.Raffles;
namespace Content.Shared.Ghost.Roles.Raffles;
/// <summary>
/// Defines settings for a ghost role raffle.

View File

@@ -1,5 +1,4 @@
using Content.Server.Ghost.Roles.Raffles;
using Robust.Shared.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Ghost.Roles.Raffles;

View File

@@ -1,4 +1,4 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
@@ -8,7 +8,7 @@ namespace Content.Shared.HealthExaminable;
public sealed partial class HealthExaminableComponent : Component
{
public List<FixedPoint2> Thresholds = new()
{ FixedPoint2.New(10), FixedPoint2.New(25), FixedPoint2.New(50), FixedPoint2.New(75) };
{ FixedPoint2.New(8), FixedPoint2.New(15), FixedPoint2.New(30), FixedPoint2.New(50), FixedPoint2.New(75), FixedPoint2.New(100), FixedPoint2.New(200) };
[DataField(required: true)]
public HashSet<ProtoId<DamageTypePrototype>> ExaminableTypes = default!;

View File

@@ -38,7 +38,13 @@ public abstract partial class SharedInstrumentComponent : Component
/// Component that indicates that musical instrument was activated (ui opened).
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ActiveInstrumentComponent : Component;
[AutoGenerateComponentState(true)]
public sealed partial class ActiveInstrumentComponent : Component
{
[DataField]
[AutoNetworkedField]
public MidiTrack?[] Tracks = [];
}
[Serializable, NetSerializable]
public sealed class InstrumentComponentState : ComponentState
@@ -144,3 +150,72 @@ public enum InstrumentUiKey
{
Key,
}
/// <summary>
/// Sets the MIDI channels on an instrument.
/// </summary>
[Serializable, NetSerializable]
public sealed class InstrumentSetChannelsEvent : EntityEventArgs
{
public NetEntity Uid { get; }
public MidiTrack?[] Tracks { get; set; }
public InstrumentSetChannelsEvent(NetEntity uid, MidiTrack?[] tracks)
{
Uid = uid;
Tracks = tracks;
}
}
/// <summary>
/// Represents a single midi track with the track name, instrument name and bank instrument name extracted.
/// </summary>
[Serializable, NetSerializable]
public sealed class MidiTrack
{
/// <summary>
/// The first specified Track Name
/// </summary>
public string? TrackName;
/// <summary>
/// The first specified instrument name
/// </summary>
public string? InstrumentName;
/// <summary>
/// The first program change resolved to the name.
/// </summary>
public string? ProgramName;
public override string ToString()
{
return $"Track Name: {TrackName}; Instrument Name: {InstrumentName}; Program Name: {ProgramName}";
}
/// <summary>
/// Truncates the fields based on the limit inputted into this method.
/// </summary>
public void TruncateFields(int limit)
{
if (InstrumentName != null)
InstrumentName = Truncate(InstrumentName, limit);
if (TrackName != null)
TrackName = Truncate(TrackName, limit);
if (ProgramName != null)
ProgramName = Truncate(ProgramName, limit);
}
private const string Postfix = "…";
// TODO: Make a general method to use in RT? idk if we have that.
private string Truncate(string input, int limit)
{
if (string.IsNullOrEmpty(input) || limit <= 0 || input.Length <= limit)
return input;
var truncatedLength = limit - Postfix.Length;
return input.Substring(0, truncatedLength) + Postfix;
}
}

View File

@@ -4,7 +4,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
namespace Content.Server.Light.Components;
namespace Content.Shared.Light.Components;
/// <summary>
/// Device that allows user to quikly change bulbs in <see cref="PoweredLightComponent"/>

View File

@@ -1,6 +1,7 @@
using System.Linq;
using System.Numerics;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Light.Components;
@@ -25,11 +26,32 @@ public sealed partial class SunShadowCycleComponent : Component
/// Time to have each direction applied. Will lerp from the current value to the next one.
/// </summary>
[DataField, AutoNetworkedField]
public List<(float Ratio, Vector2 Direction, float Alpha)> Directions = new()
public List<SunShadowCycleDirection> Directions = new()
{
(0f, new Vector2(0f, 3f), 0f),
(0.25f, new Vector2(-3f, -0.1f), 0.5f),
(0.5f, new Vector2(0f, -3f), 0.8f),
(0.75f, new Vector2(3f, -0.1f), 0.5f),
new SunShadowCycleDirection(0f, new Vector2(0f, 3f), 0f),
new SunShadowCycleDirection(0.25f, new Vector2(-3f, -0.1f), 0.5f),
new SunShadowCycleDirection(0.5f, new Vector2(0f, -3f), 0.8f),
new SunShadowCycleDirection(0.75f, new Vector2(3f, -0.1f), 0.5f),
};
}
};
[DataDefinition]
[Serializable, NetSerializable]
public partial record struct SunShadowCycleDirection
{
[DataField]
public float Ratio;
[DataField]
public Vector2 Direction;
[DataField]
public float Alpha;
public SunShadowCycleDirection(float ratio, Vector2 direction, float alpha)
{
Ratio = ratio;
Direction = direction;
Alpha = alpha;
}
};

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Medical.Components;
namespace Content.Shared.Medical.Cryogenics;
/// <summary>
/// Tracking component for an enabled cryo pod (which periodically tries to inject chemicals in the occupant, if one exists)

View File

@@ -1,4 +1,3 @@
using Content.Server.Medical.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database;

View File

@@ -73,7 +73,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary>
/// Returns whether an entity is a member of a faction.
/// </summary>
public bool IsMember(Entity<NpcFactionMemberComponent?> ent, string faction)
public bool IsMember(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] string faction)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
@@ -85,7 +85,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// Returns whether an entity is a member of any listed faction.
/// If the list is empty this returns false.
/// </summary>
public bool IsMemberOfAny(Entity<NpcFactionMemberComponent?> ent, IEnumerable<ProtoId<NpcFactionPrototype>> factions)
public bool IsMemberOfAny(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] IEnumerable<ProtoId<NpcFactionPrototype>> factions)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
@@ -102,7 +102,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary>
/// Adds this entity to the particular faction.
/// </summary>
public void AddFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true)
public void AddFaction(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] string faction, bool dirty = true)
{
if (!_proto.HasIndex<NpcFactionPrototype>(faction))
{
@@ -121,7 +121,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary>
/// Adds this entity to the particular faction.
/// </summary>
public void AddFactions(Entity<NpcFactionMemberComponent?> ent, HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true)
public void AddFactions(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true)
{
ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent);
@@ -143,7 +143,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary>
/// Removes this entity from the particular faction.
/// </summary>
public void RemoveFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true)
public void RemoveFaction(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] string faction, bool dirty = true)
{
if (!_proto.HasIndex<NpcFactionPrototype>(faction))
{
@@ -202,7 +202,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
return GetNearbyFactions(ent, range, ent.Comp.FriendlyFactions);
}
private IEnumerable<EntityUid> GetNearbyFactions(EntityUid entity, float range, HashSet<ProtoId<NpcFactionPrototype>> factions)
private IEnumerable<EntityUid> GetNearbyFactions(EntityUid entity, float range, [ForbidLiteral] HashSet<ProtoId<NpcFactionPrototype>> factions)
{
var xform = Transform(entity);
foreach (var ent in _lookup.GetEntitiesInRange<NpcFactionMemberComponent>(_xform.GetMapCoordinates((entity, xform)), range))
@@ -228,12 +228,12 @@ public sealed partial class NpcFactionSystem : EntitySystem
return ent.Comp.Factions.Overlaps(other.Comp.Factions) || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions);
}
public bool IsFactionFriendly(string target, string with)
public bool IsFactionFriendly([ForbidLiteral] string target, [ForbidLiteral] string with)
{
return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target);
}
public bool IsFactionFriendly(string target, Entity<NpcFactionMemberComponent?> with)
public bool IsFactionFriendly([ForbidLiteral] string target, Entity<NpcFactionMemberComponent?> with)
{
if (!Resolve(with, ref with.Comp, false))
return false;
@@ -242,12 +242,12 @@ public sealed partial class NpcFactionSystem : EntitySystem
with.Comp.FriendlyFactions.Contains(target);
}
public bool IsFactionHostile(string target, string with)
public bool IsFactionHostile([ForbidLiteral] string target, [ForbidLiteral] string with)
{
return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target);
}
public bool IsFactionHostile(string target, Entity<NpcFactionMemberComponent?> with)
public bool IsFactionHostile([ForbidLiteral] string target, Entity<NpcFactionMemberComponent?> with)
{
if (!Resolve(with, ref with.Comp, false))
return false;
@@ -256,7 +256,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
with.Comp.HostileFactions.Contains(target);
}
public bool IsFactionNeutral(string target, string with)
public bool IsFactionNeutral([ForbidLiteral] string target, [ForbidLiteral] string with)
{
return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with);
}
@@ -264,7 +264,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary>
/// Makes the source faction friendly to the target faction, 1-way.
/// </summary>
public void MakeFriendly(string source, string target)
public void MakeFriendly([ForbidLiteral] string source, [ForbidLiteral] string target)
{
if (!_factions.TryGetValue(source, out var sourceFaction))
{
@@ -286,7 +286,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary>
/// Makes the source faction hostile to the target faction, 1-way.
/// </summary>
public void MakeHostile(string source, string target)
public void MakeHostile([ForbidLiteral] string source, [ForbidLiteral] string target)
{
if (!_factions.TryGetValue(source, out var sourceFaction))
{

View File

@@ -1,22 +1,24 @@
namespace Content.Server.Nutrition.Components;
using Robust.Shared.GameStates;
[RegisterComponent]
namespace Content.Shared.Nutrition.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class FlavorProfileComponent : Component
{
/// <summary>
/// Localized string containing the base flavor of this entity.
/// </summary>
[DataField("flavors")]
[DataField]
public HashSet<string> Flavors { get; private set; } = new();
/// <summary>
/// Reagent IDs to ignore when processing this flavor profile. Defaults to nutriment.
/// </summary>
[DataField("ignoreReagents")]
[DataField]
public HashSet<string> IgnoreReagents { get; private set; } = new()
{
"Nutriment",
"Vitamin",
"Protein"
"Protein",
};
}

View File

@@ -1,11 +1,10 @@
using Content.Server.Body.Components;
using Content.Shared.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.Nutrition.Components;
namespace Content.Shared.Nutrition.Components;
[RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))]
public sealed partial class FoodComponent : Component

View File

@@ -1,6 +1,6 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Nutrition.Components;
namespace Content.Shared.Nutrition.Components;
/// <summary>
/// Component that denotes a piece of clothing that blocks the mouth or otherwise prevents eating & drinking.
@@ -9,7 +9,7 @@ namespace Content.Server.Nutrition.Components;
/// In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of
/// masks), then this component might become redundant.
/// </remarks>
[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem), typeof(IngestionBlockerSystem))]
[RegisterComponent, Access(typeof(FoodSystem), typeof(SharedDrinkSystem), typeof(IngestionBlockerSystem))]
public sealed partial class IngestionBlockerComponent : Component
{
/// <summary>

View File

@@ -4,7 +4,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Nutrition.Components
{
[RegisterComponent, NetworkedComponent, Access(typeof(SharedUtensilSystem))]
[RegisterComponent, NetworkedComponent, Access(typeof(UtensilSystem))]
public sealed partial class UtensilComponent : Component
{
[DataField("types")]

View File

@@ -1,12 +1,11 @@
using Content.Server.Nutrition.Components;
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
/// <summary>
/// Deals with flavor profiles when you eat something.

View File

@@ -1,21 +1,17 @@
using System.Numerics;
using System.Text;
using Content.Server.Nutrition.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Prototypes;
using Content.Shared.Popups;
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
{
@@ -26,7 +22,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
@@ -126,7 +122,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
if (start.Comp.FoodLayers.Count >= start.Comp.MaxLayers && !elementIndexed.Final || start.Comp.Finished)
{
if (user is not null)
_popup.PopupEntity(Loc.GetString("food-sequence-no-space"), start, user.Value);
_popup.PopupClient(Loc.GetString("food-sequence-no-space"), start, user.Value);
return false;
}

View File

@@ -1,16 +1,13 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Inventory;
using Content.Server.Nutrition.Components;
using Content.Shared.Nutrition.Components;
using Content.Server.Popups;
using Content.Server.Stack;
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Organ;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.Components;
@@ -21,42 +18,38 @@ using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.Containers.ItemSlots;
using Robust.Server.GameObjects;
using Content.Shared.Whitelist;
using Content.Shared.Destructible;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
/// <summary>
/// Handles feeding attempts both on yourself and on the target.
/// </summary>
public sealed class FoodSystem : EntitySystem
{
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ReactiveSystem _reaction = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly StomachSystem _stomach = default!;
[Dependency] private readonly UtensilSystem _utensil = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
@@ -69,7 +62,7 @@ public sealed class FoodSystem : EntitySystem
// TODO add InteractNoHandEvent for entities like mice.
// run after openable for wrapped/peelable foods
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(ServerInventorySystem) });
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
SubscribeLocalEvent<FoodComponent, GetVerbsEvent<AlternativeVerb>>(AddEatVerb);
SubscribeLocalEvent<FoodComponent, ConsumeDoAfterEvent>(OnDoAfter);
@@ -116,7 +109,7 @@ public sealed class FoodSystem : EntitySystem
if (HasComp<UnremoveableComponent>(food))
return (false, false);
if (_openable.IsClosed(food, user))
if (_openable.IsClosed(food, user, predicted: true))
return (false, true);
if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out _, out var foodSolution))
@@ -135,7 +128,7 @@ public sealed class FoodSystem : EntitySystem
// Check for used storage on the food item
if (TryComp<StorageComponent>(food, out var storageState) && storageState.Container.ContainedEntities.Any())
{
_popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
return (false, true);
}
@@ -144,7 +137,7 @@ public sealed class FoodSystem : EntitySystem
{
if (itemSlots.Slots.Any(slot => slot.Value.HasItem))
{
_popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
return (false, true);
}
}
@@ -153,7 +146,7 @@ public sealed class FoodSystem : EntitySystem
if (GetUsesRemaining(food, foodComp) <= 0)
{
_popup.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
_popup.PopupClient(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
DeleteAndSpawnTrash(foodComp, food, user);
return (false, true);
}
@@ -171,7 +164,7 @@ public sealed class FoodSystem : EntitySystem
if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
{
var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
_popup.PopupEntity(message, user, user);
_popup.PopupClient(message, user, user);
return (false, true);
}
@@ -268,7 +261,7 @@ public sealed class FoodSystem : EntitySystem
if (stomachToUse == null)
{
_solutionContainer.TryAddSolution(soln.Value, split);
_popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
_popup.PopupClient(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
return;
}
@@ -283,20 +276,20 @@ public sealed class FoodSystem : EntitySystem
var userName = Identity.Entity(args.User, EntityManager);
_popup.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), entity.Owner, entity.Owner);
_popup.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
_popup.PopupClient(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
// log successful force feed
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity.Owner):food}");
}
else
{
_popup.PopupEntity(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
_popup.PopupClient(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
// log successful voluntary eating
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
}
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
_audio.PlayPredicted(entity.Comp.UseSound, args.Target.Value, args.User, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
// Try to break all used utensils
foreach (var utensil in utensils)
@@ -484,7 +477,7 @@ public sealed class FoodSystem : EntitySystem
// If "required" field is set, try to block eating without proper utensils used
if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil)
{
_popup.PopupEntity(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
_popup.PopupClient(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
return false;
}
@@ -533,7 +526,7 @@ public sealed class FoodSystem : EntitySystem
RaiseLocalEvent(uid, attempt, false);
if (attempt.Cancelled && attempt.Blocker != null && popupUid != null)
{
_popup.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
_popup.PopupClient(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
uid, popupUid.Value);
}

View File

@@ -1,7 +1,7 @@
using Content.Server.Nutrition.Components;
using Content.Shared.Clothing;
using Content.Shared.Clothing;
using Content.Shared.Nutrition.Components;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
public sealed class IngestionBlockerSystem : EntitySystem
{

View File

@@ -1,8 +1,8 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Examine;
using Content.Shared.Lock;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Lock;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
@@ -166,7 +166,7 @@ public sealed partial class OpenableSystem : EntitySystem
/// Drinks that don't have OpenableComponent are automatically open, so it returns false.
/// If user is not null a popup will be shown to them.
/// </summary>
public bool IsClosed(EntityUid uid, EntityUid? user = null, OpenableComponent? comp = null)
public bool IsClosed(EntityUid uid, EntityUid? user = null, OpenableComponent? comp = null, bool predicted = false)
{
if (!Resolve(uid, ref comp, false))
return false;
@@ -175,7 +175,12 @@ public sealed partial class OpenableSystem : EntitySystem
return false;
if (user != null)
_popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
{
if (predicted)
_popup.PopupClient(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
else
_popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
}
return true;
}

View File

@@ -1,15 +1,36 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared.Nutrition.EntitySystems;
public abstract partial class SharedDrinkSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
[Dependency] private readonly FoodSystem _food = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
public override void Initialize()
{
@@ -17,6 +38,7 @@ public abstract partial class SharedDrinkSystem : EntitySystem
SubscribeLocalEvent<DrinkComponent, AttemptShakeEvent>(OnAttemptShake);
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
}
protected void OnAttemptShake(Entity<DrinkComponent> entity, ref AttemptShakeEvent args)
@@ -28,7 +50,7 @@ public abstract partial class SharedDrinkSystem : EntitySystem
protected void OnExamined(Entity<DrinkComponent> entity, ref ExaminedEvent args)
{
TryComp<OpenableComponent>(entity, out var openable);
if (_openable.IsClosed(entity.Owner, null, openable) || !args.IsInDetailsRange || !entity.Comp.Examinable)
if (_openable.IsClosed(entity.Owner, null, openable, true) || !args.IsInDetailsRange || !entity.Comp.Examinable)
return;
var empty = IsEmpty(entity, entity.Comp);
@@ -57,6 +79,38 @@ public abstract partial class SharedDrinkSystem : EntitySystem
}
}
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
{
if (entity.Owner == ev.User ||
!ev.CanInteract ||
!ev.CanAccess ||
!TryComp<BodyComponent>(ev.User, out var body) ||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
return;
// Make sure the solution exists
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
return;
// no drinking from living drinks, have to kill them first.
if (_mobState.IsAlive(entity))
return;
var user = ev.User;
AlternativeVerb verb = new()
{
Act = () =>
{
TryDrink(user, user, entity.Comp, entity);
},
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
Text = Loc.GetString("drink-system-verb-drink"),
Priority = 2
};
ev.Verbs.Add(verb);
}
protected FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -87,4 +141,74 @@ public abstract partial class SharedDrinkSystem : EntitySystem
return remainingString;
}
/// <summary>
/// Tries to feed the drink item to the target entity
/// </summary>
protected bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
{
if (!HasComp<BodyComponent>(target))
return false;
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
return false;
if (_openable.IsClosed(item, user, predicted: true))
return true;
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
{
if (drink.IgnoreEmpty)
return false;
_popup.PopupClient(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
return true;
}
if (_food.IsMouthBlocked(target, user))
return true;
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
return true;
var forceDrink = user != target;
if (forceDrink)
{
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
// logging
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
else
{
// log voluntary drinking
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
var doAfterEventArgs = new DoAfterArgs(EntityManager,
user,
forceDrink ? drink.ForceFeedDelay : drink.Delay,
new ConsumeDoAfterEvent(drink.Solution, flavors),
eventTarget: item,
target: target,
used: item)
{
BreakOnHandChange = false,
BreakOnMove = forceDrink,
BreakOnDamage = true,
MovementThreshold = 0.01f,
DistanceThreshold = 1.0f,
// do-after will stop if item is dropped when trying to feed someone else
// or if the item started out in the user's own hands
NeedHand = forceDrink || _hands.IsHolding(user, item),
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
return true;
}
}

View File

@@ -1,5 +0,0 @@
namespace Content.Shared.Nutrition.EntitySystems;
public abstract class SharedUtensilSystem : EntitySystem
{
}

View File

@@ -0,0 +1,73 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Tools.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Shared.Nutrition.EntitySystems;
public sealed class UtensilSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly FoodSystem _foodSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
}
/// <summary>
/// Clicked with utensil
/// </summary>
private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
{
if (ev.Handled || ev.Target == null || !ev.CanReach)
return;
var result = TryUseUtensil(ev.User, ev.Target.Value, entity);
ev.Handled = result.Handled;
}
public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
{
if (!EntityManager.TryGetComponent(target, out FoodComponent? food))
return (false, false);
//Prevents food usage with a wrong utensil
if ((food.Utensil & utensil.Comp.Types) == 0)
{
_popupSystem.PopupEntity(Loc.GetString("food-system-wrong-utensil", ("food", target), ("utensil", utensil.Owner)), user, user);
return (false, true);
}
if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
return (false, true);
return _foodSystem.TryFeed(user, user, target, food);
}
/// <summary>
/// Attempt to break the utensil after interaction.
/// </summary>
/// <param name="uid">Utensil.</param>
/// <param name="userUid">User of the utensil.</param>
public void TryBreak(EntityUid uid, EntityUid userUid, UtensilComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (_robustRandom.Prob(component.BreakChance))
{
_audio.PlayPredicted(component.BreakSound, userUid, userUid, AudioParams.Default.WithVolume(-2f));
EntityManager.DeleteEntity(uid);
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Lathe;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Research.Components;
@@ -20,6 +21,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedResearchSystem _research = default!;
[Dependency] private readonly SharedLatheSystem _lathe = default!;
[Dependency] private readonly NameModifierSystem _nameModifier = default!;
public override void Initialize()
{
@@ -28,6 +30,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<TechnologyDiskComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
}
private void OnMapInit(Entity<TechnologyDiskComponent> ent, ref MapInitEvent args)
@@ -55,6 +58,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
ent.Comp.Recipes = [];
ent.Comp.Recipes.Add(_random.Pick(techs));
Dirty(ent);
_nameModifier.RefreshNameModifiers(ent.Owner);
}
private void OnAfterInteract(Entity<TechnologyDiskComponent> ent, ref AfterInteractEvent args)
@@ -90,4 +94,16 @@ public sealed class TechnologyDiskSystem : EntitySystem
}
args.PushMarkup(message);
}
private void OnRefreshNameModifiers(Entity<TechnologyDiskComponent> entity, ref RefreshNameModifiersEvent args)
{
if (entity.Comp.Recipes != null)
{
foreach (var recipe in entity.Comp.Recipes)
{
var proto = _protoMan.Index(recipe);
args.AddModifier("tech-disk-name-format", extraArgs: ("technology", _lathe.GetRecipeName(proto)));
}
}
}
}

View File

@@ -20,6 +20,12 @@ public sealed partial class BinComponent : Component
[ViewVariables]
public Container ItemContainer = default!;
/// <summary>
/// ID of the container used to hold the items in the bin.
/// </summary>
[DataField]
public string ContainerId = "bin-container";
/// <summary>
/// A list representing the order in which
/// all the entities are stored in the bin.

View File

@@ -1,6 +1,6 @@
using Content.Shared.Inventory;
namespace Content.Server.Storage.Components;
namespace Content.Shared.Storage.Components;
/// <summary>
/// Applies an ongoing pickup area around the attached entity.

View File

@@ -24,13 +24,12 @@ public sealed class BinSystem : EntitySystem
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
public const string BinContainerId = "bin-container";
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<BinComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<BinComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BinComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<BinComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<BinComponent, InteractHandEvent>(OnInteractHand, before: new[] { typeof(SharedItemSystem) });
SubscribeLocalEvent<BinComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
@@ -45,7 +44,7 @@ public sealed class BinSystem : EntitySystem
private void OnStartup(EntityUid uid, BinComponent component, ComponentStartup args)
{
component.ItemContainer = _container.EnsureContainer<Container>(uid, BinContainerId);
component.ItemContainer = _container.EnsureContainer<Container>(uid, component.ContainerId);
}
private void OnMapInit(EntityUid uid, BinComponent component, MapInitEvent args)
@@ -66,6 +65,11 @@ public sealed class BinSystem : EntitySystem
}
}
private void OnEntInserted(Entity<BinComponent> ent, ref EntInsertedIntoContainerMessage args)
{
ent.Comp.Items.Add(args.Entity);
}
private void OnEntRemoved(EntityUid uid, BinComponent component, EntRemovedFromContainerMessage args)
{
component.Items.Remove(args.Entity);
@@ -96,7 +100,7 @@ public sealed class BinSystem : EntitySystem
if (args.Using != null)
{
var canReach = args.CanAccess && args.CanInteract;
InsertIntoBin(args.User, args.Target, (EntityUid) args.Using, component, false, canReach);
InsertIntoBin(args.User, args.Target, (EntityUid)args.Using, component, false, canReach);
}
}
@@ -136,7 +140,6 @@ public sealed class BinSystem : EntitySystem
return false;
_container.Insert(toInsert, component.ItemContainer);
component.Items.Add(toInsert);
Dirty(uid, component);
return true;
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Storage.Components;
using Content.Shared.Inventory;
using Content.Shared.Storage.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;

View File

@@ -115,7 +115,7 @@ public abstract partial class SharedToolSystem : EntitySystem
EntityUid user,
EntityUid? target,
float doAfterDelay,
IEnumerable<string> toolQualitiesNeeded,
[ForbidLiteral] IEnumerable<string> toolQualitiesNeeded,
DoAfterEvent doAfterEv,
float fuel = 0,
ToolComponent? toolComponent = null)
@@ -153,7 +153,7 @@ public abstract partial class SharedToolSystem : EntitySystem
EntityUid user,
EntityUid? target,
TimeSpan delay,
IEnumerable<string> toolQualitiesNeeded,
[ForbidLiteral] IEnumerable<string> toolQualitiesNeeded,
DoAfterEvent doAfterEv,
out DoAfterId? id,
float fuel = 0,
@@ -200,7 +200,7 @@ public abstract partial class SharedToolSystem : EntitySystem
EntityUid user,
EntityUid? target,
float doAfterDelay,
string toolQualityNeeded,
[ForbidLiteral] string toolQualityNeeded,
DoAfterEvent doAfterEv,
float fuel = 0,
ToolComponent? toolComponent = null)
@@ -219,7 +219,7 @@ public abstract partial class SharedToolSystem : EntitySystem
/// <summary>
/// Whether a tool entity has the specified quality or not.
/// </summary>
public bool HasQuality(EntityUid uid, string quality, ToolComponent? tool = null)
public bool HasQuality(EntityUid uid, [ForbidLiteral] string quality, ToolComponent? tool = null)
{
return Resolve(uid, ref tool, false) && tool.Qualities.Contains(quality);
}
@@ -228,7 +228,7 @@ public abstract partial class SharedToolSystem : EntitySystem
/// Whether a tool entity has all specified qualities or not.
/// </summary>
[PublicAPI]
public bool HasAllQualities(EntityUid uid, IEnumerable<string> qualities, ToolComponent? tool = null)
public bool HasAllQualities(EntityUid uid, [ForbidLiteral] IEnumerable<string> qualities, ToolComponent? tool = null)
{
return Resolve(uid, ref tool, false) && tool.Qualities.ContainsAll(qualities);
}

View File

@@ -0,0 +1,36 @@
using Content.Shared.FixedPoint;
using Robust.Shared.Console;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Toolshed.TypeParsers;
using Robust.Shared.Utility;
namespace Content.Shared.Toolshed.TypeParsers;
public sealed class FixedPoint2TypeParser : TypeParser<FixedPoint2>
{
public override bool TryParse(ParserContext ctx, out FixedPoint2 result)
{
if (Toolshed.TryParse(ctx, out int? value))
{
result = FixedPoint2.New(value.Value);
return true;
}
if (Toolshed.TryParse(ctx, out float? fValue))
{
result = FixedPoint2.New(fValue.Value);
return true;
}
// Toolshed's number parser (NumberBaseTypeParser) should have assigned ctx.Error so we don't have to.
DebugTools.AssertNotNull(ctx.Error);
result = FixedPoint2.Zero;
return false;
}
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
{
return CompletionResult.FromHint(GetArgHint(arg));
}
}

View File

@@ -13,7 +13,6 @@ namespace Content.Shared.Weather;
public abstract class SharedWeatherSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;

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