Traitor activation fix for missing PDA (#30359)

* Implant the uplink if no PDA is found

* comments

* tidy up loose ends

* Whoops usually I start with the namespace, how did I forget it, shame shame

* Consistent data type for starting TC balance, misc changes

* Implant briefing, guidebook

* Update AutoTraitor, add uplink, codeword and briefing parameters to TraitorRuleComponent,  no pda for reinforcements

* engine 5c0ce43

* pass pda to AddUplink

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* nicer string handling

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* case typo 1

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* case typo 2

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* case typo 3

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* minor layout changes

* removed redundant implant check

* minor cleanup

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
Errant
2024-10-18 14:55:43 +02:00
committed by GitHub
parent 338781c243
commit 23e4f81b30
11 changed files with 247 additions and 127 deletions

View File

@@ -1,4 +1,5 @@
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Content.Shared.FixedPoint;
using Content.Shared.NPC.Prototypes; using Content.Shared.NPC.Prototypes;
using Content.Shared.Random; using Content.Shared.Random;
using Content.Shared.Roles; using Content.Shared.Roles;
@@ -31,6 +32,24 @@ public sealed partial class TraitorRuleComponent : Component
[DataField] [DataField]
public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations"; public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
/// <summary>
/// Give this traitor an Uplink on spawn.
/// </summary>
[DataField]
public bool GiveUplink = true;
/// <summary>
/// Give this traitor the codewords.
/// </summary>
[DataField]
public bool GiveCodewords = true;
/// <summary>
/// Give this traitor a briefing in chat.
/// </summary>
[DataField]
public bool GiveBriefing = true;
public int TotalTraitors => TraitorMinds.Count; public int TotalTraitors => TraitorMinds.Count;
public string[] Codewords = new string[3]; public string[] Codewords = new string[3];
@@ -68,5 +87,5 @@ public sealed partial class TraitorRuleComponent : Component
/// The amount of TC traitors start with. /// The amount of TC traitors start with.
/// </summary> /// </summary>
[DataField] [DataField]
public int StartingBalance = 20; public FixedPoint2 StartingBalance = 20;
} }

View File

@@ -7,6 +7,7 @@ using Content.Server.PDA.Ringer;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.Traitor.Uplink; using Content.Server.Traitor.Uplink;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components; using Content.Shared.GameTicking.Components;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.NPC.Systems; using Content.Shared.NPC.Systems;
@@ -75,38 +76,46 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return codewords; return codewords;
} }
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true) public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
{ {
//Grab the mind if it wasn't provided //Grab the mind if it wasn't provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
return false; return false;
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords))); var briefing = "";
if (component.GiveCodewords)
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values); var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
// Uplink code will go here if applicable, but we still need the variable if there aren't any
Note[]? code = null; Note[]? code = null;
if (giveUplink)
if (component.GiveUplink)
{ {
// Calculate the amount of currency on the uplink. // Calculate the amount of currency on the uplink.
var startingBalance = component.StartingBalance; var startingBalance = component.StartingBalance;
if (_jobs.MindTryGetJob(mindId, out var prototype)) if (_jobs.MindTryGetJob(mindId, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0); {
if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2
startingBalance = 0;
else
startingBalance = startingBalance - prototype.AntagAdvantage;
}
// creadth: we need to create uplink for the antag. // Choose and generate an Uplink, and return the uplink code if applicable
// PDA should be in place already var uplinkParams = RequestUplink(traitor, startingBalance, briefing);
var pda = _uplink.FindUplinkTarget(traitor); code = uplinkParams.Item1;
if (pda == null || !_uplink.AddUplink(traitor, startingBalance, giveDiscounts: true)) briefing = uplinkParams.Item2;
return false;
// Give traitors their codewords and uplink code to keep in their character info menu
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
// 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", "#"))));
} }
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification); string[]? codewords = null;
if (component.GiveCodewords)
codewords = component.Codewords;
if (component.GiveBriefing)
_antag.SendBriefing(traitor, GenerateBriefing(codewords, code, issuer), null, component.GreetSoundNotification);
component.TraitorMinds.Add(mindId); component.TraitorMinds.Add(mindId);
@@ -134,6 +143,32 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return true; return true;
} }
private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing)
{
var pda = _uplink.FindUplinkTarget(traitor);
Note[]? code = null;
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
if (pda is not null && uplinked)
{
// Codes are only generated if the uplink is a PDA
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
// 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)
{
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
}
return (null, briefing);
}
// TODO: AntagCodewordsComponent // TODO: AntagCodewordsComponent
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args) private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{ {
@@ -141,13 +176,17 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
} }
// TODO: figure out how to handle this? add priority to briefing event? // TODO: figure out how to handle this? add priority to briefing event?
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null) private string GenerateBriefing(string[]? codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown")))); sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown"))));
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords)))); if (codewords != null)
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
if (uplinkCode != null) if (uplinkCode != null)
sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#")))); sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#"))));
else
sb.AppendLine(Loc.GetString("traitor-role-uplink-implant"));
return sb.ToString(); return sb.ToString();
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Traitor.Systems; using Content.Server.Traitor.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Components; namespace Content.Server.Traitor.Components;
@@ -9,14 +10,8 @@ namespace Content.Server.Traitor.Components;
public sealed partial class AutoTraitorComponent : Component public sealed partial class AutoTraitorComponent : Component
{ {
/// <summary> /// <summary>
/// Whether to give the traitor an uplink or not. /// The traitor profile to use
/// </summary> /// </summary>
[DataField("giveUplink"), ViewVariables(VVAccess.ReadWrite)] [DataField]
public bool GiveUplink = true; public EntProtoId Profile = "Traitor";
/// <summary>
/// Whether to give the traitor objectives or not.
/// </summary>
[DataField("giveObjectives"), ViewVariables(VVAccess.ReadWrite)]
public bool GiveObjectives = true;
} }

