Merge remote-tracking branch 'upstream/master' into upstream-sync

This commit is contained in:
Arthur 2022-02-07 01:38:12 +03:00
commit 85694b8513
104 changed files with 63937 additions and 48011 deletions

View file

@ -1,72 +1,15 @@
using Content.Client.Items.Components;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Crayon;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Client.Crayon
{
[RegisterComponent]
public class CrayonComponent : SharedCrayonComponent, IItemStatus
public sealed class CrayonComponent : SharedCrayonComponent
{
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
[ViewVariables(VVAccess.ReadWrite)] private string Color => _color;
[ViewVariables] private int Charges { get; set; }
[ViewVariables] private int Capacity { get; set; }
Control IItemStatus.MakeControl()
{
return new StatusControl(this);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState is not CrayonComponentState state)
return;
_color = state.Color;
SelectedState = state.State;
Charges = state.Charges;
Capacity = state.Capacity;
_uiUpdateNeeded = true;
}
private sealed class StatusControl : Control
{
private readonly CrayonComponent _parent;
private readonly RichTextLabel _label;
public StatusControl(CrayonComponent parent)
{
_parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
parent._uiUpdateNeeded = true;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_parent._uiUpdateNeeded)
{
return;
}
_parent._uiUpdateNeeded = false;
_label.SetMarkup(Loc.GetString("crayon-drawing-label",
("color",_parent.Color),
("state",_parent.SelectedState),
("charges", _parent.Charges),
("capacity",_parent.Capacity)));
}
}
[ViewVariables(VVAccess.ReadWrite)] public bool UIUpdateNeeded;
[ViewVariables(VVAccess.ReadWrite)] public string Color => _color;
[ViewVariables] public int Charges { get; set; }
[ViewVariables] public int Capacity { get; set; }
}
}

View file

@ -0,0 +1,72 @@
using Content.Client.Items;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Crayon;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Localization;
using Robust.Shared.Timing;
namespace Content.Client.Crayon;
public sealed class CrayonSystem : EntitySystem
{
// Didn't do in shared because I don't think most of the server stuff can be predicted.
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrayonComponent, ComponentHandleState>(OnCrayonHandleState);
SubscribeLocalEvent<CrayonComponent, ItemStatusCollectMessage>(OnCrayonItemStatus);
}
private static void OnCrayonHandleState(EntityUid uid, CrayonComponent component, ref ComponentHandleState args)
{
if (args.Current is not CrayonComponentState state) return;
component._color = state.Color;
component.SelectedState = state.State;
component.Charges = state.Charges;
component.Capacity = state.Capacity;
component.UIUpdateNeeded = true;
}
private static void OnCrayonItemStatus(EntityUid uid, CrayonComponent component, ItemStatusCollectMessage args)
{
args.Controls.Add(new StatusControl(component));
}
private sealed class StatusControl : Control
{
private readonly CrayonComponent _parent;
private readonly RichTextLabel _label;
public StatusControl(CrayonComponent parent)
{
_parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
parent.UIUpdateNeeded = true;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_parent.UIUpdateNeeded)
{
return;
}
_parent.UIUpdateNeeded = false;
_label.SetMarkup(Loc.GetString("crayon-drawing-label",
("color",_parent.Color),
("state",_parent.SelectedState),
("charges", _parent.Charges),
("capacity",_parent.Capacity)));
}
}
}

View file

@ -47,6 +47,7 @@ namespace Content.Client.Crayon.UI
if (disposing)
{
_menu?.Close();
_menu = null;
}
}
}

View file

@ -78,6 +78,7 @@ namespace Content.Client.Entry
"BodyScanner",
"Stunbaton",
"Tool",
"TriggerOnProximity",
"TilePrying",
"RandomSpriteColor",
"ConditionalSpawner",
@ -301,7 +302,8 @@ namespace Content.Client.Entry
"ArtifactInteractionTrigger",
"Artifact",
"RandomArtifactSprite",
"EnergySword"
"EnergySword",
"DoorRemote",
};
}
}

View file

@ -0,0 +1,12 @@
// Global usings for Content.Client
global using System;
global using System.Collections.Generic;
global using Robust.Shared.Analyzers;
global using Robust.Shared.Log;
global using Robust.Shared.Localization;
global using Robust.Shared.GameObjects;
global using Robust.Shared.IoC;
global using Robust.Shared.Maths;
global using Robust.Shared.ViewVariables;
global using Robust.Shared.Serialization.Manager.Attributes;

View file

@ -0,0 +1,96 @@
using System;
using Content.Shared.Trigger;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Trigger
{
public sealed class ProximityTriggerVisualizer : AppearanceVisualizer
{
[DataField("animationState")]
private string? _animationState;
[DataField("duration")]
private float _animationDuration = 0.3f;
private const string AnimKey = "proximity";
private static Animation _animation = default!;
public override void InitializeEntity(EntityUid entityUid)
{
base.InitializeEntity(entityUid);
if (_animationState == null) return;
IoCManager.Resolve<IEntityManager>().EnsureComponent<AnimationPlayerComponent>(entityUid);
_animation = new Animation
{
Length = TimeSpan.FromSeconds(_animationDuration),
AnimationTracks = {new AnimationTrackSpriteFlick
{
LayerKey = ProximityTriggerVisualLayers.Base,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(_animationState, 0f)}
},
new AnimationTrackComponentProperty()
{
ComponentType = typeof(PointLightComponent),
InterpolationMode = AnimationInterpolationMode.Nearest,
Property = nameof(PointLightComponent.Radius),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(0.1f, 0),
new AnimationTrackProperty.KeyFrame(3f, 0.1f),
new AnimationTrackProperty.KeyFrame(0.1f, 0.5f)
}
}
}
};
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!entityManager.TryGetComponent(component.Owner, out SpriteComponent spriteComponent)) return;
var animSystem = EntitySystem.Get<AnimationPlayerSystem>();
entityManager.TryGetComponent(component.Owner, out AnimationPlayerComponent? player);
component.TryGetData(ProximityTriggerVisualState.State, out ProximityTriggerVisuals state);
switch (state)
{
case ProximityTriggerVisuals.Inactive:
if (player != null)
animSystem.Stop(player, AnimKey);
spriteComponent.LayerSetState(ProximityTriggerVisualLayers.Base, "on");
break;
case ProximityTriggerVisuals.Active:
/* TODO: Waiting on ECS OnChangeData so we can actually subscribe to the animation finishing properly.
if (_animationState == null || player == null ||
animSystem.HasRunningAnimation(player, AnimKey)) return;
animSystem.Play(player, _animation, AnimKey);
*/
break;
case ProximityTriggerVisuals.Off:
default:
spriteComponent.LayerSetState(ProximityTriggerVisualLayers.Base, "off");
break;
}
}
}
public enum ProximityTriggerVisualLayers : byte
{
Base,
}
}

View file

@ -1,4 +1,5 @@
using System;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer;
@ -6,12 +7,15 @@ using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.Atmos.Piping.Binary.EntitySystems
{
[UsedImplicitly]
public class GasPassiveGateSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
base.Initialize();
@ -46,7 +50,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
var transferMoles = pressureDelta * outlet.Air.Volume / (inlet.Air.Temperature * Atmospherics.R);
// Actually transfer the gas.
outlet.AssumeAir(inlet.Air.Remove(transferMoles));
_atmosphereSystem.Merge(outlet.Air, inlet.Air.Remove(transferMoles));
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer;
@ -25,6 +26,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
{
[Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private AdminLogSystem _adminLogSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
@ -81,7 +83,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
var transferMoles = pressureDelta * outlet.Air.Volume / inlet.Air.Temperature * Atmospherics.R;
var removed = inlet.Air.Remove(transferMoles);
outlet.AssumeAir(removed);
_atmosphereSystem.Merge(outlet.Air, removed);
}
}

View file

@ -95,7 +95,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
}
}
outlet.AssumeAir(removed);
_atmosphereSystem.Merge(outlet.Air, removed);
}
private void OnPumpInteractHand(EntityUid uid, GasVolumePumpComponent component, InteractHandEvent args)

View file

@ -1,5 +1,6 @@
using System;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.NodeContainer;
@ -26,6 +27,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private AdminLogSystem _adminLogSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
@ -76,10 +78,10 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
removed.SetMoles(filter.FilteredGas.Value, 0f);
var target = filterNode.Air.Pressure < Atmospherics.MaxOutputPressure ? filterNode : inletNode;
target.AssumeAir(filteredOut);
_atmosphereSystem.Merge(target.Air, filteredOut);
}
outletNode.AssumeAir(removed);
_atmosphereSystem.Merge(outletNode.Air, removed);
}
private void OnFilterInteractHand(EntityUid uid, GasFilterComponent component, InteractHandEvent args)

View file

@ -1,5 +1,6 @@
using System;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.NodeContainer;
@ -22,6 +23,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
{
[Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private AdminLogSystem _adminLogSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
@ -98,13 +100,13 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
if (transferMolesOne > 0f)
{
var removed = inletOne.Air.Remove(transferMolesOne);
outlet.AssumeAir(removed);
_atmosphereSystem.Merge(outlet.Air, removed);
}
if (transferMolesTwo > 0f)
{
var removed = inletTwo.Air.Remove(transferMolesTwo);
outlet.AssumeAir(removed);
_atmosphereSystem.Merge(outlet.Air, removed);
}
}

View file

@ -55,7 +55,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
var transferMoles = (pressureDelta * outputVolume) / (airTemperature * Atmospherics.R);
transferMoles = MathF.Min(transferMoles, environment.TotalMoles * inlet.Air.Volume / environment.Volume);
var removed = environment.Remove(transferMoles);
inlet.AssumeAir(removed);
_atmosphereSystem.Merge(inlet.Air, removed);
}
}

View file

@ -100,7 +100,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
if (molesDelta > 0)
{
var removed = environment.Remove(molesDelta);
pipe.AssumeAir(removed);
_atmosphereSystem.Merge(pipe.Air, removed);
}
}
}

View file

@ -112,7 +112,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
var removed = tile.Remove(transferMoles);
outlet.AssumeAir(removed);
_atmosphereSystem.Merge(outlet.Air, removed);
}
}

View file

@ -1,37 +1,19 @@
using System.Threading.Tasks;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Random.Helpers;
using Content.Shared.Tag;
using Content.Server.Botany.Systems;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Botany.Components
namespace Content.Server.Botany.Components;
// TODO: This should probably be merged with SliceableFood somehow or made into a more generic Choppable.
[RegisterComponent]
[Friend(typeof(LogSystem))]
public sealed class LogComponent : Component
{
[RegisterComponent]
public class LogComponent : Component, IInteractUsing
{
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(eventArgs.User))
return false;
[DataField("spawnedPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SpawnedPrototype = "MaterialWoodPlank1";
var entMan = IoCManager.Resolve<IEntityManager>();
if (eventArgs.Using.HasTag("BotanySharp"))
{
for (var i = 0; i < 2; i++)
{
var plank = entMan.SpawnEntity("MaterialWoodPlank1", entMan.GetComponent<TransformComponent>(Owner).Coordinates);
plank.RandomOffset(0.25f);
}
entMan.QueueDeleteEntity(Owner);
return true;
}
return false;
}
}
[DataField("spawnCount")] public int SpawnCount = 2;
}

View file

@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Systems;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Chemistry.Components;
using Content.Server.Fluids.Components;
@ -107,7 +108,7 @@ namespace Content.Server.Botany.Components
public float WeedCoefficient { get; set; } = 1f;
[ViewVariables(VVAccess.ReadWrite)]
public Seed? Seed { get; set; }
public SeedPrototype? Seed { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public bool ImproperHeat { get; set; }
@ -146,6 +147,8 @@ namespace Content.Server.Botany.Components
_lastCycle = curTime;
// todo ecs.
var botanySystem = EntitySystem.Get<BotanySystem>();
// 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))
@ -272,7 +275,7 @@ namespace Content.Server.Botany.Components
}
}
// Seed pressure resistance.
// SeedPrototype pressure resistance.
var pressure = environment.Pressure;
if (pressure < Seed.LowPressureTolerance || pressure > Seed.HighPressureTolerance)
{
@ -286,7 +289,7 @@ namespace Content.Server.Botany.Components
ImproperPressure = false;
}
// Seed ideal temperature.
// SeedPrototype ideal temperature.
if (MathF.Abs(environment.Temperature - Seed.IdealHeat) > Seed.HeatTolerance)
{
Health -= healthMod;
@ -359,7 +362,7 @@ namespace Content.Server.Botany.Components
}
else if (Age < 0) // Revert back to seed packet!
{
Seed.SpawnSeedPacket(_entMan.GetComponent<TransformComponent>(Owner).Coordinates);
botanySystem.SpawnSeedPacket(Seed, _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
RemovePlant();
ForceUpdate = true;
Update();
@ -422,19 +425,21 @@ namespace Content.Server.Botany.Components
if (Seed == null || _entMan.Deleted(user) || !EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
return false;
var botanySystem = EntitySystem.Get<BotanySystem>();
if (Harvest && !Dead)
{
if (_entMan.TryGetComponent(user, out HandsComponent? hands))
{
if (!Seed.CheckHarvest(user, hands.GetActiveHandItem?.Owner))
if (!botanySystem.CanHarvest(Seed, hands.GetActiveHandItem?.Owner))
return false;
}
else if (!Seed.CheckHarvest(user))
else if (!botanySystem.CanHarvest(Seed))
{
return false;
}
Seed.Harvest(user, YieldMod);
botanySystem.Harvest(Seed, user, YieldMod);
AfterHarvest();
return true;
}
@ -451,7 +456,9 @@ namespace Content.Server.Botany.Components
if (Seed == null || !Harvest)
return;
Seed.AutoHarvest(_entMan.GetComponent<TransformComponent>(Owner).Coordinates);
var botanySystem = EntitySystem.Get<BotanySystem>();
botanySystem.AutoHarvest(Seed, _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
AfterHarvest();
}
@ -633,7 +640,7 @@ namespace Content.Server.Botany.Components
// 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>();
var plantSystem = EntitySystem.Get<BotanySystem>();
if (plantSystem.Seeds.ContainsKey(Seed.Uid))
Seed = Seed.Diverge(modified);
}
@ -653,22 +660,21 @@ namespace Content.Server.Botany.Components
if ((!_entMan.EntityExists(usingItem) ? EntityLifeStage.Deleted : _entMan.GetComponent<MetaDataComponent>(usingItem).EntityLifeStage) >= EntityLifeStage.Deleted || !EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
return false;
var botanySystem = EntitySystem.Get<BotanySystem>();
if (_entMan.TryGetComponent(usingItem, out SeedComponent? seeds))
{
if (Seed == null)
{
if (seeds.Seed == null)
{
user.PopupMessageCursor(Loc.GetString("plant-holder-component-empty-seed-packet-message"));
_entMan.QueueDeleteEntity(usingItem);
var protoMan = IoCManager.Resolve<IPrototypeManager>();
if (!protoMan.TryIndex<SeedPrototype>(seeds.SeedName, out var seed))
return false;
}
user.PopupMessageCursor(Loc.GetString("plant-holder-component-plant-success-message",
("seedName", seeds.Seed.SeedName),
("seedNoun", seeds.Seed.SeedNoun)));
("seedName", seed.SeedName),
("seedNoun", seed.SeedNoun)));
Seed = seeds.Seed;
Seed = seed;
Dead = false;
Age = 1;
Health = Seed.Endurance;
@ -778,7 +784,7 @@ namespace Content.Server.Botany.Components
return false;
}
var seed = Seed.SpawnSeedPacket(_entMan.GetComponent<TransformComponent>(user).Coordinates);
var seed = botanySystem.SpawnSeedPacket(Seed, _entMan.GetComponent<TransformComponent>(user).Coordinates);
seed.RandomOffset(0.25f);
user.PopupMessageCursor(Loc.GetString("plant-holder-component-take-sample-message",
("seedName", Seed.DisplayName)));

View file

@ -1,62 +1,15 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Server.GameObjects;
using Content.Server.Botany.Systems;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
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.Botany.Components
namespace Content.Server.Botany.Components;
[RegisterComponent]
[Friend(typeof(BotanySystem))]
public sealed class ProduceComponent : Component
{
[RegisterComponent]
public class ProduceComponent : Component, ISerializationHooks
{
[DataField("targetSolution")] public string SolutionName { get; set; } = "food";
[DataField("targetSolution")] public string SolutionName { get; set; } = "food";
[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 (IoCManager.Resolve<IEntityManager>().TryGetComponent(Owner, out SpriteComponent? sprite))
{
sprite.LayerSetRSI(0, Seed.PlantRsi);
sprite.LayerSetState(0, Seed.PlantIconState);
}
var solutionContainer = EntitySystem.Get<SolutionContainerSystem>().EnsureSolution(Owner, SolutionName);
if (solutionContainer == null)
{
Logger.Warning($"No solution container found in {nameof(ProduceComponent)}.");
return;
}
solutionContainer.RemoveAllSolution();
foreach (var (chem, quantity) in Seed.Chemicals)
{
var amount = FixedPoint2.New(quantity.Min);
if (quantity.PotencyDivisor > 0 && Potency > 0)
amount += FixedPoint2.New(Potency / quantity.PotencyDivisor);
amount = FixedPoint2.New((int) MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max));
solutionContainer.MaxVolume += amount;
solutionContainer.AddReagent(chem, amount);
}
}
}
[DataField("seed", required: true)] public string SeedName = default!;
}

View file

@ -1,51 +1,15 @@
using Content.Shared.Examine;
using Content.Server.Botany.Systems;
using Robust.Shared.Analyzers;
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;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Botany.Components
{
[RegisterComponent]
#pragma warning disable 618
public class SeedComponent : Component, IExamine
#pragma warning restore 618
[RegisterComponent, Friend(typeof(BotanySystem))]
public sealed class SeedComponent : Component
{
[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("seed-component-no-seeds-message") + "\n");
return;
}
message.AddMarkup(Loc.GetString($"seed-component-description", ("seedName", Seed.DisplayName)) + "\n");
if (!Seed.RoundStart)
{
message.AddMarkup(Loc.GetString($"seed-component-has-variety-tag", ("seedUid", Seed.Uid)) + "\n");
}
else
{
message.AddMarkup(Loc.GetString($"seed-component-plant-yield-text", ("seedYield", Seed.Yield)) + "\n");
message.AddMarkup(Loc.GetString($"seed-component-plant-potency-text", ("seedPotency", Seed.Potency)) + "\n");
}
}
[DataField("seed", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<SeedPrototype>))]
public string SeedName = default!;
}
}

View file

@ -1,46 +1,16 @@
using System.Threading.Tasks;
using Content.Server.Power.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Server.Botany.Systems;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Botany.Components
namespace Content.Server.Botany.Components;
[RegisterComponent]
[Friend(typeof(SeedExtractorSystem))]
public sealed class SeedExtractorComponent : Component
{
[RegisterComponent]
public class SeedExtractorComponent : Component, IInteractUsing
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
// TODO: Upgradeable machines.
[DataField("minSeeds")] public int MinSeeds = 1;
// TODO: Upgradeable machines.
private int _minSeeds = 1;
private int _maxSeeds = 4;
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!_entMan.TryGetComponent<ApcPowerReceiverComponent>(Owner, out var powerReceiverComponent) || !powerReceiverComponent.Powered)
return false;
if (_entMan.TryGetComponent(eventArgs.Using, out ProduceComponent? produce) && produce.Seed != null)
{
eventArgs.User.PopupMessageCursor(Loc.GetString("seed-extractor-component-interact-message",("name", _entMan.GetComponent<MetaDataComponent>(eventArgs.Using).EntityName)));
_entMan.QueueDeleteEntity(eventArgs.Using);
var random = _random.Next(_minSeeds, _maxSeeds);
for (var i = 0; i < random; i++)
{
produce.Seed.SpawnSeedPacket(_entMan.GetComponent<TransformComponent>(Owner).Coordinates, _entMan);
}
return true;
}
return false;
}
}
[DataField("maxSeeds")] public int MaxSeeds = 4;
}

View file

@ -1,362 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Botany.Components;
using Content.Server.Plants;
using Content.Shared.Atmos;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Botany
{
public enum HarvestType : byte
{
NoRepeat,
Repeat,
SelfHarvest,
}
/*
public enum PlantSpread : byte
{
NoSpread,
Creepers,
Vines,
}
public enum PlantMutation : byte
{
NoMutation,
Mutable,
HighlyMutable,
}
public enum PlantCarnivorous : byte
{
NotCarnivorous,
EatPests,
EatLivingBeings,
}
public enum PlantJuicy : byte
{
NotJuicy,
Juicy,
Slippery,
}
*/
[DataDefinition]
public struct SeedChemQuantity
{
[DataField("Min")]
public int Min;
[DataField("Max")]
public int Max;
[DataField("PotencyDivisor")]
public int PotencyDivisor;
}
[Prototype("seed")]
public class Seed : IPrototype
{
private const string SeedPrototype = "SeedBase";
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; private init; } = default!;
/// <summary>
/// Unique identifier of this seed. Do NOT set this.
/// </summary>
public int Uid { get; internal set; } = -1;
#region Tracking
[ViewVariables] [DataField("name")] public string Name { get; set; } = string.Empty;
[ViewVariables] [DataField("seedName")] public string SeedName { get; set; } = string.Empty;
[ViewVariables]
[DataField("seedNoun")]
public string SeedNoun { get; set; } = "seeds";
[ViewVariables] [DataField("displayName")] public string DisplayName { get; set; } = string.Empty;
[ViewVariables]
[DataField("roundStart")]
public bool RoundStart { get; private set; } = true;
[ViewVariables] [DataField("mysterious")] public bool Mysterious { get; set; }
[ViewVariables] [DataField("immutable")] public bool Immutable { get; set; }
#endregion
#region Output
[ViewVariables]
[DataField("productPrototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> ProductPrototypes { get; set; } = new();
[ViewVariables]
[DataField("chemicals")]
public Dictionary<string, SeedChemQuantity> Chemicals { get; set; } = new();
[ViewVariables]
[DataField("consumeGasses")]
public Dictionary<Gas, float> ConsumeGasses { get; set; } = new();
[ViewVariables]
[DataField("exudeGasses")]
public Dictionary<Gas, float> ExudeGasses { get; set; } = new();
#endregion
#region Tolerances
[ViewVariables]
[DataField("nutrientConsumption")]
public float NutrientConsumption { get; set; } = 0.25f;
[ViewVariables] [DataField("waterConsumption")] public float WaterConsumption { get; set; } = 3f;
[ViewVariables] [DataField("idealHeat")] public float IdealHeat { get; set; } = 293f;
[ViewVariables] [DataField("heatTolerance")] public float HeatTolerance { get; set; } = 20f;
[ViewVariables] [DataField("idealLight")] public float IdealLight { get; set; } = 7f;
[ViewVariables] [DataField("lightTolerance")] public float LightTolerance { get; set; } = 5f;
[ViewVariables] [DataField("toxinsTolerance")] public float ToxinsTolerance { get; set; } = 4f;
[ViewVariables]
[DataField("lowPressureTolerance")]
public float LowPressureTolerance { get; set; } = 25f;
[ViewVariables]
[DataField("highPressureTolerance")]
public float HighPressureTolerance { get; set; } = 200f;
[ViewVariables]
[DataField("pestTolerance")]
public float PestTolerance { get; set; } = 5f;
[ViewVariables]
[DataField("weedTolerance")]
public float WeedTolerance { get; set; } = 5f;
#endregion
#region General traits
[ViewVariables]
[DataField("endurance")]
public float Endurance { get; set; } = 100f;
[ViewVariables] [DataField("yield")] public int Yield { get; set; }
[ViewVariables] [DataField("lifespan")] public float Lifespan { get; set; }
[ViewVariables] [DataField("maturation")] public float Maturation { get; set; }
[ViewVariables] [DataField("production")] public float Production { get; set; }
[ViewVariables] [DataField("growthStages")] public int GrowthStages { get; set; } = 6;
[ViewVariables] [DataField("harvestRepeat")] public HarvestType HarvestRepeat { get; set; } = HarvestType.NoRepeat;
[ViewVariables] [DataField("potency")] public float Potency { get; set; } = 1f;
// No, I'm not removing these.
//public PlantSpread Spread { get; set; }
//public PlantMutation Mutation { get; set; }
//public float AlterTemperature { get; set; }
//public PlantCarnivorous Carnivorous { get; set; }
//public bool Parasite { get; set; }
//public bool Hematophage { get; set; }
//public bool Thorny { get; set; }
//public bool Stinging { get; set; }
[DataField("ligneous")]
public bool Ligneous { get; set; }
// public bool Teleporting { get; set; }
// public PlantJuicy Juicy { get; set; }
#endregion
#region Cosmetics
[ViewVariables]
[DataField("plantRsi", required: true)]
public ResourcePath PlantRsi { get; set; } = default!;
[ViewVariables]
[DataField("plantIconState")]
public string PlantIconState { get; set; } = "produce";
[ViewVariables]
[DataField("bioluminescent")]
public bool Bioluminescent { get; set; }
[ViewVariables]
[DataField("bioluminescentColor")]
public Color BioluminescentColor { get; set; } = Color.White;
[ViewVariables]
[DataField("splatPrototype")]
public string? SplatPrototype { get; set; }
#endregion
public Seed Clone()
{
var newSeed = new Seed()
{
ID = ID,
Name = Name,
SeedName = SeedName,
SeedNoun = SeedNoun,
RoundStart = RoundStart,
Mysterious = Mysterious,
ProductPrototypes = new List<string>(ProductPrototypes),
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),
ExudeGasses = new Dictionary<Gas, float>(ExudeGasses),
NutrientConsumption = NutrientConsumption,
WaterConsumption = WaterConsumption,
IdealHeat = IdealHeat,
HeatTolerance = HeatTolerance,
IdealLight = IdealLight,
LightTolerance = LightTolerance,
ToxinsTolerance = ToxinsTolerance,
LowPressureTolerance = LowPressureTolerance,
HighPressureTolerance = HighPressureTolerance,
PestTolerance = PestTolerance,
WeedTolerance = WeedTolerance,
Endurance = Endurance,
Yield = Yield,
Lifespan = Lifespan,
Maturation = Maturation,
Production = Production,
GrowthStages = GrowthStages,
HarvestRepeat = HarvestRepeat,
Potency = Potency,
PlantRsi = PlantRsi,
PlantIconState = PlantIconState,
Bioluminescent = Bioluminescent,
BioluminescentColor = BioluminescentColor,
SplatPrototype = SplatPrototype,
};
return newSeed;
}
public EntityUid SpawnSeedPacket(EntityCoordinates transformCoordinates, IEntityManager? entityManager = null)
{
entityManager ??= IoCManager.Resolve<IEntityManager>();
var seed = entityManager.SpawnEntity(SeedPrototype, transformCoordinates);
var seedComp = seed.EnsureComponent<SeedComponent>();
seedComp.Seed = this;
if (entityManager.TryGetComponent(seed, out SpriteComponent? sprite))
{
// Seed state will always be seed. Blame the spriter if that's not the case!
sprite.LayerSetSprite(0, new SpriteSpecifier.Rsi(PlantRsi, "seed"));
}
string val = Loc.GetString("botany-seed-packet-name", ("seedName", SeedName), ("seedNoun", SeedNoun));
entityManager.GetComponent<MetaDataComponent>(seed).EntityName = val;
return seed;
}
private void AddToDatabase()
{
var plantSystem = EntitySystem.Get<PlantSystem>();
if (plantSystem.AddSeedToDatabase(this))
{
Name = Uid.ToString();
}
}
public IEnumerable<EntityUid> AutoHarvest(EntityCoordinates position, int yieldMod = 1)
{
if (position.IsValid(IoCManager.Resolve<IEntityManager>()) && ProductPrototypes != null &&
ProductPrototypes.Count > 0)
return GenerateProduct(position, yieldMod);
return Enumerable.Empty<EntityUid>();
}
public IEnumerable<EntityUid> Harvest(EntityUid user, int yieldMod = 1)
{
AddToDatabase();
if (ProductPrototypes == null || ProductPrototypes.Count == 0 || Yield <= 0)
{
user.PopupMessageCursor(Loc.GetString("botany-harvest-fail-message"));
return Enumerable.Empty<EntityUid>();
}
user.PopupMessageCursor(Loc.GetString("botany-harvest-success-message", ("name", DisplayName)));
return GenerateProduct(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(user).Coordinates, yieldMod);
}
public IEnumerable<EntityUid> GenerateProduct(EntityCoordinates position, int yieldMod = 1)
{
var totalYield = 0;
if (Yield > -1)
{
if (yieldMod < 0)
{
yieldMod = 1;
totalYield = Yield;
}
else
{
totalYield = Yield * yieldMod;
}
totalYield = Math.Max(1, totalYield);
}
var random = IoCManager.Resolve<IRobustRandom>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var products = new List<EntityUid>();
for (var i = 0; i < totalYield; i++)
{
var product = random.Pick(ProductPrototypes);
var entity = entityManager.SpawnEntity(product, position);
entity.RandomOffset(0.25f);
products.Add(entity);
var produce = entity.EnsureComponent<ProduceComponent>();
produce.Seed = this;
produce.Grown();
if (Mysterious)
{
var metaData = entityManager.GetComponent<MetaDataComponent>(entity);
metaData.EntityName += "?";
metaData.EntityDescription += (" " + Loc.GetString("botany-mysterious-description-addon"));
}
}
return products;
}
public Seed Diverge(bool modified)
{
return Clone();
}
public bool CheckHarvest(EntityUid user, EntityUid? held = null)
{
return !Ligneous || (Ligneous && held != null && held.Value.HasTag("BotanySharp"));
}
}
}

View file

