CatStation/Content.Server/Traitor/Uplink/UplinkSystem.cs
SlamBamActionman 0d616cf6df
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>
2026-04-04 19:46:43 +00:00

206 lines
7.1 KiB
C#

using System.Linq;
using Content.Server.PDA.Ringer;
using Content.Server.Store.Systems;
using Content.Server.StoreDiscount.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Implants;
using Content.Shared.Inventory;
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;
public sealed class UplinkSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[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";
/// <summary>
/// Adds an uplink to the target
/// </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>
/// <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,
EntityUid? storeEntity = null,
bool giveDiscounts = false,
bool bindToPda = false)
{
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 false;
var ev = new GenerateUplinkCodeEvent();
RaiseLocalEvent(storeEntity.Value, ref ev);
if (ev.Code == null)
{
QueueDel(storeEntity);
return false;
}
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;
}
/// <summary>
/// Configure TC for the uplink
/// </summary>
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 storeComp = EnsureComp<StoreComponent>(store);
storeComp.AccountOwner = mind;
storeComp.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } },
store,
storeComp);
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: mind,
Store: store,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(mind, store, storeComp)
.ToArray());
RaiseLocalEvent(ref uplinkInitializedEvent);
}
/// <summary>
/// Implant an uplink as a fallback measure if the traitor had no PDA
/// </summary>
public bool TryImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
{
if (!_proto.Resolve<ListingPrototype>(FallbackUplinkCatalog, out var catalog))
return false;
if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost))
return false;
if (balance < cost) // Can't use Math functions on FixedPoint2
balance = 0;
else
balance = balance - cost;
var implant = _subdermalImplant.AddImplant(user, FallbackUplinkImplant);
if (!HasComp<StoreComponent>(implant))
{
Log.Error($"Implant does not have the store component {implant}");
return false;
}
SetUplink(user, implant.Value, balance, giveDiscounts);
return true;
}
/// <summary>
/// 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 Entity<RemoteStoreComponent>? FindUplinkTarget(EntityUid user)
{
// Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
while (containerSlotEnumerator.MoveNext(out var containerSlot))
{
var pdaUid = containerSlot.ContainedEntity;
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<StoreComponent>(item) && TryComp<RemoteStoreComponent>(item, out var remote))
return (item, remote);
}
return null;
}
}
public enum AddUplinkResult
{
Pda,
Implant,
Failure,
}