Merge branch 'master' into fix_highlight

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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