Re-organize all projects (#4166)

This commit is contained in:
DrSmugleaf
2021-06-09 22:19:39 +02:00
committed by GitHub
parent 9f50e4061b
commit ff1a2d97ea
1773 changed files with 5258 additions and 5508 deletions

View File

@@ -1,21 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.AI
{
[Prototype("aiFaction")]
public class AiFactionPrototype : IPrototype
{
// These are immutable so any dynamic changes aren't saved back over.
// AiFactionSystem will just read these and then store them.
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[DataField("hostile")]
public IReadOnlyList<string> Hostile { get; private set; } = new List<string>();
}
}

View File

@@ -1,16 +0,0 @@
#nullable enable
using Content.Server.GameObjects.EntitySystems.AI;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.GameObjects.Components.AI
{
[RegisterComponent]
public sealed class AiFactionTagComponent : Component
{
public override string Name => "AiFactionTag";
[DataField("factions")]
public Faction Factions { get; private set; } = Faction.None;
}
}

View File

@@ -1,32 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Access
{
/// <summary>
/// Simple mutable access provider found on ID cards and such.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IAccess))]
public class AccessComponent : Component, IAccess
{
public override string Name => "Access";
[DataField("tags")]
[ViewVariables]
private readonly HashSet<string> _tags = new();
public ISet<string> Tags => _tags;
public bool IsReadOnly => false;
public void SetTags(IEnumerable<string> newTags)
{
_tags.Clear();
_tags.UnionWith(newTags);
}
}
}

View File

@@ -1,17 +0,0 @@
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Access
{
public sealed class AccessReaderChangeMessage : EntityEventArgs
{
public IEntity Sender { get; }
public bool Enabled { get; }
public AccessReaderChangeMessage(IEntity entity, bool enabled)
{
Sender = entity;
Enabled = enabled;
}
}
}

View File

@@ -1,125 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.Access;
using Content.Shared.GameObjects.Components.Inventory;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Access
{
/// <summary>
/// Stores access levels necessary to "use" an entity
/// and allows checking if something or somebody is authorized with these access levels.
/// </summary>
[PublicAPI]
[RegisterComponent]
public class AccessReader : Component
{
public override string Name => "AccessReader";
private readonly HashSet<string> _denyTags = new();
/// <summary>
/// List of access lists to check allowed against. For an access check to pass
/// there has to be an access list that is a subset of the access in the checking list.
/// </summary>
[DataField("access")]
[ViewVariables]
public List<HashSet<string>> AccessLists { get; } = new();
/// <summary>
/// The set of tags that will automatically deny an allowed check, if any of them are present.
/// </summary>
[ViewVariables] public ISet<string> DenyTags => _denyTags;
/// <summary>
/// Searches an <see cref="IAccess"/> in the entity itself, in its active hand or in its ID slot.
/// Then compares the found access with the configured access lists to see if it is allowed.
/// </summary>
/// <remarks>
/// If no access is found, an empty set is used instead.
/// </remarks>
/// <param name="entity">The entity to be searched for access.</param>
public bool IsAllowed(IEntity entity)
{
var tags = FindAccessTags(entity);
return IsAllowed(tags);
}
public bool IsAllowed(IAccess access)
{
return IsAllowed(access.Tags);
}
public bool IsAllowed(ICollection<string> accessTags)
{
if (_denyTags.Overlaps(accessTags))
{
// Sec owned by cargo.
return false;
}
return AccessLists.Count == 0 || AccessLists.Any(a => a.IsSubsetOf(accessTags));
}
public static ICollection<string> FindAccessTags(IEntity entity)
{
if (entity.TryGetComponent(out IAccess? accessComponent))
{
return accessComponent.Tags;
}
if (entity.TryGetComponent(out IHandsComponent? handsComponent))
{
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
if (activeHandEntity != null &&
activeHandEntity.TryGetComponent(out IAccess? handAccessComponent))
{
return handAccessComponent.Tags;
}
}
else
{
return Array.Empty<string>();
}
if (entity.TryGetComponent(out InventoryComponent? inventoryComponent))
{
if (inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) &&
inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent? item) &&
item.Owner.TryGetComponent(out IAccess? idAccessComponent)
)
{
return idAccessComponent.Tags;
}
}
return Array.Empty<string>();
}
public override void Initialize()
{
base.Initialize();
var proto = IoCManager.Resolve<IPrototypeManager>();
foreach (var level in AccessLists.SelectMany(c => c).Union(DenyTags))
{
if (!proto.HasIndex<AccessLevelPrototype>(level))
{
Logger.ErrorS("access", $"Invalid access level: {level}");
}
}
}
}
}

View File

@@ -1,73 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Access
{
[RegisterComponent]
public class IdCardComponent : Component
{
public override string Name => "IdCard";
/// See <see cref="UpdateEntityName"/>.
[DataField("originalOwnerName")]
private string _originalOwnerName = default!;
[DataField("fullName")]
private string? _fullName;
[ViewVariables(VVAccess.ReadWrite)]
public string? FullName
{
get => _fullName;
set
{
_fullName = value;
UpdateEntityName();
}
}
[DataField("jobTitle")]
private string? _jobTitle;
[ViewVariables(VVAccess.ReadWrite)]
public string? JobTitle
{
get => _jobTitle;
set
{
_jobTitle = value;
UpdateEntityName();
}
}
/// <summary>
/// Changes the <see cref="Entity.Name"/> of <see cref="Component.Owner"/>.
/// </summary>
/// <remarks>
/// If either <see cref="FullName"/> or <see cref="JobTitle"/> is empty, it's replaced by placeholders.
/// If both are empty, the original entity's name is restored.
/// </remarks>
private void UpdateEntityName()
{
if (string.IsNullOrWhiteSpace(FullName) && string.IsNullOrWhiteSpace(JobTitle))
{
Owner.Name = _originalOwnerName;
return;
}
var jobSuffix = string.IsNullOrWhiteSpace(JobTitle) ? "" : $" ({JobTitle})";
Owner.Name = string.IsNullOrWhiteSpace(FullName)
? Loc.GetString("{0}{1}", _originalOwnerName, jobSuffix)
: Loc.GetString("{0}'s ID card{1}", FullName, jobSuffix);
}
public override void Initialize()
{
base.Initialize();
// ReSharper disable once ConstantNullCoalescingCondition
_originalOwnerName ??= Owner.Name;
UpdateEntityName();
}
}
}

View File

@@ -1,319 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Server.Utility;
using Content.Shared.Access;
using Content.Shared.GameObjects.Components.Access;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.ViewVariables;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
namespace Content.Server.GameObjects.Components.Access
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class IdCardConsoleComponent : SharedIdCardConsoleComponent, IActivate, IInteractUsing, IBreakAct
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private ContainerSlot _privilegedIdContainer = default!;
private ContainerSlot _targetIdContainer = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(IdCardConsoleUiKey.Key);
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private bool PrivilegedIDEmpty => _privilegedIdContainer.ContainedEntities.Count < 1;
private bool TargetIDEmpty => _targetIdContainer.ContainedEntities.Count < 1;
public override void Initialize()
{
base.Initialize();
_privilegedIdContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-privilegedId");
_targetIdContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-targetId");
Owner.EnsureComponentWarn<AccessReader>();
Owner.EnsureComponentWarn<ServerUserInterfaceComponent>();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
UpdateUserInterface();
}
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
switch (obj.Message)
{
case IdButtonPressedMessage msg:
switch (msg.Button)
{
case UiButton.PrivilegedId:
HandleId(obj.Session.AttachedEntity, _privilegedIdContainer);
break;
case UiButton.TargetId:
HandleId(obj.Session.AttachedEntity, _targetIdContainer);
break;
}
break;
case WriteToTargetIdMessage msg:
TryWriteToTargetId(msg.FullName, msg.JobTitle, msg.AccessList);
break;
}
UpdateUserInterface();
}
/// <summary>
/// Returns true if there is an ID in <see cref="_privilegedIdContainer"/> and said ID satisfies the requirements of <see cref="AccessReader"/>.
/// </summary>
private bool PrivilegedIdIsAuthorized()
{
if (!Owner.TryGetComponent(out AccessReader? reader))
{
return true;
}
var privilegedIdEntity = _privilegedIdContainer.ContainedEntity;
return privilegedIdEntity != null && reader.IsAllowed(privilegedIdEntity);
}
/// <summary>
/// Called when the "Submit" button in the UI gets pressed.
/// Writes data passed from the UI into the ID stored in <see cref="_targetIdContainer"/>, if present.
/// </summary>
private void TryWriteToTargetId(string newFullName, string newJobTitle, List<string> newAccessList)
{
if (!PrivilegedIdIsAuthorized() || _targetIdContainer.ContainedEntity == null)
{
return;
}
var targetIdEntity = _targetIdContainer.ContainedEntity;
var targetIdComponent = targetIdEntity.GetComponent<IdCardComponent>();
targetIdComponent.FullName = newFullName;
targetIdComponent.JobTitle = newJobTitle;
if (!newAccessList.TrueForAll(x => _prototypeManager.HasIndex<AccessLevelPrototype>(x)))
{
Logger.Warning("Tried to write unknown access tag.");
return;
}
var targetIdAccess = targetIdEntity.GetComponent<AccessComponent>();
targetIdAccess.SetTags(newAccessList);
}
/// <summary>
/// Called when one of the insert/remove ID buttons gets pressed.
/// </summary>
private void HandleId(IEntity user, ContainerSlot container)
{
if (!user.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(user, Loc.GetString("You have no hands."));
return;
}
if (container.ContainedEntity == null)
{
InsertIdFromHand(user, container, hands);
}
else
{
PutIdInHand(container, hands);
}
}
private void InsertIdFromHand(IEntity user, ContainerSlot container, IHandsComponent hands)
{
var isId = hands.GetActiveHand?.Owner.HasComponent<IdCardComponent>();
if (isId != true)
{
return;
}
if (hands.ActiveHand == null)
{
return;
}
if (!hands.Drop(hands.ActiveHand, container))
{
Owner.PopupMessage(user, Loc.GetString("You can't let go of the ID card!"));
return;
}
UpdateUserInterface();
}
private void PutIdInHand(ContainerSlot container, IHandsComponent hands)
{
var idEntity = container.ContainedEntity;
if (idEntity == null || !container.Remove(idEntity))
{
return;
}
UpdateUserInterface();
hands.PutInHand(idEntity.GetComponent<ItemComponent>());
}
private void UpdateUserInterface()
{
var isPrivilegedIdPresent = _privilegedIdContainer.ContainedEntity != null;
var targetIdEntity = _targetIdContainer.ContainedEntity;
IdCardConsoleBoundUserInterfaceState newState;
// this could be prettier
if (targetIdEntity == null)
{
newState = new IdCardConsoleBoundUserInterfaceState(
isPrivilegedIdPresent,
PrivilegedIdIsAuthorized(),
false,
null,
null,
null,
_privilegedIdContainer.ContainedEntity?.Name ?? "",
_targetIdContainer.ContainedEntity?.Name ?? "");
}
else
{
var targetIdComponent = targetIdEntity.GetComponent<IdCardComponent>();
var targetAccessComponent = targetIdEntity.GetComponent<AccessComponent>();
newState = new IdCardConsoleBoundUserInterfaceState(
isPrivilegedIdPresent,
PrivilegedIdIsAuthorized(),
true,
targetIdComponent.FullName,
targetIdComponent.JobTitle,
targetAccessComponent.Tags.ToArray(),
_privilegedIdContainer.ContainedEntity?.Name ?? "",
_targetIdContainer.ContainedEntity?.Name ?? "");
}
UserInterface?.SetState(newState);
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if(!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if(!Powered) return;
UserInterface?.Open(actor.PlayerSession);
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
var item = eventArgs.Using;
var user = eventArgs.User;
if (!PrivilegedIDEmpty && !TargetIDEmpty)
{
return false;
}
if (!item.TryGetComponent<IdCardComponent>(out var idCardComponent) || !user.TryGetComponent(out IHandsComponent? hand))
{
return false;
}
if (PrivilegedIDEmpty)
{
InsertIdFromHand(user, _privilegedIdContainer, hand);
}
else if (TargetIDEmpty)
{
InsertIdFromHand(user, _targetIdContainer, hand);
}
UpdateUserInterface();
return true;
}
[Verb]
public sealed class EjectPrivilegedIDVerb : Verb<IdCardConsoleComponent>
{
protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Eject Privileged ID");
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
data.Visibility = component.PrivilegedIDEmpty ? VerbVisibility.Invisible : VerbVisibility.Visible;
}
protected override void Activate(IEntity user, IdCardConsoleComponent component)
{
if (!user.TryGetComponent(out IHandsComponent? hand))
{
return;
}
component.PutIdInHand(component._privilegedIdContainer, hand);
}
}
public sealed class EjectTargetIDVerb : Verb<IdCardConsoleComponent>
{
protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Eject Target ID");
data.Visibility = component.TargetIDEmpty ? VerbVisibility.Invisible : VerbVisibility.Visible;
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, IdCardConsoleComponent component)
{
if (!user.TryGetComponent(out IHandsComponent? hand))
{
return;
}
component.PutIdInHand(component._targetIdContainer, hand);
}
}
public void OnBreak(BreakageEventArgs eventArgs)
{
var privileged = _privilegedIdContainer.ContainedEntity;
if (privileged != null)
_privilegedIdContainer.Remove(privileged);
var target = _targetIdContainer.ContainedEntity;
if (target != null)
_targetIdContainer.Remove(target);
}
}
}

View File

@@ -1,36 +0,0 @@
#nullable enable
using Content.Shared.Roles;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.GameObjects.Components.Access
{
[RegisterComponent]
public class PresetIdCardComponent : Component, IMapInit
{
public override string Name => "PresetIdCard";
[DataField("job")]
private string? _jobName;
void IMapInit.MapInit()
{
if (_jobName == null)
{
return;
}
var prototypes = IoCManager.Resolve<IPrototypeManager>();
var job = prototypes.Index<JobPrototype>(_jobName);
var access = Owner.GetComponent<AccessComponent>();
var idCard = Owner.GetComponent<IdCardComponent>();
access.SetTags(job.Access);
idCard.JobTitle = job.Name;
}
}
}

View File

@@ -1,340 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.ActionBlocking;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Content.Shared.Utility;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.ActionBlocking
{
[RegisterComponent]
public class CuffableComponent : SharedCuffableComponent
{
/// <summary>
/// How many of this entity's hands are currently cuffed.
/// </summary>
[ViewVariables]
public int CuffedHandCount => Container.ContainedEntities.Count * 2;
protected IEntity LastAddedCuffs => Container.ContainedEntities[^1];
public IReadOnlyList<IEntity> StoredEntities => Container.ContainedEntities;
/// <summary>
/// Container of various handcuffs currently applied to the entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public Container Container { get; set; } = default!;
// TODO: Make a component message
public event Action? OnCuffedStateChanged;
private bool _uncuffing;
public override void Initialize()
{
base.Initialize();
Container = ContainerHelpers.EnsureContainer<Container>(Owner, Name);
Owner.EnsureComponentWarn<HandsComponent>();
}
public override ComponentState GetComponentState(ICommonSession player)
{
// there are 2 approaches i can think of to handle the handcuff overlay on players
// 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
// 2 - allow for several different player overlays for each different cuff type.
// approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
// right now we're doing approach #1.
if (CuffedHandCount > 0)
{
if (LastAddedCuffs.TryGetComponent<HandcuffComponent>(out var cuffs))
{
return new CuffableComponentState(CuffedHandCount,
CanStillInteract,
cuffs.CuffedRSI,
$"{cuffs.OverlayIconState}-{CuffedHandCount}",
cuffs.Color);
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
// the number corresponds to how many hands are cuffed.
}
}
return new CuffableComponentState(CuffedHandCount,
CanStillInteract,
"/Objects/Misc/handcuffs.rsi",
"body-overlay-2",
Color.White);
}
/// <summary>
/// Add a set of cuffs to an existing CuffedComponent.
/// </summary>
/// <param name="prototype"></param>
public bool TryAddNewCuffs(IEntity user, IEntity handcuff)
{
if (!handcuff.HasComponent<HandcuffComponent>())
{
Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
return false;
}
if (!handcuff.InRangeUnobstructed(Owner))
{
Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
return true;
}
// Success!
if (user.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.IsHolding(handcuff))
{
// Good lord handscomponent is scuffed, I hope some smug person will fix it someday
handsComponent.Drop(handcuff);
}
Container.Insert(handcuff);
CanStillInteract = Owner.TryGetComponent(out HandsComponent? ownerHands) && ownerHands.Hands.Count() > CuffedHandCount;
OnCuffedStateChanged?.Invoke();
UpdateAlert();
UpdateHeldItems();
Dirty();
return true;
}
public void CuffedStateChanged()
{
UpdateAlert();
OnCuffedStateChanged?.Invoke();
}
/// <summary>
/// Check how many items the user is holding and if it's more than the number of cuffed hands, drop some items.
/// </summary>
public void UpdateHeldItems()
{
if (!Owner.TryGetComponent(out HandsComponent? handsComponent)) return;
var itemCount = handsComponent.GetAllHeldItems().Count();
var freeHandCount = handsComponent.Hands.Count() - CuffedHandCount;
if (freeHandCount < itemCount)
{
foreach (var item in handsComponent.GetAllHeldItems())
{
if (freeHandCount < itemCount)
{
freeHandCount++;
handsComponent.Drop(item.Owner, false);
}
else
{
break;
}
}
}
}
/// <summary>
/// Updates the status effect indicator on the HUD.
/// </summary>
private void UpdateAlert()
{
if (Owner.TryGetComponent(out ServerAlertsComponent? status))
{
if (CanStillInteract)
{
status.ClearAlert(AlertType.Handcuffed);
}
else
{
status.ShowAlert(AlertType.Handcuffed);
}
}
}
/// <summary>
/// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
/// If the uncuffing succeeds, the cuffs will drop on the floor.
/// </summary>
/// <param name="user">The cuffed entity</param>
/// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
public async void TryUncuff(IEntity user, IEntity? cuffsToRemove = null)
{
if (_uncuffing) return;
var isOwner = user == Owner;
if (cuffsToRemove == null)
{
if (Container.ContainedEntities.Count == 0)
{
return;
}
cuffsToRemove = LastAddedCuffs;
}
else
{
if (!Container.ContainedEntities.Contains(cuffsToRemove))
{
Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
}
}
if (!cuffsToRemove.TryGetComponent<HandcuffComponent>(out var cuff))
{
Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!");
return;
}
if (!ActionBlockerSystem.CanInteract(user))
{
user.PopupMessage(Loc.GetString("You can't do that!"));
return;
}
if (!isOwner && !user.InRangeUnobstructed(Owner))
{
user.PopupMessage(Loc.GetString("You are too far away to remove the cuffs."));
return;
}
if (!cuffsToRemove.InRangeUnobstructed(Owner))
{
Logger.Warning("Handcuffs being removed from player are obstructed or too far away! This should not happen!");
return;
}
user.PopupMessage(Loc.GetString("You start removing the cuffs."));
var audio = EntitySystem.Get<AudioSystem>();
if (isOwner)
{
if (cuff.StartBreakoutSound != null)
SoundSystem.Play(Filter.Pvs(Owner), cuff.StartBreakoutSound, Owner);
}
else
{
if (cuff.StartUncuffSound != null)
SoundSystem.Play(Filter.Pvs(Owner), cuff.StartUncuffSound, Owner);
}
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
NeedHand = true
};
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
_uncuffing = true;
var result = await doAfterSystem.DoAfter(doAfterEventArgs);
_uncuffing = false;
if (result != DoAfterStatus.Cancelled)
{
if (cuff.EndUncuffSound != null)
SoundSystem.Play(Filter.Pvs(Owner), cuff.EndUncuffSound, Owner);
Container.ForceRemove(cuffsToRemove);
cuffsToRemove.Transform.AttachToGridOrMap();
cuffsToRemove.Transform.WorldPosition = Owner.Transform.WorldPosition;
if (cuff.BreakOnRemove)
{
cuff.Broken = true;
cuffsToRemove.Name = cuff.BrokenName;
cuffsToRemove.Description = cuff.BrokenDesc;
if (cuffsToRemove.TryGetComponent<SpriteComponent>(out var sprite) && cuff.BrokenState != null)
{
sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state?
}
}
CanStillInteract = Owner.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.Hands.Count() > CuffedHandCount;
OnCuffedStateChanged?.Invoke();
UpdateAlert();
Dirty();
if (CuffedHandCount == 0)
{
user.PopupMessage(Loc.GetString("You successfully remove the cuffs."));
if (!isOwner)
{
user.PopupMessage(Owner, Loc.GetString("{0:theName} uncuffs your hands.", user));
}
}
else
{
if (!isOwner)
{
user.PopupMessage(Loc.GetString("You successfully remove the cuffs. {0} of {1:theName}'s hands remain cuffed.", CuffedHandCount, user));
user.PopupMessage(Owner, Loc.GetString("{0:theName} removes your cuffs. {1} of your hands remain cuffed.", user, CuffedHandCount));
}
else
{
user.PopupMessage(Loc.GetString("You successfully remove the cuffs. {0} of your hands remain cuffed.", CuffedHandCount));
}
}
}
else
{
user.PopupMessage(Loc.GetString("You fail to remove the cuffs."));
}
return;
}
/// <summary>
/// Allows the uncuffing of a cuffed person. Used by other people and by the component owner to break out of cuffs.
/// </summary>
[Verb]
private sealed class UncuffVerb : Verb<CuffableComponent>
{
protected override void GetData(IEntity user, CuffableComponent component, VerbData data)
{
if ((user != component.Owner && !ActionBlockerSystem.CanInteract(user)) || component.CuffedHandCount == 0)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Uncuff");
}
protected override void Activate(IEntity user, CuffableComponent component)
{
if (component.CuffedHandCount > 0)
{
component.TryUncuff(user);
}
}
}
}
}

View File

@@ -1,239 +0,0 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.GameObjects.Components.ActionBlocking;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.ActionBlocking
{
[RegisterComponent]
[ComponentReference(typeof(SharedHandcuffComponent))]
public class HandcuffComponent : SharedHandcuffComponent, IAfterInteract
{
/// <summary>
/// The time it takes to apply a <see cref="CuffedComponent"/> to an entity.
/// </summary>
[ViewVariables]
[DataField("cuffTime")]
public float CuffTime { get; set; } = 5f;
/// <summary>
/// The time it takes to remove a <see cref="CuffedComponent"/> from an entity.
/// </summary>
[ViewVariables]
[DataField("uncuffTime")]
public float UncuffTime { get; set; } = 5f;
/// <summary>
/// The time it takes for a cuffed entity to remove <see cref="CuffedComponent"/> from itself.
/// </summary>
[ViewVariables]
[DataField("breakoutTime")]
public float BreakoutTime { get; set; } = 30f;
/// <summary>
/// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
/// </summary>
[ViewVariables]
[DataField("stunBonus")]
public float StunBonus { get; set; } = 2f;
/// <summary>
/// Will the cuffs break when removed?
/// </summary>
[ViewVariables]
[DataField("breakOnRemove")]
public bool BreakOnRemove { get; set; }
/// <summary>
/// The path of the RSI file used for the player cuffed overlay.
/// </summary>
[ViewVariables]
[DataField("cuffedRSI")]
public string? CuffedRSI { get; set; } = "Objects/Misc/handcuffs.rsi";
/// <summary>
/// The iconstate used with the RSI file for the player cuffed overlay.
/// </summary>
[ViewVariables]
[DataField("bodyIconState")]
public string? OverlayIconState { get; set; } = "body-overlay";
/// <summary>
/// The iconstate used for broken handcuffs
/// </summary>
[ViewVariables]
[DataField("brokenIconState")]
public string? BrokenState { get; set; }
/// <summary>
/// The iconstate used for broken handcuffs
/// </summary>
[ViewVariables]
[DataField("brokenName")]
public string BrokenName { get; set; } = default!;
/// <summary>
/// The iconstate used for broken handcuffs
/// </summary>
[ViewVariables]
[DataField("brokenDesc")]
public string BrokenDesc { get; set; } = default!;
[ViewVariables]
public bool Broken
{
get
{
return _isBroken;
}
set
{
if (_isBroken != value)
{
_isBroken = value;
Dirty();
}
}
}
[DataField("startCuffSound")]
public string StartCuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_start.ogg";
[DataField("endCuffSound")] public string EndCuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_end.ogg";
[DataField("startBreakoutSound")]
public string StartBreakoutSound { get; set; } = "/Audio/Items/Handcuffs/cuff_breakout_start.ogg";
[DataField("startUncuffSound")]
public string StartUncuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_takeoff_start.ogg";
[DataField("endUncuffSound")]
public string EndUncuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_takeoff_end.ogg";
[DataField("color")]
public Color Color { get; set; } = Color.White;
// Non-exposed data fields
private bool _isBroken = false;
/// <summary>
/// Used to prevent DoAfter getting spammed.
/// </summary>
private bool _cuffing;
public override ComponentState GetComponentState(ICommonSession player)
{
return new HandcuffedComponentState(Broken ? BrokenState : string.Empty);
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (_cuffing) return true;
if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent<CuffableComponent>(out var cuffed))
{
return false;
}
if (eventArgs.Target == eventArgs.User)
{
eventArgs.User.PopupMessage(Loc.GetString("You can't cuff yourself!"));
return true;
}
if (Broken)
{
eventArgs.User.PopupMessage(Loc.GetString("The cuffs are broken!"));
return true;
}
if (!eventArgs.Target.TryGetComponent<HandsComponent>(out var hands))
{
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no hands!", eventArgs.Target));
return true;
}
if (cuffed.CuffedHandCount == hands.Count)
{
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target));
return true;
}
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true))
{
eventArgs.User.PopupMessage(Loc.GetString("You are too far away to use the cuffs!"));
return true;
}
eventArgs.User.PopupMessage(Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target));
eventArgs.User.PopupMessage(eventArgs.Target, Loc.GetString("{0:theName} starts cuffing you!", eventArgs.User));
if (StartCuffSound != null)
SoundSystem.Play(Filter.Pvs(Owner), StartCuffSound, Owner);
TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed);
return true;
}
/// <summary>
/// Update the cuffed state of an entity
/// </summary>
private async void TryUpdateCuff(IEntity user, IEntity target, CuffableComponent cuffs)
{
var cuffTime = CuffTime;
if (target.TryGetComponent<StunnableComponent>(out var stun) && stun.Stunned)
{
cuffTime = MathF.Max(0.1f, cuffTime - StunBonus);
}
var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
NeedHand = true
};
_cuffing = true;
var result = await EntitySystem.Get<DoAfterSystem>().DoAfter(doAfterEventArgs);
_cuffing = false;
if (result != DoAfterStatus.Cancelled)
{
if (cuffs.TryAddNewCuffs(user, Owner))
{
if (EndCuffSound != null)
SoundSystem.Play(Filter.Pvs(Owner), EndCuffSound, Owner);
user.PopupMessage(Loc.GetString("You successfully cuff {0:theName}.", target));
target.PopupMessage(Loc.GetString("You have been cuffed by {0:theName}!", user));
}
}
else
{
user.PopupMessage(Loc.GetString("You were interrupted while cuffing {0:theName}!", target));
target.PopupMessage(Loc.GetString("You interrupt {0:theName} while they are cuffing you!", user));
}
}
}
}

View File

@@ -1,59 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.Mobs.Roles;
using Content.Shared.GameObjects.Components.Actor;
using Content.Shared.Objectives;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Players;
namespace Content.Server.GameObjects.Components.Actor
{
[RegisterComponent]
public class CharacterInfoComponent : SharedCharacterInfoComponent
{
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null)
{
if(session?.AttachedEntity != Owner) return;
switch (message)
{
case RequestCharacterInfoMessage _:
var conditions = new Dictionary<string, List<ConditionInfo>>();
var jobTitle = "No Profession";
if (Owner.TryGetComponent(out MindComponent? mindComponent))
{
var mind = mindComponent.Mind;
if (mind != null)
{
// getting conditions
foreach (var objective in mind.AllObjectives)
{
if (!conditions.ContainsKey(objective.Prototype.Issuer))
conditions[objective.Prototype.Issuer] = new List<ConditionInfo>();
foreach (var condition in objective.Conditions)
{
conditions[objective.Prototype.Issuer].Add(new ConditionInfo(condition.Title,
condition.Description, condition.Icon, condition.Progress));
}
}
// getting jobtitle
foreach (var role in mind.AllRoles)
{
if (role.GetType() == typeof(Job))
{
jobTitle = role.Name;
break;
}
}
}
}
SendNetworkMessage(new CharacterInfoMessage(jobTitle, conditions));
break;
}
}
}
}

View File

@@ -1,144 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using Content.Server.Advertisements;
using Content.Server.Interfaces.Chat;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
public class AdvertiseComponent : Component
{
public override string Name => "Advertise";
private CancellationTokenSource _cancellationSource = new();
/// <summary>
/// Minimum time to wait before saying a new ad, in seconds. Has to be larger than or equal to 1.
/// </summary>
[ViewVariables]
[DataField("minWait")]
private int MinWait { get; } = 480; // 8 minutes
/// <summary>
/// Maximum time to wait before saying a new ad, in seconds. Has to be larger than or equal
/// to <see cref="MinWait"/>
/// </summary>
[ViewVariables]
[DataField("maxWait")]
private int MaxWait { get; } = 600; // 10 minutes
[DataField("pack")]
private string PackPrototypeId { get; } = string.Empty;
private List<string> _advertisements = new();
public override void Initialize()
{
base.Initialize();
IoCManager.Resolve<IPrototypeManager>().TryIndex(PackPrototypeId, out AdvertisementsPackPrototype? packPrototype);
// Load advertisements pack
if (string.IsNullOrEmpty(PackPrototypeId) || packPrototype == null)
{
// If there is no pack, log a warning and don't start timer
Logger.Warning($"{Owner} has {Name}Component but no advertisments pack.");
return;
}
_advertisements = packPrototype.Advertisements;
// Do not start timer if advertisement list is empty
if (_advertisements.Count == 0)
{
// If no advertisements could be loaded, log a warning and don't start timer
Logger.Warning($"{Owner} tried to load advertisements pack without ads.");
return;
}
// Throw exception if MinWait is smaller than 1.
if (MinWait < 1)
{
throw new PrototypeLoadException($"{Owner} has illegal minWait for {Name}Component: {MinWait}.");
}
// Throw exception if MinWait larger than MaxWait.
if (MinWait > MaxWait)
{
throw new PrototypeLoadException($"{Owner} should have minWait greater than or equal to maxWait for {Name}Component.");
}
// Start timer at initialization, without MinWait bound
RefreshTimer(false);
}
public override void OnRemove()
{
_cancellationSource.Cancel();
_cancellationSource.Dispose();
base.OnRemove();
}
/// <summary>
/// Say advertisement and restart timer.
/// </summary>
private void SayAndRefresh()
{
IRobustRandom random = IoCManager.Resolve<IRobustRandom>();
IChatManager chatManager = IoCManager.Resolve<IChatManager>();
// Say advertisement
chatManager.EntitySay(Owner, Loc.GetString(random.Pick(_advertisements)));
// Refresh timer to repeat cycle
RefreshTimer();
}
/// <summary>
/// Refresh cancellation token and spawn new timer with random wait between <see cref="MinWait"/>
/// and <see cref="MaxWait"/>.
/// <param name="minBound">
/// Whether <see cref="MinWait"/> should be used to have a minimum waiting time.
/// </param>
/// </summary>
private void RefreshTimer(bool minBound = true)
{
// Generate new source
_cancellationSource.Cancel();
_cancellationSource.Dispose();
_cancellationSource = new CancellationTokenSource();
// Generate random wait time, then create timer
IRobustRandom random = IoCManager.Resolve<IRobustRandom>();
var wait = minBound ? random.Next(MinWait * 1000, MaxWait * 1000) : random.Next(MaxWait * 1000);
Owner.SpawnTimer(wait, SayAndRefresh, _cancellationSource.Token);
}
/// <summary>
/// Pause the advertising until <see cref="Resume"/> is called.
/// </summary>
public void Pause()
{
// Cancel current timer
_cancellationSource.Cancel();
}
/// <summary>
/// Resume the advertising after pausing.
/// </summary>
public void Resume()
{
// Restart timer, without minBound
RefreshTimer(false);
}
}
}

View File