@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Tag;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Utility;
namespace Content.Server.Botany;
public enum HarvestType : byte
{
NoRepeat,
Repeat,
SelfHarvest
}
/*
public enum PlantSpread : byte
{
NoSpread,
Creepers,
Vines,
}
public enum PlantMutation : byte
{
NoMutation,
Mutable,
HighlyMutable,
}
public enum PlantCarnivorous : byte
{
NotCarnivorous,
EatPests,
EatLivingBeings,
}
public enum PlantJuicy : byte
{
NotJuicy,
Juicy,
Slippery,
}
*/
[DataDefinition]
public struct SeedChemQuantity
{
[DataField("Min")] public int Min;
[DataField("Max")] public int Max;
[DataField("PotencyDivisor")] public int PotencyDivisor;
}
[Prototype("seed")]
public sealed class SeedPrototype : IPrototype
{
public const string Prototype = "SeedBase";
[DataField("id", required: true)] public string ID { get; private init; } = default!;
/// <summary>
/// Unique identifier of this seed. Do NOT set this.
/// </summary>
public int Uid { get; internal set; } = -1;
#region Tracking
[DataField("name")] public string Name = string.Empty;
[DataField("seedName")] public string SeedName = string.Empty;
[DataField("seedNoun")] public string SeedNoun = "seeds";
[DataField("displayName")] public string DisplayName = string.Empty;
[DataField("roundStart")] public bool RoundStart = true;
[DataField("mysterious")] public bool Mysterious;
[DataField("immutable")] public bool Immutable;
#endregion
#region Output
[DataField("productPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> ProductPrototypes = new();
[DataField("chemicals")] public Dictionary<string, SeedChemQuantity> Chemicals = new();
[DataField("consumeGasses")] public Dictionary<Gas, float> ConsumeGasses = new();
[DataField("exudeGasses")] public Dictionary<Gas, float> ExudeGasses = new();
#endregion
#region Tolerances
[DataField("nutrientConsumption")] public float NutrientConsumption = 0.25f;
[DataField("waterConsumption")] public float WaterConsumption = 3f;
[DataField("idealHeat")] public float IdealHeat = 293f;
[DataField("heatTolerance")] public float HeatTolerance = 20f;
[DataField("idealLight")] public float IdealLight = 7f;
[DataField("lightTolerance")] public float LightTolerance = 5f;
[DataField("toxinsTolerance")] public float ToxinsTolerance = 4f;
[DataField("lowPressureTolerance")] public float LowPressureTolerance = 25f;
[DataField("highPressureTolerance")] public float HighPressureTolerance = 200f;
[DataField("pestTolerance")] public float PestTolerance = 5f;
[DataField("weedTolerance")] public float WeedTolerance = 5f;
#endregion
#region General traits
[DataField("endurance")] public float Endurance = 100f;
[DataField("yield")] public int Yield;
[DataField("lifespan")] public float Lifespan;
[DataField("maturation")] public float Maturation;
[DataField("production")] public float Production;
[DataField("growthStages")] public int GrowthStages = 6;
[DataField("harvestRepeat")] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
[DataField("potency")] public float Potency = 1f;
// No, I'm not removing these.
//public PlantSpread Spread { get; set; }
//public PlantMutation Mutation { get; set; }
//public float AlterTemperature { get; set; }
//public PlantCarnivorous Carnivorous { get; set; }
//public bool Parasite { get; set; }
//public bool Hematophage { get; set; }
//public bool Thorny { get; set; }
//public bool Stinging { get; set; }
[DataField("ligneous")] public bool Ligneous;
// public bool Teleporting { get; set; }
// public PlantJuicy Juicy { get; set; }
#endregion
#region Cosmetics
[DataField("plantRsi", required: true)]
public ResourcePath PlantRsi { get; set; } = default!;
[DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
[DataField("bioluminescent")] public bool Bioluminescent { get; set; }
[DataField("bioluminescentColor")] public Color BioluminescentColor { get; set; } = Color.White;
[DataField("splatPrototype")] public string? SplatPrototype { get; set; }
#endregion
public SeedPrototype Clone()
{
var newSeed = new SeedPrototype
{
ID = ID,
Name = Name,
SeedName = SeedName,
SeedNoun = SeedNoun,
RoundStart = RoundStart,
Mysterious = Mysterious,
ProductPrototypes = new List<string>(ProductPrototypes),
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),
ExudeGasses = new Dictionary<Gas, float>(ExudeGasses),
NutrientConsumption = NutrientConsumption,
WaterConsumption = WaterConsumption,
IdealHeat = IdealHeat,
HeatTolerance = HeatTolerance,
IdealLight = IdealLight,
LightTolerance = LightTolerance,
ToxinsTolerance = ToxinsTolerance,
LowPressureTolerance = LowPressureTolerance,
HighPressureTolerance = HighPressureTolerance,
PestTolerance = PestTolerance,
WeedTolerance = WeedTolerance,
Endurance = Endurance,
Yield = Yield,
Lifespan = Lifespan,
Maturation = Maturation,
Production = Production,
GrowthStages = GrowthStages,
HarvestRepeat = HarvestRepeat,
Potency = Potency,
PlantRsi = PlantRsi,
PlantIconState = PlantIconState,
Bioluminescent = Bioluminescent,
BioluminescentColor = BioluminescentColor,
SplatPrototype = SplatPrototype
};
return newSeed;
}
public SeedPrototype Diverge(bool modified)
{
return Clone();
}
}

View file

@ -1,25 +1,28 @@
using System.Collections.Generic;
using Content.Server.Botany;
using Content.Server.Botany.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Popups;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Plants
namespace Content.Server.Botany.Systems
{
[UsedImplicitly]
public class PlantSystem : EntitySystem
public sealed partial class BotanySystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
private int _nextUid = 0;
private readonly Dictionary<int, Seed> _seeds = new();
private float _timer = 0f;
public IReadOnlyDictionary<int, Seed> Seeds => _seeds;
public readonly Dictionary<int, SeedPrototype> Seeds = new();
public override void Initialize()
{
@ -27,6 +30,8 @@ namespace Content.Server.Plants
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
InitializeSeeds();
PopulateDatabase();
}
@ -34,22 +39,22 @@ namespace Content.Server.Plants
{
_nextUid = 0;
_seeds.Clear();
Seeds.Clear();
foreach (var seed in _prototypeManager.EnumeratePrototypes<Seed>())
foreach (var seed in _prototypeManager.EnumeratePrototypes<SeedPrototype>())
{
AddSeedToDatabase(seed);
}
}
public bool AddSeedToDatabase(Seed seed)
public bool AddSeedToDatabase(SeedPrototype seed)
{
// If it's not -1, it's already in the database. Probably.
if (seed.Uid != -1)
return false;
seed.Uid = GetNextSeedUid();
_seeds[seed.Uid] = seed;
Seeds[seed.Uid] = seed;
return true;
}

View file

@ -0,0 +1,35 @@
using Content.Server.Botany.Components;
using Content.Shared.FixedPoint;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Server.Botany.Systems;
public partial class BotanySystem
{
public void ProduceGrown(EntityUid uid, ProduceComponent produce)
{
if (!_prototypeManager.TryIndex<SeedPrototype>(produce.SeedName, out var seed))
return;
if (TryComp(uid, out SpriteComponent? sprite))
{
sprite.LayerSetRSI(0, seed.PlantRsi);
sprite.LayerSetState(0, seed.PlantIconState);
}
var solutionContainer = _solutionContainerSystem.EnsureSolution(uid, produce.SolutionName);
solutionContainer.RemoveAllSolution();
foreach (var (chem, quantity) in seed.Chemicals)
{
var amount = FixedPoint2.New(quantity.Min);
if (quantity.PotencyDivisor > 0 && seed.Potency > 0)
amount += FixedPoint2.New(seed.Potency / quantity.PotencyDivisor);
amount = FixedPoint2.New((int) MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max));
solutionContainer.MaxVolume += amount;
solutionContainer.AddReagent(chem, amount);
}
}
}

View file

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Botany.Components;
using Content.Shared.Examine;
using Content.Shared.Random.Helpers;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Botany.Systems;
public partial class BotanySystem
{
public void InitializeSeeds()
{
SubscribeLocalEvent<SeedComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(EntityUid uid, SeedComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (!_prototypeManager.TryIndex<SeedPrototype>(component.SeedName, out var seed))
return;
args.PushMarkup(Loc.GetString($"seed-component-description", ("seedName", seed.DisplayName)));
if (!seed.RoundStart)
{
args.PushMarkup(Loc.GetString($"seed-component-has-variety-tag", ("seedUid", seed.Uid)));
}
else
{
args.PushMarkup(Loc.GetString($"seed-component-plant-yield-text", ("seedYield", seed.Yield)));
args.PushMarkup(Loc.GetString($"seed-component-plant-potency-text", ("seedPotency", seed.Potency)));
}
}
#region SeedPrototype prototype stuff
public EntityUid SpawnSeedPacket(SeedPrototype proto, EntityCoordinates transformCoordinates)
{
var seed = Spawn(SeedPrototype.Prototype, transformCoordinates);
var seedComp = EnsureComp<SeedComponent>(seed);
seedComp.SeedName = proto.ID;
if (TryComp(seed, out SpriteComponent? sprite))
{
// TODO visualizer
// SeedPrototype state will always be seed. Blame the spriter if that's not the case!
sprite.LayerSetSprite(0, new SpriteSpecifier.Rsi(proto.PlantRsi, "seed"));
}
string val = Loc.GetString("botany-seed-packet-name", ("seedName", proto.SeedName), ("seedNoun", proto.SeedNoun));
MetaData(seed).EntityName = val;
return seed;
}
public IEnumerable<EntityUid> AutoHarvest(SeedPrototype proto, EntityCoordinates position, int yieldMod = 1)
{
if (position.IsValid(EntityManager) &&
proto.ProductPrototypes.Count > 0)
return GenerateProduct(proto, position, yieldMod);
return Enumerable.Empty<EntityUid>();
}
public IEnumerable<EntityUid> Harvest(SeedPrototype proto, EntityUid user, int yieldMod = 1)
{
if (AddSeedToDatabase(proto)) proto.Name = proto.Uid.ToString();
if (proto.ProductPrototypes.Count == 0 || proto.Yield <= 0)
{
_popupSystem.PopupCursor(Loc.GetString("botany-harvest-fail-message"),
Filter.Entities(user));
return Enumerable.Empty<EntityUid>();
}
_popupSystem.PopupCursor(Loc.GetString("botany-harvest-success-message", ("name", proto.DisplayName)),
Filter.Entities(user));
return GenerateProduct(proto, Transform(user).Coordinates, yieldMod);
}
public IEnumerable<EntityUid> GenerateProduct(SeedPrototype proto, EntityCoordinates position, int yieldMod = 1)
{
var totalYield = 0;
if (proto.Yield > -1)
{
if (yieldMod < 0)
{
yieldMod = 1;
totalYield = proto.Yield;
}
else
totalYield = proto.Yield * yieldMod;
totalYield = Math.Max(1, totalYield);
}
var products = new List<EntityUid>();
for (var i = 0; i < totalYield; i++)
{
var product = _robustRandom.Pick(proto.ProductPrototypes);
var entity = Spawn(product, position);
entity.RandomOffset(0.25f);
products.Add(entity);
var produce = EnsureComp<ProduceComponent>(entity);
produce.SeedName = proto.ID;
ProduceGrown(entity, produce);
if (proto.Mysterious)
{
var metaData = MetaData(entity);
metaData.EntityName += "?";
metaData.EntityDescription += " " + Loc.GetString("botany-mysterious-description-addon");
}
}
return products;
}
public bool CanHarvest(SeedPrototype proto, EntityUid? held = null)
{
return !proto.Ligneous || proto.Ligneous && held != null && held.Value.HasTag("BotanySharp");
}
#endregion
}

View file

@ -0,0 +1,32 @@
using Content.Server.Botany.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Random.Helpers;
using Content.Shared.Tag;
using Robust.Shared.GameObjects;
namespace Content.Server.Botany.Systems;
public sealed class LogSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LogComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnInteractUsing(EntityUid uid, LogComponent component, InteractUsingEvent args)
{
if (args.Used.HasTag("BotanySharp"))
{
for (var i = 0; i < component.SpawnCount; i++)
{
var plank = Spawn(component.SpawnedPrototype, Transform(uid).Coordinates);
plank.RandomOffset(0.25f);
}
QueueDel(uid);
}
}
}

View file

@ -0,0 +1,52 @@
using Content.Server.Botany.Components;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Botany.Systems;
public sealed class SeedExtractorSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly BotanySystem _botanySystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SeedExtractorComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnInteractUsing(EntityUid uid, SeedExtractorComponent component, InteractUsingEvent args)
{
if (!TryComp<ApcPowerReceiverComponent>(uid, out var powerReceiverComponent) || !powerReceiverComponent.Powered)
return;
if (TryComp(args.Used, out ProduceComponent? produce))
{
if (!_prototypeManager.TryIndex<SeedPrototype>(produce.SeedName, out var seed))
return;
_popupSystem.PopupCursor(Loc.GetString("seed-extractor-component-interact-message",("name", args.Used)),
Filter.Entities(args.User));
QueueDel(args.Used);
var random = _random.Next(component.MinSeeds, component.MaxSeeds);
var coords = Transform(uid).Coordinates;
for (var i = 0; i < random; i++)
{
_botanySystem.SpawnSeedPacket(seed, coords);
}
}
}
}

View file

@ -34,10 +34,8 @@ namespace Content.Server.Chemistry.Components
/// containers, and can directly inject into a mobs bloodstream.
/// </summary>
[RegisterComponent]
public class InjectorComponent : SharedInjectorComponent, IAfterInteract, IUse
public class InjectorComponent : SharedInjectorComponent
{
[Dependency] private readonly IEntityManager _entities = default!;
public const string SolutionName = "injector";
/// <summary>
@ -46,7 +44,7 @@ namespace Content.Server.Chemistry.Components
/// </summary>
[ViewVariables]
[DataField("injectOnly")]
private bool _injectOnly;
public bool InjectOnly;
/// <summary>
/// Amount to inject or draw on each usage. If the injector is inject only, it will
@ -54,7 +52,7 @@ namespace Content.Server.Chemistry.Components
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("transferAmount")]
private FixedPoint2 _transferAmount = FixedPoint2.New(5);
public FixedPoint2 TransferAmount = FixedPoint2.New(5);
/// <summary>
/// Injection delay (seconds) when the target is a mob.
@ -90,331 +88,5 @@ namespace Content.Server.Chemistry.Components
Dirty();
}
}
protected override void Startup()
{
base.Startup();
Dirty();
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
private void Toggle(EntityUid user)
{
if (_injectOnly)
{
return;
}
string msg;
switch (ToggleState)
{
case InjectorToggleMode.Inject:
ToggleState = InjectorToggleMode.Draw;
msg = "injector-component-drawing-text";
break;
case InjectorToggleMode.Draw:
ToggleState = InjectorToggleMode.Inject;
msg = "injector-component-injecting-text";
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 (CancelToken != null)
{
CancelToken.Cancel();
return true;
}
if (!eventArgs.CanReach)
return false;
var solutionsSys = EntitySystem.Get<SolutionContainerSystem>();
//Make sure we have the attacking entity
if (eventArgs.Target is not {Valid: true} target ||
!_entities.HasComponent<SolutionContainerManagerComponent>(Owner))
{
return false;
}
// Is the target a mob? If yes, use a do-after to give them time to respond.
if (_entities.HasComponent<MobStateComponent>(target) ||
_entities.HasComponent<BloodstreamComponent>(target))
{
if (!await TryInjectDoAfter(eventArgs.User, target))
return true;
}
// Handle injecting/drawing for solutions
if (ToggleState == InjectorToggleMode.Inject)
{
if (solutionsSys.TryGetInjectableSolution(target, out var injectableSolution))
{
TryInject(target, injectableSolution, eventArgs.User, false);
}
else if (solutionsSys.TryGetRefillableSolution(target, out var refillableSolution))
{
TryInject(target, refillableSolution, eventArgs.User, true);
}
else if (_entities.TryGetComponent(target, out BloodstreamComponent? bloodstream))
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("injector-component-cannot-transfer-message",
("target", target)));
}
}
else if (ToggleState == InjectorToggleMode.Draw)
{
if (solutionsSys.TryGetDrawableSolution(target, out var drawableSolution))
{
TryDraw(target, drawableSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("injector-component-cannot-draw-message",
("target", target)));
}
}
return true;
}
/// <summary>
/// Send informative pop-up messages and wait for a do-after to complete.
/// </summary>
public async Task<bool> TryInjectDoAfter(EntityUid user, EntityUid target)
{
var popupSys = EntitySystem.Get<SharedPopupSystem>();
// Create a pop-up for the user
popupSys.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user));
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
return false;
// Get entity for logging. Log with EntityUids when?
var logSys = EntitySystem.Get<AdminLogSystem>();
var actualDelay = MathF.Max(Delay, 1f);
if (user != target)
{
// Create a pop-up for the target
var userName = _entities.GetComponent<MetaDataComponent>(user).EntityName;
popupSys.PopupEntity(Loc.GetString("injector-component-injecting-target",
("user", userName)), user, Filter.Entities(target));
// Check if the target is incapacitated or in combat mode and modify time accordingly.
if (_entities.TryGetComponent<MobStateComponent>(target, out var mobState) &&
mobState.IsIncapacitated())
{
actualDelay /= 2;
}
else if (_entities.TryGetComponent<CombatModeComponent>(target, out var combat) &&
combat.IsInCombatMode)
{
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
// combat with fast syringes & lag.
actualDelay += 1;
}
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
if (ToggleState == InjectorToggleMode.Inject)
{
logSys.Add(LogType.ForceFeed,
$"{_entities.ToPrettyString(user):user} is attempting to inject {_entities.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}");
// TODO solution pretty string.
}
}
else
{
// Self-injections take half as long.
actualDelay /= 2;
if (ToggleState == InjectorToggleMode.Inject)
logSys.Add(LogType.Ingestion,
$"{_entities.ToPrettyString(user):user} is attempting to inject themselves with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}.");
//TODO solution pretty string.
}
CancelToken = new();
var status = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(
new DoAfterEventArgs(user, actualDelay, CancelToken.Token, target)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
MovementThreshold = 1.0f
});
CancelToken = null;
return status == DoAfterStatus.Finished;
}
/// <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, EntityUid user)
{
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(_transferAmount, targetBloodstream.Solution.AvailableVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user,
Loc.GetString("injector-component-cannot-inject-message", ("target", targetBloodstream.Owner)));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution =
EntitySystem.Get<SolutionContainerSystem>().SplitSolution(user, targetBloodstream.Solution, realTransferAmount);
var bloodstreamSys = EntitySystem.Get<BloodstreamSystem>();
bloodstreamSys.TryAddToBloodstream((targetBloodstream).Owner, removedSolution, targetBloodstream);
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
Owner.PopupMessage(user,
Loc.GetString("injector-component-inject-success-message",
("amount", removedSolution.TotalVolume),
("target", targetBloodstream.Owner)));
Dirty();
AfterInject();
}
private void TryInject(EntityUid targetEntity, Solution targetSolution, EntityUid user, bool asRefill)
{
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|| solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(_transferAmount, targetSolution.AvailableVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user,
Loc.GetString("injector-component-target-already-full-message", ("target", targetEntity)));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = EntitySystem.Get<SolutionContainerSystem>().SplitSolution(Owner, solution, realTransferAmount);
removedSolution.DoEntityReaction(targetEntity, ReactionMethod.Injection);
if (!asRefill)
{
EntitySystem.Get<SolutionContainerSystem>()
.Inject(targetEntity, targetSolution, removedSolution);
}
else
{
EntitySystem.Get<SolutionContainerSystem>()
.Refill(targetEntity, targetSolution, removedSolution);
}
Owner.PopupMessage(user,
Loc.GetString("injector-component-transfer-success-message",
("amount", removedSolution.TotalVolume),
("target", targetEntity)));
Dirty();
AfterInject();
}
private void AfterInject()
{
// Automatically set syringe to draw after completely draining it.
if (EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
&& solution.CurrentVolume == 0)
{
ToggleState = InjectorToggleMode.Draw;
}
}
private void AfterDraw()
{
// Automatically set syringe to inject after completely filling it.
if (EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
&& solution.AvailableVolume == 0)
{
ToggleState = InjectorToggleMode.Inject;
}
}
private void TryDraw(EntityUid targetEntity, Solution targetSolution, EntityUid user)
{
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|| solution.AvailableVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(_transferAmount, targetSolution.DrawAvailable);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user,
Loc.GetString("injector-component-target-is-empty-message", ("target", targetEntity)));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = EntitySystem.Get<SolutionContainerSystem>()
.Draw(targetEntity, targetSolution, realTransferAmount);
if (!EntitySystem.Get<SolutionContainerSystem>().TryAddSolution(targetEntity, solution, removedSolution))
{
return;
}
Owner.PopupMessage(user,
Loc.GetString("injector-component-draw-success-message",
("amount", removedSolution.TotalVolume),
("target", targetEntity)));
Dirty();
AfterDraw();
}
public override ComponentState GetComponentState()
{
_entities.EntitySysManager.GetEntitySystem<SolutionContainerSystem>()
.TryGetSolution(Owner, SolutionName, out var solution);
var currentVolume = solution?.CurrentVolume ?? FixedPoint2.Zero;
var maxVolume = solution?.MaxVolume ?? FixedPoint2.Zero;
return new InjectorComponentState(currentVolume, maxVolume, ToggleState);
}
}
}

View file

@ -0,0 +1,385 @@
using System;
using System.Threading;
using Content.Server.Body.Components;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.CombatMode;
using Content.Server.DoAfter;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.MobState.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Localization;
using Robust.Shared.Player;
namespace Content.Server.Chemistry.EntitySystems;
public sealed partial class ChemistrySystem
{
private void InitializeInjector()
{
SubscribeLocalEvent<InjectorComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<InjectorComponent, HandDeselectedEvent>(OnInjectorDeselected);
SubscribeLocalEvent<InjectorComponent, ComponentStartup>(OnInjectorStartup);
SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
SubscribeLocalEvent<InjectorComponent, ComponentGetState>(OnInjectorGetState);
SubscribeLocalEvent<InjectionCompleteEvent>(OnInjectionComplete);
SubscribeLocalEvent<InjectionCancelledEvent>(OnInjectionCancelled);
}
private static void OnInjectionCancelled(InjectionCancelledEvent ev)
{
ev.Component.CancelToken = null;
}
private void OnInjectionComplete(InjectionCompleteEvent ev)
{
var component = ev.Component;
var user = ev.User;
var target = ev.Target;
component.CancelToken = null;
// Handle injecting/drawing for solutions
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
{
if (_solutions.TryGetInjectableSolution(target, out var injectableSolution))
{
TryInject(component, target, injectableSolution, user, false);
}
else if (_solutions.TryGetRefillableSolution(target, out var refillableSolution))
{
TryInject(component, target, refillableSolution, user, true);
}
else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
{
TryInjectIntoBloodstream(component, bloodstream, user);
}
else
{
_popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
("target", target)), component.Owner, Filter.Entities(user));
}
}
else if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Draw)
{
if (_solutions.TryGetDrawableSolution(target, out var drawableSolution))
{
TryDraw(component, target, drawableSolution, user);
}
else
{
_popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
("target", target)), component.Owner, Filter.Entities(user));
}
}
}
private static void OnInjectorDeselected(EntityUid uid, InjectorComponent component, HandDeselectedEvent args)
{
component.CancelToken?.Cancel();
component.CancelToken = null;
}
private void OnSolutionChange(EntityUid uid, InjectorComponent component, SolutionChangedEvent args)
{
Dirty(component);
}
private void OnInjectorGetState(EntityUid uid, InjectorComponent component, ref ComponentGetState args)
{
_solutions.TryGetSolution(uid, InjectorComponent.SolutionName, out var solution);
var currentVolume = solution?.CurrentVolume ?? FixedPoint2.Zero;
var maxVolume = solution?.MaxVolume ?? FixedPoint2.Zero;
args.State = new SharedInjectorComponent.InjectorComponentState(currentVolume, maxVolume, component.ToggleState);
}
private void OnInjectorAfterInteract(EntityUid uid, InjectorComponent component, AfterInteractEvent args)
{
if (args.Handled || !args.CanReach) return;
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
component.CancelToken = null;
args.Handled = true;
return;
}
if (!_blocker.CanInteract(args.User))
return;
//Make sure we have the attacking entity
if (args.Target is not { Valid: true } target ||
!HasComp<SolutionContainerManagerComponent>(uid))
{
return;
}
// Is the target a mob? If yes, use a do-after to give them time to respond.
if (HasComp<MobStateComponent>(target) ||
HasComp<BloodstreamComponent>(target))
{
InjectDoAfter(component, args.User, target);
args.Handled = true;
return;
}
}
private void OnInjectorStartup(EntityUid uid, InjectorComponent component, ComponentStartup args)
{
Dirty(component);
}
private void OnInjectorUse(EntityUid uid, InjectorComponent component, UseInHandEvent args)
{
if (args.Handled) return;
Toggle(component, args.User);
args.Handled = true;
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
private void Toggle(InjectorComponent component, EntityUid user)
{
if (component.InjectOnly)
{
return;
}
string msg;
switch (component.ToggleState)
{
case SharedInjectorComponent.InjectorToggleMode.Inject:
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
msg = "injector-component-drawing-text";
break;
case SharedInjectorComponent.InjectorToggleMode.Draw:
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
msg = "injector-component-injecting-text";
break;
default:
throw new ArgumentOutOfRangeException();
}
_popup.PopupEntity(Loc.GetString(msg), component.Owner, Filter.Entities(user));
}
/// <summary>
/// Send informative pop-up messages and wait for a do-after to complete.
/// </summary>
private void InjectDoAfter(InjectorComponent component, EntityUid user, EntityUid target)
{
// Create a pop-up for the user
_popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user));
if (!_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution))
return;
// Get entity for logging. Log with EntityUids when?
var actualDelay = MathF.Max(component.Delay, 1f);
if (user != target)
{
// Create a pop-up for the target
var userName = MetaData(user).EntityName;
_popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
("user", userName)), user, Filter.Entities(target));
// Check if the target is incapacitated or in combat mode and modify time accordingly.
if (TryComp<MobStateComponent>(target, out var mobState) && mobState.IsIncapacitated())
{
actualDelay /= 2;
}
else if (TryComp<CombatModeComponent>(target, out var combat) && combat.IsInCombatMode)
{
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
// combat with fast syringes & lag.
actualDelay += 1;
}
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
{
_logs.Add(LogType.ForceFeed,
$"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}");
// TODO solution pretty string.
}
}
else
{
// Self-injections take half as long.
actualDelay /= 2;
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
_logs.Add(LogType.Ingestion,
$"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}.");
//TODO solution pretty string.
}
component.CancelToken = new CancellationTokenSource();
_doAfter.DoAfter(new DoAfterEventArgs(user, actualDelay, component.CancelToken.Token, target)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
MovementThreshold = 0.1f,
BroadcastFinishedEvent = new InjectionCompleteEvent()
{
Component = component,
User = user,
Target = target,
},
BroadcastCancelledEvent = new InjectionCancelledEvent()
{
Component = component,
}
});
}
private void TryInjectIntoBloodstream(InjectorComponent component, BloodstreamComponent targetBloodstream, EntityUid user)
{
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetBloodstream.Solution.AvailableVolume);
if (realTransferAmount <= 0)
{
_popup.PopupEntity(Loc.GetString("injector-component-cannot-inject-message", ("target", targetBloodstream.Owner)),
component.Owner, Filter.Entities(user));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solutions.SplitSolution(user, targetBloodstream.Solution, realTransferAmount);
_blood.TryAddToBloodstream((targetBloodstream).Owner, removedSolution, targetBloodstream);
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
_popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
("amount", removedSolution.TotalVolume),
("target", targetBloodstream.Owner)), component.Owner, Filter.Entities(user));
Dirty(component);
AfterInject(component);
}
private void TryInject(InjectorComponent component, EntityUid targetEntity, Solution targetSolution, EntityUid user, bool asRefill)
{
if (!_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
|| solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.AvailableVolume);
if (realTransferAmount <= 0)
{
_popup.PopupEntity(Loc.GetString("injector-component-target-already-full-message", ("target", targetEntity)),
component.Owner, Filter.Entities(user));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solutions.SplitSolution(component.Owner, solution, realTransferAmount);
removedSolution.DoEntityReaction(targetEntity, ReactionMethod.Injection);
if (!asRefill)
{
_solutions.Inject(targetEntity, targetSolution, removedSolution);
}
else
{
_solutions.Refill(targetEntity, targetSolution, removedSolution);
}
_popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
("amount", removedSolution.TotalVolume),
("target", targetEntity)), component.Owner, Filter.Entities(user));
Dirty(component);
AfterInject(component);
}
private void AfterInject(InjectorComponent component)
{
// Automatically set syringe to draw after completely draining it.
if (_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
&& solution.CurrentVolume == 0)
{
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
}
}
private void AfterDraw(InjectorComponent component)
{
// Automatically set syringe to inject after completely filling it.
if (_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
&& solution.AvailableVolume == 0)
{
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
}
}
private void TryDraw(InjectorComponent component, EntityUid targetEntity, Solution targetSolution, EntityUid user)
{
if (!_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
|| solution.AvailableVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.DrawAvailable);
if (realTransferAmount <= 0)
{
_popup.PopupEntity(Loc.GetString("injector-component-target-is-empty-message", ("target", targetEntity)),
component.Owner, Filter.Entities(user));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solutions.Draw(targetEntity, targetSolution, realTransferAmount);
if (!_solutions.TryAddSolution(targetEntity, solution, removedSolution))
{
return;
}
_popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
("amount", removedSolution.TotalVolume),
("target", targetEntity)), component.Owner, Filter.Entities(user));
Dirty(component);
AfterDraw(component);
}
private sealed class InjectionCompleteEvent : EntityEventArgs
{
public InjectorComponent Component { get; init; } = default!;
public EntityUid User { get; init; }
public EntityUid Target { get; init; }
}
private sealed class InjectionCancelledEvent : EntityEventArgs
{
public InjectorComponent Component { get; init; } = default!;
}
}