View File

@@ -12,9 +12,6 @@ public sealed class AutoTraitorSystem : EntitySystem
{ {
[Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly AntagSelectionSystem _antag = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -24,6 +21,6 @@ public sealed class AutoTraitorSystem : EntitySystem
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args) private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
{ {
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule); _antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, comp.Profile);
} }
} }

View File

@@ -1,97 +1,136 @@
using System.Linq; using System.Linq;
using Content.Server.Store.Systems; using Content.Server.Store.Systems;
using Content.Server.StoreDiscount.Systems; using Content.Server.StoreDiscount.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Implants;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.FixedPoint;
using Content.Shared.Store; using Content.Shared.Store;
using Content.Shared.Store.Components; using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Uplink namespace Content.Server.Traitor.Uplink;
public sealed class UplinkSystem : EntitySystem
{ {
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!;
[ValidatePrototypeId<CurrencyPrototype>]
public const string TelecrystalCurrencyPrototype = "Telecrystal";
private const string FallbackUplinkImplant = "UplinkImplant";
private const string 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="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</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(
EntityUid user,
FixedPoint2 balance,
EntityUid? uplinkEntity = null,
bool giveDiscounts = false)
{ {
[Dependency] private readonly InventorySystem _inventorySystem = default!; // Try to find target item if none passed
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly StoreSystem _store = default!;
[ValidatePrototypeId<CurrencyPrototype>] uplinkEntity ??= FindUplinkTarget(user);
public const string TelecrystalCurrencyPrototype = "Telecrystal";
/// <summary> if (uplinkEntity == null)
/// Adds an uplink to the target return ImplantUplink(user, balance, giveDiscounts);
/// </summary>
/// <param name="user">The person who is getting the uplink</param> EnsureComp<UplinkComponent>(uplinkEntity.Value);
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param> SetUplink(user, uplinkEntity.Value, balance, giveDiscounts);
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
/// <returns>Whether or not the uplink was added successfully</returns> // TODO add BUI. Currently can't be done outside of yaml -_-
public bool AddUplink( // ^ What does this even mean?
EntityUid user,
FixedPoint2? balance, return true;
EntityUid? uplinkEntity = null, }
bool giveDiscounts = false
) /// <summary>
/// Configure TC for the uplink
/// </summary>
private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts)
{
var store = EnsureComp<StoreComponent>(uplink);
store.AccountOwner = user;
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } },
uplink,
store);
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: user,
Store: uplink,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(user, uplink, store)
.ToArray());
RaiseLocalEvent(ref uplinkInitializedEvent);
}
/// <summary>
/// Implant an uplink as a fallback measure if the traitor had no PDA
/// </summary>
private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
{
var implantProto = new string(FallbackUplinkImplant);
if (!_proto.TryIndex<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, implantProto);
if (!HasComp<StoreComponent>(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 EntityUid? FindUplinkTarget(EntityUid user)
{
// Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{ {
// Try to find target item if none passed while (containerSlotEnumerator.MoveNext(out var pdaUid))
uplinkEntity ??= FindUplinkTarget(user);
if (uplinkEntity == null)
{ {
return false; if (!pdaUid.ContainedEntity.HasValue)
continue;
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
} }
EnsureComp<UplinkComponent>(uplinkEntity.Value);
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
store.AccountOwner = user;
store.Balance.Clear();
if (balance != null)
{
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance.Value } }, uplinkEntity.Value, store);
}
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: user,
Store: uplinkEntity.Value,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(user, uplinkEntity.Value, store)
.ToArray()
);
RaiseLocalEvent(ref uplinkInitializedEvent);
// TODO add BUI. Currently can't be done outside of yaml -_-
return true;
} }
/// <summary> // Also check hands
/// Finds the entity that can hold an uplink for a user. foreach (var item in _handsSystem.EnumerateHeld(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)
{ {
// Try to find PDA in inventory if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) return item;
{
while (containerSlotEnumerator.MoveNext(out var pdaUid))
{
if (!pdaUid.ContainedEntity.HasValue)
continue;
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
}
}
// Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user))
{
if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
return item;
}
return null;
} }
return null;
} }
} }