@@ -1,218 +0,0 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Pulling;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Physics;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
// TODO: Move this component's logic to an EntitySystem.
[RegisterComponent]
public class AnchorableComponent : Component, IInteractUsing
{
public override string Name => "Anchorable";
[ComponentDependency] private PhysicsComponent? _physicsComponent = default!;
[ViewVariables]
[DataField("tool")]
public ToolQuality Tool { get; private set; } = ToolQuality.Anchoring;
[ViewVariables]
int IInteractUsing.Priority => 1;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("snap")]
public bool Snap { get; private set; } = true;
/// <summary>
/// Checks if a tool can change the anchored status.
/// </summary>
/// <param name="user">The user doing the action</param>
/// <param name="utilizing">The tool being used</param>
/// <param name="anchoring">True if we're anchoring, and false if we're unanchoring.</param>
/// <returns>true if it is valid, false otherwise</returns>
private async Task<bool> Valid(IEntity user, IEntity utilizing, bool anchoring)
{
if (!Owner.HasComponent<IPhysBody>())
{
return false;
}
BaseAnchoredAttemptEvent attempt =
anchoring ? new AnchorAttemptEvent(user, utilizing) : new UnanchorAttemptEvent(user, utilizing);
// Need to cast the event or it will be raised as BaseAnchoredAttemptEvent.
if (anchoring)
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, (AnchorAttemptEvent) attempt, false);
else
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, (UnanchorAttemptEvent) attempt, false);
if (attempt.Cancelled)
return false;
return utilizing.TryGetComponent(out ToolComponent? tool) && await tool.UseTool(user, Owner, 0.5f + attempt.Delay, Tool);
}
/// <summary>
/// Tries to anchor the owner of this component.
/// </summary>
/// <param name="user">The entity doing the anchoring</param>
/// <param name="utilizing">The tool being used</param>
/// <returns>true if anchored, false otherwise</returns>
public async Task<bool> TryAnchor(IEntity user, IEntity utilizing)
{
if (!(await Valid(user, utilizing, true)))
{
return false;
}
if (_physicsComponent == null)
return false;
// Snap rotation to cardinal (multiple of 90)
var rot = Owner.Transform.LocalRotation;
Owner.Transform.LocalRotation = Math.Round(rot / (Math.PI / 2)) * (Math.PI / 2);
if (Owner.TryGetComponent(out PullableComponent? pullableComponent))
{
if (pullableComponent.Puller != null)
{
pullableComponent.TryStopPull();
}
}
if (Snap)
Owner.SnapToGrid(Owner.EntityManager);
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new BeforeAnchoredEvent(user, utilizing), false);
_physicsComponent.BodyType = BodyType.Static;
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new AnchoredEvent(user, utilizing), false);
return true;
}
/// <summary>
/// Tries to unanchor the owner of this component.
/// </summary>
/// <param name="user">The entity doing the unanchoring</param>
/// <param name="utilizing">The tool being used, if any</param>
/// <param name="force">Whether or not to ignore valid tool checks</param>
/// <returns>true if unanchored, false otherwise</returns>
public async Task<bool> TryUnAnchor(IEntity user, IEntity utilizing)
{
if (!(await Valid(user, utilizing, false)))
{
return false;
}
if (_physicsComponent == null)
return false;
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new BeforeUnanchoredEvent(user, utilizing), false);
_physicsComponent.BodyType = BodyType.Dynamic;
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new UnanchoredEvent(user, utilizing), false);
return true;
}
/// <summary>
/// Tries to toggle the anchored status of this component's owner.
/// </summary>
/// <param name="user">The entity doing the unanchoring</param>
/// <param name="utilizing">The tool being used</param>
/// <returns>true if toggled, false otherwise</returns>
public async Task<bool> TryToggleAnchor(IEntity user, IEntity utilizing)
{
if (_physicsComponent == null)
return false;
return _physicsComponent.BodyType == BodyType.Static ?
await TryUnAnchor(user, utilizing) :
await TryAnchor(user, utilizing);
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
return await TryToggleAnchor(eventArgs.User, eventArgs.Using);
}
}
public abstract class BaseAnchoredAttemptEvent : CancellableEntityEventArgs
{
public IEntity User { get; }
public IEntity Tool { get; }
/// <summary>
/// Extra delay to add to the do_after.
/// Add to this, don't replace it.
/// Output parameter.
/// </summary>
public float Delay { get; set; } = 0f;
protected BaseAnchoredAttemptEvent(IEntity user, IEntity tool)
{
User = user;
Tool = tool;
}
}
public class AnchorAttemptEvent : BaseAnchoredAttemptEvent
{
public AnchorAttemptEvent(IEntity user, IEntity tool) : base(user, tool) { }
}
public class UnanchorAttemptEvent : BaseAnchoredAttemptEvent
{
public UnanchorAttemptEvent(IEntity user, IEntity tool) : base(user, tool) { }
}
public abstract class BaseAnchoredEvent : EntityEventArgs
{
public IEntity User { get; }
public IEntity Tool { get; }
protected BaseAnchoredEvent(IEntity user, IEntity tool)
{
User = user;
Tool = tool;
}
}
/// <summary>
/// Raised just before the entity's body type is changed.
/// </summary>
public class BeforeAnchoredEvent : BaseAnchoredEvent
{
public BeforeAnchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
}
public class AnchoredEvent : BaseAnchoredEvent
{
public AnchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
}
/// <summary>
/// Raised just before the entity's body type is changed.
/// </summary>
public class BeforeUnanchoredEvent : BaseAnchoredEvent
{
public BeforeUnanchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
}
public class UnanchoredEvent : BaseAnchoredEvent
{
public UnanchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
}
}

View File

@@ -1,899 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Utility;
using Content.Shared.Arcade;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Random;
namespace Content.Server.GameObjects.Components.Arcade
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class BlockGameArcadeComponent : Component, IActivate
{
[Dependency] private readonly IRobustRandom _random = default!;
public override string Name => "BlockGameArcade";
public override uint? NetID => ContentNetIDs.BLOCKGAME_ARCADE;
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
private bool Powered => _powerReceiverComponent?.Powered ?? false;
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);
private BlockGame? _game;
private IPlayerSession? _player;
private readonly List<IPlayerSession> _spectators = new();
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
OnPowerStateChanged(powerChanged);
break;
}
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if(!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!Powered)
{
return;
}
if(!ActionBlockerSystem.CanInteract(actor.PlayerSession.AttachedEntity)) return;
UserInterface?.Toggle(actor.PlayerSession);
RegisterPlayerSession(actor.PlayerSession);
}
private void RegisterPlayerSession(IPlayerSession session)
{
if (_player == null) _player = session;
else _spectators.Add(session);
UpdatePlayerStatus(session);
_game?.UpdateNewPlayerUI(session);
}
private void DeactivePlayer(IPlayerSession session)
{
if (_player != session) return;
var temp = _player;
_player = null;
if (_spectators.Count != 0)
{
_player = _spectators[0];
_spectators.Remove(_player);
UpdatePlayerStatus(_player);
}
_spectators.Add(temp);
UpdatePlayerStatus(temp);
}
private void UnRegisterPlayerSession(IPlayerSession session)
{
if (_player == session)
{
DeactivePlayer(_player);
}
else
{
_spectators.Remove(session);
UpdatePlayerStatus(session);
}
}
private void UpdatePlayerStatus(IPlayerSession session)
{
UserInterface?.SendMessage(new BlockGameMessages.BlockGameUserStatusMessage(_player == session), session);
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
UserInterface.OnClosed += UnRegisterPlayerSession;
}
_game = new BlockGame(this);
}
private void OnPowerStateChanged(PowerChangedMessage e)
{
if (e.Powered) return;
UserInterface?.CloseAll();
_player = null;
_spectators.Clear();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
switch (obj.Message)
{
case BlockGameMessages.BlockGamePlayerActionMessage playerActionMessage:
if (obj.Session != _player) break;
if (!ActionBlockerSystem.CanInteract(Owner))
{
DeactivePlayer(obj.Session);
break;
}
if (playerActionMessage.PlayerAction == BlockGamePlayerAction.NewGame)
{
if(_game?.Started == true) _game = new BlockGame(this);
_game?.StartGame();
}
else
{
_game?.ProcessInput(playerActionMessage.PlayerAction);
}
break;
}
}
public void DoGameTick(float frameTime)
{
_game?.GameTick(frameTime);
}
private class BlockGame
{
//note: field is 10(0 -> 9) wide and 20(0 -> 19) high
private readonly BlockGameArcadeComponent _component;
private readonly List<BlockGameBlock> _field = new();
private BlockGamePiece _currentPiece;
private BlockGamePiece _nextPiece
{
get => _internalNextPiece;
set
{
_internalNextPiece = value;
SendNextPieceUpdate();
}
}
private BlockGamePiece _internalNextPiece;
private void SendNextPieceUpdate()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
}
private void SendNextPieceUpdate(IPlayerSession session)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock), session);
}
private bool _holdBlock = false;
private BlockGamePiece? _heldPiece
{
get => _internalHeldPiece;
set
{
_internalHeldPiece = value;
SendHoldPieceUpdate();
}
}
private BlockGamePiece? _internalHeldPiece = null;
private void SendHoldPieceUpdate()
{
if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock));
else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock));
}
private void SendHoldPieceUpdate(IPlayerSession session)
{
if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock), session);
else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock), session);
}
private Vector2i _currentPiecePosition;
private BlockGamePieceRotation _currentRotation;
private float _softDropModifier = 0.1f;
private float Speed =>
-0.03f * Level + 1 * (!_softDropPressed ? 1 : _softDropModifier);
private const float _pressCheckSpeed = 0.08f;
private bool _running;
public bool Paused => !(_running && _started);
private bool _started;
public bool Started => _started;
private bool _gameOver;
private bool _leftPressed;
private bool _rightPressed;
private bool _softDropPressed;
private int Points
{
get => _internalPoints;
set
{
if (_internalPoints == value) return;
_internalPoints = value;
SendPointsUpdate();
}
}
private int _internalPoints;
private BlockGameSystem.HighScorePlacement? _highScorePlacement = null;
private void SendPointsUpdate()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points));
}
private void SendPointsUpdate(IPlayerSession session)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points));
}
public int Level
{
get => _level;
set
{
_level = value;
SendLevelUpdate();
}
}
private int _level = 0;
private void SendLevelUpdate()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level));
}
private void SendLevelUpdate(IPlayerSession session)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level));
}
private int ClearedLines
{
get => _clearedLines;
set
{
_clearedLines = value;
if (_clearedLines < LevelRequirement) return;
_clearedLines -= LevelRequirement;
Level++;
}
}
private int _clearedLines = 0;
private int LevelRequirement => Math.Min(100, Math.Max(Level * 10 - 50, 10));
public BlockGame(BlockGameArcadeComponent component)
{
_component = component;
_allBlockGamePieces = (BlockGamePieceType[]) Enum.GetValues(typeof(BlockGamePieceType));
_internalNextPiece = GetRandomBlockGamePiece(_component._random);
InitializeNewBlock();
}
private void SendHighscoreUpdate()
{
var entitySystem = EntitySystem.Get<BlockGameSystem>();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()));
}
private void SendHighscoreUpdate(IPlayerSession session)
{
var entitySystem = EntitySystem.Get<BlockGameSystem>();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()), session);
}
public void StartGame()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
FullUpdate();
_running = true;
_started = true;
}
private void FullUpdate()
{
UpdateAllFieldUI();
SendHoldPieceUpdate();
SendNextPieceUpdate();
SendPointsUpdate();
SendHighscoreUpdate();
SendLevelUpdate();
}
private void FullUpdate(IPlayerSession session)
{
UpdateFieldUI(session);
SendPointsUpdate(session);
SendNextPieceUpdate(session);
SendHoldPieceUpdate(session);
SendHighscoreUpdate(session);
SendLevelUpdate(session);
}
public void GameTick(float frameTime)
{
if (!_running) return;
InputTick(frameTime);
FieldTick(frameTime);
}
private float _accumulatedLeftPressTime;
private float _accumulatedRightPressTime;
private void InputTick(float frameTime)
{
bool anythingChanged = false;
if (_leftPressed)
{
_accumulatedLeftPressTime += frameTime;
while (_accumulatedLeftPressTime >= _pressCheckSpeed)
{
if (_currentPiece.Positions(_currentPiecePosition.AddToX(-1), _currentRotation)
.All(MoveCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToX(-1);
anythingChanged = true;
}
_accumulatedLeftPressTime -= _pressCheckSpeed;
}
}
if (_rightPressed)
{
_accumulatedRightPressTime += frameTime;
while (_accumulatedRightPressTime >= _pressCheckSpeed)
{
if (_currentPiece.Positions(_currentPiecePosition.AddToX(1), _currentRotation)
.All(MoveCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToX(1);
anythingChanged = true;
}
_accumulatedRightPressTime -= _pressCheckSpeed;
}
}
if(anythingChanged) UpdateAllFieldUI();
}
private float _accumulatedFieldFrameTime;
private void FieldTick(float frameTime)
{
_accumulatedFieldFrameTime += frameTime;
var checkTime = Speed;
while (_accumulatedFieldFrameTime >= checkTime)
{
if (_softDropPressed) AddPoints(1);
InternalFieldTick();
_accumulatedFieldFrameTime -= checkTime;
}
}
private void InternalFieldTick()
{
if (_currentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation)
.All(DropCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToY(1);
}
else
{
var blocks = _currentPiece.Blocks(_currentPiecePosition, _currentRotation);
_field.AddRange(blocks);
//check loose conditions
if (IsGameOver)
{
InvokeGameover();
return;
}
InitializeNewBlock();
}
CheckField();
UpdateAllFieldUI();
}
private void CheckField()
{
int pointsToAdd = 0;
int consecutiveLines = 0;
int clearedLines = 0;
for (int y = 0; y < 20; y++)
{
if (CheckLine(y))
{
//line was cleared
y--;
consecutiveLines++;
clearedLines++;
}
else if(consecutiveLines != 0)
{
var mod = consecutiveLines switch
{
1 => 40,
2 => 100,
3 => 300,
4 => 1200,
_ => 0
};
pointsToAdd += mod * (_level + 1);
}
}
ClearedLines += clearedLines;
AddPoints(pointsToAdd);
}
private bool CheckLine(int y)
{
for (var x = 0; x < 10; x++)
{
if (!_field.Any(b => b.Position.X == x && b.Position.Y == y)) return false;
}
//clear line
_field.RemoveAll(b => b.Position.Y == y);
//move everything down
FillLine(y);
return true;
}
private void AddPoints(int amount)
{
if (amount == 0) return;
Points += amount;
}
private void FillLine(int y)
{
for (int c_y = y; c_y > 0; c_y--)
{
for (int j = 0; j < _field.Count; j++)
{
if(_field[j].Position.Y != c_y-1) continue;
_field[j] = new BlockGameBlock(_field[j].Position.AddToY(1), _field[j].GameBlockColor);
}
}
}
private void InitializeNewBlock()
{
InitializeNewBlock(_nextPiece);
_nextPiece = GetRandomBlockGamePiece(_component._random);
_holdBlock = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
}
private void InitializeNewBlock(BlockGamePiece piece)
{
_currentPiecePosition = new Vector2i(5,0);
_currentRotation = BlockGamePieceRotation.North;
_currentPiece = piece;
UpdateAllFieldUI();
}
private bool LowerBoundCheck(Vector2i position) => position.Y < 20;
private bool BorderCheck(Vector2i position) => position.X >= 0 && position.X < 10;
private bool ClearCheck(Vector2i position) => _field.All(block => !position.Equals(block.Position));
private bool DropCheck(Vector2i position) => LowerBoundCheck(position) && ClearCheck(position);
private bool MoveCheck(Vector2i position) => BorderCheck(position) && ClearCheck(position);
private bool RotateCheck(Vector2i position) => BorderCheck(position) && LowerBoundCheck(position) && ClearCheck(position);
public void ProcessInput(BlockGamePlayerAction action)
{
if (_running)
{
switch (action)
{
case BlockGamePlayerAction.StartLeft:
_leftPressed = true;
break;
case BlockGamePlayerAction.StartRight:
_rightPressed = true;
break;
case BlockGamePlayerAction.Rotate:
TrySetRotation(Next(_currentRotation, false));
break;
case BlockGamePlayerAction.CounterRotate:
TrySetRotation(Next(_currentRotation, true));
break;
case BlockGamePlayerAction.SoftdropStart:
_softDropPressed = true;
if (_accumulatedFieldFrameTime > Speed) _accumulatedFieldFrameTime = Speed; //to prevent jumps
break;
case BlockGamePlayerAction.Harddrop:
PerformHarddrop();
break;
case BlockGamePlayerAction.Hold:
HoldPiece();
break;
}
}
switch (action)
{
case BlockGamePlayerAction.EndLeft:
_leftPressed = false;
break;
case BlockGamePlayerAction.EndRight:
_rightPressed = false;
break;
case BlockGamePlayerAction.SoftdropEnd:
_softDropPressed = false;
break;
case BlockGamePlayerAction.Pause:
_running = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, _started));
break;
case BlockGamePlayerAction.Unpause:
if (!_gameOver && _started)
{
_running = true;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
}
break;
case BlockGamePlayerAction.ShowHighscores:
_running = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Highscores, _started));
break;
}
}
private void TrySetRotation(BlockGamePieceRotation rotation)
{
if(!_running) return;
if (!_currentPiece.CanSpin) return;
if (!_currentPiece.Positions(_currentPiecePosition, rotation)
.All(RotateCheck))
{
return;
}
_currentRotation = rotation;
UpdateAllFieldUI();
}
private void HoldPiece()
{
if (!_running) return;
if (_holdBlock) return;
var tempHeld = _heldPiece;
_heldPiece = _currentPiece;
_holdBlock = true;
if (!tempHeld.HasValue)
{
InitializeNewBlock();
return;
}
InitializeNewBlock(tempHeld.Value);
}
private void PerformHarddrop()
{
int spacesDropped = 0;
while (_currentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation)
.All(DropCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToY(1);
spacesDropped++;
}
AddPoints(spacesDropped * 2);
InternalFieldTick();
}
public void UpdateAllFieldUI()
{
if (!_started) return;
var computedField = ComputeField();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField));
}
public void UpdateFieldUI(IPlayerSession session)
{
if (!_started) return;
var computedField = ComputeField();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField), session);
}
private bool IsGameOver => _field.Any(block => block.Position.Y == 0);
private void InvokeGameover()
{
_running = false;
_gameOver = true;
if (_component._player?.AttachedEntity != null)
{
var blockGameSystem = EntitySystem.Get<BlockGameSystem>();
_highScorePlacement = blockGameSystem.RegisterHighScore(_component._player.AttachedEntity.Name, Points);
SendHighscoreUpdate();
}
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement));
}
public void UpdateNewPlayerUI(IPlayerSession session)
{
if (_gameOver)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement), session);
}
else
{
if (Paused)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started), session);
}
else
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game, Started), session);
}
}
FullUpdate(session);
}
public List<BlockGameBlock> ComputeField()
{
var result = new List<BlockGameBlock>();
result.AddRange(_field);
result.AddRange(_currentPiece.Blocks(_currentPiecePosition, _currentRotation));
return result;
}
private enum BlockGamePieceType
{
I,
L,
LInverted,
S,
SInverted,
T,
O
}
private enum BlockGamePieceRotation
{
North,
East,
South,
West
}
private static BlockGamePieceRotation Next(BlockGamePieceRotation rotation, bool inverted)
{
return rotation switch
{
BlockGamePieceRotation.North => inverted ? BlockGamePieceRotation.West : BlockGamePieceRotation.East,
BlockGamePieceRotation.East => inverted ? BlockGamePieceRotation.North : BlockGamePieceRotation.South,
BlockGamePieceRotation.South => inverted ? BlockGamePieceRotation.East : BlockGamePieceRotation.West,
BlockGamePieceRotation.West => inverted ? BlockGamePieceRotation.South : BlockGamePieceRotation.North,
_ => throw new ArgumentOutOfRangeException(nameof(rotation), rotation, null)
};
}
private readonly BlockGamePieceType[] _allBlockGamePieces;
private List<BlockGamePieceType> _blockGamePiecesBuffer = new();
private BlockGamePiece GetRandomBlockGamePiece(IRobustRandom random)
{
if (_blockGamePiecesBuffer.Count == 0)
{
_blockGamePiecesBuffer = _allBlockGamePieces.ToList();
}
var chosenPiece = random.Pick(_blockGamePiecesBuffer);
_blockGamePiecesBuffer.Remove(chosenPiece);
return BlockGamePiece.GetPiece(chosenPiece);
}
private struct BlockGamePiece
{
public Vector2i[] Offsets;
private BlockGameBlock.BlockGameBlockColor _gameBlockColor;
public bool CanSpin;
public Vector2i[] Positions(Vector2i center,
BlockGamePieceRotation rotation)
{
return RotatedOffsets(rotation).Select(v => center + v).ToArray();
}
private Vector2i[] RotatedOffsets(BlockGamePieceRotation rotation)
{
Vector2i[] rotatedOffsets = (Vector2i[])Offsets.Clone();
//until i find a better algo
var amount = rotation switch
{
BlockGamePieceRotation.North => 0,
BlockGamePieceRotation.East => 1,
BlockGamePieceRotation.South => 2,
BlockGamePieceRotation.West => 3,
_ => 0
};
for (var i = 0; i < amount; i++)
{
for (var j = 0; j < rotatedOffsets.Length; j++)
{
rotatedOffsets[j] = rotatedOffsets[j].Rotate90DegreesAsOffset();
}
}
return rotatedOffsets;
}
public BlockGameBlock[] Blocks(Vector2i center,
BlockGamePieceRotation rotation)
{
var positions = Positions(center, rotation);
var result = new BlockGameBlock[positions.Length];
var i = 0;
foreach (var position in positions)
{
result[i++] = position.ToBlockGameBlock(_gameBlockColor);
}
return result;
}
public BlockGameBlock[] BlocksForPreview()
{
var xOffset = 0;
var yOffset = 0;
foreach (var offset in Offsets)
{
if (offset.X < xOffset) xOffset = offset.X;
if (offset.Y < yOffset) yOffset = offset.Y;
}
return Blocks(new Vector2i(-xOffset, -yOffset), BlockGamePieceRotation.North);
}
public static BlockGamePiece GetPiece(BlockGamePieceType type)
{
//switch statement, hardcoded offsets
return type switch
{
BlockGamePieceType.I => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(0, 2),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.LightBlue,
CanSpin = true
},
BlockGamePieceType.L => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(1, 1),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Orange,
CanSpin = true
},
BlockGamePieceType.LInverted => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(-1, 1),
new Vector2i(0, 1),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Blue,
CanSpin = true
},
BlockGamePieceType.S => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(-1, 0),
new Vector2i(0, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Green,
CanSpin = true
},
BlockGamePieceType.SInverted => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(-1, -1), new Vector2i(0, -1), new Vector2i(0, 0),
new Vector2i(1, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Red,
CanSpin = true
},
BlockGamePieceType.T => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1),
new Vector2i(-1, 0), new Vector2i(0, 0), new Vector2i(1, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Purple,
CanSpin = true
},
BlockGamePieceType.O => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(0, 0),
new Vector2i(1, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Yellow,
CanSpin = false
},
_ => new BlockGamePiece {Offsets = new[] {new Vector2i(0, 0)}}
};
}
}
}
}
}

View File

@@ -1,30 +0,0 @@
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Random;
namespace Content.Server.GameObjects.Components.Arcade
{
[RegisterComponent]
public class RandomArcadeGameComponent : Component, IMapInit
{
public override string Name => "RandomArcade";
public void MapInit()
{
var arcades = new[]
{
"BlockGameArcade",
"SpaceVillainArcade"
};
var entityManager = IoCManager.Resolve<IEntityManager>();
entityManager.SpawnEntity(
IoCManager.Resolve<IRobustRandom>().Pick(arcades),
Owner.Transform.Coordinates);
Owner.Delete();
}
}
}

View File

@@ -1,442 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.Components.VendingMachines;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Arcade;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Arcade
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent, IActivate, IWires
{
[Dependency] private readonly IRobustRandom _random = null!;
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
[ComponentDependency] private readonly WiresComponent? _wiresComponent = default!;
private bool Powered => _powerReceiverComponent != null && _powerReceiverComponent.Powered;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SpaceVillainArcadeUiKey.Key);
[ViewVariables] private bool _overflowFlag;
[ViewVariables] private bool _playerInvincibilityFlag;
[ViewVariables] private bool _enemyInvincibilityFlag;
[ViewVariables] private SpaceVillainGame _game = null!;
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleFightVerbs")] private List<string> _possibleFightVerbs = new List<string>()
{"Defeat", "Annihilate", "Save", "Strike", "Stop", "Destroy", "Robust", "Romance", "Pwn", "Own"};
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleFirstEnemyNames")] private List<string> _possibleFirstEnemyNames = new List<string>(){
"the Automatic", "Farmer", "Lord", "Professor", "the Cuban", "the Evil", "the Dread King",
"the Space", "Lord", "the Great", "Duke", "General"
};
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleLastEnemyNames")] private List<string> _possibleLastEnemyNames = new List<string>()
{
"Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid",
"Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn"
};
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleRewards")] private List<string> _possibleRewards = new List<string>()
{
"ToyMouse", "ToyAi", "ToyNuke", "ToyAssistant", "ToyGriffin", "ToyHonk", "ToyIan",
"ToyMarauder", "ToyMauler", "ToyGygax", "ToyOdysseus", "ToyOwlman", "ToyDeathRipley",
"ToyPhazon", "ToyFireRipley", "ToyReticence", "ToyRipley", "ToySeraph", "ToyDurand", "ToySkeleton"
};
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if(!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!Powered)
{
return;
}
if(!ActionBlockerSystem.CanInteract(actor.PlayerSession.AttachedEntity)) return;
_game ??= new SpaceVillainGame(this);
if (_wiresComponent?.IsPanelOpen == true)
{
_wiresComponent.OpenInterface(actor.PlayerSession);
} else
{
UserInterface?.Toggle(actor.PlayerSession);
}
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
OnOnPowerStateChanged(powerChanged);
break;
}
}
private void OnOnPowerStateChanged(PowerChangedMessage e)
{
if(e.Powered) return;
UserInterface?.CloseAll();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
if (!Powered)
return;
if (serverMsg.Message is not SpaceVillainArcadePlayerActionMessage msg) return;
switch (msg.PlayerAction)
{
case PlayerAction.Attack:
_game?.ExecutePlayerAction(msg.PlayerAction);
break;
case PlayerAction.Heal:
_game?.ExecutePlayerAction(msg.PlayerAction);
break;
case PlayerAction.Recharge:
_game?.ExecutePlayerAction(msg.PlayerAction);
break;
case PlayerAction.NewGame:
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Effects/Arcade/newgame.ogg", Owner, AudioParams.Default.WithVolume(-4f));
_game = new SpaceVillainGame(this);
UserInterface?.SendMessage(_game.GenerateMetaDataMessage());
break;
case PlayerAction.RequestData:
UserInterface?.SendMessage(_game.GenerateMetaDataMessage());
break;
}
}
public enum Wires
{
/// <summary>
/// Disables Max Health&Mana for both Enemy and Player.
/// </summary>
Overflow,
/// <summary>
/// Makes Player Invincible.
/// </summary>
PlayerInvincible,
/// <summary>
/// Makes Enemy Invincible.
/// </summary>
EnemyInvincible
}
public void RegisterWires(WiresComponent.WiresBuilder builder)
{
builder.CreateWire(Wires.Overflow);
builder.CreateWire(Wires.PlayerInvincible);
builder.CreateWire(Wires.EnemyInvincible);
builder.CreateWire(4);
builder.CreateWire(5);
builder.CreateWire(6);
IndicatorUpdate();
}
public void WiresUpdate(WiresUpdateEventArgs args)
{
var wire = (Wires) args.Identifier;
var value = args.Action != SharedWiresComponent.WiresAction.Mend;
switch (wire)
{
case Wires.Overflow:
_overflowFlag = value;
break;
case Wires.PlayerInvincible:
_playerInvincibilityFlag = value;
break;
case Wires.EnemyInvincible:
_enemyInvincibilityFlag = value;
break;
}
IndicatorUpdate();
}
public void IndicatorUpdate()
{
_wiresComponent?.SetStatus(Indicators.HealthManager,
new SharedWiresComponent.StatusLightData(Color.Purple,
_playerInvincibilityFlag || _enemyInvincibilityFlag
? SharedWiresComponent.StatusLightState.BlinkingSlow
: SharedWiresComponent.StatusLightState.On,
"MNGR"));
_wiresComponent?.SetStatus(Indicators.HealthLimiter,
new SharedWiresComponent.StatusLightData(Color.Red,
_overflowFlag
? SharedWiresComponent.StatusLightState.BlinkingSlow
: SharedWiresComponent.StatusLightState.On,
"LIMT"));
}
/// <summary>
/// Called when the user wins the game.
/// </summary>
public void ProcessWin()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
entityManager.SpawnEntity(_random.Pick(_possibleRewards), Owner.Transform.MapPosition);
}
/// <summary>
/// Picks a fight-verb from the list of possible Verbs.
/// </summary>
/// <returns>A fight-verb.</returns>
public string GenerateFightVerb()
{
return _random.Pick(_possibleFightVerbs);
}
/// <summary>
/// Generates an enemy-name comprised of a first- and last-name.
/// </summary>
/// <returns>An enemy-name.</returns>
public string GenerateEnemyName()
{
return $"{_random.Pick(_possibleFirstEnemyNames)} {_random.Pick(_possibleLastEnemyNames)}";
}
/// <summary>
/// A Class to handle all the game-logic of the SpaceVillain-game.
/// </summary>
public class SpaceVillainGame
{
[Dependency] private readonly IRobustRandom _random = default!;
[ViewVariables] private readonly SpaceVillainArcadeComponent _owner;
[ViewVariables] public string Name => $"{_fightVerb} {_enemyName}";
[ViewVariables(VVAccess.ReadWrite)] private int _playerHp = 30;
[ViewVariables(VVAccess.ReadWrite)] private int _playerHpMax = 30;
[ViewVariables(VVAccess.ReadWrite)] private int _playerMp = 10;
[ViewVariables(VVAccess.ReadWrite)] private int _playerMpMax = 10;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyHp = 45;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyHpMax = 45;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyMp = 20;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyMpMax = 20;
[ViewVariables(VVAccess.ReadWrite)] private int _turtleTracker;
[ViewVariables(VVAccess.ReadWrite)] private readonly string _fightVerb;
[ViewVariables(VVAccess.ReadWrite)] private readonly string _enemyName;
[ViewVariables] private bool _running = true;
private string _latestPlayerActionMessage = "";
private string _latestEnemyActionMessage = "";
public SpaceVillainGame(SpaceVillainArcadeComponent owner) : this(owner, owner.GenerateFightVerb(), owner.GenerateEnemyName()){}
public SpaceVillainGame(SpaceVillainArcadeComponent owner, string fightVerb, string enemyName)
{
IoCManager.InjectDependencies(this);
_owner = owner;
//todo defeat the curse secret game mode
_fightVerb = fightVerb;
_enemyName = enemyName;
}
/// <summary>
/// Validates all vars incase they overshoot their max-values.
/// Does not check if vars surpass 0.
/// </summary>
private void ValidateVars()
{
if(_owner._overflowFlag) return;
if (_playerHp > _playerHpMax) _playerHp = _playerHpMax;
if (_playerMp > _playerMpMax) _playerMp = _playerMpMax;
if (_enemyHp > _enemyHpMax) _enemyHp = _enemyHpMax;
if (_enemyMp > _enemyMpMax) _enemyMp = _enemyMpMax;
}
/// <summary>
/// Called by the SpaceVillainArcadeComponent when Userinput is received.
/// </summary>
/// <param name="action">The action the user picked.</param>
public void ExecutePlayerAction(PlayerAction action)
{
if (!_running) return;
switch (action)
{
case PlayerAction.Attack:
var attackAmount = _random.Next(2, 6);
_latestPlayerActionMessage = Loc.GetString("You attack {0} for {1}!", _enemyName, attackAmount);
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/player_attack.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
if(!_owner._enemyInvincibilityFlag) _enemyHp -= attackAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
break;
case PlayerAction.Heal:
var pointAmount = _random.Next(1, 3);
var healAmount = _random.Next(6, 8);
_latestPlayerActionMessage = Loc.GetString("You use {0} magic to heal for {1} damage!", pointAmount, healAmount);
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/player_heal.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
if(!_owner._playerInvincibilityFlag) _playerMp -= pointAmount;
_playerHp += healAmount;
_turtleTracker++;
break;
case PlayerAction.Recharge:
var chargeAmount = _random.Next(4, 7);
_latestPlayerActionMessage = Loc.GetString("You regain {0} points", chargeAmount);
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/player_charge.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
_playerMp += chargeAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
break;
}
if (!CheckGameConditions())
{
return;
}
ValidateVars();
ExecuteAiAction();
if (!CheckGameConditions())
{
return;
}
ValidateVars();
UpdateUi();
}
/// <summary>
/// Checks the Game conditions and Updates the Ui & Plays a sound accordingly.
/// </summary>
/// <returns>A bool indicating if the game should continue.</returns>
private bool CheckGameConditions()
{
if ((_playerHp > 0 && _playerMp > 0) && (_enemyHp <= 0 || _enemyMp <= 0))
{
_running = false;
UpdateUi(Loc.GetString("You won!"), Loc.GetString("{0} dies.", _enemyName), true);
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/win.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
_owner.ProcessWin();
return false;
}
if (_playerHp > 0 && _playerMp > 0) return true;
if ((_enemyHp > 0 && _enemyMp > 0))
{
_running = false;
UpdateUi(Loc.GetString("You lost!"), Loc.GetString("{0} cheers.", _enemyName), true);
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/gameover.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
return false;
}
if (_enemyHp <= 0 || _enemyMp <= 0)
{
_running = false;
UpdateUi(Loc.GetString("You lost!"), Loc.GetString("{0} dies, but takes you with him.", _enemyName), true);
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/gameover.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
return false;
}
return true;
}
/// <summary>
/// Updates the UI.
/// </summary>
private void UpdateUi(bool metadata = false)
{
_owner.UserInterface?.SendMessage(metadata ? GenerateMetaDataMessage() : GenerateUpdateMessage());
}
private void UpdateUi(string message1, string message2, bool metadata = false)
{
_latestPlayerActionMessage = message1;
_latestEnemyActionMessage = message2;
UpdateUi(metadata);
}
/// <summary>
/// Handles the logic of the AI
/// </summary>
/// <returns>An Enemyaction-message.</returns>
private void ExecuteAiAction()
{
if (_turtleTracker >= 4)
{
var boomAmount = _random.Next(5, 10);
_latestEnemyActionMessage = Loc.GetString("{0} throws a bomb, exploding you for {1} damage!", _enemyName, boomAmount);
if (_owner._playerInvincibilityFlag) return;
_playerHp -= boomAmount;
_turtleTracker--;
}else if (_enemyMp <= 5 && _random.Prob(0.7f))
{
var stealAmount = _random.Next(2, 3);
_latestEnemyActionMessage = Loc.GetString("{0} steals {1} of your power!", _enemyName, stealAmount);
if (_owner._playerInvincibilityFlag) return;
_playerMp -= stealAmount;
_enemyMp += stealAmount;
}else if (_enemyHp <= 10 && _enemyMp > 4)
{
_enemyHp += 4;
_enemyMp -= 4;
_latestEnemyActionMessage = Loc.GetString("{0} heals for 4 health!", _enemyName);
}
else
{
var attackAmount = _random.Next(3, 6);
_latestEnemyActionMessage =
Loc.GetString("{0} attacks you for {1} damage!", _enemyName, attackAmount);
if (_owner._playerInvincibilityFlag) return;
_playerHp -= attackAmount;
}
}
/// <summary>
/// Generates a Metadata-message based on the objects values.
/// </summary>
/// <returns>A Metadata-message.</returns>
public SpaceVillainArcadeMetaDataUpdateMessage GenerateMetaDataMessage()
{
return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage, _latestEnemyActionMessage, Name, _enemyName, !_running);
}
/// <summary>
/// Creates an Update-message based on the objects values.
/// </summary>
/// <returns>An Update-Message.</returns>
public SpaceVillainArcadeDataUpdateMessage
GenerateUpdateMessage()
{
return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage,
_latestEnemyActionMessage);
}
}
}
}

