mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
Universal uplink codes (#38712)
* Halfway commit * Finishing commit, maybe? * Fix test, update ringtone UI to look nicer * Fix command, add failsafe * Documentation * Can we just mark ValidatePrototypeId as obsolete please * I'm too tired and my bones hurt * Change uplink code generation method * Move RingerAccessUplinkComponent to Server, because cheat clients could make use of it * Fix uplink implant changes, review changes, repair broken serialization. * cleanup and master merge * forgot the Linq * Linqd * Our store system is pretty goddamn awful wow --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
parent
ef3929be86
commit
0d616cf6df
19 changed files with 525 additions and 114 deletions
5
Content.Client/Store/StoreSystem.cs
Normal file
5
Content.Client/Store/StoreSystem.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Store;
|
||||
|
||||
namespace Content.Client.Store;
|
||||
|
||||
public sealed class StoreSystem : SharedStoreSystem;
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.Store;
|
||||
using JetBrains.Annotations;
|
||||
using System.Linq;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
|
|
@ -11,6 +10,7 @@ namespace Content.Client.Store.Ui;
|
|||
public sealed class StoreBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private IPrototypeManager _prototypeManager = default!;
|
||||
private readonly StoreSystem _storeSystem = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private StoreMenu? _menu;
|
||||
|
|
@ -23,6 +23,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
|||
|
||||
public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
_storeSystem = EntMan.System<StoreSystem>();
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
|
|
@ -30,12 +31,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
|
|||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<StoreMenu>();
|
||||
if (EntMan.TryGetComponent<StoreComponent>(Owner, out var store))
|
||||
_menu.Title = Loc.GetString(store.Name);
|
||||
if (_storeSystem.TryGetStore(Owner, out var store))
|
||||
_menu.Title = Loc.GetString(store.Value.Comp.Name);
|
||||
|
||||
_menu.OnListingButtonPressed += (_, listing) =>
|
||||
{
|
||||
SendMessage(new StoreBuyListingMessage(listing.ID));
|
||||
SendMessage(new StoreBuyListingMessage(listing.ID, EntMan.GetNetEntity(Owner)));
|
||||
};
|
||||
|
||||
_menu.OnCategoryButtonPressed += (_, category) =>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public sealed class StoreTests : GameTest
|
|||
- type: entity
|
||||
name: InventoryPdaDummy
|
||||
id: InventoryPdaDummy
|
||||
parent: BasePDA
|
||||
parent: [BasePDA, StorePresetUplink]
|
||||
components:
|
||||
- type: Clothing
|
||||
QuickEquip: false
|
||||
|
|
@ -89,10 +89,13 @@ public sealed class StoreTests : GameTest
|
|||
mindSystem.TransferTo(mind, human, mind: mind);
|
||||
|
||||
FixedPoint2 originalBalance = 20;
|
||||
uplinkSystem.AddUplink(human, originalBalance, null, true);
|
||||
uplinkSystem.AddUplink(human, originalBalance, out var notes, pda, null, true);
|
||||
|
||||
var storeComponent = entManager.GetComponent<StoreComponent>(pda);
|
||||
var discountComponent = entManager.GetComponent<StoreDiscountComponent>(pda);
|
||||
var remote = entManager.GetComponent<RemoteStoreComponent>(pda);
|
||||
var storeEnt = remote.Store;
|
||||
Assert.That(storeEnt.HasValue);
|
||||
var storeComponent = entManager.GetComponent<StoreComponent>(storeEnt.Value);
|
||||
var discountComponent = entManager.GetComponent<StoreDiscountComponent>(storeEnt.Value);
|
||||
Assert.That(
|
||||
discountComponent.Discounts,
|
||||
Has.Exactly(6).Items,
|
||||
|
|
@ -138,7 +141,7 @@ public sealed class StoreTests : GameTest
|
|||
Assert.That(plainDiscountedCost.Value, Is.LessThan(prototypeCost.Value), "Expected discounted cost to be lower then prototype cost.");
|
||||
|
||||
|
||||
var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID){Actor = human};
|
||||
var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID, null){Actor = human};
|
||||
server.EntMan.EventBus.RaiseLocalEvent(pda, buyMsg);
|
||||
|
||||
var newBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using Robust.Shared.Random;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Codewords;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
|
|
@ -148,32 +149,22 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||
private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing)
|
||||
{
|
||||
var pda = _uplink.FindUplinkTarget(traitor);
|
||||
Note[]? code = null;
|
||||
|
||||
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink add");
|
||||
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
|
||||
var uplinked = _uplink.AddUplink(traitor, startingBalance, out var code, pda, giveDiscounts: true, bindToPda: false);
|
||||
|
||||
if (pda is not null && uplinked)
|
||||
if (code != null && uplinked == AddUplinkResult.Pda)
|
||||
{
|
||||
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA");
|
||||
// Codes are only generated if the uplink is a PDA
|
||||
var ev = new GenerateUplinkCodeEvent();
|
||||
RaiseLocalEvent(pda.Value, ref ev);
|
||||
|
||||
if (ev.Code is { } generatedCode)
|
||||
{
|
||||
code = generatedCode;
|
||||
|
||||
// If giveUplink is false the uplink code part is omitted
|
||||
briefing = string.Format("{0}\n{1}",
|
||||
briefing,
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
return (code, briefing);
|
||||
}
|
||||
|
||||
Log.Error($"MakeTraitor {ToPrettyString(traitor)} failed to generate an uplink code on {ToPrettyString(pda)}.");
|
||||
// If giveUplink is false the uplink code part is omitted
|
||||
briefing = string.Format("{0}\n{1}",
|
||||
briefing,
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
return (code, briefing);
|
||||
}
|
||||
else if (pda is null && uplinked)
|
||||
|
||||
if (uplinked == AddUplinkResult.Implant)
|
||||
{
|
||||
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant");
|
||||
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
|
||||
|
|
@ -183,6 +174,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||
Log.Error($"MakeTraitor failed on {ToPrettyString(traitor)} - No uplink could be added");
|
||||
}
|
||||
|
||||
|
||||
return (null, briefing);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Access.Systems;
|
||||
using Content.Server.AlertLevel;
|
||||
using Content.Server.CartridgeLoader;
|
||||
|
|
@ -17,6 +18,7 @@ using Content.Shared.Light;
|
|||
using Content.Shared.Light.EntitySystems;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.PDA.Ringer;
|
||||
using Content.Shared.Store.Components;
|
||||
using Content.Shared.VoiceMask;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
|
|
@ -189,7 +191,7 @@ namespace Content.Server.PDA
|
|||
|
||||
var address = GetDeviceNetAddress(uid);
|
||||
var hasInstrument = HasComp<InstrumentComponent>(uid);
|
||||
var showUplink = HasComp<UplinkComponent>(uid) && IsUnlocked(uid);
|
||||
var showUplink = TryGetUnlockedStore(uid, out _);
|
||||
|
||||
UpdateStationName(uid, pda);
|
||||
UpdateAlertLevel(uid, pda);
|
||||
|
|
@ -274,8 +276,19 @@ namespace Content.Server.PDA
|
|||
return;
|
||||
|
||||
// check if its locked again to prevent malicious clients opening locked uplinks
|
||||
if (HasComp<UplinkComponent>(uid) && IsUnlocked(uid))
|
||||
_store.ToggleUi(msg.Actor, uid);
|
||||
if (TryGetUnlockedStore(uid, out var store))
|
||||
{
|
||||
if (store != uid)
|
||||
{
|
||||
if (TryComp<RemoteStoreComponent>(uid, out var remoteStore))
|
||||
remoteStore.Store = store;
|
||||
_store.ToggleUi(msg.Actor, store.Value, remoteAccess: uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
_store.ToggleUi(msg.Actor, store.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaLockUplinkMessage msg)
|
||||
|
|
@ -285,14 +298,24 @@ namespace Content.Server.PDA
|
|||
|
||||
if (TryComp<RingerUplinkComponent>(uid, out var uplink))
|
||||
{
|
||||
if (TryComp<RemoteStoreComponent>(uid, out var remoteStore))
|
||||
remoteStore.Store = null;
|
||||
_ringer.LockUplink((uid, uplink));
|
||||
UpdatePdaUi(uid, pda);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsUnlocked(EntityUid uid)
|
||||
/// <summary>
|
||||
/// Returns the currently unlocked store, if there is one.
|
||||
/// </summary>
|
||||
private bool TryGetUnlockedStore(EntityUid uid, [NotNullWhen(true)] out EntityUid? store)
|
||||
{
|
||||
return !TryComp<RingerUplinkComponent>(uid, out var uplink) || uplink.Unlocked;
|
||||
store = null;
|
||||
if (!TryComp<RingerUplinkComponent>(uid, out var uplink) || !uplink.Unlocked || uplink.TargetStore == null)
|
||||
return false;
|
||||
|
||||
store = uplink.TargetStore;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateStationName(EntityUid uid, PdaComponent pda)
|
||||
|
|
|
|||
24
Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs
Normal file
24
Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using Content.Shared.PDA;
|
||||
|
||||
namespace Content.Server.PDA.Ringer;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the store UI when a PDA's ringtone is set to the secret code.
|
||||
/// Traitors are told the code when greeted.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(RingerSystem))]
|
||||
public sealed partial class RingerAccessUplinkComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Notes to set ringtone to in order to lock or unlock the uplink.
|
||||
/// Set via GenerateUplinkCodeEvent.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Note[]? Code;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the uplink store can only be opened with the given entity.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? BoundEntity;
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.PDA.Ringer;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.PDA.Ringer;
|
||||
|
||||
|
|
@ -14,15 +16,35 @@ public sealed class RingerSystem : SharedRingerSystem
|
|||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public static Note[] AllowedNotes =
|
||||
{
|
||||
Note.C,
|
||||
Note.D,
|
||||
Note.E,
|
||||
Note.F,
|
||||
Note.G,
|
||||
Note.A,
|
||||
Note.B
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Stores the serialized version of any ringtone that can be excluded from new ringtone generations.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public readonly HashSet<int> ReservedSerializedRingtones = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RingerComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RingerComponent, CurrencyInsertAttemptEvent>(OnCurrencyInsert);
|
||||
|
||||
SubscribeLocalEvent<RingerUplinkComponent, GenerateUplinkCodeEvent>(OnGenerateUplinkCode);
|
||||
SubscribeLocalEvent<RingerAccessUplinkComponent, GenerateUplinkCodeEvent>(OnGenerateUplinkCode);
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(CleanupReserved);
|
||||
|
||||
InitialSetup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -30,7 +52,11 @@ public sealed class RingerSystem : SharedRingerSystem
|
|||
/// </summary>
|
||||
private void OnMapInit(Entity<RingerComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
UpdateRingerRingtone(ent, GenerateRingtone());
|
||||
var ringtone = GenerateRingtone();
|
||||
|
||||
ringtone ??= new Note[RingtoneLength] { Note.A, Note.A, Note.A, Note.A, Note.A, Note.A }; // Fallback
|
||||
|
||||
UpdateRingerRingtone(ent, ringtone);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -53,71 +79,209 @@ public sealed class RingerSystem : SharedRingerSystem
|
|||
/// <summary>
|
||||
/// Handles the <see cref="GenerateUplinkCodeEvent"/> for generating an uplink code.
|
||||
/// </summary>
|
||||
private void OnGenerateUplinkCode(Entity<RingerUplinkComponent> ent, ref GenerateUplinkCodeEvent ev)
|
||||
private void OnGenerateUplinkCode(Entity<RingerAccessUplinkComponent> ent, ref GenerateUplinkCodeEvent ev)
|
||||
{
|
||||
var code = GenerateRingtone();
|
||||
var code = GenerateRingtone(true, true);
|
||||
|
||||
// Set the code on the component
|
||||
ent.Comp.Code = code;
|
||||
|
||||
// Return the code via the event
|
||||
ev.Code = code;
|
||||
}
|
||||
|
||||
private void InitialSetup()
|
||||
{
|
||||
ReservedSerializedRingtones.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null)
|
||||
{
|
||||
if (!TryComp<RingerUplinkComponent>(uid, out var uplink))
|
||||
return false;
|
||||
|
||||
if (!HasComp<StoreComponent>(uid))
|
||||
return false;
|
||||
|
||||
// Wasn't generated yet
|
||||
if (uplink.Code is null)
|
||||
return false;
|
||||
|
||||
// On the server, we always check if the code matches
|
||||
if (!uplink.Code.SequenceEqual(ringtone))
|
||||
if (!TryMatchRingtoneToStore(ringtone, out var store, uid))
|
||||
return false;
|
||||
|
||||
uplink.TargetStore = store;
|
||||
|
||||
return ToggleUplinkInternal((uid, uplink));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random ringtone using the C pentatonic scale.
|
||||
/// Generates a random ringtone using the C major scale.
|
||||
/// </summary>
|
||||
/// <param name="excludeReserved">Exclude any ringtone registered to ReservedSerializedRingtones.</param>
|
||||
/// <param name="reserveRingtone">Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true.</param>
|
||||
/// <returns>An array of Notes representing the ringtone.</returns>
|
||||
/// <remarks>The logic for this is on the Server so that we don't get a different result on the Client every time.</remarks>
|
||||
private Note[] GenerateRingtone()
|
||||
private Note[]? GenerateRingtone(bool excludeReserved = false, bool reserveRingtone = false)
|
||||
{
|
||||
// Default to using C pentatonic so it at least sounds not terrible.
|
||||
return GenerateRingtone(new[]
|
||||
{
|
||||
Note.C,
|
||||
Note.D,
|
||||
Note.E,
|
||||
Note.G,
|
||||
Note.A
|
||||
});
|
||||
// Default to using C major so it at least sounds not terrible.
|
||||
return GenerateRingtone(AllowedNotes, excludeReserved, reserveRingtone);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random ringtone using the specified notes.
|
||||
/// </summary>
|
||||
/// <param name="notes">The notes to choose from when generating the ringtone.</param>
|
||||
/// <param name="excludeReserved">Exclude any ringtone registered to ReservedSerializedRingtones.</param>
|
||||
/// <param name="reserveRingtone">Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true.</param>
|
||||
/// <returns>An array of Notes representing the ringtone.</returns>
|
||||
/// <remarks>The logic for this is on the Server so that we don't get a different result on the Client every time.</remarks>
|
||||
private Note[] GenerateRingtone(Note[] notes)
|
||||
private Note[]? GenerateRingtone(Note[] notes, bool excludeReserved = false, bool reserveRingtone = false)
|
||||
{
|
||||
var ringtone = new Note[RingtoneLength];
|
||||
var excludedRingtones = excludeReserved ? ReservedSerializedRingtones.ToArray() : null;
|
||||
|
||||
var maxPow = Math.Pow(notes.Length, RingtoneLength);
|
||||
if (maxPow > int.MaxValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var generatedRingtone = NextIntInRangeButExclude(0, Convert.ToInt32(maxPow) - 1, excludedRingtones);
|
||||
|
||||
if (!TryDeserializeRingtone(notes, generatedRingtone, out var ringtone))
|
||||
return null;
|
||||
|
||||
if (excludeReserved && reserveRingtone)
|
||||
ReservedSerializedRingtones.Add(generatedRingtone);
|
||||
|
||||
return ringtone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a ringtone, representing it as an Int32.
|
||||
/// </summary>
|
||||
/// <param name="allowedNotes">The array of notes used to generate the ringtone.</param>
|
||||
/// <param name="ringtone">The ringtone which needs to be serialized.</param>
|
||||
/// <param name="serializedRingtone">The ringtone in a serialized format.</param>
|
||||
/// <returns>Whether the ringtone could be serialized or not.</returns>
|
||||
private bool TrySerializeRingtone(Note[] allowedNotes, Note[] ringtone, [NotNullWhen(true)] out int? serializedRingtone)
|
||||
{
|
||||
var noteLength = allowedNotes.Length;
|
||||
|
||||
// The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk.
|
||||
// If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting.
|
||||
var maxPow = Math.Pow(noteLength, ringtone.Length);
|
||||
if (maxPow > int.MaxValue)
|
||||
{
|
||||
serializedRingtone = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var serializationValue = 0;
|
||||
|
||||
for (var i = 0; i < ringtone.Length; i++)
|
||||
{
|
||||
var pow = Math.Pow(noteLength, i);
|
||||
var index = Array.IndexOf(allowedNotes, ringtone[i]);
|
||||
if (index == -1)
|
||||
{
|
||||
serializedRingtone = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
serializationValue += Convert.ToInt32(pow) * index;
|
||||
}
|
||||
|
||||
serializedRingtone = serializationValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a serialized ringtone into a Note array.
|
||||
/// </summary>
|
||||
/// <param name="allowedNotes">The array of notes used to generate the ringtone.</param>
|
||||
/// <param name="serializedRingtone">The ringtone in a serialized format.</param>
|
||||
/// <param name="ringtone">The ringtone resulting from the deserialization.</param>
|
||||
/// <returns>Whether the ringtone could be deserialized or not.</returns>
|
||||
private bool TryDeserializeRingtone(Note[] allowedNotes, int serializedRingtone, [NotNullWhen(true)] out Note[]? ringtone)
|
||||
{
|
||||
var noteLength = allowedNotes.Length;
|
||||
ringtone = new Note[RingtoneLength];
|
||||
|
||||
// The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk.
|
||||
// If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting.
|
||||
var maxPow = Math.Pow(noteLength, RingtoneLength);
|
||||
if (maxPow > int.MaxValue)
|
||||
{
|
||||
ringtone = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < RingtoneLength; i++)
|
||||
{
|
||||
ringtone[i] = _random.Pick(notes);
|
||||
var pow = Math.Pow(noteLength, RingtoneLength - 1 - i);
|
||||
var powInt = Convert.ToInt32(pow);
|
||||
var val = serializedRingtone / powInt;
|
||||
if (!AllowedNotes.TryGetValue(val, out var note))
|
||||
{
|
||||
ringtone = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
ringtone[RingtoneLength - 1 - i] = note;
|
||||
serializedRingtone -= val * powInt;
|
||||
}
|
||||
|
||||
return ringtone;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the store entity that has the matching ringer access.
|
||||
/// </summary>
|
||||
/// <param name="notes">Notes from the ringer.</param>
|
||||
/// <param name="store">The store entity, if there is one.</param>
|
||||
/// <param name="ringer">The entity providing the code.</param>
|
||||
public bool TryMatchRingtoneToStore(Note[] notes, [NotNullWhen(true)] out EntityUid? store, EntityUid? ringer = null)
|
||||
{
|
||||
var query = EntityQueryEnumerator<RingerAccessUplinkComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
if (comp.Code != null && notes.SequenceEqual(comp.Code))
|
||||
{
|
||||
if (comp.BoundEntity != null && comp.BoundEntity != ringer)
|
||||
break;
|
||||
|
||||
store = uid;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
store = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CleanupReserved(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
ReservedSerializedRingtones.Clear();
|
||||
}
|
||||
|
||||
private int NextIntInRangeButExclude(int start, int end, int[]? excludes)
|
||||
{
|
||||
excludes ??= new int[0];
|
||||
Array.Sort(excludes);
|
||||
var rangeLength = end - start - excludes.Length;
|
||||
var randomInt = _random.Next(rangeLength) + start;
|
||||
|
||||
for (var i = 0; i < excludes.Length; i++)
|
||||
{
|
||||
if (excludes[i] > randomInt)
|
||||
{
|
||||
return randomInt;
|
||||
}
|
||||
|
||||
randomInt++;
|
||||
}
|
||||
|
||||
return randomInt;
|
||||
}
|
||||
|
||||
public void SetBoundUplinkEntity(Entity<RingerAccessUplinkComponent> entity, EntityUid? targetEntity)
|
||||
{
|
||||
entity.Comp.BoundEntity = targetEntity;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,16 @@ public sealed partial class StoreSystem
|
|||
SubscribeLocalEvent<StoreComponent, StoreRequestWithdrawMessage>(OnRequestWithdraw);
|
||||
SubscribeLocalEvent<StoreComponent, StoreRequestRefundMessage>(OnRequestRefund);
|
||||
SubscribeLocalEvent<StoreComponent, RefundEntityDeletedEvent>(OnRefundEntityDeleted);
|
||||
SubscribeLocalEvent<RemoteStoreComponent, StoreRequestUpdateInterfaceMessage>((e, c, ev) =>
|
||||
RemoteStoreRelay((e, c), ev));
|
||||
SubscribeLocalEvent<RemoteStoreComponent, StoreBuyListingMessage>((e, c, ev) =>
|
||||
RemoteStoreRelay((e, c), ev));
|
||||
SubscribeLocalEvent<RemoteStoreComponent, StoreRequestWithdrawMessage>((e, c, ev) =>
|
||||
RemoteStoreRelay((e, c), ev));
|
||||
SubscribeLocalEvent<RemoteStoreComponent, StoreRequestRefundMessage>((e, c, ev) =>
|
||||
RemoteStoreRelay((e, c), ev));
|
||||
SubscribeLocalEvent<RemoteStoreComponent, RefundEntityDeletedEvent>((e, c, ev) =>
|
||||
RemoteStoreRelay((e, c), ev));
|
||||
}
|
||||
|
||||
private void OnRefundEntityDeleted(Entity<StoreComponent> ent, ref RefundEntityDeletedEvent args)
|
||||
|
|
@ -48,21 +58,34 @@ public sealed partial class StoreSystem
|
|||
ent.Comp.BoughtEntities.Remove(args.Uid);
|
||||
}
|
||||
|
||||
private void RemoteStoreRelay(Entity<RemoteStoreComponent> entity, object ev)
|
||||
{
|
||||
if (entity.Comp.Store == null || !TryComp<StoreComponent>(entity.Comp.Store, out var store))
|
||||
return;
|
||||
|
||||
RaiseLocalEvent(entity.Comp.Store.Value, ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the store Ui open and closed
|
||||
/// </summary>
|
||||
/// <param name="user">the person doing the toggling</param>
|
||||
/// <param name="storeEnt">the store being toggled</param>
|
||||
/// <param name="component"></param>
|
||||
public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null)
|
||||
/// <param name="remoteAccess">The entity remotely accessing the store, if any.</param>
|
||||
/// <param name="remoteComponent">The remote access component, if any.</param>
|
||||
public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null, EntityUid? remoteAccess = null, RemoteStoreComponent? remoteComponent = null)
|
||||
{
|
||||
if (!Resolve(storeEnt, ref component))
|
||||
return;
|
||||
|
||||
if (remoteAccess != null && !Resolve(remoteAccess.Value, ref remoteComponent) && remoteComponent!.Store != storeEnt)
|
||||
return;
|
||||
|
||||
if (!TryComp<ActorComponent>(user, out var actor))
|
||||
return;
|
||||
|
||||
if (!_ui.TryToggleUi(storeEnt, StoreUiKey.Key, actor.PlayerSession))
|
||||
if (!_ui.TryToggleUi(remoteAccess != null ? remoteAccess.Value : storeEnt, StoreUiKey.Key, actor.PlayerSession))
|
||||
return;
|
||||
|
||||
UpdateUserInterface(user, storeEnt, component);
|
||||
|
|
@ -114,9 +137,27 @@ public sealed partial class StoreSystem
|
|||
var showFooter = HasComp<RingerUplinkComponent>(store);
|
||||
|
||||
var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed);
|
||||
UpdateRemoteStores(store, state);
|
||||
_ui.SetUiState(store, StoreUiKey.Key, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates any remote store connections to a specific store.
|
||||
/// </summary>
|
||||
/// <param name="store">The store being updated.</param>
|
||||
/// <param name="state">The state being applied.</param>
|
||||
public void UpdateRemoteStores(EntityUid store, StoreUpdateState state)
|
||||
{
|
||||
var query = EntityQueryEnumerator<RemoteStoreComponent, UserInterfaceComponent>();
|
||||
while (query.MoveNext(out var uid, out var remote, out var ui))
|
||||
{
|
||||
if (remote.Store != store)
|
||||
continue;
|
||||
|
||||
_ui.SetUiState((uid, ui), StoreUiKey.Key, state);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRequestUpdate(EntityUid uid, StoreComponent component, StoreRequestUpdateInterfaceMessage args)
|
||||
{
|
||||
UpdateUserInterface(args.Actor, GetEntity(args.Entity), component);
|
||||
|
|
@ -284,7 +325,8 @@ public sealed partial class StoreSystem
|
|||
$"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _proto)}\" from {ToPrettyString(uid)}{logExtraInfo}.");
|
||||
|
||||
listing.PurchaseAmount++; //track how many times something has been purchased
|
||||
_audio.PlayEntity(component.BuySuccessSound, msg.Actor, uid); //cha-ching!
|
||||
if (msg.SoundSource != null && GetEntity(msg.SoundSource) != null)
|
||||
_audio.PlayEntity(component.BuySuccessSound, msg.Actor, GetEntity(msg.SoundSource.Value)); //cha-ching!
|
||||
|
||||
var buyFinished = new StoreBuyFinishedEvent
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,11 +15,7 @@ using Robust.Shared.Utility;
|
|||
|
||||
namespace Content.Server.Store.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Manages general interactions with a store and different entities,
|
||||
/// getting listings for stores, and interfacing with the store UI.
|
||||
/// </summary>
|
||||
public sealed partial class StoreSystem : EntitySystem
|
||||
public sealed partial class StoreSystem : SharedStoreSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
|
@ -95,23 +91,23 @@ public sealed partial class StoreSystem : EntitySystem
|
|||
|
||||
private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach)
|
||||
if (args.Handled || !args.CanReach || args.Target is not { } target)
|
||||
return;
|
||||
|
||||
if (!TryComp<StoreComponent>(args.Target, out var store))
|
||||
if (!TryGetStore(target, out var store))
|
||||
return;
|
||||
|
||||
var ev = new CurrencyInsertAttemptEvent(args.User, args.Target.Value, args.Used, store);
|
||||
RaiseLocalEvent(args.Target.Value, ev);
|
||||
var ev = new CurrencyInsertAttemptEvent(args.User, target, args.Used, store.Value.Comp);
|
||||
RaiseLocalEvent(target, ev);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
if (!TryAddCurrency((uid, component), (args.Target.Value, store)))
|
||||
if (!TryAddCurrency((uid, component), (store.Value, store.Value.Comp)))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target));
|
||||
_popup.PopupEntity(msg, args.Target.Value, args.User);
|
||||
var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", target));
|
||||
_popup.PopupEntity(msg, target, args.User);
|
||||
}
|
||||
|
||||
private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,13 @@ public sealed class AddUplinkCommand : LocalizedEntityCommands
|
|||
}
|
||||
|
||||
// Finally add uplink
|
||||
if (!_uplinkSystem.AddUplink(user, 20, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted))
|
||||
var result = _uplinkSystem.AddUplink(user, 20, out var code, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted);
|
||||
|
||||
if (code != null && result == AddUplinkResult.Pda)
|
||||
shell.WriteLine(Loc.GetString("add-uplink-command-success-pda", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
else if (result == AddUplinkResult.Implant)
|
||||
shell.WriteLine(Loc.GetString("add-uplink-command-success-implant"));
|
||||
else if (result == AddUplinkResult.Failure)
|
||||
shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using Content.Server.PDA.Ringer;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Server.StoreDiscount.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
|
|
@ -9,6 +10,7 @@ using Content.Shared.Mind;
|
|||
using Content.Shared.PDA;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink;
|
||||
|
|
@ -21,7 +23,9 @@ public sealed class UplinkSystem : EntitySystem
|
|||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly RingerSystem _ringer = default!;
|
||||
|
||||
public static readonly EntProtoId<StoreComponent> TraitorUplinkStore = "StorePresetRemoteUplink";
|
||||
public static readonly ProtoId<CurrencyPrototype> TelecrystalCurrencyPrototype = "Telecrystal";
|
||||
private static readonly EntProtoId FallbackUplinkImplant = "UplinkImplant";
|
||||
private static readonly ProtoId<ListingPrototype> FallbackUplinkCatalog = "UplinkUplinkImplanter";
|
||||
|
|
@ -31,28 +35,72 @@ public sealed class UplinkSystem : EntitySystem
|
|||
/// </summary>
|
||||
/// <param name="user">The person who is getting the uplink</param>
|
||||
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
|
||||
/// <param name="code">The code which was generated, if any.</param>
|
||||
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
|
||||
/// <param name="storeEntity">The entity that will have the store in it.</param>
|
||||
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
|
||||
/// <returns>Whether or not the uplink was added successfully</returns>
|
||||
public bool AddUplink(
|
||||
/// <param name="bindToPda">Binds the uplink to the specific uplink entity.</param>
|
||||
/// <returns>Whether the uplink was added successfully to a PDA, implant or not at all.</returns>
|
||||
public AddUplinkResult AddUplink(
|
||||
EntityUid user,
|
||||
FixedPoint2 balance,
|
||||
out Note[]? code,
|
||||
EntityUid? uplinkEntity = null,
|
||||
bool giveDiscounts = false)
|
||||
EntityUid? storeEntity = null,
|
||||
bool giveDiscounts = false,
|
||||
bool bindToPda = false)
|
||||
{
|
||||
// Try to find target item if none passed
|
||||
code = null;
|
||||
|
||||
if (TryAddEntityUplink(user, balance, out var generatedCode, uplinkEntity, storeEntity, giveDiscounts, bindToPda))
|
||||
{
|
||||
code = generatedCode;
|
||||
return AddUplinkResult.Pda;
|
||||
}
|
||||
|
||||
if (TryImplantUplink(user, balance, giveDiscounts))
|
||||
{
|
||||
return AddUplinkResult.Implant;
|
||||
}
|
||||
|
||||
return AddUplinkResult.Failure;
|
||||
}
|
||||
|
||||
public bool TryAddEntityUplink(
|
||||
EntityUid user,
|
||||
FixedPoint2 balance,
|
||||
out Note[]? code,
|
||||
EntityUid? uplinkEntity,
|
||||
EntityUid? storeEntity,
|
||||
bool giveDiscounts = false,
|
||||
bool bindToPda = false)
|
||||
{
|
||||
code = null;
|
||||
|
||||
storeEntity ??= Spawn(TraitorUplinkStore, MapCoordinates.Nullspace);
|
||||
uplinkEntity ??= FindUplinkTarget(user);
|
||||
|
||||
if (uplinkEntity == null)
|
||||
return ImplantUplink(user, balance, giveDiscounts);
|
||||
return false;
|
||||
|
||||
EnsureComp<UplinkComponent>(uplinkEntity.Value);
|
||||
var ev = new GenerateUplinkCodeEvent();
|
||||
RaiseLocalEvent(storeEntity.Value, ref ev);
|
||||
|
||||
SetUplink(user, uplinkEntity.Value, balance, giveDiscounts);
|
||||
if (ev.Code == null)
|
||||
{
|
||||
QueueDel(storeEntity);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO add BUI. Currently can't be done outside of yaml -_-
|
||||
// ^ What does this even mean?
|
||||
code = ev.Code;
|
||||
|
||||
if (bindToPda)
|
||||
{
|
||||
var accessComp = EnsureComp<RingerAccessUplinkComponent>(storeEntity.Value);
|
||||
_ringer.SetBoundUplinkEntity((storeEntity.Value, accessComp), uplinkEntity.Value);
|
||||
}
|
||||
|
||||
SetUplink(user, storeEntity.Value, uplinkEntity.Value, balance, giveDiscounts);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -60,25 +108,35 @@ public sealed class UplinkSystem : EntitySystem
|
|||
/// <summary>
|
||||
/// Configure TC for the uplink
|
||||
/// </summary>
|
||||
private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts)
|
||||
private void SetUplink(EntityUid user, EntityUid store, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts)
|
||||
{
|
||||
SetUplink(user, store, balance, giveDiscounts);
|
||||
var remote = EnsureComp<RemoteStoreComponent>(uplink);
|
||||
remote.Store = store;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure TC for the uplink
|
||||
/// </summary>
|
||||
private void SetUplink(EntityUid user, EntityUid store, FixedPoint2 balance, bool giveDiscounts)
|
||||
{
|
||||
if (!_mind.TryGetMind(user, out var mind, out _))
|
||||
return;
|
||||
|
||||
var store = EnsureComp<StoreComponent>(uplink);
|
||||
var storeComp = EnsureComp<StoreComponent>(store);
|
||||
|
||||
store.AccountOwner = mind;
|
||||
storeComp.AccountOwner = mind;
|
||||
|
||||
store.Balance.Clear();
|
||||
storeComp.Balance.Clear();
|
||||
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } },
|
||||
uplink,
|
||||
store);
|
||||
store,
|
||||
storeComp);
|
||||
|
||||
var uplinkInitializedEvent = new StoreInitializedEvent(
|
||||
TargetUser: mind,
|
||||
Store: uplink,
|
||||
Store: store,
|
||||
UseDiscounts: giveDiscounts,
|
||||
Listings: _store.GetAvailableListings(mind, uplink, store)
|
||||
Listings: _store.GetAvailableListings(mind, store, storeComp)
|
||||
.ToArray());
|
||||
RaiseLocalEvent(ref uplinkInitializedEvent);
|
||||
}
|
||||
|
|
@ -86,7 +144,7 @@ public sealed class UplinkSystem : EntitySystem
|
|||
/// <summary>
|
||||
/// Implant an uplink as a fallback measure if the traitor had no PDA
|
||||
/// </summary>
|
||||
private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
|
||||
public bool TryImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
|
||||
{
|
||||
if (!_proto.Resolve<ListingPrototype>(FallbackUplinkCatalog, out var catalog))
|
||||
return false;
|
||||
|
|
@ -115,7 +173,7 @@ public sealed class UplinkSystem : EntitySystem
|
|||
/// Finds the entity that can hold an uplink for a user.
|
||||
/// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.)
|
||||
/// </summary>
|
||||
public EntityUid? FindUplinkTarget(EntityUid user)
|
||||
public Entity<RemoteStoreComponent>? FindUplinkTarget(EntityUid user)
|
||||
{
|
||||
// Try to find PDA in inventory
|
||||
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
|
||||
|
|
@ -124,18 +182,25 @@ public sealed class UplinkSystem : EntitySystem
|
|||
{
|
||||
var pdaUid = containerSlot.ContainedEntity;
|
||||
|
||||
if (HasComp<PdaComponent>(pdaUid) && HasComp<StoreComponent>(pdaUid))
|
||||
return pdaUid;
|
||||
if (HasComp<StoreComponent>(pdaUid) && TryComp<RemoteStoreComponent>(pdaUid, out var remote))
|
||||
return (pdaUid.Value, remote);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check hands
|
||||
foreach (var item in _handsSystem.EnumerateHeld(user))
|
||||
{
|
||||
if (HasComp<PdaComponent>(item) && HasComp<StoreComponent>(item))
|
||||
return item;
|
||||
if (HasComp<StoreComponent>(item) && TryComp<RemoteStoreComponent>(item, out var remote))
|
||||
return (item, remote);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum AddUplinkResult
|
||||
{
|
||||
Pda,
|
||||
Implant,
|
||||
Failure,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,21 @@ using Robust.Shared.GameStates;
|
|||
namespace Content.Shared.PDA.Ringer;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the store UI when the ringstone is set to the secret code.
|
||||
/// Makes a PDA able to open store UIs when the ringtone is set to a secret code.
|
||||
/// Traitors are told the code when greeted.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedRingerSystem))]
|
||||
public sealed partial class RingerUplinkComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Notes to set ringtone to in order to lock or unlock the uplink.
|
||||
/// Set via GenerateUplinkCodeEvent.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Note[]? Code;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show the toggle uplink button in PDA settings.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Unlocked;
|
||||
|
||||
/// <summary>
|
||||
/// The store which the ringer is targetting.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? TargetStore;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ public abstract class SharedRingerSystem : EntitySystem
|
|||
return;
|
||||
|
||||
ent.Comp.Unlocked = false;
|
||||
ent.Comp.TargetStore = null;
|
||||
UI.CloseUi(ent.Owner, StoreUiKey.Key);
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +250,10 @@ public abstract class SharedRingerSystem : EntitySystem
|
|||
|
||||
// Close store UI if we're locking
|
||||
if (!ent.Comp.Unlocked)
|
||||
{
|
||||
ent.Comp.TargetStore = null;
|
||||
UI.CloseUi(ent.Owner, StoreUiKey.Key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
17
Content.Shared/Store/Components/RemoteStoreComponent.cs
Normal file
17
Content.Shared/Store/Components/RemoteStoreComponent.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Store.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component manages a store which players can use to purchase different listings
|
||||
/// through the ui. The currency, listings, and categories are defined in yaml.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class RemoteStoreComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The store which is currently being targetted for remote opening
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? Store;
|
||||
}
|
||||
59
Content.Shared/Store/SharedStoreSystem.cs
Normal file
59
Content.Shared/Store/SharedStoreSystem.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Store.Components;
|
||||
|
||||
namespace Content.Shared.Store;
|
||||
|
||||
/// <summary>
|
||||
/// Manages general interactions with a store and different entities,
|
||||
/// getting listings for stores, and interfacing with the store UI.
|
||||
/// </summary>
|
||||
public abstract partial class SharedStoreSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly EntityQuery<StoreComponent> StoreQuery = default!;
|
||||
[Dependency] protected readonly EntityQuery<RemoteStoreComponent> RemoteStoreQuery = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find a store connected to this entity.
|
||||
/// First checking for a <see cref="StoreComponent"/> on this entity,
|
||||
/// then checking for a <see cref="RemoteStoreComponent"/> to find a remotely connected store.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity we're checking for an attached store on</param>
|
||||
/// <param name="store">Store entity we're returning.</param>
|
||||
/// <returns>True if a store was found.</returns>
|
||||
public bool TryGetStore(Entity<RemoteStoreComponent?> entity, [NotNullWhen(true)] out Entity<StoreComponent>? store)
|
||||
{
|
||||
store = GetStore(entity);
|
||||
return store != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find a store connected to this entity.
|
||||
/// First checking for a <see cref="StoreComponent"/> on this entity,
|
||||
/// then checking for a <see cref="RemoteStoreComponent"/> to find a remotely connected store.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity we're checking for an attached store on</param>
|
||||
/// <returns>The store entity and component if found.</returns>
|
||||
public Entity<StoreComponent>? GetStore(Entity<RemoteStoreComponent?> entity)
|
||||
{
|
||||
if (StoreQuery.TryComp(entity, out var storeComp))
|
||||
return (entity, storeComp);
|
||||
|
||||
return GetRemoteStore(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find a remote store connected to this entity.
|
||||
/// Checking for a <see cref="RemoteStoreComponent"/> with an attached store entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity we're checking for an attached store on</param>
|
||||
/// <returns>The store entity and component if found.</returns>
|
||||
public Entity<StoreComponent>? GetRemoteStore(Entity<RemoteStoreComponent?> entity)
|
||||
{
|
||||
if (RemoteStoreQuery.Resolve(entity, ref entity.Comp, false)
|
||||
&& entity.Comp.Store != null
|
||||
&& StoreQuery.TryComp(entity.Comp.Store, out var storeComp))
|
||||
return (entity.Comp.Store.Value, storeComp);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,9 +37,10 @@ public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessa
|
|||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StoreBuyListingMessage(ProtoId<ListingPrototype> listing) : BoundUserInterfaceMessage
|
||||
public sealed class StoreBuyListingMessage(ProtoId<ListingPrototype> listing, NetEntity? soundSource) : BoundUserInterfaceMessage
|
||||
{
|
||||
public ProtoId<ListingPrototype> Listing = listing;
|
||||
public NetEntity? SoundSource = soundSource;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
|
|
|||
|
|
@ -5,4 +5,6 @@ add-uplink-command-completion-1 = Username (defaults to self)
|
|||
add-uplink-command-completion-2 = Uplink uid (default to PDA)
|
||||
add-uplink-command-completion-3 = Is uplink discount enabled
|
||||
add-uplink-command-error-1 = Selected player doesn't control any entity
|
||||
add-uplink-command-error-2 = Failed to add uplink to the player
|
||||
add-uplink-command-error-2 = Failed to add uplink to the player
|
||||
add-uplink-command-success-pda = Uplink added to player PDA with code {$code}
|
||||
add-uplink-command-success-implant = Uplink added to player as an implant
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- type: entity
|
||||
abstract: true
|
||||
parent: [ BaseItem, StorePresetUplink ] #PDA's have uplinks so they have to inherit the data.
|
||||
parent: [ BaseItem ]
|
||||
id: BasePDA
|
||||
name: PDA
|
||||
description: Personal Data Assistant.
|
||||
|
|
@ -107,6 +107,7 @@
|
|||
type: InstrumentBoundUserInterface
|
||||
enum.HealthAnalyzerUiKey.Key:
|
||||
type: HealthAnalyzerBoundUserInterface
|
||||
- type: RemoteStore # PDAs can access uplinks so they need this component.
|
||||
- type: Tag
|
||||
tags:
|
||||
- DoorBumpOpener
|
||||
|
|
|
|||
|
|
@ -51,3 +51,10 @@
|
|||
- ChangelingAbilities
|
||||
currencyWhitelist:
|
||||
- ChangelingDNA
|
||||
|
||||
- type: entity
|
||||
parent: StorePresetUplink
|
||||
id: StorePresetRemoteUplink
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: RingerAccessUplink
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue