Merge branch 'master' into fix_highlight
This commit is contained in:
54
.github/workflows/check-trailing-whitespace.yml
vendored
54
.github/workflows/check-trailing-whitespace.yml
vendored
@@ -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
|
||||
@@ -203,6 +203,9 @@ namespace Content.Client.Cargo.UI
|
||||
/// </summary>
|
||||
public void PopulateOrders(IEnumerable<CargoOrderData> orders)
|
||||
{
|
||||
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
|
||||
return;
|
||||
|
||||
Requests.DisposeAllChildren();
|
||||
|
||||
foreach (var order in orders)
|
||||
@@ -237,6 +240,7 @@ namespace Content.Client.Cargo.UI
|
||||
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
|
||||
|
||||
// TODO: Disable based on access.
|
||||
row.SetApproveVisible(orderConsole.Mode != CargoOrderConsoleMode.SendToPrimary);
|
||||
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
|
||||
Requests.AddChild(row);
|
||||
}
|
||||
@@ -290,8 +294,8 @@ namespace Content.Client.Cargo.UI
|
||||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
|
||||
_timing.CurTime < orderConsole.NextAccountActionTime;
|
||||
|
||||
OrdersSpacer.Visible = !orderConsole.SlipPrinter;
|
||||
Orders.Visible = !orderConsole.SlipPrinter;
|
||||
OrdersSpacer.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
Orders.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,15 @@ namespace Content.Client.Cargo.UI
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetApproveVisible(bool visible)
|
||||
{
|
||||
Approve.Visible = visible;
|
||||
|
||||
if (visible)
|
||||
Cancel.AddStyleClass("OpenLeft");
|
||||
else
|
||||
Cancel.RemoveStyleClass("OpenLeft");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Content.Client.Chat.UI
|
||||
{
|
||||
public abstract class SpeechBubble : Control
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] protected readonly IConfigurationManager ConfigManager = default!;
|
||||
@@ -30,12 +31,12 @@ namespace Content.Client.Chat.UI
|
||||
/// <summary>
|
||||
/// The total time a speech bubble stays on screen.
|
||||
/// </summary>
|
||||
private const float TotalTime = 4;
|
||||
private static readonly TimeSpan TotalTime = TimeSpan.FromSeconds(4);
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time at the end of the bubble's life at which it starts fading.
|
||||
/// </summary>
|
||||
private const float FadeTime = 0.25f;
|
||||
private static readonly TimeSpan FadeTime = TimeSpan.FromSeconds(0.25f);
|
||||
|
||||
/// <summary>
|
||||
/// The distance in world space to offset the speech bubble from the center of the entity.
|
||||
@@ -50,7 +51,10 @@ namespace Content.Client.Chat.UI
|
||||
|
||||
private readonly EntityUid _senderEntity;
|
||||
|
||||
private float _timeLeft = TotalTime;
|
||||
/// <summary>
|
||||
/// The time at which this bubble will die.
|
||||
/// </summary>
|
||||
private TimeSpan _deathTime;
|
||||
|
||||
public float VerticalOffset { get; set; }
|
||||
private float _verticalOffsetAchieved;
|
||||
@@ -99,6 +103,7 @@ namespace Content.Client.Chat.UI
|
||||
bubble.Measure(Vector2Helpers.Infinity);
|
||||
ContentSize = bubble.DesiredSize;
|
||||
_verticalOffsetAchieved = -ContentSize.Y;
|
||||
_deathTime = _timing.RealTime + TotalTime;
|
||||
}
|
||||
|
||||
protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null);
|
||||
@@ -107,8 +112,8 @@ namespace Content.Client.Chat.UI
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
_timeLeft -= args.DeltaSeconds;
|
||||
if (_entityManager.Deleted(_senderEntity) || _timeLeft <= 0)
|
||||
var timeLeft = (float)(_deathTime - _timing.RealTime).TotalSeconds;
|
||||
if (_entityManager.Deleted(_senderEntity) || timeLeft <= 0)
|
||||
{
|
||||
// Timer spawn to prevent concurrent modification exception.
|
||||
Timer.Spawn(0, Die);
|
||||
@@ -131,10 +136,10 @@ namespace Content.Client.Chat.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (_timeLeft <= FadeTime)
|
||||
if (timeLeft <= FadeTime.TotalSeconds)
|
||||
{
|
||||
// Update alpha if we're fading.
|
||||
Modulate = Color.White.WithAlpha(_timeLeft / FadeTime);
|
||||
Modulate = Color.White.WithAlpha(timeLeft / (float)FadeTime.TotalSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -144,7 +149,7 @@ namespace Content.Client.Chat.UI
|
||||
|
||||
var baseOffset = 0f;
|
||||
|
||||
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
|
||||
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
|
||||
baseOffset = speech.SpeechBubbleOffset;
|
||||
|
||||
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
|
||||
@@ -175,9 +180,9 @@ namespace Content.Client.Chat.UI
|
||||
/// </summary>
|
||||
public void FadeNow()
|
||||
{
|
||||
if (_timeLeft > FadeTime)
|
||||
if (_deathTime > _timing.RealTime)
|
||||
{
|
||||
_timeLeft = FadeTime;
|
||||
_deathTime = _timing.RealTime + FadeTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Content.Client.Construction.UI
|
||||
private ConstructionSystem? _constructionSystem;
|
||||
private ConstructionPrototype? _selected;
|
||||
private List<ConstructionPrototype> _favoritedRecipes = [];
|
||||
private Dictionary<string, ContainerButton> _recipeButtons = new();
|
||||
private readonly Dictionary<string, ContainerButton> _recipeButtons = new();
|
||||
private string _selectedCategory = string.Empty;
|
||||
|
||||
private const string FavoriteCatName = "construction-category-favorites";
|
||||
@@ -217,8 +217,8 @@ namespace Content.Client.Construction.UI
|
||||
var itemButton = new ContainerButton()
|
||||
{
|
||||
VerticalAlignment = Control.VAlignment.Center,
|
||||
Name = recipe.TargetPrototype.Name,
|
||||
ToolTip = recipe.TargetPrototype.Name,
|
||||
Name = recipe.Prototype.Name,
|
||||
ToolTip = recipe.Prototype.Name,
|
||||
ToggleMode = true,
|
||||
Children = { protoView },
|
||||
};
|
||||
@@ -235,7 +235,7 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
if (buttonToggledEventArgs.Pressed &&
|
||||
_selected != null &&
|
||||
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton))
|
||||
_recipeButtons.TryGetValue(_selected.ID, out var oldButton))
|
||||
{
|
||||
oldButton.Pressed = false;
|
||||
SelectGridButton(oldButton, false);
|
||||
@@ -245,7 +245,7 @@ namespace Content.Client.Construction.UI
|
||||
};
|
||||
|
||||
recipesGrid.AddChild(itemButtonPanelContainer);
|
||||
_recipeButtons[recipe.Prototype.Name!] = itemButton;
|
||||
_recipeButtons[recipe.Prototype.ID] = itemButton;
|
||||
var isCurrentButtonSelected = _selected == recipe.Prototype;
|
||||
itemButton.Pressed = isCurrentButtonSelected;
|
||||
SelectGridButton(itemButton, isCurrentButtonSelected);
|
||||
@@ -307,7 +307,7 @@ namespace Content.Client.Construction.UI
|
||||
if (button.Parent is not PanelContainer buttonPanel)
|
||||
return;
|
||||
|
||||
button.Modulate = select ? Color.Green : Color.Transparent;
|
||||
button.Children.Single().Modulate = select ? Color.Green : Color.White;
|
||||
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
|
||||
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
|
||||
}
|
||||
|
||||
54
Content.Client/Instruments/InstrumentSystem.MidiParsing.cs
Normal file
54
Content.Client/Instruments/InstrumentSystem.MidiParsing.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Instruments;
|
||||
@@ -12,7 +13,7 @@ using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Instruments;
|
||||
|
||||
public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
@@ -23,6 +24,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
public int MaxMidiEventsPerBatch { get; private set; }
|
||||
public int MaxMidiEventsPerSecond { get; private set; }
|
||||
|
||||
public event Action? OnChannelsUpdated;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -38,6 +41,26 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
|
||||
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState);
|
||||
SubscribeLocalEvent<ActiveInstrumentComponent, AfterAutoHandleStateEvent>(OnActiveInstrumentAfterHandleState);
|
||||
}
|
||||
|
||||
private bool _isUpdateQueued = false;
|
||||
|
||||
private void OnActiveInstrumentAfterHandleState(Entity<ActiveInstrumentComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
// Called in the update loop so that the components update client side for resolving them in TryComps.
|
||||
_isUpdateQueued = true;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (!_isUpdateQueued)
|
||||
return;
|
||||
|
||||
_isUpdateQueued = false;
|
||||
OnChannelsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, SharedInstrumentComponent component, ref ComponentHandleState args)
|
||||
@@ -252,7 +275,13 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
|
||||
}
|
||||
|
||||
[Obsolete("Use overload that takes in byte[] instead.")]
|
||||
public bool OpenMidi(EntityUid uid, ReadOnlySpan<byte> data, InstrumentComponent? instrument = null)
|
||||
{
|
||||
return OpenMidi(uid, data.ToArray(), instrument);
|
||||
}
|
||||
|
||||
public bool OpenMidi(EntityUid uid, byte[] data, InstrumentComponent? instrument = null)
|
||||
{
|
||||
if (!Resolve(uid, ref instrument))
|
||||
return false;
|
||||
@@ -263,6 +292,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
return false;
|
||||
|
||||
SetMaster(uid, null);
|
||||
TrySetChannels(uid, data);
|
||||
|
||||
instrument.MidiEventBuffer.Clear();
|
||||
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
|
||||
return true;
|
||||
|
||||
147
Content.Client/Instruments/MidiParser/MidiInstrument.cs
Normal file
147
Content.Client/Instruments/MidiParser/MidiInstrument.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
184
Content.Client/Instruments/MidiParser/MidiParser.cs
Normal file
184
Content.Client/Instruments/MidiParser/MidiParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
103
Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs
Normal file
103
Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,7 @@
|
||||
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
<CheckButton Name="DisplayTrackNames"
|
||||
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,26 +1,56 @@
|
||||
using Content.Shared.Instruments;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChannelsMenu : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = null!;
|
||||
|
||||
private readonly InstrumentBoundUserInterface _owner;
|
||||
|
||||
public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_owner = owner;
|
||||
|
||||
ChannelList.OnItemSelected += OnItemSelected;
|
||||
ChannelList.OnItemDeselected += OnItemDeselected;
|
||||
AllButton.OnPressed += OnAllPressed;
|
||||
ClearButton.OnPressed += OnClearPressed;
|
||||
DisplayTrackNames.OnPressed += OnDisplayTrackNamesPressed;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated += UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnDisplayTrackNamesPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
DisplayTrackNames.SetClickPressed(!DisplayTrackNames.Pressed);
|
||||
Populate();
|
||||
}
|
||||
|
||||
private void UpdateChannelList()
|
||||
{
|
||||
Populate(); // This is kind of in-efficent because we don't filter for which instrument updated its channels, but idc
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated -= UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
@@ -51,15 +81,71 @@ public sealed partial class ChannelsMenu : DefaultWindow
|
||||
}
|
||||
}
|
||||
|
||||
public void Populate(InstrumentComponent? instrument)
|
||||
/// <summary>
|
||||
/// Walks up the tree of instrument masters to find the truest master of them all.
|
||||
/// </summary>
|
||||
private ActiveInstrumentComponent ResolveActiveInstrument(InstrumentComponent? comp)
|
||||
{
|
||||
comp ??= _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
|
||||
var instrument = new Entity<InstrumentComponent>(_owner.Owner, comp);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (instrument.Comp.Master == null)
|
||||
break;
|
||||
|
||||
instrument = new Entity<InstrumentComponent>((EntityUid)instrument.Comp.Master,
|
||||
_entityManager.GetComponent<InstrumentComponent>((EntityUid)instrument.Comp.Master));
|
||||
}
|
||||
|
||||
return _entityManager.GetComponent<ActiveInstrumentComponent>(instrument.Owner);
|
||||
}
|
||||
|
||||
public void Populate()
|
||||
{
|
||||
ChannelList.Clear();
|
||||
var instrument = _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
var activeInstrument = ResolveActiveInstrument(instrument);
|
||||
|
||||
for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
|
||||
{
|
||||
var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name",
|
||||
("number", i)), null, true, i);
|
||||
var label = _owner.Loc.GetString("instrument-component-channel-name",
|
||||
("number", i));
|
||||
if (activeInstrument != null
|
||||
&& activeInstrument.Tracks.TryGetValue(i, out var resolvedMidiChannel)
|
||||
&& resolvedMidiChannel != null)
|
||||
{
|
||||
if (DisplayTrackNames.Pressed)
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ TrackName: not null, InstrumentName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-multi",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName),
|
||||
("other", resolvedMidiChannel.InstrumentName)),
|
||||
{ TrackName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ ProgramName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.ProgramName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var item = ChannelList.AddItem(label, null, true, i);
|
||||
|
||||
item.Selected = !instrument?.FilteredChannels[i] ?? false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Instruments.UI;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Client.Audio.Midi;
|
||||
@@ -101,9 +102,7 @@ namespace Content.Client.Instruments.UI
|
||||
public void OpenChannelsMenu()
|
||||
{
|
||||
_channelsMenu ??= new ChannelsMenu(this);
|
||||
EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument);
|
||||
|
||||
_channelsMenu.Populate(instrument);
|
||||
_channelsMenu.Populate();
|
||||
_channelsMenu.OpenCenteredRight();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using Range = Robust.Client.UserInterface.Controls.Range;
|
||||
|
||||
@@ -145,10 +146,6 @@ namespace Content.Client.Instruments.UI
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
await using var memStream = new MemoryStream((int) file.Length);
|
||||
|
||||
await file.CopyToAsync(memStream);
|
||||
|
||||
if (!_entManager.TryGetComponent<InstrumentComponent>(Entity, out var instrument))
|
||||
{
|
||||
return;
|
||||
@@ -156,7 +153,7 @@ namespace Content.Client.Instruments.UI
|
||||
|
||||
if (!_entManager.System<InstrumentSystem>()
|
||||
.OpenMidi(Entity,
|
||||
memStream.GetBuffer().AsSpan(0, (int) memStream.Length),
|
||||
file.CopyToArray(),
|
||||
instrument))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -50,6 +50,18 @@ namespace Content.Client.Inventory
|
||||
[ViewVariables]
|
||||
private readonly EntityUid _virtualHiddenEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The current amount of added hand buttons.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private int _handCount;
|
||||
|
||||
/// <summary>
|
||||
/// The current shape of the inventory, needed to calculate the window size.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private Vector2i _inventoryDimensions;
|
||||
|
||||
public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
_examine = EntMan.System<ExamineSystem>();
|
||||
@@ -93,6 +105,8 @@ namespace Content.Client.Inventory
|
||||
return;
|
||||
|
||||
_strippingMenu.ClearButtons();
|
||||
_handCount = 0;
|
||||
_inventoryDimensions = Vector2i.Zero;
|
||||
|
||||
if (EntMan.TryGetComponent<InventoryComponent>(Owner, out var inv))
|
||||
{
|
||||
@@ -152,9 +166,15 @@ namespace Content.Client.Inventory
|
||||
// TODO allow windows to resize based on content's desired size
|
||||
|
||||
// for now: shit-code
|
||||
// this breaks for drones (too many hands, lots of empty vertical space), and looks shit for monkeys and the like.
|
||||
// but the window is realizable, so eh.
|
||||
_strippingMenu.SetSize = new Vector2(220, snare?.IsEnsnared == true ? 550 : 530);
|
||||
// calculate the window size manually
|
||||
// +20 horizontally and vertically from the ContentsContainer margin
|
||||
// +16 vertically from the BoxContainer margin
|
||||
// +27 vertically from the window header
|
||||
var horizontalMenuSize = Math.Max(200, Math.Max(_handCount, _inventoryDimensions.X + 1) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 20);
|
||||
var verticalMenuSize = Math.Max(200, (_inventoryDimensions.Y + (_handCount > 0 ? 2 : 1)) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 53);
|
||||
if (snare?.IsEnsnared == true)
|
||||
verticalMenuSize += 20;
|
||||
_strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize);
|
||||
}
|
||||
|
||||
private void AddHandButton(Hand hand)
|
||||
@@ -172,6 +192,8 @@ namespace Content.Client.Inventory
|
||||
|
||||
UpdateEntityIcon(button, hand.HeldEntity);
|
||||
_strippingMenu!.HandsContainer.AddChild(button);
|
||||
LayoutContainer.SetPosition(button, new Vector2i(_handCount, 0) * (SlotControl.DefaultButtonSize + ButtonSeparation));
|
||||
_handCount++;
|
||||
}
|
||||
|
||||
private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot)
|
||||
@@ -220,6 +242,10 @@ namespace Content.Client.Inventory
|
||||
UpdateEntityIcon(button, entity);
|
||||
|
||||
LayoutContainer.SetPosition(button, slotDef.StrippingWindowPos * (SlotControl.DefaultButtonSize + ButtonSeparation));
|
||||
if (slotDef.StrippingWindowPos.X > _inventoryDimensions.X)
|
||||
_inventoryDimensions = new Vector2i(slotDef.StrippingWindowPos.X, _inventoryDimensions.Y);
|
||||
if (slotDef.StrippingWindowPos.Y > _inventoryDimensions.Y)
|
||||
_inventoryDimensions = new Vector2i(_inventoryDimensions.X, slotDef.StrippingWindowPos.Y);
|
||||
}
|
||||
|
||||
private void UpdateEntityIcon(SlotControl button, EntityUid? entity)
|
||||
|
||||
@@ -454,7 +454,7 @@ public sealed class MappingState : GameplayStateBase
|
||||
switch (prototype)
|
||||
{
|
||||
case EntityPrototype entity:
|
||||
textures.AddRange(SpriteComponent.GetPrototypeTextures(entity, _resources).Select(t => t.Default));
|
||||
textures.AddRange(_sprite.GetPrototypeTextures(entity).Select(t => t.Default));
|
||||
break;
|
||||
case DecalPrototype decal:
|
||||
textures.Add(_sprite.Frame0(decal.Sprite));
|
||||
|
||||
@@ -3,15 +3,15 @@ using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Client.Shuttles.Commands;
|
||||
|
||||
public sealed class ShowEmergencyShuttleCommand : IConsoleCommand
|
||||
public sealed class ShowEmergencyShuttleCommand : LocalizedEntityCommands
|
||||
{
|
||||
public string Command => "showemergencyshuttle";
|
||||
public string Description => "Shows the expected position of the emergency shuttle";
|
||||
public string Help => $"{Command}";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
[Dependency] private readonly ShuttleSystem _shuttle = default!;
|
||||
|
||||
public override string Command => "showemergencyshuttle";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var tstalker = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ShuttleSystem>();
|
||||
tstalker.EnableShuttlePosition ^= true;
|
||||
shell.WriteLine($"Set emergency shuttle debug to {tstalker.EnableShuttlePosition}");
|
||||
_shuttle.EnableShuttlePosition ^= true;
|
||||
shell.WriteLine(Loc.GetString($"cmd-showemergencyshuttle-status", ("status", _shuttle.EnableShuttlePosition)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Content.Client.Strip
|
||||
public sealed class StrippingMenu : DefaultWindow
|
||||
{
|
||||
public LayoutContainer InventoryContainer = new();
|
||||
public BoxContainer HandsContainer = new() { Orientation = LayoutOrientation.Horizontal };
|
||||
public LayoutContainer HandsContainer = new();
|
||||
public BoxContainer SnareContainer = new();
|
||||
public bool Dirty = true;
|
||||
|
||||
|
||||
@@ -14,12 +14,17 @@ namespace Content.Client.UserInterface.Systems.Chat;
|
||||
/// </summary>
|
||||
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
|
||||
|
||||
private static readonly Regex StartDoubleQuote = new("\"$");
|
||||
private static readonly Regex EndDoubleQuote = new("^\"|(?<=^@)\"");
|
||||
private static readonly Regex StartAtSign = new("^@");
|
||||
|
||||
/// <summary>
|
||||
/// The list of words to be highlighted in the chatbox.
|
||||
/// </summary>
|
||||
private List<string> _highlights = new();
|
||||
private readonly List<string> _highlights = new();
|
||||
|
||||
/// <summary>
|
||||
/// The string holding the hex color used to highlight words.
|
||||
@@ -42,7 +47,7 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
|
||||
_config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }, true);
|
||||
|
||||
// Load highlights if any were saved.
|
||||
string highlights = _config.GetCVar(CCVars.ChatHighlights);
|
||||
var highlights = _config.GetCVar(CCVars.ChatHighlights);
|
||||
|
||||
if (!string.IsNullOrEmpty(highlights))
|
||||
{
|
||||
@@ -84,12 +89,12 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
|
||||
|
||||
// We first subdivide the highlights based on newlines to prevent replacing
|
||||
// a valid "\n" tag and adding it to the final regex.
|
||||
string[] splittedHighlights = newHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var splittedHighlights = newHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
for (int i = 0; i < splittedHighlights.Length; i++)
|
||||
for (var i = 0; i < splittedHighlights.Length; i++)
|
||||
{
|
||||
// Replace every "\" character with a "\\" to prevent "\n", "\0", etc...
|
||||
string keyword = splittedHighlights[i].Replace(@"\", @"\\");
|
||||
var keyword = splittedHighlights[i].Replace(@"\", @"\\");
|
||||
|
||||
// Escape the keyword to prevent special characters like "(" and ")" to be considered valid regex.
|
||||
keyword = Regex.Escape(keyword);
|
||||
@@ -102,18 +107,18 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
|
||||
// that make sure the words to match are separated by spaces or punctuation.
|
||||
// NOTE: The reason why we don't use \b tags is that \b doesn't match reverse slash characters "\" so
|
||||
// a pre-sanitized (see 1.) string like "\[test]" wouldn't get picked up by the \b.
|
||||
if (keyword.Count(c => (c == '"')) > 0)
|
||||
if (keyword.Any(c => c == '"'))
|
||||
{
|
||||
// Matches the last double quote character.
|
||||
keyword = Regex.Replace(keyword, "\"$", "(?!\\w)");
|
||||
keyword = StartDoubleQuote.Replace(keyword, "(?!\\w)");
|
||||
// When matching for the first double quote character we also consider the possibility
|
||||
// of the double quote being preceded by a @ character.
|
||||
keyword = Regex.Replace(keyword, "^\"|(?<=^@)\"", "(?<!\\w)");
|
||||
keyword = EndDoubleQuote.Replace(keyword, "(?<!\\w)");
|
||||
}
|
||||
|
||||
// Make sure the character's name is highlighted only when mentioned directly (eg. it's said by someone),
|
||||
// for example in 'Name Surname says, "..."' 'Name Surname' won't be highlighted.
|
||||
keyword = Regex.Replace(keyword, "^@", @"(?<=(?<=,.*"".*)|(?<!\[Name].*))");
|
||||
keyword = StartAtSign.Replace(keyword, @"(?<=(?<=,.*"".*)|(?<!\[Name].*))");
|
||||
|
||||
_highlights.Add(keyword);
|
||||
}
|
||||
@@ -133,7 +138,7 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
|
||||
var (_, job, _, _, entityName) = data;
|
||||
|
||||
// Mark this entity's name as our character name for the "UpdateHighlights" function.
|
||||
string newHighlights = "@" + entityName;
|
||||
var newHighlights = "@" + entityName;
|
||||
|
||||
// Subdivide the character's name based on spaces or hyphens so that every word gets highlighted.
|
||||
if (newHighlights.Count(c => (c == ' ' || c == '-')) == 1)
|
||||
@@ -145,9 +150,9 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
|
||||
newHighlights = newHighlights.Split('-')[0] + "\n@" + newHighlights.Split('-')[^1];
|
||||
|
||||
// Convert the job title to kebab-case and use it as a key for the loc file.
|
||||
string jobKey = job.Replace(' ', '-').ToLower();
|
||||
var jobKey = job.Replace(' ', '-').ToLower();
|
||||
|
||||
if (Loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
|
||||
if (_loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
|
||||
newHighlights += '\n' + jobMatches.Replace(", ", "\n");
|
||||
|
||||
UpdateHighlights(newHighlights);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Content.Client.Eui;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Player;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -3,39 +3,33 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Weapons.Melee;
|
||||
|
||||
|
||||
public sealed class MeleeSpreadCommand : IConsoleCommand
|
||||
public sealed class MeleeSpreadCommand : LocalizedEntityCommands
|
||||
{
|
||||
public string Command => "showmeleespread";
|
||||
public string Description => "Shows the current weapon's range and arc for debugging";
|
||||
public string Help => $"{Command}";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlay = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly MeleeWeaponSystem _meleeSystem = default!;
|
||||
[Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
|
||||
public override string Command => "showmeleespread";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var collection = IoCManager.Instance;
|
||||
|
||||
if (collection == null)
|
||||
if (_overlay.RemoveOverlay<MeleeArcOverlay>())
|
||||
return;
|
||||
|
||||
var overlayManager = collection.Resolve<IOverlayManager>();
|
||||
|
||||
if (overlayManager.RemoveOverlay<MeleeArcOverlay>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sysManager = collection.Resolve<IEntitySystemManager>();
|
||||
|
||||
overlayManager.AddOverlay(new MeleeArcOverlay(
|
||||
collection.Resolve<IEntityManager>(),
|
||||
collection.Resolve<IEyeManager>(),
|
||||
collection.Resolve<IInputManager>(),
|
||||
collection.Resolve<IPlayerManager>(),
|
||||
sysManager.GetEntitySystem<MeleeWeaponSystem>(),
|
||||
sysManager.GetEntitySystem<SharedCombatModeSystem>(),
|
||||
sysManager.GetEntitySystem<SharedTransformSystem>()));
|
||||
_overlay.AddOverlay(new MeleeArcOverlay(
|
||||
EntityManager,
|
||||
_eyeManager,
|
||||
_inputManager,
|
||||
_playerManager,
|
||||
_meleeSystem,
|
||||
_combatSystem,
|
||||
_transformSystem));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Content.Client.Implants;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Implants;
|
||||
@@ -11,17 +9,17 @@ using Robust.Shared.Prototypes;
|
||||
namespace Content.IntegrationTests.Tests.Chameleon;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all round <see cref="IsProbablyRoundStartJob">"round start jobs"</see> have an associated chameleon loadout.
|
||||
/// Ensures all <see cref="IsProbablyRoundStartJob">"round start jobs"</see> have an associated chameleon loadout.
|
||||
/// </summary>
|
||||
public sealed class ChameleonJobLoadoutTest : InteractionTest
|
||||
{
|
||||
private readonly List<ProtoId<JobPrototype>> JobBlacklist =
|
||||
private static readonly List<ProtoId<JobPrototype>> JobBlacklist =
|
||||
[
|
||||
|
||||
];
|
||||
|
||||
[Test]
|
||||
public async Task CheckAllJobs()
|
||||
public Task CheckAllJobs()
|
||||
{
|
||||
var alljobs = ProtoMan.EnumeratePrototypes<JobPrototype>();
|
||||
|
||||
@@ -47,24 +45,16 @@ public sealed class ChameleonJobLoadoutTest : InteractionTest
|
||||
validJobs[chameleon.Job.Value] += 1;
|
||||
}
|
||||
|
||||
var errorMessage = new StringBuilder();
|
||||
errorMessage.AppendLine("The following job(s) have no chameleon prototype(s):");
|
||||
var invalid = false;
|
||||
|
||||
// All round start jobs have a chameleon loadout
|
||||
foreach (var job in validJobs)
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
if (job.Value != 0)
|
||||
continue;
|
||||
foreach (var job in validJobs)
|
||||
{
|
||||
Assert.That(job.Value, Is.Not.Zero,
|
||||
$"{job.Key} has no chameleonOutfit prototype.");
|
||||
}
|
||||
});
|
||||
|
||||
errorMessage.AppendLine(job.Key + " has no chameleonOutfit prototype.");
|
||||
invalid = true;
|
||||
}
|
||||
|
||||
if (!invalid)
|
||||
return;
|
||||
|
||||
Assert.Fail(errorMessage.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Content.IntegrationTests.Tests
|
||||
var client = pair.Client;
|
||||
var prototypeManager = client.ResolveDependency<IPrototypeManager>();
|
||||
var resourceCache = client.ResolveDependency<IResourceCache>();
|
||||
var spriteSys = client.System<SpriteSystem>();
|
||||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
@@ -26,7 +27,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
var _ = SpriteComponent.GetPrototypeTextures(proto, resourceCache).ToList();
|
||||
var _ = spriteSys.GetPrototypeTextures(proto).ToList();
|
||||
}, "Prototype {0} threw an exception when getting its textures.",
|
||||
proto.ID);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using Content.Shared.FixedPoint;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.NukeOps;
|
||||
using Content.Shared.Pinpointer;
|
||||
@@ -23,12 +24,16 @@ using Content.Shared.Station.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameRules;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class NukeOpsTest
|
||||
{
|
||||
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
|
||||
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
|
||||
|
||||
/// <summary>
|
||||
/// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded.
|
||||
/// </summary>
|
||||
@@ -119,8 +124,8 @@ public sealed class NukeOpsTest
|
||||
Assert.That(entMan.HasComponent<NukeOperativeComponent>(player));
|
||||
Assert.That(roleSys.MindIsAntagonist(mind));
|
||||
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
|
||||
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
|
||||
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
|
||||
Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
|
||||
Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
|
||||
var roles = roleSys.MindGetAllRoleInfo(mind);
|
||||
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
|
||||
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
|
||||
@@ -130,8 +135,8 @@ public sealed class NukeOpsTest
|
||||
Assert.That(entMan.HasComponent<NukeOperativeComponent>(dummyEnts[1]));
|
||||
Assert.That(roleSys.MindIsAntagonist(dummyMind));
|
||||
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
|
||||
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
|
||||
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
|
||||
Assert.That(factionSys.IsMember(dummyEnts[1], SyndicateFaction), Is.True);
|
||||
Assert.That(factionSys.IsMember(dummyEnts[1], NanotrasenFaction), Is.False);
|
||||
roles = roleSys.MindGetAllRoleInfo(dummyMind);
|
||||
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
|
||||
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
|
||||
@@ -146,8 +151,8 @@ public sealed class NukeOpsTest
|
||||
Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
|
||||
Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
|
||||
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
|
||||
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
|
||||
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
|
||||
Assert.That(factionSys.IsMember(ent, SyndicateFaction), Is.False);
|
||||
Assert.That(factionSys.IsMember(ent, NanotrasenFaction), Is.True);
|
||||
var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
|
||||
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Server.Roles;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -20,6 +21,8 @@ public sealed class TraitorRuleTest
|
||||
{
|
||||
private const string TraitorGameRuleProtoId = "Traitor";
|
||||
private const string TraitorAntagRoleName = "Traitor";
|
||||
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
|
||||
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
|
||||
|
||||
[Test]
|
||||
public async Task TestTraitorObjectives()
|
||||
@@ -108,8 +111,8 @@ public sealed class TraitorRuleTest
|
||||
// Make sure the player is a traitor.
|
||||
var mind = mindSys.GetMind(player)!.Value;
|
||||
Assert.That(roleSys.MindIsAntagonist(mind));
|
||||
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
|
||||
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
|
||||
Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
|
||||
Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
|
||||
Assert.That(traitorRule.TotalTraitors, Is.EqualTo(1));
|
||||
Assert.That(traitorRule.TraitorMinds[0], Is.EqualTo(mind));
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,6 @@ namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
"/Maps/centcomm.yml",
|
||||
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
|
||||
"/Maps/gate.yml", // Contains positronic brain and LSE-1200c "Perforator"
|
||||
"/Maps/meta.yml", // Contains warden's rubber stamp
|
||||
"/Maps/reach.yml", // Contains handheld crew monitor
|
||||
"/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
|
||||
"/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp
|
||||
@@ -62,27 +60,20 @@ namespace Content.IntegrationTests.Tests
|
||||
"Dev",
|
||||
"TestTeg",
|
||||
"Fland",
|
||||
"Meta",
|
||||
"Packed",
|
||||
"Omega",
|
||||
"Bagel",
|
||||
"CentComm",
|
||||
"Box",
|
||||
"Core",
|
||||
"Marathon",
|
||||
"MeteorArena",
|
||||
"Saltern",
|
||||
"Reach",
|
||||
"Train",
|
||||
"Oasis",
|
||||
"Gate",
|
||||
"Amber",
|
||||
"Loop",
|
||||
"Plasma",
|
||||
"Elkridge",
|
||||
"Convex",
|
||||
"Relic",
|
||||
"dm01-entryway"
|
||||
"dm01-entryway",
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -12,12 +12,10 @@ namespace Content.Server.Administration.Commands;
|
||||
public sealed class PersistenceSave : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _system = default!;
|
||||
[Dependency] private readonly SharedMapSystem _map = default!;
|
||||
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
|
||||
|
||||
public override string Command => "persistencesave";
|
||||
public override string Description => "Saves server data to a persistence file to be loaded later.";
|
||||
public override string Help => "persistencesave [mapId] [filePath - default: game.map (CCVar) ]";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
@@ -47,8 +45,7 @@ public sealed class PersistenceSave : LocalizedEntityCommands
|
||||
return;
|
||||
}
|
||||
|
||||
var mapLoader = _system.GetEntitySystem<MapLoaderSystem>();
|
||||
mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath));
|
||||
_mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath));
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = pinballName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "basketball"),
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Balls/basketball.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
var xform = Transform(args.Target);
|
||||
@@ -685,7 +685,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
Text = reptilianName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "plushie_lizard"),
|
||||
Icon = new SpriteSpecifier.Rsi(new ("Textures/Objects/Fun/Plushies/lizard.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");
|
||||
|
||||
@@ -38,18 +38,21 @@ public sealed class SolutionCommand : ToolshedCommand
|
||||
public SolutionRef AdjReagent(
|
||||
[PipedArgument] SolutionRef input,
|
||||
ProtoId<ReagentPrototype> proto,
|
||||
FixedPoint2 amount
|
||||
float amount
|
||||
)
|
||||
{
|
||||
_solutionContainer ??= GetSys<SharedSolutionContainerSystem>();
|
||||
|
||||
if (amount > 0)
|
||||
// Convert float to FixedPoint2
|
||||
var amountFixed = FixedPoint2.New(amount);
|
||||
|
||||
if (amountFixed > 0)
|
||||
{
|
||||
_solutionContainer.TryAddReagent(input.Solution, proto, amount, out _);
|
||||
_solutionContainer.TryAddReagent(input.Solution, proto, amountFixed, out _);
|
||||
}
|
||||
else if (amount < 0)
|
||||
else if (amountFixed < 0)
|
||||
{
|
||||
_solutionContainer.RemoveReagent(input.Solution, proto, -amount);
|
||||
_solutionContainer.RemoveReagent(input.Solution, proto, -amountFixed);
|
||||
}
|
||||
|
||||
return input;
|
||||
@@ -59,7 +62,7 @@ public sealed class SolutionCommand : ToolshedCommand
|
||||
public IEnumerable<SolutionRef> AdjReagent(
|
||||
[PipedArgument] IEnumerable<SolutionRef> input,
|
||||
ProtoId<ReagentPrototype> name,
|
||||
FixedPoint2 amount
|
||||
float amount
|
||||
)
|
||||
=> input.Select(x => AdjReagent(x, name, amount));
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed partial class AtmosPipeLayersSystem : SharedAtmosPipeLayersSystem
|
||||
if (ent.Comp.PipeLayersLocked)
|
||||
return;
|
||||
|
||||
base.SetPipeLayer(ent, layer);
|
||||
base.SetPipeLayer(ent, layer, user, used);
|
||||
|
||||
if (!TryComp<NodeContainerComponent>(ent, out var nodeContainer))
|
||||
return;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Bed.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Bed;
|
||||
using Content.Shared.Bed.Components;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Emag.Systems;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Drunk;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Forensics;
|
||||
using Content.Shared.Forensics.Components;
|
||||
@@ -247,18 +248,30 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
/// </summary>
|
||||
private void OnHealthBeingExamined(Entity<BloodstreamComponent> ent, ref HealthBeingExaminedEvent args)
|
||||
{
|
||||
// Shows profusely bleeding at half the max bleed rate.
|
||||
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2)
|
||||
// Shows massively bleeding at 0.75x the max bleed rate.
|
||||
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.75f)
|
||||
{
|
||||
args.Message.PushNewline();
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-massive-bleeding", ("target", ent.Owner)));
|
||||
}
|
||||
// Shows bleeding message when bleeding, but less than profusely.
|
||||
else if (ent.Comp.BleedAmount > 0)
|
||||
// Shows bleeding message when bleeding above half the max rate, but less than massively.
|
||||
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.5f)
|
||||
{
|
||||
args.Message.PushNewline();
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-strong-bleeding", ("target", ent.Owner)));
|
||||
}
|
||||
// Shows bleeding message when bleeding above 0.25x the max rate, but less than half the max.
|
||||
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.25f)
|
||||
{
|
||||
args.Message.PushNewline();
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
|
||||
}
|
||||
// Shows bleeding message when bleeding below 0.25x the max cap
|
||||
else if (ent.Comp.BleedAmount > 0)
|
||||
{
|
||||
args.Message.PushNewline();
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-slight-bleeding", ("target", ent.Owner)));
|
||||
}
|
||||
|
||||
// If the mob's blood level is below the damage threshhold, the pale message is added.
|
||||
if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
@@ -11,8 +14,6 @@ using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
namespace Content.Server.Body.Systems;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Body.Organ;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.EntityEffects;
|
||||
@@ -231,29 +232,4 @@ namespace Content.Server.Body.Systems
|
||||
_solutionContainerSystem.UpdateChemicals(soln.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO REFACTOR THIS
|
||||
// This will cause rates to slowly drift over time due to floating point errors.
|
||||
// Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
|
||||
[ByRefEvent]
|
||||
public readonly record struct ApplyMetabolicMultiplierEvent(
|
||||
EntityUid Uid,
|
||||
float Multiplier,
|
||||
bool Apply)
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity whose metabolism is being modified.
|
||||
/// </summary>
|
||||
public readonly EntityUid Uid = Uid;
|
||||
|
||||
/// <summary>
|
||||
/// What the metabolism's update rate will be multiplied by.
|
||||
/// </summary>
|
||||
public readonly float Multiplier = Multiplier;
|
||||
|
||||
/// <summary>
|
||||
/// If true, apply the multiplier. If false, revert it.
|
||||
/// </summary>
|
||||
public readonly bool Apply = Apply;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,19 @@ using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.EntityEffects;
|
||||
using Content.Shared.EntityEffects.EffectConditions;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.EntityEffects.EffectConditions;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Content.Server.Cargo.Systems
|
||||
{
|
||||
OnInteractUsingCash(uid, component, ref args);
|
||||
}
|
||||
else if (TryComp<CargoSlipComponent>(args.Used, out var slip) && !component.SlipPrinter)
|
||||
else if (TryComp<CargoSlipComponent>(args.Used, out var slip) && component.Mode == CargoOrderConsoleMode.DirectOrder)
|
||||
{
|
||||
OnInteractUsingSlip((uid, component), ref args, slip);
|
||||
}
|
||||
@@ -144,7 +144,7 @@ namespace Content.Server.Cargo.Systems
|
||||
if (args.Actor is not { Valid: true } player)
|
||||
return;
|
||||
|
||||
if (component.SlipPrinter)
|
||||
if (component.Mode != CargoOrderConsoleMode.DirectOrder)
|
||||
return;
|
||||
|
||||
if (!_accessReaderSystem.IsAllowed(player, uid))
|
||||
@@ -181,7 +181,7 @@ namespace Content.Server.Cargo.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
var amount = GetOutstandingOrderCount(orderDatabase, order.Account);
|
||||
var amount = GetOutstandingOrderCount((station.Value, orderDatabase), order.Account);
|
||||
var capacity = orderDatabase.Capacity;
|
||||
|
||||
// Too many orders, avoid them getting spammed in the UI.
|
||||
@@ -312,7 +312,7 @@ namespace Content.Server.Cargo.Systems
|
||||
{
|
||||
var station = _station.GetOwningStation(uid);
|
||||
|
||||
if (component.SlipPrinter)
|
||||
if (component.Mode != CargoOrderConsoleMode.DirectOrder)
|
||||
return;
|
||||
|
||||
if (!TryGetOrderDatabase(station, out var orderDatabase))
|
||||
@@ -367,6 +367,9 @@ namespace Content.Server.Cargo.Systems
|
||||
if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
|
||||
return;
|
||||
|
||||
if (!TryComp<StationBankAccountComponent>(stationUid, out var bank))
|
||||
return;
|
||||
|
||||
if (!_protoMan.TryIndex<CargoProductPrototype>(args.CargoProductId, out var product))
|
||||
{
|
||||
Log.Error($"Tried to add invalid cargo product {args.CargoProductId} as order!");
|
||||
@@ -376,15 +379,17 @@ namespace Content.Server.Cargo.Systems
|
||||
if (!GetAvailableProducts((uid, component)).Contains(args.CargoProductId))
|
||||
return;
|
||||
|
||||
if (component.SlipPrinter)
|
||||
if (component.Mode == CargoOrderConsoleMode.PrintSlip)
|
||||
{
|
||||
OnAddOrderMessageSlipPrinter(uid, component, args, product);
|
||||
return;
|
||||
}
|
||||
|
||||
var targetAccount = component.Mode == CargoOrderConsoleMode.SendToPrimary ? bank.PrimaryAccount : component.Account;
|
||||
|
||||
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase), component.Account);
|
||||
|
||||
if (!TryAddOrder(stationUid.Value, component.Account, data, orderDatabase))
|
||||
if (!TryAddOrder(stationUid.Value, targetAccount, data, orderDatabase))
|
||||
{
|
||||
PlayDenySound(uid, component);
|
||||
return;
|
||||
@@ -419,15 +424,33 @@ namespace Content.Server.Cargo.Systems
|
||||
CargoConsoleUiKey.Orders,
|
||||
new CargoConsoleInterfaceState(
|
||||
MetaData(station.Value).EntityName,
|
||||
GetOutstandingOrderCount(orderDatabase, console.Account),
|
||||
GetOutstandingOrderCount((station!.Value, orderDatabase), console.Account),
|
||||
orderDatabase.Capacity,
|
||||
GetNetEntity(station.Value),
|
||||
orderDatabase.Orders[console.Account],
|
||||
RelevantOrders((station!.Value, orderDatabase), (consoleUid, console)),
|
||||
GetAvailableProducts((consoleUid, console))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets orders relevant to this account, i.e. orders on the account directly or orders on behalf of the account in the primary account.
|
||||
/// </summary>
|
||||
private List<CargoOrderData> RelevantOrders(Entity<StationCargoOrderDatabaseComponent> station, Entity<CargoOrderConsoleComponent> console)
|
||||
{
|
||||
if (!TryComp<StationBankAccountComponent>(station, out var bank))
|
||||
return [];
|
||||
|
||||
var ourOrders = station.Comp.Orders[console.Comp.Account];
|
||||
|
||||
if (console.Comp.Account == bank.PrimaryAccount)
|
||||
return ourOrders;
|
||||
|
||||
var otherOrders = station.Comp.Orders[bank.PrimaryAccount].Where(order => order.Account == console.Comp.Account);
|
||||
|
||||
return ourOrders.Concat(otherOrders).ToList();
|
||||
}
|
||||
|
||||
private void ConsolePopup(EntityUid actor, string text)
|
||||
{
|
||||
_popup.PopupCursor(text, actor);
|
||||
@@ -447,17 +470,32 @@ namespace Content.Server.Cargo.Systems
|
||||
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason, account);
|
||||
}
|
||||
|
||||
public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component, ProtoId<CargoAccountPrototype> account)
|
||||
public int GetOutstandingOrderCount(Entity<StationCargoOrderDatabaseComponent> station, ProtoId<CargoAccountPrototype> account)
|
||||
{
|
||||
var amount = 0;
|
||||
|
||||
foreach (var order in component.Orders[account])
|
||||
if (!TryComp<StationBankAccountComponent>(station, out var bank))
|
||||
return amount;
|
||||
|
||||
foreach (var order in station.Comp.Orders[account])
|
||||
{
|
||||
if (!order.Approved)
|
||||
continue;
|
||||
amount += order.OrderQuantity - order.NumDispatched;
|
||||
}
|
||||
|
||||
if (account == bank.PrimaryAccount)
|
||||
return amount;
|
||||
|
||||
foreach (var order in station.Comp.Orders[bank.PrimaryAccount])
|
||||
{
|
||||
if (order.Account != account)
|
||||
continue;
|
||||
if (!order.Approved)
|
||||
continue;
|
||||
amount += order.OrderQuantity - order.NumDispatched;
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,17 @@ using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Decals;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Crayon;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Crayon;
|
||||
|
||||
@@ -5,29 +5,31 @@ using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Tools;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Fax;
|
||||
using Content.Shared.Fax.Systems;
|
||||
using Content.Shared.Fax.Components;
|
||||
using Content.Shared.Fax.Systems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Labels.Components;
|
||||
using Content.Shared.Labels.EntitySystems;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.NameModifier.Components;
|
||||
using Content.Shared.Paper;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Tools;
|
||||
using Content.Shared.UserInterface;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.NameModifier.Components;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Fax;
|
||||
|
||||
@@ -50,6 +52,8 @@ public sealed class FaxSystem : EntitySystem
|
||||
[Dependency] private readonly FaxecuteSystem _faxecute = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
private static readonly ProtoId<ToolQualityPrototype> ScrewingQuality = "Screwing";
|
||||
|
||||
private const string PaperSlotId = "Paper";
|
||||
|
||||
public override void Initialize()
|
||||
@@ -209,7 +213,7 @@ public sealed class FaxSystem : EntitySystem
|
||||
{
|
||||
if (args.Handled ||
|
||||
!TryComp<ActorComponent>(args.User, out var actor) ||
|
||||
!_toolSystem.HasQuality(args.Used, "Screwing")) // Screwing because Pulsing already used by device linking
|
||||
!_toolSystem.HasQuality(args.Used, ScrewingQuality)) // Screwing because Pulsing already used by device linking
|
||||
return;
|
||||
|
||||
_quickDialog.OpenDialog(actor.PlayerSession,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Interaction;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Instruments.UI;
|
||||
@@ -17,6 +21,7 @@ using Robust.Shared.Console;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Instruments;
|
||||
|
||||
@@ -31,6 +36,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _admingLogSystem = default!;
|
||||
|
||||
private const float MaxInstrumentBandRange = 10f;
|
||||
|
||||
@@ -50,6 +56,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
|
||||
SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster);
|
||||
SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel);
|
||||
SubscribeNetworkEvent<InstrumentSetChannelsEvent>(OnMidiSetChannels);
|
||||
|
||||
Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs =>
|
||||
{
|
||||
@@ -132,6 +139,44 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
Clean(uid, instrument);
|
||||
}
|
||||
|
||||
|
||||
private void OnMidiSetChannels(InstrumentSetChannelsEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = GetEntity(msg.Uid);
|
||||
|
||||
if (!TryComp(uid, out InstrumentComponent? instrument) || !TryComp(uid, out ActiveInstrumentComponent? activeInstrument))
|
||||
return;
|
||||
|
||||
if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
|
||||
return;
|
||||
|
||||
if (msg.Tracks.Length > RobustMidiEvent.MaxChannels)
|
||||
{
|
||||
Log.Warning($"{args.SenderSession.UserId.ToString()} - Tried to send tracks over the limit! Received: {msg.Tracks.Length}; Limit: {RobustMidiEvent.MaxChannels}");
|
||||
return;
|
||||
}
|
||||
|
||||
var tracksString = string.Join("\n",
|
||||
msg.Tracks
|
||||
.Where(t => t != null)
|
||||
.Select(t => t!.ToString()));
|
||||
|
||||
_admingLogSystem.Add(
|
||||
LogType.Instrument,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(args.SenderSession.AttachedEntity)} set the midi channels for {ToPrettyString(uid)} to {tracksString}");
|
||||
|
||||
// Truncate any track names too long.
|
||||
foreach (var t in msg.Tracks)
|
||||
{
|
||||
t?.TruncateFields(_cfg.GetCVar(CCVars.MidiMaxChannelNameLength));
|
||||
}
|
||||
|
||||
activeInstrument.Tracks = msg.Tracks;
|
||||
|
||||
Dirty(uid, activeInstrument);
|
||||
}
|
||||
|
||||
private void OnMidiSetMaster(InstrumentSetMasterEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = GetEntity(msg.Uid);
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Mech.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
@@ -13,16 +14,17 @@ using Content.Shared.Mech.Components;
|
||||
using Content.Shared.Mech.EntitySystems;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tools;
|
||||
using Content.Shared.Tools.Components;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Wires;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Whitelist;
|
||||
using Content.Shared.Wires;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Mech.Systems;
|
||||
|
||||
@@ -40,6 +42,8 @@ public sealed partial class MechSystem : SharedMechSystem
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
|
||||
|
||||
private static readonly ProtoId<ToolQualityPrototype> PryingQuality = "Prying";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -91,7 +95,7 @@ public sealed partial class MechSystem : SharedMechSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (_toolSystem.HasQuality(args.Used, "Prying") && component.BatterySlot.ContainedEntity != null)
|
||||
if (_toolSystem.HasQuality(args.Used, PryingQuality) && component.BatterySlot.ContainedEntity != null)
|
||||
{
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.BatteryRemovalDelay,
|
||||
new RemoveBatteryEvent(), uid, target: uid, used: args.Target)
|
||||
|
||||
@@ -9,12 +9,11 @@ using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Climbing.Systems;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Database;
|
||||
@@ -26,11 +25,14 @@ using Content.Shared.Interaction;
|
||||
using Content.Shared.Medical.Cryogenics;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Tools;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
|
||||
|
||||
namespace Content.Server.Medical;
|
||||
|
||||
@@ -51,6 +53,8 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
|
||||
|
||||
private static readonly ProtoId<ToolQualityPrototype> PryingQuality = "Prying";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -211,7 +215,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
|
||||
if (args.Handled || !entity.Comp.Locked || entity.Comp.BodyContainer.ContainedEntity == null)
|
||||
return;
|
||||
|
||||
args.Handled = _toolSystem.UseTool(args.Used, args.User, entity.Owner, entity.Comp.PryDelay, "Prying", new CryoPodPryFinished());
|
||||
args.Handled = _toolSystem.UseTool(args.Used, args.User, entity.Owner, entity.Comp.PryDelay, PryingQuality, new CryoPodPryFinished());
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<CryoPodComponent> entity, ref ExaminedEvent args)
|
||||
|
||||
@@ -4,8 +4,10 @@ using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server.Player;
|
||||
@@ -60,4 +59,12 @@ public sealed class RenameCommand : LocalizedEntityCommands
|
||||
entityUid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
return CompletionResult.FromOptions(CompletionHelper.SessionNames());
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Server.Nutrition.Components;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Server.Nutrition.Components;
|
||||
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Inventory;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Player;
|
||||
@@ -36,19 +31,15 @@ namespace Content.Server.Nutrition.EntitySystems;
|
||||
public sealed class DrinkSystem : SharedDrinkSystem
|
||||
{
|
||||
[Dependency] private readonly BodySystem _body = default!;
|
||||
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
|
||||
[Dependency] private readonly FoodSystem _food = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reaction = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly StomachSystem _stomach = default!;
|
||||
@@ -65,7 +56,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
|
||||
// run after openable so its always open -> drink
|
||||
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]);
|
||||
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
|
||||
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
|
||||
SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
|
||||
@@ -157,76 +147,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
|
||||
_appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to feed the drink item to the target entity
|
||||
/// </summary>
|
||||
private bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
|
||||
{
|
||||
if (!HasComp<BodyComponent>(target))
|
||||
return false;
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
|
||||
return false;
|
||||
|
||||
if (_openable.IsClosed(item, user))
|
||||
return true;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
|
||||
{
|
||||
if (drink.IgnoreEmpty)
|
||||
return false;
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_food.IsMouthBlocked(target, user))
|
||||
return true;
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
|
||||
return true;
|
||||
|
||||
var forceDrink = user != target;
|
||||
|
||||
if (forceDrink)
|
||||
{
|
||||
var userName = Identity.Entity(user, EntityManager);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
|
||||
|
||||
// logging
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// log voluntary drinking
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
|
||||
}
|
||||
|
||||
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
|
||||
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager,
|
||||
user,
|
||||
forceDrink ? drink.ForceFeedDelay : drink.Delay,
|
||||
new ConsumeDoAfterEvent(drink.Solution, flavors),
|
||||
eventTarget: item,
|
||||
target: target,
|
||||
used: item)
|
||||
{
|
||||
BreakOnHandChange = false,
|
||||
BreakOnMove = forceDrink,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.01f,
|
||||
DistanceThreshold = 1.0f,
|
||||
// do-after will stop if item is dropped when trying to feed someone else
|
||||
// or if the item started out in the user's own hands
|
||||
NeedHand = forceDrink || _hands.IsHolding(user, item),
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterEventArgs);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at a victim when someone has force fed them a drink.
|
||||
/// </summary>
|
||||
@@ -241,7 +161,7 @@ public sealed class DrinkSystem : SharedDrinkSystem
|
||||
if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
|
||||
return;
|
||||
|
||||
if (_openable.IsClosed(args.Used.Value, args.Target.Value))
|
||||
if (_openable.IsClosed(args.Used.Value, args.Target.Value, predicted: true))
|
||||
return;
|
||||
|
||||
// TODO this should really be checked every tick.
|
||||
@@ -330,36 +250,4 @@ public sealed class DrinkSystem : SharedDrinkSystem
|
||||
if (!forceDrink && solution.Volume > 0)
|
||||
args.Repeat = true;
|
||||
}
|
||||
|
||||
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
|
||||
{
|
||||
if (entity.Owner == ev.User ||
|
||||
!ev.CanInteract ||
|
||||
!ev.CanAccess ||
|
||||
!TryComp<BodyComponent>(ev.User, out var body) ||
|
||||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
|
||||
return;
|
||||
|
||||
// Make sure the solution exists
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
|
||||
return;
|
||||
|
||||
// no drinking from living drinks, have to kill them first.
|
||||
if (_mobState.IsAlive(entity))
|
||||
return;
|
||||
|
||||
var user = ev.User;
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
TryDrink(user, user, entity.Comp, entity);
|
||||
},
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
|
||||
Text = Loc.GetString("drink-system-verb-drink"),
|
||||
Priority = 2
|
||||
};
|
||||
|
||||
ev.Verbs.Add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Nutrition;
|
||||
using System.Threading;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// System for vapes
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ using Content.Shared.Popups;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
using Content.Shared.Security.Components;
|
||||
using Content.Shared.Security.Systems;
|
||||
using Content.Shared.Wall;
|
||||
|
||||
namespace Content.Server.Security;
|
||||
|
||||
public sealed class GenpopSystem : SharedGenpopSystem
|
||||
{
|
||||
private const float GenpopIDEjectDistanceFromWall = 1f;
|
||||
protected override void CreateId(Entity<GenpopLockerComponent> ent, string name, float sentence, string crime)
|
||||
{
|
||||
// Default to prisoner locker coordinates for ID spawn
|
||||
var xform = Transform(ent);
|
||||
var uid = Spawn(ent.Comp.IdCardProto, xform.Coordinates);
|
||||
var spawnCoordinates = xform.Coordinates;
|
||||
// Offset prisoner wall locker coordinates in wallmount direction for ID spawn; avoids spawning ID inside wall
|
||||
if (TryComp<WallMountComponent>(ent, out var wallMountComponent))
|
||||
{
|
||||
var offset = (wallMountComponent.Direction + xform.LocalRotation - Math.PI / 2).ToVec() * GenpopIDEjectDistanceFromWall;
|
||||
spawnCoordinates = spawnCoordinates.Offset(offset);
|
||||
}
|
||||
var uid = Spawn(ent.Comp.IdCardProto, spawnCoordinates);
|
||||
ent.Comp.LinkedId = uid;
|
||||
IdCard.TryChangeFullName(uid, name);
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class CargoGiftsRule : StationEventSystem<CargoGiftsRuleComponent>
|
||||
}
|
||||
|
||||
// Add some presents
|
||||
var outstanding = CargoSystem.GetOutstandingOrderCount(cargoDb, component.Account);
|
||||
var outstanding = _cargoSystem.GetOutstandingOrderCount((station.Value, cargoDb), component.Account);
|
||||
while (outstanding < cargoDb.Capacity - component.OrderSpaceToLeave && component.Gifts.Count > 0)
|
||||
{
|
||||
// I wish there was a nice way to pop this
|
||||
|
||||
@@ -227,14 +227,13 @@ namespace Content.Server.VendingMachines
|
||||
}
|
||||
|
||||
// Default spawn coordinates
|
||||
var spawnCoordinates = Transform(uid).Coordinates;
|
||||
var xform = Transform(uid);
|
||||
var spawnCoordinates = xform.Coordinates;
|
||||
|
||||
//Make sure the wallvends spawn outside of the wall.
|
||||
|
||||
if (TryComp<WallMountComponent>(uid, out var wallMountComponent))
|
||||
{
|
||||
|
||||
var offset = wallMountComponent.Direction.ToWorldVec() * WallVendEjectDistanceFromWall;
|
||||
var offset = (wallMountComponent.Direction + xform.LocalRotation - Math.PI / 2).ToVec() * WallVendEjectDistanceFromWall;
|
||||
spawnCoordinates = spawnCoordinates.Offset(offset);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Tools;
|
||||
using Content.Shared.Tools.Components;
|
||||
using Content.Shared.Wires;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -29,6 +30,9 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ConstructionSystem _construction = default!;
|
||||
|
||||
private static readonly ProtoId<ToolQualityPrototype> CuttingQuality = "Cutting";
|
||||
private static readonly ProtoId<ToolQualityPrototype> PulsingQuality = "Pulsing";
|
||||
|
||||
// This is where all the wire layouts are stored.
|
||||
[ViewVariables] private readonly Dictionary<string, WireLayout> _layouts = new();
|
||||
|
||||
@@ -443,8 +447,8 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
if (!IsPanelOpen(uid))
|
||||
return;
|
||||
|
||||
if (Tool.HasQuality(args.Used, "Cutting", tool) ||
|
||||
Tool.HasQuality(args.Used, "Pulsing", tool))
|
||||
if (Tool.HasQuality(args.Used, CuttingQuality, tool) ||
|
||||
Tool.HasQuality(args.Used, PulsingQuality, tool))
|
||||
{
|
||||
if (TryComp(args.User, out ActorComponent? actor))
|
||||
{
|
||||
@@ -623,7 +627,7 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
switch (action)
|
||||
{
|
||||
case WiresAction.Cut:
|
||||
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
|
||||
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
|
||||
{
|
||||
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
|
||||
return;
|
||||
@@ -637,7 +641,7 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
|
||||
break;
|
||||
case WiresAction.Mend:
|
||||
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
|
||||
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
|
||||
{
|
||||
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
|
||||
return;
|
||||
@@ -651,7 +655,7 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
|
||||
break;
|
||||
case WiresAction.Pulse:
|
||||
if (!Tool.HasQuality(toolEntity, "Pulsing", tool))
|
||||
if (!Tool.HasQuality(toolEntity, PulsingQuality, tool))
|
||||
{
|
||||
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), user);
|
||||
return;
|
||||
@@ -710,7 +714,7 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
switch (action)
|
||||
{
|
||||
case WiresAction.Cut:
|
||||
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
|
||||
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
|
||||
{
|
||||
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
|
||||
break;
|
||||
@@ -731,7 +735,7 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
UpdateUserInterface(used);
|
||||
break;
|
||||
case WiresAction.Mend:
|
||||
if (!Tool.HasQuality(toolEntity, "Cutting", tool))
|
||||
if (!Tool.HasQuality(toolEntity, CuttingQuality, tool))
|
||||
{
|
||||
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-wirecutters"), user);
|
||||
break;
|
||||
@@ -752,7 +756,7 @@ public sealed class WiresSystem : SharedWiresSystem
|
||||
UpdateUserInterface(used);
|
||||
break;
|
||||
case WiresAction.Pulse:
|
||||
if (!Tool.HasQuality(toolEntity, "Pulsing", tool))
|
||||
if (!Tool.HasQuality(toolEntity, PulsingQuality, tool))
|
||||
{
|
||||
_popupSystem.PopupCursor(Loc.GetString("wires-component-ui-on-receive-message-need-multitool"), user);
|
||||
break;
|
||||
|
||||
@@ -38,6 +38,7 @@ using Content.Shared.Ghost.Roles.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
|
||||
namespace Content.Server.Zombies;
|
||||
|
||||
@@ -66,6 +67,8 @@ public sealed partial class ZombieSystem
|
||||
|
||||
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
|
||||
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
|
||||
private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
|
||||
|
||||
/// <summary>
|
||||
/// Handles an entity turning into a zombie when they die or go into crit
|
||||
/// </summary>
|
||||
@@ -223,7 +226,7 @@ public sealed partial class ZombieSystem
|
||||
_mobState.ChangeMobState(target, MobState.Alive);
|
||||
|
||||
_faction.ClearFactions(target, dirty: false);
|
||||
_faction.AddFaction(target, "Zombie");
|
||||
_faction.AddFaction(target, ZombieFaction);
|
||||
|
||||
//gives it the funny "Zombie ___" name.
|
||||
_nameMod.RefreshNameModifiers(target);
|
||||
|
||||
@@ -472,5 +472,10 @@ public enum LogType
|
||||
/// <summary>
|
||||
/// Damaging grid collision has occurred.
|
||||
/// </summary>
|
||||
ShuttleImpact = 102
|
||||
ShuttleImpact = 102,
|
||||
|
||||
/// <summary>
|
||||
/// Events relating to midi playback.
|
||||
/// </summary>
|
||||
Instrument = 103,
|
||||
}
|
||||
|
||||
@@ -320,8 +320,7 @@ namespace Content.Shared.Atmos
|
||||
/// (The pressure threshold is so low that it doesn't make sense to do any calculations,
|
||||
/// so it just applies this flat value).
|
||||
/// </summary>
|
||||
// Original value is 4, buff back when we have proper ways for players to deal with breaches.
|
||||
public const int LowPressureDamage = 1;
|
||||
public const int LowPressureDamage = 4;
|
||||
|
||||
public const float WindowHeatTransferCoefficient = 0.1f;
|
||||
|
||||
|
||||
@@ -119,14 +119,16 @@ public abstract partial class SharedAtmosPipeLayersSystem : EntitySystem
|
||||
if (ent.Comp.NumberOfPipeLayers <= 1 || ent.Comp.PipeLayersLocked)
|
||||
return;
|
||||
|
||||
if (!TryComp<ToolComponent>(args.Used, out var tool) || !_tool.HasQuality(args.Used, ent.Comp.Tool, tool))
|
||||
return;
|
||||
|
||||
if (TryComp<SubFloorHideComponent>(ent, out var subFloorHide) && subFloorHide.IsUnderCover)
|
||||
{
|
||||
_popup.PopupPredicted(Loc.GetString("atmos-pipe-layers-component-cannot-adjust-pipes"), ent, args.User);
|
||||
_popup.PopupClient(Loc.GetString("atmos-pipe-layers-component-cannot-adjust-pipes"), ent, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<ToolComponent>(args.Used, out var tool) && _tool.HasQuality(args.Used, ent.Comp.Tool, tool))
|
||||
_tool.UseTool(args.Used, args.User, ent, ent.Comp.Delay, tool.Qualities, new TrySetNextPipeLayerCompletedEvent());
|
||||
_tool.UseTool(args.Used, args.User, ent, ent.Comp.Delay, tool.Qualities, new TrySetNextPipeLayerCompletedEvent());
|
||||
}
|
||||
|
||||
private void OnUseInHandEvent(Entity<AtmosPipeLayersComponent> ent, ref UseInHandEvent args)
|
||||
@@ -141,7 +143,7 @@ public abstract partial class SharedAtmosPipeLayersSystem : EntitySystem
|
||||
var toolName = Loc.GetString(toolProto.ToolName).ToLower();
|
||||
var message = Loc.GetString("atmos-pipe-layers-component-tool-missing", ("toolName", toolName));
|
||||
|
||||
_popup.PopupPredicted(message, ent, args.User);
|
||||
_popup.PopupClient(message, ent, args.User);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -217,7 +219,7 @@ public abstract partial class SharedAtmosPipeLayersSystem : EntitySystem
|
||||
var layerName = GetPipeLayerName(ent.Comp.CurrentPipeLayer);
|
||||
var message = Loc.GetString("atmos-pipe-layers-component-change-layer", ("layerName", layerName));
|
||||
|
||||
_popup.PopupPredicted(message, ent, user);
|
||||
_popup.PopupClient(message, ent, user);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Body.Components
|
||||
namespace Content.Shared.Body.Components
|
||||
{
|
||||
[RegisterComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
|
||||
public sealed partial class StomachComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
@@ -32,7 +33,7 @@ namespace Content.Server.Body.Components
|
||||
/// What solution should this stomach push reagents into, on the body?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
|
||||
public string BodySolutionName = "chemicals";
|
||||
|
||||
/// <summary>
|
||||
/// Time between reagents being ingested and them being
|
||||
26
Content.Shared/Body/Events/ApplyMetabolicMultiplierEvent.cs
Normal file
26
Content.Shared/Body/Events/ApplyMetabolicMultiplierEvent.cs
Normal 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;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Body.Organ;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Body.Systems
|
||||
namespace Content.Shared.Body.Systems
|
||||
{
|
||||
public sealed class StomachSystem : EntitySystem
|
||||
{
|
||||
@@ -19,6 +21,7 @@ namespace Content.Server.Body.Systems
|
||||
{
|
||||
SubscribeLocalEvent<StomachComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<StomachComponent, EntityUnpausedEvent>(OnUnpaused);
|
||||
SubscribeLocalEvent<StomachComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
|
||||
SubscribeLocalEvent<StomachComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
|
||||
}
|
||||
|
||||
@@ -32,6 +35,16 @@ namespace Content.Server.Body.Systems
|
||||
ent.Comp.NextUpdate += args.PausedTime;
|
||||
}
|
||||
|
||||
private void OnEntRemoved(Entity<StomachComponent> ent, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
// Make sure the removed entity was our contained solution
|
||||
if (ent.Comp.Solution is not { } solution || args.Entity != solution.Owner)
|
||||
return;
|
||||
|
||||
// Cleared our cached reference to the solution entity
|
||||
ent.Comp.Solution = null;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<StomachComponent, OrganComponent, SolutionContainerManagerComponent>();
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Burial;
|
||||
using Content.Shared.Burial.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
@@ -10,7 +9,7 @@ using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
|
||||
namespace Content.Server.Burial.Systems;
|
||||
namespace Content.Shared.Burial;
|
||||
|
||||
public sealed class BurialSystem : EntitySystem
|
||||
{
|
||||
|
||||
@@ -15,4 +15,10 @@ public sealed partial class CCVars
|
||||
|
||||
public static readonly CVarDef<int> MaxMidiLaggedBatches =
|
||||
CVarDef.Create("midi.max_lagged_batches", 8, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the max amount of characters to allow in the "Midi channel selector".
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> MidiMaxChannelNameLength =
|
||||
CVarDef.Create("midi.max_channel_name_length", 64, CVar.SERVERONLY);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed partial class CCVars
|
||||
/// some food object won't spam a user with flavors.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int>
|
||||
FlavorLimit = CVarDef.Create("flavor.limit", 10, CVar.SERVERONLY);
|
||||
FlavorLimit = CVarDef.Create("flavor.limit", 10, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
public static readonly CVarDef<string> DestinationFile =
|
||||
CVarDef.Create("autogen.destination_file", "", CVar.SERVER | CVar.SERVERONLY);
|
||||
|
||||
@@ -104,10 +104,10 @@ public sealed partial class CargoOrderConsoleComponent : Component
|
||||
public static readonly ProtoId<RadioChannelPrototype> BaseAnnouncementChannel = "Supply";
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, restricts this console from ordering and has it print slips instead
|
||||
/// The behaviour of the cargo console regarding orders
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool SlipPrinter;
|
||||
public CargoOrderConsoleMode Mode = CargoOrderConsoleMode.DirectOrder;
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the console will be able to print a slip again.
|
||||
@@ -146,6 +146,26 @@ public sealed partial class CargoOrderConsoleComponent : Component
|
||||
public TimeSpan DenySoundDelay = TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The behaviour of the cargo order console
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum CargoOrderConsoleMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Place orders directly
|
||||
/// </summary>
|
||||
DirectOrder,
|
||||
/// <summary>
|
||||
/// Print a slip to be inserted into a DirectOrder console
|
||||
/// </summary>
|
||||
PrintSlip,
|
||||
/// <summary>
|
||||
/// Transfers the order to the primary account
|
||||
/// </summary>
|
||||
SendToPrimary,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Withdraw funds from an account
|
||||
/// </summary>
|
||||
|
||||
@@ -170,7 +170,7 @@ public sealed partial class ClimbSystem : VirtualController
|
||||
|
||||
private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User))
|
||||
if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User) || !component.Vaultable)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing || !climbingComponent.CanClimb)
|
||||
|
||||
@@ -17,33 +17,46 @@ namespace Content.Shared.CombatMode.Pacification;
|
||||
[Access(typeof(PacificationSystem))]
|
||||
public sealed partial class PacifiedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, this will prevent you from disarming opponents in combat.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool DisallowDisarm = false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, this will disable combat entirely instead of only disallowing attacking living creatures and harmful things.
|
||||
/// If true, this will disable combat entirely instead of only disallowing attacking living creatures and harmful things.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool DisallowAllCombat = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// When attempting attack against the same entity multiple times,
|
||||
/// don't spam popups every frame and instead have a cooldown.
|
||||
/// When attempting attack against the same entity multiple times,
|
||||
/// don't spam popups every frame and instead have a cooldown.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan PopupCooldown = TimeSpan.FromSeconds(3.0);
|
||||
|
||||
/// <summary>
|
||||
/// Time at which the next popup can be shown.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[AutoPausedField]
|
||||
public TimeSpan? NextPopupTime = null;
|
||||
|
||||
/// <summary>
|
||||
/// The last entity attacked, used for popup purposes (avoid spam)
|
||||
/// The last entity attacked, used for popup purposes (avoid spam)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? LastAttackedEntity = null;
|
||||
|
||||
/// <summary>
|
||||
/// The alert to show to owners of this component.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<AlertPrototype> PacifiedAlert = "Pacified";
|
||||
|
||||
// Prevent cheat clients from using this to identify thieves and players that cannot fight back.
|
||||
// This should not matter for prediction reasons since it only blocks user input.
|
||||
public override bool SendOnlyToOwner => true;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.EntityEffects.EffectConditions;
|
||||
namespace Content.Shared.EntityEffects.EffectConditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition for if the entity is or isn't wearing internals.
|
||||
|
||||
@@ -303,15 +303,7 @@ namespace Content.Shared.FixedPoint
|
||||
|
||||
public readonly int CompareTo(FixedPoint2 other)
|
||||
{
|
||||
if (other.Value > Value)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (other.Value < Value)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
return Value.CompareTo(other.Value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
namespace Content.Shared.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Defines settings for a ghost role raffle.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Ghost.Roles.Raffles;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Content.Shared.HealthExaminable;
|
||||
public sealed partial class HealthExaminableComponent : Component
|
||||
{
|
||||
public List<FixedPoint2> Thresholds = new()
|
||||
{ FixedPoint2.New(10), FixedPoint2.New(25), FixedPoint2.New(50), FixedPoint2.New(75) };
|
||||
{ FixedPoint2.New(8), FixedPoint2.New(15), FixedPoint2.New(30), FixedPoint2.New(50), FixedPoint2.New(75), FixedPoint2.New(100), FixedPoint2.New(200) };
|
||||
|
||||
[DataField(required: true)]
|
||||
public HashSet<ProtoId<DamageTypePrototype>> ExaminableTypes = default!;
|
||||
|
||||
@@ -38,7 +38,13 @@ public abstract partial class SharedInstrumentComponent : Component
|
||||
/// Component that indicates that musical instrument was activated (ui opened).
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ActiveInstrumentComponent : Component;
|
||||
[AutoGenerateComponentState(true)]
|
||||
public sealed partial class ActiveInstrumentComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
[AutoNetworkedField]
|
||||
public MidiTrack?[] Tracks = [];
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class InstrumentComponentState : ComponentState
|
||||
@@ -144,3 +150,72 @@ public enum InstrumentUiKey
|
||||
{
|
||||
Key,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the MIDI channels on an instrument.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class InstrumentSetChannelsEvent : EntityEventArgs
|
||||
{
|
||||
public NetEntity Uid { get; }
|
||||
public MidiTrack?[] Tracks { get; set; }
|
||||
|
||||
public InstrumentSetChannelsEvent(NetEntity uid, MidiTrack?[] tracks)
|
||||
{
|
||||
Uid = uid;
|
||||
Tracks = tracks;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single midi track with the track name, instrument name and bank instrument name extracted.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MidiTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// The first specified Track Name
|
||||
/// </summary>
|
||||
public string? TrackName;
|
||||
/// <summary>
|
||||
/// The first specified instrument name
|
||||
/// </summary>
|
||||
public string? InstrumentName;
|
||||
|
||||
/// <summary>
|
||||
/// The first program change resolved to the name.
|
||||
/// </summary>
|
||||
public string? ProgramName;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Track Name: {TrackName}; Instrument Name: {InstrumentName}; Program Name: {ProgramName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates the fields based on the limit inputted into this method.
|
||||
/// </summary>
|
||||
public void TruncateFields(int limit)
|
||||
{
|
||||
if (InstrumentName != null)
|
||||
InstrumentName = Truncate(InstrumentName, limit);
|
||||
|
||||
if (TrackName != null)
|
||||
TrackName = Truncate(TrackName, limit);
|
||||
|
||||
if (ProgramName != null)
|
||||
ProgramName = Truncate(ProgramName, limit);
|
||||
}
|
||||
|
||||
private const string Postfix = "…";
|
||||
// TODO: Make a general method to use in RT? idk if we have that.
|
||||
private string Truncate(string input, int limit)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || limit <= 0 || input.Length <= limit)
|
||||
return input;
|
||||
|
||||
var truncatedLength = limit - Postfix.Length;
|
||||
|
||||
return input.Substring(0, truncatedLength) + Postfix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Server.Light.Components;
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Device that allows user to quikly change bulbs in <see cref="PoweredLightComponent"/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
@@ -25,11 +26,32 @@ public sealed partial class SunShadowCycleComponent : Component
|
||||
/// Time to have each direction applied. Will lerp from the current value to the next one.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public List<(float Ratio, Vector2 Direction, float Alpha)> Directions = new()
|
||||
public List<SunShadowCycleDirection> Directions = new()
|
||||
{
|
||||
(0f, new Vector2(0f, 3f), 0f),
|
||||
(0.25f, new Vector2(-3f, -0.1f), 0.5f),
|
||||
(0.5f, new Vector2(0f, -3f), 0.8f),
|
||||
(0.75f, new Vector2(3f, -0.1f), 0.5f),
|
||||
new SunShadowCycleDirection(0f, new Vector2(0f, 3f), 0f),
|
||||
new SunShadowCycleDirection(0.25f, new Vector2(-3f, -0.1f), 0.5f),
|
||||
new SunShadowCycleDirection(0.5f, new Vector2(0f, -3f), 0.8f),
|
||||
new SunShadowCycleDirection(0.75f, new Vector2(3f, -0.1f), 0.5f),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public partial record struct SunShadowCycleDirection
|
||||
{
|
||||
[DataField]
|
||||
public float Ratio;
|
||||
|
||||
[DataField]
|
||||
public Vector2 Direction;
|
||||
|
||||
[DataField]
|
||||
public float Alpha;
|
||||
|
||||
public SunShadowCycleDirection(float ratio, Vector2 direction, float alpha)
|
||||
{
|
||||
Ratio = ratio;
|
||||
Direction = direction;
|
||||
Alpha = alpha;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Content.Server.Medical.Components;
|
||||
namespace Content.Shared.Medical.Cryogenics;
|
||||
|
||||
/// <summary>
|
||||
/// Tracking component for an enabled cryo pod (which periodically tries to inject chemicals in the occupant, if one exists)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Server.Medical.Components;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Database;
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Returns whether an entity is a member of a faction.
|
||||
/// </summary>
|
||||
public bool IsMember(Entity<NpcFactionMemberComponent?> ent, string faction)
|
||||
public bool IsMember(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] string faction)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
@@ -85,7 +85,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
/// Returns whether an entity is a member of any listed faction.
|
||||
/// If the list is empty this returns false.
|
||||
/// </summary>
|
||||
public bool IsMemberOfAny(Entity<NpcFactionMemberComponent?> ent, IEnumerable<ProtoId<NpcFactionPrototype>> factions)
|
||||
public bool IsMemberOfAny(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] IEnumerable<ProtoId<NpcFactionPrototype>> factions)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
@@ -102,7 +102,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Adds this entity to the particular faction.
|
||||
/// </summary>
|
||||
public void AddFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true)
|
||||
public void AddFaction(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] string faction, bool dirty = true)
|
||||
{
|
||||
if (!_proto.HasIndex<NpcFactionPrototype>(faction))
|
||||
{
|
||||
@@ -121,7 +121,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Adds this entity to the particular faction.
|
||||
/// </summary>
|
||||
public void AddFactions(Entity<NpcFactionMemberComponent?> ent, HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true)
|
||||
public void AddFactions(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true)
|
||||
{
|
||||
ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent);
|
||||
|
||||
@@ -143,7 +143,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Removes this entity from the particular faction.
|
||||
/// </summary>
|
||||
public void RemoveFaction(Entity<NpcFactionMemberComponent?> ent, string faction, bool dirty = true)
|
||||
public void RemoveFaction(Entity<NpcFactionMemberComponent?> ent, [ForbidLiteral] string faction, bool dirty = true)
|
||||
{
|
||||
if (!_proto.HasIndex<NpcFactionPrototype>(faction))
|
||||
{
|
||||
@@ -202,7 +202,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
return GetNearbyFactions(ent, range, ent.Comp.FriendlyFactions);
|
||||
}
|
||||
|
||||
private IEnumerable<EntityUid> GetNearbyFactions(EntityUid entity, float range, HashSet<ProtoId<NpcFactionPrototype>> factions)
|
||||
private IEnumerable<EntityUid> GetNearbyFactions(EntityUid entity, float range, [ForbidLiteral] HashSet<ProtoId<NpcFactionPrototype>> factions)
|
||||
{
|
||||
var xform = Transform(entity);
|
||||
foreach (var ent in _lookup.GetEntitiesInRange<NpcFactionMemberComponent>(_xform.GetMapCoordinates((entity, xform)), range))
|
||||
@@ -228,12 +228,12 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
return ent.Comp.Factions.Overlaps(other.Comp.Factions) || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions);
|
||||
}
|
||||
|
||||
public bool IsFactionFriendly(string target, string with)
|
||||
public bool IsFactionFriendly([ForbidLiteral] string target, [ForbidLiteral] string with)
|
||||
{
|
||||
return _factions[target].Friendly.Contains(with) && _factions[with].Friendly.Contains(target);
|
||||
}
|
||||
|
||||
public bool IsFactionFriendly(string target, Entity<NpcFactionMemberComponent?> with)
|
||||
public bool IsFactionFriendly([ForbidLiteral] string target, Entity<NpcFactionMemberComponent?> with)
|
||||
{
|
||||
if (!Resolve(with, ref with.Comp, false))
|
||||
return false;
|
||||
@@ -242,12 +242,12 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
with.Comp.FriendlyFactions.Contains(target);
|
||||
}
|
||||
|
||||
public bool IsFactionHostile(string target, string with)
|
||||
public bool IsFactionHostile([ForbidLiteral] string target, [ForbidLiteral] string with)
|
||||
{
|
||||
return _factions[target].Hostile.Contains(with) && _factions[with].Hostile.Contains(target);
|
||||
}
|
||||
|
||||
public bool IsFactionHostile(string target, Entity<NpcFactionMemberComponent?> with)
|
||||
public bool IsFactionHostile([ForbidLiteral] string target, Entity<NpcFactionMemberComponent?> with)
|
||||
{
|
||||
if (!Resolve(with, ref with.Comp, false))
|
||||
return false;
|
||||
@@ -256,7 +256,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
with.Comp.HostileFactions.Contains(target);
|
||||
}
|
||||
|
||||
public bool IsFactionNeutral(string target, string with)
|
||||
public bool IsFactionNeutral([ForbidLiteral] string target, [ForbidLiteral] string with)
|
||||
{
|
||||
return !IsFactionFriendly(target, with) && !IsFactionHostile(target, with);
|
||||
}
|
||||
@@ -264,7 +264,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Makes the source faction friendly to the target faction, 1-way.
|
||||
/// </summary>
|
||||
public void MakeFriendly(string source, string target)
|
||||
public void MakeFriendly([ForbidLiteral] string source, [ForbidLiteral] string target)
|
||||
{
|
||||
if (!_factions.TryGetValue(source, out var sourceFaction))
|
||||
{
|
||||
@@ -286,7 +286,7 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Makes the source faction hostile to the target faction, 1-way.
|
||||
/// </summary>
|
||||
public void MakeHostile(string source, string target)
|
||||
public void MakeHostile([ForbidLiteral] string source, [ForbidLiteral] string target)
|
||||
{
|
||||
if (!_factions.TryGetValue(source, out var sourceFaction))
|
||||
{
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
namespace Content.Server.Nutrition.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
[RegisterComponent]
|
||||
namespace Content.Shared.Nutrition.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class FlavorProfileComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Localized string containing the base flavor of this entity.
|
||||
/// </summary>
|
||||
[DataField("flavors")]
|
||||
[DataField]
|
||||
public HashSet<string> Flavors { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Reagent IDs to ignore when processing this flavor profile. Defaults to nutriment.
|
||||
/// </summary>
|
||||
[DataField("ignoreReagents")]
|
||||
[DataField]
|
||||
public HashSet<string> IgnoreReagents { get; private set; } = new()
|
||||
{
|
||||
"Nutriment",
|
||||
"Vitamin",
|
||||
"Protein"
|
||||
"Protein",
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Nutrition.Components;
|
||||
namespace Content.Shared.Nutrition.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))]
|
||||
public sealed partial class FoodComponent : Component
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
namespace Content.Server.Nutrition.Components;
|
||||
namespace Content.Shared.Nutrition.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Component that denotes a piece of clothing that blocks the mouth or otherwise prevents eating & drinking.
|
||||
@@ -9,7 +9,7 @@ namespace Content.Server.Nutrition.Components;
|
||||
/// In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of
|
||||
/// masks), then this component might become redundant.
|
||||
/// </remarks>
|
||||
[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem), typeof(IngestionBlockerSystem))]
|
||||
[RegisterComponent, Access(typeof(FoodSystem), typeof(SharedDrinkSystem), typeof(IngestionBlockerSystem))]
|
||||
public sealed partial class IngestionBlockerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Nutrition.Components
|
||||
{
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedUtensilSystem))]
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(UtensilSystem))]
|
||||
public sealed partial class UtensilComponent : Component
|
||||
{
|
||||
[DataField("types")]
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using Content.Server.Nutrition.Components;
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Nutrition.EntitySystems;
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Deals with flavor profiles when you eat something.
|
||||
@@ -1,21 +1,17 @@
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.Prototypes;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Nutrition.EntitySystems;
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
|
||||
{
|
||||
@@ -26,7 +22,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -126,7 +122,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
|
||||
if (start.Comp.FoodLayers.Count >= start.Comp.MaxLayers && !elementIndexed.Final || start.Comp.Finished)
|
||||
{
|
||||
if (user is not null)
|
||||
_popup.PopupEntity(Loc.GetString("food-sequence-no-space"), start, user.Value);
|
||||
_popup.PopupClient(Loc.GetString("food-sequence-no-space"), start, user.Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Server.Inventory;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Stack;
|
||||
using System.Linq;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Organ;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands.Components;
|
||||
@@ -21,42 +18,38 @@ using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Robust.Server.GameObjects;
|
||||
using Content.Shared.Whitelist;
|
||||
using Content.Shared.Destructible;
|
||||
|
||||
namespace Content.Server.Nutrition.EntitySystems;
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles feeding attempts both on yourself and on the target.
|
||||
/// </summary>
|
||||
public sealed class FoodSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly BodySystem _body = default!;
|
||||
[Dependency] private readonly SharedBodySystem _body = default!;
|
||||
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reaction = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly StackSystem _stack = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedStackSystem _stack = default!;
|
||||
[Dependency] private readonly StomachSystem _stomach = default!;
|
||||
[Dependency] private readonly UtensilSystem _utensil = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
@@ -69,7 +62,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
|
||||
// TODO add InteractNoHandEvent for entities like mice.
|
||||
// run after openable for wrapped/peelable foods
|
||||
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(ServerInventorySystem) });
|
||||
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
|
||||
SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
|
||||
SubscribeLocalEvent<FoodComponent, GetVerbsEvent<AlternativeVerb>>(AddEatVerb);
|
||||
SubscribeLocalEvent<FoodComponent, ConsumeDoAfterEvent>(OnDoAfter);
|
||||
@@ -116,7 +109,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
if (HasComp<UnremoveableComponent>(food))
|
||||
return (false, false);
|
||||
|
||||
if (_openable.IsClosed(food, user))
|
||||
if (_openable.IsClosed(food, user, predicted: true))
|
||||
return (false, true);
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out _, out var foodSolution))
|
||||
@@ -135,7 +128,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
// Check for used storage on the food item
|
||||
if (TryComp<StorageComponent>(food, out var storageState) && storageState.Container.ContainedEntities.Any())
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
|
||||
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
@@ -144,7 +137,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
{
|
||||
if (itemSlots.Slots.Any(slot => slot.Value.HasItem))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
|
||||
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
|
||||
return (false, true);
|
||||
}
|
||||
}
|
||||
@@ -153,7 +146,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
|
||||
if (GetUsesRemaining(food, foodComp) <= 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
|
||||
_popup.PopupClient(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
|
||||
DeleteAndSpawnTrash(foodComp, food, user);
|
||||
return (false, true);
|
||||
}
|
||||
@@ -171,7 +164,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
|
||||
{
|
||||
var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
|
||||
_popup.PopupEntity(message, user, user);
|
||||
_popup.PopupClient(message, user, user);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
@@ -268,7 +261,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
if (stomachToUse == null)
|
||||
{
|
||||
_solutionContainer.TryAddSolution(soln.Value, split);
|
||||
_popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
|
||||
_popup.PopupClient(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -283,20 +276,20 @@ public sealed class FoodSystem : EntitySystem
|
||||
var userName = Identity.Entity(args.User, EntityManager);
|
||||
_popup.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), entity.Owner, entity.Owner);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
|
||||
_popup.PopupClient(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
|
||||
|
||||
// log successful force feed
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity.Owner):food}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
|
||||
_popup.PopupClient(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
|
||||
|
||||
// log successful voluntary eating
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
|
||||
}
|
||||
|
||||
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
|
||||
_audio.PlayPredicted(entity.Comp.UseSound, args.Target.Value, args.User, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
|
||||
|
||||
// Try to break all used utensils
|
||||
foreach (var utensil in utensils)
|
||||
@@ -484,7 +477,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
// If "required" field is set, try to block eating without proper utensils used
|
||||
if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
|
||||
_popup.PopupClient(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -533,7 +526,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
RaiseLocalEvent(uid, attempt, false);
|
||||
if (attempt.Cancelled && attempt.Blocker != null && popupUid != null)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
|
||||
_popup.PopupClient(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
|
||||
uid, popupUid.Value);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -166,7 +166,7 @@ public sealed partial class OpenableSystem : EntitySystem
|
||||
/// Drinks that don't have OpenableComponent are automatically open, so it returns false.
|
||||
/// If user is not null a popup will be shown to them.
|
||||
/// </summary>
|
||||
public bool IsClosed(EntityUid uid, EntityUid? user = null, OpenableComponent? comp = null)
|
||||
public bool IsClosed(EntityUid uid, EntityUid? user = null, OpenableComponent? comp = null, bool predicted = false)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return false;
|
||||
@@ -175,7 +175,12 @@ public sealed partial class OpenableSystem : EntitySystem
|
||||
return false;
|
||||
|
||||
if (user != null)
|
||||
_popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
|
||||
{
|
||||
if (predicted)
|
||||
_popup.PopupClient(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
|
||||
else
|
||||
_popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedBodySystem _body = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
|
||||
[Dependency] private readonly FoodSystem _food = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -17,6 +38,7 @@ public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, AttemptShakeEvent>(OnAttemptShake);
|
||||
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
|
||||
}
|
||||
|
||||
protected void OnAttemptShake(Entity<DrinkComponent> entity, ref AttemptShakeEvent args)
|
||||
@@ -28,7 +50,7 @@ public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
protected void OnExamined(Entity<DrinkComponent> entity, ref ExaminedEvent args)
|
||||
{
|
||||
TryComp<OpenableComponent>(entity, out var openable);
|
||||
if (_openable.IsClosed(entity.Owner, null, openable) || !args.IsInDetailsRange || !entity.Comp.Examinable)
|
||||
if (_openable.IsClosed(entity.Owner, null, openable, true) || !args.IsInDetailsRange || !entity.Comp.Examinable)
|
||||
return;
|
||||
|
||||
var empty = IsEmpty(entity, entity.Comp);
|
||||
@@ -57,6 +79,38 @@ public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
|
||||
{
|
||||
if (entity.Owner == ev.User ||
|
||||
!ev.CanInteract ||
|
||||
!ev.CanAccess ||
|
||||
!TryComp<BodyComponent>(ev.User, out var body) ||
|
||||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
|
||||
return;
|
||||
|
||||
// Make sure the solution exists
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
|
||||
return;
|
||||
|
||||
// no drinking from living drinks, have to kill them first.
|
||||
if (_mobState.IsAlive(entity))
|
||||
return;
|
||||
|
||||
var user = ev.User;
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
TryDrink(user, user, entity.Comp, entity);
|
||||
},
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
|
||||
Text = Loc.GetString("drink-system-verb-drink"),
|
||||
Priority = 2
|
||||
};
|
||||
|
||||
ev.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
protected FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
@@ -87,4 +141,74 @@ public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
|
||||
return remainingString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to feed the drink item to the target entity
|
||||
/// </summary>
|
||||
protected bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
|
||||
{
|
||||
if (!HasComp<BodyComponent>(target))
|
||||
return false;
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
|
||||
return false;
|
||||
|
||||
if (_openable.IsClosed(item, user, predicted: true))
|
||||
return true;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
|
||||
{
|
||||
if (drink.IgnoreEmpty)
|
||||
return false;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_food.IsMouthBlocked(target, user))
|
||||
return true;
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
|
||||
return true;
|
||||
|
||||
var forceDrink = user != target;
|
||||
|
||||
if (forceDrink)
|
||||
{
|
||||
var userName = Identity.Entity(user, EntityManager);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
|
||||
|
||||
// logging
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// log voluntary drinking
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
|
||||
}
|
||||
|
||||
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
|
||||
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager,
|
||||
user,
|
||||
forceDrink ? drink.ForceFeedDelay : drink.Delay,
|
||||
new ConsumeDoAfterEvent(drink.Solution, flavors),
|
||||
eventTarget: item,
|
||||
target: target,
|
||||
used: item)
|
||||
{
|
||||
BreakOnHandChange = false,
|
||||
BreakOnMove = forceDrink,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.01f,
|
||||
DistanceThreshold = 1.0f,
|
||||
// do-after will stop if item is dropped when trying to feed someone else
|
||||
// or if the item started out in the user's own hands
|
||||
NeedHand = forceDrink || _hands.IsHolding(user, item),
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterEventArgs);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
public abstract class SharedUtensilSystem : EntitySystem
|
||||
{
|
||||
}
|
||||
73
Content.Shared/Nutrition/EntitySystems/UtensilSystem.cs
Normal file
73
Content.Shared/Nutrition/EntitySystems/UtensilSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Lathe;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Research.Components;
|
||||
@@ -20,6 +21,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedResearchSystem _research = default!;
|
||||
[Dependency] private readonly SharedLatheSystem _lathe = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameModifier = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -28,6 +30,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<TechnologyDiskComponent> ent, ref MapInitEvent args)
|
||||
@@ -55,6 +58,7 @@ public sealed class TechnologyDiskSystem : EntitySystem
|
||||
ent.Comp.Recipes = [];
|
||||
ent.Comp.Recipes.Add(_random.Pick(techs));
|
||||
Dirty(ent);
|
||||
_nameModifier.RefreshNameModifiers(ent.Owner);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(Entity<TechnologyDiskComponent> ent, ref AfterInteractEvent args)
|
||||
@@ -90,4 +94,16 @@ public sealed class TechnologyDiskSystem : EntitySystem
|
||||
}
|
||||
args.PushMarkup(message);
|
||||
}
|
||||
|
||||
private void OnRefreshNameModifiers(Entity<TechnologyDiskComponent> entity, ref RefreshNameModifiersEvent args)
|
||||
{
|
||||
if (entity.Comp.Recipes != null)
|
||||
{
|
||||
foreach (var recipe in entity.Comp.Recipes)
|
||||
{
|
||||
var proto = _protoMan.Index(recipe);
|
||||
args.AddModifier("tech-disk-name-format", extraArgs: ("technology", _lathe.GetRecipeName(proto)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ public sealed partial class BinComponent : Component
|
||||
[ViewVariables]
|
||||
public Container ItemContainer = default!;
|
||||
|
||||
/// <summary>
|
||||
/// ID of the container used to hold the items in the bin.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string ContainerId = "bin-container";
|
||||
|
||||
/// <summary>
|
||||
/// A list representing the order in which
|
||||
/// all the entities are stored in the bin.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Server.Storage.Components;
|
||||
namespace Content.Shared.Storage.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Applies an ongoing pickup area around the attached entity.
|
||||
|
||||
@@ -24,13 +24,12 @@ public sealed class BinSystem : EntitySystem
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
|
||||
public const string BinContainerId = "bin-container";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<BinComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<BinComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<BinComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
|
||||
SubscribeLocalEvent<BinComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
|
||||
SubscribeLocalEvent<BinComponent, InteractHandEvent>(OnInteractHand, before: new[] { typeof(SharedItemSystem) });
|
||||
SubscribeLocalEvent<BinComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
|
||||
@@ -45,7 +44,7 @@ public sealed class BinSystem : EntitySystem
|
||||
|
||||
private void OnStartup(EntityUid uid, BinComponent component, ComponentStartup args)
|
||||
{
|
||||
component.ItemContainer = _container.EnsureContainer<Container>(uid, BinContainerId);
|
||||
component.ItemContainer = _container.EnsureContainer<Container>(uid, component.ContainerId);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, BinComponent component, MapInitEvent args)
|
||||
@@ -66,6 +65,11 @@ public sealed class BinSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEntInserted(Entity<BinComponent> ent, ref EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
ent.Comp.Items.Add(args.Entity);
|
||||
}
|
||||
|
||||
private void OnEntRemoved(EntityUid uid, BinComponent component, EntRemovedFromContainerMessage args)
|
||||
{
|
||||
component.Items.Remove(args.Entity);
|
||||
@@ -96,7 +100,7 @@ public sealed class BinSystem : EntitySystem
|
||||
if (args.Using != null)
|
||||
{
|
||||
var canReach = args.CanAccess && args.CanInteract;
|
||||
InsertIntoBin(args.User, args.Target, (EntityUid) args.Using, component, false, canReach);
|
||||
InsertIntoBin(args.User, args.Target, (EntityUid)args.Using, component, false, canReach);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +140,6 @@ public sealed class BinSystem : EntitySystem
|
||||
return false;
|
||||
|
||||
_container.Insert(toInsert, component.ItemContainer);
|
||||
component.Items.Add(toInsert);
|
||||
Dirty(uid, component);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ public abstract partial class SharedToolSystem : EntitySystem
|
||||
EntityUid user,
|
||||
EntityUid? target,
|
||||
float doAfterDelay,
|
||||
IEnumerable<string> toolQualitiesNeeded,
|
||||
[ForbidLiteral] IEnumerable<string> toolQualitiesNeeded,
|
||||
DoAfterEvent doAfterEv,
|
||||
float fuel = 0,
|
||||
ToolComponent? toolComponent = null)
|
||||
@@ -153,7 +153,7 @@ public abstract partial class SharedToolSystem : EntitySystem
|
||||
EntityUid user,
|
||||
EntityUid? target,
|
||||
TimeSpan delay,
|
||||
IEnumerable<string> toolQualitiesNeeded,
|
||||
[ForbidLiteral] IEnumerable<string> toolQualitiesNeeded,
|
||||
DoAfterEvent doAfterEv,
|
||||
out DoAfterId? id,
|
||||
float fuel = 0,
|
||||
@@ -200,7 +200,7 @@ public abstract partial class SharedToolSystem : EntitySystem
|
||||
EntityUid user,
|
||||
EntityUid? target,
|
||||
float doAfterDelay,
|
||||
string toolQualityNeeded,
|
||||
[ForbidLiteral] string toolQualityNeeded,
|
||||
DoAfterEvent doAfterEv,
|
||||
float fuel = 0,
|
||||
ToolComponent? toolComponent = null)
|
||||
@@ -219,7 +219,7 @@ public abstract partial class SharedToolSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Whether a tool entity has the specified quality or not.
|
||||
/// </summary>
|
||||
public bool HasQuality(EntityUid uid, string quality, ToolComponent? tool = null)
|
||||
public bool HasQuality(EntityUid uid, [ForbidLiteral] string quality, ToolComponent? tool = null)
|
||||
{
|
||||
return Resolve(uid, ref tool, false) && tool.Qualities.Contains(quality);
|
||||
}
|
||||
@@ -228,7 +228,7 @@ public abstract partial class SharedToolSystem : EntitySystem
|
||||
/// Whether a tool entity has all specified qualities or not.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public bool HasAllQualities(EntityUid uid, IEnumerable<string> qualities, ToolComponent? tool = null)
|
||||
public bool HasAllQualities(EntityUid uid, [ForbidLiteral] IEnumerable<string> qualities, ToolComponent? tool = null)
|
||||
{
|
||||
return Resolve(uid, ref tool, false) && tool.Qualities.ContainsAll(qualities);
|
||||
}
|
||||
|
||||
36
Content.Shared/Toolshed/TypeParsers/FixedPoint2TypeParser.cs
Normal file
36
Content.Shared/Toolshed/TypeParsers/FixedPoint2TypeParser.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ namespace Content.Shared.Weather;
|
||||
public abstract class SharedWeatherSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] protected readonly IMapManager MapManager = default!;
|
||||
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user