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

View File

@@ -14,5 +14,15 @@ namespace Content.Client.Cargo.UI
{ {
RobustXamlLoader.Load(this); 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 public abstract class SpeechBubble : Control
{ {
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] protected readonly IConfigurationManager ConfigManager = default!; [Dependency] protected readonly IConfigurationManager ConfigManager = default!;
@@ -30,12 +31,12 @@ namespace Content.Client.Chat.UI
/// <summary> /// <summary>
/// The total time a speech bubble stays on screen. /// The total time a speech bubble stays on screen.
/// </summary> /// </summary>
private const float TotalTime = 4; private static readonly TimeSpan TotalTime = TimeSpan.FromSeconds(4);
/// <summary> /// <summary>
/// The amount of time at the end of the bubble's life at which it starts fading. /// The amount of time at the end of the bubble's life at which it starts fading.
/// </summary> /// </summary>
private const float FadeTime = 0.25f; private static readonly TimeSpan FadeTime = TimeSpan.FromSeconds(0.25f);
/// <summary> /// <summary>
/// The distance in world space to offset the speech bubble from the center of the entity. /// 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 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; } public float VerticalOffset { get; set; }
private float _verticalOffsetAchieved; private float _verticalOffsetAchieved;
@@ -99,6 +103,7 @@ namespace Content.Client.Chat.UI
bubble.Measure(Vector2Helpers.Infinity); bubble.Measure(Vector2Helpers.Infinity);
ContentSize = bubble.DesiredSize; ContentSize = bubble.DesiredSize;
_verticalOffsetAchieved = -ContentSize.Y; _verticalOffsetAchieved = -ContentSize.Y;
_deathTime = _timing.RealTime + TotalTime;
} }
protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null);
@@ -107,8 +112,8 @@ namespace Content.Client.Chat.UI
{ {
base.FrameUpdate(args); base.FrameUpdate(args);
_timeLeft -= args.DeltaSeconds; var timeLeft = (float)(_deathTime - _timing.RealTime).TotalSeconds;
if (_entityManager.Deleted(_senderEntity) || _timeLeft <= 0) if (_entityManager.Deleted(_senderEntity) || timeLeft <= 0)
{ {
// Timer spawn to prevent concurrent modification exception. // Timer spawn to prevent concurrent modification exception.
Timer.Spawn(0, Die); Timer.Spawn(0, Die);
@@ -131,10 +136,10 @@ namespace Content.Client.Chat.UI
return; return;
} }
if (_timeLeft <= FadeTime) if (timeLeft <= FadeTime.TotalSeconds)
{ {
// Update alpha if we're fading. // Update alpha if we're fading.
Modulate = Color.White.WithAlpha(_timeLeft / FadeTime); Modulate = Color.White.WithAlpha(timeLeft / (float)FadeTime.TotalSeconds);
} }
else else
{ {
@@ -144,7 +149,7 @@ namespace Content.Client.Chat.UI
var baseOffset = 0f; var baseOffset = 0f;
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech)) if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
baseOffset = speech.SpeechBubbleOffset; baseOffset = speech.SpeechBubbleOffset;
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset); var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
@@ -175,9 +180,9 @@ namespace Content.Client.Chat.UI
/// </summary> /// </summary>
public void FadeNow() 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 ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected; private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = []; private List<ConstructionPrototype> _favoritedRecipes = [];
private Dictionary<string, ContainerButton> _recipeButtons = new(); private readonly Dictionary<string, ContainerButton> _recipeButtons = new();
private string _selectedCategory = string.Empty; private string _selectedCategory = string.Empty;
private const string FavoriteCatName = "construction-category-favorites"; private const string FavoriteCatName = "construction-category-favorites";
@@ -217,8 +217,8 @@ namespace Content.Client.Construction.UI
var itemButton = new ContainerButton() var itemButton = new ContainerButton()
{ {
VerticalAlignment = Control.VAlignment.Center, VerticalAlignment = Control.VAlignment.Center,
Name = recipe.TargetPrototype.Name, Name = recipe.Prototype.Name,
ToolTip = recipe.TargetPrototype.Name, ToolTip = recipe.Prototype.Name,
ToggleMode = true, ToggleMode = true,
Children = { protoView }, Children = { protoView },
}; };
@@ -235,7 +235,7 @@ namespace Content.Client.Construction.UI
if (buttonToggledEventArgs.Pressed && if (buttonToggledEventArgs.Pressed &&
_selected != null && _selected != null &&
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton)) _recipeButtons.TryGetValue(_selected.ID, out var oldButton))
{ {
oldButton.Pressed = false; oldButton.Pressed = false;
SelectGridButton(oldButton, false); SelectGridButton(oldButton, false);
@@ -245,7 +245,7 @@ namespace Content.Client.Construction.UI
}; };
recipesGrid.AddChild(itemButtonPanelContainer); recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Prototype.Name!] = itemButton; _recipeButtons[recipe.Prototype.ID] = itemButton;
var isCurrentButtonSelected = _selected == recipe.Prototype; var isCurrentButtonSelected = _selected == recipe.Prototype;
itemButton.Pressed = isCurrentButtonSelected; itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected); SelectGridButton(itemButton, isCurrentButtonSelected);
@@ -307,7 +307,7 @@ namespace Content.Client.Construction.UI
if (button.Parent is not PanelContainer buttonPanel) if (button.Parent is not PanelContainer buttonPanel)
return; return;
button.Modulate = select ? Color.Green : Color.Transparent; button.Children.Single().Modulate = select ? Color.Green : Color.White;
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent; var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor }; 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 System.Linq;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Instruments; using Content.Shared.Instruments;
@@ -12,7 +13,7 @@ using Robust.Shared.Timing;
namespace Content.Client.Instruments; namespace Content.Client.Instruments;
public sealed class InstrumentSystem : SharedInstrumentSystem public sealed partial class InstrumentSystem : SharedInstrumentSystem
{ {
[Dependency] private readonly IClientNetManager _netManager = default!; [Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IMidiManager _midiManager = default!; [Dependency] private readonly IMidiManager _midiManager = default!;
@@ -23,6 +24,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
public int MaxMidiEventsPerBatch { get; private set; } public int MaxMidiEventsPerBatch { get; private set; }
public int MaxMidiEventsPerSecond { get; private set; } public int MaxMidiEventsPerSecond { get; private set; }
public event Action? OnChannelsUpdated;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -38,6 +41,26 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown); SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState); 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) 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) 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)) if (!Resolve(uid, ref instrument))
return false; return false;
@@ -263,6 +292,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
return false; return false;
SetMaster(uid, null); SetMaster(uid, null);
TrySetChannels(uid, data);
instrument.MidiEventBuffer.Clear(); instrument.MidiEventBuffer.Clear();
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add; instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
return true; 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="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"/> <Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
</BoxContainer> </BoxContainer>
<CheckButton Name="DisplayTrackNames"
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
</BoxContainer> </BoxContainer>
</DefaultWindow> </DefaultWindow>