View file

@ -0,0 +1,25 @@
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.DoAfter;
using Content.Server.Popups;
using Content.Shared.ActionBlocker;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.Chemistry.EntitySystems;
public sealed partial class ChemistrySystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly AdminLogSystem _logs = default!;
[Dependency] private readonly BloodstreamSystem _blood = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
public override void Initialize()
{
InitializeHypospray();
InitializeInjector();
}
}

View file

@ -6,13 +6,10 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Chemistry.EntitySystems
{
[UsedImplicitly]
public class HypospraySystem : EntitySystem
public sealed partial class ChemistrySystem
{
public override void Initialize()
private void InitializeHypospray()
{
base.Initialize();
SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<HyposprayComponent, ClickAttackEvent>(OnClickAttack);
SubscribeLocalEvent<HyposprayComponent, SolutionChangedEvent>(OnSolutionChange);
@ -27,6 +24,7 @@ namespace Content.Server.Chemistry.EntitySystems
{
if (!args.CanReach)
return;
var target = args.Target;
var user = args.User;

View file

@ -1,34 +0,0 @@
using Content.Server.Chemistry.Components;
using Content.Shared.Hands;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using System;
namespace Content.Server.Chemistry.EntitySystems
{
[UsedImplicitly]
public class InjectorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InjectorComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<InjectorComponent, HandDeselectedEvent>(OnInjectorDeselected);
}
private void OnInjectorDeselected(EntityUid uid, InjectorComponent component, HandDeselectedEvent args)
{
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
component.CancelToken = null;
}
}
private void OnSolutionChange(EntityUid uid, InjectorComponent component, SolutionChangedEvent args)
{
Dirty(component);
}
}
}

View file

@ -1,25 +1,9 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.UserInterface;
using Content.Shared.Audio;
using Content.Shared.Crayon;
using Content.Server.Decals;
using Content.Shared.Decals;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Popups;
using Content.Shared.Sound;
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;
@ -27,16 +11,12 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Crayon
{
[RegisterComponent]
public class CrayonComponent : SharedCrayonComponent, IAfterInteract, IUse, IDropped, ISerializationHooks
public sealed class CrayonComponent : SharedCrayonComponent, ISerializationHooks
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[DataField("useSound")]
private SoundSpecifier? _useSound = null;
[DataField("useSound")] public SoundSpecifier? UseSound;
[ViewVariables]
public Color Color { get; set; }
public Color Color { get; private set; }
[ViewVariables(VVAccess.ReadWrite)]
public int Charges { get; set; }
@ -45,104 +25,11 @@ namespace Content.Server.Crayon
[DataField("capacity")]
public int Capacity { get; set; } = 30;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CrayonUiKey.Key);
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(CrayonUiKey.Key);
void ISerializationHooks.AfterDeserialization()
{
Color = Color.FromName(_color);
}
protected 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 decal = _prototypeManager.EnumeratePrototypes<DecalPrototype>().FirstOrDefault(x => x.Tags.Contains("crayon"));
SelectedState = decal?.ID ?? string.Empty;
Dirty();
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
switch (serverMsg.Message)
{
case CrayonSelectMessage msg:
// Check if the selected state is valid
if (_prototypeManager.TryIndex<DecalPrototype>(msg.State, out var prototype) && prototype.Tags.Contains("crayon"))
{
SelectedState = msg.State;
Dirty();
}
break;
default:
break;
}
}
public override ComponentState GetComponentState()
{
return new CrayonComponentState(_color, SelectedState, Charges, Capacity);
}
// Opens the selection window
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
if (_entMan.TryGetComponent(eventArgs.User, 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.User.InRangeUnobstructed(eventArgs.ClickLocation, ignoreInsideBlocker: false, popup: true,
collisionMask: Shared.Physics.CollisionGroup.MobImpassable))
{
return true;
}
if (Charges <= 0)
{
eventArgs.User.PopupMessage(Loc.GetString("crayon-interact-not-enough-left-text"));
return true;
}
if (!eventArgs.ClickLocation.IsValid(_entMan))
{
eventArgs.User.PopupMessage(Loc.GetString("crayon-interact-invalid-location"));
return true;
}
if(!EntitySystem.Get<DecalSystem>().TryAddDecal(SelectedState, eventArgs.ClickLocation.Offset(new Vector2(-0.5f,-0.5f)), out _, Color.FromName(_color), cleanable: true))
return false;
if (_useSound != null)
SoundSystem.Play(Filter.Pvs(Owner), _useSound.GetSound(), Owner, AudioHelpers.WithVariation(0.125f));
// Decrease "Ammo"
Charges--;
Dirty();
EntitySystem.Get<AdminLogSystem>().Add(LogType.CrayonDraw, LogImpact.Low, $"{_entMan.ToPrettyString(eventArgs.User):user} drew a {_color:color} {SelectedState}");
return true;
}
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
if (_entMan.TryGetComponent(eventArgs.User, out ActorComponent? actor))
UserInterface?.Close(actor.PlayerSession);
}
}
}

View file

@ -0,0 +1,121 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Decals;
using Content.Server.Popups;
using Content.Shared.Audio;
using Content.Shared.Crayon;
using Content.Shared.Database;
using Content.Shared.Decals;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Crayon;
public sealed class CrayonSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly AdminLogSystem _logs = default!;
[Dependency] private readonly DecalSystem _decals = default!;
[Dependency] private readonly PopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrayonComponent, ComponentInit>(OnCrayonInit);
SubscribeLocalEvent<CrayonComponent, CrayonSelectMessage>(OnCrayonBoundUI);
SubscribeLocalEvent<CrayonComponent, UseInHandEvent>(OnCrayonUse);
SubscribeLocalEvent<CrayonComponent, AfterInteractEvent>(OnCrayonAfterInteract);
SubscribeLocalEvent<CrayonComponent, DroppedEvent>(OnCrayonDropped);
SubscribeLocalEvent<CrayonComponent, ComponentGetState>(OnCrayonGetState);
}
private static void OnCrayonGetState(EntityUid uid, CrayonComponent component, ref ComponentGetState args)
{
args.State = new CrayonComponentState(component._color, component.SelectedState, component.Charges, component.Capacity);
}
private void OnCrayonAfterInteract(EntityUid uid, CrayonComponent component, AfterInteractEvent args)
{
if (args.Handled || !args.CanReach)
return;
if (component.Charges <= 0)
{
_popup.PopupEntity(Loc.GetString("crayon-interact-not-enough-left-text"), uid, Filter.Entities(args.User));
args.Handled = true;
return;
}
if (!args.ClickLocation.IsValid(EntityManager))
{
_popup.PopupEntity(Loc.GetString("crayon-interact-invalid-location"), uid, Filter.Entities(args.User));
args.Handled = true;
return;
}
if(!_decals.TryAddDecal(component.SelectedState, args.ClickLocation.Offset(new Vector2(-0.5f,-0.5f)), out _, Color.FromName(component._color), cleanable: true))
return;
if (component.UseSound != null)
SoundSystem.Play(Filter.Pvs(uid), component.UseSound.GetSound(), uid, AudioHelpers.WithVariation(0.125f));
// Decrease "Ammo"
component.Charges--;
Dirty(component);
_logs.Add(LogType.CrayonDraw, LogImpact.Low, $"{EntityManager.ToPrettyString(args.User):user} drew a {component._color:color} {component.SelectedState}");
args.Handled = true;
}
private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEvent args)
{
// Open crayon window if neccessary.
if (args.Handled)
return;
if (!TryComp<ActorComponent>(args.User, out var actor)) return;
component.UserInterface?.Toggle(actor.PlayerSession);
if (component.UserInterface?.SessionHasOpen(actor.PlayerSession) == true)
{
// Tell the user interface the selected stuff
component.UserInterface.SetState(new CrayonBoundUserInterfaceState(component.SelectedState, component.Color));
}
args.Handled = true;
}
private void OnCrayonBoundUI(EntityUid uid, CrayonComponent component, CrayonSelectMessage args)
{
// Check if the selected state is valid
if (!_prototypeManager.TryIndex<DecalPrototype>(args.State, out var prototype) || !prototype.Tags.Contains("crayon")) return;
component.SelectedState = args.State;
Dirty(component);
}
private void OnCrayonInit(EntityUid uid, CrayonComponent component, ComponentInit args)
{
component.Charges = component.Capacity;
// Get the first one from the catalog and set it as default
var decal = _prototypeManager.EnumeratePrototypes<DecalPrototype>().FirstOrDefault(x => x.Tags.Contains("crayon"));
component.SelectedState = decal?.ID ?? string.Empty;
Dirty(component);
}
private void OnCrayonDropped(EntityUid uid, CrayonComponent component, DroppedEvent args)
{
if (TryComp<ActorComponent>(args.UserUid, out var actor))
component.UserInterface?.Close(actor.PlayerSession);
}
}

View file

@ -1,9 +1,8 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Electrocution
@ -16,10 +15,13 @@ namespace Content.Server.Electrocution
[DataField("node")]
public string NodeName = default!;
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
var ent = IoCManager.Resolve<IEntityManager>();
if (!ent.TryGetComponent(CableEntity, out NodeContainerComponent? nodeContainer))
if (!nodeQuery.TryGetComponent(CableEntity, out var nodeContainer))
yield break;
if (nodeContainer.TryGetNode(NodeName, out Node? node))

View file

@ -0,0 +1,64 @@
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using System.Collections.Generic;
namespace Content.Server.Explosion.Components
{
/// <summary>
/// Raises a <see cref="TriggerEvent"/> whenever an entity collides with a fixture attached to the owner of this component.
/// </summary>
[RegisterComponent]
public sealed class TriggerOnProximityComponent : Component
{
public const string FixtureID = "trigger-on-proximity-fixture";
public HashSet<PhysicsComponent> Colliding = new();
[DataField("shape", required: true)]
public IPhysShape Shape { get; set; } = new PhysShapeCircle {Radius = 2};
/// <summary>
/// How long the the proximity trigger animation plays for.
/// </summary>
[ViewVariables]
[DataField("animationDuration")]
public float AnimationDuration = 0.3f;
/// <summary>
/// Whether the entity needs to be anchored for the proximity to work.
/// </summary>
[ViewVariables]
[DataField("requiresAnchored")]
public bool RequiresAnchored { get; set; } = true;
[ViewVariables]
[DataField("enabled")]
public bool Enabled = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("cooldown")]
public float Cooldown { get; set; } = 5f;
/// <summary>
/// How much cooldown has elapsed (if relevant).
/// </summary>
public float Accumulator = 0f;
/// <summary>
/// What speed should the other object be moving at to trigger the proximity fixture?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("triggerSpeed")]
public float TriggerSpeed { get; set; } = 3.5f;
/// <summary>
/// If this proximity is triggered should we continually repeat it?
/// </summary>
[DataField("repeating")]
internal bool Repeating = true;
}
}

View file

@ -0,0 +1,158 @@
using System.Collections.Generic;
using Content.Server.Explosion.Components;
using Content.Shared.Physics;
using Content.Shared.Trigger;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class TriggerSystem
{
/// <summary>
/// Anything that has stuff touching it (to check speed) or is on cooldown.
/// </summary>
private HashSet<TriggerOnProximityComponent> _activeProximities = new();
private void InitializeProximity()
{
SubscribeLocalEvent<TriggerOnProximityComponent, StartCollideEvent>(OnProximityStartCollide);
SubscribeLocalEvent<TriggerOnProximityComponent, EndCollideEvent>(OnProximityEndCollide);
SubscribeLocalEvent<TriggerOnProximityComponent, ComponentStartup>(OnProximityStartup);
SubscribeLocalEvent<TriggerOnProximityComponent, ComponentShutdown>(OnProximityShutdown);
SubscribeLocalEvent<TriggerOnProximityComponent, AnchorStateChangedEvent>(OnProximityAnchor);
}
private void OnProximityAnchor(EntityUid uid, TriggerOnProximityComponent component, ref AnchorStateChangedEvent args)
{
component.Enabled = !component.RequiresAnchored ||
args.Anchored;
SetProximityAppearance(uid, component);
if (!component.Enabled)
{
_activeProximities.Remove(component);
component.Colliding.Clear();
}
// Re-check for contacts as we cleared them.
else if (TryComp<PhysicsComponent>(uid, out var body))
{
_broadphase.RegenerateContacts(body);
}
}
private void OnProximityShutdown(EntityUid uid, TriggerOnProximityComponent component, ComponentShutdown args)
{
_activeProximities.Remove(component);
component.Colliding.Clear();
}
private void OnProximityStartup(EntityUid uid, TriggerOnProximityComponent component, ComponentStartup args)
{
component.Enabled = !component.RequiresAnchored ||
EntityManager.GetComponent<TransformComponent>(uid).Anchored;
SetProximityAppearance(uid, component);
if (!TryComp<PhysicsComponent>(uid, out var body)) return;
_fixtures.CreateFixture(body, new Fixture(body, component.Shape)
{
// TODO: Should probably have these settable via datafield but I'm lazy and it's a pain
CollisionLayer = (int) (CollisionGroup.MobImpassable | CollisionGroup.SmallImpassable | CollisionGroup.VaultImpassable), Hard = false, ID = TriggerOnProximityComponent.FixtureID
});
}
private void OnProximityStartCollide(EntityUid uid, TriggerOnProximityComponent component, StartCollideEvent args)
{
if (args.OurFixture.ID != TriggerOnProximityComponent.FixtureID) return;
_activeProximities.Add(component);
component.Colliding.Add(args.OtherFixture.Body);
}
private void OnProximityEndCollide(EntityUid uid, TriggerOnProximityComponent component, EndCollideEvent args)
{
if (args.OurFixture.ID != TriggerOnProximityComponent.FixtureID) return;
component.Colliding.Remove(args.OtherFixture.Body);
if (component.Colliding.Count == 0)
_activeProximities.Remove(component);
}
private void SetProximityAppearance(EntityUid uid, TriggerOnProximityComponent component)
{
if (EntityManager.TryGetComponent(uid, out AppearanceComponent? appearanceComponent))
{
appearanceComponent.SetData(ProximityTriggerVisualState.State, component.Enabled ? ProximityTriggerVisuals.Inactive : ProximityTriggerVisuals.Off);
}
}
private void Activate(TriggerOnProximityComponent component)
{
DebugTools.Assert(component.Enabled);
if (!component.Repeating)
{
component.Enabled = false;
_activeProximities.Remove(component);
component.Colliding.Clear();
}
else
{
component.Accumulator = component.Cooldown;
}
SetProximityAppearance(component.Owner, component);
Trigger(component.Owner);
}
private void UpdateProximity(float frameTime)
{
var toRemove = new RemQueue<TriggerOnProximityComponent>();
foreach (var comp in _activeProximities)
{
if (!comp.Enabled)
{
toRemove.Add(comp);
continue;
}
MetaDataComponent? metadata = null;
if (Deleted(comp.Owner, metadata))
{
toRemove.Add(comp);
continue;
}
if (Paused(comp.Owner, metadata)) continue;
comp.Accumulator -= frameTime;
if (comp.Accumulator > 0f) continue;
// Alright now that we have no cd check everything in range.
foreach (var colliding in comp.Colliding)
{
if (Deleted(colliding.Owner)) continue;
if (colliding.LinearVelocity.Length < comp.TriggerSpeed) continue;
// Trigger!
Activate(comp);
break;
}
}
foreach (var prox in toRemove)
{
_activeProximities.Remove(prox);
}
}
}

View file

