Skip to content

Commit 00d4c4f

Browse files
ketufaispikinutnomdéraisonnablementlongScarKy0beck-thompsonslarticodefast
authored
Changeling voice mimicry (#43675)
* working yay * fixed the thing I had removed * there it works * i know my coding I hope * Update Resources/Textures/Interface/Actions/changeling2.rsi/meta.json Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Server/VoiceMask/VoiceMaskSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Server/VoiceMask/VoiceMaskSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Server/VoiceMask/VoiceMaskSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update Content.Server/VoiceMask/VoiceMaskSystem.cs Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Apply suggestions from code review oh god you can just do batches oh my god Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update changeling-catalog.ftl new line at EOF * Update changeling.yml same * Update changeling.yml line * Update StoreSystem.Ui.cs * Update StoreSystem.Ui.cs * Update StoreSystem.Ui.cs just reverted it to master * Apply suggestions from code review Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Update VoiceMaskSystem.cs there ya go skar * fixed the bug, yay * clean * GITHUB, DO YOUR THING * [DataField] is in the air? WRONG. * minor spelling mistake * all is well all is well sometimes * minor fix * apathy * it works * VIVE LA RÉVOLUTION * minor spelling mistake *dies* * why the fuck are high-rez vest making tests fail * stuff * NEWLINENEWLINENEWLINE * Update Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/VoiceMask/VoiceMaskSystem.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --------- Co-authored-by: nomdéraisonnablementlong <you@example.com> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: ScarKy0 <scarky0@onet.eu> Co-authored-by: beck-thompson <beck314159@hotmail.com> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
1 parent a2438de commit 00d4c4f

16 files changed

Lines changed: 194 additions & 29 deletions

File tree

Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
5252
return;
5353
}
5454

55-
_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide);
55+
_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide, cast.TitleText);
5656
}
5757

5858
protected override void Dispose(bool disposing)

Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
1818
private List<(string, string)> _verbs = new();
1919

