Merge branch 'master' into fix_highlight
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user