@ -6,25 +6,29 @@ using Content.Server.Doors.Systems;
using Content.Server.Explosion.Components;
using Content.Server.Flash;
using Content.Server.Flash.Components;
using Content.Server.Projectiles.Components;
using Content.Shared.Audio;
using Content.Shared.Database;
using Content.Shared.Doors;
using Content.Shared.Throwing;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using System.Threading;
using Content.Server.Construction.Components;
using Content.Shared.Trigger;
using Timer = Robust.Shared.Timing.Timer;
using Content.Shared.Physics;
using System.Collections.Generic;
namespace Content.Server.Explosion.EntitySystems
{
/// <summary>
/// Raised whenever something is Triggered on the entity.
/// </summary>
public class TriggerEvent : HandledEntityEventArgs
public sealed class TriggerEvent : HandledEntityEventArgs
{
public EntityUid Triggered { get; }
public EntityUid? User { get; }
@ -40,16 +44,19 @@ namespace Content.Server.Explosion.EntitySystems
public sealed partial class TriggerSystem : EntitySystem
{
[Dependency] private readonly ExplosionSystem _explosions = default!;
[Dependency] private readonly FixtureSystem _fixtures = default!;
[Dependency] private readonly FlashSystem _flashSystem = default!;
[Dependency] private readonly DoorSystem _sharedDoorSystem = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
public override void Initialize()
{
base.Initialize();
InitializeProximity();
InitializeOnUse();
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(HandleCollide);
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(HandleSoundTrigger);
@ -88,11 +95,8 @@ namespace Content.Server.Explosion.EntitySystems
#region Flash
private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args)
{
if (component.Flashed) return;
// TODO Make flash durations sane ffs.
_flashSystem.FlashArea(uid, args.User, component.Range, component.Duration * 1000f);
component.Flashed = true;
}
#endregion
@ -112,15 +116,9 @@ namespace Content.Server.Explosion.EntitySystems
_sharedDoorSystem.TryToggleDoor(uid);
}
private void HandleCollide(EntityUid uid, TriggerOnCollideComponent component, StartCollideEvent args)
private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, StartCollideEvent args)
{
EntityUid? user = null;
if (EntityManager.TryGetComponent(uid, out ProjectileComponent projectile))
user = projectile.Shooter;
else if (EntityManager.TryGetComponent(uid, out ThrownItemComponent thrown))
user = thrown.Thrower;
Trigger(component.Owner, user);
Trigger(component.Owner);
}
@ -144,5 +142,12 @@ namespace Content.Server.Explosion.EntitySystems
Trigger(triggered, user);
});
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateProximity(frameTime);
}
}
}

View file

@ -11,7 +11,5 @@ namespace Content.Server.Flash.Components
{
[DataField("range")] internal float Range = 1.0f;
[DataField("duration")] internal float Duration = 8.0f;
internal bool Flashed;
}
}

View file

@ -0,0 +1,12 @@
// Global usings for Content.Server
global using System;
global using System.Collections.Generic;
global using Robust.Shared.Analyzers;
global using Robust.Shared.Log;
global using Robust.Shared.Localization;
global using Robust.Shared.GameObjects;
global using Robust.Shared.IoC;
global using Robust.Shared.Maths;
global using Robust.Shared.ViewVariables;
global using Robust.Shared.Serialization.Manager.Attributes;

View file

@ -12,6 +12,7 @@ using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@ -28,6 +29,7 @@ namespace Content.Server.NodeContainer.EntitySystems
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly INodeGroupFactory _nodeGroupFactory = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly List<int> _visDeletes = new();
private readonly List<BaseNodeGroup> _visSends = new();
@ -141,6 +143,9 @@ namespace Content.Server.NodeContainer.EntitySystems
var sw = Stopwatch.StartNew();
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var nodeQuery = EntityManager.GetEntityQuery<NodeContainerComponent>();
foreach (var toRemove in _toRemove)
{
if (toRemove.NodeGroup == null)
@ -182,7 +187,11 @@ namespace Content.Server.NodeContainer.EntitySystems
QueueRemakeGroup((BaseNodeGroup) node.NodeGroup);
}
foreach (var compatible in GetCompatibleNodes(node))
// GetCompatibleNodes will involve getting the transform & grid as most connection requirements are
// based on position & anchored neighbours However, here more than one node could be attached to the
// same parent. So there is probably a better way of doing this.
foreach (var compatible in GetCompatibleNodes(node, xformQuery, nodeQuery))
{
ClearReachableIfNecessary(compatible);
@ -303,14 +312,23 @@ namespace Content.Server.NodeContainer.EntitySystems
return allNodes;
}
private static IEnumerable<Node> GetCompatibleNodes(Node node)
private IEnumerable<Node> GetCompatibleNodes(Node node, EntityQuery<TransformComponent> xformQuery, EntityQuery<NodeContainerComponent> nodeQuery)
{
foreach (var reachable in node.GetReachableNodes())
var xform = xformQuery.GetComponent(node.Owner);
_mapManager.TryGetGrid(xform.GridID, out var grid);
if (!node.Connectable(EntityManager, xform))
yield break;
foreach (var reachable in node.GetReachableNodes(xform, nodeQuery, xformQuery, grid, EntityManager))
{
DebugTools.Assert(reachable != node, "GetReachableNodes() should not include self.");
if (reachable.Connectable && reachable.NodeGroupID == node.NodeGroupID)
if (reachable.NodeGroupID == node.NodeGroupID
&& reachable.Connectable(EntityManager, xformQuery.GetComponent(reachable.Owner)))
{
yield return reachable;
}
}
}

View file

@ -12,16 +12,18 @@ namespace Content.Server.NodeContainer.Nodes
[DataDefinition]
public class AdjacentNode : Node
{
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
if (!IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored)
if (!xform.Anchored || grid == null)
yield break;
var entMan = IoCManager.Resolve<IEntityManager>();
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(entMan, grid, gridIndex))
foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex))
{
if (node != this)
yield return node;

View file

@ -3,6 +3,7 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@ -36,7 +37,20 @@ namespace Content.Server.NodeContainer.Nodes
/// <summary>
/// If this node should be considered for connection by other nodes.
/// </summary>
public bool Connectable => !Deleting && Anchored;
public virtual bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
{
if (Deleting)
return false;
if (entMan.IsQueuedForDeletion(Owner))
return false;
if (!NeedAnchored)
return true;
xform ??= entMan.GetComponent<TransformComponent>(Owner);
return xform.Anchored;
}
protected bool Anchored => !NeedAnchored || IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored;
@ -145,6 +159,10 @@ namespace Content.Server.NodeContainer.Nodes
/// of this asymmetric relation are made to manually update with <see cref="NodeGroupSystem.QueueReflood"/>.
/// </para>
/// </remarks>
public abstract IEnumerable<Node> GetReachableNodes();
public abstract IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan);
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
@ -12,11 +12,11 @@ namespace Content.Server.NodeContainer.Nodes
/// </summary>
public static class NodeHelpers
{
public static IEnumerable<Node> GetNodesInTile(IEntityManager entMan, IMapGrid grid, Vector2i coords)
public static IEnumerable<Node> GetNodesInTile(EntityQuery<NodeContainerComponent> nodeQuery, IMapGrid grid, Vector2i coords)
{
foreach (var entityUid in grid.GetAnchoredEntities(coords))
{
if (!entMan.TryGetComponent(entityUid, out NodeContainerComponent? container))
if (!nodeQuery.TryGetComponent(entityUid, out var container))
continue;
foreach (var node in container.Nodes.Values)
@ -27,14 +27,14 @@ namespace Content.Server.NodeContainer.Nodes
}
public static IEnumerable<(Direction dir, Node node)> GetCardinalNeighborNodes(
IEntityManager entMan,
EntityQuery<NodeContainerComponent> nodeQuery,
IMapGrid grid,
Vector2i coords,
bool includeSameTile = true)
{
foreach (var (dir, entityUid) in GetCardinalNeighborCells(grid, coords, includeSameTile))
{
if (!entMan.TryGetComponent(entityUid, out NodeContainerComponent? container))
if (!nodeQuery.TryGetComponent(entityUid, out var container))
continue;
foreach (var node in container.Nodes.Values)
@ -68,22 +68,5 @@ namespace Content.Server.NodeContainer.Nodes
foreach (var uid in grid.GetAnchoredEntities(coords + (-1, 0)))
yield return (Direction.West, uid);
}
public static Vector2i TileOffsetForDir(Direction dir)
{
return dir switch
{
Direction.Invalid => (0, 0),
Direction.South => (0, -1),
Direction.SouthEast => (1, -1),
Direction.East => (1, 0),
Direction.NorthEast => (1, 1),
Direction.North => (0, 1),
Direction.NorthWest => (-1, 1),
Direction.West => (-1, 0),
Direction.SouthWest => (-1, -1),
_ => throw new ArgumentOutOfRangeException(nameof(dir), dir, null)
};
}
}
}

View file

@ -7,6 +7,7 @@ using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@ -89,6 +90,11 @@ namespace Content.Server.NodeContainer.Nodes
[DataField("connectionsEnabled")]
private bool _connectionsEnabled = true;
public override bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
{
return _connectionsEnabled && base.Connectable(entMan, xform);
}
[DataField("rotationsEnabled")]
public bool RotationsEnabled { get; set; } = true;
@ -112,12 +118,6 @@ namespace Content.Server.NodeContainer.Nodes
}
}
public void AssumeAir(GasMixture giver)
{
if(PipeNet != null)
EntitySystem.Get<AtmosphereSystem>().Merge(PipeNet.Air, giver);
}
[ViewVariables]
[DataField("volume")]
public float Volume { get; set; } = DefaultVolume;
@ -150,25 +150,16 @@ namespace Content.Server.NodeContainer.Nodes
UpdateAppearance();
}
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
for (var i = 0; i < PipeDirectionHelpers.AllPipeDirections; i++)
{
var pipeDir = (PipeDirection) (1 << i);
if (!CurrentPipeDirection.HasDirection(pipeDir))
continue;
foreach (var pipe in LinkableNodesInDirection(pipeDir))
{
yield return pipe;
}
}
if(_alwaysReachable != null)
if (_alwaysReachable != null)
{
var remQ = new RemQueue<PipeNode>();
foreach(var pipe in _alwaysReachable)
foreach (var pipe in _alwaysReachable)
{
if (pipe.Deleting)
{
@ -177,64 +168,58 @@ namespace Content.Server.NodeContainer.Nodes
yield return pipe;
}
foreach(var pipe in remQ)
foreach (var pipe in remQ)
{
_alwaysReachable.Remove(pipe);
}
}
if (!xform.Anchored || grid == null)
yield break;
var pos = grid.TileIndicesFor(xform.Coordinates);
for (var i = 0; i < PipeDirectionHelpers.PipeDirections; i++)
{
var pipeDir = (PipeDirection) (1 << i);
if (!CurrentPipeDirection.HasDirection(pipeDir))
continue;
foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, grid, nodeQuery))
{
yield return pipe;
}
}
}
/// <summary>
/// Gets the pipes that can connect to us from entities on the tile or adjacent in a direction.
/// </summary>
private IEnumerable<PipeNode> LinkableNodesInDirection(PipeDirection pipeDir)
private IEnumerable<PipeNode> LinkableNodesInDirection(Vector2i pos, PipeDirection pipeDir, IMapGrid grid,
EntityQuery<NodeContainerComponent> nodeQuery)
{
if (!Anchored)
yield break;
foreach (var pipe in PipesInDirection(pipeDir))
foreach (var pipe in PipesInDirection(pos, pipeDir, grid, nodeQuery))
{
if (pipe.ConnectionsEnabled && pipe.CurrentPipeDirection.HasDirection(pipeDir.GetOpposite()))
if (pipe.NodeGroupID == NodeGroupID
&& pipe.CurrentPipeDirection.HasDirection(pipeDir.GetOpposite()))
{
yield return pipe;
}
}
}
/// <summary>
/// Gets the pipes from entities on the tile adjacent in a direction.
/// </summary>
protected IEnumerable<PipeNode> PipesInDirection(PipeDirection pipeDir)
protected IEnumerable<PipeNode> PipesInDirection(Vector2i pos, PipeDirection pipeDir, IMapGrid grid,
EntityQuery<NodeContainerComponent> nodeQuery)
{
if (!IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored)
yield break;
var offsetPos = pos.Offset(pipeDir.ToDirection());
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
var position = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates;
foreach (var entity in grid.GetInDir(position, pipeDir.ToDirection()))
foreach (var entity in grid.GetAnchoredEntities(offsetPos))
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<NodeContainerComponent>(entity, out var container))
continue;
foreach (var node in container.Nodes.Values)
{
if (node is PipeNode pipe)
yield return pipe;
}
}
}
/// <summary>
/// Gets the pipes from entities on the same tile.
/// </summary>
protected IEnumerable<PipeNode> PipesInTile()
{
if (!IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored)
yield break;
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
var position = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates;
foreach (var entity in grid.GetLocal(position))
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<NodeContainerComponent>(entity, out var container))
if (!nodeQuery.TryGetComponent(entity, out var container))
continue;
foreach (var node in container.Nodes.Values)
@ -271,6 +256,13 @@ namespace Content.Server.NodeContainer.Nodes
{
ConnectedDirections = PipeDirection.None;
var entMan = IoCManager.Resolve<IEntityManager>();
var xform = entMan.GetComponent<TransformComponent>(Owner);
if (!IoCManager.Resolve<IMapManager>().TryGetGrid(xform.GridID, out var grid))
return;
var pos = grid.WorldToTile(xform.WorldPosition);
var query = entMan.GetEntityQuery<NodeContainerComponent>();
for (var i = 0; i < PipeDirectionHelpers.AllPipeDirections; i++)
{
var pipeDir = (PipeDirection) (1 << i);
@ -278,9 +270,9 @@ namespace Content.Server.NodeContainer.Nodes
if (!CurrentPipeDirection.HasDirection(pipeDir))
continue;
foreach (var pipe in LinkableNodesInDirection(pipeDir))
foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, grid, query))
{
if (pipe.Connectable && pipe.NodeGroupID == NodeGroupID)
if (pipe.Connectable(entMan) && pipe.NodeGroupID == NodeGroupID)
{
ConnectedDirections |= pipeDir;
break;
@ -295,11 +287,18 @@ namespace Content.Server.NodeContainer.Nodes
/// </summary>
private void UpdateAdjacentConnectedDirections()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var xform = entMan.GetComponent<TransformComponent>(Owner);
if (!IoCManager.Resolve<IMapManager>().TryGetGrid(xform.GridID, out var grid))
return;
var pos = grid.WorldToTile(xform.WorldPosition);
var query = entMan.GetEntityQuery<NodeContainerComponent>();
for (var i = 0; i < PipeDirectionHelpers.PipeDirections; i++)
{
var pipeDir = (PipeDirection) (1 << i);
foreach (var pipe in LinkableNodesInDirection(pipeDir))
foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, grid, query))
{
pipe.UpdateConnectedDirections();
pipe.UpdateAppearance();

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.NodeContainer.Nodes
@ -6,15 +8,24 @@ namespace Content.Server.NodeContainer.Nodes
[DataDefinition]
public class PortPipeNode : PipeNode
{
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
foreach (var node in PipesInTile())
if (!xform.Anchored || grid == null)
yield break;
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex))
{
if (node is PortablePipeNode)
yield return node;
}
foreach (var node in base.GetReachableNodes())
foreach (var node in base.GetReachableNodes(xform, nodeQuery, xformQuery, grid, entMan))
{
yield return node;
}

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.NodeContainer.Nodes
@ -6,15 +8,24 @@ namespace Content.Server.NodeContainer.Nodes
[DataDefinition]
public class PortablePipeNode : PipeNode
{
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
foreach (var node in PipesInTile())
if (!xform.Anchored || grid == null)
yield break;
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex))
{
if (node is PortPipeNode)
yield return node;
}
foreach (var node in base.GetReachableNodes())
foreach (var node in base.GetReachableNodes(xform, nodeQuery, xformQuery, grid, entMan))
{
yield return node;
}

View file