2020
private string? _verb;
21-
2221
public VoiceMaskNameChangeWindow()
2322
{
2423
RobustXamlLoader.Load(this);
25-
2624
NameSelectorSet.OnPressed += _ =>
2725
{
2826
OnNameChange?.Invoke(NameSelector.Text);
@@ -69,13 +67,13 @@ private void AddVerb(string name, string? verb)
6967
SpeechVerbSelector.SelectId(id);
7068
}
7169

72-
public void UpdateState(string name, string? verb, bool active, bool accentHide)
70+
public void UpdateState(string name, string? verb, bool active, bool accentHide, LocId titleText)
7371
{
7472
NameSelector.Text = name;
7573
_verb = verb;
7674
ToggleButton.Pressed = active;
7775
ToggleAccentButton.Pressed = accentHide;
78-
76+
Title = Loc.GetString(titleText);
7977
for (int id = 0; id < SpeechVerbSelector.ItemCount; id++)
8078
{
8179
if (string.Equals(verb, SpeechVerbSelector.GetItemMetadata(id)))

Content.Server/VoiceMask/VoiceMaskSystem.cs

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,40 @@ public sealed partial class VoiceMaskSystem : EntitySystem
3131
[Dependency] private readonly SharedContainerSystem _container = default!;
3232
[Dependency] private readonly IdentitySystem _identity = default!;
3333

34+
/// <summary>
35+
/// The name of the client-side type that represents the user interface window.
36+
/// Used for innate voice masks, which need to be able to create their own UIs.
37+
/// </summary>
38+
private const string UiGeneratedName = "VoiceMaskBoundUserInterface";
39+
3440
// CCVar.
3541
private int _maxNameLength;
3642

3743
public override void Initialize()
3844
{
3945
base.Initialize();
46+
47+
// These events should fire in the order Innate -> Implant -> Inventory
48+
// Transform speaker name events
4049
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameInventory);
4150
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameImplant);
51+
SubscribeLocalEvent<VoiceMaskComponent, TransformSpeakerNameEvent>(OnInnateTransformSpeakerName);
52+
53+
// See identity attempt events
4254
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<SeeIdentityAttemptEvent>>(OnSeeIdentityAttemptEvent);
55+
SubscribeLocalEvent<VoiceMaskComponent, SeeIdentityAttemptEvent>(OnInnateSeeIdentityAttemptEvent);
56+
57+
// Transform speech events
58+
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeechEvent>>(OnTransformSpeechInventory, before: [typeof(AccentSystem)]);
59+
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeechEvent>>(OnTransformSpeechImplant, before: [typeof(AccentSystem)]);
60+
SubscribeLocalEvent<VoiceMaskComponent, TransformSpeechEvent>(OnTransformSpeech, before: [typeof(AccentSystem)]);
61+
62+
// Voice mask transform things
63+
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<VoiceMaskToggledEvent>>((ent, ref ev) => OnVoiceMaskToggledEvent(ent, ref ev.Args));
64+
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<VoiceMaskToggledEvent>>((ent, ref ev) => OnVoiceMaskToggledEvent(ent, ref ev.Args));
65+
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskToggledEvent>(OnVoiceMaskToggledEvent);
66+
67+
// Other events
4368
SubscribeLocalEvent<VoiceMaskComponent, ImplantImplantedEvent>(OnImplantImplantedEvent);
4469
SubscribeLocalEvent<VoiceMaskComponent, ImplantRemovedEvent>(OnImplantRemovedEventEvent);
4570
SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
@@ -49,13 +74,45 @@ public override void Initialize()
4974
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskAccentToggleMessage>(OnAccentToggle);
5075
SubscribeLocalEvent<VoiceMaskComponent, ClothingGotEquippedEvent>(OnEquip);
5176
SubscribeLocalEvent<VoiceMaskSetNameEvent>(OpenUI);
52-
SubscribeLocalEvent<VoiceMaskComponent, TransformSpeechEvent>(OnTransformSpeech, before: [typeof(AccentSystem)]);
53-
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeechEvent>>(OnTransformSpeechInventory, before: [typeof(AccentSystem)]);
54-
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeechEvent>>(OnTransformSpeechImplant, before: [typeof(AccentSystem)]);
77+
SubscribeLocalEvent<VoiceMaskComponent, MapInitEvent>(OnMapInit);
5578

5679
Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true);
5780
}
5881

82+
private void OnMapInit(Entity<VoiceMaskComponent> ent, ref MapInitEvent args)
83+
{
84+
if (!ent.Comp.IsInnate)
85+
return;
86+
87+
// all masks should be inactive on creation
88+
ent.Comp.Active = false;
89+
90+
_actions.AddAction(ent, ent.Comp.Action);
91+
_uiSystem.SetUi((ent, null), VoiceMaskUIKey.Key, new InterfaceData(UiGeneratedName));
92+
_identity.QueueIdentityUpdate(ent.Owner);
93+
}
94+
95+
/// <summary>
96+
/// Toggles this mask off it it isn't the mask turned on
97+
/// </summary>
98+
private void OnVoiceMaskToggledEvent(Entity<VoiceMaskComponent> ent, ref VoiceMaskToggledEvent args)
99+
{
100+
// we only toggle when the other mask turns on
101+
if (!args.Active)
102+
return;
103+
104+
// we don't want the entity turned on to be turned off, and there isn't any work to do if it already inactive
105+
if (ent.Owner == args.Mask || !ent.Comp.Active)
106+
return;
107+
108+
// turn it off
109+
ent.Comp.Active = false;
110+
111+
// update the
112+
UpdateUI(ent);
113+
_identity.QueueIdentityUpdate(args.Source);
114+
}
115+
59116
/// <summary>
60117
/// Hides accent if the voice mask is on and the option to block accents is on
61118
/// </summary>
@@ -77,7 +134,12 @@ private void OnTransformSpeechInventory(Entity<VoiceMaskComponent> entity, ref I
77134

78135
private void OnTransformSpeechImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeechEvent> args)
79136
{
80-
TransformSpeech(entity, args.Event);
137+
TransformSpeech(entity, args.Args);
138+
}
139+
140+
private void OnInnateTransformSpeakerName(Entity<VoiceMaskComponent> ent, ref TransformSpeakerNameEvent args)
141+
{
142+
TransformVoice(ent, args);
81143
}
82144