View File

@@ -1,26 +1,56 @@
using Content.Shared.Instruments;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Audio.Midi; using Robust.Shared.Audio.Midi;
using Robust.Shared.Timing; using Robust.Shared.Utility;
namespace Content.Client.Instruments.UI; namespace Content.Client.Instruments.UI;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class ChannelsMenu : DefaultWindow public sealed partial class ChannelsMenu : DefaultWindow
{ {
[Dependency] private readonly IEntityManager _entityManager = null!;
private readonly InstrumentBoundUserInterface _owner; private readonly InstrumentBoundUserInterface _owner;
public ChannelsMenu(InstrumentBoundUserInterface owner) : base() public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_owner = owner; _owner = owner;
ChannelList.OnItemSelected += OnItemSelected; ChannelList.OnItemSelected += OnItemSelected;
ChannelList.OnItemDeselected += OnItemDeselected; ChannelList.OnItemDeselected += OnItemDeselected;
AllButton.OnPressed += OnAllPressed; AllButton.OnPressed += OnAllPressed;
ClearButton.OnPressed += OnClearPressed; 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) 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(); ChannelList.Clear();
var instrument = _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
var activeInstrument = ResolveActiveInstrument(instrument);
for (int i = 0; i < RobustMidiEvent.MaxChannels; i++) for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
{ {
var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name", var label = _owner.Loc.GetString("instrument-component-channel-name",
("number", i)), null, true, i); ("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; item.Selected = !instrument?.FilteredChannels[i] ?? false;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,12 +14,17 @@ namespace Content.Client.UserInterface.Systems.Chat;
/// </summary> /// </summary>
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem> public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
{ {
[Dependency] private readonly ILocalizationManager _loc = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = 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> /// <summary>
/// The list of words to be highlighted in the chatbox. /// The list of words to be highlighted in the chatbox.
/// </summary> /// </summary>
private List<string> _highlights = new(); private readonly List<string> _highlights = new();
/// <summary> /// <summary>
/// The string holding the hex color used to highlight words. /// 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); _config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }, true);
// Load highlights if any were saved. // Load highlights if any were saved.
string highlights = _config.GetCVar(CCVars.ChatHighlights); var highlights = _config.GetCVar(CCVars.ChatHighlights);
if (!string.IsNullOrEmpty(highlights)) 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 // We first subdivide the highlights based on newlines to prevent replacing
// a valid "\n" tag and adding it to the final regex. // 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... // 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. // Escape the keyword to prevent special characters like "(" and ")" to be considered valid regex.
keyword = Regex.Escape(keyword); 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. // 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 // 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. // 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. // 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 // When matching for the first double quote character we also consider the possibility
// of the double quote being preceded by a @ character. // 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), // 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. // 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); _highlights.Add(keyword);
} }
@@ -133,7 +138,7 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
var (_, job, _, _, entityName) = data; var (_, job, _, _, entityName) = data;
// Mark this entity's name as our character name for the "UpdateHighlights" function. // 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. // Subdivide the character's name based on spaces or hyphens so that every word gets highlighted.
if (newHighlights.Count(c => (c == ' ' || c == '-')) == 1) 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]; 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. // 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"); newHighlights += '\n' + jobMatches.Replace(", ", "\n");
UpdateHighlights(newHighlights); UpdateHighlights(newHighlights);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems; using Content.Shared.NPC.Systems;
using Content.Shared.NukeOps; using Content.Shared.NukeOps;
using Content.Shared.Pinpointer; using Content.Shared.Pinpointer;
@@ -23,12 +24,16 @@ using Content.Shared.Station.Components;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.GameRules; namespace Content.IntegrationTests.Tests.GameRules;
[TestFixture] [TestFixture]
public sealed class NukeOpsTest public sealed class NukeOpsTest
{ {
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
/// <summary> /// <summary>
/// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded. /// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded.
/// </summary> /// </summary>
@@ -119,8 +124,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(player)); Assert.That(entMan.HasComponent<NukeOperativeComponent>(player));
Assert.That(roleSys.MindIsAntagonist(mind)); Assert.That(roleSys.MindIsAntagonist(mind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind)); Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True); Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False); Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
var roles = roleSys.MindGetAllRoleInfo(mind); var roles = roleSys.MindGetAllRoleInfo(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander"); var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
Assert.That(cmdRoles.Count(), Is.EqualTo(1)); Assert.That(cmdRoles.Count(), Is.EqualTo(1));
@@ -130,8 +135,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(dummyEnts[1])); Assert.That(entMan.HasComponent<NukeOperativeComponent>(dummyEnts[1]));
Assert.That(roleSys.MindIsAntagonist(dummyMind)); Assert.That(roleSys.MindIsAntagonist(dummyMind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind)); Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True); Assert.That(factionSys.IsMember(dummyEnts[1], SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False); Assert.That(factionSys.IsMember(dummyEnts[1], NanotrasenFaction), Is.False);
roles = roleSys.MindGetAllRoleInfo(dummyMind); roles = roleSys.MindGetAllRoleInfo(dummyMind);
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic"); cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
Assert.That(cmdRoles.Count(), Is.EqualTo(1)); 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(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False); Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False); Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False); Assert.That(factionSys.IsMember(ent, SyndicateFaction), Is.False);
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True); Assert.That(factionSys.IsMember(ent, NanotrasenFaction), Is.True);
var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" }; var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False); 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;
using Content.Shared.GameTicking.Components; using Content.Shared.GameTicking.Components;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems; using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -20,6 +21,8 @@ public sealed class TraitorRuleTest
{ {
private const string TraitorGameRuleProtoId = "Traitor"; private const string TraitorGameRuleProtoId = "Traitor";
private const string TraitorAntagRoleName = "Traitor"; private const string TraitorAntagRoleName = "Traitor";
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
[Test] [Test]
public async Task TestTraitorObjectives() public async Task TestTraitorObjectives()
@@ -108,8 +111,8 @@ public sealed class TraitorRuleTest
// Make sure the player is a traitor. // Make sure the player is a traitor.
var mind = mindSys.GetMind(player)!.Value; var mind = mindSys.GetMind(player)!.Value;
Assert.That(roleSys.MindIsAntagonist(mind)); Assert.That(roleSys.MindIsAntagonist(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True); Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False); Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
Assert.That(traitorRule.TotalTraitors, Is.EqualTo(1)); Assert.That(traitorRule.TotalTraitors, Is.EqualTo(1));
Assert.That(traitorRule.TraitorMinds[0], Is.EqualTo(mind)); 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/centcomm.yml",
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional. "/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/reach.yml", // Contains handheld crew monitor
"/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator" "/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
"/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp "/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp
@@ -62,27 +60,20 @@ namespace Content.IntegrationTests.Tests
"Dev", "Dev",
"TestTeg", "TestTeg",
"Fland", "Fland",
"Meta",
"Packed", "Packed",
"Omega",
"Bagel", "Bagel",
"CentComm", "CentComm",
"Box", "Box",
"Core",
"Marathon", "Marathon",
"MeteorArena", "MeteorArena",
"Saltern", "Saltern",
"Reach", "Reach",
"Train",
"Oasis", "Oasis",
"Gate",
"Amber", "Amber",
"Loop",
"Plasma", "Plasma",
"Elkridge", "Elkridge",
"Convex",
"Relic", "Relic",
"dm01-entryway" "dm01-entryway",
}; };