View File

@@ -1,6 +1,6 @@
#nullable enable
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Temperature;
using Content.Server.Temperature.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;

View File

@@ -1,11 +1,11 @@
using System;
using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.Interfaces.GameObjects;
using Content.Server.Alert;
using Content.Server.Pressure;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Atmos

View File

@@ -1,7 +1,6 @@
#nullable enable
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Server.Body.Respiratory;
using Content.Shared.Inventory;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;

View File

@@ -1,12 +1,12 @@
#nullable enable
using Content.Server.GameObjects.Components.Doors;
using Content.Server.Interfaces.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Content.Server.Atmos;
using Content.Server.Doors;
using Content.Server.Doors.Components;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Atmos

View File

@@ -1,19 +1,20 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Alert;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Temperature;
using Content.Server.Stunnable.Components;
using Content.Server.Temperature.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Chemistry;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Temperature;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;

View File

@@ -2,14 +2,15 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Server.Utility;
using Content.Server.Hands.Components;
using Content.Server.UserInterface;
using Content.Shared.Atmos;
using Content.Shared.DragDrop;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;

View File

@@ -5,11 +5,11 @@ using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.Interfaces;
using Content.Server.Utility;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;

View File

@@ -2,19 +2,22 @@
#nullable disable warnings
using System;
using Content.Server.Atmos;
using Content.Server.Explosions;
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Server.Body.Respiratory;
using Content.Server.Explosion;
using Content.Server.Interfaces;
using Content.Server.Utility;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
using Content.Shared.Actions.Behaviors.Item;
using Content.Shared.Actions.Components;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.DragDrop;
using Content.Shared.Examine;
using Content.Shared.GameObjects.Components.Atmos.GasTank;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interaction;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;

View File

@@ -7,9 +7,9 @@ using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.EntitySystems.Atmos;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Content.Shared.Maps;
using Robust.Server.GameObjects;

View File

@@ -2,7 +2,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.MobState;
using Content.Shared.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;

View File

@@ -1,8 +1,8 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;

View File

@@ -2,8 +2,8 @@
using System;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Atmos;
using Robust.Server.GameObjects;

View File

@@ -1,7 +1,7 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;

View File

@@ -1,7 +1,7 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;

View File

@@ -1,10 +1,10 @@
#nullable enable
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.GameObjects.Components.Atmos;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;

View File

@@ -1,7 +1,7 @@
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;

View File

@@ -1,7 +1,7 @@
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;

View File

@@ -1,9 +1,9 @@
#nullable enable
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.GameObjects.Components.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;

View File

@@ -1,9 +1,9 @@
#nullable enable
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.GameObjects.Components.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;

View File

@@ -1,4 +1,4 @@
using Content.Server.Interfaces.GameObjects;
using Content.Server.Pressure;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;

View File

@@ -2,7 +2,7 @@
using System;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos.Piping;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;

View File

@@ -1,117 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.BarSign
{
[RegisterComponent]
public class BarSignComponent : Component, IMapInit
{
public override string Name => "BarSign";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[DataField("current")]
private string? _currentSign;
[ViewVariables(VVAccess.ReadWrite)]
public string? CurrentSign
{
get => _currentSign;
set
{
_currentSign = value;
UpdateSignInfo();
}
}
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private void UpdateSignInfo()
{
if (_currentSign == null)
{
return;
}
if (!_prototypeManager.TryIndex(_currentSign, out BarSignPrototype? prototype))
{
Logger.ErrorS("barSign", $"Invalid bar sign prototype: \"{_currentSign}\"");
return;
}
if (Owner.TryGetComponent(out SpriteComponent? sprite))
{
if (!Powered)
{
sprite.LayerSetState(0, "empty");
sprite.LayerSetShader(0, "shaded");
}
else
{
sprite.LayerSetState(0, prototype.Icon);
sprite.LayerSetShader(0, "unshaded");
}
}
if (!string.IsNullOrEmpty(prototype.Name))
{
Owner.Name = prototype.Name;
}
else
{
Owner.Name = Loc.GetString("bar sign");
}
Owner.Description = prototype.Description;
}
public override void Initialize()
{
base.Initialize();
UpdateSignInfo();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
PowerOnOnPowerStateChanged(powerChanged);
break;
}
}
private void PowerOnOnPowerStateChanged(PowerChangedMessage e)
{
UpdateSignInfo();
}
public void MapInit()
{
if (_currentSign != null)
{
return;
}
var prototypes = _prototypeManager.EnumeratePrototypes<BarSignPrototype>().Where(p => !p.Hidden)
.ToList();
var prototype = _robustRandom.Pick(prototypes);
CurrentSign = prototype.ID;
}
}
}

View File

@@ -1,41 +0,0 @@
#nullable enable
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.BarSign
{
[Prototype("barSign")]
public class BarSignPrototype : IPrototype
{
private string _description = "";
private string _name = "";
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[DataField("icon")] public string Icon { get; private set; } = "";
[DataField("name")]
public string Name
{
get => _name;
private set => _name = Loc.GetString(value);
}
[DataField("description")]
public string Description
{
get => _description;
private set => _description = Loc.GetString(value);
}
[DataField("renameArea")]
public bool RenameArea { get; private set; } = true;
[DataField("hidden")]
public bool Hidden { get; private set; }
}
}

View File

@@ -1,70 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Movement;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Body.Behavior
{
public class BrainBehavior : MechanismBehavior
{
protected override void OnAddedToBody(IBody body)
{
base.OnAddedToBody(body);
HandleMind(body.Owner, Owner);
}
protected override void OnAddedToPart(IBodyPart part)
{
base.OnAddedToPart(part);
HandleMind(part.Owner, Owner);
}
protected override void OnAddedToPartInBody(IBody body, IBodyPart part)
{
base.OnAddedToPartInBody(body, part);
HandleMind(body.Owner, Owner);
}
protected override void OnRemovedFromBody(IBody old)
{
base.OnRemovedFromBody(old);
HandleMind(Part!.Owner, old.Owner);
}
protected override void OnRemovedFromPart(IBodyPart old)
{
base.OnRemovedFromPart(old);
HandleMind(Owner, old.Owner);
}
protected override void OnRemovedFromPartInBody(IBody oldBody, IBodyPart oldPart)
{
base.OnRemovedFromPartInBody(oldBody, oldPart);
HandleMind(oldBody.Owner, Owner);
}
private void HandleMind(IEntity newEntity, IEntity oldEntity)
{
newEntity.EnsureComponent<MindComponent>();
var oldMind = oldEntity.EnsureComponent<MindComponent>();
if (!newEntity.HasComponent<IGhostOnMove>())
newEntity.AddComponent<GhostOnMoveComponent>();
// TODO: This is an awful solution.
if (!newEntity.HasComponent<IMoverComponent>())
newEntity.AddComponent<SharedDummyInputMoverComponent>();
oldMind.Mind?.TransferTo(newEntity);
}
}
}

View File

@@ -1,29 +0,0 @@
using Content.Shared.GameObjects.Components.Body.Networks;
namespace Content.Server.GameObjects.Components.Body.Behavior
{
public class HeartBehavior : MechanismBehavior
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
// TODO BODY do between pre and metabolism
if (Parent.Body == null ||
!Parent.Body.Owner.HasComponent<SharedBloodstreamComponent>())
{
return;
}
// Update at most once per second
_accumulatedFrameTime += frameTime;
// TODO: Move/accept/process bloodstream reagents only when the heart is pumping
if (_accumulatedFrameTime >= 1)
{
// bloodstream.Update(_accumulatedFrameTime);
_accumulatedFrameTime -= 1;
}
}
}
}

View File

@@ -1,111 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body.Networks;
using Content.Shared.Interfaces.Chemistry;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Behavior
{
/// <summary>
/// Metabolizes reagents in <see cref="SharedBloodstreamComponent"/> after they are digested.
/// </summary>
public class LiverBehavior : MechanismBehavior
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private float _accumulatedFrameTime;
/// <summary>
/// Whether the liver is functional.
/// </summary>
//[ViewVariables] private bool _liverFailing = false;
/// <summary>
/// Modifier for alcohol damage.
/// </summary>
//[DataField("alcoholLethality")]
//[ViewVariables] private float _alcoholLethality = 0.005f;
/// <summary>
/// Modifier for alcohol damage.
/// </summary>
//[DataField("alcoholExponent")]
//[ViewVariables] private float _alcoholExponent = 1.6f;
/// <summary>
/// Toxin volume that can be purged without damage.
/// </summary>
//[DataField("toxinTolerance")]
//[ViewVariables] private float _toxinTolerance = 3f;
/// <summary>
/// Toxin damage modifier.
/// </summary>
//[DataField("toxinLethality")]
//[ViewVariables] private float _toxinLethality = 0.01f;
/// <summary>
/// Loops through each reagent in _internalSolution,
/// and calls <see cref="IMetabolizable.Metabolize"/> for each of them.
/// Also handles toxins and alcohol.
/// </summary>
/// <param name="frameTime">
/// The time since the last update in seconds.
/// </param>
public override void Update(float frameTime)
{
if (Body == null)
{
return;
}
_accumulatedFrameTime += frameTime;
// Update at most once per second
if (_accumulatedFrameTime < 1)
{
return;
}
_accumulatedFrameTime -= 1;
if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
{
return;
}
if (bloodstream.Solution.CurrentVolume <= ReagentUnit.Zero)
{
return;
}
// Run metabolism for each reagent, remove metabolized reagents
// Using ToList here lets us edit reagents while iterating
foreach (var reagent in bloodstream.Solution.ReagentList.ToList())
{
if (!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? prototype))
{
continue;
}
//TODO BODY Check if it's a Toxin. If volume < _toxinTolerance, just remove it. If greater, add damage = volume * _toxinLethality
//TODO BODY Check if it has BoozePower > 0. Affect drunkenness, apply damage. Proposed formula (SS13-derived): damage = sqrt(volume) * BoozePower^_alcoholExponent * _alcoholLethality / 10
//TODO BODY Liver failure.
//TODO Make sure reagent prototypes actually have the toxin and boozepower vars set.
// Run metabolism code for each reagent
foreach (var metabolizable in prototype.Metabolism)
{
var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, frameTime);
bloodstream.Solution.TryRemoveReagent(reagent.ReagentId, reagentDelta);
}
}
}
}
}

View File

@@ -1,202 +0,0 @@
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Server.Utility;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Behavior
{
[DataDefinition]
public class LungBehavior : MechanismBehavior
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
private float _accumulatedFrameTime;
[ViewVariables] private TimeSpan _lastGaspPopupTime;
[DataField("air")]
[ViewVariables]
public GasMixture Air { get; set; } = new()
{
Volume = 6,
Temperature = Atmospherics.NormalBodyTemperature
};
[DataField("gaspPopupCooldown")]
[ViewVariables]
public TimeSpan GaspPopupCooldown { get; private set; } = TimeSpan.FromSeconds(8);
[ViewVariables] public LungStatus Status { get; set; }
[DataField("cycleDelay")]
[ViewVariables]
public float CycleDelay { get; set; } = 2;
public LungBehavior()
{
IoCManager.InjectDependencies(this);
}
protected override void OnAddedToBody(IBody body)
{
base.OnAddedToBody(body);
Inhale(CycleDelay);
}
public void Gasp()
{
if (_gameTiming.CurTime >= _lastGaspPopupTime + GaspPopupCooldown)
{
_lastGaspPopupTime = _gameTiming.CurTime;
Owner.PopupMessageEveryone(Loc.GetString("Gasp"));
}
Inhale(CycleDelay);
}
public void Transfer(GasMixture from, GasMixture to, float ratio)
{
to.Merge(from.RemoveRatio(ratio));
}
public void ToBloodstream(GasMixture mixture)
{
if (Body == null)
{
return;
}
if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
{
return;
}
var to = bloodstream.Air;
to.Merge(mixture);
mixture.Clear();
}
public override void Update(float frameTime)
{
if (Body != null && Body.Owner.TryGetComponent(out IMobStateComponent? mobState) && mobState.IsCritical())
{
return;
}
if (Status == LungStatus.None)
{
Status = LungStatus.Inhaling;
}
_accumulatedFrameTime += Status switch
{
LungStatus.Inhaling => frameTime,
LungStatus.Exhaling => -frameTime,
_ => throw new ArgumentOutOfRangeException()
};
var absoluteTime = Math.Abs(_accumulatedFrameTime);
var delay = CycleDelay;
if (absoluteTime < delay)
{
return;
}
switch (Status)
{
case LungStatus.Inhaling:
Inhale(absoluteTime);
Status = LungStatus.Exhaling;
break;
case LungStatus.Exhaling:
Exhale(absoluteTime);
Status = LungStatus.Inhaling;
break;
default:
throw new ArgumentOutOfRangeException();
}
_accumulatedFrameTime = absoluteTime - delay;
}
public void Inhale(float frameTime)
{
if (Body != null &&
Body.Owner.TryGetComponent(out InternalsComponent? internals) &&
internals.BreathToolEntity != null &&
internals.GasTankEntity != null &&
internals.BreathToolEntity.TryGetComponent(out BreathToolComponent? breathTool) &&
breathTool.IsFunctional &&
internals.GasTankEntity.TryGetComponent(out GasTankComponent? gasTank) &&
gasTank.Air != null)
{
Inhale(frameTime, gasTank.RemoveAirVolume(Atmospherics.BreathVolume));
return;
}
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
{
return;
}
Inhale(frameTime, tileAir);
}
public void Inhale(float frameTime, GasMixture from)
{
var ratio = (Atmospherics.BreathVolume / from.Volume) * frameTime;
Transfer(from, Air, ratio);
ToBloodstream(Air);
}
public void Exhale(float frameTime)
{
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
{
return;
}
Exhale(frameTime, tileAir);
}
public void Exhale(float frameTime, GasMixture to)
{
// TODO: Make the bloodstream separately pump toxins into the lungs, making the lungs' only job to empty.
if (Body == null)
{
return;
}
if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
{
return;
}
bloodstream.PumpToxins(Air);
var lungRemoved = Air.RemoveRatio(0.5f);
to.Merge(lungRemoved);
}
}
public enum LungStatus
{
None = 0,
Inhaling,
Exhaling
}
}

View File

@@ -1,109 +0,0 @@
#nullable enable
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Behavior;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Body.Part;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.Components.Body.Behavior
{
public abstract class MechanismBehavior : IMechanismBehavior
{
public IBody? Body => Part?.Body;
public IBodyPart? Part => Parent.Part;
public IMechanism Parent { get; private set; } = default!;
public IEntity Owner => Parent.Owner;
public virtual void Initialize(IMechanism parent)
{
Parent = parent;
}
public virtual void Startup()
{
if (Part == null)
{
return;
}
if (Body == null)
{
AddedToPart(Part);
}
else
{
AddedToPartInBody(Body, Part);
}
}
public void AddedToBody(IBody body)
{
DebugTools.AssertNotNull(Body);
DebugTools.AssertNotNull(body);
OnAddedToBody(body);
}
public void AddedToPart(IBodyPart part)
{
DebugTools.AssertNotNull(Part);
DebugTools.AssertNotNull(part);
OnAddedToPart(part);
}
public void AddedToPartInBody(IBody body, IBodyPart part)
{
DebugTools.AssertNotNull(Body);
DebugTools.AssertNotNull(body);
DebugTools.AssertNotNull(Part);
DebugTools.AssertNotNull(part);
OnAddedToPartInBody(body, part);
}
public void RemovedFromBody(IBody old)
{
DebugTools.AssertNull(Body);
DebugTools.AssertNotNull(old);
OnRemovedFromBody(old);
}
public void RemovedFromPart(IBodyPart old)
{
DebugTools.AssertNull(Part);
DebugTools.AssertNotNull(old);
OnRemovedFromPart(old);
}
public void RemovedFromPartInBody(IBody oldBody, IBodyPart oldPart)
{
DebugTools.AssertNull(Body);
DebugTools.AssertNull(Part);
DebugTools.AssertNotNull(oldBody);
DebugTools.AssertNotNull(oldPart);
OnRemovedFromPartInBody(oldBody, oldPart);
}
protected virtual void OnAddedToBody(IBody body) { }
protected virtual void OnAddedToPart(IBodyPart part) { }
protected virtual void OnAddedToPartInBody(IBody body, IBodyPart part) { }
protected virtual void OnRemovedFromBody(IBody old) { }
protected virtual void OnRemovedFromPart(IBodyPart old) { }
protected virtual void OnRemovedFromPartInBody(IBody oldBody, IBodyPart oldPart) { }
public virtual void Update(float frameTime) { }
}
}

View File

@@ -1,74 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Behavior;
using Content.Shared.GameObjects.Components.Body.Part;
namespace Content.Server.GameObjects.Components.Body.Behavior
{
public static class MechanismExtensions
{
public static bool HasMechanismBehavior<T>(this IBody body) where T : IMechanismBehavior
{
return body.Parts.Any(p => p.Key.HasMechanismBehavior<T>());
}
public static bool HasMechanismBehavior<T>(this IBodyPart part) where T : IMechanismBehavior
{
return part.Mechanisms.Any(m => m.HasBehavior<T>());
}
public static IEnumerable<IMechanismBehavior> GetMechanismBehaviors(this IBody body)
{
foreach (var (part, _) in body.Parts)
foreach (var mechanism in part.Mechanisms)
foreach (var behavior in mechanism.Behaviors.Values)
{
yield return behavior;
}
}
public static bool TryGetMechanismBehaviors(this IBody body,
[NotNullWhen(true)] out List<IMechanismBehavior>? behaviors)
{
behaviors = body.GetMechanismBehaviors().ToList();
if (behaviors.Count == 0)
{
behaviors = null;
return false;
}
return true;
}
public static IEnumerable<T> GetMechanismBehaviors<T>(this IBody body) where T : class, IMechanismBehavior
{
foreach (var (part, _) in body.Parts)
foreach (var mechanism in part.Mechanisms)
foreach (var behavior in mechanism.Behaviors.Values)
{
if (behavior is T tBehavior)
{
yield return tBehavior;
}
}
}
public static bool TryGetMechanismBehaviors<T>(this IBody entity, [NotNullWhen(true)] out List<T>? behaviors)
where T : class, IMechanismBehavior
{
behaviors = entity.GetMechanismBehaviors<T>().ToList();
if (behaviors.Count == 0)
{
behaviors = null;
return false;
}
return true;
}
}
}

View File

@@ -1,174 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body.Networks;
using Content.Shared.GameObjects.Components.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Behavior
{
/// <summary>
/// Where reagents go when ingested. Tracks ingested reagents over time, and
/// eventually transfers them to <see cref="SharedBloodstreamComponent"/> once digested.
/// </summary>
public class StomachBehavior : MechanismBehavior
{
private float _accumulatedFrameTime;
/// <summary>
/// Updates digestion status of ingested reagents.
/// Once reagents surpass _digestionDelay they are moved to the
/// bloodstream, where they are then metabolized.
/// </summary>
/// <param name="frameTime">
/// The time since the last update in seconds.
/// </param>
public override void Update(float frameTime)
{
if (Body == null)
{
return;
}
_accumulatedFrameTime += frameTime;
// Update at most once per second
if (_accumulatedFrameTime < 1)
{
return;
}
_accumulatedFrameTime -= 1;
if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solution) ||
!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
{
return;
}
// Add reagents ready for transfer to bloodstream to transferSolution
var transferSolution = new Solution();
// Use ToList here to remove entries while iterating
foreach (var delta in _reagentDeltas.ToList())
{
//Increment lifetime of reagents
delta.Increment(1);
if (delta.Lifetime > _digestionDelay)
{
solution.TryRemoveReagent(delta.ReagentId, delta.Quantity);
transferSolution.AddReagent(delta.ReagentId, delta.Quantity);
_reagentDeltas.Remove(delta);
}
}
// Transfer digested reagents to bloodstream
bloodstream.TryTransferSolution(transferSolution);
}
/// <summary>
/// Max volume of internal solution storage
/// </summary>
public ReagentUnit MaxVolume
{
get => Owner.TryGetComponent(out SharedSolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
set
{
if (Owner.TryGetComponent(out SharedSolutionContainerComponent? solution))
{
solution.MaxVolume = value;
}
}
}
/// <summary>
/// Initial internal solution storage volume
/// </summary>
[DataField("maxVolume")]
[ViewVariables]
protected ReagentUnit InitialMaxVolume { get; private set; } = ReagentUnit.New(100);
/// <summary>
/// Time in seconds between reagents being ingested and them being
/// transferred to <see cref="SharedBloodstreamComponent"/>
/// </summary>
[DataField("digestionDelay")] [ViewVariables]
private float _digestionDelay = 20;
/// <summary>
/// Used to track how long each reagent has been in the stomach
/// </summary>
[ViewVariables]
private readonly List<ReagentDelta> _reagentDeltas = new();
public override void Startup()
{
base.Startup();
Owner.EnsureComponentWarn(out SolutionContainerComponent solution);
solution.MaxVolume = InitialMaxVolume;
}
public bool CanTransferSolution(Solution solution)
{
if (!Owner.TryGetComponent(out SharedSolutionContainerComponent? solutionComponent))
{
return false;
}
// TODO: For now no partial transfers. Potentially change by design
if (!solutionComponent.CanAddSolution(solution))
{
return false;
}
return true;
}
public bool TryTransferSolution(Solution solution)
{
if (Body == null || !CanTransferSolution(solution))
return false;
if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent))
{
return false;
}
// Add solution to _stomachContents
solutionComponent.TryAddSolution(solution);
// Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
foreach (var reagent in solution.Contents)
{
_reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
}
return true;
}
/// <summary>
/// Used to track quantity changes when ingesting & digesting reagents
/// </summary>
protected class ReagentDelta
{
public readonly string ReagentId;
public readonly ReagentUnit Quantity;
public float Lifetime { get; private set; }
public ReagentDelta(string reagentId, ReagentUnit quantity)
{
ReagentId = reagentId;
Quantity = quantity;
Lifetime = 0.0f;
}
public void Increment(float delta) => Lifetime += delta;
}
}
}

View File

@@ -1,129 +0,0 @@
#nullable enable
using System;
using Content.Server.Commands.Observer;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.Interfaces.GameTicking;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Body.Slot;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.Utility;
using Robust.Server.Console;
using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Content.Server.GameObjects.Components.Body
{
[RegisterComponent]
[ComponentReference(typeof(SharedBodyComponent))]
[ComponentReference(typeof(IBody))]
[ComponentReference(typeof(IGhostOnMove))]
public class BodyComponent : SharedBodyComponent, IRelayMoveInput, IGhostOnMove
{
private Container _partContainer = default!;
[Dependency] private readonly IGameTicker _gameTicker = default!;
protected override bool CanAddPart(string slotId, IBodyPart part)
{
return base.CanAddPart(slotId, part) &&
_partContainer.CanInsert(part.Owner);
}
protected override void OnAddPart(BodyPartSlot slot, IBodyPart part)
{
base.OnAddPart(slot, part);
_partContainer.Insert(part.Owner);
}
protected override void OnRemovePart(BodyPartSlot slot, IBodyPart part)
{
base.OnRemovePart(slot, part);
_partContainer.ForceRemove(part.Owner);
part.Owner.RandomOffset(0.25f);
}
public override void Initialize()
{
base.Initialize();
_partContainer = Owner.EnsureContainer<Container>($"{Name}-{nameof(BodyComponent)}");
var preset = Preset;
if (preset != null)
{
foreach (var slot in Slots)
{
// Using MapPosition instead of Coordinates here prevents
// a crash within the character preview menu in the lobby
var entity = Owner.EntityManager.SpawnEntity(preset.PartIDs[slot.Id], Owner.Transform.MapPosition);
if (!entity.TryGetComponent(out IBodyPart? part))
{
Logger.Error($"Entity {slot.Id} does not have a {nameof(IBodyPart)} component.");
continue;
}
SetPart(slot.Id, part);
}
}
}
protected override void Startup()
{
base.Startup();
// This is ran in Startup as entities spawned in Initialize
// are not synced to the client since they are assumed to be
// identical on it
foreach (var (part, _) in Parts)
{
part.Dirty();
}
}
void IRelayMoveInput.MoveInputPressed(ICommonSession session)
{
if (Owner.TryGetComponent(out IMobStateComponent? mobState) &&
mobState.IsDead() &&
Owner.TryGetComponent(out MindComponent? mind) &&
mind.HasMind)
{
_gameTicker.OnGhostAttempt(mind.Mind!, true);
}
}
public override void Gib(bool gibParts = false)
{
base.Gib(gibParts);
SoundSystem.Play(Filter.Pvs(Owner), AudioHelpers.GetRandomFileFromSoundCollection("gib"), Owner.Transform.Coordinates,
AudioHelpers.WithVariation(0.025f));
if (Owner.TryGetComponent(out ContainerManagerComponent? container))
{
foreach (var cont in container.GetAllContainers())
{
foreach (var ent in cont.ContainedEntities)
{
cont.ForceRemove(ent);
ent.Transform.Coordinates = Owner.Transform.Coordinates;
ent.RandomOffset(0.25f);
}
}
}
Owner.QueueDelete();
}
}
}

View File

@@ -1,20 +0,0 @@
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Damage;
namespace Content.Server.GameObjects.Components.Body
{
public interface IBodyHealthChangeParams
{
BodyPartType Part { get; }
}
public class BodyDamageChangeParams : DamageChangeParams, IBodyHealthChangeParams
{
public BodyDamageChangeParams(BodyPartType part)
{
Part = part;
}
public BodyPartType Part { get; }
}
}

View File

@@ -1,64 +0,0 @@
#nullable enable
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Scanner;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(SharedBodyScannerComponent))]
public class BodyScannerComponent : SharedBodyScannerComponent, IActivate
{
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BodyScannerUiKey.Key);
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
var session = actor.PlayerSession;
if (session.AttachedEntity == null)
{
return;
}
if (session.AttachedEntity.TryGetComponent(out IBody? body))
{
var state = InterfaceState(body);
UserInterface?.SetState(state);
}
UserInterface?.Open(session);
}
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<ServerUserInterfaceComponent>();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) { }
/// <summary>
/// Copy BodyTemplate and BodyPart data into a common data class that the client can read.
/// </summary>
private BodyScannerUIState InterfaceState(IBody body)
{
return new(body.Owner.Uid);
}
}
}

View File

@@ -1,95 +0,0 @@
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Server.Interfaces;
using Content.Shared.Atmos;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body.Networks;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Circulatory
{
[RegisterComponent]
[ComponentReference(typeof(SharedBloodstreamComponent))]
public class BloodstreamComponent : SharedBloodstreamComponent, IGasMixtureHolder
{
public override string Name => "Bloodstream";
/// <summary>
/// Max volume of internal solution storage
/// </summary>
[DataField("maxVolume")]
[ViewVariables] private ReagentUnit _initialMaxVolume = ReagentUnit.New(250);
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[ViewVariables] private SolutionContainerComponent _internalSolution = default!;
/// <summary>
/// Empty volume of internal solution
/// </summary>
[ViewVariables] public ReagentUnit EmptyVolume => _internalSolution.EmptyVolume;
[ViewVariables]
public GasMixture Air { get; set; } = new(6)
{Temperature = Atmospherics.NormalBodyTemperature};
[ViewVariables] public SolutionContainerComponent Solution => _internalSolution;
public override void Initialize()
{
base.Initialize();
_internalSolution = Owner.EnsureComponent<SolutionContainerComponent>();
_internalSolution.MaxVolume = _initialMaxVolume;
}
/// <summary>
/// Attempt to transfer provided solution to internal solution.
/// Only supports complete transfers
/// </summary>
/// <param name="solution">Solution to be transferred</param>
/// <returns>Whether or not transfer was a success</returns>
public override bool TryTransferSolution(Solution solution)
{
// For now doesn't support partial transfers
if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
{
return false;
}
_internalSolution.TryAddSolution(solution);
return true;
}
public void PumpToxins(GasMixture to)
{
if (!Owner.TryGetComponent(out MetabolismComponent? metabolism))
{
to.Merge(Air);
Air.Clear();
return;
}
var toxins = metabolism.Clean(this);
var toOld = to.Gases.ToArray();
to.Merge(toxins);
for (var i = 0; i < toOld.Length; i++)
{
var newAmount = to.GetMoles(i);
var oldAmount = toOld[i];
var delta = newAmount - oldAmount;
toxins.AdjustMoles(i, -delta);
}
Air.Merge(toxins);
}
}
}

View File

@@ -1,162 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Body.Surgery;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body
{
[RegisterComponent]
[ComponentReference(typeof(SharedMechanismComponent))]
[ComponentReference(typeof(IMechanism))]
public class MechanismComponent : SharedMechanismComponent, IAfterInteract
{
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUIMessage;
}
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return false;
}
CloseAllSurgeryUIs();
OptionsCache.Clear();
PerformerCache = null;
BodyCache = null;
if (eventArgs.Target.TryGetComponent(out IBody? body))
{
SendBodyPartListToUser(eventArgs, body);
}
else if (eventArgs.Target.TryGetComponent<IBodyPart>(out var part))
{
DebugTools.AssertNotNull(part);
if (!part.TryAddMechanism(this))
{
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!"));
}
}
return true;
}
private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, IBody body)
{
// Create dictionary to send to client (text to be shown : data sent back if selected)
var toSend = new Dictionary<string, int>();
foreach (var (part, slot) in body.Parts)
{
// For each limb in the target, add it to our cache if it is a valid option.
if (part.CanAddMechanism(this))
{
OptionsCache.Add(IdHash, slot);
toSend.Add(part + ": " + part.Name, IdHash++);
}
}
if (OptionsCache.Count > 0 &&
eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
OpenSurgeryUI(actor.PlayerSession);
UpdateSurgeryUIBodyPartRequest(actor.PlayerSession, toSend);
PerformerCache = eventArgs.User;
BodyCache = body;
}
else // If surgery cannot be performed, show message saying so.
{
eventArgs.Target?.PopupMessage(eventArgs.User,
Loc.GetString("You see no way to install the {0}.", Owner.Name));
}
}
/// <summary>
/// Called after the client chooses from a list of possible BodyParts that can be operated on.
/// </summary>
private void HandleReceiveBodyPart(int key)
{
if (PerformerCache == null ||
!PerformerCache.TryGetComponent(out ActorComponent? actor))
{
return;
}
CloseSurgeryUI(actor.PlayerSession);
if (BodyCache == null)
{
return;
}
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!OptionsCache.TryGetValue(key, out var targetObject))
{
BodyCache.Owner.PopupMessage(PerformerCache,
Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name));
return;
}
var target = (IBodyPart) targetObject;
var message = target.TryAddMechanism(this)
? Loc.GetString("You jam {0:theName} inside {1:them}.", Owner, PerformerCache)
: Loc.GetString("You can't fit it in!");
BodyCache.Owner.PopupMessage(PerformerCache, message);
// TODO: {1:theName}
}
private void OpenSurgeryUI(IPlayerSession session)
{
UserInterface?.Open(session);
}
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
{
UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
}
private void CloseSurgeryUI(IPlayerSession session)
{
UserInterface?.Close(session);
}
private void CloseAllSurgeryUIs()
{
UserInterface?.CloseAll();
}
private void OnUIMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSurgeryUIMessage msg:
HandleReceiveBodyPart(msg.SelectedOptionId);
break;
}
}
}
}