83145
private void OnTransformSpeakerNameInventory(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerNameEvent> args)
@@ -87,19 +149,28 @@ private void OnTransformSpeakerNameInventory(Entity<VoiceMaskComponent> entity,
87149

88150
private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeakerNameEvent> args)
89151
{
90-
TransformVoice(entity, args.Event);
152+
TransformVoice(entity, args.Args);
153+
}
154+
155+
private void OnInnateSeeIdentityAttemptEvent(Entity<VoiceMaskComponent> entity, ref SeeIdentityAttemptEvent args)
156+
{
157+
if (!entity.Comp.OverrideIdentity || !entity.Comp.Active || !entity.Comp.IsInnate)
158+
return;
159+
160+
args.NameOverride = GetCurrentVoiceName(entity);
91161
}
92162

93163
private void OnSeeIdentityAttemptEvent(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<SeeIdentityAttemptEvent> args)
94164
{
95165
if (!entity.Comp.OverrideIdentity || !entity.Comp.Active)
96166
return;
97167

98-
args.Event.NameOverride = GetCurrentVoiceName(entity);
168+
args.Args.NameOverride = GetCurrentVoiceName(entity);
99169
}
100170

101171
private void OnImplantImplantedEvent(Entity<VoiceMaskComponent> entity, ref ImplantImplantedEvent ev)
102172
{
173+
entity.Comp.Active = false;
103174
_identity.QueueIdentityUpdate(ev.Implanted);
104175
}
105176

@@ -139,7 +210,10 @@ private void OnChangeName(Entity<VoiceMaskComponent> entity, ref VoiceMaskChange
139210
}
140211

141212
var nameUpdatedEvent = new VoiceMaskNameUpdatedEvent(entity, entity.Comp.VoiceMaskName, message.Name);
142-
RaiseLocalEvent(message.Actor, ref nameUpdatedEvent);
213+
if (entity.Comp.IsInnate)
214+
RaiseLocalEvent(entity.Owner, ref nameUpdatedEvent);
215+
else
216+
RaiseLocalEvent(message.Actor, ref nameUpdatedEvent);
143217

144218
entity.Comp.VoiceMaskName = message.Name;
145219
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}");
@@ -154,14 +228,20 @@ private void OnToggle(Entity<VoiceMaskComponent> entity, ref VoiceMaskToggleMess
154228
_popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-toggle"), entity, args.Actor);
155229
entity.Comp.Active = !entity.Comp.Active;
156230

231+
var ev = new VoiceMaskToggledEvent(entity.Owner, args.Actor, entity.Comp.Active);
232+
RaiseLocalEvent(entity.Owner, ev);
233+
157234
// Update identity because of possible name override
158235
_identity.QueueIdentityUpdate(args.Actor);
236+
237+
UpdateUI(entity);
159238
}
160239

161240
private void OnAccentToggle(Entity<VoiceMaskComponent> entity, ref VoiceMaskAccentToggleMessage args)
162241
{
163242
_popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-accent-toggle"), entity, args.Actor);
164243
entity.Comp.AccentHide = !entity.Comp.AccentHide;
244+
UpdateUI(entity);
165245
}
166246
#endregion
167247

@@ -171,6 +251,7 @@ private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEqu
171251
if (_lock.IsLocked(uid))
172252
return;
173253

254+
component.Active = false;
174255
_actions.AddAction(args.Wearer, ref component.ActionEntity, component.Action, uid);
175256
}
176257