View File

@@ -12,12 +12,10 @@ namespace Content.Server.Administration.Commands;
public sealed class PersistenceSave : LocalizedEntityCommands public sealed class PersistenceSave : LocalizedEntityCommands
{ {
[Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
public override string Command => "persistencesave"; 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) public override void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
@@ -47,8 +45,7 @@ public sealed class PersistenceSave : LocalizedEntityCommands
return; 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")); shell.WriteLine(Loc.GetString("cmd-savemap-success"));
} }
} }

View File

@@ -416,7 +416,7 @@ public sealed partial class AdminVerbSystem
{ {
Text = pinballName, Text = pinballName,
Category = VerbCategory.Smite, 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 = () => Act = () =>
{ {
var xform = Transform(args.Target); var xform = Transform(args.Target);
@@ -685,7 +685,7 @@ public sealed partial class AdminVerbSystem
{ {
Text = reptilianName, Text = reptilianName,
Category = VerbCategory.Smite, 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 = () => Act = () =>
{ {
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite"); _polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");

View File

@@ -38,18 +38,21 @@ public sealed class SolutionCommand : ToolshedCommand
public SolutionRef AdjReagent( public SolutionRef AdjReagent(
[PipedArgument] SolutionRef input, [PipedArgument] SolutionRef input,
ProtoId<ReagentPrototype> proto, ProtoId<ReagentPrototype> proto,
FixedPoint2 amount float amount
) )
{ {
_solutionContainer ??= GetSys<SharedSolutionContainerSystem>(); _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; return input;
@@ -59,7 +62,7 @@ public sealed class SolutionCommand : ToolshedCommand
public IEnumerable<SolutionRef> AdjReagent( public IEnumerable<SolutionRef> AdjReagent(
[PipedArgument] IEnumerable<SolutionRef> input, [PipedArgument] IEnumerable<SolutionRef> input,
ProtoId<ReagentPrototype> name, ProtoId<ReagentPrototype> name,
FixedPoint2 amount float amount
) )
=> input.Select(x => AdjReagent(x, name, amount)); => input.Select(x => AdjReagent(x, name, amount));
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ; using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.EntityEffects; using Content.Shared.EntityEffects;
@@ -231,29 +232,4 @@ namespace Content.Server.Body.Systems
_solutionContainerSystem.UpdateChemicals(soln.Value); _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.Body.Components;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.EntityEffects; 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.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Prototypes; using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.EntityEffects; using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -100,7 +100,7 @@ namespace Content.Server.Cargo.Systems
{ {
OnInteractUsingCash(uid, component, ref args); 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); OnInteractUsingSlip((uid, component), ref args, slip);
} }
@@ -144,7 +144,7 @@ namespace Content.Server.Cargo.Systems
if (args.Actor is not { Valid: true } player) if (args.Actor is not { Valid: true } player)
return; return;
if (component.SlipPrinter) if (component.Mode != CargoOrderConsoleMode.DirectOrder)
return; return;
if (!_accessReaderSystem.IsAllowed(player, uid)) if (!_accessReaderSystem.IsAllowed(player, uid))
@@ -181,7 +181,7 @@ namespace Content.Server.Cargo.Systems
return; return;
} }
var amount = GetOutstandingOrderCount(orderDatabase, order.Account); var amount = GetOutstandingOrderCount((station.Value, orderDatabase), order.Account);
var capacity = orderDatabase.Capacity; var capacity = orderDatabase.Capacity;
// Too many orders, avoid them getting spammed in the UI. // Too many orders, avoid them getting spammed in the UI.
@@ -312,7 +312,7 @@ namespace Content.Server.Cargo.Systems
{ {
var station = _station.GetOwningStation(uid); var station = _station.GetOwningStation(uid);
if (component.SlipPrinter) if (component.Mode != CargoOrderConsoleMode.DirectOrder)
return; return;
if (!TryGetOrderDatabase(station, out var orderDatabase)) if (!TryGetOrderDatabase(station, out var orderDatabase))
@@ -367,6 +367,9 @@ namespace Content.Server.Cargo.Systems
if (!TryGetOrderDatabase(stationUid, out var orderDatabase)) if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
return; return;
if (!TryComp<StationBankAccountComponent>(stationUid, out var bank))
return;
if (!_protoMan.TryIndex<CargoProductPrototype>(args.CargoProductId, out var product)) if (!_protoMan.TryIndex<CargoProductPrototype>(args.CargoProductId, out var product))
{ {
Log.Error($"Tried to add invalid cargo product {args.CargoProductId} as order!"); 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)) if (!GetAvailableProducts((uid, component)).Contains(args.CargoProductId))
return; return;
if (component.SlipPrinter) if (component.Mode == CargoOrderConsoleMode.PrintSlip)
{ {
OnAddOrderMessageSlipPrinter(uid, component, args, product); OnAddOrderMessageSlipPrinter(uid, component, args, product);
return; return;
} }
var targetAccount = component.Mode == CargoOrderConsoleMode.SendToPrimary ? bank.PrimaryAccount : component.Account;
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase), 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); PlayDenySound(uid, component);
return; return;
@@ -419,15 +424,33 @@ namespace Content.Server.Cargo.Systems
CargoConsoleUiKey.Orders, CargoConsoleUiKey.Orders,
new CargoConsoleInterfaceState( new CargoConsoleInterfaceState(
MetaData(station.Value).EntityName, MetaData(station.Value).EntityName,
GetOutstandingOrderCount(orderDatabase, console.Account), GetOutstandingOrderCount((station!.Value, orderDatabase), console.Account),
orderDatabase.Capacity, orderDatabase.Capacity,
GetNetEntity(station.Value), GetNetEntity(station.Value),
orderDatabase.Orders[console.Account], RelevantOrders((station!.Value, orderDatabase), (consoleUid, console)),
GetAvailableProducts((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) private void ConsolePopup(EntityUid actor, string text)
{ {
_popup.PopupCursor(text, actor); _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); 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; 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) if (!order.Approved)
continue; continue;
amount += order.OrderQuantity - order.NumDispatched; 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; return amount;
} }

View File

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

View File

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

View File

@@ -1,8 +1,12 @@
using System.Linq;
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Interaction; using Content.Server.Interaction;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Instruments; using Content.Shared.Instruments;
using Content.Shared.Instruments.UI; using Content.Shared.Instruments.UI;
@@ -17,6 +21,7 @@ using Robust.Shared.Console;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Instruments; namespace Content.Server.Instruments;
@@ -31,6 +36,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
[Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly IAdminLogManager _admingLogSystem = default!;
private const float MaxInstrumentBandRange = 10f; private const float MaxInstrumentBandRange = 10f;
@@ -50,6 +56,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop); SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster); SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster);
SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel); SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel);
SubscribeNetworkEvent<InstrumentSetChannelsEvent>(OnMidiSetChannels);
Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs => Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs =>
{ {
@@ -132,6 +139,44 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
Clean(uid, instrument); 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) private void OnMidiSetMaster(InstrumentSetMasterEvent msg, EntitySessionEventArgs args)
{ {
var uid = GetEntity(msg.Uid); var uid = GetEntity(msg.Uid);

View File

@@ -1,21 +1,20 @@
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Kitchen.Components; using Content.Server.Kitchen.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database; 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.Destructible;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; 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.Containers;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Random; using Robust.Shared.Random;

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Mech.Components; using Content.Server.Mech.Components;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
@@ -13,16 +14,17 @@ using Content.Shared.Mech.Components;
using Content.Shared.Mech.EntitySystems; using Content.Shared.Mech.EntitySystems;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Tools;
using Content.Shared.Tools.Components; 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.Tools.Systems;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Content.Shared.Wires;
using Robust.Server.Containers; using Robust.Server.Containers;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Player; using Robust.Shared.Player;
using Content.Shared.Whitelist; using Robust.Shared.Prototypes;
namespace Content.Server.Mech.Systems; namespace Content.Server.Mech.Systems;
@@ -40,6 +42,8 @@ public sealed partial class MechSystem : SharedMechSystem
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!; [Dependency] private readonly SharedToolSystem _toolSystem = default!;
private static readonly ProtoId<ToolQualityPrototype> PryingQuality = "Prying";
/// <inheritdoc/> /// <inheritdoc/>
public override void Initialize() public override void Initialize()
{ {
@@ -91,7 +95,7 @@ public sealed partial class MechSystem : SharedMechSystem
return; 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, var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.BatteryRemovalDelay,
new RemoveBatteryEvent(), uid, target: uid, used: args.Target) 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.NodeGroups;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Content.Server.Temperature.Components; using Content.Server.Temperature.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.UserInterface;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Climbing.Systems; using Content.Shared.Climbing.Systems;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database; using Content.Shared.Database;
@@ -26,11 +25,14 @@ using Content.Shared.Interaction;
using Content.Shared.Medical.Cryogenics; using Content.Shared.Medical.Cryogenics;
using Content.Shared.MedicalScanner; using Content.Shared.MedicalScanner;
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Tools;
using Content.Shared.Tools.Systems;
using Content.Shared.UserInterface;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
namespace Content.Server.Medical; namespace Content.Server.Medical;
@@ -51,6 +53,8 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!; [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
private static readonly ProtoId<ToolQualityPrototype> PryingQuality = "Prying";
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -211,7 +215,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
if (args.Handled || !entity.Comp.Locked || entity.Comp.BodyContainer.ContainedEntity == null) if (args.Handled || !entity.Comp.Locked || entity.Comp.BodyContainer.ContainedEntity == null)
return; 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) 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.Forensics;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Stunnable; 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.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;

View File

@@ -1,6 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration; using Content.Server.Administration;
using Content.Shared.Access.Components;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Robust.Server.Player; using Robust.Server.Player;
@@ -60,4 +59,12 @@ public sealed class RenameCommand : LocalizedEntityCommands
entityUid = EntityUid.Invalid; entityUid = EntityUid.Invalid;
return false; 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; 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; namespace Content.Server.Nutrition.Components;

View File

@@ -1,30 +1,25 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Shared.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics; using Content.Server.Forensics;
using Content.Server.Inventory; using Content.Server.Inventory;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter; using Content.Shared.EntityEffects.Effects;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition; using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Verbs;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -36,19 +31,15 @@ namespace Content.Server.Nutrition.EntitySystems;
public sealed class DrinkSystem : SharedDrinkSystem public sealed class DrinkSystem : SharedDrinkSystem
{ {
[Dependency] private readonly BodySystem _body = default!; [Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
[Dependency] private readonly FoodSystem _food = default!; [Dependency] private readonly FoodSystem _food = default!;
[Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly OpenableSystem _openable = default!; [Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PuddleSystem _puddle = default!; [Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly ReactiveSystem _reaction = default!; [Dependency] private readonly ReactiveSystem _reaction = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = 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 SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly StomachSystem _stomach = default!; [Dependency] private readonly StomachSystem _stomach = default!;
@@ -65,7 +56,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
// run after openable so its always open -> drink // run after openable so its always open -> drink
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]); SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]);
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract); SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter); SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter);
} }
@@ -157,76 +147,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
_appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance); _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> /// <summary>
/// Raised directed at a victim when someone has force fed them a drink. /// Raised directed at a victim when someone has force fed them a drink.
/// </summary> /// </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)) if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
return; return;
if (_openable.IsClosed(args.Used.Value, args.Target.Value)) if (_openable.IsClosed(args.Used.Value, args.Target.Value, predicted: true))
return; return;
// TODO this should really be checked every tick. // TODO this should really be checked every tick.
@@ -330,36 +250,4 @@ public sealed class DrinkSystem : SharedDrinkSystem
if (!forceDrink && solution.Volume > 0) if (!forceDrink && solution.Volume > 0)
args.Repeat = true; 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.Body.Components;
using Content.Server.DoAfter; using Content.Server.DoAfter;
using Content.Server.Explosion.EntitySystems; using Content.Server.Explosion.EntitySystems;
using Content.Server.Nutrition.Components; using Content.Server.Nutrition.Components;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems; using Content.Shared.Emag.Systems;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Nutrition; using Content.Shared.Nutrition;
using System.Threading; using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Atmos;
/// <summary> /// <summary>
/// System for vapes /// 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.Audio;
using Robust.Server.Containers; using Robust.Server.Containers;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;

View File

@@ -1,14 +1,24 @@
using Content.Shared.Security.Components; using Content.Shared.Security.Components;
using Content.Shared.Security.Systems; using Content.Shared.Security.Systems;
using Content.Shared.Wall;
namespace Content.Server.Security; namespace Content.Server.Security;
public sealed class GenpopSystem : SharedGenpopSystem public sealed class GenpopSystem : SharedGenpopSystem
{ {
private const float GenpopIDEjectDistanceFromWall = 1f;
protected override void CreateId(Entity<GenpopLockerComponent> ent, string name, float sentence, string crime) 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 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; ent.Comp.LinkedId = uid;
IdCard.TryChangeFullName(uid, name); IdCard.TryChangeFullName(uid, name);

View File

@@ -53,7 +53,7 @@ public sealed class CargoGiftsRule : StationEventSystem<CargoGiftsRuleComponent>
} }
// Add some presents // 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) while (outstanding < cargoDb.Capacity - component.OrderSpaceToLeave && component.Gifts.Count > 0)
{ {
// I wish there was a nice way to pop this // I wish there was a nice way to pop this

View File

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

View File

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

View File

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

View File

@@ -472,5 +472,10 @@ public enum LogType
/// <summary> /// <summary>
/// Damaging grid collision has occurred. /// Damaging grid collision has occurred.
/// </summary> /// </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, /// (The pressure threshold is so low that it doesn't make sense to do any calculations,
/// so it just applies this flat value). /// so it just applies this flat value).
/// </summary> /// </summary>
// Original value is 4, buff back when we have proper ways for players to deal with breaches. public const int LowPressureDamage = 4;
public const int LowPressureDamage = 1;
public const float WindowHeatTransferCoefficient = 0.1f; 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) if (ent.Comp.NumberOfPipeLayers <= 1 || ent.Comp.PipeLayersLocked)
return; 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) 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; 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) 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 toolName = Loc.GetString(toolProto.ToolName).ToLower();
var message = Loc.GetString("atmos-pipe-layers-component-tool-missing", ("toolName", toolName)); 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; return;
@@ -217,7 +219,7 @@ public abstract partial class SharedAtmosPipeLayersSystem : EntitySystem
var layerName = GetPipeLayerName(ent.Comp.CurrentPipeLayer); var layerName = GetPipeLayerName(ent.Comp.CurrentPipeLayer);
var message = Loc.GetString("atmos-pipe-layers-component-change-layer", ("layerName", layerName)); 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.Shared.Body.Systems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Whitelist; using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; 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 public sealed partial class StomachComponent : Component
{ {
/// <summary> /// <summary>
@@ -32,7 +33,7 @@ namespace Content.Server.Body.Components
/// What solution should this stomach push reagents into, on the body? /// What solution should this stomach push reagents into, on the body?
/// </summary> /// </summary>
[DataField] [DataField]
public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName; public string BodySolutionName = "chemicals";
/// <summary> /// <summary>
/// Time between reagents being ingested and them being /// 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.Body.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Body.Events;
using Content.Shared.Body.Organ; using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Body.Systems namespace Content.Shared.Body.Systems
{ {
public sealed class StomachSystem : EntitySystem public sealed class StomachSystem : EntitySystem
{ {
@@ -19,6 +21,7 @@ namespace Content.Server.Body.Systems
{ {
SubscribeLocalEvent<StomachComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<StomachComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StomachComponent, EntityUnpausedEvent>(OnUnpaused); SubscribeLocalEvent<StomachComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<StomachComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<StomachComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier); SubscribeLocalEvent<StomachComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
} }
@@ -32,6 +35,16 @@ namespace Content.Server.Body.Systems
ent.Comp.NextUpdate += args.PausedTime; 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) public override void Update(float frameTime)
{ {
var query = EntityQueryEnumerator<StomachComponent, OrganComponent, SolutionContainerManagerComponent>(); var query = EntityQueryEnumerator<StomachComponent, OrganComponent, SolutionContainerManagerComponent>();

View File

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

View File

@@ -15,4 +15,10 @@ public sealed partial class CCVars
public static readonly CVarDef<int> MaxMidiLaggedBatches = public static readonly CVarDef<int> MaxMidiLaggedBatches =
CVarDef.Create("midi.max_lagged_batches", 8, CVar.SERVERONLY); 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. /// some food object won't spam a user with flavors.
/// </summary> /// </summary>
public static readonly CVarDef<int> 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 = public static readonly CVarDef<string> DestinationFile =
CVarDef.Create("autogen.destination_file", "", CVar.SERVER | CVar.SERVERONLY); 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"; public static readonly ProtoId<RadioChannelPrototype> BaseAnnouncementChannel = "Supply";
/// <summary> /// <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> /// </summary>
[DataField] [DataField]
public bool SlipPrinter; public CargoOrderConsoleMode Mode = CargoOrderConsoleMode.DirectOrder;
/// <summary> /// <summary>
/// The time at which the console will be able to print a slip again. /// 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); 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> /// <summary>
/// Withdraw funds from an account /// Withdraw funds from an account
/// </summary> /// </summary>

View File

@@ -170,7 +170,7 @@ public sealed partial class ClimbSystem : VirtualController
private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent<AlternativeVerb> args) 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; return;
if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing || !climbingComponent.CanClimb) 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))] [Access(typeof(PacificationSystem))]
public sealed partial class PacifiedComponent : Component public sealed partial class PacifiedComponent : Component
{ {
/// <summary>
/// If true, this will prevent you from disarming opponents in combat.
/// </summary>
[DataField] [DataField]
public bool DisallowDisarm = false; public bool DisallowDisarm = false;
/// <summary> /// <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> /// </summary>
[DataField] [DataField]
public bool DisallowAllCombat = false; public bool DisallowAllCombat = false;
/// <summary> /// <summary>
/// When attempting attack against the same entity multiple times, /// When attempting attack against the same entity multiple times,
/// don't spam popups every frame and instead have a cooldown. /// don't spam popups every frame and instead have a cooldown.
/// </summary> /// </summary>
[DataField] [DataField]
public TimeSpan PopupCooldown = TimeSpan.FromSeconds(3.0); public TimeSpan PopupCooldown = TimeSpan.FromSeconds(3.0);
/// <summary>
/// Time at which the next popup can be shown.
/// </summary>
[DataField] [DataField]
[AutoPausedField] [AutoPausedField]
public TimeSpan? NextPopupTime = null; public TimeSpan? NextPopupTime = null;
/// <summary> /// <summary>
/// The last entity attacked, used for popup purposes (avoid spam) /// The last entity attacked, used for popup purposes (avoid spam)
/// </summary> /// </summary>
[DataField] [DataField]
public EntityUid? LastAttackedEntity = null; public EntityUid? LastAttackedEntity = null;
/// <summary>
/// The alert to show to owners of this component.
/// </summary>
[DataField] [DataField]
public ProtoId<AlertPrototype> PacifiedAlert = "Pacified"; 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.Body.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.EffectConditions; namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary> /// <summary>
/// Condition for if the entity is or isn't wearing internals. /// 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) public readonly int CompareTo(FixedPoint2 other)
{ {
if (other.Value > Value) return Value.CompareTo(other.Value);
{
return -1;
}
if (other.Value < Value)
{
return 1;
}
return 0;
} }
} }

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Ghost.Roles.Raffles; namespace Content.Shared.Ghost.Roles.Raffles;
/// <summary> /// <summary>
/// Defines settings for a ghost role raffle. /// 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; 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 Content.Shared.FixedPoint;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -8,7 +8,7 @@ namespace Content.Shared.HealthExaminable;
public sealed partial class HealthExaminableComponent : Component public sealed partial class HealthExaminableComponent : Component
{ {
public List<FixedPoint2> Thresholds = new() 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)] [DataField(required: true)]
public HashSet<ProtoId<DamageTypePrototype>> ExaminableTypes = default!; 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). /// Component that indicates that musical instrument was activated (ui opened).
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent] [RegisterComponent, NetworkedComponent]
public sealed partial class ActiveInstrumentComponent : Component; [AutoGenerateComponentState(true)]
public sealed partial class ActiveInstrumentComponent : Component
{
[DataField]
[AutoNetworkedField]
public MidiTrack?[] Tracks = [];
}
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class InstrumentComponentState : ComponentState public sealed class InstrumentComponentState : ComponentState
@@ -144,3 +150,72 @@ public enum InstrumentUiKey
{ {
Key, 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.Containers;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
namespace Content.Server.Light.Components; namespace Content.Shared.Light.Components;
/// <summary> /// <summary>
/// Device that allows user to quikly change bulbs in <see cref="PoweredLightComponent"/> /// Device that allows user to quikly change bulbs in <see cref="PoweredLightComponent"/>

View File

@@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Light.Components; 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. /// Time to have each direction applied. Will lerp from the current value to the next one.
/// </summary> /// </summary>
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public List<(float Ratio, Vector2 Direction, float Alpha)> Directions = new() public List<SunShadowCycleDirection> Directions = new()
{ {
(0f, new Vector2(0f, 3f), 0f), new SunShadowCycleDirection(0f, new Vector2(0f, 3f), 0f),
(0.25f, new Vector2(-3f, -0.1f), 0.5f), new SunShadowCycleDirection(0.25f, new Vector2(-3f, -0.1f), 0.5f),
(0.5f, new Vector2(0f, -3f), 0.8f), new SunShadowCycleDirection(0.5f, new Vector2(0f, -3f), 0.8f),
(0.75f, new Vector2(3f, -0.1f), 0.5f), 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> /// <summary>
/// Tracking component for an enabled cryo pod (which periodically tries to inject chemicals in the occupant, if one exists) /// 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.Administration.Logs;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Database; using Content.Shared.Database;

View File

@@ -73,7 +73,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary> /// <summary>
/// Returns whether an entity is a member of a faction. /// Returns whether an entity is a member of a faction.
/// </summary> /// </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)) if (!Resolve(ent, ref ent.Comp, false))
return false; return false;
@@ -85,7 +85,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// Returns whether an entity is a member of any listed faction. /// Returns whether an entity is a member of any listed faction.
/// If the list is empty this returns false. /// If the list is empty this returns false.
/// </summary> /// </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)) if (!Resolve(ent, ref ent.Comp, false))
return false; return false;
@@ -102,7 +102,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary> /// <summary>
/// Adds this entity to the particular faction. /// Adds this entity to the particular faction.
/// </summary> /// </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)) if (!_proto.HasIndex<NpcFactionPrototype>(faction))
{ {
@@ -121,7 +121,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary> /// <summary>
/// Adds this entity to the particular faction. /// Adds this entity to the particular faction.
/// </summary> /// </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); ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent);
@@ -143,7 +143,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary> /// <summary>
/// Removes this entity from the particular faction. /// Removes this entity from the particular faction.
/// </summary> /// </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)) if (!_proto.HasIndex<NpcFactionPrototype>(faction))
{ {
@@ -202,7 +202,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
return GetNearbyFactions(ent, range, ent.Comp.FriendlyFactions); 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); var xform = Transform(entity);
foreach (var ent in _lookup.GetEntitiesInRange<NpcFactionMemberComponent>(_xform.GetMapCoordinates((entity, xform)), range)) 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); 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); 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)) if (!Resolve(with, ref with.Comp, false))
return false; return false;
@@ -242,12 +242,12 @@ public sealed partial class NpcFactionSystem : EntitySystem
with.Comp.FriendlyFactions.Contains(target); 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); 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)) if (!Resolve(with, ref with.Comp, false))
return false; return false;
@@ -256,7 +256,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
with.Comp.HostileFactions.Contains(target); 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); return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with);
} }
@@ -264,7 +264,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary> /// <summary>
/// Makes the source faction friendly to the target faction, 1-way. /// Makes the source faction friendly to the target faction, 1-way.
/// </summary> /// </summary>
public void MakeFriendly(string source, string target) public void MakeFriendly([ForbidLiteral] string source, [ForbidLiteral] string target)
{ {
if (!_factions.TryGetValue(source, out var sourceFaction)) if (!_factions.TryGetValue(source, out var sourceFaction))
{ {
@@ -286,7 +286,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
/// <summary> /// <summary>
/// Makes the source faction hostile to the target faction, 1-way. /// Makes the source faction hostile to the target faction, 1-way.
/// </summary> /// </summary>
public void MakeHostile(string source, string target) public void MakeHostile([ForbidLiteral] string source, [ForbidLiteral] string target)
{ {
if (!_factions.TryGetValue(source, out var sourceFaction)) 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 public sealed partial class FlavorProfileComponent : Component
{ {
/// <summary> /// <summary>
/// Localized string containing the base flavor of this entity. /// Localized string containing the base flavor of this entity.
/// </summary> /// </summary>
[DataField("flavors")] [DataField]
public HashSet<string> Flavors { get; private set; } = new(); public HashSet<string> Flavors { get; private set; } = new();
/// <summary> /// <summary>
/// Reagent IDs to ignore when processing this flavor profile. Defaults to nutriment. /// Reagent IDs to ignore when processing this flavor profile. Defaults to nutriment.
/// </summary> /// </summary>
[DataField("ignoreReagents")] [DataField]
public HashSet<string> IgnoreReagents { get; private set; } = new() public HashSet<string> IgnoreReagents { get; private set; } = new()
{ {
"Nutriment", "Nutriment",
"Vitamin", "Vitamin",
"Protein" "Protein",
}; };
} }

View File

@@ -1,11 +1,10 @@
using Content.Server.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Nutrition.Components; namespace Content.Shared.Nutrition.Components;
[RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))] [RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))]
public sealed partial class FoodComponent : Component 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> /// <summary>
/// Component that denotes a piece of clothing that blocks the mouth or otherwise prevents eating & drinking. /// 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 /// 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. /// masks), then this component might become redundant.
/// </remarks> /// </remarks>
[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem), typeof(IngestionBlockerSystem))] [RegisterComponent, Access(typeof(FoodSystem), typeof(SharedDrinkSystem), typeof(IngestionBlockerSystem))]
public sealed partial class IngestionBlockerComponent : Component public sealed partial class IngestionBlockerComponent : Component
{ {
/// <summary> /// <summary>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Lock;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Lock;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Verbs; 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. /// 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. /// If user is not null a popup will be shown to them.
/// </summary> /// </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)) if (!Resolve(uid, ref comp, false))
return false; return false;
@@ -175,7 +175,12 @@ public sealed partial class OpenableSystem : EntitySystem
return false; return false;
if (user != null) 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; 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.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.FixedPoint; 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.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared.Nutrition.EntitySystems; namespace Content.Shared.Nutrition.EntitySystems;
public abstract partial class SharedDrinkSystem : EntitySystem 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 OpenableSystem _openable = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -17,6 +38,7 @@ public abstract partial class SharedDrinkSystem : EntitySystem
SubscribeLocalEvent<DrinkComponent, AttemptShakeEvent>(OnAttemptShake); SubscribeLocalEvent<DrinkComponent, AttemptShakeEvent>(OnAttemptShake);
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
} }
protected void OnAttemptShake(Entity<DrinkComponent> entity, ref AttemptShakeEvent args) 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) protected void OnExamined(Entity<DrinkComponent> entity, ref ExaminedEvent args)
{ {
TryComp<OpenableComponent>(entity, out var openable); 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; return;
var empty = IsEmpty(entity, entity.Comp); 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) protected FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null)
{ {
if (!Resolve(uid, ref component)) if (!Resolve(uid, ref component))
@@ -87,4 +141,74 @@ public abstract partial class SharedDrinkSystem : EntitySystem
return remainingString; 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.Examine;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Lathe; using Content.Shared.Lathe;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Random.Helpers; using Content.Shared.Random.Helpers;
using Content.Shared.Research.Components; using Content.Shared.Research.Components;
@@ -20,6 +21,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedResearchSystem _research = default!; [Dependency] private readonly SharedResearchSystem _research = default!;
[Dependency] private readonly SharedLatheSystem _lathe = default!; [Dependency] private readonly SharedLatheSystem _lathe = default!;
[Dependency] private readonly NameModifierSystem _nameModifier = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -28,6 +30,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine); SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<TechnologyDiskComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
} }
private void OnMapInit(Entity<TechnologyDiskComponent> ent, ref MapInitEvent args) private void OnMapInit(Entity<TechnologyDiskComponent> ent, ref MapInitEvent args)
@@ -55,6 +58,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
ent.Comp.Recipes = []; ent.Comp.Recipes = [];
ent.Comp.Recipes.Add(_random.Pick(techs)); ent.Comp.Recipes.Add(_random.Pick(techs));
Dirty(ent); Dirty(ent);
_nameModifier.RefreshNameModifiers(ent.Owner);
} }
private void OnAfterInteract(Entity<TechnologyDiskComponent> ent, ref AfterInteractEvent args) private void OnAfterInteract(Entity<TechnologyDiskComponent> ent, ref AfterInteractEvent args)
@@ -90,4 +94,16 @@ public sealed class TechnologyDiskSystem : EntitySystem
} }
args.PushMarkup(message); 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] [ViewVariables]
public Container ItemContainer = default!; 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> /// <summary>
/// A list representing the order in which /// A list representing the order in which
/// all the entities are stored in the bin. /// all the entities are stored in the bin.