View File

@@ -1,273 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Body.Surgery;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Part
{
[RegisterComponent]
[ComponentReference(typeof(SharedBodyPartComponent))]
[ComponentReference(typeof(IBodyPart))]
public class BodyPartComponent : SharedBodyPartComponent, IAfterInteract
{
private readonly Dictionary<int, object> _optionsCache = new();
private IBody? _owningBodyCache;
private int _idHash;
private IEntity? _surgeonCache;
private Container _mechanismContainer = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
public override bool CanAddMechanism(IMechanism mechanism)
{
return base.CanAddMechanism(mechanism) &&
_mechanismContainer.CanInsert(mechanism.Owner);
}
protected override void OnAddMechanism(IMechanism mechanism)
{
base.OnAddMechanism(mechanism);
_mechanismContainer.Insert(mechanism.Owner);
}
protected override void OnRemoveMechanism(IMechanism mechanism)
{
base.OnRemoveMechanism(mechanism);
_mechanismContainer.Remove(mechanism.Owner);
mechanism.Owner.RandomOffset(0.25f);
}
public override void Initialize()
{
base.Initialize();
_mechanismContainer = Owner.EnsureContainer<Container>($"{Name}-{nameof(BodyPartComponent)}");
// This is ran in Startup as entities spawned in Initialize
// are not synced to the client since they are assumed to be
// identical on it
foreach (var mechanismId in MechanismIds)
{
var entity = Owner.EntityManager.SpawnEntity(mechanismId, Owner.Transform.MapPosition);
if (!entity.TryGetComponent(out IMechanism? mechanism))
{
Logger.Error($"Entity {mechanismId} does not have a {nameof(IMechanism)} component.");
continue;
}
TryAddMechanism(mechanism, true);
}
}
protected override void Startup()
{
base.Startup();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUIMessage;
}
foreach (var mechanism in Mechanisms)
{
mechanism.Dirty();
}
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
// TODO BODY
if (eventArgs.Target == null)
{
return false;
}
CloseAllSurgeryUIs();
_optionsCache.Clear();
_surgeonCache = null;
_owningBodyCache = null;
if (eventArgs.Target.TryGetComponent(out IBody? body))
{
SendSlots(eventArgs, body);
}
return true;
}
private void SendSlots(AfterInteractEventArgs eventArgs, IBody body)
{
// Create dictionary to send to client (text to be shown : data sent back if selected)
var toSend = new Dictionary<string, int>();
// Here we are trying to grab a list of all empty BodySlots adjacent to an existing BodyPart that can be
// attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid.
foreach (var slot in body.EmptySlots)
{
if (slot.PartType != PartType)
{
continue;
}
foreach (var connection in slot.Connections)
{
if (connection.Part == null ||
!connection.Part.CanAttachPart(this))
{
continue;
}
_optionsCache.Add(_idHash, slot);
toSend.Add(slot.Id, _idHash++);
}
}
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(eventArgs.User.GetComponent<ActorComponent>().PlayerSession);
BodyPartSlotRequest(eventArgs.User.GetComponent<ActorComponent>().PlayerSession,
toSend);
_surgeonCache = eventArgs.User;
_owningBodyCache = body;
}
else // If surgery cannot be performed, show message saying so.
{
eventArgs.Target?.PopupMessage(eventArgs.User,
Loc.GetString("You see no way to install {0:theName}.", Owner));
}
}
/// <summary>
/// Called after the client chooses from a list of possible
/// BodyPartSlots to install the limb on.
/// </summary>
private void ReceiveBodyPartSlot(int key)
{
if (_surgeonCache == null ||
!_surgeonCache.TryGetComponent(out ActorComponent? actor))
{
return;
}
CloseSurgeryUI(actor.PlayerSession);
if (_owningBodyCache == null)
{
return;
}
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out var targetObject))
{
_owningBodyCache.Owner.PopupMessage(_surgeonCache,
Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner));
}
var target = (string) targetObject!;
var message = _owningBodyCache.TryAddPart(target, this)
? Loc.GetString("You attach {0:theName}.", Owner)
: Loc.GetString("You can't attach {0:theName}!", Owner);
_owningBodyCache.Owner.PopupMessage(_surgeonCache, message);
}
private void OpenSurgeryUI(IPlayerSession session)
{
UserInterface?.Open(session);
}
private void BodyPartSlotRequest(IPlayerSession session, Dictionary<string, int> options)
{
UserInterface?.SendMessage(new RequestBodyPartSlotSurgeryUIMessage(options), session);
}
private void CloseSurgeryUI(IPlayerSession session)
{
UserInterface?.Close(session);
}
private void CloseAllSurgeryUIs()
{
UserInterface?.CloseAll();
}
private void OnUIMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSlotSurgeryUIMessage msg:
ReceiveBodyPartSlot(msg.SelectedOptionId);
break;
}
}
[Verb]
public class AttachBodyPartVerb : Verb<BodyPartComponent>
{
protected override void GetData(IEntity user, BodyPartComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (user == component.Owner)
{
return;
}
if (!user.TryGetComponent(out ActorComponent? actor))
{
return;
}
var groupController = IoCManager.Resolve<IConGroupController>();
if (!groupController.CanCommand(actor.PlayerSession, "attachbodypart"))
{
return;
}
if (!user.TryGetComponent(out IBody? body))
{
return;
}
if (body.HasPart(component))
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Attach Body Part");
}
protected override void Activate(IEntity user, BodyPartComponent component)
{
if (!user.TryGetComponent(out IBody? body))
{
return;
}
body.SetPart($"{nameof(AttachBodyPartVerb)}-{component.Owner.Uid}", component);
}
}
}
}

View File

@@ -1,72 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Respiratory
{
[RegisterComponent]
public class InternalsComponent : Component
{
public override string Name => "Internals";
[ViewVariables] public IEntity? GasTankEntity { get; set; }
[ViewVariables] public IEntity? BreathToolEntity { get; set; }
public void DisconnectBreathTool()
{
var old = BreathToolEntity;
BreathToolEntity = null;
if (old != null && old.TryGetComponent(out BreathToolComponent? breathTool) )
{
breathTool.DisconnectInternals();
DisconnectTank();
}
}
public void ConnectBreathTool(IEntity toolEntity)
{
if (BreathToolEntity != null && BreathToolEntity.TryGetComponent(out BreathToolComponent? tool))
{
tool.DisconnectInternals();
}
BreathToolEntity = toolEntity;
}
public void DisconnectTank()
{
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
{
tank.DisconnectFromInternals(Owner);
}
GasTankEntity = null;
}
public bool TryConnectTank(IEntity tankEntity)
{
if (BreathToolEntity == null)
return false;
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
{
tank.DisconnectFromInternals(Owner);
}
GasTankEntity = tankEntity;
return true;
}
public bool AreInternalsWorking()
{
return BreathToolEntity != null &&
GasTankEntity != null &&
BreathToolEntity.TryGetComponent(out BreathToolComponent? breathTool) &&
breathTool.IsFunctional &&
GasTankEntity.TryGetComponent(out GasTankComponent? gasTank) &&
gasTank.Air != null;
}
}
}

View File

@@ -1,371 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Body.Surgery;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using static Content.Shared.GameObjects.Components.Body.Surgery.ISurgeryData;
namespace Content.Server.GameObjects.Components.Body.Surgery
{
/// <summary>
/// Data class representing the surgery state of a biological entity.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(ISurgeryData))]
public class BiologicalSurgeryDataComponent : Component, ISurgeryData
{
public override string Name => "BiologicalSurgeryData";
private readonly HashSet<IMechanism> _disconnectedOrgans = new();
private bool SkinOpened { get; set; }
private bool SkinRetracted { get; set; }
private bool VesselsClamped { get; set; }
public IBodyPart? Parent => Owner.GetComponentOrNull<IBodyPart>();
public BodyPartType? ParentType => Parent?.PartType;
private void AddDisconnectedOrgan(IMechanism mechanism)
{
if (_disconnectedOrgans.Add(mechanism))
{
Dirty();
}
}
private void RemoveDisconnectedOrgan(IMechanism mechanism)
{
if (_disconnectedOrgans.Remove(mechanism))
{
Dirty();
}
}
private async Task<bool> SurgeryDoAfter(IEntity performer)
{
if (!performer.HasComponent<DoAfterComponent>())
{
return true;
}
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
var target = Parent?.Body?.Owner ?? Owner;
var args = new DoAfterEventArgs(performer, 3, target: target)
{
BreakOnUserMove = true,
BreakOnTargetMove = true
};
return await doAfterSystem.DoAfter(args) == DoAfterStatus.Finished;
}
private bool HasIncisionNotClamped()
{
return SkinOpened && !VesselsClamped;
}
private bool HasClampedIncisionNotRetracted()
{
return SkinOpened && VesselsClamped && !SkinRetracted;
}
private bool HasFullyOpenIncision()
{
return SkinOpened && VesselsClamped && SkinRetracted;
}
public string GetDescription()
{
if (Parent == null)
{
return string.Empty;
}
var toReturn = new StringBuilder();
if (HasIncisionNotClamped())
{
toReturn.Append(Loc.GetString("The skin on {0:their} {1} has an incision, but it is prone to bleeding.",
Owner, Parent.Name));
}
else if (HasClampedIncisionNotRetracted())
{
toReturn.AppendLine(Loc.GetString("The skin on {0:their} {1} has an incision, but it is not retracted.",
Owner, Parent.Name));
}
else if (HasFullyOpenIncision())
{
toReturn.AppendLine(Loc.GetString("There is an incision on {0:their} {1}.\n", Owner, Parent.Name));
foreach (var mechanism in _disconnectedOrgans)
{
toReturn.AppendLine(Loc.GetString("{0:their} {1} is loose.", Owner, mechanism.Name));
}
}
return toReturn.ToString();
}
public bool CanAddMechanism(IMechanism mechanism)
{
return Parent != null &&
SkinOpened &&
VesselsClamped &&
SkinRetracted;
}
public bool CanAttachBodyPart(IBodyPart part)
{
return Parent != null;
// TODO BODY if a part is disconnected, you should have to do some surgery to allow another body part to be attached.
}
public SurgeryAction? GetSurgeryStep(SurgeryType toolType)
{
if (Parent == null)
{
return null;
}
if (toolType == SurgeryType.Amputation)
{
return RemoveBodyPartSurgery;
}
if (!SkinOpened)
{
// Case: skin is normal.
if (toolType == SurgeryType.Incision)
{
return OpenSkinSurgery;
}
}
else if (!VesselsClamped)
{
// Case: skin is opened, but not clamped.
switch (toolType)
{
case SurgeryType.VesselCompression:
return ClampVesselsSurgery;
case SurgeryType.Cauterization:
return CauterizeIncisionSurgery;
}
}
else if (!SkinRetracted)
{
// Case: skin is opened and clamped, but not retracted.
switch (toolType)
{
case SurgeryType.Retraction:
return RetractSkinSurgery;
case SurgeryType.Cauterization:
return CauterizeIncisionSurgery;
}
}
else
{
// Case: skin is fully open.
if (Parent.Mechanisms.Count > 0 &&
toolType == SurgeryType.VesselCompression)
{
if (_disconnectedOrgans.Except(Parent.Mechanisms).Count() != 0 ||
Parent.Mechanisms.Except(_disconnectedOrgans).Count() != 0)
{
return LoosenOrganSurgery;
}
}
if (_disconnectedOrgans.Count > 0 && toolType == SurgeryType.Incision)
{
return RemoveOrganSurgery;
}
if (toolType == SurgeryType.Cauterization)
{
return CauterizeIncisionSurgery;
}
}
return null;
}
public bool CheckSurgery(SurgeryType toolType)
{
return GetSurgeryStep(toolType) != null;
}
public bool PerformSurgery(SurgeryType surgeryType, IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
var step = GetSurgeryStep(surgeryType);
if (step == null)
{
return false;
}
step(container, surgeon, performer);
return true;
}
private async void OpenSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent == null)
{
return;
}
performer.PopupMessage(Loc.GetString("Cut open the skin..."));
if (await SurgeryDoAfter(performer))
{
SkinOpened = true;
}
}
private async void ClampVesselsSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent == null) return;
performer.PopupMessage(Loc.GetString("Clamp the vessels..."));
if (await SurgeryDoAfter(performer))
{
VesselsClamped = true;
}
}
private async void RetractSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent == null) return;
performer.PopupMessage(Loc.GetString("Retracting the skin..."));
if (await SurgeryDoAfter(performer))
{
SkinRetracted = true;
}
}
private async void CauterizeIncisionSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent == null) return;
performer.PopupMessage(Loc.GetString("Cauterizing the incision..."));
if (await SurgeryDoAfter(performer))
{
SkinOpened = false;
VesselsClamped = false;
SkinRetracted = false;
}
}
private void LoosenOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent == null) return;
if (Parent.Mechanisms.Count <= 0) return;
var toSend = new List<IMechanism>();
foreach (var mechanism in Parent.Mechanisms)
{
if (!_disconnectedOrgans.Contains(mechanism))
{
toSend.Add(mechanism);
}
}
if (toSend.Count > 0)
{
surgeon.RequestMechanism(toSend, LoosenOrganSurgeryCallback);
}
}
private async void LoosenOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon,
IEntity performer)
{
if (Parent == null || target == null || !Parent.Mechanisms.Contains(target))
{
return;
}
performer.PopupMessage(Loc.GetString("Loosening the organ..."));
if (!performer.HasComponent<DoAfterComponent>())
{
AddDisconnectedOrgan(target);
return;
}
if (await SurgeryDoAfter(performer))
{
AddDisconnectedOrgan(target);
}
}
private void RemoveOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent == null) return;
if (_disconnectedOrgans.Count <= 0)
{
return;
}
if (_disconnectedOrgans.Count == 1)
{
RemoveOrganSurgeryCallback(_disconnectedOrgans.First(), container, surgeon, performer);
}
else
{
surgeon.RequestMechanism(_disconnectedOrgans, RemoveOrganSurgeryCallback);
}
}
private async void RemoveOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon,
IEntity performer)
{
if (Parent == null || target == null || !Parent.Mechanisms.Contains(target))
{
return;
}
performer.PopupMessage(Loc.GetString("Removing the organ..."));
if (!performer.HasComponent<DoAfterComponent>())
{
Parent.RemoveMechanism(target, performer.Transform.Coordinates);
RemoveDisconnectedOrgan(target);
return;
}
if (await SurgeryDoAfter(performer))
{
Parent.RemoveMechanism(target, performer.Transform.Coordinates);
RemoveDisconnectedOrgan(target);
}
}
private async void RemoveBodyPartSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent == null) return;
if (container is not IBody body) return;
performer.PopupMessage(Loc.GetString("Sawing off the limb!"));
if (await SurgeryDoAfter(performer))
{
body.RemovePart(Parent);
}
}
}
}

View File

@@ -1,12 +0,0 @@
namespace Content.Server.GameObjects.Components.Body.Surgery.Messages
{
public class SurgeryWindowCloseMessage
{
public SurgeryWindowCloseMessage(SurgeryToolComponent tool)
{
Tool = tool;
}
public SurgeryToolComponent Tool { get; }
}
}

View File

@@ -1,14 +0,0 @@
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Body.Surgery.Messages
{
public class SurgeryWindowOpenMessage : ComponentMessage
{
public SurgeryWindowOpenMessage(SurgeryToolComponent tool)
{
Tool = tool;
}
public SurgeryToolComponent Tool { get; }
}
}

View File

@@ -1,277 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Body.Surgery.Messages;
using Content.Server.Utility;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Body.Surgery;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Surgery
{
/// <summary>
/// Server-side component representing a generic tool capable of performing surgery.
/// For instance, the scalpel.
/// </summary>
[RegisterComponent]
public class SurgeryToolComponent : Component, ISurgeon, IAfterInteract
{
public override string Name => "SurgeryTool";
public override uint? NetID => ContentNetIDs.SURGERY;
private readonly Dictionary<int, object> _optionsCache = new();
[DataField("baseOperateTime")]
private float _baseOperateTime = 5;
private ISurgeon.MechanismRequestCallback? _callbackCache;
private int _idHash;
[DataField("surgeryType")]
private SurgeryType _surgeryType = SurgeryType.Incision;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
public IBody? BodyCache { get; private set; }
public IEntity? PerformerCache { get; private set; }
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return false;
}
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return false;
}
CloseAllSurgeryUIs();
// Attempt surgery on a body by sending a list of operable parts for the client to choose from
if (eventArgs.Target.TryGetComponent(out IBody? body))
{
// Create dictionary to send to client (text to be shown : data sent back if selected)
var toSend = new Dictionary<string, int>();
foreach (var (part, slot) in body.Parts)
{
// For each limb in the target, add it to our cache if it is a valid option.
if (part.SurgeryCheck(_surgeryType))
{
_optionsCache.Add(_idHash, part);
toSend.Add(slot.Id + ": " + part.Name, _idHash++);
}
}
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(actor.PlayerSession);
UpdateSurgeryUIBodyPartRequest(actor.PlayerSession, toSend);
PerformerCache = eventArgs.User; // Also, cache the data.
BodyCache = body;
}
else // If surgery cannot be performed, show message saying so.
{
NotUsefulPopup();
}
}
else if (eventArgs.Target.TryGetComponent<IBodyPart>(out var part))
{
// Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI
PerformerCache = eventArgs.User;
// If surgery can be performed...
if (!part.SurgeryCheck(_surgeryType))
{
NotUsefulPopup();
return true;
}
// ...do the surgery.
if (part.AttemptSurgery(_surgeryType, part, this,
eventArgs.User))
{
return true;
}
// Log error if the surgery fails somehow.
Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}");
throw new InvalidOperationException();
}
return true;
}
public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }
public void RequestMechanism(IEnumerable<IMechanism> options, ISurgeon.MechanismRequestCallback callback)
{
var toSend = new Dictionary<string, int>();
foreach (var mechanism in options)
{
_optionsCache.Add(_idHash, mechanism);
toSend.Add(mechanism.Name, _idHash++);
}
if (_optionsCache.Count > 0 && PerformerCache != null)
{
OpenSurgeryUI(PerformerCache.GetComponent<ActorComponent>().PlayerSession);
UpdateSurgeryUIMechanismRequest(PerformerCache.GetComponent<ActorComponent>().PlayerSession,
toSend);
_callbackCache = callback;
}
else
{
Logger.Debug("Error on callback from mechanisms: there were no viable options to choose from!");
throw new InvalidOperationException();
}
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
}
private void OpenSurgeryUI(IPlayerSession session)
{
UserInterface?.Open(session);
var message = new SurgeryWindowOpenMessage(this);
SendMessage(message);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
}
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
{
UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
}
private void UpdateSurgeryUIMechanismRequest(IPlayerSession session, Dictionary<string, int> options)
{
UserInterface?.SendMessage(new RequestMechanismSurgeryUIMessage(options), session);
}
private void ClearUIData()
{
_optionsCache.Clear();
PerformerCache = null;
BodyCache = null;
_callbackCache = null;
}
private void CloseSurgeryUI(IPlayerSession session)
{
UserInterface?.Close(session);
ClearUIData();
}
public void CloseAllSurgeryUIs()
{
UserInterface?.CloseAll();
ClearUIData();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSurgeryUIMessage msg:
HandleReceiveBodyPart(msg.SelectedOptionId);
break;
case ReceiveMechanismSurgeryUIMessage msg:
HandleReceiveMechanism(msg.SelectedOptionId);
break;
}
}
/// <summary>
/// Called after the client chooses from a list of possible
/// <see cref="IBodyPart"/> that can be operated on.
/// </summary>
private void HandleReceiveBodyPart(int key)
{
if (PerformerCache == null ||
!PerformerCache.TryGetComponent(out ActorComponent? actor))
{
return;
}
CloseSurgeryUI(actor.PlayerSession);
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (!_optionsCache.TryGetValue(key, out var targetObject) ||
BodyCache == null)
{
NotUsefulAnymorePopup();
return;
}
var target = (IBodyPart) targetObject!;
// TODO BODY Reconsider
if (!target.AttemptSurgery(_surgeryType, BodyCache, this, PerformerCache))
{
NotUsefulAnymorePopup();
}
}
/// <summary>
/// Called after the client chooses from a list of possible
/// <see cref="IMechanism"/> to choose from.
/// </summary>
private void HandleReceiveMechanism(int key)
{
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
if (BodyCache == null ||
!_optionsCache.TryGetValue(key, out var targetObject) ||
targetObject is not MechanismComponent target ||
PerformerCache == null ||
!PerformerCache.TryGetComponent(out ActorComponent? actor))
{
NotUsefulAnymorePopup();
return;
}
CloseSurgeryUI(actor.PlayerSession);
_callbackCache?.Invoke(target, BodyCache, this, PerformerCache);
}
private void NotUsefulPopup()
{
if (PerformerCache == null) return;
BodyCache?.Owner.PopupMessage(PerformerCache,
Loc.GetString("You see no useful way to use {0:theName}.", Owner));
}
private void NotUsefulAnymorePopup()
{
if (PerformerCache == null) return;
BodyCache?.Owner.PopupMessage(PerformerCache,
Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
}
}
}

View File

@@ -1,36 +0,0 @@
using System.Threading.Tasks;
using Content.Shared.GameObjects.Components.Tag;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Botany
{
[RegisterComponent]
public class LogComponent : Component, IInteractUsing
{
public override string Name => "Log";
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!ActionBlockerSystem.CanInteract(eventArgs.User))
return false;
if (eventArgs.Using.HasTag("BotanySharp"))
{
for (var i = 0; i < 2; i++)
{
var plank = Owner.EntityManager.SpawnEntity("MaterialWoodPlank1", Owner.Transform.Coordinates);
plank.RandomOffset(0.25f);
}
Owner.QueueDelete();
return true;
}
return false;
}
}
}

View File