@@ -191,7 +272,7 @@ private void OpenUI(VoiceMaskSetNameEvent ev)
191272
private void UpdateUI(Entity<VoiceMaskComponent> entity)
192273
{
193274
if (_uiSystem.HasUi(entity, VoiceMaskUIKey.Key))
194-
_uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide));
275+
_uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide, entity.Comp.TitleText));
195276
}
196277
#endregion
197278

@@ -211,3 +292,4 @@ private void TransformVoice(Entity<VoiceMaskComponent> entity, TransformSpeakerN
211292
}
212293
#endregion
213294
}
295+

Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Content.Shared.Interaction.Events;
55
using Content.Shared.Mobs;
66
using Content.Shared.Store;
7+
using Content.Shared.VoiceMask;
78

89
namespace Content.Shared.Implants;
910

@@ -16,6 +17,7 @@ public void InitializeRelay()
1617
SubscribeLocalEvent<ImplantedComponent, TransformSpeakerNameEvent>(RelayToImplantEvent);
1718
SubscribeLocalEvent<ImplantedComponent, TransformSpeechEvent>(RelayToImplantEvent);
1819
SubscribeLocalEvent<ImplantedComponent, SeeIdentityAttemptEvent>(RelayToImplantEvent);
20+
SubscribeLocalEvent<ImplantedComponent, VoiceMaskToggledEvent>(RelayToImplantEvent);
1921

2022
// Ref relays, for when you need to write to the event!
2123
SubscribeLocalEvent<ImplantedComponent, CurrencyInsertAttemptEvent>(RefRelayToImplantEvent);
@@ -57,7 +59,7 @@ private void RefRelayToImplantEvent<T>(Entity<ImplantedComponent> entity, ref T
5759
RaiseLocalEvent(implant, relayEv);
5860
}
5961

60-
args = relayEv.Event;
62+
args = relayEv.Args;
6163
}
6264
}
6365

@@ -66,13 +68,13 @@ private void RefRelayToImplantEvent<T>(Entity<ImplantedComponent> entity, ref T
6668
/// </summary>
6769
public sealed class ImplantRelayEvent<T> where T : notnull
6870
{
69-
public T Event;
71+
public T Args;
7072

7173
public readonly EntityUid ImplantedEntity;
7274

7375
public ImplantRelayEvent(T ev, EntityUid implantedEntity)
7476
{
75-
Event = ev;
77+
Args = ev;
7678
ImplantedEntity = implantedEntity;
7779
}
7880
}

Content.Shared/Inventory/InventorySystem.Relay.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public void InitializeRelay()
5959
SubscribeLocalEvent<InventoryComponent, IsUnequippingTargetAttemptEvent>(RelayInventoryEvent);
6060
SubscribeLocalEvent<InventoryComponent, ChameleonControllerOutfitSelectedEvent>(RelayInventoryEvent);
6161
SubscribeLocalEvent<InventoryComponent, VoiceMaskNameUpdatedEvent>(RelayInventoryEvent);
62+
SubscribeLocalEvent<InventoryComponent, VoiceMaskToggledEvent>(RelayInventoryEvent);
6263
SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);
6364
SubscribeLocalEvent<InventoryComponent, StoodEvent>(RelayInventoryEvent);
6465
SubscribeLocalEvent<InventoryComponent, DownedEvent>(RelayInventoryEvent);

Content.Shared/Store/SharedStoreSystem.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ public override void Initialize()
3535
SubscribeLocalEvent<RemoteStoreComponent, GetStoreEvent>(OnGetStore);
3636
SubscribeLocalEvent<RemoteStoreComponent, ImplantRelayEvent<GetStoreEvent>>((x, ref y) =>
3737
{
38-
var ev = y.Event;
38+
var ev = y.Args;
3939
OnGetStore(x, ref ev);
40-
y.Event = ev;
40+
y.Args = ev;
4141
});
4242
SubscribeLocalEvent<RemoteStoreComponent, ImplantRelayEvent<CurrencyInsertAttemptEvent>>(OnImplantInsertAttempt);
4343
SubscribeLocalEvent<StoreComponent, IntrinsicStoreActionEvent>(OnIntrinsicStoreAction);
@@ -56,15 +56,15 @@ private void OnGetStore(Entity<RemoteStoreComponent> entity, ref GetStoreEvent a
5656

5757
private void OnImplantInsertAttempt(Entity<RemoteStoreComponent> implant, ref ImplantRelayEvent<CurrencyInsertAttemptEvent> args)
5858
{
59-
var ev = args.Event;
59+
var ev = args.Args;
6060

6161
// Only allow insertion if the person implanted is doing the action.
6262
if (ev.User == ev.Target)
6363
ev.TargetOverride = implant;
6464
else
6565
ev.Cancel();
6666

67-
args.Event = ev;
67+
args.Args = ev;
6868
}
6969

7070
private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args)