View File

@@ -94,22 +94,38 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// </summary> /// </summary>
public void AddImplants(EntityUid uid, IEnumerable<String> implants) public void AddImplants(EntityUid uid, IEnumerable<String> implants)
{ {
var coords = Transform(uid).Coordinates;
foreach (var id in implants) foreach (var id in implants)
{ {
var ent = Spawn(id, coords); AddImplant(uid, id);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
}
} }
} }
/// <summary>
/// Adds a single implant to a person, and returns the implant.
/// Logs any implant ids that don't have <see cref="SubdermalImplantComponent"/>.
/// </summary>
/// <returns>
/// The implant, if it was successfully created. Otherwise, null.
/// </returns>>
public EntityUid? AddImplant(EntityUid uid, String implantId)
{
var coords = Transform(uid).Coordinates;
var ent = Spawn(implantId, coords);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
return null;
}
return ent;
}
/// <summary> /// <summary>
/// Forces an implant into a person /// Forces an implant into a person
/// Good for on spawn related code or admin additions /// Good for on spawn related code or admin additions

View File

@@ -26,7 +26,7 @@ traitor-death-match-end-round-description-entry = {$originalName}'s PDA, with {$
traitor-role-greeting = traitor-role-greeting =
You are an agent sent by {$corporation} on behalf of [color = darkred]The Syndicate.[/color] You are an agent sent by {$corporation} on behalf of [color = darkred]The Syndicate.[/color]
Your objectives and codewords are listed in the character menu. Your objectives and codewords are listed in the character menu.
Use the uplink loaded into your PDA to buy the tools you'll need for this mission. Use your uplink to buy the tools you'll need for this mission.
Death to Nanotrasen! Death to Nanotrasen!
traitor-role-codewords = traitor-role-codewords =
The codewords are: [color = lightgray] The codewords are: [color = lightgray]
@@ -36,9 +36,13 @@ traitor-role-codewords =
traitor-role-uplink-code = traitor-role-uplink-code =
Set your ringtone to the notes [color = lightgray]{$code}[/color] to lock or unlock your uplink. Set your ringtone to the notes [color = lightgray]{$code}[/color] to lock or unlock your uplink.
Remember to lock it after, or the stations crew will easily open it too! Remember to lock it after, or the stations crew will easily open it too!
traitor-role-uplink-implant =
Your uplink implant has been activated, access it from your hotbar.
The uplink is secure unless someone removes it from your body.
# don't need all the flavour text for character menu # don't need all the flavour text for character menu
traitor-role-codewords-short = traitor-role-codewords-short =
The codewords are: The codewords are:
{$codewords}. {$codewords}.
traitor-role-uplink-code-short = Your uplink code is {$code}. Set it as your PDA ringtone to access uplink. traitor-role-uplink-code-short = Your uplink code is {$code}. Set it as your PDA ringtone to access uplink.
traitor-role-uplink-implant-short = Your uplink was implanted. Access it from your hotbar.