@@ -1,871 +0,0 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Server.Botany;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Fluids;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Utility;
using Content.Shared.Audio;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Botany;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.Components.Tag;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Botany
{
[RegisterComponent]
public class PlantHolderComponent : Component, IInteractUsing, IInteractHand, IActivate, IExamine
{
public const float HydroponicsSpeedMultiplier = 1f;
public const float HydroponicsConsumptionMultiplier = 4f;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
public override string Name => "PlantHolder";
[ViewVariables] private int _lastProduce;
[ViewVariables(VVAccess.ReadWrite)] private int _missingGas;
private readonly TimeSpan _cycleDelay = TimeSpan.FromSeconds(15f);
[ViewVariables] private TimeSpan _lastCycle = TimeSpan.Zero;
[ViewVariables(VVAccess.ReadWrite)] private bool _updateSpriteAfterUpdate;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("drawWarnings")]
public bool DrawWarnings { get; private set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
public float WaterLevel { get; private set; } = 100f;
[ViewVariables(VVAccess.ReadWrite)]
public float NutritionLevel { get; private set; } = 100f;
[ViewVariables(VVAccess.ReadWrite)]
public float PestLevel { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float WeedLevel { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float Toxins { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public int Age { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public int SkipAging { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool Dead { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool Harvest { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool Sampled { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public int YieldMod { get; set; } = 1;
[ViewVariables(VVAccess.ReadWrite)]
public float MutationMod { get; set; } = 1f;
[ViewVariables(VVAccess.ReadWrite)]
public float MutationLevel { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float Health { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float WeedCoefficient { get; set; } = 1f;
[ViewVariables(VVAccess.ReadWrite)]
public Seed? Seed { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool ImproperHeat { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool ImproperPressure { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool ImproperLight { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool ForceUpdate { get; set; }
[ComponentDependency] private readonly SolutionContainerComponent? _solutionContainer = default!;
[ComponentDependency] private readonly AppearanceComponent? _appearanceComponent = default!;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn<SolutionContainerComponent>();
}
public void WeedInvasion()
{
// TODO
}
public void Update()
{
UpdateReagents();
var curTime = _gameTiming.CurTime;
if (ForceUpdate)
ForceUpdate = false;
else if (curTime < (_lastCycle + _cycleDelay))
{
if(_updateSpriteAfterUpdate)
UpdateSprite();
return;
}
_lastCycle = curTime;
// Weeds like water and nutrients! They may appear even if there's not a seed planted.
if (WaterLevel > 10 && NutritionLevel > 2 && _random.Prob(Seed == null ? 0.05f : 0.01f))
{
WeedLevel += 1 * HydroponicsSpeedMultiplier * WeedCoefficient;
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
// There's a chance for a weed explosion to happen if weeds take over.
// Plants that are themselves weeds (WeedTolerance > 8) are unaffected.
if (WeedLevel >= 10 && _random.Prob(0.1f))
{
if (Seed == null || WeedLevel >= Seed.WeedTolerance + 2)
WeedInvasion();
}
// If we have no seed planted, or the plant is dead, stop processing here.
if (Seed == null || Dead)
{
if (_updateSpriteAfterUpdate)
UpdateSprite();
return;
}
// There's a small chance the pest population increases.
// Can only happen when there's a live seed planted.
if (_random.Prob(0.01f))
{
PestLevel += 0.5f * HydroponicsSpeedMultiplier;
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
// Advance plant age here.
if (SkipAging > 0)
SkipAging--;
else
{
if(_random.Prob(0.8f))
Age += (int)(1 * HydroponicsSpeedMultiplier);
_updateSpriteAfterUpdate = true;
}
// Nutrient consumption.
if (Seed.NutrientConsumption > 0 && NutritionLevel > 0 && _random.Prob(0.75f))
{
NutritionLevel -= MathF.Max(0f, Seed.NutrientConsumption * HydroponicsSpeedMultiplier);
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
// Water consumption.
if (Seed.WaterConsumption > 0 && WaterLevel > 0 && _random.Prob(0.75f))
{
WaterLevel -= MathF.Max(0f, Seed.NutrientConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier);
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
var healthMod = _random.Next(1, 3) * HydroponicsSpeedMultiplier;
// Make sure the plant is not starving.
if (_random.Prob(0.35f))
{
if (NutritionLevel > 2)
{
Health += healthMod;
}
else
{
AffectGrowth(-1);
Health -= healthMod;
}
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
// Make sure the plant is not thirsty.
if (_random.Prob(0.35f))
{
if (WaterLevel > 10)
{
Health += healthMod;
}
else
{
AffectGrowth(-1);
Health -= healthMod;
}
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
var environment = tileAtmos?.Air ?? GasMixture.SpaceGas;
if (Seed.ConsumeGasses.Count > 0)
{
_missingGas = 0;
foreach (var (gas, amount) in Seed.ConsumeGasses)
{
if (environment.GetMoles(gas) < amount)
{
_missingGas++;
continue;
}
environment.AdjustMoles(gas, -amount);
}
if (_missingGas > 0)
{
Health -= _missingGas * HydroponicsSpeedMultiplier;
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
}
// Seed pressure resistance.
var pressure = environment.Pressure;
if (pressure < Seed.LowPressureTolerance || pressure > Seed.HighPressureTolerance)
{
Health -= healthMod;
ImproperPressure = true;
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
else
{
ImproperPressure = false;
}
// Seed ideal temperature.
if (MathF.Abs(environment.Temperature - Seed.IdealHeat) > Seed.HeatTolerance)
{
Health -= healthMod;
ImproperHeat = true;
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
else
{
ImproperHeat = false;
}
// Gas production.
var exudeCount = Seed.ExudeGasses.Count;
if (exudeCount > 0)
{
foreach (var (gas, amount) in Seed.ExudeGasses)
{
environment.AdjustMoles(gas, MathF.Max(1f, MathF.Round((amount * MathF.Round(Seed.Potency)) / exudeCount)));
}
}
// Toxin levels beyond the plant's tolerance cause damage.
// They are, however, slowly reduced over time.
if (Toxins > 0)
{
var toxinUptake = MathF.Max(1, MathF.Round(Toxins / 10f));
if (Toxins > Seed.ToxinsTolerance)
{
Health -= toxinUptake;
}
Toxins -= toxinUptake;
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
// Weed levels.
if (PestLevel > 0)
{
// TODO: Carnivorous plants?
if (PestLevel > Seed.PestTolerance)
{
Health -= HydroponicsSpeedMultiplier;
}
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
// Weed levels.
if (WeedLevel > 0)
{
// TODO: Parasitic plants.
if (WeedLevel >= Seed.WeedTolerance)
{
Health -= HydroponicsSpeedMultiplier;
}
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
if (Age > Seed.Lifespan)
{
Health -= _random.Next(3, 5) * HydroponicsSpeedMultiplier;
if (DrawWarnings)
_updateSpriteAfterUpdate = true;
}
else if(Age < 0) // Revert back to seed packet!
{
Seed.SpawnSeedPacket(Owner.Transform.Coordinates);
RemovePlant();
ForceUpdate = true;
Update();
}
CheckHealth();
if (Harvest && Seed.HarvestRepeat == HarvestType.SelfHarvest)
AutoHarvest();
// If enough time has passed since the plant was harvested, we're ready to harvest again!
if (!Dead && Seed.ProductPrototypes.Count > 0)
{
if (Age > Seed.Production)
{
if ((Age - _lastProduce) > Seed.Production && !Harvest)
{
Harvest = true;
_lastProduce = Age;
}
}
else
{
if (Harvest)
{
Harvest = false;
_lastProduce = Age;
}
}
}
CheckLevelSanity();
if(_updateSpriteAfterUpdate)
UpdateSprite();
}
private void CheckLevelSanity()
{
if (Seed != null)
Health = MathHelper.Clamp(Health, 0, Seed.Endurance);
else
{
Health = 0f;
Dead = false;
}
MutationLevel = MathHelper.Clamp(MutationLevel, 0f, 100f);
NutritionLevel = MathHelper.Clamp(NutritionLevel, 0f, 100f);
WaterLevel = MathHelper.Clamp(WaterLevel, 0f, 100f);
PestLevel = MathHelper.Clamp(PestLevel, 0f, 10f);
WeedLevel = MathHelper.Clamp(WeedLevel, 0f, 10f);
Toxins = MathHelper.Clamp(Toxins, 0f, 100f);
YieldMod = MathHelper.Clamp(YieldMod, 0, 2);
MutationMod = MathHelper.Clamp(MutationMod, 0f, 3f);
}
public bool DoHarvest(IEntity user)
{
if (Seed == null || user.Deleted || !ActionBlockerSystem.CanInteract(user))
return false;
if (Harvest && !Dead)
{
if (user.TryGetComponent(out HandsComponent? hands))
{
if (!Seed.CheckHarvest(user, hands.GetActiveHand?.Owner))
return false;
} else if (!Seed.CheckHarvest(user))
{
return false;
}
Seed.Harvest(user, YieldMod);
AfterHarvest();
return true;
}
if (!Dead) return false;
RemovePlant();
AfterHarvest();
return true;
}
public void AutoHarvest()
{
if (Seed == null || !Harvest)
return;
Seed.AutoHarvest(Owner.Transform.Coordinates);
AfterHarvest();
}
private void AfterHarvest()
{
Harvest = false;
_lastProduce = Age;
if(Seed?.HarvestRepeat == HarvestType.NoRepeat)
RemovePlant();
CheckLevelSanity();
UpdateSprite();
}
public void CheckHealth()
{
if (Health <= 0)
{
Die();
}
}
public void Die()
{
Dead = true;
Harvest = false;
MutationLevel = 0;
YieldMod = 1;
MutationMod = 1;
ImproperLight = false;
ImproperHeat = false;
ImproperPressure = false;
WeedLevel += 1 * HydroponicsSpeedMultiplier;
PestLevel = 0;
UpdateSprite();
}
public void RemovePlant()
{
YieldMod = 1;
MutationMod = 1;
PestLevel = 0;
Seed = null;
Dead = false;
Age = 0;
Sampled = false;
Harvest = false;
ImproperLight = false;
ImproperPressure = false;
ImproperHeat = false;
UpdateSprite();
}
public void AffectGrowth(int amount)
{
if (Seed == null)
return;
if (amount > 0)
{
if (Age < Seed.Maturation)
Age += amount;
else if (!Harvest && Seed.Yield <= 0f)
_lastProduce -= amount;
}
else
{
if (Age < Seed.Maturation)
SkipAging++;
else if (!Harvest && Seed.Yield <= 0f)
_lastProduce += amount;
}
}
public void AdjustNutrient(float amount)
{
NutritionLevel += amount;
}
public void AdjustWater(float amount)
{
WaterLevel += amount;
// Water dilutes toxins.
if (amount > 0)
{
Toxins -= amount * 4f;
}
}
public void UpdateReagents()
{
if (_solutionContainer == null)
return;
if (_solutionContainer.Solution.TotalVolume <= 0 || MutationLevel >= 25)
{
if (MutationLevel >= 0)
{
Mutate(Math.Min(MutationLevel, 25));
MutationLevel = 0;
}
}
else
{
var one = ReagentUnit.New(1);
foreach (var (reagent, amount) in _solutionContainer.ReagentList.ToArray())
{
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent);
reagentProto.ReactionPlant(Owner);
_solutionContainer.Solution.RemoveReagent(reagent, amount < one ? amount : one);
}
}
CheckLevelSanity();
}
private void Mutate(float severity)
{
// TODO: Coming soon in "Botany 2: Plant boogaloo".
}
public void UpdateSprite()
{
_updateSpriteAfterUpdate = false;
if (_appearanceComponent == null)
return;
if (Seed != null)
{
if(DrawWarnings)
_appearanceComponent.SetData(PlantHolderVisuals.HealthLight, Health <= (Seed.Endurance / 2f));
if (Dead)
{
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi, "dead"));
}
else if (Harvest)
{
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi, "harvest"));
}
else if (Age < Seed.Maturation)
{
var growthStage = Math.Max(1, (int)((Age * Seed.GrowthStages) / Seed.Maturation));
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi,$"stage-{growthStage}"));
_lastProduce = Age;
}
else
{
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi,$"stage-{Seed.GrowthStages}"));
}
}
else
{
_appearanceComponent.SetData(PlantHolderVisuals.Plant, SpriteSpecifier.Invalid);
_appearanceComponent.SetData(PlantHolderVisuals.HealthLight, false);
}
if (!DrawWarnings) return;
_appearanceComponent.SetData(PlantHolderVisuals.WaterLight, WaterLevel <= 10);
_appearanceComponent.SetData(PlantHolderVisuals.NutritionLight, NutritionLevel <= 2);
_appearanceComponent.SetData(PlantHolderVisuals.AlertLight, WeedLevel >= 5 || PestLevel >= 5 || Toxins >= 40 || ImproperHeat || ImproperLight || ImproperPressure || _missingGas > 0);
_appearanceComponent.SetData(PlantHolderVisuals.HarvestLight, Harvest);
}
public void CheckForDivergence(bool modified)
{
// Make sure we're not modifying a "global" seed.
// If this seed is not in the global seed list, then no products of this line have been harvested yet.
// It is then safe to assume it's restricted to this tray.
if (Seed == null) return;
var plantSystem = EntitySystem.Get<PlantSystem>();
if (plantSystem.Seeds.ContainsKey(Seed.Uid))
Seed = Seed.Diverge(modified);
}
private void ForceUpdateByExternalCause()
{
SkipAging++; // We're forcing an update cycle, so one age hasn't passed.
ForceUpdate = true;
Update();
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
var user = eventArgs.User;
var usingItem = eventArgs.Using;
if (usingItem == null || usingItem.Deleted || !ActionBlockerSystem.CanInteract(user))
return false;
if (usingItem.TryGetComponent(out SeedComponent? seeds))
{
if (Seed == null)
{
if (seeds.Seed == null)
{
user.PopupMessageCursor(Loc.GetString("The packet seems to be empty. You throw it away."));
usingItem.QueueDelete();
return false;
}
user.PopupMessageCursor(Loc.GetString("You plant the {0} {1}.", seeds.Seed.SeedName, seeds.Seed.SeedNoun));
Seed = seeds.Seed;
Dead = false;
Age = 1;
Health = Seed.Endurance;
_lastCycle = _gameTiming.CurTime;
usingItem.QueueDelete();
CheckLevelSanity();
UpdateSprite();
return true;
}
user.PopupMessageCursor(Loc.GetString("The {0} already has seeds in it!", Owner.Name));
return false;
}
if (usingItem.HasTag("Hoe"))
{
if (WeedLevel > 0)
{
user.PopupMessageCursor(Loc.GetString("You remove the weeds from the {0}.", Owner.Name));
user.PopupMessageOtherClients(Loc.GetString("{0} starts uprooting the weeds.", user.Name));
WeedLevel = 0;
UpdateSprite();
}
else
{
user.PopupMessageCursor(Loc.GetString("This plot is devoid of weeds! It doesn't need uprooting."));
}
return true;
}
if (usingItem.HasTag("Shovel"))
{
if (Seed != null)
{
user.PopupMessageCursor(Loc.GetString("You remove the plant from the {0}.", Owner.Name));
user.PopupMessageOtherClients(Loc.GetString("{0} removes the plant.", user.Name));
RemovePlant();
}
else
{
user.PopupMessageCursor(Loc.GetString("There is no plant to remove."));
}
return true;
}
if (usingItem.TryGetComponent(out ISolutionInteractionsComponent? solution) && solution.CanDrain)
{
var amount = ReagentUnit.New(5);
var sprayed = false;
if (usingItem.TryGetComponent(out SprayComponent? spray))
{
sprayed = true;
amount = ReagentUnit.New(1);
if (!string.IsNullOrEmpty(spray.SpraySound))
{
SoundSystem.Play(Filter.Pvs(usingItem), spray.SpraySound, usingItem, AudioHelpers.WithVariation(0.125f));
}
}
var split = solution.Drain(amount);
if (split.TotalVolume == 0)
{
user.PopupMessageCursor(Loc.GetString("{0:TheName} is empty!", usingItem));
return true;
}
user.PopupMessageCursor(Loc.GetString(
sprayed ? "You spray {0:TheName}" : "You transfer {1}u to {0:TheName}",
Owner, split.TotalVolume));
_solutionContainer?.TryAddSolution(split);
ForceUpdateByExternalCause();
return true;
}
if (usingItem.HasTag("PlantSampleTaker"))
{
if (Seed == null)
{
user.PopupMessageCursor(Loc.GetString("There is nothing to take a sample of!"));
return false;
}
if (Sampled)
{
user.PopupMessageCursor(Loc.GetString("This plant has already been sampled."));
return false;
}
if (Dead)
{
user.PopupMessageCursor(Loc.GetString("This plant is dead."));
return false;
}
var seed = Seed.SpawnSeedPacket(user.Transform.Coordinates);
seed.RandomOffset(0.25f);
user.PopupMessageCursor(Loc.GetString($"You take a sample from the {Seed.DisplayName}."));
Health -= (_random.Next(3, 5) * 10);
if (_random.Prob(0.3f))
Sampled = true;
// Just in case.
CheckLevelSanity();
ForceUpdateByExternalCause();
return true;
}
if (usingItem.HasTag("BotanySharp"))
{
return DoHarvest(user);
}
if (usingItem.HasComponent<ProduceComponent>())
{
user.PopupMessageCursor(Loc.GetString("You compost {1:theName} into {0:theName}.", Owner, usingItem));
user.PopupMessageOtherClients(Loc.GetString("{0:TheName} composts {1:theName} into {2:theName}.", user, usingItem, Owner));
if (usingItem.TryGetComponent(out SolutionContainerComponent? solution2))
{
// This deliberately discards overfill.
_solutionContainer?.TryAddSolution(solution2.SplitSolution(solution2.Solution.TotalVolume));
ForceUpdateByExternalCause();
}
usingItem.QueueDelete();
return true;
}
return false;
}
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{
// DoHarvest does the sanity checks.
return DoHarvest(eventArgs.User);
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
// DoHarvest does the sanity checks.
DoHarvest(eventArgs.User);
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
if (!inDetailsRange)
return;
if (Seed == null)
{
message.AddMarkup(Loc.GetString("It has nothing planted in it.\n"));
}
else if (!Dead)
{
message.AddMarkup(Loc.GetString($"[color=green]{Seed.DisplayName}[/color] {(Seed.DisplayName.EndsWith('s') ? "are" : "is")} growing here.\n"));
if(Health <= Seed.Endurance / 2)
message.AddMarkup(Loc.GetString($"The plant looks [color=red]{(Age > Seed.Lifespan ? "old and wilting" : "unhealthy")}[/color].\n"));
}
else
{
message.AddMarkup(Loc.GetString("It is full of [color=red]dead plant matter[/color].\n"));
}
if(WeedLevel >= 5)
message.AddMarkup(Loc.GetString("It is filled with [color=green]weeds[/color]!\n"));
if(PestLevel >= 5)
message.AddMarkup(Loc.GetString("It is filled with [color=gray]tiny worms[/color]!\n"));
message.AddMarkup(Loc.GetString($"Water: [color=cyan]{(int)WaterLevel}[/color]\n"));
message.AddMarkup(Loc.GetString($"Nutrient: [color=orange]{(int)NutritionLevel}[/color]\n"));
if (DrawWarnings)
{
if(Toxins > 40f)
message.AddMarkup(Loc.GetString("The [color=red]toxicity level alert[/color] is flashing red.\n"));
if(ImproperLight)
message.AddMarkup(Loc.GetString("The [color=yellow]improper light level alert[/color] is blinking.\n"));
if(ImproperHeat)
message.AddMarkup(Loc.GetString("The [color=orange]improper temperature level alert[/color] is blinking.\n"));
if(ImproperPressure)
message.AddMarkup(Loc.GetString("The [color=lightblue]improper environment pressure alert[/color] is blinking.\n"));
if(_missingGas > 0)
message.AddMarkup(Loc.GetString("The [color=cyan]improper gas environment alert[/color] is blinking.\n"));
}
}
}
}

View File

@@ -1,59 +0,0 @@
#nullable enable
using Content.Server.Botany;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Botany
{
[RegisterComponent]
public class ProduceComponent : Component, ISerializationHooks
{
public override string Name => "Produce";
[DataField("seed")]
private string? _seedName;
[ViewVariables]
public Seed? Seed
{
get => _seedName != null ? IoCManager.Resolve<IPrototypeManager>().Index<Seed>(_seedName) : null;
set => _seedName = value?.ID;
}
public float Potency => Seed?.Potency ?? 0;
public void Grown()
{
if (Seed == null)
return;
if (Owner.TryGetComponent(out SpriteComponent? sprite))
{
sprite.LayerSetRSI(0, Seed.PlantRsi);
sprite.LayerSetState(0, Seed.PlantIconState);
}
var solutionContainer = Owner.EnsureComponent<SolutionContainerComponent>();
solutionContainer.RemoveAllSolution();
foreach (var (chem, quantity) in Seed.Chemicals)
{
var amount = ReagentUnit.New(quantity.Min);
if(quantity.PotencyDivisor > 0 && Potency > 0)
amount += ReagentUnit.New(Potency/quantity.PotencyDivisor);
amount = ReagentUnit.New((int) MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max));
solutionContainer.MaxVolume += amount;
solutionContainer.Solution.AddReagent(chem, amount);
}
}
}
}

View File

@@ -1,52 +0,0 @@
#nullable enable
using Content.Server.Botany;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Botany
{
[RegisterComponent]
public class SeedComponent : Component, IExamine
{
public override string Name => "Seed";
[DataField("seed")]
private string? _seedName;
[ViewVariables]
public Seed? Seed
{
get => _seedName != null ? IoCManager.Resolve<IPrototypeManager>().Index<Seed>(_seedName) : null;
set => _seedName = value?.ID;
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
if (!inDetailsRange)
return;
if (Seed == null)
{
message.AddMarkup(Loc.GetString("It doesn't seem to contain any seeds.\n"));
return;
}
message.AddMarkup(Loc.GetString($"It has a picture of [color=yellow]{Seed.DisplayName}[/color] on the front.\n"));
if(!Seed.RoundStart)
message.AddMarkup(Loc.GetString($"It's tagged as variety [color=lightgray]no. {Seed.Uid}[/color].\n"));
else
{
message.AddMarkup(Loc.GetString($"Plant Yield: [color=lightblue]{Seed.Yield}[/color]\n"));
message.AddMarkup(Loc.GetString($"Plant Potency: [color=lightblue]{Seed.Potency}[/color]\n"));
}
}
}
}

View File

@@ -1,50 +0,0 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Random;
namespace Content.Server.GameObjects.Components.Botany
{
[RegisterComponent]
public class SeedExtractorComponent : Component, IInteractUsing
{
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiver = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override string Name => "SeedExtractor";
// TODO: Upgradeable machines.
private int _minSeeds = 1;
private int _maxSeeds = 4;
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!_powerReceiver?.Powered ?? false)
return false;
if (eventArgs.Using.TryGetComponent(out ProduceComponent? produce) && produce.Seed != null)
{
eventArgs.User.PopupMessageCursor(Loc.GetString("You extract some seeds from the {0}.", eventArgs.Using.Name));
eventArgs.Using.QueueDelete();
var random = _random.Next(_minSeeds, _maxSeeds);
for (var i = 0; i < random; i++)
{
produce.Seed.SpawnSeedPacket(Owner.Transform.Coordinates, Owner.EntityManager);
}
return true;
}
return false;
}
}
}

View File

@@ -1,457 +0,0 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Mobs.State;
using Content.Server.GameObjects.Components.Pulling;
using Content.Server.GameObjects.Components.Strap;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Buckle;
using Content.Shared.GameObjects.Components.Strap;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Content.Shared.Utility;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Buckle
{
/// <summary>
/// Component that handles sitting entities into <see cref="StrapComponent"/>s.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(SharedBuckleComponent))]
public class BuckleComponent : SharedBuckleComponent
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[ComponentDependency] public readonly AppearanceComponent? Appearance = null;
[ComponentDependency] private readonly ServerAlertsComponent? _serverAlerts = null;
[ComponentDependency] private readonly StunnableComponent? _stunnable = null;
[ComponentDependency] private readonly MobStateComponent? _mobState = null;
[DataField("size")]
private int _size = 100;
/// <summary>
/// The amount of time that must pass for this entity to
/// be able to unbuckle after recently buckling.
/// </summary>
[DataField("delay")]
[ViewVariables]
private TimeSpan _unbuckleDelay = TimeSpan.FromSeconds(0.25f);
/// <summary>
/// The time that this entity buckled at.
/// </summary>
[ViewVariables]
private TimeSpan _buckleTime;
/// <summary>
/// The position offset that is being applied to this entity if buckled.
/// </summary>
public Vector2 BuckleOffset { get; private set; }
private StrapComponent? _buckledTo;
/// <summary>
/// The strap that this component is buckled to.
/// </summary>
[ViewVariables]
public StrapComponent? BuckledTo
{
get => _buckledTo;
private set
{
_buckledTo = value;
_buckleTime = _gameTiming.CurTime;
Dirty();
}
}
[ViewVariables]
public override bool Buckled => BuckledTo != null;
/// <summary>
/// The amount of space that this entity occupies in a
/// <see cref="StrapComponent"/>.
/// </summary>
[ViewVariables]
public int Size => _size;
/// <summary>
/// Shows or hides the buckled status effect depending on if the
/// entity is buckled or not.
/// </summary>
private void UpdateBuckleStatus()
{
if (_serverAlerts == null)
{
return;
}
if (Buckled)
{
_serverAlerts.ShowAlert(BuckledTo?.BuckledAlertType ?? AlertType.Buckled);
}
else
{
_serverAlerts.ClearAlertCategory(AlertCategory.Buckled);
}
}
/// <summary>
/// Reattaches this entity to the strap, modifying its position and rotation.
/// </summary>
/// <param name="strap">The strap to reattach to.</param>
public void ReAttach(StrapComponent strap)
{
var ownTransform = Owner.Transform;
var strapTransform = strap.Owner.Transform;
ownTransform.AttachParent(strapTransform);
switch (strap.Position)
{
case StrapPosition.None:
ownTransform.WorldRotation = strapTransform.WorldRotation;
break;
case StrapPosition.Stand:
EntitySystem.Get<StandingStateSystem>().Standing(Owner);
ownTransform.WorldRotation = strapTransform.WorldRotation;
break;
case StrapPosition.Down:
EntitySystem.Get<StandingStateSystem>().Down(Owner, force: true);
ownTransform.WorldRotation = Angle.South;
break;
}
// Assign BuckleOffset first, before causing a MoveEvent to fire
if (strapTransform.WorldRotation.GetCardinalDir() == Direction.North)
{
BuckleOffset = (0, 0.15f);
ownTransform.WorldPosition = strapTransform.WorldPosition + BuckleOffset;
}
else
{
BuckleOffset = Vector2.Zero;
ownTransform.WorldPosition = strapTransform.WorldPosition;
}
}
private bool CanBuckle(IEntity? user, IEntity to, [NotNullWhen(true)] out StrapComponent? strap)
{
strap = null;
if (user == null || user == to)
{
return false;
}
if (!ActionBlockerSystem.CanInteract(user))
{
user.PopupMessage(Loc.GetString("You can't do that!"));
return false;
}
if (!to.TryGetComponent(out strap))
{
return false;
}
var component = strap;
bool Ignored(IEntity entity) => entity == Owner || entity == user || entity == component.Owner;
if (!Owner.InRangeUnobstructed(strap, Range, predicate: Ignored, popup: true))
{
return false;
}
// If in a container
if (Owner.TryGetContainer(out var ownerContainer))
{
// And not in the same container as the strap
if (!strap.Owner.TryGetContainer(out var strapContainer) ||
ownerContainer != strapContainer)
{
return false;
}
}
if (!user.HasComponent<HandsComponent>())
{
user.PopupMessage(Loc.GetString("You don't have hands!"));
return false;
}
if (Buckled)
{
var message = Loc.GetString(Owner == user
? "You are already buckled in!"
: "{0:They} are already buckled in!", Owner);
Owner.PopupMessage(user, message);
return false;
}
var parent = to.Transform.Parent;
while (parent != null)
{
if (parent == user.Transform)
{
var message = Loc.GetString(Owner == user
? "You can't buckle yourself there!"
: "You can't buckle {0:them} there!", Owner);
Owner.PopupMessage(user, message);
return false;
}
parent = parent.Parent;
}
if (!strap.HasSpace(this))
{
var message = Loc.GetString(Owner == user
? "You can't fit there!"
: "{0:They} can't fit there!", Owner);
Owner.PopupMessage(user, message);
return false;
}
return true;
}
public override bool TryBuckle(IEntity? user, IEntity to)
{
if (user == null || !CanBuckle(user, to, out var strap))
{
return false;
}
SoundSystem.Play(Filter.Pvs(Owner), strap.BuckleSound, Owner);
if (!strap.TryAdd(this))
{
var message = Loc.GetString(Owner == user
? "You can't buckle yourself there!"
: "You can't buckle {0:them} there!", Owner);
Owner.PopupMessage(user, message);
return false;
}
Appearance?.SetData(BuckleVisuals.Buckled, true);
ReAttach(strap);
BuckledTo = strap;
LastEntityBuckledTo = BuckledTo.Owner.Uid;
DontCollide = true;
UpdateBuckleStatus();
SendMessage(new BuckleMessage(Owner, to));
if (Owner.TryGetComponent(out PullableComponent? ownerPullable))
{
if (ownerPullable.Puller != null)
{
ownerPullable.TryStopPull();
}
}
if (to.TryGetComponent(out PullableComponent? toPullable))
{
if (toPullable.Puller == Owner)
{
// can't pull it and buckle to it at the same time
toPullable.TryStopPull();
}
}
return true;
}
/// <summary>
/// Tries to unbuckle the Owner of this component from its current strap.
/// </summary>
/// <param name="user">The entity doing the unbuckling.</param>
/// <param name="force">
/// Whether to force the unbuckling or not. Does not guarantee true to
/// be returned, but guarantees the owner to be unbuckled afterwards.
/// </param>
/// <returns>
/// true if the owner was unbuckled, otherwise false even if the owner
/// was previously already unbuckled.
/// </returns>
public bool TryUnbuckle(IEntity user, bool force = false)
{
if (BuckledTo == null)
{
return false;
}
var oldBuckledTo = BuckledTo;
if (!force)
{
if (_gameTiming.CurTime < _buckleTime + _unbuckleDelay)
{
return false;
}
if (!ActionBlockerSystem.CanInteract(user))
{
user.PopupMessage(Loc.GetString("You can't do that!"));
return false;
}
if (!user.InRangeUnobstructed(oldBuckledTo, Range, popup: true))
{
return false;
}
}
BuckledTo = null;
if (Owner.Transform.Parent == oldBuckledTo.Owner.Transform)
{
Owner.Transform.AttachParentToContainerOrGrid();
Owner.Transform.WorldRotation = oldBuckledTo.Owner.Transform.WorldRotation;
}
Appearance?.SetData(BuckleVisuals.Buckled, false);
if (_stunnable != null && _stunnable.KnockedDown
|| (_mobState?.IsIncapacitated() ?? false))
{
EntitySystem.Get<StandingStateSystem>().Down(Owner);
}
else
{
EntitySystem.Get<StandingStateSystem>().Standing(Owner);
}
_mobState?.CurrentState?.EnterState(Owner);
UpdateBuckleStatus();
oldBuckledTo.Remove(this);
SoundSystem.Play(Filter.Pvs(Owner), oldBuckledTo.UnbuckleSound, Owner);
SendMessage(new UnbuckleMessage(Owner, oldBuckledTo.Owner));
return true;
}
/// <summary>
/// Makes an entity toggle the buckling status of the owner to a
/// specific entity.
/// </summary>
/// <param name="user">The entity doing the buckling/unbuckling.</param>
/// <param name="to">
/// The entity to toggle the buckle status of the owner to.
/// </param>
/// <param name="force">
/// Whether to force the unbuckling or not, if it happens. Does not
/// guarantee true to be returned, but guarantees the owner to be
/// unbuckled afterwards.
/// </param>
/// <returns>true if the buckling status was changed, false otherwise.</returns>
public bool ToggleBuckle(IEntity user, IEntity to, bool force = false)
{
if (BuckledTo?.Owner == to)
{
return TryUnbuckle(user, force);
}
return TryBuckle(user, to);
}
protected override void Startup()
{
base.Startup();
UpdateBuckleStatus();
}
public override void OnRemove()
{
BuckledTo?.Remove(this);
TryUnbuckle(Owner, true);
_buckleTime = default;
UpdateBuckleStatus();
base.OnRemove();
}
public override ComponentState GetComponentState(ICommonSession player)
{
int? drawDepth = null;
if (BuckledTo != null &&
Owner.Transform.WorldRotation.GetCardinalDir() == Direction.North &&
BuckledTo.SpriteComponent != null)
{
drawDepth = BuckledTo.SpriteComponent.DrawDepth - 1;
}
return new BuckleComponentState(Buckled, drawDepth, LastEntityBuckledTo, DontCollide);
}
public void Update(PhysicsComponent physics)
{
if (!DontCollide)
return;
physics.WakeBody();
if (!IsOnStrapEntityThisFrame && DontCollide)
{
DontCollide = false;
TryUnbuckle(Owner);
Dirty();
}
IsOnStrapEntityThisFrame = false;
}
/// <summary>
/// Allows the unbuckling of the owning entity through a verb if
/// anyone right clicks them.
/// </summary>
[Verb]
private sealed class BuckleVerb : Verb<BuckleComponent>
{
protected override void GetData(IEntity user, BuckleComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) || !component.Buckled)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Unbuckle");
}
protected override void Activate(IEntity user, BuckleComponent component)
{
component.TryUnbuckle(user);
}
}
}
}

View File

@@ -1,230 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.Cargo;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Cargo;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Prototypes.Cargo;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Cargo
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class CargoConsoleComponent : SharedCargoConsoleComponent, IActivate
{
[Dependency] private readonly IMapManager _mapManager = default!;
[ViewVariables]
public int Points = 1000;
private CargoBankAccount? _bankAccount;
[ViewVariables]
public CargoBankAccount? BankAccount
{
get => _bankAccount;
private set
{
if (_bankAccount == value)
{
return;
}
if (_bankAccount != null)
{
_bankAccount.OnBalanceChange -= UpdateUIState;
}
_bankAccount = value;
if (value != null)
{
value.OnBalanceChange += UpdateUIState;
}
UpdateUIState();
}
}
[DataField("requestOnly")]
private bool _requestOnly = false;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private CargoConsoleSystem _cargoConsoleSystem = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CargoConsoleUiKey.Key);
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn(out GalacticMarketComponent _);
Owner.EnsureComponentWarn(out CargoOrderDatabaseComponent _);
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
_cargoConsoleSystem = EntitySystem.Get<CargoConsoleSystem>();
BankAccount = _cargoConsoleSystem.StationAccount;
}
public override void OnRemove()
{
if (UserInterface != null)
{
UserInterface.OnReceiveMessage -= UserInterfaceOnOnReceiveMessage;
}
base.OnRemove();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
if (!Owner.TryGetComponent(out CargoOrderDatabaseComponent? orders))
{
return;
}
var message = serverMsg.Message;
if (orders.Database == null)
return;
if (!Powered)
return;
switch (message)
{
case CargoConsoleAddOrderMessage msg:
{
if (msg.Amount <= 0 || _bankAccount == null)
{
break;
}
if (!_cargoConsoleSystem.AddOrder(orders.Database.Id, msg.Requester, msg.Reason, msg.ProductId,
msg.Amount, _bankAccount.Id))
{
SoundSystem.Play(Filter.Local(), "/Audio/Effects/error.ogg", Owner, AudioParams.Default);
}
break;
}
case CargoConsoleRemoveOrderMessage msg:
{
_cargoConsoleSystem.RemoveOrder(orders.Database.Id, msg.OrderNumber);
break;
}
case CargoConsoleApproveOrderMessage msg:
{
if (_requestOnly ||
!orders.Database.TryGetOrder(msg.OrderNumber, out var order) ||
_bankAccount == null)
{
break;
}
PrototypeManager.TryIndex(order.ProductId, out CargoProductPrototype? product);
if (product == null!)
break;
var capacity = _cargoConsoleSystem.GetCapacity(orders.Database.Id);
if (
capacity.CurrentCapacity == capacity.MaxCapacity
|| capacity.CurrentCapacity + order.Amount > capacity.MaxCapacity
|| !_cargoConsoleSystem.CheckBalance(_bankAccount.Id, (-product.PointCost) * order.Amount)
|| !_cargoConsoleSystem.ApproveOrder(orders.Database.Id, msg.OrderNumber)
|| !_cargoConsoleSystem.ChangeBalance(_bankAccount.Id, (-product.PointCost) * order.Amount)
)
{
SoundSystem.Play(Filter.Local(), "/Audio/Effects/error.ogg", Owner, AudioParams.Default);
break;
}
UpdateUIState();
break;
}
case CargoConsoleShuttleMessage _:
{
//var approvedOrders = _cargoOrderDataManager.RemoveAndGetApprovedFrom(orders.Database);
//orders.Database.ClearOrderCapacity();
// TODO replace with shuttle code
// TEMPORARY loop for spawning stuff on telepad (looks for a telepad adjacent to the console)
IEntity? cargoTelepad = null;
var indices = Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, _mapManager);
var offsets = new Vector2i[] { new Vector2i(0, 1), new Vector2i(1, 1), new Vector2i(1, 0), new Vector2i(1, -1),
new Vector2i(0, -1), new Vector2i(-1, -1), new Vector2i(-1, 0), new Vector2i(-1, 1), };
var adjacentEntities = new List<IEnumerable<IEntity>>(); //Probably better than IEnumerable.concat
foreach (var offset in offsets)
{
adjacentEntities.Add((indices+offset).GetEntitiesInTileFast(Owner.Transform.GridID));
}
foreach (var enumerator in adjacentEntities)
{
foreach (IEntity entity in enumerator)
{
if (entity.HasComponent<CargoTelepadComponent>() && entity.TryGetComponent<PowerReceiverComponent>(out var powerReceiver) && powerReceiver.Powered)
{
cargoTelepad = entity;
break;
}
}
}
if (cargoTelepad != null)
{
if (cargoTelepad.TryGetComponent<CargoTelepadComponent>(out var telepadComponent))
{
var approvedOrders = _cargoConsoleSystem.RemoveAndGetApprovedOrders(orders.Database.Id);
orders.Database.ClearOrderCapacity();
foreach (var order in approvedOrders)
{
if (!PrototypeManager.TryIndex(order.ProductId, out CargoProductPrototype? product))
continue;
for (var i = 0; i < order.Amount; i++)
{
telepadComponent.QueueTeleport(product);
}
}
}
}
break;
}
}
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!Powered)
return;
UserInterface?.Open(actor.PlayerSession);
}
private void UpdateUIState()
{
if (_bankAccount == null || !Owner.IsValid())
{
return;
}
var id = _bankAccount.Id;
var name = _bankAccount.Name;
var balance = _bankAccount.Balance;
var capacity = _cargoConsoleSystem.GetCapacity(id);
UserInterface?.SetState(new CargoConsoleInterfaceState(_requestOnly, id, name, balance, capacity));
}
}
}

View File

@@ -1,29 +0,0 @@
using Content.Server.Cargo;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Cargo;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
namespace Content.Server.GameObjects.Components.Cargo
{
[RegisterComponent]
public class CargoOrderDatabaseComponent : SharedCargoOrderDatabaseComponent
{
public CargoOrderDatabase? Database { get; set; }
public bool ConnectedToDatabase => Database != null;
public override void Initialize()
{
base.Initialize();
Database = EntitySystem.Get<CargoConsoleSystem>().StationOrderDatabase;
}
public override ComponentState GetComponentState(ICommonSession player)
{
if (!ConnectedToDatabase)
return new CargoOrderDatabaseState(null);
return new CargoOrderDatabaseState(Database?.GetOrders());
}
}
}

View File

@@ -1,91 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Shared.Prototypes.Cargo;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using System.Collections.Generic;
using Robust.Shared.Player;
namespace Content.Server.GameObjects.Components.Cargo
{
//This entire class is a PLACEHOLDER for the cargo shuttle.
[RegisterComponent]
public class CargoTelepadComponent : Component
{
public override string Name => "CargoTelepad";
private const float TeleportDuration = 0.5f;
private const float TeleportDelay = 15f;
private List<CargoProductPrototype> _teleportQueue = new List<CargoProductPrototype>();
private CargoTelepadState _currentState = CargoTelepadState.Unpowered;
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
PowerUpdate(powerChanged);
break;
}
}
public void QueueTeleport(CargoProductPrototype product)
{
_teleportQueue.Add(product);
TeleportLoop();
}
private void PowerUpdate(PowerChangedMessage args)
{
if (args.Powered && _currentState == CargoTelepadState.Unpowered) {
_currentState = CargoTelepadState.Idle;
if(Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
spriteComponent.LayerSetState(0, "idle");
TeleportLoop();
}
else if (!args.Powered)
{
_currentState = CargoTelepadState.Unpowered;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
spriteComponent.LayerSetState(0, "offline");
}
}
private void TeleportLoop()
{
if (_currentState == CargoTelepadState.Idle && _teleportQueue.Count > 0)
{
_currentState = CargoTelepadState.Charging;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
spriteComponent.LayerSetState(0, "idle");
Owner.SpawnTimer((int) (TeleportDelay * 1000), () =>
{
if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Charging && _teleportQueue.Count > 0)
{
_currentState = CargoTelepadState.Teleporting;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
spriteComponent.LayerSetState(0, "beam");
Owner.SpawnTimer((int) (TeleportDuration * 1000), () =>
{
if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Teleporting && _teleportQueue.Count > 0)
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/phasein.ogg", Owner, AudioParams.Default.WithVolume(-8f));
Owner.EntityManager.SpawnEntity(_teleportQueue[0].Product, Owner.Transform.Coordinates);
_teleportQueue.RemoveAt(0);
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
spriteComponent.LayerSetState(0, "idle");
_currentState = CargoTelepadState.Idle;
TeleportLoop();
}
});
}
});
}
}
private enum CargoTelepadState { Unpowered, Idle, Charging, Teleporting };
}
}

View File

@@ -1,15 +0,0 @@
using Content.Shared.GameObjects.Components.Cargo;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
namespace Content.Server.GameObjects.Components.Cargo
{
[RegisterComponent]
public class GalacticMarketComponent : SharedGalacticMarketComponent
{
public override ComponentState GetComponentState(ICommonSession player)
{
return new GalacticMarketState(GetProductIdList());
}
}
}

View File

@@ -1,156 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration;
using Content.Server.Eui;
using Content.Server.GameObjects.Components.GUI;
using Content.Shared.Administration;
using Content.Shared.Chemistry;
using Content.Shared.Eui;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
#nullable enable
namespace Content.Server.GameObjects.Components.Chemistry
{
[GlobalVerb]
internal sealed class AdminAddReagentVerb : GlobalVerb
{
public override bool RequireInteractionRange => false;
public override bool BlockedByContainers => false;
private const AdminFlags ReqFlags = AdminFlags.Fun;
private static void OpenAddReagentMenu(IPlayerSession player, IEntity target)
{
var euiMgr = IoCManager.Resolve<EuiManager>();
euiMgr.OpenEui(new AdminAddReagentEui(target), player);
}
public override void GetData(IEntity user, IEntity target, VerbData data)
{
// ISolutionInteractionsComponent doesn't exactly have an interface for "admin tries to refill this", so...
// Still have a path for SolutionContainerComponent in case it doesn't allow direct refilling.
if (!target.HasComponent<SolutionContainerComponent>()
&& !(target.TryGetComponent(out ISolutionInteractionsComponent? interactions)
&& interactions.CanInject))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Add Reagent...");
data.CategoryData = VerbCategories.Debug;
data.Visibility = VerbVisibility.Invisible;
var adminManager = IoCManager.Resolve<IAdminManager>();
if (user.TryGetComponent<ActorComponent>(out var player))
{
if (adminManager.HasAdminFlag(player.PlayerSession, ReqFlags))
{
data.Visibility = VerbVisibility.Visible;
}
}
}
public override void Activate(IEntity user, IEntity target)
{
var groupController = IoCManager.Resolve<IAdminManager>();
if (user.TryGetComponent<ActorComponent>(out var player))
{
if (groupController.HasAdminFlag(player.PlayerSession, ReqFlags))
OpenAddReagentMenu(player.PlayerSession, target);
}
}
private sealed class AdminAddReagentEui : BaseEui
{
private readonly IEntity _target;
[Dependency] private readonly IAdminManager _adminManager = default!;
public AdminAddReagentEui(IEntity target)
{
_target = target;
IoCManager.InjectDependencies(this);
}
public override void Opened()
{
StateDirty();
}
public override EuiStateBase GetNewState()
{
if (_target.TryGetComponent(out SolutionContainerComponent? container))
{
return new AdminAddReagentEuiState
{
CurVolume = container.CurrentVolume,
MaxVolume = container.MaxVolume
};
}
if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions))
{
return new AdminAddReagentEuiState
{
// We don't exactly have an absolute total volume so good enough.
CurVolume = ReagentUnit.Zero,
MaxVolume = interactions.InjectSpaceAvailable
};
}
return new AdminAddReagentEuiState
{
CurVolume = ReagentUnit.Zero,
MaxVolume = ReagentUnit.Zero
};
}
public override void HandleMessage(EuiMessageBase msg)
{
switch (msg)
{
case AdminAddReagentEuiMsg.Close:
Close();
break;
case AdminAddReagentEuiMsg.DoAdd doAdd:
// Double check that user wasn't de-adminned in the mean time...
// Or the target was deleted.
if (!_adminManager.HasAdminFlag(Player, ReqFlags) || _target.Deleted)
{
Close();
return;
}
var id = doAdd.ReagentId;
var amount = doAdd.Amount;
if (_target.TryGetComponent(out SolutionContainerComponent? container))
{
container.TryAddReagent(id, amount, out _);
}
else if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions))
{
var solution = new Solution(id, amount);
interactions.Inject(solution);
}
StateDirty();
if (doAdd.CloseAfter)
Close();
break;
}
}
}
}
}