View File

@@ -1,6 +1,6 @@
using Content.Shared.Inventory; using Content.Shared.Inventory;
namespace Content.Server.Storage.Components; namespace Content.Shared.Storage.Components;
/// <summary> /// <summary>
/// Applies an ongoing pickup area around the attached entity. /// 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 SharedHandsSystem _hands = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
public const string BinContainerId = "bin-container";
/// <inheritdoc/> /// <inheritdoc/>
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<BinComponent, ComponentStartup>(OnStartup); SubscribeLocalEvent<BinComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<BinComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<BinComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BinComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<BinComponent, EntRemovedFromContainerMessage>(OnEntRemoved); SubscribeLocalEvent<BinComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<BinComponent, InteractHandEvent>(OnInteractHand, before: new[] { typeof(SharedItemSystem) }); SubscribeLocalEvent<BinComponent, InteractHandEvent>(OnInteractHand, before: new[] { typeof(SharedItemSystem) });
SubscribeLocalEvent<BinComponent, AfterInteractUsingEvent>(OnAfterInteractUsing); SubscribeLocalEvent<BinComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
@@ -45,7 +44,7 @@ public sealed class BinSystem : EntitySystem
private void OnStartup(EntityUid uid, BinComponent component, ComponentStartup args) 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) 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) private void OnEntRemoved(EntityUid uid, BinComponent component, EntRemovedFromContainerMessage args)
{ {
component.Items.Remove(args.Entity); component.Items.Remove(args.Entity);
@@ -96,7 +100,7 @@ public sealed class BinSystem : EntitySystem
if (args.Using != null) if (args.Using != null)
{ {
var canReach = args.CanAccess && args.CanInteract; 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; return false;
_container.Insert(toInsert, component.ItemContainer); _container.Insert(toInsert, component.ItemContainer);
component.Items.Add(toInsert);
Dirty(uid, component); Dirty(uid, component);
return true; return true;
} }

View File

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

View File

@@ -115,7 +115,7 @@ public abstract partial class SharedToolSystem : EntitySystem
EntityUid user, EntityUid user,
EntityUid? target, EntityUid? target,
float doAfterDelay, float doAfterDelay,
IEnumerable<string> toolQualitiesNeeded, [ForbidLiteral] IEnumerable<string> toolQualitiesNeeded,
DoAfterEvent doAfterEv, DoAfterEvent doAfterEv,
float fuel = 0, float fuel = 0,
ToolComponent? toolComponent = null) ToolComponent? toolComponent = null)
@@ -153,7 +153,7 @@ public abstract partial class SharedToolSystem : EntitySystem
EntityUid user, EntityUid user,
EntityUid? target, EntityUid? target,
TimeSpan delay, TimeSpan delay,
IEnumerable<string> toolQualitiesNeeded, [ForbidLiteral] IEnumerable<string> toolQualitiesNeeded,
DoAfterEvent doAfterEv, DoAfterEvent doAfterEv,
out DoAfterId? id, out DoAfterId? id,
float fuel = 0, float fuel = 0,
@@ -200,7 +200,7 @@ public abstract partial class SharedToolSystem : EntitySystem
EntityUid user, EntityUid user,
EntityUid? target, EntityUid? target,
float doAfterDelay, float doAfterDelay,
string toolQualityNeeded, [ForbidLiteral] string toolQualityNeeded,
DoAfterEvent doAfterEv, DoAfterEvent doAfterEv,
float fuel = 0, float fuel = 0,
ToolComponent? toolComponent = null) ToolComponent? toolComponent = null)
@@ -219,7 +219,7 @@ public abstract partial class SharedToolSystem : EntitySystem
/// <summary> /// <summary>
/// Whether a tool entity has the specified quality or not. /// Whether a tool entity has the specified quality or not.
/// </summary> /// </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); 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. /// Whether a tool entity has all specified qualities or not.
/// </summary> /// </summary>
[PublicAPI] [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); 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 public abstract class SharedWeatherSystem : EntitySystem
{ {
[Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!; [Dependency] protected readonly IPrototypeManager ProtoMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!; [Dependency] private readonly MetaDataSystem _metadata = default!;

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