View File

@@ -1407,8 +1407,7 @@
components: components:
# make the player a traitor once its taken # make the player a traitor once its taken
- type: AutoTraitor - type: AutoTraitor
giveUplink: false profile: TraitorReinforcement
giveObjectives: false
- type: entity - type: entity
id: MobMonkeySyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink id: MobMonkeySyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink
@@ -1569,8 +1568,7 @@
components: components:
# make the player a traitor once its taken # make the player a traitor once its taken
- type: AutoTraitor - type: AutoTraitor
giveUplink: false profile: TraitorReinforcement
giveObjectives: false
- type: entity - type: entity
id: MobKoboldSyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink id: MobKoboldSyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink

View File

@@ -31,8 +31,7 @@
components: components:
# make the player a traitor once its taken # make the player a traitor once its taken
- type: AutoTraitor - type: AutoTraitor
giveUplink: false profile: TraitorReinforcement
giveObjectives: false
- type: entity - type: entity
parent: MobHumanSyndicateAgent parent: MobHumanSyndicateAgent

View File

@@ -189,6 +189,15 @@
mindRoles: mindRoles:
- MindRoleTraitor - MindRoleTraitor
- type: entity
id: TraitorReinforcement
parent: Traitor
components:
- type: TraitorRule
giveUplink: false
giveCodewords: false # It would actually give them a different set of codewords than the regular traitors, anyway
giveBriefing: false
- type: entity - type: entity
id: Revolutionary id: Revolutionary
parent: BaseGameRule parent: BaseGameRule

View File

@@ -18,12 +18,17 @@
By pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color], you'll see your personal uplink code. [bold]Setting your PDA's ringtone as this code will open the uplink.[/bold] By pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color], you'll see your personal uplink code. [bold]Setting your PDA's ringtone as this code will open the uplink.[/bold]
Pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color] also lets you view your objectives and the codewords. Pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color] also lets you view your objectives and the codewords.
If you do not have a PDA when you are activated, an [color=cyan]uplink implant[/color] is provided [bold]for the full [color=red]TC[/color] price of the implant.[/bold]
It can be accessed from your hotbar.
<Box> <Box>
<GuideEntityEmbed Entity="PassengerPDA" Caption="PDA"/> <GuideEntityEmbed Entity="PassengerPDA" Caption="PDA"/>
<GuideEntityEmbed Entity="Telecrystal" Caption="Telecrystals"/> <GuideEntityEmbed Entity="Telecrystal" Caption="Telecrystals"/>
<GuideEntityEmbed Entity="BaseUplinkRadio" Caption="Uplink Implant"/>
</Box> </Box>
[bold]Make sure to close your uplink to prevent anyone else from seeing it.[/bold] You don't want [color=#cb0000]Security[/color] to get their hands on this premium selection of contraband! [bold]Make sure to close your PDA uplink to prevent anyone else from seeing it.[/bold] You don't want [color=#cb0000]Security[/color] to get their hands on this premium selection of contraband!
Implanted uplinks are not normally accessible to other people, so they do not have any security measures. They can, however, be removed from you with an empty implanter.
<Box> <Box>
<GuideEntityEmbed Entity="WeaponPistolCobra" Caption="Weaponry"/> <GuideEntityEmbed Entity="WeaponPistolCobra" Caption="Weaponry"/>