View File

@@ -1,445 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Server.Utility;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry.ChemMaster;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Content.Shared.GameObjects.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Contains all the server-side logic for chem masters. See also <see cref="SharedChemMasterComponent"/>.
/// This includes initializing the component based on prototype data, and sending and receiving messages from the client.
/// Messages sent to the client are used to update update the user interface for a component instance.
/// Messages sent from the client are used to handle ui button presses.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IInteractUsing))]
public class ChemMasterComponent : SharedChemMasterComponent, IActivate, IInteractUsing, ISolutionChange
{
[ViewVariables] private ContainerSlot _beakerContainer = default!;
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private bool _bufferModeTransfer = true;
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private readonly Solution BufferSolution = new();
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ChemMasterUiKey.Key);
/// <summary>
/// Called once per instance of this component. Gets references to any other components needed
/// by this component and initializes it's UI and other data.
/// </summary>
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
_beakerContainer =
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-reagentContainerContainer");
//BufferSolution = Owner.BufferSolution
BufferSolution.RemoveAllSolution();
UpdateUserInterface();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
OnPowerChanged(powerChanged);
break;
}
}
private void OnPowerChanged(PowerChangedMessage e)
{
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
var msg = (UiActionMessage) obj.Message;
var needsPower = msg.action switch
{
UiAction.Eject => false,
_ => true,
};
if (!PlayerCanUseChemMaster(obj.Session.AttachedEntity, needsPower))
return;
switch (msg.action)
{
case UiAction.Eject:
TryEject(obj.Session.AttachedEntity);
break;
case UiAction.ChemButton:
TransferReagent(msg.id, msg.amount, msg.isBuffer);
break;
case UiAction.Transfer:
_bufferModeTransfer = true;
UpdateUserInterface();
break;
case UiAction.Discard:
_bufferModeTransfer = false;
UpdateUserInterface();
break;
case UiAction.CreatePills:
case UiAction.CreateBottles:
TryCreatePackage(obj.Session.AttachedEntity, msg.action, msg.pillAmount, msg.bottleAmount);
break;
default:
throw new ArgumentOutOfRangeException();
}
ClickSound();
}
/// <summary>
/// Checks whether the player entity is able to use the chem master.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the chem master, and false if it cannot.</returns>
private bool PlayerCanUseChemMaster(IEntity? playerEntity, bool needsPower = true)
{
//Need player entity to check if they are still able to use the chem master
if (playerEntity == null)
return false;
//Check if player can interact in their current state
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
return false;
//Check if device is powered
if (needsPower && !Powered)
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="SharedChemMasterComponent.ChemMasterBoundUserInterfaceState"/></returns>
private ChemMasterBoundUserInterfaceState GetUserInterfaceState()
{
var beaker = _beakerContainer.ContainedEntity;
if (beaker == null)
{
return new ChemMasterBoundUserInterfaceState(Powered, false, ReagentUnit.New(0), ReagentUnit.New(0),
"", Owner.Name, new List<Solution.ReagentQuantity>(), BufferSolution.Contents, _bufferModeTransfer, BufferSolution.TotalVolume);
}
var solution = beaker.GetComponent<SolutionContainerComponent>();
return new ChemMasterBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
beaker.Name, Owner.Name, solution.ReagentList, BufferSolution.Contents, _bufferModeTransfer, BufferSolution.TotalVolume);
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
UserInterface?.SetState(state);
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
/// Tries to eject into user's hands first, then ejects onto chem master if both hands are full.
/// </summary>
private void TryEject(IEntity user)
{
if (!HasBeaker)
return;
var beaker = _beakerContainer.ContainedEntity;
if(beaker is null)
return;
_beakerContainer.Remove(beaker);
UpdateUserInterface();
if(!user.TryGetComponent<HandsComponent>(out var hands) || !beaker.TryGetComponent<ItemComponent>(out var item))
return;
if (hands.CanPutInHand(item))
hands.PutInHand(item);
}
private void TransferReagent(string id, ReagentUnit amount, bool isBuffer)
{
if (!HasBeaker && _bufferModeTransfer) return;
var beaker = _beakerContainer.ContainedEntity;
if(beaker is null)
return;
var beakerSolution = beaker.GetComponent<SolutionContainerComponent>();
if (isBuffer)
{
foreach (var reagent in BufferSolution.Contents)
{
if (reagent.ReagentId == id)
{
ReagentUnit actualAmount;
if (amount == ReagentUnit.New(-1)) //amount is ReagentUnit.New(-1) when the client sends a message requesting to remove all solution from the container
{
actualAmount = ReagentUnit.Min(reagent.Quantity, beakerSolution.EmptyVolume);
}
else
{
actualAmount = ReagentUnit.Min(reagent.Quantity, amount, beakerSolution.EmptyVolume);
}
BufferSolution.RemoveReagent(id, actualAmount);
if (_bufferModeTransfer)
{
beakerSolution.TryAddReagent(id, actualAmount, out var _);
// beakerSolution.Solution.AddReagent(id, actualAmount);
}
break;
}
}
}
else
{
foreach (var reagent in beakerSolution.Solution.Contents)
{
if (reagent.ReagentId == id)
{
ReagentUnit actualAmount;
if (amount == ReagentUnit.New(-1))
{
actualAmount = reagent.Quantity;
}
else
{
actualAmount = ReagentUnit.Min(reagent.Quantity, amount);
}
beakerSolution.TryRemoveReagent(id, actualAmount);
BufferSolution.AddReagent(id, actualAmount);
break;
}
}
}
UpdateUserInterface();
}
private void TryCreatePackage(IEntity user, UiAction action, int pillAmount, int bottleAmount)
{
if (BufferSolution.TotalVolume == 0)
return;
if (action == UiAction.CreateBottles)
{
var individualVolume = BufferSolution.TotalVolume / ReagentUnit.New(bottleAmount);
if (individualVolume < ReagentUnit.New(1))
return;
var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(30));
for (int i = 0; i < bottleAmount; i++)
{
var bottle = Owner.EntityManager.SpawnEntity("ChemistryEmptyBottle01", Owner.Transform.Coordinates);
var bufferSolution = BufferSolution.SplitSolution(actualVolume);
bottle.TryGetComponent<SolutionContainerComponent>(out var bottleSolution);
bottleSolution?.TryAddSolution(bufferSolution);
//Try to give them the bottle
if (user.TryGetComponent<HandsComponent>(out var hands) &&
bottle.TryGetComponent<ItemComponent>(out var item))
{
if (hands.CanPutInHand(item))
{
hands.PutInHand(item);
continue;
}
}
//Put it on the floor
bottle.Transform.Coordinates = user.Transform.Coordinates;
//Give it an offset
bottle.RandomOffset(0.2f);
}
}
else //Pills
{
var individualVolume = BufferSolution.TotalVolume / ReagentUnit.New(pillAmount);
if (individualVolume < ReagentUnit.New(1))
return;
var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(50));
for (int i = 0; i < pillAmount; i++)
{
var pill = Owner.EntityManager.SpawnEntity("pill", Owner.Transform.Coordinates);
var bufferSolution = BufferSolution.SplitSolution(actualVolume);
pill.TryGetComponent<SolutionContainerComponent>(out var pillSolution);
pillSolution?.TryAddSolution(bufferSolution);
//Try to give them the bottle
if (user.TryGetComponent<HandsComponent>(out var hands) &&
pill.TryGetComponent<ItemComponent>(out var item))
{
if (hands.CanPutInHand(item))
{
hands.PutInHand(item);
continue;
}
}
//Put it on the floor
pill.Transform.Coordinates = user.Transform.Coordinates;
//Give it an offset
pill.RandomOffset(0.2f);
}
}
UpdateUserInterface();
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
UserInterface?.Open(actor.PlayerSession);
}
}
/// <summary>
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the chem master doesn't already
/// hold a container, it will be added to the chem master.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
/// <returns></returns>
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
{
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands!"));
return true;
}
if (hands.GetActiveHand == null)
{
Owner.PopupMessage(args.User, Loc.GetString("You have nothing in your hand!"));
return false;
}
var activeHandEntity = hands.GetActiveHand.Owner;
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
{
if (HasBeaker)
{
Owner.PopupMessage(args.User, Loc.GetString("This ChemMaster already has a container in it."));
}
else if (!solution.CanUseWithChemDispenser)
{
//If it can't fit in the chem master, don't put it in. For example, buckets and mop buckets can't fit.
Owner.PopupMessage(args.User, Loc.GetString("The {0:theName} is too large for the ChemMaster!", activeHandEntity));
}
else
{
_beakerContainer.Insert(activeHandEntity);
UpdateUserInterface();
}
}
else
{
Owner.PopupMessage(args.User, Loc.GetString("You can't put {0:theName} in the ChemMaster!", activeHandEntity));
}
return true;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => UpdateUserInterface();
private void ClickSound()
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
[Verb]
public sealed class EjectBeakerVerb : Verb<ChemMasterComponent>
{
protected override void GetData(IEntity user, ChemMasterComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Eject Beaker");
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
}
protected override void Activate(IEntity user, ChemMasterComponent component)
{
component.TryEject(user);
}
}
}
}

View File

@@ -1,82 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.Components.Inventory;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
[ComponentReference(typeof(SolutionAreaEffectComponent))]
public class FoamSolutionAreaEffectComponent : SolutionAreaEffectComponent
{
public override string Name => "FoamSolutionAreaEffect";
[DataField("foamedMetalPrototype")] private string? _foamedMetalPrototype;
protected override void UpdateVisuals()
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
SolutionContainerComponent != null)
{
appearance.SetData(FoamVisuals.Color, SolutionContainerComponent.Color.WithAlpha(0.80f));
}
}
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
{
if (SolutionContainerComponent == null)
return;
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
return;
// TODO: Add a permeability property to clothing
// For now it just adds to protection for each clothing equipped
var protection = 0f;
if (entity.TryGetComponent(out InventoryComponent? inventory))
{
foreach (var slot in inventory.Slots)
{
if (slot == EquipmentSlotDefines.Slots.BACKPACK ||
slot == EquipmentSlotDefines.Slots.POCKET1 ||
slot == EquipmentSlotDefines.Slots.POCKET2 ||
slot == EquipmentSlotDefines.Slots.IDCARD)
continue;
if (inventory.TryGetSlotItem(slot, out ItemComponent _))
protection += 0.025f;
}
}
var cloneSolution = SolutionContainerComponent.Solution.Clone();
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction * (1 - protection), bloodstream.EmptyVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
bloodstream.TryTransferSolution(transferSolution);
}
protected override void OnKill()
{
if (Owner.Deleted)
return;
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(FoamVisuals.State, true);
}
Owner.SpawnTimer(600, () =>
{
if (!string.IsNullOrEmpty(_foamedMetalPrototype))
{
Owner.EntityManager.SpawnEntity(_foamedMetalPrototype, Owner.Transform.Coordinates);
}
Owner.QueueDelete();
});
}
}
}

View File

@@ -1,119 +0,0 @@
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Mobs.State;
using Content.Server.GameObjects.EntitySystems.Weapon.Melee;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
public sealed class HyposprayComponent : SharedHyposprayComponent, ISolutionChange
{
[DataField("ClumsyFailChance")]
[ViewVariables(VVAccess.ReadWrite)]
public float ClumsyFailChance { get; set; } = 0.5f;
[DataField("TransferAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(5);
[ComponentDependency] private readonly SolutionContainerComponent? _solution = default!;
public override void Initialize()
{
base.Initialize();
Dirty();
}
public bool TryDoInject(IEntity? target, IEntity user)
{
if (target == null || !EligibleEntity(target))
return false;
var msgFormat = "You inject {0:TheName}.";
if (target == user)
{
msgFormat = "You inject yourself.";
}
else if (EligibleEntity(user) && ClumsyComponent.TryRollClumsy(user, ClumsyFailChance))
{
msgFormat = "Oops! You injected yourself!";
target = user;
}
if (_solution == null || _solution.CurrentVolume == 0)
{
user.PopupMessageCursor(Loc.GetString("It's empty!"));
return true;
}
user.PopupMessage(Loc.GetString(msgFormat, target));
if (target != user)
{
target.PopupMessage(Loc.GetString("You feel a tiny prick!"));
var meleeSys = EntitySystem.Get<MeleeWeaponSystem>();
var angle = Angle.FromWorldVec(target.Transform.WorldPosition - user.Transform.WorldPosition);
meleeSys.SendLunge(angle, user);
}
SoundSystem.Play(Filter.Pvs(user), "/Audio/Items/hypospray.ogg", user);
var targetSolution = target.GetComponent<SolutionContainerComponent>();
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(TransferAmount, targetSolution.EmptyVolume);
if (realTransferAmount <= 0)
{
user.PopupMessage(user, Loc.GetString("{0:TheName} is already full!", targetSolution.Owner));
return true;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solution.SplitSolution(realTransferAmount);
if (!targetSolution.CanAddSolution(removedSolution))
{
return true;
}
removedSolution.DoEntityReaction(target, ReactionMethod.Injection);
targetSolution.TryAddSolution(removedSolution);
static bool EligibleEntity(IEntity entity)
{
// TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag?
// In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else.
return entity.HasComponent<SolutionContainerComponent>() && entity.HasComponent<MobStateComponent>();
}
return true;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
Dirty();
}
public override ComponentState GetComponentState(ICommonSession player)
{
if (_solution == null)
return new HyposprayComponentState(ReagentUnit.Zero, ReagentUnit.Zero);
return new HyposprayComponentState(_solution.CurrentVolume, _solution.MaxVolume);
}
}
}

View File

@@ -1,302 +0,0 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Server behavior for reagent injectors and syringes. Can optionally support both
/// injection and drawing or just injection. Can inject/draw reagents from solution
/// containers, and can directly inject into a mobs bloodstream.
/// </summary>
[RegisterComponent]
public class InjectorComponent : SharedInjectorComponent, IAfterInteract, IUse, ISolutionChange
{
/// <summary>
/// Whether or not the injector is able to draw from containers or if it's a single use
/// device that can only inject.
/// </summary>
[ViewVariables]
[DataField("injectOnly")]
private bool _injectOnly;
/// <summary>
/// Amount to inject or draw on each usage. If the injector is inject only, it will
/// attempt to inject it's entire contents upon use.
/// </summary>
[ViewVariables]
[DataField("transferAmount")]
private ReagentUnit _transferAmount = ReagentUnit.New(5);
/// <summary>
/// Initial storage volume of the injector
/// </summary>
[ViewVariables]
[DataField("initialMaxVolume")]
private ReagentUnit _initialMaxVolume = ReagentUnit.New(15);
private InjectorToggleMode _toggleState;
/// <summary>
/// The state of the injector. Determines it's attack behavior. Containers must have the
/// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
/// only ever be set to Inject
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public InjectorToggleMode ToggleState
{
get => _toggleState;
set
{
_toggleState = value;
Dirty();
}
}
protected override void Startup()
{
base.Startup();
Dirty();
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
private void Toggle(IEntity user)
{
if (_injectOnly)
{
return;
}
string msg;
switch (ToggleState)
{
case InjectorToggleMode.Inject:
ToggleState = InjectorToggleMode.Draw;
msg = "Now drawing";
break;
case InjectorToggleMode.Draw:
ToggleState = InjectorToggleMode.Inject;
msg = "Now injecting";
break;
default:
throw new ArgumentOutOfRangeException();
}
Owner.PopupMessage(user, Loc.GetString(msg));
}
/// <summary>
/// Called when clicking on entities while holding in active hand
/// </summary>
/// <param name="eventArgs"></param>
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return false;
//Make sure we have the attacking entity
if (eventArgs.Target == null || !Owner.HasComponent<SolutionContainerComponent>())
{
return false;
}
var targetEntity = eventArgs.Target;
// Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<ISolutionInteractionsComponent>(out var targetSolution))
{
if (ToggleState == InjectorToggleMode.Inject)
{
if (targetSolution.CanInject)
{
TryInject(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner));
}
}
else if (ToggleState == InjectorToggleMode.Draw)
{
if (targetSolution.CanDraw)
{
TryDraw(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner));
}
}
}
// Handle injecting into bloodstream
else if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) &&
ToggleState == InjectorToggleMode.Inject)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
return true;
}
/// <summary>
/// Called when use key is pressed when held in active hand
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
Toggle(eventArgs.User);
return true;
}
private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetBloodstream.EmptyVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user,
Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
if (!solution.CanAddSolution(removedSolution))
{
return;
}
// TODO: Account for partial transfer.
removedSolution.DoEntityReaction(solution.Owner, ReactionMethod.Injection);
solution.TryAddSolution(removedSolution);
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
Owner.PopupMessage(user,
Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume,
targetBloodstream.Owner));
Dirty();
AfterInject();
}
private void TryInject(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.InjectSpaceAvailable);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("{0:theName} is already full!", targetSolution.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
removedSolution.DoEntityReaction(targetSolution.Owner, ReactionMethod.Injection);
targetSolution.Inject(removedSolution);
Owner.PopupMessage(user,
Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterInject();
}
private void AfterInject()
{
// Automatically set syringe to draw after completely draining it.
if (Owner.GetComponent<SolutionContainerComponent>().CurrentVolume == 0)
{
ToggleState = InjectorToggleMode.Draw;
}
}
private void TryDraw(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.EmptyVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.DrawAvailable);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("{0:theName} is empty!", targetSolution.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = targetSolution.Draw(realTransferAmount);
if (!solution.TryAddSolution(removedSolution))
{
return;
}
Owner.PopupMessage(user,
Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterDraw();
}
private void AfterDraw()
{
// Automatically set syringe to inject after completely filling it.
if (Owner.GetComponent<SolutionContainerComponent>().EmptyVolume == 0)
{
ToggleState = InjectorToggleMode.Inject;
}
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
Dirty();
}
public override ComponentState GetComponentState(ICommonSession player)
{
Owner.TryGetComponent(out SolutionContainerComponent? solution);
var currentVolume = solution?.CurrentVolume ?? ReagentUnit.Zero;
var maxVolume = solution?.MaxVolume ?? ReagentUnit.Zero;
return new InjectorComponentState(currentVolume, maxVolume, ToggleState);
}
}
}

View File

@@ -1,115 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Body.Behavior;
using Content.Server.GameObjects.Components.Culinary;
using Content.Server.GameObjects.Components.Nutrition;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
public class PillComponent : FoodComponent, IUse, IAfterInteract
{
public override string Name => "Pill";
[ViewVariables]
[DataField("useSound")]
protected override string? UseSound { get; set; } = default;
[ViewVariables]
[DataField("trash")]
protected override string? TrashPrototype { get; set; } = default;
[ViewVariables]
[DataField("transferAmount")]
protected override ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(1000);
[ViewVariables]
private SolutionContainerComponent _contents = default!;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn(out _contents);
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
return TryUseFood(eventArgs.User, null);
}
// Feeding someone else
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return false;
}
TryUseFood(eventArgs.User, eventArgs.Target);
return true;
}
public override bool TryUseFood(IEntity? user, IEntity? target, UtensilComponent? utensilUsed = null)
{
if (user == null)
{
return false;
}
var trueTarget = target ?? user;
if (!trueTarget.TryGetComponent(out IBody? body) ||
!body.TryGetMechanismBehaviors<StomachBehavior>(out var stomachs))
{
return false;
}
if (!user.InRangeUnobstructed(trueTarget, popup: true))
{
return false;
}
var transferAmount = ReagentUnit.Min(TransferAmount, _contents.CurrentVolume);
var split = _contents.SplitSolution(transferAmount);
var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split));
if (firstStomach == null)
{
_contents.TryAddSolution(split);
trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!"));
return false;
}
// TODO: Account for partial transfer.
split.DoEntityReaction(trueTarget, ReactionMethod.Ingestion);
firstStomach.TryTransferSolution(split);
if (UseSound != null)
{
SoundSystem.Play(Filter.Pvs(trueTarget), UseSound, trueTarget, AudioParams.Default.WithVolume(-1f));
}
trueTarget.PopupMessage(user, Loc.GetString("You swallow the pill."));
Owner.QueueDelete();
return true;
}
}
}

View File

@@ -1,392 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Server.Utility;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry.ReagentDispenser;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.GameObjects.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Contains all the server-side logic for reagent dispensers. See also <see cref="SharedReagentDispenserComponent"/>.
/// This includes initializing the component based on prototype data, and sending and receiving messages from the client.
/// Messages sent to the client are used to update update the user interface for a component instance.
/// Messages sent from the client are used to handle ui button presses.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IInteractUsing))]
public class ReagentDispenserComponent : SharedReagentDispenserComponent, IActivate, IInteractUsing, ISolutionChange
{
private static ReagentInventoryComparer _comparer = new();
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[ViewVariables] private ContainerSlot _beakerContainer = default!;
[ViewVariables] [DataField("pack")] private string _packPrototypeId = "";
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10);
[UsedImplicitly] [ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentDispenserUiKey.Key);
/// <summary>
/// Called once per instance of this component. Gets references to any other components needed
/// by this component and initializes it's UI and other data.
/// </summary>
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
_beakerContainer =
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-reagentContainerContainer");
InitializeFromPrototype();
UpdateUserInterface();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
OnPowerChanged(powerChanged);
break;
}
}
/// <summary>
/// Checks to see if the <c>pack</c> defined in this components yaml prototype
/// exists. If so, it fills the reagent inventory list.
/// </summary>
private void InitializeFromPrototype()
{
if (string.IsNullOrEmpty(_packPrototypeId)) return;
if (!_prototypeManager.TryIndex(_packPrototypeId, out ReagentDispenserInventoryPrototype? packPrototype))
{
return;
}
foreach (var entry in packPrototype.Inventory)
{
Inventory.Add(new ReagentDispenserInventoryEntry(entry));
}
Inventory.Sort(_comparer);
}
private void OnPowerChanged(PowerChangedMessage e)
{
UpdateUserInterface();
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
var msg = (UiButtonPressedMessage) obj.Message;
var needsPower = msg.Button switch
{
UiButton.Eject => false,
_ => true,
};
if(!PlayerCanUseDispenser(obj.Session.AttachedEntity, needsPower))
return;
switch (msg.Button)
{
case UiButton.Eject:
TryEject(obj.Session.AttachedEntity);
break;
case UiButton.Clear:
TryClear();
break;
case UiButton.SetDispenseAmount1:
_dispenseAmount = ReagentUnit.New(1);
break;
case UiButton.SetDispenseAmount5:
_dispenseAmount = ReagentUnit.New(5);
break;
case UiButton.SetDispenseAmount10:
_dispenseAmount = ReagentUnit.New(10);
break;
case UiButton.SetDispenseAmount15:
_dispenseAmount = ReagentUnit.New(15);
break;
case UiButton.SetDispenseAmount20:
_dispenseAmount = ReagentUnit.New(20);
break;
case UiButton.SetDispenseAmount25:
_dispenseAmount = ReagentUnit.New(25);
break;
case UiButton.SetDispenseAmount30:
_dispenseAmount = ReagentUnit.New(30);
break;
case UiButton.SetDispenseAmount50:
_dispenseAmount = ReagentUnit.New(50);
break;
case UiButton.SetDispenseAmount100:
_dispenseAmount = ReagentUnit.New(100);
break;
case UiButton.Dispense:
if (HasBeaker)
{
TryDispense(msg.DispenseIndex);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
ClickSound();
}
/// <summary>
/// Checks whether the player entity is able to use the chem dispenser.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the dispenser, and false if it cannot.</returns>
private bool PlayerCanUseDispenser(IEntity? playerEntity, bool needsPower = true)
{
//Need player entity to check if they are still able to use the dispenser
if (playerEntity == null)
return false;
//Check if player can interact in their current state
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
return false;
//Check if device is powered
if (needsPower && !Powered)
return false;
return true;
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="SharedReagentDispenserComponent.ReagentDispenserBoundUserInterfaceState"/></returns>
private ReagentDispenserBoundUserInterfaceState GetUserInterfaceState()
{
var beaker = _beakerContainer.ContainedEntity;
if (beaker == null)
{
return new ReagentDispenserBoundUserInterfaceState(Powered, false, ReagentUnit.New(0), ReagentUnit.New(0),
string.Empty, Inventory, Owner.Name, null, _dispenseAmount);
}
var solution = beaker.GetComponent<SolutionContainerComponent>();
return new ReagentDispenserBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
beaker.Name, Inventory, Owner.Name, solution.ReagentList.ToList(), _dispenseAmount);
}
private void UpdateUserInterface()
{
var state = GetUserInterfaceState();
UserInterface?.SetState(state);
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
/// Tries to eject into user's hands first, then ejects onto dispenser if both hands are full.
/// </summary>
private void TryEject(IEntity user)
{
if (!HasBeaker)
return;
var beaker = _beakerContainer.ContainedEntity;
if(beaker is null)
return;
_beakerContainer.Remove(beaker);
UpdateUserInterface();
if(!user.TryGetComponent<HandsComponent>(out var hands) || !beaker.TryGetComponent<ItemComponent>(out var item))
return;
if (hands.CanPutInHand(item))
hands.PutInHand(item);
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, remove all of it's reagents / solutions.
/// </summary>
private void TryClear()
{
if (!HasBeaker) return;
var solution = _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
if(solution is null)
return;
solution.RemoveAllSolution();
UpdateUserInterface();
}
/// <summary>
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, attempt to dispense the specified reagent to it.
/// </summary>
/// <param name="dispenseIndex">The index of the reagent in <c>Inventory</c>.</param>
private void TryDispense(int dispenseIndex)
{
if (!HasBeaker) return;
var solution = _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
if (solution is null)
return;
solution.TryAddReagent(Inventory[dispenseIndex].ID, _dispenseAmount, out _);
UpdateUserInterface();
}
/// <summary>
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
void IActivate.Activate(ActivateEventArgs args)
{
if (!args.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = hands.GetActiveHand?.Owner;
if (activeHandEntity == null)
{
UserInterface?.Open(actor.PlayerSession);
}
}
/// <summary>
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the dispenser doesn't already
/// hold a container, it will be added to the dispenser.
/// </summary>
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
/// <returns></returns>
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
{
if (!args.User.TryGetComponent(out IHandsComponent? hands))
{
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
return true;
}
if (hands.GetActiveHand == null)
{
Owner.PopupMessage(args.User, Loc.GetString("You have nothing on your hand."));
return false;
}
var activeHandEntity = hands.GetActiveHand.Owner;
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
{
if (HasBeaker)
{
Owner.PopupMessage(args.User, Loc.GetString("This dispenser already has a container in it."));
}
else if ((solution.Capabilities & SolutionContainerCaps.FitsInDispenser) == 0)
{
//If it can't fit in the dispenser, don't put it in. For example, buckets and mop buckets can't fit.
Owner.PopupMessage(args.User, Loc.GetString("That can't fit in the dispenser."));
}
else
{
_beakerContainer.Insert(activeHandEntity);
UpdateUserInterface();
}
}
else
{
Owner.PopupMessage(args.User, Loc.GetString("You can't put this in the dispenser."));
}
return true;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => UpdateUserInterface();
private void ClickSound()
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
[Verb]
public sealed class EjectBeakerVerb : Verb<ReagentDispenserComponent>
{
protected override void GetData(IEntity user, ReagentDispenserComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Eject Beaker");
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
}
protected override void Activate(IEntity user, ReagentDispenserComponent component)
{
component.TryEject(user);
}
}
private class ReagentInventoryComparer : Comparer<ReagentDispenserInventoryEntry>
{
public override int Compare(ReagentDispenserInventoryEntry x, ReagentDispenserInventoryEntry y)
{
return string.Compare(x.ID, y.ID, StringComparison.InvariantCultureIgnoreCase);
}
}
}
}

View File

@@ -1,29 +0,0 @@
using Content.Shared.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
#nullable enable
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
public class ReagentTankComponent : Component
{
public override string Name => "ReagentTank";
[DataField("transferAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(10);
[DataField("tankType")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentTankType TankType { get; set; } = ReagentTankType.Unspecified;
}
public enum ReagentTankType : byte
{
Unspecified,
Fuel
}
}

View File

@@ -1,60 +0,0 @@
#nullable enable
using Content.Server.Utility;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Basically, monkey cubes.
/// But specifically, this component deletes the entity and spawns in a new entity when the entity is exposed to a given reagent.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(ISolutionChange))]
public class RehydratableComponent : Component, ISolutionChange
{
public override string Name => "Rehydratable";
[ViewVariables]
[DataField("catalyst")]
private string _catalystPrototype = "Water";
[ViewVariables]
[DataField("target")]
private string? _targetPrototype = default!;
private bool _expanding;
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
var solution = eventArgs.Owner.GetComponent<SolutionContainerComponent>();
if (solution.Solution.GetReagentQuantity(_catalystPrototype) > ReagentUnit.Zero)
{
Expand();
}
}
// Try not to make this public if you can help it.
private void Expand()
{
if (_expanding)
{
return;
}
_expanding = true;
Owner.PopupMessageEveryone(Loc.GetString("{0:TheName} expands!", Owner));
if (!string.IsNullOrEmpty(_targetPrototype))
{
var ent = Owner.EntityManager.SpawnEntity(_targetPrototype, Owner.Transform.Coordinates);
ent.Transform.AttachToGridOrMap();
}
Owner.Delete();
}
}
}

View File

@@ -1,63 +0,0 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
[ComponentReference(typeof(SolutionAreaEffectComponent))]
public class SmokeSolutionAreaEffectComponent : SolutionAreaEffectComponent
{
public override string Name => "SmokeSolutionAreaEffect";
protected override void UpdateVisuals()
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
SolutionContainerComponent != null)
{
appearance.SetData(SmokeVisuals.Color, SolutionContainerComponent.Color);
}
}
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
{
if (SolutionContainerComponent == null)
return;
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
return;
if (entity.TryGetComponent(out InternalsComponent? internals) &&
internals.AreInternalsWorking())
return;
var chemistry = EntitySystem.Get<ChemistrySystem>();
var cloneSolution = SolutionContainerComponent.Solution.Clone();
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction, bloodstream.EmptyVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
{
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
chemistry.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.ReagentId, reagentQuantity.Quantity, transferSolution);
}
bloodstream.TryTransferSolution(transferSolution);
}
protected override void OnKill()
{
if (Owner.Deleted)
return;
Owner.Delete();
}
}
}