@ -12,7 +12,7 @@ namespace Content.Server.Nutrition.Components
{
[RegisterComponent]
[Friend(typeof(DrinkSystem))]
public class DrinkComponent : Component
public sealed class DrinkComponent : Component
{
[DataField("solution")]
public string SolutionName { get; set; } = DefaultSolutionName;
@ -41,6 +41,12 @@ namespace Content.Server.Nutrition.Components
[DataField("burstSound")]
public SoundSpecifier BurstSound = new SoundPathSpecifier("/Audio/Effects/flash_bang.ogg");
/// <summary>
/// How long it takes to drink this yourself.
/// </summary>
[DataField("delay")]
public float Delay = 1;
/// <summary>
/// This is how many seconds it takes to force feed someone this drink.
/// </summary>

View file

@ -46,6 +46,12 @@ namespace Content.Server.Nutrition.Components
[DataField("eatMessage")]
public string EatMessage = "food-nom";
/// <summary>
/// How long it takes to eat the food personally.
/// </summary>
[DataField("delay")]
public float Delay = 1;
/// <summary>
/// This is how many seconds it takes to force feed someone this food.
/// Should probably be smaller for small items like pills.

View file

@ -1,25 +1,22 @@
using System.Threading;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using JetBrains.Annotations;
using Robust.Shared.Audio;
@ -54,24 +51,10 @@ namespace Content.Server.Nutrition.EntitySystems
SubscribeLocalEvent<DrinkComponent, ComponentInit>(OnDrinkInit);
SubscribeLocalEvent<DrinkComponent, LandEvent>(HandleLand);
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<DrinkComponent, HandDeselectedEvent>(OnDrinkDeselected);
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<SharedBodyComponent, ForceDrinkEvent>(OnForceDrink);
SubscribeLocalEvent<ForceDrinkCancelledEvent>(OnForceDrinkCancelled);
}
/// <summary>
/// If the user is currently forcing someone do drink, this cancels the attempt if they swap hands or
/// otherwise loose the item. Prevents force-feeding dual-wielding.
/// </summary>
private void OnDrinkDeselected(EntityUid uid, DrinkComponent component, HandDeselectedEvent args)
{
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
component.CancelToken = null;
}
SubscribeLocalEvent<SharedBodyComponent, DrinkEvent>(OnDrink);
SubscribeLocalEvent<DrinkCancelledEvent>(OnDrinkCancelled);
}
public bool IsEmpty(EntityUid uid, DrinkComponent? component = null)
@ -85,9 +68,7 @@ namespace Content.Server.Nutrition.EntitySystems
private void OnExamined(EntityUid uid, DrinkComponent component, ExaminedEvent args)
{
if (!component.Opened || !args.IsInDetailsRange)
{
return;
}
var color = IsEmpty(uid, component) ? "gray" : "yellow";
var openedText =
@ -106,9 +87,7 @@ namespace Content.Server.Nutrition.EntitySystems
component.Opened = opened;
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out _))
{
return;
}
if (EntityManager.TryGetComponent<AppearanceComponent>(uid, out var appearance))
{
@ -125,7 +104,6 @@ namespace Content.Server.Nutrition.EntitySystems
EntityManager.RemoveComponent<RefillableSolutionComponent>(uid);
EntityManager.RemoveComponent<DrainableSolutionComponent>(uid);
}
}
private void AfterInteract(EntityUid uid, DrinkComponent component, AfterInteractEvent args)
@ -133,22 +111,23 @@ namespace Content.Server.Nutrition.EntitySystems
if (args.Handled || args.Target == null || !args.CanReach)
return;
if (args.User == args.Target)
{
args.Handled = TryUseDrink(uid, args.User);
// CanInteract already checked CanInteract
if (!_actionBlockerSystem.CanUse(args.User))
return;
}
if (args.User == args.Target)
args.Handled = TryUseDrink(uid, args.User, component);
else
args.Handled = TryForceDrink(uid, args.User, args.Target.Value, component);
args.Handled = TryDrink(args.User, args.Target.Value, component);
}
private void OnUse(EntityUid uid, DrinkComponent component, UseInHandEvent args)
{
if (args.Handled) return;
if (!args.User.InRangeUnobstructed(uid, popup: true))
{
args.Handled = true;
return;
}
if (!component.Opened)
{
//Do the opening stuff like playing the sounds.
@ -158,13 +137,11 @@ namespace Content.Server.Nutrition.EntitySystems
return;
}
if (_solutionContainerSystem.DrainAvailable(uid) <= 0)
{
args.User.PopupMessage(Loc.GetString("drink-component-on-use-is-empty", ("owner", uid)));
// CanUse already checked; trying to keep it consistent if we interact with ourselves.
if (!_actionBlockerSystem.CanInteract(args.User))
return;
}
args.Handled = TryUseDrink(uid, args.User, component);
args.Handled = TryDrink(args.User, args.User, component);
}
private void HandleLand(EntityUid uid, DrinkComponent component, LandEvent args)
@ -219,94 +196,8 @@ namespace Content.Server.Nutrition.EntitySystems
appearance.SetData(DrinkCanStateVisual.Opened, component.Opened);
}
/// <summary>
/// Attempt to drink some of a drink. Returns true if any interaction took place, including generation of
/// pop-up messages.
/// </summary>
private bool TryUseDrink(EntityUid uid, EntityUid userUid, DrinkComponent? drink = null)
private bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink)
{
if (!Resolve(uid, ref drink))
return false;
// if currently being used to force-feed, cancel that action.
if (drink.CancelToken != null)
{
drink.CancelToken.Cancel();
drink.CancelToken = null;
return true;
}
if (!drink.Opened)
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open",
("owner", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
return true;
}
if (!EntityManager.TryGetComponent(userUid, out SharedBodyComponent? body))
return false;
if (!_solutionContainerSystem.TryGetDrainableSolution((drink).Owner, out var drinkSolution) ||
drinkSolution.DrainAvailable <= 0)
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty",
("entity", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
return true;
}
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(userUid, out var stomachs, body))
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink"),
userUid, Filter.Entities(userUid));
return true;
}
if (_foodSystem.IsMouthBlocked(userUid, userUid))
return true;
var transferAmount = FixedPoint2.Min(drink.TransferAmount, drinkSolution.DrainAvailable);
var drain = _solutionContainerSystem.Drain(uid, drinkSolution, transferAmount);
var firstStomach = stomachs.FirstOrNull(
stomach => _stomachSystem.CanTransferSolution((stomach.Comp).Owner, drain));
// All stomach are full or can't handle whatever solution we have.
if (firstStomach == null)
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough"),
userUid, Filter.Entities(userUid));
if (EntityManager.HasComponent<RefillableSolutionComponent>(uid))
{
_spillableSystem.SpillAt(userUid, drain, "PuddleSmear");
return true;
}
_solutionContainerSystem.Refill(uid, drinkSolution, drain);
return true;
}
SoundSystem.Play(Filter.Pvs(userUid), drink.UseSound.GetSound(), userUid,
AudioParams.Default.WithVolume(-2f));
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-success-slurp"), userUid,
Filter.Pvs(userUid));
drain.DoEntityReaction(userUid, ReactionMethod.Ingestion);
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, drain, firstStomach.Value.Comp);
return true;
}
/// <summary>
/// Attempt to force someone else to drink some of a drink. Returns true if any interaction took place,
/// including generation of pop-up messages.
/// </summary>
private bool TryForceDrink(EntityUid uid, EntityUid userUid, EntityUid targetUid,
DrinkComponent? drink = null)
{
if (!Resolve(uid, ref drink))
return false;
// cannot stack do-afters
if (drink.CancelToken != null)
{
@ -315,69 +206,89 @@ namespace Content.Server.Nutrition.EntitySystems
return true;
}
if (!EntityManager.HasComponent<SharedBodyComponent>(targetUid))
if (!EntityManager.HasComponent<SharedBodyComponent>(target))
return false;
if (!drink.Opened)
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open",
("owner", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
("owner", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), drink.Owner, Filter.Entities(user));
return true;
}
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var drinkSolution) ||
if (!_solutionContainerSystem.TryGetDrainableSolution(drink.Owner, out var drinkSolution) ||
drinkSolution.DrainAvailable <= 0)
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty",
("entity", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
("entity", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), drink.Owner, Filter.Entities(user));
return true;
}
if (_foodSystem.IsMouthBlocked(targetUid, userUid))
if (_foodSystem.IsMouthBlocked(target, user))
return true;
EntityManager.TryGetComponent(userUid, out MetaDataComponent? meta);
var userName = meta?.EntityName ?? string.Empty;
if (!user.InRangeUnobstructed(drink.Owner, popup: true))
return true;
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)),
userUid, Filter.Entities(targetUid));
var forceDrink = user != target;
drink.CancelToken = new();
_doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, drink.ForceFeedDelay, drink.CancelToken.Token, targetUid)
if (forceDrink)
{
EntityManager.TryGetComponent(user, out MetaDataComponent? meta);
var userName = meta?.EntityName ?? string.Empty;
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)),
user, Filter.Entities(target));
// logging
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(drink.Owner):drink} {SolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
drink.CancelToken = new CancellationTokenSource();
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, forceDrink ? drink.ForceFeedDelay : drink.Delay, drink.CancelToken.Token, target)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
MovementThreshold = 1.0f,
TargetFinishedEvent = new ForceDrinkEvent(userUid, drink, drinkSolution),
BroadcastCancelledEvent = new ForceDrinkCancelledEvent(drink),
MovementThreshold = 0.01f,
TargetFinishedEvent = new DrinkEvent(user, drink, drinkSolution),
BroadcastCancelledEvent = new DrinkCancelledEvent(drink),
NeedHand = true,
});
// logging
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(userUid):user} is forcing {ToPrettyString(targetUid):target} to drink {ToPrettyString(uid):drink} {SolutionContainerSystem.ToPrettyString(drinkSolution):solution}");
return true;
}
/// <summary>
/// Raised directed at a victim when someone has force fed them a drink.
/// </summary>
private void OnForceDrink(EntityUid uid, SharedBodyComponent body, ForceDrinkEvent args)
private void OnDrink(EntityUid uid, SharedBodyComponent body, DrinkEvent args)
{
if (args.Drink.Deleted)
return;
args.Drink.CancelToken = null;
var transferAmount = FixedPoint2.Min(args.Drink.TransferAmount, args.DrinkSolution.DrainAvailable);
var drained = _solutionContainerSystem.Drain((args.Drink).Owner, args.DrinkSolution, transferAmount);
var drained = _solutionContainerSystem.Drain(args.Drink.Owner, args.DrinkSolution, transferAmount);
var forceDrink = uid != args.User;
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(uid, out var stomachs, body))
{
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink-other"),
_popupSystem.PopupEntity(
forceDrink ?
Loc.GetString("drink-component-try-use-drink-cannot-drink-other") :
Loc.GetString("drink-component-try-use-drink-had-enough"),
uid, Filter.Entities(args.User));
_spillableSystem.SpillAt(uid, drained, "PuddleSmear");
if (EntityManager.HasComponent<RefillableSolutionComponent>(uid))
{
_spillableSystem.SpillAt(args.User, drained, "PuddleSmear");
return;
}
_solutionContainerSystem.Refill(uid, args.DrinkSolution, drained);
return;
}
@ -394,25 +305,34 @@ namespace Content.Server.Nutrition.EntitySystems
return;
}
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
var targetName = targetMeta?.EntityName ?? string.Empty;
if (forceDrink)
{
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
var targetName = targetMeta?.EntityName ?? string.Empty;
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
var userName = userMeta?.EntityName ?? string.Empty;
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
var userName = userMeta?.EntityName ?? string.Empty;
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed-success", ("user", userName)),
uid, Filter.Entities(uid));
_popupSystem.PopupEntity(
Loc.GetString("drink-component-force-feed-success", ("user", userName)), uid, Filter.Entities(uid));
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed-success-user", ("target", targetName)),
args.User, Filter.Entities(args.User));
_popupSystem.PopupEntity(
Loc.GetString("drink-component-force-feed-success-user", ("target", targetName)),
args.User, Filter.Entities(args.User));
}
else
{
_popupSystem.PopupEntity(
Loc.GetString("drink-component-try-use-drink-success-slurp"), args.User, Filter.Pvs(args.User));
}
SoundSystem.Play(Filter.Pvs(uid), args.Drink.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-2f));
drained.DoEntityReaction(uid, ReactionMethod.Ingestion);
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, drained, firstStomach.Value.Comp);
_stomachSystem.TryTransferSolution(firstStomach.Value.Comp.Owner, drained, firstStomach.Value.Comp);
}
private void OnForceDrinkCancelled(ForceDrinkCancelledEvent args)
private static void OnDrinkCancelled(DrinkCancelledEvent args)
{
args.Drink.CancelToken = null;
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.EntitySystems;
@ -22,7 +23,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using Content.Shared.Hands;
using Content.Shared.Inventory;
using Content.Shared.Item;
@ -31,7 +31,7 @@ namespace Content.Server.Nutrition.EntitySystems
/// <summary>
/// Handles feeding attempts both on yourself and on the target.
/// </summary>
internal class FoodSystem : EntitySystem
public sealed class FoodSystem : EntitySystem
{
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
@ -49,26 +49,12 @@ namespace Content.Server.Nutrition.EntitySystems
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand);
SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
SubscribeLocalEvent<FoodComponent, HandDeselectedEvent>(OnFoodDeselected);
SubscribeLocalEvent<FoodComponent, GetInteractionVerbsEvent>(AddEatVerb);
SubscribeLocalEvent<SharedBodyComponent, ForceFeedEvent>(OnForceFeed);
SubscribeLocalEvent<ForceFeedCancelledEvent>(OnForceFeedCancelled);
SubscribeLocalEvent<SharedBodyComponent, FeedEvent>(OnFeed);
SubscribeLocalEvent<ForceFeedCancelledEvent>(OnFeedCancelled);
SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(OnInventoryIngestAttempt);
}
/// <summary>
/// If the user is currently force feeding someone, this cancels the attempt if they swap hands or otherwise
/// loose the item. Prevents force-feeding dual-wielding.
/// </summary>
private void OnFoodDeselected(EntityUid uid, FoodComponent component, HandDeselectedEvent args)
{
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
component.CancelToken = null;
}
}
/// <summary>
/// Eat item
/// </summary>
@ -77,7 +63,7 @@ namespace Content.Server.Nutrition.EntitySystems
if (ev.Handled)
return;
ev.Handled = TryUseFood(uid, ev.User);
ev.Handled = TryFeed(ev.User, ev.User, foodComponent);
}
/// <summary>
@ -88,27 +74,15 @@ namespace Content.Server.Nutrition.EntitySystems
if (args.Handled || args.Target == null || !args.CanReach)
return;
if (args.User == args.Target)
{
args.Handled = TryUseFood(uid, args.User);
return;
}
args.Handled = TryForceFeed(uid, args.User, args.Target.Value);
args.Handled = TryFeed(args.User, args.Target.Value, foodComponent);
}
/// <summary>
/// Tries to eat some food
/// </summary>
/// <param name="uid">Food entity.</param>
/// <param name="user">Feeding initiator.</param>
/// <returns>True if an interaction occurred (i.e., food was consumed, or a pop-up message was created)</returns>
public bool TryUseFood(EntityUid uid, EntityUid user, FoodComponent? food = null)
public bool TryFeed(EntityUid user, EntityUid target, FoodComponent food)
{
if (!Resolve(uid, ref food))
if (!_actionBlockerSystem.CanInteract(user) || !_actionBlockerSystem.CanUse(user))
return false;
// if currently being used to force-feed, cancel that action.
// if currently being used to feed, cancel that action.
if (food.CancelToken != null)
{
food.CancelToken.Cancel();
@ -116,73 +90,134 @@ namespace Content.Server.Nutrition.EntitySystems
return true;
}
if (uid == user || //Suppresses self-eating
EntityManager.TryGetComponent<MobStateComponent>(uid, out var mobState) && mobState.IsAlive()) // Suppresses eating alive mobs
if (food.Owner == user || //Suppresses self-eating
EntityManager.TryGetComponent<MobStateComponent>(food.Owner, out var mobState) && mobState.IsAlive()) // Suppresses eating alive mobs
return false;
if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var solution))
// Target can't be fed
if (!EntityManager.HasComponent<SharedBodyComponent>(target))
return false;
if (!_solutionContainerSystem.TryGetSolution(food.Owner, food.SolutionName, out var foodSolution))
return false;
if (food.UsesRemaining <= 0)
{
_popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", uid)), user, Filter.Entities(user));
_popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty",
("entity", food.Owner)), user, Filter.Entities(user));
DeleteAndSpawnTrash(food, user);
return true;
return false;
}
if (!EntityManager.TryGetComponent(user, out SharedBodyComponent ? body) ||
!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(user, out var stomachs, body))
if (IsMouthBlocked(target, user))
return false;
if (IsMouthBlocked(user, user))
{
if (!TryGetRequiredUtensils(user, food, out var utensils))
return false;
if (!user.InRangeUnobstructed(food.Owner, popup: true))
return true;
var forceFeed = user != target;
food.CancelToken = new CancellationTokenSource();
if (forceFeed)
{
EntityManager.TryGetComponent(user, out MetaDataComponent? meta);
var userName = meta?.EntityName ?? string.Empty;
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
user, Filter.Entities(target));
// logging
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food.Owner):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}");
}
var usedUtensils = new List<UtensilComponent>();
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, forceFeed ? food.ForceFeedDelay : food.Delay, food.CancelToken.Token, target)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
MovementThreshold = 0.01f,
TargetFinishedEvent = new FeedEvent(user, food, foodSolution, utensils),
BroadcastCancelledEvent = new ForceFeedCancelledEvent(food),
NeedHand = true,
});
if (!TryGetRequiredUtensils(user, food, out var utensils))
return true;
return true;
if (!user.InRangeUnobstructed(uid, popup: true))
return true;
}
var transferAmount = food.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) food.TransferAmount, solution.CurrentVolume) : solution.CurrentVolume;
var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount);
private void OnFeed(EntityUid uid, SharedBodyComponent body, FeedEvent args)
{
if (args.Food.Deleted)
return;
args.Food.CancelToken = null;
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(uid, out var stomachs, body))
return;
var transferAmount = args.Food.TransferAmount != null
? FixedPoint2.Min((FixedPoint2) args.Food.TransferAmount, args.FoodSolution.CurrentVolume)
: args.FoodSolution.CurrentVolume;
var split = _solutionContainerSystem.SplitSolution((args.Food).Owner, args.FoodSolution, transferAmount);
var firstStomach = stomachs.FirstOrNull(
stomach => _stomachSystem.CanTransferSolution((stomach.Comp).Owner, split));
var forceFeed = uid != args.User;
// No stomach so just popup a message that they can't eat.
if (firstStomach == null)
{
_solutionContainerSystem.TryAddSolution(uid, solution, split);
_popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more"), user, Filter.Entities(user));
return true;
_solutionContainerSystem.TryAddSolution(uid, args.FoodSolution, split);
_popupSystem.PopupEntity(
forceFeed ?
Loc.GetString("food-system-you-cannot-eat-any-more-other") :
Loc.GetString("food-system-you-cannot-eat-any-more")
, uid, Filter.Entities(args.User));
return;
}
// TODO: Account for partial transfer.
split.DoEntityReaction(user, ReactionMethod.Ingestion);
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, split, firstStomach.Value.Comp);
split.DoEntityReaction(uid, ReactionMethod.Ingestion);
_stomachSystem.TryTransferSolution(firstStomach.Value.Comp.Owner, split, firstStomach.Value.Comp);
SoundSystem.Play(Filter.Pvs(user), food.UseSound.GetSound(), user, AudioParams.Default.WithVolume(-1f));
_popupSystem.PopupEntity(Loc.GetString(food.EatMessage, ("food", food.Owner)), user, Filter.Entities(user));
if (forceFeed)
{
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
var targetName = targetMeta?.EntityName ?? string.Empty;
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
var userName = userMeta?.EntityName ?? string.Empty;
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName)),
uid, Filter.Entities(uid));
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)),
args.User, Filter.Entities(args.User));
}
else
{
_popupSystem.PopupEntity(Loc.GetString(args.Food.EatMessage, ("food", args.Food.Owner)), args.User, Filter.Entities(args.User));
}
SoundSystem.Play(Filter.Pvs(uid), args.Food.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-1f));
// Try to break all used utensils
foreach (var utensil in usedUtensils)
foreach (var utensil in args.Utensils)
{
_utensilSystem.TryBreak((utensil).Owner, user);
_utensilSystem.TryBreak((utensil).Owner, args.User);
}
if (food.UsesRemaining > 0)
{
return true;
}
if (args.Food.UsesRemaining > 0)
return;
if (string.IsNullOrEmpty(food.TrashPrototype))
EntityManager.QueueDeleteEntity(food.Owner);
if (string.IsNullOrEmpty(args.Food.TrashPrototype))
EntityManager.QueueDeleteEntity(args.Food.Owner);
else
DeleteAndSpawnTrash(food, user);
return true;
DeleteAndSpawnTrash(args.Food, args.User);
}
private void DeleteAndSpawnTrash(FoodComponent component, EntityUid? user = null)
@ -229,7 +264,7 @@ namespace Content.Server.Nutrition.EntitySystems
{
Act = () =>
{
TryUseFood(uid, ev.User, component);
TryFeed(uid, ev.User, component);
},
Text = Loc.GetString("food-system-verb-eat"),
Priority = -1
@ -238,131 +273,12 @@ namespace Content.Server.Nutrition.EntitySystems
ev.Verbs.Add(verb);
}
/// <summary>
/// Attempts to force feed a target. Returns true if any interaction occurred, including pop-up generation
/// </summary>
public bool TryForceFeed(EntityUid uid, EntityUid user, EntityUid target, FoodComponent? food = null)
{
if (!Resolve(uid, ref food))
return false;
// if currently being used to force-feed, cancel that action.
if (food.CancelToken != null)
{
food.CancelToken.Cancel();
food.CancelToken = null;
return true;
}
if (!EntityManager.HasComponent<SharedBodyComponent>(target))
return false;
if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution))
return false;
if (food.UsesRemaining <= 0)
{
_popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty",
("entity", uid)), user, Filter.Entities(user));
DeleteAndSpawnTrash(food, user);
return true;
}
if (IsMouthBlocked(target, user))
{
return true;
}
if (!TryGetRequiredUtensils(user, food, out var utensils))
return true;
EntityManager.TryGetComponent(user, out MetaDataComponent? meta);
var userName = meta?.EntityName ?? string.Empty;
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
user, Filter.Entities(target));
food.CancelToken = new();
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, food.ForceFeedDelay, food.CancelToken.Token, target)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
MovementThreshold = 1.0f,
TargetFinishedEvent = new ForceFeedEvent(user, food, foodSolution, utensils),
BroadcastCancelledEvent = new ForceFeedCancelledEvent(food)
});
// logging
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(uid):food} {SolutionContainerSystem.ToPrettyString(foodSolution):solution}");
return true;
}
private void OnForceFeed(EntityUid uid, SharedBodyComponent body, ForceFeedEvent args)
{
if (args.Food.Deleted)
return;
args.Food.CancelToken = null;
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(uid, out var stomachs, body))
return;
var transferAmount = args.Food.TransferAmount != null
? FixedPoint2.Min((FixedPoint2) args.Food.TransferAmount, args.FoodSolution.CurrentVolume)
: args.FoodSolution.CurrentVolume;
var split = _solutionContainerSystem.SplitSolution((args.Food).Owner, args.FoodSolution, transferAmount);
var firstStomach = stomachs.FirstOrNull(
stomach => _stomachSystem.CanTransferSolution((stomach.Comp).Owner, split));
if (firstStomach == null)
{
_solutionContainerSystem.TryAddSolution(uid, args.FoodSolution, split);
_popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more-other"), uid, Filter.Entities(args.User));
return;
}
split.DoEntityReaction(uid, ReactionMethod.Ingestion);
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, split, firstStomach.Value.Comp);
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
var targetName = targetMeta?.EntityName ?? string.Empty;
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
var userName = userMeta?.EntityName ?? string.Empty;
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName)),
uid, Filter.Entities(uid));
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)),
args.User, Filter.Entities(args.User));
SoundSystem.Play(Filter.Pvs(uid), args.Food.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-1f));
// Try to break all used utensils
foreach (var utensil in args.Utensils)
{
_utensilSystem.TryBreak((utensil).Owner, args.User);
}
if (args.Food.UsesRemaining > 0)
return;
if (string.IsNullOrEmpty(args.Food.TrashPrototype))
EntityManager.QueueDeleteEntity((args.Food).Owner);
else
DeleteAndSpawnTrash(args.Food, args.User);
}
/// <summary>
/// Force feeds someone remotely. Does not require utensils (well, not the normal type anyways).
/// </summary>
public void ProjectileForceFeed(EntityUid uid, EntityUid target, EntityUid? user, FoodComponent? food = null, BodyComponent? body = null)
{
// TODO: Combine with regular feeding because holy code duplication batman.
if (!Resolve(uid, ref food, false) || !Resolve(target, ref body, false))
return;
@ -398,7 +314,7 @@ namespace Content.Server.Nutrition.EntitySystems
SoundSystem.Play(Filter.Pvs(target), food.UseSound.GetSound(), target, AudioParams.Default.WithVolume(-1f));
if (string.IsNullOrEmpty(food.TrashPrototype))
EntityManager.QueueDeleteEntity(((IComponent) food).Owner);
EntityManager.QueueDeleteEntity(food.Owner);
else
DeleteAndSpawnTrash(food);
}
@ -406,7 +322,7 @@ namespace Content.Server.Nutrition.EntitySystems
private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component,
out List<UtensilComponent> utensils, HandsComponent? hands = null)
{
utensils = new();
utensils = new List<UtensilComponent>();
if (component.Utensil != UtensilType.None)
return true;
@ -441,7 +357,7 @@ namespace Content.Server.Nutrition.EntitySystems
return true;
}
private void OnForceFeedCancelled(ForceFeedCancelledEvent args)
private static void OnFeedCancelled(ForceFeedCancelledEvent args)
{
args.Food.CancelToken = null;
}