Content.Shared/Trigger/Systems/TriggerOnMobstateChangeSystem.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent c
3131

3232
private void OnMobStateRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<MobStateChangedEvent> args)
3333
{
34-
if (!component.MobState.Contains(args.Event.NewMobState))
34+
if (!component.MobState.Contains(args.Args.NewMobState))
3535
return;
3636

37-
Trigger.Trigger(uid, component.TargetMobstateEntity ? args.ImplantedEntity : args.Event.Origin, component.KeyOut);
37+
Trigger.Trigger(uid, component.TargetMobstateEntity ? args.ImplantedEntity : args.Args.Origin, component.KeyOut);
3838
}
3939

4040
/// <summary>
@@ -56,13 +56,13 @@ private void OnSuicide(EntityUid uid, TriggerOnMobstateChangeComponent component
5656

5757
private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<SuicideEvent> args)
5858
{
59-
if (args.Event.Handled)
59+
if (args.Args.Handled)
6060
return;
6161

6262
if (!component.PreventSuicide)
6363
return;
6464

65-
_popup.PopupClient(Loc.GetString("suicide-prevented"), args.Event.Victim);
66-
args.Event.Handled = true;
65+
_popup.PopupClient(Loc.GetString("suicide-prevented"), args.Args.Victim);
66+
args.Args.Handled = true;
6767
}
6868
}

Content.Shared/VoiceMask/SharedVoiceMaskSystem.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Content.Shared.Inventory;
12
using Robust.Shared.Serialization;
23

34
namespace Content.Shared.VoiceMask;
@@ -15,13 +16,15 @@ public sealed class VoiceMaskBuiState : BoundUserInterfaceState
1516
public readonly string? Verb;
1617
public readonly bool Active;
1718
public readonly bool AccentHide;
19+
public readonly LocId TitleText;
1820

19-
public VoiceMaskBuiState(string name, string? verb, bool active, bool accentHide)
21+
public VoiceMaskBuiState(string name, string? verb, bool active, bool accentHide, LocId titleText)
2022
{
2123
Name = name;
2224
Verb = verb;
2325
Active = active;
2426
AccentHide = accentHide;
27+
TitleText = titleText;
2528
}
2629
}
2730

@@ -61,3 +64,19 @@ public sealed class VoiceMaskToggleMessage : BoundUserInterfaceMessage;
6164
/// </summary>
6265
[Serializable, NetSerializable]
6366
public sealed class VoiceMaskAccentToggleMessage : BoundUserInterfaceMessage;
67+
68+
/// <summary>
69+
/// Fired when a voice mask is turned on.
70+
/// </summary>
71+
/// <param name=="Mask">The voice mask that was turned on</param>
72+
/// <param name=="Source">The entity that owns the voice mask</param>
73+
/// <param name=="Active">The new value of the voice mask</param>
74+
public sealed class VoiceMaskToggledEvent(EntityUid mask, EntityUid source, bool active) : IInventoryRelayEvent
75+
{
76+
public EntityUid Mask = mask;
77+
public EntityUid Source = source;
78+
79+
public bool Active = active;
80+
81+
SlotFlags IInventoryRelayEvent.TargetSlots => SlotFlags.WITHOUT_POCKET;
82+
}

0 commit comments

Comments
 (0)