View File

@@ -1,179 +0,0 @@
#nullable enable
using System;
using System.Linq;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.Utility;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Used to clone its owner repeatedly and group up them all so they behave like one unit, that way you can have
/// effects that cover an area. Inherited by <see cref="SmokeSolutionAreaEffectComponent"/> and <see cref="FoamSolutionAreaEffectComponent"/>.
/// </summary>
public abstract class SolutionAreaEffectComponent : Component
{
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[ComponentDependency] protected readonly SolutionContainerComponent? SolutionContainerComponent = default!;
public int Amount { get; set; }
public SolutionAreaEffectInceptionComponent? Inception { get; set; }
/// <summary>
/// Adds an <see cref="SolutionAreaEffectInceptionComponent"/> to owner so the effect starts spreading and reacting.
/// </summary>
/// <param name="amount">The range of the effect</param>
/// <param name="duration"></param>
/// <param name="spreadDelay"></param>
/// <param name="removeDelay"></param>
public void Start(int amount, float duration, float spreadDelay, float removeDelay)
{
if (Inception != null)
return;
if (Owner.HasComponent<SolutionAreaEffectInceptionComponent>())
return;
Amount = amount;
var inception = Owner.AddComponent<SolutionAreaEffectInceptionComponent>();
inception.Add(this);
inception.Setup(amount, duration, spreadDelay, removeDelay);
}
/// <summary>
/// Gets called by an AreaEffectInceptionComponent. "Clones" Owner into the four directions and copies the
/// solution into each of them.
/// </summary>
public void Spread()
{
if (Owner.Prototype == null)
{
Logger.Error("AreaEffectComponent needs its owner to be spawned by a prototype.");
return;
}
void SpreadToDir(Direction dir)
{
var grid = MapManager.GetGrid(Owner.Transform.GridID);
var coords = Owner.Transform.Coordinates;
foreach (var neighbor in grid.GetInDir(coords, dir))
{
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out SolutionAreaEffectComponent? comp) && comp.Inception == Inception)
return;
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out AirtightComponent? airtight) && airtight.AirBlocked)
return;
}
var newEffect = Owner.EntityManager.SpawnEntity(Owner.Prototype.ID, grid.DirectionToGrid(coords, dir));
if (!newEffect.TryGetComponent(out SolutionAreaEffectComponent? effectComponent))
{
newEffect.Delete();
return;
}
if (SolutionContainerComponent != null)
{
effectComponent.TryAddSolution(SolutionContainerComponent.Solution.Clone());
}
effectComponent.Amount = Amount - 1;
Inception?.Add(effectComponent);
}
SpreadToDir(Direction.North);
SpreadToDir(Direction.East);
SpreadToDir(Direction.South);
SpreadToDir(Direction.West);
}
/// <summary>
/// Gets called by an AreaEffectInceptionComponent.
/// Removes this component from its inception and calls OnKill(). The implementation of OnKill() should
/// eventually delete the entity.
/// </summary>
public void Kill()
{
Inception?.Remove(this);
OnKill();
}
protected abstract void OnKill();
/// <summary>
/// Gets called by an AreaEffectInceptionComponent.
/// Makes this effect's reagents react with the tile its on and with the entities it covers. Also calls
/// ReactWithEntity on the entities so inheritors can implement more specific behavior.
/// </summary>
/// <param name="averageExposures">How many times will this get called over this area effect's duration, averaged
/// with the other area effects from the inception.</param>
public void React(float averageExposures)
{
if (SolutionContainerComponent == null)
return;
var chemistry = EntitySystem.Get<ChemistrySystem>();
var mapGrid = MapManager.GetGrid(Owner.Transform.GridID);
var tile = mapGrid.GetTileRef(Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, MapManager));
var solutionFraction = 1 / Math.Floor(averageExposures);
foreach (var reagentQuantity in SolutionContainerComponent.ReagentList.ToArray())
{
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
var reagent = PrototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
// React with the tile the effect is on
reagent.ReactionTile(tile, reagentQuantity.Quantity * solutionFraction);
// Touch every entity on the tile
foreach (var entity in tile.GetEntitiesInTileFast().ToArray())
{
chemistry.ReactionEntity(entity, ReactionMethod.Touch, reagent, reagentQuantity.Quantity * solutionFraction, SolutionContainerComponent.Solution);
}
}
foreach (var entity in tile.GetEntitiesInTileFast().ToArray())
{
ReactWithEntity(entity, solutionFraction);
}
}
protected abstract void ReactWithEntity(IEntity entity, double solutionFraction);
public void TryAddSolution(Solution solution)
{
if (solution.TotalVolume == 0)
return;
if (SolutionContainerComponent == null)
return;
var addSolution =
solution.SplitSolution(ReagentUnit.Min(solution.TotalVolume, SolutionContainerComponent.EmptyVolume));
SolutionContainerComponent.TryAddSolution(addSolution);
UpdateVisuals();
}
protected abstract void UpdateVisuals();
public override void OnRemove()
{
base.OnRemove();
Inception?.Remove(this);
}
}
}

View File

@@ -1,142 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// The "mastermind" of a SolutionAreaEffect group. It gets updated by the SolutionAreaEffectSystem and tells the
/// group when to spread, react and remove itself. This makes the group act like a single unit.
/// </summary>
/// <remarks> It should only be manually added to an entity by the <see cref="SolutionAreaEffectComponent"/> and not with a prototype.</remarks>
[RegisterComponent]
public class SolutionAreaEffectInceptionComponent : Component
{
public override string Name => "AreaEffectInception";
private const float ReactionDelay = 0.5f;
private readonly HashSet<SolutionAreaEffectComponent> _group = new();
[ViewVariables] private float _lifeTimer;
[ViewVariables] private float _spreadTimer;
[ViewVariables] private float _reactionTimer;
[ViewVariables] private int _amountCounterSpreading;
[ViewVariables] private int _amountCounterRemoving;
/// <summary>
/// How much time to wait after fully spread before starting to remove itself.
/// </summary>
[ViewVariables] private float _duration;
/// <summary>
/// Time between each spread step. Decreasing this makes spreading faster.
/// </summary>
[ViewVariables] private float _spreadDelay;
/// <summary>
/// Time between each remove step. Decreasing this makes removing faster.
/// </summary>
[ViewVariables] private float _removeDelay;
/// <summary>
/// How many times will the effect react. As some entities from the group last a different amount of time than
/// others, they will react a different amount of times, so we calculate the average to make the group behave
/// a bit more uniformly.
/// </summary>
[ViewVariables] private float _averageExposures;
public void Setup(int amount, float duration, float spreadDelay, float removeDelay)
{
_amountCounterSpreading = amount;
_duration = duration;
_spreadDelay = spreadDelay;
_removeDelay = removeDelay;
// So the first square reacts immediately after spawning
_reactionTimer = ReactionDelay;
/*
The group takes amount*spreadDelay seconds to fully spread, same with fully disappearing.
The outer squares will last duration seconds.
The first square will last duration + how many seconds the group takes to fully spread and fully disappear, so
it will last duration + amount*(spreadDelay+removeDelay).
Thus, the average lifetime of the smokes will be (outerSmokeLifetime + firstSmokeLifetime)/2 = duration + amount*(spreadDelay+removeDelay)/2
*/
_averageExposures = (duration + amount * (spreadDelay+removeDelay) / 2)/ReactionDelay;
}
public void InceptionUpdate(float frameTime)
{
_group.RemoveWhere(effect => effect.Deleted);
if (_group.Count == 0)
return;
// Make every outer square from the group spread
if (_amountCounterSpreading > 0)
{
_spreadTimer += frameTime;
if (_spreadTimer > _spreadDelay)
{
_spreadTimer -= _spreadDelay;
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterSpreading));
foreach (var effect in outerEffects)
{
effect.Spread();
}
_amountCounterSpreading -= 1;
}
}
// Start counting for _duration after fully spreading
else
{
_lifeTimer += frameTime;
}
// Delete every outer square
if (_lifeTimer > _duration)
{
_spreadTimer += frameTime;
if (_spreadTimer > _removeDelay)
{
_spreadTimer -= _removeDelay;
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterRemoving));
foreach (var effect in outerEffects)
{
effect.Kill();
}
_amountCounterRemoving += 1;
}
}
// Make every square from the group react with the tile and entities
_reactionTimer += frameTime;
if (_reactionTimer > ReactionDelay)
{
_reactionTimer -= ReactionDelay;
foreach (var effect in _group)
{
effect.React(_averageExposures);
}
}
}
public void Add(SolutionAreaEffectComponent effect)
{
_group.Add(effect);
effect.Inception = this;
}
public void Remove(SolutionAreaEffectComponent effect)
{
_group.Remove(effect);
effect.Inception = null;
}
}
}

View File

@@ -1,13 +0,0 @@
#nullable enable
using Content.Shared.GameObjects.Components.Chemistry;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
[ComponentReference(typeof(SharedSolutionContainerComponent))]
[ComponentReference(typeof(ISolutionInteractionsComponent))]
public class SolutionContainerComponent : SharedSolutionContainerComponent
{
}
}

View File

@@ -1,123 +0,0 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Gives click behavior for transferring to/from other reagent containers.
/// </summary>
[RegisterComponent]
public sealed class SolutionTransferComponent : Component, IAfterInteract
{
// Behavior is as such:
// If it's a reagent tank, TAKE reagent.
// If it's anything else, GIVE reagent.
// Of course, only if possible.
public override string Name => "SolutionTransfer";
/// <summary>
/// The amount of solution to be transferred from this solution when clicking on other solutions with it.
/// </summary>
[DataField("transferAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(5);
/// <summary>
/// Can this entity take reagent from reagent tanks?
/// </summary>
[DataField("canReceive")]
[ViewVariables(VVAccess.ReadWrite)]
public bool CanReceive { get; set; } = true;
/// <summary>
/// Can this entity give reagent to other reagent containers?
/// </summary>
[DataField("canSend")]
[ViewVariables(VVAccess.ReadWrite)]
public bool CanSend { get; set; } = true;
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed() || eventArgs.Target == null)
return false;
if (!Owner.TryGetComponent(out ISolutionInteractionsComponent? ownerSolution))
return false;
var target = eventArgs.Target;
if (!target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution))
{
return false;
}
if (CanReceive && target.TryGetComponent(out ReagentTankComponent? tank)
&& ownerSolution.CanRefill && targetSolution.CanDrain)
{
var transferred = DoTransfer(targetSolution, ownerSolution, tank.TransferAmount, eventArgs.User);
if (transferred > 0)
{
var toTheBrim = ownerSolution.RefillSpaceAvailable == 0;
var msg = toTheBrim
? "You fill {0:TheName} to the brim with {1}u from {2:theName}"
: "You fill {0:TheName} with {1}u from {2:theName}";
target.PopupMessage(eventArgs.User, Loc.GetString(msg, Owner, transferred, target));
return true;
}
}
if (CanSend && targetSolution.CanRefill && ownerSolution.CanDrain)
{
var transferred = DoTransfer(ownerSolution, targetSolution, TransferAmount, eventArgs.User);
if (transferred > 0)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.",
transferred, target));
return true;
}
}
return true;
}
/// <returns>The actual amount transferred.</returns>
private static ReagentUnit DoTransfer(
ISolutionInteractionsComponent source,
ISolutionInteractionsComponent target,
ReagentUnit amount,
IEntity user)
{
if (source.DrainAvailable == 0)
{
source.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is empty!", source.Owner));
return ReagentUnit.Zero;
}
if (target.RefillSpaceAvailable == 0)
{
target.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is full!", target.Owner));
return ReagentUnit.Zero;
}
var actualAmount =
ReagentUnit.Min(amount, ReagentUnit.Min(source.DrainAvailable, target.RefillSpaceAvailable));
var solution = source.Drain(actualAmount);
target.Refill(solution);
return actualAmount;
}
}
}

View File

@@ -1,101 +0,0 @@
#nullable enable
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
public class TransformableContainerComponent : Component, ISolutionChange
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Name => "TransformableContainer";
private SpriteSpecifier? _initialSprite;
private string _initialName = default!;
private string _initialDescription = default!;
private ReagentPrototype? _currentReagent;
public bool Transformed { get; private set; }
public override void Initialize()
{
base.Initialize();
if (Owner.TryGetComponent(out SpriteComponent? sprite) &&
sprite.BaseRSIPath != null)
{
_initialSprite = new SpriteSpecifier.Rsi(new ResourcePath(sprite.BaseRSIPath), "icon");
}
_initialName = Owner.Name;
_initialDescription = Owner.Description;
}
protected override void Startup()
{
base.Startup();
Owner.EnsureComponentWarn(out SolutionContainerComponent solution);
solution.Capabilities |= SolutionContainerCaps.FitsInDispenser;
}
public void CancelTransformation()
{
_currentReagent = null;
Transformed = false;
if (Owner.TryGetComponent(out SpriteComponent? sprite) &&
_initialSprite != null)
{
sprite.LayerSetSprite(0, _initialSprite);
}
Owner.Name = _initialName;
Owner.Description = _initialDescription;
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
var solution = eventArgs.Owner.GetComponent<SolutionContainerComponent>();
//Transform container into initial state when emptied
if (_currentReagent != null && solution.ReagentList.Count == 0)
{
CancelTransformation();
}
//the biggest reagent in the solution decides the appearance
var reagentId = solution.Solution.GetPrimaryReagentId();
//If biggest reagent didn't changed - don't change anything at all
if (_currentReagent != null && _currentReagent.ID == reagentId)
{
return;
}
//Only reagents with spritePath property can change appearance of transformable containers!
if (!string.IsNullOrWhiteSpace(reagentId) &&
_prototypeManager.TryIndex(reagentId, out ReagentPrototype? proto) &&
!string.IsNullOrWhiteSpace(proto.SpriteReplacementPath))
{
var spriteSpec = new SpriteSpecifier.Rsi(new ResourcePath("Objects/Consumable/Drinks/" + proto.SpriteReplacementPath),"icon");
if (Owner.TryGetComponent(out SpriteComponent? sprite))
{
sprite?.LayerSetSprite(0, spriteSpec);
}
Owner.Name = proto.Name + " glass";
Owner.Description = proto.Description;
_currentReagent = proto;
Transformed = true;
}
}
}
}

View File

@@ -1,132 +0,0 @@
using System.Linq;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
class VaporComponent : SharedVaporComponent, IStartCollide
{
public const float ReactTime = 0.125f;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[ViewVariables]
[DataField("transferAmount")]
private ReagentUnit _transferAmount = ReagentUnit.New(0.5);
private bool _reached;
private float _reactTimer;
private float _timer;
private EntityCoordinates _target;
private bool _running;
private float _aliveTime;
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponentWarn(out SolutionContainerComponent _);
}
public void Start(Vector2 dir, float speed, EntityCoordinates target, float aliveTime)
{
_running = true;
_target = target;
_aliveTime = aliveTime;
// Set Move
if (Owner.TryGetComponent(out PhysicsComponent? physics))
{
physics.BodyStatus = BodyStatus.InAir;
physics.ApplyLinearImpulse(dir * speed);
}
}
public void Update(float frameTime)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
return;
if (!_running)
return;
_timer += frameTime;
_reactTimer += frameTime;
if (_reactTimer >= ReactTime && Owner.Transform.GridID.IsValid())
{
_reactTimer = 0;
var mapGrid = _mapManager.GetGrid(Owner.Transform.GridID);
var tile = mapGrid.GetTileRef(Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, _mapManager));
foreach (var reagentQuantity in contents.ReagentList.ToArray())
{
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
var reagent = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
contents.TryRemoveReagent(reagentQuantity.ReagentId, reagent.ReactionTile(tile, (reagentQuantity.Quantity / _transferAmount) * 0.25f));
}
}
// Check if we've reached our target.
if(!_reached && _target.TryDistance(Owner.EntityManager, Owner.Transform.Coordinates, out var distance) && distance <= 0.5f)
{
_reached = true;
}
if (contents.CurrentVolume == 0 || _timer > _aliveTime)
{
// Delete this
Owner.Delete();
}
}
internal bool TryAddSolution(Solution solution)
{
if (solution.TotalVolume == 0)
{
return false;
}
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
{
return false;
}
var result = contents.TryAddSolution(solution);
if (!result)
{
return false;
}
return true;
}
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
return;
contents.Solution.DoEntityReaction(otherFixture.Body.Owner, ReactionMethod.Touch);
// Check for collision with a impassable object (e.g. wall) and stop
if ((otherFixture.CollisionLayer & (int) CollisionGroup.Impassable) != 0 && otherFixture.Hard)
{
Owner.QueueDelete();
}
}
}
}

View File

@@ -1,20 +0,0 @@
using Content.Shared.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
public class CleanableComponent : Component
{
public override string Name => "Cleanable";
[DataField("cleanAmount")]
private ReagentUnit _cleanAmount = ReagentUnit.Zero;
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit CleanAmount => _cleanAmount;
}
}

View File

@@ -1,142 +0,0 @@
#nullable enable
using System;
using System.Globalization;
using System.Threading;
using Content.Server.GameObjects.Components.PDA;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.Chat;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Command;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameObjects.Components.Command
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent, IActivate
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private RoundEndSystem RoundEndSystem => EntitySystem.Get<RoundEndSystem>();
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CommunicationsConsoleUiKey.Key);
public TimeSpan LastAnnounceTime { get; private set; } = TimeSpan.Zero;
public TimeSpan AnnounceCooldown { get; } = TimeSpan.FromSeconds(90);
private CancellationTokenSource _announceCooldownEndedTokenSource = new();
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
RoundEndSystem.OnRoundEndCountdownStarted += UpdateBoundInterface;
RoundEndSystem.OnRoundEndCountdownCancelled += UpdateBoundInterface;
RoundEndSystem.OnRoundEndCountdownFinished += UpdateBoundInterface;
RoundEndSystem.OnCallCooldownEnded += UpdateBoundInterface;
}
protected override void Startup()
{
base.Startup();
UpdateBoundInterface();
}
private void UpdateBoundInterface()
{
if (!Deleted)
{
var system = RoundEndSystem;
UserInterface?.SetState(new CommunicationsConsoleInterfaceState(CanAnnounce(), system.CanCall(), system.ExpectedCountdownEnd));
}
}
public bool CanAnnounce()
{
if (LastAnnounceTime == TimeSpan.Zero)
{
return true;
}
return _gameTiming.CurTime >= LastAnnounceTime + AnnounceCooldown;
}
public override void OnRemove()
{
RoundEndSystem.OnRoundEndCountdownStarted -= UpdateBoundInterface;
RoundEndSystem.OnRoundEndCountdownCancelled -= UpdateBoundInterface;
RoundEndSystem.OnRoundEndCountdownFinished -= UpdateBoundInterface;
base.OnRemove();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
switch (obj.Message)
{
case CommunicationsConsoleCallEmergencyShuttleMessage _:
RoundEndSystem.RequestRoundEnd();
break;
case CommunicationsConsoleRecallEmergencyShuttleMessage _:
RoundEndSystem.CancelRoundEndCountdown();
break;
case CommunicationsConsoleAnnounceMessage msg:
if (!CanAnnounce())
{
return;
}
_announceCooldownEndedTokenSource.Cancel();
_announceCooldownEndedTokenSource = new CancellationTokenSource();
LastAnnounceTime = _gameTiming.CurTime;
Timer.Spawn(AnnounceCooldown, () => UpdateBoundInterface(), _announceCooldownEndedTokenSource.Token);
UpdateBoundInterface();
var message = msg.Message.Length <= 256 ? msg.Message.Trim() : $"{msg.Message.Trim().Substring(0, 256)}...";
var author = "Unknown";
var mob = obj.Session.AttachedEntity;
if (mob != null && mob.TryGetHeldId(out var id))
{
author = $"{id.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id.JobTitle ?? string.Empty)})".Trim();
}
message += $"\nSent by {author}";
_chatManager.DispatchStationAnnouncement(message, "Communications Console");
break;
}
}
public void OpenUserInterface(IPlayerSession session)
{
UserInterface?.Open(session);
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
return;
/*
if (!Powered)
{
return;
}
*/
OpenUserInterface(actor.PlayerSession);
}
}
}

View File

@@ -1,88 +0,0 @@
using Content.Server.GameObjects.Components.Construction;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Shared.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
public sealed class ComputerComponent : SharedComputerComponent, IMapInit
{
[ViewVariables]
[DataField("board")]
private string? _boardPrototype;
public override void Initialize()
{
base.Initialize();
// Let's ensure the container manager and container are here.
Owner.EnsureContainer<Container>("board", out var _);
if (Owner.TryGetComponent(out PowerReceiverComponent? powerReceiver) &&
Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(ComputerVisuals.Powered, powerReceiver.Powered);
}
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
PowerReceiverOnOnPowerStateChanged(powerChanged);
break;
}
}
private void PowerReceiverOnOnPowerStateChanged(PowerChangedMessage e)
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(ComputerVisuals.Powered, e.Powered);
}
}
/// <summary>
/// Creates the corresponding computer board on the computer.
/// This exists so when you deconstruct computers that were serialized with the map,
/// you can retrieve the computer board.
/// </summary>
private void CreateComputerBoard()
{
// Ensure that the construction component is aware of the board container.
if (Owner.TryGetComponent(out ConstructionComponent? construction))
construction.AddContainer("board");
// We don't do anything if this is null or empty.
if (string.IsNullOrEmpty(_boardPrototype))
return;
var container = Owner.EnsureContainer<Container>("board", out var existed);
if (existed)
{
// We already contain a board. Note: We don't check if it's the right one!
if (container.ContainedEntities.Count != 0)
return;
}
var board = Owner.EntityManager.SpawnEntity(_boardPrototype, Owner.Transform.Coordinates);
if(!container.Insert(board))
Logger.Warning($"Couldn't insert board {board} to computer {Owner}!");
}
public void MapInit()
{
CreateComputerBoard();
}
}
}

View File

@@ -1,160 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
[ComponentReference(typeof(SharedConfigurationComponent))]
public class ConfigurationComponent : SharedConfigurationComponent, IInteractUsing, ISerializationHooks
{
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ConfigurationUiKey.Key);
[DataField("keys")] private List<string> _keys = new();
[ViewVariables]
private readonly Dictionary<string, string> _config = new();
[DataField("validation")]
private readonly Regex _validation = new ("^[a-zA-Z0-9 ]*$", RegexOptions.Compiled);
void ISerializationHooks.BeforeSerialization()
{
_keys = _config.Keys.ToList();
}
void ISerializationHooks.AfterDeserialization()
{
foreach (var key in _keys)
{
_config.Add(key, "");
}
}
public override void OnAdd()
{
base.OnAdd();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
}
}
public override void OnRemove()
{
base.OnRemove();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage -= UserInterfaceOnReceiveMessage;
}
}
public string? GetConfig(string name)
{
return _config.GetValueOrDefault(name);
}
protected override void Startup()
{
base.Startup();
UpdateUserInterface();
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (UserInterface == null || !eventArgs.User.TryGetComponent(out ActorComponent? actor))
return false;
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
return false;
if (!await tool.UseTool(eventArgs.User, Owner, 0.2f, ToolQuality.Multitool))
return false;
OpenUserInterface(actor);
return true;
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
var message = serverMsg.Message;
var config = new Dictionary<string, string>(_config);
if (message is ConfigurationUpdatedMessage msg)
{
foreach (var key in config.Keys)
{
var value = msg.Config.GetValueOrDefault(key);
if (value == null || _validation != null && !_validation.IsMatch(value) && value != "")
continue;
_config[key] = value;
}
SendMessage(new ConfigUpdatedComponentMessage(config));
}
}
private void UpdateUserInterface()
{
UserInterface?.SetState(new ConfigurationBoundUserInterfaceState(_config));
}
private void OpenUserInterface(ActorComponent actor)
{
UpdateUserInterface();
UserInterface?.Open(actor.PlayerSession);
UserInterface?.SendMessage(new ValidationUpdateMessage(_validation.ToString()), actor.PlayerSession);
}
private static void FillConfiguration<T>(List<string> list, Dictionary<string, T> configuration, T value){
for (var index = 0; index < list.Count; index++)
{
configuration.Add(list[index], value);
}
}
[Verb]
public sealed class ConfigureVerb : Verb<ConfigurationComponent>
{
protected override void GetData(IEntity user, ConfigurationComponent component, VerbData data)
{
var session = user.PlayerSession();
var groupController = IoCManager.Resolve<IConGroupController>();
if (session == null || !groupController.CanAdminMenu(session))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Open Configuration");
data.IconTexture = "/Textures/Interface/VerbIcons/settings.svg.192dpi.png";
}
protected override void Activate(IEntity user, ConfigurationComponent component)
{
if (user.TryGetComponent(out ActorComponent? actor))
{
component.OpenUserInterface(actor);
}
}
}
}
}

View File

@@ -1,16 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class ComputerBoardComponent : Component
{
public override string Name => "ComputerBoard";
[ViewVariables]
[DataField("prototype")]
public string? Prototype { get; private set; }
}
}

View File

@@ -1,49 +0,0 @@
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Construction
{
public partial class ConstructionComponent
{
[Verb]
public sealed class DeconstructibleVerb : Verb<ConstructionComponent>
{
protected override void GetData(IEntity user, ConstructionComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
if (((component.Target != null) && (component.Target.Name == component.DeconstructionNodeIdentifier)) ||
((component.Node != null) && (component.Node.Name == component.DeconstructionNodeIdentifier)))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.CategoryData = VerbCategories.Construction;
data.Text = Loc.GetString("Begin deconstructing");
data.IconTexture = "/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png";
}
protected override void Activate(IEntity user, ConstructionComponent component)
{
component.SetNewTarget(component.DeconstructionNodeIdentifier);
if (component.Target == null)
{
// Maybe check, but on the flip-side a better solution might be to not make it undeconstructible in the first place, no?
component.Owner.PopupMessage(user, Loc.GetString("There is no way to deconstruct this."));
}
else
{
component.Owner.PopupMessage(user, Loc.GetString("Examine to see instructions."));
}
}
}
}
}

View File