View file

@ -54,7 +54,7 @@ namespace Content.Server.Nutrition.EntitySystems
if (!user.InRangeUnobstructed(target, popup: true))
return false;
return _foodSystem.TryUseFood(target, user);
return _foodSystem.TryFeed(user, target, food);
}
/// <summary>

View file

@ -17,16 +17,16 @@ public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
}
/// <summary>
/// Raised directed at the food after a successful force-feed do-after.
/// Raised directed at the food after a successful feed do-after.
/// </summary>
public sealed class ForceFeedEvent : EntityEventArgs
public sealed class FeedEvent : EntityEventArgs
{
public readonly EntityUid User;
public readonly FoodComponent Food;
public readonly Solution FoodSolution;
public readonly List<UtensilComponent> Utensils;
public ForceFeedEvent(EntityUid user, FoodComponent food, Solution foodSolution, List<UtensilComponent> utensils)
public FeedEvent(EntityUid user, FoodComponent food, Solution foodSolution, List<UtensilComponent> utensils)
{
User = user;
Food = food;
@ -51,13 +51,13 @@ public sealed class ForceFeedCancelledEvent : EntityEventArgs
/// <summary>
/// Raised directed at the drink after a successful force-drink do-after.
/// </summary>
public sealed class ForceDrinkEvent : EntityEventArgs
public sealed class DrinkEvent : EntityEventArgs
{
public readonly EntityUid User;
public readonly DrinkComponent Drink;
public readonly Solution DrinkSolution;
public ForceDrinkEvent(EntityUid user, DrinkComponent drink, Solution drinkSolution)
public DrinkEvent(EntityUid user, DrinkComponent drink, Solution drinkSolution)
{
User = user;
Drink = drink;
@ -68,11 +68,11 @@ public sealed class ForceDrinkEvent : EntityEventArgs
/// <summary>
/// Raised directed at the food after a failed force-dink do-after.
/// </summary>
public sealed class ForceDrinkCancelledEvent : EntityEventArgs
public sealed class DrinkCancelledEvent : EntityEventArgs
{
public readonly DrinkComponent Drink;
public ForceDrinkCancelledEvent(DrinkComponent drink)
public DrinkCancelledEvent(DrinkComponent drink)
{
Drink = drink;
}

View file

@ -72,6 +72,9 @@ namespace Content.Server.Physics.Controllers
worldPos += Convey(direction, speed, frameTime, itemRelative);
transform.WorldPosition = worldPos;
if (TryComp<PhysicsComponent>(entity, out var body))
body.Awake = true;
}
}

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
@ -13,20 +13,18 @@ namespace Content.Server.Power.Nodes
[DataDefinition]
public class CableDeviceNode : Node
{
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
if (!Anchored)
if (!xform.Anchored || grid == null)
yield break;
var entMan = IoCManager.Resolve<IEntityManager>();
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
// If we're in an invalid grid, such as grid 0, we cannot connect to anything.
if(!IoCManager.Resolve<IMapManager>().TryGetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID, out var grid))
yield break;
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
foreach (var node in NodeHelpers.GetNodesInTile(entMan, grid, gridIndex))
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex))
{
if (node is CableNode)
yield return node;

View file

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
@ -12,21 +12,23 @@ namespace Content.Server.Power.Nodes
[DataDefinition]
public class CableNode : Node
{
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
if (!Anchored)
if (!xform.Anchored || grid == null)
yield break;
var entMan = IoCManager.Resolve<IEntityManager>();
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
// While we go over adjacent nodes, we build a list of blocked directions due to
// incoming or outgoing wire terminals.
var terminalDirs = 0;
List<(Direction, Node)> nodeDirs = new();
foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(entMan, grid, gridIndex))
foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex))
{
if (node is CableNode && node != this)
{
@ -44,11 +46,11 @@ namespace Content.Server.Power.Nodes
if (dir == Direction.Invalid)
{
// On own tile, block direction it faces
terminalDirs |= 1 << (int) IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(node.Owner).LocalRotation.GetCardinalDir();
terminalDirs |= 1 << (int) xformQuery.GetComponent(node.Owner).LocalRotation.GetCardinalDir();
}
else
{
var terminalDir = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(node.Owner).LocalRotation.GetCardinalDir();
var terminalDir = xformQuery.GetComponent(node.Owner).LocalRotation.GetCardinalDir();
if (terminalDir.GetOpposite() == dir)
{
// Target tile has a terminal towards us, block the direction.

View file

@ -1,8 +1,9 @@
using System.Collections.Generic;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Power.Nodes
@ -10,29 +11,27 @@ namespace Content.Server.Power.Nodes
[DataDefinition]
public class CableTerminalNode : CableDeviceNode
{
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
if (!Anchored)
if (!xform.Anchored || grid == null)
yield break;
if (IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID == GridId.Invalid)
yield break; // No funny nodes in spess.
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
var dir = xform.LocalRotation.GetDir();
var targetIdx = gridIndex.Offset(dir);
var entMan = IoCManager.Resolve<IEntityManager>();
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
var dir = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).LocalRotation.GetDir();
var targetIdx = gridIndex + NodeHelpers.TileOffsetForDir(dir);
foreach (var node in NodeHelpers.GetNodesInTile(entMan, grid, targetIdx))
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx))
{
if (node is CableTerminalPortNode)
yield return node;
}
foreach (var node in base.GetReachableNodes())
foreach (var node in base.GetReachableNodes(xform, nodeQuery, xformQuery, grid, entMan))
{
yield return node;
}

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
@ -10,19 +10,18 @@ namespace Content.Server.Power.Nodes
[DataDefinition]
public class CableTerminalPortNode : Node
{
public override IEnumerable<Node> GetReachableNodes()
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery,
IMapGrid? grid,
IEntityManager entMan)
{
if (!Anchored)
if (!xform.Anchored || grid == null)
yield break;
if (IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID == GridId.Invalid)
yield break; // No funny nodes in spess.
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
var entMan = IoCManager.Resolve<IEntityManager>();
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
var nodes = NodeHelpers.GetCardinalNeighborNodes(entMan, grid, gridIndex, includeSameTile: false);
var nodes = NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex, includeSameTile: false);
foreach (var (_, node) in nodes)
{
if (node is CableTerminalNode)

View file

@ -0,0 +1,21 @@
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
namespace Content.Server.Remotes
{
[RegisterComponent]
[Friend(typeof(DoorRemoteSystem))]
public class DoorRemoteComponent : Component
{
public override string Name => "DoorRemote";
public OperatingMode Mode = OperatingMode.OpenClose;
public enum OperatingMode : byte
{
OpenClose,
ToggleBolts
// ToggleEmergencyAccess
}
}
}

View file

@ -0,0 +1,93 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Physics;
using Content.Shared.Access.Components;
using Content.Server.Doors.Systems;
using Content.Server.Doors.Components;
namespace Content.Server.Remotes
{
public sealed class DoorRemoteSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedDoorSystem _sharedDoorSystem = default!;
[Dependency] private readonly DoorSystem _doorSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DoorRemoteComponent, UseInHandEvent>(OnInHandActivation);
SubscribeLocalEvent<DoorRemoteComponent, AfterInteractEvent>(OnAfterInteract);
}
public void OnInHandActivation(EntityUid user, DoorRemoteComponent component, UseInHandEvent args)
{
switch (component.Mode)
{
case DoorRemoteComponent.OperatingMode.OpenClose:
component.Mode = DoorRemoteComponent.OperatingMode.ToggleBolts;
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-toggle-bolts"), args.User, Filter.Entities(args.User));
break;
case DoorRemoteComponent.OperatingMode.ToggleBolts:
component.Mode = DoorRemoteComponent.OperatingMode.OpenClose; // TODO: Swítch to ToggleEmergencyAcces when EA is implemented
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-open-close"), args.User, Filter.Entities(args.User)); // TODO: See the above comment
break;
/*
case DoorRemoteComponent.OperatingMode.ToggleEmergencyAccess:
component.Mode = DoorRemoteComponent.OperatingMode.OpenClose;
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-open-close"), args.User, Filter.Entities(args.User));
break;
*/
}
}
private void OnAfterInteract(EntityUid uid, DoorRemoteComponent component, AfterInteractEvent args)
{
if (args.Handled
|| args.Target == null
|| !TryComp<DoorComponent>(args.Target, out var doorComponent) // If it isn't a door we don't use it
|| !HasComp<AccessReaderComponent>(args.Target) // Remotes do not work on doors without access requirements
|| !TryComp<AirlockComponent>(args.Target, out var airlockComponent) // Remotes only work on airlocks
|| !_interactionSystem.InRangeUnobstructed(args.User, doorComponent.Owner, -1f, CollisionGroup.Opaque))
{
return;
}
args.Handled = true;
if (component.Mode == DoorRemoteComponent.OperatingMode.OpenClose)
{
_sharedDoorSystem.TryToggleDoor(doorComponent.Owner, user: args.Used);
}
if (component.Mode == DoorRemoteComponent.OperatingMode.ToggleBolts
&& airlockComponent.IsPowered())
{
if (_doorSystem.HasAccess(doorComponent.Owner, args.Used))
{
airlockComponent.SetBoltsWithAudio(!airlockComponent.IsBolted());
}
else
{
if (doorComponent.State != DoorState.Open)
{
_sharedDoorSystem.Deny(airlockComponent.Owner, user: args.User);
}
else if (doorComponent.DenySound != null)
{
SoundSystem.Play(Filter.Pvs(args.Target.Value), doorComponent.DenySound.GetSound(), args.Target.Value);
}
}
}
}
}
}

View file

@ -1,5 +1,4 @@
using System;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@ -10,20 +9,20 @@ namespace Content.Shared.Chemistry.Components
/// <summary>
/// Shared class for injectors & syringes
/// </summary>
[NetworkedComponent()]
public class SharedInjectorComponent : Component
[NetworkedComponent, ComponentProtoName("Injector")]
public abstract class SharedInjectorComponent : Component
{
/// <summary>
/// Component data used for net updates. Used by client for item status ui
/// </summary>
[Serializable, NetSerializable]
protected sealed class InjectorComponentState : ComponentState
public sealed class InjectorComponentState : ComponentState
{
public FixedPoint2 CurrentVolume { get; }
public FixedPoint2 TotalVolume { get; }
public InjectorToggleMode CurrentMode { get; }
public InjectorComponentState(FixedPoint2 currentVolume, FixedPoint2 totalVolume, InjectorToggleMode currentMode)
public InjectorComponentState(FixedPoint2 currentVolume, FixedPoint2 totalVolume, SharedInjectorComponent.InjectorToggleMode currentMode)
{
CurrentVolume = currentVolume;
TotalVolume = totalVolume;
@ -31,7 +30,7 @@ namespace Content.Shared.Chemistry.Components
}
}
public enum InjectorToggleMode
public enum InjectorToggleMode : byte
{
Inject,
Draw

View file

@ -1,25 +1,21 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Crayon
{
[NetworkedComponent()]
public class SharedCrayonComponent : Component
[NetworkedComponent, ComponentProtoName("Crayon")]
public abstract class SharedCrayonComponent : Component
{
public string SelectedState { get; set; } = string.Empty;
[DataField("color")]
protected string _color = "white";
[DataField("color")] public string _color = "white";
[Serializable, NetSerializable]
public enum CrayonUiKey
public enum CrayonUiKey : byte
{
Key,
}

View file

@ -0,0 +1,15 @@
// There isn't really a 'default place' to put these,
// so a file in the project top level directory it is
global using System;
global using System.Collections.Generic;
global using Robust.Shared.Analyzers;
global using Robust.Shared.Log;
global using Robust.Shared.Localization;
global using Robust.Shared.GameObjects;
global using Robust.Shared.IoC;
global using Robust.Shared.Maths;
global using Robust.Shared.ViewVariables;
global using Robust.Shared.Serialization.Manager.Attributes;

View file

@ -30,6 +30,7 @@ namespace Content.Shared.Pulling
public class SharedPullingStateManagementSystem : EntitySystem
{
[Dependency] private readonly SharedJointSystem _jointSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
public override void Initialize()
{
@ -116,7 +117,7 @@ namespace Content.Shared.Pulling
pullable.Puller = puller.Owner;
// Joint startup
var union = pullerPhysics.GetWorldAABB().Union(pullablePhysics.GetWorldAABB());
var union = _physics.GetHardAABB(pullerPhysics).Union(_physics.GetHardAABB(pullablePhysics));
var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
pullable.PullJoint = _jointSystem.CreateDistanceJoint(pullablePhysics.Owner, pullerPhysics.Owner, id:$"pull-joint-{pullablePhysics.Owner}");

View file

@ -1,18 +1,29 @@
using System;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization;
namespace Content.Shared.Trigger
{
[NetSerializable]
[Serializable]
public enum TriggerVisuals
[Serializable, NetSerializable]
public enum ProximityTriggerVisuals : byte
{
Off,
Inactive,
Active,
}
[Serializable, NetSerializable]
public enum ProximityTriggerVisualState : byte
{
State,
}
[Serializable, NetSerializable]
public enum TriggerVisuals : byte
{
VisualState,
}
[NetSerializable]
[Serializable]
public enum TriggerVisualState
[Serializable, NetSerializable]
public enum TriggerVisualState : byte
{
Primed,
Unprimed,

View file

@ -1,64 +1,4 @@
Entries:
- author: metalgearsloth
changes:
- {message: Station will now start with a random rotation., type: Tweak}
id: 483
time: '2021-10-25T04:22:57.0000000+00:00'
- author: mirrorcult
changes:
- {message: Walking over slippery items (usually) won't slip you anymore., type: Tweak}
- {message: Turned-on magboots now stop you from slipping., type: Add}
id: 484
time: '2021-10-25T06:43:49.0000000+00:00'
- author: 20kdc
changes:
- {message: Construction works on rotated grids, type: Fix}
id: 485
time: '2021-10-25T14:14:21.0000000+00:00'
- author: Zumorica
changes:
- {message: Added electrocution., type: Add}
- {message: Cutting cables without insulated gloves shocks you., type: Tweak}
- {message: Grilles can now be electrified by placing a cable below them., type: Tweak}
id: 486
time: '2021-10-25T14:21:56.0000000+00:00'
- author: ike709
changes:
- {message: Stacks can now be split in half by alt-clicking or into different amounts
with the new Split verb., type: Add}
id: 487
time: '2021-10-25T14:36:04.0000000+00:00'
- author: Zumorica
changes:
- {message: Added insulated gloves (and their cheap knockoffs) to the station.,
type: Add}
id: 488
time: '2021-10-25T14:58:04.292881+00:00'
- author: 20kdc
changes:
- {message: ChemMaster bottles are no longer 'empty', type: Fix}
id: 489
time: '2021-10-25T14:58:16.0000000+00:00'
- author: 20kdc
changes:
- {message: 'Wall lights can be placed again, APCs place where they should', type: Fix}
id: 490
time: '2021-10-26T14:15:02.0000000+00:00'
- author: metalgearsloth
changes:
- {message: Construction recipes are now sorted alphabetically., type: Tweak}
id: 491
time: '2021-10-26T14:18:43.0000000+00:00'
- author: Zumorica
changes:
- {message: Fixed not being able to construct or deconstruct tables., type: Fix}
id: 492
time: '2021-10-26T14:38:03.0000000+00:00'
- author: Zumorica
changes:
- {message: Fix computer dupe bug., type: Fix}
id: 493
time: '2021-10-26T23:30:17.397968+00:00'
- author: 20kdc
changes:
- {message: Morgue trays now extend to the proper position., type: Fix}
@ -2934,7 +2874,7 @@ Entries:
time: '2022-02-04T23:17:11.0000000+00:00'
- author: Lamrr
changes:
- {message: 'The digital audio workstaiton [sic] has been relabelled.', type: Fix}
- {message: The digital audio workstaiton (sic) has been relabelled., type: Fix}
id: 981
time: '2022-02-05T01:23:43.0000000+00:00'
- author: Willhelm53
@ -2953,3 +2893,65 @@ Entries:
of just discarding your wrench.', type: Tweak}
id: 983
time: '2022-02-05T02:39:02.0000000+00:00'
- author: walrusfromspace
changes:
- {message: Added door remotes to the heads' lockers, type: Add}
id: 984
time: '2022-02-05T13:33:24.0000000+00:00'
- author: ElectroJr
changes:
- {message: Windoors should no longer crush people as they auto close., type: Fix}
id: 985
time: '2022-02-05T17:27:06.0000000+00:00'
- author: Peptide90
changes:
- {message: Blast doors can't be opened with a crowbar anymore., type: Tweak}
id: 986
time: '2022-02-05T18:06:33.0000000+00:00'
- author: router
changes:
- {message: Replaced all ingot mints to fix a typo in their stamping. All "Nanotransen"
gold and silver bars have been melted down and reformed., type: Fix}
- {message: Removed Herobrine., type: Remove}
id: 987
time: '2022-02-05T19:23:06.0000000+00:00'
- author: Peptide90
changes:
- {message: Massive list of changes and fixes for NSS Pillar. See Github for list.,
type: Fix}
id: 988
time: '2022-02-05T21:27:43.0000000+00:00'
- author: Rane
changes:
- {message: Brig access is now the least privileged form of security access and
given to lawyers. It may take a while for stations to update their airlock software
to the latest version., type: Tweak}
id: 989
time: '2022-02-05T23:08:15.0000000+00:00'
- author: moony
changes:
- {message: Hopefully fix pillar spawning., type: Fix}
id: 990
time: '2022-02-06T02:22:28.0000000+00:00'
- author: mirrorcult
changes:
- {message: Directional windows no longer look weird when viewed while rotated.,
type: Tweak}
id: 991
time: '2022-02-06T02:25:13.0000000+00:00'
- author: Peptide90
changes:
- {message: 'NSS Pillar minor update! Check out changes to security, permabrig and
salvage among other things.', type: Tweak}
id: 992
time: '2022-02-06T12:56:41.0000000+00:00'
- author: lajolico
changes:
- {message: Lasers can now pass through glass airlocks, type: Add}
id: 993
time: '2022-02-06T13:48:31.0000000+00:00'
- author: scaly-chimp
changes:
- {message: adds a portable flasher for security to lock down areas, type: Add}
id: 994
time: '2022-02-06T15:59:42.0000000+00:00'

View file

@ -1,6 +1,5 @@
## Entity
plant-holder-component-empty-seed-packet-message = The packet seems to be empty. You throw it away.
plant-holder-component-plant-success-message = You plant the {$seedName} {$seedNoun}.
plant-holder-component-already-seeded-message = The {$name} already has seeds in it!
plant-holder-component-remove-weeds-message = You remove the weeds from the {$name}.
@ -32,4 +31,4 @@ plant-holder-component-toxins-high-warning = The [color=red]toxicity level alert
plant-holder-component-light-improper-warning = The [color=yellow]improper light level alert[/color] is blinking.
plant-holder-component-heat-improper-warning = The [color=orange]improper temperature level alert[/color] is blinking.
plant-holder-component-pressure-improper-warning = The [color=lightblue]improper environment pressure alert[/color] is blinking.
plant-holder-component-gas-missing-warning = The [color=cyan]improper gas environment alert[/color] is blinking.
plant-holder-component-gas-missing-warning = The [color=cyan]improper gas environment alert[/color] is blinking.

View file

@ -1,6 +1,5 @@
## Entity
seed-component-no-seeds-message = It doesn't seem to contain any seeds.
seed-component-description = It has a picture of [color=yellow]{$seedName}[/color] on the front.
seed-component-has-variety-tag = It's tagged as variety [color=lightgray]no. {$seedUid}[/color].
seed-component-plant-yield-text = Plant Yield: [color=lightblue]{$seedYield}[/color]
@ -9,4 +8,4 @@ seed-component-plant-potency-text = Plant Potency: [color=lightblue]{$seedPotenc
botany-seed-packet-name = packet of {$seedName} {$seedNoun}
botany-harvest-fail-message = You fail to harvest anything useful.
botany-harvest-success-message = You harvest from the {$name}
botany-mysterious-description-addon = On second thought, something about this one looks strange.
botany-mysterious-description-addon = On second thought, something about this one looks strange.

View file

@ -0,0 +1,3 @@
door-remote-switch-state-open-close = You switch the remote to open and close doors
door-remote-switch-state-toggle-bolts = You switch the remote to toggle bolts
door-remote-switch-state-toggle-emergency-access = You switch the remote to toggle emergency access

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,7 @@
prob: 1
- id: CigPackGreen
prob: 0.50
- id: DoorRemoteCargo
- type: entity
id: LockerCaptainFilled
@ -63,6 +64,7 @@
- id: CigarGoldCase
prob: 0.25
- id: ClothingBeltSheathFilled
- id: DoorRemoteCommand
- type: entity
id: LockerHeadOfPersonnelFilled
@ -94,6 +96,7 @@
- id: CigarGoldCase
prob: 0.10
# Fuck the HoP they don't deserve fucking cigars.
- id: DoorRemoteService
- type: entity
id: LockerChiefEngineerFilled
@ -114,6 +117,7 @@
- id: ClothingHandsGlovesColorYellow
- id: CigarCase
prob: 0.15
- id: DoorRemoteEngineering
- type: entity
id: LockerChiefMedicalOfficerFilled
@ -135,6 +139,7 @@
- id: ClothingOuterHardsuitMedical
- id: Hypospray
- id: HandheldCrewMonitor
- id: DoorRemoteMedical
- type: entity
id: LockerResearchDirectorFilled
@ -157,6 +162,7 @@
prob: 1
- id: PlushieSlime
prob: 0.1
- id: DoorRemoteResearch
- type: entity
id: LockerHeadOfSecurityFilled
@ -205,3 +211,4 @@
prob: 1
- id: CigarGoldCase
prob: 0.50
- id: DoorRemoteSecurity

View file

@ -13,6 +13,7 @@
path: /Audio/Effects/Fluids/splat.ogg
recolor: true
- type: Clickable
- type: Evaporation
- type: Slippery
- type: Physics
- type: Fixtures
@ -67,7 +68,6 @@
state: smear-0
netsync: false
- type: Puddle
- type: Evaporation
- type: Appearance
visuals:
- type: PuddleVisualizer
@ -84,7 +84,6 @@
state: splatter-0
netsync: false
- type: Puddle
- type: Evaporation
- type: Appearance
visuals:
- type: PuddleVisualizer
@ -146,7 +145,6 @@
state: writing-0
netsync: false
- type: Puddle
- type: Evaporation
evaporateTime: 10
- type: Appearance
visuals:

View file

@ -7,9 +7,6 @@
abstract: true
components:
- type: Food
- type: ItemCooldown
- type: UseDelay
delay: 1.5
# This base type is used to cover all of the "obvious" things that should be doable to open-package food.
# Practically this means injection.

View file

@ -20,9 +20,6 @@
interfaces:
- key: enum.TransferAmountUiKey.Key
type: TransferAmountBoundUserInterface
- type: ItemCooldown
- type: UseDelay
delay: 1.0
- type: entity
parent: DrinkBase

View file

@ -38,8 +38,6 @@
tags:
- Trash
- type: ItemCooldown
- type: UseDelay
delay: 1.0
- type: entity
parent: DrinkCanBaseFull

View file

@ -29,8 +29,6 @@
- type: Spillable
solution: drink
- type: ItemCooldown
- type: UseDelay
delay: 1.0
- type: entity
parent: DrinkBaseCup

View file

@ -0,0 +1,137 @@
- type: entity
parent: BaseItem
id: DoorRemoteDefault
name: door remote
description: A gadget which can open and bolt doors remotely
abstract: true
components:
- type: Sprite
sprite: Objects/Devices/door_remote.rsi
netsync: false
- type: Access
- type: DoorRemote
- type: entity
parent: DoorRemoteDefault
id: DoorRemoteCommand
name: command door remote
components:
- type: Sprite
layers:
- state: door_remotebase
- state: door_remotelightscolour
color: "#e6e600"
- state: door_remotescreencolour
color: "#9f9f00"
- type: Access
tags:
- Captain
- HeadOfPersonnel
- Command
- type: entity
parent: DoorRemoteDefault
id: DoorRemoteSecurity
name: security door remote
components:
- type: Sprite
layers:
- state: door_remotebase
- state: door_remotelightscolour
color: "#cb0000"
- state: door_remotescreencolour
color: "#830000"
- type: Access
tags:
- HeadOfSecurity
- Security
- type: entity
parent: DoorRemoteDefault
id: DoorRemoteService
name: service door remote
components:
- type: Sprite
layers:
- state: door_remotebase
- state: door_remotelightscolour
color: "#58c800"
- state: door_remotescreencolour
color: "#3a7231"
- type: Access
tags:
- HeadOfPersonnel
- Service
- Bar
- Janitor
- Theatre
- Kitchen
- Hydroponics
- type: entity
parent: DoorRemoteDefault
id: DoorRemoteResearch
name: research door remote
components:
- type: Sprite
layers:
- state: door_remotebase
- state: door_remotelightscolour
color: "#a53aaa"
- state: door_remotescreencolour
color: "#652368"
- type: Access
tags:
- ResearchDirector
- Research
- type: entity
parent: DoorRemoteDefault
id: DoorRemoteCargo
name: cargo door remote
components:
- type: Sprite
layers:
- state: door_remotebase
- state: door_remotelightscolour
color: "#b18644"
- state: door_remotescreencolour
color: "#5b4523"
- type: Access
tags:
- Cargo
- Quartermaster
- type: entity
parent: DoorRemoteDefault
id: DoorRemoteMedical
name: medical door remote
components:
- type: Sprite
layers:
- state: door_remotebase
- state: door_remotelightscolour
color: "#68aed6"
- state: door_remotescreencolour
color: "#325f7a"
- type: Access
tags:
- Medical
- ChiefMedicalOfficer
- type: entity
parent: DoorRemoteDefault
id: DoorRemoteEngineering
name: engineering door remote
components:
- type: Sprite
layers:
- state: door_remotebase
- state: door_remotelightscolour
color: "#ffa62b"
- state: door_remotescreencolour
color: "#bc5b00"
- type: Access
tags:
- ChiefEngineer
- Engineering

View file

@ -2,7 +2,7 @@
abstract: true
parent: BaseItem
id: IngotBase
description: A heavy metal ingot stamped with the Nanotransen logo.
description: A heavy metal ingot stamped with the Nanotrasen logo.
components:
- type: Sprite
netsync: false

View file

@ -49,3 +49,53 @@
size: 2
sprite: Objects/Weapons/Melee/flash.rsi
- type: ItemCooldown
- type: entity
name: portable flasher
parent: BaseStructure
id: Portableflasher
description: An ultrabright flashbulb with a proximity trigger, useful for making an area security-only.
components:
- type: SoundOnTrigger
sound:
path: /Audio/Weapons/flash.ogg
- type: FlashOnTrigger
repeating: true
range: 3
- type: TriggerOnProximity
enabled: true
cooldown: 5
shape:
!type:PhysShapeCircle
radius: 2
- type: Anchorable
- type: Sprite
netsync: false
sprite: Objects/Weapons/pflash.rsi
layers:
- state: "off"
map: ["enum.ProximityTriggerVisualLayers.Base"]
- type: InteractionOutline
- type: Physics
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.15,-0.3,0.15,0.3"
mask:
- Impassable
- VaultImpassable
- SmallImpassable
layer:
- MobImpassable
mass: 70
- type: Appearance
visuals:
- type: ProximityTriggerVisualizer
animationState: flashing
- type: PointLight
energy: 2.0
radius: 0
softness: 0
enabled: true

View file

@ -29,7 +29,7 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/medical.rsi
- type: entity
parent: Airlock
id: AirlockChemistry
@ -86,6 +86,21 @@
enabled: false
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/glass.rsi
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.49,-0.49,0.49,0.49" # don't want this colliding with walls or they won't close
mass: 100
mask:
- Impassable
layer: #removed opaque from the layer, allowing lasers to pass through glass airlocks
- Impassable
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: entity
parent: AirlockGlass

View file

@ -29,7 +29,7 @@
bounds: "-0.49,-0.49,0.49,0.49" # don't want this colliding with walls or they won't close
mass: 100
mask:
- MobImpassable
- Impassable
layer:
- Opaque
- Impassable

View file

@ -15,6 +15,7 @@
closeTimeTwo: 0.4
openTimeOne: 0.4
openTimeTwo: 0.4
pryTime: -1
crushDamage:
types:
Blunt: 25 # yowch

View file

@ -71,10 +71,8 @@
# needed so that windoors will close regardless of whether there are people in it; it doesn't crush after all
safety: false
- type: Door
canCrush: false
weldable: false
crushDamage:
types:
Blunt: 10
openSound:
path: /Audio/Machines/windoor_open.ogg
closeSound:

View file

@ -34,7 +34,7 @@
components:
- type: AccessReader
access: [["Medical"]]
- type: entity
parent: WindoorSecure
id: WindoorChemistryLocked
@ -78,7 +78,7 @@
access: [["Security"]]
- type: Wires
LayoutId: WindoorSecurity
- type: entity
parent: WindoorSecurityLocked
id: WindoorBrigLocked
@ -86,7 +86,15 @@
components:
- type: AccessReader
access: [["Brig"]]
- type: entity
parent: WindoorSecurityLocked
id: WindoorArmoryLocked
suffix: Armory, Locked
components:
- type: AccessReader
access: [["Armory"]]
- type: entity
parent: WindoorSecure
id: WindoorEngineeringLocked
@ -94,7 +102,7 @@
components:
- type: AccessReader
access: [["Engineering"]]
- type: entity
parent: WindoorSecure
id: WindoorChapelLocked
@ -102,7 +110,7 @@
components:
- type: AccessReader
access: [["Chapel"]]
- type: entity
parent: WindoorSecure
id: WindoorJanitorLocked
@ -110,11 +118,11 @@
components:
- type: AccessReader
access: [["Janitor"]]
- type: entity
parent: WindoorSecure
id: WindoorKitchenLocked
suffix: Kitchen, Locked
components:
- type: AccessReader
access: [["Kitchen"]]
access: [["Kitchen"]]

View file

@ -32,7 +32,7 @@
state_open: qm_open
state_closed: qm_door
- type: AccessReader
access: [["Quartermaster"]]
access: [["Quartermaster"]]
# Command
- type: entity
@ -286,7 +286,7 @@
state_open: warden_open
state_closed: warden_door
- type: AccessReader
access: [["Brig"]]
access: [["Armory"]]
# Security Officer
- type: entity

View file

@ -43,6 +43,7 @@
prefixCreator: 'VG'
mapPath: /Maps/packedstation.yml
minPlayers: 15
maxPlayers: 55
conditions:
- !type:HolidayMapCondition
inverted: true
@ -103,30 +104,29 @@
!type:NanotrasenNameGenerator
prefixCreator: '14'
mapPath: /Maps/nss_pillar.yml
minPlayers: 20
votable: false
minPlayers: 40
overflowJobs:
- Assistant
availableJobs:
Assistant: [ -1, -1 ]
CargoTechnician: [ 4, 5 ]
Bartender: [ 1, 1 ]
Botanist: [ 2, 2 ]
Chef: [ 2, 2 ]
Clown: [ 1, 1 ]
Janitor: [ 2, 3 ]
Mime: [ 1, 1 ]
CargoTechnician: [ 4, 6 ]
Bartender: [ 2, 2 ]
Botanist: [ 2, 4 ]
Chef: [ 2, 4 ]
Clown: [ 1, 2 ]
Janitor: [ 2, 4 ]
Mime: [ 1, 2 ]
Captain: [ 1, 1 ]
HeadOfPersonnel: [ 1, 1 ]
ChiefEngineer: [ 1, 1 ]
StationEngineer: [ 4, 6 ]
StationEngineer: [ 6, 10 ]
ChiefMedicalOfficer: [ 1, 1 ]
MedicalDoctor: [ 4, 6 ]
Chemist: [ 2, 2 ]
MedicalDoctor: [ 6, 10 ]
Chemist: [ 2, 3 ]
ResearchDirector: [ 1, 1 ]
Scientist: [ 3, 5 ]
Scientist: [ 3, 8 ]
HeadOfSecurity: [ 1, 1 ]
SecurityOfficer: [ 5, 6 ]
SecurityOfficer: [ 6, 10 ]
Chaplain: [ 1, 1 ]
Librarian: [ 1, 1 ]
Lawyer: [ 1, 2 ]

View file

@ -8,6 +8,7 @@
supervisors: "the head of personnel"
access:
- Service
- Brig
- Maintenance
- type: startingGear

View file

@ -9,6 +9,7 @@
canBeAntag: false
access:
- Security
- Brig
- Maintenance
- Service

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,20 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Command remote copied from tgstation, rest are recolors",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "door_remotebase"
},
{
"name": "door_remotelightscolour"
},
{
"name": "door_remotescreencolour"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,30 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/f349b842c84f500399bd5673e5e34a6bc45b001a",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "off"
},
{
"name": "on"
},
{
"name": "on-unlit"
},
{
"name": "flashing",
"delays": [
[
0.1,
0.1,
0.1
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 724 B

View file

@ -9,27 +9,27 @@
"states": [
{
"name": "window",
"directions": 8
"directions": 4
},
{
"name": "reinforced_window",
"directions": 8
"directions": 4
},
{
"name": "tinted_window",
"directions": 8
"directions": 4
},
{
"name": "frosted_window",
"directions": 8
"directions": 4
},
{
"name": "plasma_window",
"directions": 8
"directions": 4
},
{
"name": "plasma_reinforced_window",
"directions": 8
"directions": 4
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 635 B

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