@@ -1,582 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.Construction;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public partial class ConstructionComponent : Component, IExamine, IInteractUsing
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Name => "Construction";
private bool _handling = false;
private TaskCompletionSource<object>? _handlingTask = null;
[DataField("graph")]
private string _graphIdentifier = string.Empty;
[DataField("node")]
private string _startingNodeIdentifier = string.Empty;
[DataField("defaultTarget")]
private string _startingTargetNodeIdentifier = string.Empty;
[ViewVariables]
private HashSet<string> _containers = new();
[ViewVariables]
private List<List<ConstructionGraphStep>>? _edgeNestedStepProgress = null;
private ConstructionGraphNode? _target = null;
[ViewVariables]
public ConstructionGraphPrototype? GraphPrototype { get; private set; }
[ViewVariables]
public ConstructionGraphNode? Node { get; private set; } = null;
[ViewVariables]
public ConstructionGraphEdge? Edge { get; private set; } = null;
public IReadOnlyCollection<string> Containers => _containers;
[ViewVariables]
int IInteractUsing.Priority => 2;
[ViewVariables]
public ConstructionGraphNode? Target
{
get => _target;
set
{
ClearTarget();
_target = value;
UpdateTarget();
}
}
[ViewVariables]
public ConstructionGraphEdge? TargetNextEdge { get; private set; } = null;
[ViewVariables]
public Queue<ConstructionGraphNode>? TargetPathfinding { get; private set; } = null;
[ViewVariables]
public int EdgeStep { get; private set; } = 0;
[ViewVariables]
[DataField("deconstructionTarget")]
public string DeconstructionNodeIdentifier { get; private set; } = "start";
/// <summary>
/// Attempts to set a new pathfinding target.
/// </summary>
public void SetNewTarget(string node)
{
if (GraphPrototype != null && GraphPrototype.Nodes.TryGetValue(node, out var target))
{
Target = target;
}
}
public void ClearTarget()
{
_target = null;
TargetNextEdge = null;
TargetPathfinding = null;
}
public void UpdateTarget()
{
// Can't pathfind without a target or no node.
if (Target == null || Node == null || GraphPrototype == null) return;
// If we're at our target, stop pathfinding.
if (Target == Node)
{
ClearTarget();
return;
}
// If we don't have the path, set it!
if (TargetPathfinding == null)
{
var path = GraphPrototype.Path(Node.Name, Target.Name);
if (path == null)
{
ClearTarget();
return;
}
TargetPathfinding = new Queue<ConstructionGraphNode>(path);
}
// Dequeue the pathfinding queue if the next is the node we're at.
if (TargetPathfinding.Peek() == Node)
TargetPathfinding.Dequeue();
// If we went the wrong way, we stop pathfinding.
if (Edge != null && TargetNextEdge != Edge)
{
ClearTarget();
return;
}
// Let's set the next target edge.
if (Edge == null && TargetNextEdge == null && TargetPathfinding != null)
TargetNextEdge = Node.GetEdge(TargetPathfinding.Peek().Name);
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (_handling)
return true;
_handlingTask = new TaskCompletionSource<object>();
_handling = true;
bool result;
if (Edge == null)
result = await HandleNode(eventArgs);
else
result = await HandleEdge(eventArgs);
_handling = false;
_handlingTask.SetResult(null!);
return result;
}
private async Task<bool> HandleNode(InteractUsingEventArgs eventArgs)
{
EdgeStep = 0;
if (Node == null || GraphPrototype == null) return false;
foreach (var edge in Node.Edges)
{
if(edge.Steps.Count == 0)
throw new InvalidDataException($"Edge to \"{edge.Target}\" in node \"{Node.Name}\" of graph \"{GraphPrototype.ID}\" doesn't have any steps!");
var firstStep = edge.Steps[0];
switch (firstStep)
{
case MaterialConstructionGraphStep _:
case ToolConstructionGraphStep _:
case ArbitraryInsertConstructionGraphStep _:
if (await HandleStep(eventArgs, edge, firstStep))
{
if(edge.Steps.Count > 1)
Edge = edge;
return true;
}
break;
case NestedConstructionGraphStep nestedStep:
throw new IndexOutOfRangeException($"Nested construction step not supported as the first step in an edge! Graph: {GraphPrototype.ID} Node: {Node.Name} Edge: {edge.Target}");
}
}
return false;
}
private async Task<bool> HandleStep(InteractUsingEventArgs eventArgs, ConstructionGraphEdge? edge = null, ConstructionGraphStep? step = null, bool nested = false)
{
edge ??= Edge;
step ??= edge?.Steps[EdgeStep];
if (edge == null || step == null)
return false;
foreach (var condition in edge.Conditions)
{
if (!await condition.Condition(Owner)) return false;
}
var handled = false;
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, step.DoAfter, default, eventArgs.Target)
{
BreakOnDamage = false,
BreakOnStun = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = true,
};
switch (step)
{
case ToolConstructionGraphStep toolStep:
// Gotta take welder fuel into consideration.
if (toolStep.Tool == ToolQuality.Welding)
{
if (eventArgs.Using.TryGetComponent(out WelderComponent? welder) &&
await welder.UseTool(eventArgs.User, Owner, step.DoAfter, toolStep.Tool, toolStep.Fuel))
{
handled = true;
}
break;
}
if (eventArgs.Using.TryGetComponent(out ToolComponent? tool) &&
await tool.UseTool(eventArgs.User, Owner, step.DoAfter, toolStep.Tool))
{
handled = true;
}
break;
// To prevent too much code duplication.
case EntityInsertConstructionGraphStep insertStep:
var valid = false;
var entityUsing = eventArgs.Using;
switch (insertStep)
{
case ArbitraryInsertConstructionGraphStep arbitraryStep:
if (arbitraryStep.EntityValid(eventArgs.Using)
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
valid = true;
}
break;
case MaterialConstructionGraphStep materialStep:
if (materialStep.EntityValid(eventArgs.Using, out var stack)
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
var splitStack = new StackSplitEvent() {Amount = materialStep.Amount, SpawnPosition = eventArgs.User.Transform.Coordinates};
Owner.EntityManager.EventBus.RaiseLocalEvent(stack.Owner.Uid, splitStack);
if (splitStack.Result != null)
{
entityUsing = splitStack.Result;
valid = true;
}
}
break;
}
if (!valid || entityUsing == null) break;
if(string.IsNullOrEmpty(insertStep.Store))
{
entityUsing.Delete();
}
else
{
_containers.Add(insertStep.Store);
var container = Owner.EnsureContainer<Container>(insertStep.Store);
container.Insert(entityUsing);
}
handled = true;
break;
case NestedConstructionGraphStep nestedStep:
if(_edgeNestedStepProgress == null)
_edgeNestedStepProgress = new List<List<ConstructionGraphStep>>(nestedStep.Steps);
foreach (var list in _edgeNestedStepProgress.ToArray())
{
if (list.Count == 0)
{
_edgeNestedStepProgress.Remove(list);
continue;
}
if (!await HandleStep(eventArgs, edge, list[0], true)) continue;
list.RemoveAt(0);
// We check again...
if (list.Count == 0)
_edgeNestedStepProgress.Remove(list);
}
if (_edgeNestedStepProgress.Count == 0)
handled = true;
break;
}
if (handled)
{
foreach (var completed in step.Completed)
{
await completed.PerformAction(Owner, eventArgs.User);
if (Owner.Deleted)
return false;
}
}
if (nested && handled) return true;
if (!handled) return false;
EdgeStep++;
if (edge.Steps.Count == EdgeStep)
{
await HandleCompletion(edge, eventArgs.User);
}
UpdateTarget();
return true;
}
private async Task<bool> HandleCompletion(ConstructionGraphEdge edge, IEntity user)
{
if (edge.Steps.Count != EdgeStep || GraphPrototype == null)
{
return false;
}
Edge = edge;
UpdateTarget();
TargetNextEdge = null;
Edge = null;
Node = GraphPrototype.Nodes[edge.Target];
foreach (var completed in edge.Completed)
{
await completed.PerformAction(Owner, user);
if (Owner.Deleted) return true;
}
// Perform node actions!
foreach (var action in Node.Actions)
{
await action.PerformAction(Owner, user);
if (Owner.Deleted)
return false;
}
if (Target == Node)
ClearTarget();
await HandleEntityChange(Node, user);
return true;
}
public void ResetEdge()
{
_edgeNestedStepProgress = null;
TargetNextEdge = null;
Edge = null;
EdgeStep = 0;
UpdateTarget();
}
private async Task<bool> HandleEdge(InteractUsingEventArgs eventArgs)
{
if (Edge == null || EdgeStep >= Edge.Steps.Count) return false;
return await HandleStep(eventArgs, Edge, Edge.Steps[EdgeStep]);
}
private async Task<bool> HandleEntityChange(ConstructionGraphNode node, IEntity? user = null)
{
if (node.Entity == Owner.Prototype?.ID || string.IsNullOrEmpty(node.Entity)
|| GraphPrototype == null) return false;
var entity = Owner.EntityManager.SpawnEntity(node.Entity, Owner.Transform.Coordinates);
entity.Transform.LocalRotation = Owner.Transform.LocalRotation;
if (entity.TryGetComponent(out ConstructionComponent? construction))
{
if(construction.GraphPrototype != GraphPrototype)
throw new Exception($"New entity {node.Entity}'s graph {construction.GraphPrototype?.ID ?? null} isn't the same as our graph {GraphPrototype.ID} on node {node.Name}!");
construction.Node = node;
construction.Target = Target;
construction._containers = new HashSet<string>(_containers);
}
if (Owner.TryGetComponent(out ContainerManagerComponent? containerComp))
{
foreach (var container in _containers)
{
var otherContainer = entity.EnsureContainer<Container>(container);
var ourContainer = containerComp.GetContainer(container);
foreach (var ent in ourContainer.ContainedEntities.ToArray())
{
ourContainer.ForceRemove(ent);
otherContainer.Insert(ent);
}
}
}
if (Owner.TryGetComponent(out IPhysBody? physics) &&
entity.TryGetComponent(out IPhysBody? otherPhysics))
{
otherPhysics.BodyType = physics.BodyType;
}
Owner.Delete();
foreach (var action in node.Actions)
{
await action.PerformAction(entity, user);
if (entity.Deleted)
return false;
}
return true;
}
public bool AddContainer(string id)
{
return _containers.Add(id);
}
public override void Initialize()
{
base.Initialize();
if (string.IsNullOrEmpty(_graphIdentifier))
{
Logger.Warning($"Prototype {Owner.Prototype?.ID}'s construction component didn't have a graph identifier!");
return;
}
if (_prototypeManager.TryIndex(_graphIdentifier, out ConstructionGraphPrototype? graph))
{
GraphPrototype = graph;
if (GraphPrototype.Nodes.TryGetValue(_startingNodeIdentifier, out var node))
{
Node = node;
}
else
{
Logger.Error($"Couldn't find node {_startingNodeIdentifier} in graph {_graphIdentifier} in construction component!");
}
}
else
{
Logger.Error($"Couldn't find prototype {_graphIdentifier} in construction component!");
}
if (!string.IsNullOrEmpty(_startingTargetNodeIdentifier))
SetNewTarget(_startingTargetNodeIdentifier);
}
protected override void Startup()
{
base.Startup();
if (Node == null) return;
foreach (var action in Node.Actions)
{
action.PerformAction(Owner, null);
if (Owner.Deleted)
return;
}
}
public async Task ChangeNode(string node)
{
if (GraphPrototype == null) return;
var graphNode = GraphPrototype.Nodes[node];
if (_handling && _handlingTask?.Task != null)
await _handlingTask.Task;
Edge = null;
Node = graphNode;
foreach (var action in Node.Actions)
{
await action.PerformAction(Owner, null);
if (Owner.Deleted)
return;
}
await HandleEntityChange(graphNode);
}
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
if(Target != null)
message.AddMarkup(Loc.GetString("To create {0}...\n", Target.Name));
if (Edge == null && TargetNextEdge != null)
{
var preventStepExamine = false;
foreach (var condition in TargetNextEdge.Conditions)
{
preventStepExamine |= condition.DoExamine(Owner, message, inDetailsRange);
}
if(!preventStepExamine)
TargetNextEdge.Steps[0].DoExamine(message, inDetailsRange);
return;
}
if (Edge != null)
{
var preventStepExamine = false;
foreach (var condition in Edge.Conditions)
{
preventStepExamine |= condition.DoExamine(Owner, message, inDetailsRange);
}
if (preventStepExamine) return;
}
if (_edgeNestedStepProgress == null)
{
if(EdgeStep < Edge?.Steps.Count)
Edge.Steps[EdgeStep].DoExamine(message, inDetailsRange);
return;
}
foreach (var list in _edgeNestedStepProgress)
{
if(list.Count == 0) continue;
list[0].DoExamine(message, inDetailsRange);
}
}
}
}

View File

@@ -1,91 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.Construction;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Stacks;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachineBoardComponent : Component, IExamine
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Name => "MachineBoard";
[ViewVariables]
[DataField("requirements")]
public readonly Dictionary<MachinePart, int> Requirements = new();
[ViewVariables]
[DataField("materialRequirements")]
public readonly Dictionary<string, int> MaterialIdRequirements = new();
[ViewVariables]
[DataField("tagRequirements")]
public readonly Dictionary<string, GenericPartInfo> TagRequirements = new();
[ViewVariables]
[DataField("componentRequirements")]
public readonly Dictionary<string, GenericPartInfo> ComponentRequirements = new();
[ViewVariables(VVAccess.ReadWrite)]
[DataField("prototype")]
public string? Prototype { get; private set; }
public IEnumerable<KeyValuePair<StackPrototype, int>> MaterialRequirements
{
get
{
foreach (var (materialId, amount) in MaterialIdRequirements)
{
var material = _prototypeManager.Index<StackPrototype>(materialId);
yield return new KeyValuePair<StackPrototype, int>(material, amount);
}
}
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("Requires:\n"));
foreach (var (part, amount) in Requirements)
{
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", amount, Loc.GetString(part.ToString())));
}
foreach (var (material, amount) in MaterialRequirements)
{
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", amount, Loc.GetString(material.Name)));
}
foreach (var (_, info) in ComponentRequirements)
{
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", info.Amount, Loc.GetString(info.ExamineName)));
}
foreach (var (_, info) in TagRequirements)
{
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", info.Amount, Loc.GetString(info.ExamineName)));
}
}
}
[Serializable]
[DataDefinition]
public struct GenericPartInfo
{
[DataField("Amount")]
public int Amount;
[DataField("ExamineName")]
public string ExamineName;
[DataField("DefaultPrototype")]
public string DefaultPrototype;
}
}

View File

@@ -1,132 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.Construction;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachineComponent : Component, IMapInit
{
public override string Name => "Machine";
[DataField("board")]
public string? BoardPrototype { get; private set; }
private Container _boardContainer = default!;
private Container _partContainer = default!;
public override void Initialize()
{
base.Initialize();
_boardContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.BoardContainer);
_partContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.PartContainer);
}
public IEnumerable<MachinePartComponent> GetAllParts()
{
foreach (var entity in _partContainer.ContainedEntities)
{
if (entity.TryGetComponent<MachinePartComponent>(out var machinePart))
yield return machinePart;
}
}
public void RefreshParts()
{
foreach (var refreshable in Owner.GetAllComponents<IRefreshParts>())
{
refreshable.RefreshParts(GetAllParts());
}
}
public void CreateBoardAndStockParts()
{
// Entity might not be initialized yet.
var boardContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.BoardContainer, out var existedBoard);
var partContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.PartContainer, out var existedParts);
if (string.IsNullOrEmpty(BoardPrototype))
return;
var entityManager = Owner.EntityManager;
if (existedBoard || existedParts)
{
// We're done here, let's suppose all containers are correct just so we don't screw SaveLoadSave.
if (boardContainer.ContainedEntities.Count > 0)
return;
}
var board = entityManager.SpawnEntity(BoardPrototype, Owner.Transform.Coordinates);
if (!_boardContainer.Insert(board))
{
throw new Exception($"Couldn't insert board with prototype {BoardPrototype} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}!");
}
if (!board.TryGetComponent<MachineBoardComponent>(out var machineBoard))
{
throw new Exception($"Entity with prototype {BoardPrototype} doesn't have a {nameof(MachineBoardComponent)}!");
}
foreach (var (part, amount) in machineBoard.Requirements)
{
for (var i = 0; i < amount; i++)
{
var p = entityManager.SpawnEntity(MachinePartComponent.Prototypes[part], Owner.Transform.Coordinates);
if (!partContainer.Insert(p))
throw new Exception($"Couldn't insert machine part of type {part} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}!");
}
}
foreach (var (stackType, amount) in machineBoard.MaterialRequirements)
{
var stackSpawn = new StackTypeSpawnEvent()
{Amount = amount, StackType = stackType, SpawnPosition = Owner.Transform.Coordinates};
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, stackSpawn);
var s = stackSpawn.Result;
if (s == null)
throw new Exception($"Couldn't spawn stack of type {stackType}!");
if (!partContainer.Insert(s))
throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
}
foreach (var (compName, info) in machineBoard.ComponentRequirements)
{
for (var i = 0; i < info.Amount; i++)
{
var c = entityManager.SpawnEntity(info.DefaultPrototype, Owner.Transform.Coordinates);
if(!partContainer.Insert(c))
throw new Exception($"Couldn't insert machine component part with default prototype '{compName}' to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
}
}
foreach (var (tagName, info) in machineBoard.TagRequirements)
{
for (var i = 0; i < info.Amount; i++)
{
var c = entityManager.SpawnEntity(info.DefaultPrototype, Owner.Transform.Coordinates);
if(!partContainer.Insert(c))
throw new Exception($"Couldn't insert machine component part with default prototype '{tagName}' to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
}
}
}
public void MapInit()
{
CreateBoardAndStockParts();
}
}
}

View File

@@ -1,364 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Construction;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Construction;
using Content.Shared.GameObjects.Components.Tag;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachineFrameComponent : Component, IInteractUsing
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
public const string PartContainer = "machine_parts";
public const string BoardContainer = "machine_board";
public override string Name => "MachineFrame";
[ViewVariables]
public bool IsComplete
{
get
{
if (!HasBoard || Requirements == null || MaterialRequirements == null)
return false;
foreach (var (part, amount) in Requirements)
{
if (_progress[part] < amount)
return false;
}
foreach (var (type, amount) in MaterialRequirements)
{
if (_materialProgress[type] < amount)
return false;
}
foreach (var (compName, info) in ComponentRequirements)
{
if (_componentProgress[compName] < info.Amount)
return false;
}
foreach (var (tagName, info) in TagRequirements)
{
if (_tagProgress[tagName] < info.Amount)
return false;
}
return true;
}
}
[ViewVariables]
public bool HasBoard => _boardContainer?.ContainedEntities.Count != 0;
[ViewVariables]
private readonly Dictionary<MachinePart, int> _progress = new();
[ViewVariables]
private readonly Dictionary<string, int> _materialProgress = new();
[ViewVariables]
private readonly Dictionary<string, int> _componentProgress = new();
[ViewVariables]
private readonly Dictionary<string, int> _tagProgress = new();
[ViewVariables]
private Dictionary<MachinePart, int> _requirements = new();
[ViewVariables]
private Dictionary<string, int> _materialRequirements = new();
[ViewVariables]
private Dictionary<string, GenericPartInfo> _componentRequirements = new();
[ViewVariables]
private Dictionary<string, GenericPartInfo> _tagRequirements = new();
[ViewVariables]
private Container _boardContainer = default!;
[ViewVariables]
private Container _partContainer = default!;
public IReadOnlyDictionary<MachinePart, int> Progress => _progress;
public IReadOnlyDictionary<string, int> MaterialProgress => _materialProgress;
public IReadOnlyDictionary<string, int> ComponentProgress => _componentProgress;
public IReadOnlyDictionary<string, int> TagProgress => _tagProgress;
public IReadOnlyDictionary<MachinePart, int> Requirements => _requirements;
public IReadOnlyDictionary<string, int> MaterialRequirements => _materialRequirements;
public IReadOnlyDictionary<string, GenericPartInfo> ComponentRequirements => _componentRequirements;
public IReadOnlyDictionary<string, GenericPartInfo> TagRequirements => _tagRequirements;
public override void Initialize()
{
base.Initialize();
_boardContainer = ContainerHelpers.EnsureContainer<Container>(Owner, BoardContainer);
_partContainer = ContainerHelpers.EnsureContainer<Container>(Owner, PartContainer);
}
protected override void Startup()
{
base.Startup();
RegenerateProgress();
if (Owner.TryGetComponent<ConstructionComponent>(out var construction))
{
// Attempt to set pathfinding to the machine node...
construction.SetNewTarget("machine");
}
}
private void ResetProgressAndRequirements(MachineBoardComponent machineBoard)
{
_requirements = machineBoard.Requirements;
_materialRequirements = machineBoard.MaterialIdRequirements;
_componentRequirements = machineBoard.ComponentRequirements;
_tagRequirements = machineBoard.TagRequirements;
_progress.Clear();
_materialProgress.Clear();
_componentProgress.Clear();
_tagProgress.Clear();
foreach (var (machinePart, _) in Requirements)
{
_progress[machinePart] = 0;
}
foreach (var (stackType, _) in MaterialRequirements)
{
_materialProgress[stackType] = 0;
}
foreach (var (compName, _) in ComponentRequirements)
{
_componentProgress[compName] = 0;
}
foreach (var (compName, _) in TagRequirements)
{
_tagProgress[compName] = 0;
}
}
public void RegenerateProgress()
{
AppearanceComponent? appearance;
if (!HasBoard)
{
if (Owner.TryGetComponent(out appearance))
{
appearance.SetData(MachineFrameVisuals.State, 1);
}
_requirements.Clear();
_materialRequirements.Clear();
_componentRequirements.Clear();
_tagRequirements.Clear();
_progress.Clear();
_materialProgress.Clear();
_componentProgress.Clear();
_tagProgress.Clear();
return;
}
var board = _boardContainer.ContainedEntities[0];
if (!board.TryGetComponent<MachineBoardComponent>(out var machineBoard))
return;
if (Owner.TryGetComponent(out appearance))
{
appearance.SetData(MachineFrameVisuals.State, 2);
}
ResetProgressAndRequirements(machineBoard);
foreach (var part in _partContainer.ContainedEntities)
{
if (part.TryGetComponent<MachinePartComponent>(out var machinePart))
{
// Check this is part of the requirements...
if (!Requirements.ContainsKey(machinePart.PartType))
continue;
if (!_progress.ContainsKey(machinePart.PartType))
_progress[machinePart.PartType] = 1;
else
_progress[machinePart.PartType]++;
}
if (part.TryGetComponent<StackComponent>(out var stack))
{
var type = stack.StackTypeId;
// Check this is part of the requirements...
if (!MaterialRequirements.ContainsKey(type))
continue;
if (!_materialProgress.ContainsKey(type))
_materialProgress[type] = 1;
else
_materialProgress[type]++;
}
// I have many regrets.
foreach (var (compName, _) in ComponentRequirements)
{
var registration = _componentFactory.GetRegistration(compName);
if (!part.HasComponent(registration.Type))
continue;
if (!_componentProgress.ContainsKey(compName))
_componentProgress[compName] = 1;
else
_componentProgress[compName]++;
}
// I have MANY regrets.
foreach (var (tagName, _) in TagRequirements)
{
if (!part.HasTag(tagName))
continue;
if (!_tagProgress.ContainsKey(tagName))
_tagProgress[tagName] = 1;
else
_tagProgress[tagName]++;
}
}
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!HasBoard && eventArgs.Using.TryGetComponent<MachineBoardComponent>(out var machineBoard))
{
if (eventArgs.Using.TryRemoveFromContainer())
{
// Valid board!
_boardContainer.Insert(eventArgs.Using);
// Setup requirements and progress...
ResetProgressAndRequirements(machineBoard);
if (Owner.TryGetComponent<AppearanceComponent>(out var appearance))
{
appearance.SetData(MachineFrameVisuals.State, 2);
}
if (Owner.TryGetComponent(out ConstructionComponent? construction))
{
// So prying the components off works correctly.
construction.ResetEdge();
}
return true;
}
}
else if (HasBoard)
{
if (eventArgs.Using.TryGetComponent<MachinePartComponent>(out var machinePart))
{
if (!Requirements.ContainsKey(machinePart.PartType))
return false;
if (_progress[machinePart.PartType] != Requirements[machinePart.PartType]
&& eventArgs.Using.TryRemoveFromContainer() && _partContainer.Insert(eventArgs.Using))
{
_progress[machinePart.PartType]++;
return true;
}
}
if (eventArgs.Using.TryGetComponent<StackComponent>(out var stack))
{
var type = stack.StackTypeId;
if (!MaterialRequirements.ContainsKey(type))
return false;
if (_materialProgress[type] == MaterialRequirements[type])
return false;
var needed = MaterialRequirements[type] - _materialProgress[type];
var count = stack.Count;
if (count < needed)
{
if(!_partContainer.Insert(stack.Owner))
return false;
_materialProgress[type] += count;
return true;
}
var splitStack = new StackSplitEvent()
{Amount = needed, SpawnPosition = Owner.Transform.Coordinates};
Owner.EntityManager.EventBus.RaiseLocalEvent(stack.Owner.Uid, splitStack);
if (splitStack.Result == null)
return false;
if(!_partContainer.Insert(splitStack.Result))
return false;
_materialProgress[type] += needed;
return true;
}
foreach (var (compName, info) in ComponentRequirements)
{
if (_componentProgress[compName] >= info.Amount)
continue;
var registration = _componentFactory.GetRegistration(compName);
if (!eventArgs.Using.HasComponent(registration.Type))
continue;
if (!eventArgs.Using.TryRemoveFromContainer() || !_partContainer.Insert(eventArgs.Using)) continue;
_componentProgress[compName]++;
return true;
}
foreach (var (tagName, info) in TagRequirements)
{
if (_tagProgress[tagName] >= info.Amount)
continue;
if (!eventArgs.Using.HasTag(tagName))
continue;
if (!eventArgs.Using.TryRemoveFromContainer() || !_partContainer.Insert(eventArgs.Using)) continue;
_tagProgress[tagName]++;
return true;
}
}
return false;
}
}
}

View File

@@ -1,48 +0,0 @@
using System.Collections.Generic;
using Content.Server.Construction;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachinePartComponent : Component, IExamine
{
// I'm so sorry for hard-coding this. But trust me, it should make things less painful.
public static IReadOnlyDictionary<MachinePart, string> Prototypes { get; } = new Dictionary<MachinePart, string>()
{
{MachinePart.Capacitor, "CapacitorStockPart"},
{MachinePart.ScanningModule, "ScanningModuleStockPart"},
{MachinePart.Manipulator, "MicroManipulatorStockPart"},
{MachinePart.Laser, "MicroLaserStockPart"},
{MachinePart.MatterBin, "MatterBinStockPart"},
{MachinePart.Ansible, "AnsibleSubspaceStockPart"},
{MachinePart.Filter, "FilterSubspaceStockPart"},
{MachinePart.Amplifier, "AmplifierSubspaceStockPart"},
{MachinePart.Treatment, "TreatmentSubspaceStockPart"},
{MachinePart.Analyzer, "AnalyzerSubspaceStockPart"},
{MachinePart.Crystal, "CrystalSubspaceStockPart"},
{MachinePart.Transmitter, "TransmitterSubspaceStockPart"}
};
public override string Name => "MachinePart";
[ViewVariables] [DataField("part")] public MachinePart PartType { get; private set; } = MachinePart.Capacitor;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("rating")]
public int Rating { get; private set; } = 1;
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("[color=white]Rating:[/color] [color=cyan]{0}[/color]\n", Rating));
message.AddMarkup(Loc.GetString("[color=white]Type:[/color] [color=cyan]{0}[/color]\n", PartType));
}
}
}

View File

@@ -1,71 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Stack;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
/// <summary>
/// Used for something that can be refined by welder.
/// For example, glass shard can be refined to glass sheet.
/// </summary>
[RegisterComponent]
public class WelderRefinableComponent : Component, IInteractUsing
{
[ViewVariables]
[DataField("refineResult")]
private HashSet<string>? _refineResult = new() { };
[ViewVariables]
[DataField("refineTime")]
private float _refineTime = 2f;
private bool _beingWelded;
public override string Name => "WelderRefinable";
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
// check if object is welder
if (!eventArgs.Using.TryGetComponent(out ToolComponent? tool))
return false;
// check if someone is already welding object
if (_beingWelded)
return false;
_beingWelded = true;
if (!await tool.UseTool(eventArgs.User, Owner, _refineTime, ToolQuality.Welding))
{
// failed to veld - abort refine
_beingWelded = false;
return false;
}
// get last owner coordinates and delete it
var resultPosition = Owner.Transform.Coordinates;
Owner.Delete();
// spawn each result afrer refine
foreach (var result in _refineResult!)
{
var droppedEnt = Owner.EntityManager.SpawnEntity(result, resultPosition);
// TODO: If something has a stack... Just use a prototype with a single thing in the stack.
// This is not a good way to do it.
if (droppedEnt.HasComponent<StackComponent>())
Owner.EntityManager.EventBus.RaiseLocalEvent(droppedEnt.Uid, new StackChangeCountEvent(1), false);
}
return true;
}
}
}

View File

@@ -1,23 +0,0 @@
using Robust.Shared.Containers;
namespace Content.Server.GameObjects
{
public static class ContainerExt
{
public static int CountPrototypeOccurencesRecursive(this ContainerManagerComponent mgr, string prototypeId)
{
int total = 0;
foreach (var container in mgr.GetAllContainers())
{
foreach (var entity in container.ContainedEntities)
{
if (entity.Prototype?.ID == prototypeId) total++;
if(!entity.TryGetComponent<ContainerManagerComponent>(out var component)) continue;
total += component.CountPrototypeOccurencesRecursive(prototypeId);
}
}
return total;
}
}
}

View File

@@ -1,174 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.MachineLinking;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Shared.GameObjects.Components.Conveyor;
using Content.Shared.GameObjects.Components.MachineLinking;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.Physics;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Physics;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Conveyor
{
[RegisterComponent]
public class ConveyorComponent : Component, ISignalReceiver<TwoWayLeverSignal>, ISignalReceiver<bool>
{
public override string Name => "Conveyor";
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
/// <summary>
/// The angle to move entities by in relation to the owner's rotation.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("angle")]
private Angle _angle;
public float Speed => _speed;
/// <summary>
/// The amount of units to move the entity by per second.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("speed")]
private float _speed = 2f;
private ConveyorState _state;
/// <summary>
/// The current state of this conveyor
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
private ConveyorState State
{
get => _state;
set
{
_state = value;
UpdateAppearance();
}
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PowerChangedMessage powerChanged:
OnPowerChanged(powerChanged);
break;
}
}
private void OnPowerChanged(PowerChangedMessage e)
{
UpdateAppearance();
}
private void UpdateAppearance()
{
if (Owner.TryGetComponent<AppearanceComponent>(out var appearance))
{
if (Powered)
{
appearance.SetData(ConveyorVisuals.State, _state);
}
else
{
appearance.SetData(ConveyorVisuals.State, ConveyorState.Off);
}
}
}
/// <summary>
/// Calculates the angle in which entities on top of this conveyor
/// belt are pushed in
/// </summary>
/// <returns>
/// The angle when taking into account if the conveyor is reversed
/// </returns>
public Angle GetAngle()
{
var adjustment = _state == ConveyorState.Reversed ? MathHelper.Pi : 0;
var radians = MathHelper.DegreesToRadians(_angle);
return new Angle(Owner.Transform.LocalRotation.Theta + radians + adjustment);
}
public bool CanRun()
{
if (State == ConveyorState.Off)
{
return false;
}
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
!receiver.Powered)
{
return false;
}
if (Owner.HasComponent<ItemComponent>())
{
return false;
}
return true;
}
public bool CanMove(IEntity entity)
{
// TODO We should only check status InAir or Static or MapGrid or /mayber/ container
if (entity == Owner)
{
return false;
}
if (!entity.TryGetComponent(out IPhysBody? physics) ||
physics.BodyType == BodyType.Static)
{
return false;
}
if (entity.HasComponent<ConveyorComponent>())
{
return false;
}
if (entity.HasComponent<IMapGridComponent>())
{
return false;
}
if (entity.IsInContainer())
{
return false;
}
return true;
}
public void TriggerSignal(TwoWayLeverSignal signal)
{
State = signal switch
{
TwoWayLeverSignal.Left => ConveyorState.Reversed,
TwoWayLeverSignal.Middle => ConveyorState.Off,
TwoWayLeverSignal.Right => ConveyorState.Forward,
_ => ConveyorState.Off
};
}
public void TriggerSignal(bool signal)
{
State = signal ? ConveyorState.Forward : ConveyorState.Off;
}
}
}

View File

@@ -1,153 +0,0 @@
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Utility;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
public class CrayonComponent : SharedCrayonComponent, IAfterInteract, IUse, IDropped, ISerializationHooks
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
//TODO: useSound
[DataField("useSound")]
private string? _useSound = string.Empty;
[ViewVariables]
public Color Color { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public int Charges { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
[DataField("capacity")]
public int Capacity { get; set; } = 30;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CrayonUiKey.Key);
void ISerializationHooks.AfterDeserialization()
{
Color = Color.FromName(_color);
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
}
Charges = Capacity;
// Get the first one from the catalog and set it as default
var decals = _prototypeManager.EnumeratePrototypes<CrayonDecalPrototype>().FirstOrDefault();
if (decals != null)
{
SelectedState = decals.Decals.First();
}
Dirty();
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
switch (serverMsg.Message)
{
case CrayonSelectMessage msg:
// Check if the selected state is valid
var crayonDecals = _prototypeManager.EnumeratePrototypes<CrayonDecalPrototype>().FirstOrDefault();
if (crayonDecals != null)
{
if (crayonDecals.Decals.Contains(msg.State))
{
SelectedState = msg.State;
Dirty();
}
}
break;
default:
break;
}
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new CrayonComponentState(_color, SelectedState, Charges, Capacity);
}
// Opens the selection window
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
UserInterface?.Toggle(actor.PlayerSession);
if (UserInterface?.SessionHasOpen(actor.PlayerSession) == true)
{
// Tell the user interface the selected stuff
UserInterface.SetState(
new CrayonBoundUserInterfaceState(SelectedState, Color));
}
return true;
}
return false;
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: false, popup: true,
collisionMask: Shared.Physics.CollisionGroup.MobImpassable))
{
return true;
}
if (Charges <= 0)
{
eventArgs.User.PopupMessage(Loc.GetString("Not enough left."));
return true;
}
var entityManager = IoCManager.Resolve<IServerEntityManager>();
var entity = entityManager.SpawnEntity("CrayonDecal", eventArgs.ClickLocation);
if (entity.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(CrayonVisuals.State, SelectedState);
appearance.SetData(CrayonVisuals.Color, _color);
appearance.SetData(CrayonVisuals.Rotation, eventArgs.User.Transform.LocalRotation);
}
if (!string.IsNullOrEmpty(_useSound))
{
SoundSystem.Play(Filter.Pvs(Owner), _useSound, Owner, AudioHelpers.WithVariation(0.125f));
}
// Decrease "Ammo"
Charges--;
Dirty();
return true;
}
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out ActorComponent? actor))
UserInterface?.Close(actor.PlayerSession);
}
}
}

View File

@@ -1,89 +0,0 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Nutrition;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Culinary
{
[RegisterComponent]
class SliceableFoodComponent : Component, IInteractUsing, IExamine
{
public override string Name => "SliceableFood";
int IInteractUsing.Priority => 1; // take priority over eating with utensils
[DataField("slice")] [ViewVariables(VVAccess.ReadWrite)]
private string _slice = string.Empty;
[DataField("sound")] [ViewVariables(VVAccess.ReadWrite)]
private string _sound = "/Audio/Items/Culinary/chop.ogg";
[DataField("count")] [ViewVariables(VVAccess.ReadWrite)]
private ushort _totalCount = 5;
[ViewVariables(VVAccess.ReadWrite)] public ushort Count;
public override void Initialize()
{
base.Initialize();
Count = _totalCount;
Owner.EnsureComponent<FoodComponent>();
Owner.EnsureComponent<SolutionContainerComponent>();
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (string.IsNullOrEmpty(_slice))
{
return false;
}
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
return false;
}
if (!eventArgs.Using.TryGetComponent(out UtensilComponent? utensil) || !utensil.HasType(UtensilType.Knife))
{
return false;
}
var itemToSpawn = Owner.EntityManager.SpawnEntity(_slice, Owner.Transform.Coordinates);
if (eventArgs.User.TryGetComponent(out HandsComponent? handsComponent))
{
if (ContainerHelpers.IsInContainer(Owner))
{
handsComponent.PutInHandOrDrop(itemToSpawn.GetComponent<ItemComponent>());
}
}
SoundSystem.Play(Filter.Pvs(Owner), _sound, Owner.Transform.Coordinates,
AudioParams.Default.WithVolume(-2));
Count--;
if (Count < 1)
{
Owner.Delete();
return true;
}
solution.TryRemoveReagent("Nutriment", solution.CurrentVolume / ReagentUnit.New(Count + 1));
return true;
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString($"There are { Count } slices remaining."));
}
}
}

View File

@@ -1,112 +0,0 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Nutrition;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Culinary
{
[RegisterComponent]
public class UtensilComponent : Component, IAfterInteract
{
public override string Name => "Utensil";
[DataField("types")]
private UtensilType _types = UtensilType.None;
[ViewVariables]
public UtensilType Types
{
get => _types;
set
{
if (_types.Equals(value))
return;
_types = value;
}
}
/// <summary>
/// The chance that the utensil has to break with each use.
/// A value of 0 means that it is unbreakable.
/// </summary>
[ViewVariables]
[DataField("breakChance")]
private float _breakChance;
/// <summary>
/// The sound to be played if the utensil breaks.
/// </summary>
[ViewVariables]
[DataField("breakSound")]
private string? _breakSound = "/Audio/Items/snap.ogg";
public void AddType(UtensilType type)
{
Types |= type;
}
public bool HasAnyType(UtensilType type)
{
return (_types & type) != UtensilType.None;
}
public bool HasType(UtensilType type)
{
return (_types & type) != 0;
}
public void RemoveType(UtensilType type)
{
Types &= ~type;
}
internal void TryBreak(IEntity user)
{
if (_breakSound != null && IoCManager.Resolve<IRobustRandom>().Prob(_breakChance))
{
SoundSystem.Play(Filter.Pvs(user), _breakSound, user, AudioParams.Default.WithVolume(-2f));
Owner.Delete();
}
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
return TryUseUtensil(eventArgs.User, eventArgs.Target);
}
private bool TryUseUtensil(IEntity user, IEntity? target)
{
if (target == null || !target.TryGetComponent(out FoodComponent? food))
{
return false;
}
if (!user.InRangeUnobstructed(target, popup: true))
{
return false;
}
food.TryUseFood(user, null, this);
return true;
}
}
[Flags]
public enum UtensilType : byte
{
None = 0,
Fork = 1,
Spoon = 1 << 1,
Knife = 1 << 2
}
}

Some files were not shown because too many files have changed in this diff Show More