Merge remote-tracking branch 'upstream/stable' into ed-27-05-2025-upstream-sync

# Conflicts:
#	.github/CODEOWNERS
#	Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs
#	Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
#	Content.Server/Procedural/DungeonJob/DungeonJob.OreDunGen.cs
#	Resources/Prototypes/Entities/Effects/chemistry_effects.yml
#	Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml
#	Resources/Prototypes/GameRules/meteorswarms.yml
#	Resources/Prototypes/Procedural/dungeon_configs.yml
This commit is contained in:
Ed
2025-05-27 12:21:14 +03:00
1165 changed files with 76878 additions and 44718 deletions

View File

@@ -1,9 +1,8 @@
using Content.Server.Access.Components;
using Content.Server.Humanoid.Systems;
using Content.Server.PDA;
using Content.Shared.Inventory;
using Content.Shared.Mind.Components;
using Content.Shared.PDA;
using Content.Shared.Roles;
namespace Content.Server.Access.Systems;
@@ -17,10 +16,10 @@ public sealed class IdBindSystem : EntitySystem
{
base.Initialize();
//Activate on mind being added
SubscribeLocalEvent<IdBindComponent, MindAddedMessage>(TryBind);
SubscribeLocalEvent<IdBindComponent, MapInitEvent>(TryBind, after: [typeof(RandomHumanoidSystem)]);
}
private void TryBind(Entity<IdBindComponent> ent, ref MindAddedMessage args)
private void TryBind(Entity<IdBindComponent> ent, ref MapInitEvent args)
{
if (!_cardSystem.TryFindIdCard(ent, out var cardId))
return;
@@ -31,9 +30,9 @@ public sealed class IdBindSystem : EntitySystem
if (!ent.Comp.BindPDAOwner)
{
//Remove after running once
RemCompDeferred<IdBindComponent>(ent);
return;
//Remove after running once
RemCompDeferred<IdBindComponent>(ent);
return;
}
//Get PDA from main slot and set us as owner

View File

@@ -187,6 +187,6 @@ public sealed class PanicBunkerMinOverallMinutesCommand : LocalizedCommands
}
_cfg.SetCVar(CCVars.PanicBunkerMinOverallMinutes, minutes);
shell.WriteLine(Loc.GetString("panicbunker-command-overall-minutes-age-set", ("minutes", minutes)));
shell.WriteLine(Loc.GetString("panicbunker-command-min-overall-minutes-set", ("minutes", minutes)));
}
}

View File

@@ -0,0 +1,68 @@
using Content.Shared.Administration;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Debug)]
public sealed class StripAllCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
public override string Command => "stripall";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine(Loc.GetString("shell-need-exactly-one-argument"));
return;
}
if (!NetEntity.TryParse(args[0], out var targetUidNet) || !EntityManager.TryGetEntity(targetUidNet, out var targetEntity))
{
shell.WriteLine(Loc.GetString("shell-entity-uid-must-be-number"));
return;
}
if (!EntityManager.TryGetComponent<InventoryComponent>(targetEntity, out var inventory))
{
shell.WriteLine(Loc.GetString("shell-entity-target-lacks-component", ("componentName", nameof(InventoryComponent))));
return;
}
var slots = _inventorySystem.GetSlotEnumerator((targetEntity.Value, inventory));
while (slots.NextItem(out _, out var slot))
{
_inventorySystem.TryUnequip(targetEntity.Value, targetEntity.Value, slot.Name, true, true, inventory: inventory);
}
if (EntityManager.TryGetComponent<HandsComponent>(targetEntity, out var hands))
{
foreach (var hand in _handsSystem.EnumerateHands(targetEntity.Value, hands))
{
_handsSystem.TryDrop(targetEntity.Value,
hand,
checkActionBlocker: false,
doDropInteraction: false,
handsComp: hands);
}
}
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.Components<InventoryComponent>(args[0]),
Loc.GetString("cmd-stripall-player-completion"));
}
return CompletionResult.Empty;
}
}

View File

@@ -4,9 +4,10 @@ using Content.Server.Administration.UI;
using Content.Server.Disposal.Tube;
using Content.Server.EUI;
using Content.Server.Ghost.Roles;
using Content.Server.Mind;
using Content.Server.Mind.Commands;
using Content.Server.Mind;
using Content.Server.Prayer;
using Content.Server.Silicons.Laws;
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
@@ -15,26 +16,26 @@ using Content.Shared.Configurable;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Mind.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Popups;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Silicons.StationAi;
using Content.Shared.Verbs;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.Silicons.Laws;
using Content.Shared.Movement.Components;
using Content.Shared.Silicons.Laws.Components;
using Robust.Server.Player;
using Content.Shared.Silicons.StationAi;
using Robust.Shared.Physics.Components;
using static Content.Shared.Configurable.ConfigurationComponent;
namespace Content.Server.Administration.Systems
@@ -463,19 +464,34 @@ namespace Content.Server.Administration.Systems
args.Verbs.Add(verb);
}
// Set clothing verb
if (_groupController.CanCommand(player, "setoutfit") &&
EntityManager.HasComponent<InventoryComponent>(args.Target))
if (TryComp<InventoryComponent>(args.Target, out var inventoryComponent))
{
Verb verb = new()
// Strip all verb
if (_groupController.CanCommand(player, "stripall"))
{
Text = Loc.GetString("set-outfit-verb-get-data-text"),
Category = VerbCategory.Debug,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
Act = () => _euiManager.OpenEui(new SetOutfitEui(GetNetEntity(args.Target)), player),
Impact = LogImpact.Medium
};
args.Verbs.Add(verb);
args.Verbs.Add(new Verb
{
Text = Loc.GetString("strip-all-verb-get-data-text"),
Category = VerbCategory.Debug,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
Act = () => _console.RemoteExecuteCommand(player, $"stripall \"{args.Target}\""),
Impact = LogImpact.Medium
});
}
// set outfit verb
if (_groupController.CanCommand(player, "setoutfit"))
{
Verb verb = new()
{
Text = Loc.GetString("set-outfit-verb-get-data-text"),
Category = VerbCategory.Debug,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
Act = () => _euiManager.OpenEui(new SetOutfitEui(GetNetEntity(args.Target)), player),
Impact = LogImpact.Medium
};
args.Verbs.Add(verb);
}
}
// In range unoccluded verb

View File

@@ -1,21 +0,0 @@
using Content.Server.Atmos.EntitySystems;
namespace Content.Server.Atmos.Components
{
[RegisterComponent]
public sealed partial class AtmosPlaqueComponent : Component
{
[DataField("plaqueType")] public PlaqueType Type = PlaqueType.Unset;
[ViewVariables(VVAccess.ReadWrite)]
public PlaqueType TypeVV
{
get => Type;
set
{
Type = value;
IoCManager.Resolve<IEntityManager>().System<AtmosPlaqueSystem>().UpdateSign(Owner, this);
}
}
}
}

View File

@@ -1,89 +0,0 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos.Visuals;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
namespace Content.Server.Atmos.EntitySystems;
public sealed class AtmosPlaqueSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AtmosPlaqueComponent, MapInitEvent>(OnPlaqueMapInit);
}
private void OnPlaqueMapInit(EntityUid uid, AtmosPlaqueComponent component, MapInitEvent args)
{
var rand = _random.Next(100);
// Let's not pat ourselves on the back too hard.
// 1% chance of zumos
if (rand == 0) component.Type = PlaqueType.Zumos;
// 9% FEA
else if (rand <= 10) component.Type = PlaqueType.Fea;
// 45% ZAS
else if (rand <= 55) component.Type = PlaqueType.Zas;
// 45% LINDA
else component.Type = PlaqueType.Linda;
UpdateSign(uid, component);
}
public void UpdateSign(EntityUid uid, AtmosPlaqueComponent component)
{
var metaData = MetaData(uid);
var val = component.Type switch
{
PlaqueType.Zumos =>
Loc.GetString("atmos-plaque-component-desc-zum"),
PlaqueType.Fea =>
Loc.GetString("atmos-plaque-component-desc-fea"),
PlaqueType.Linda =>
Loc.GetString("atmos-plaque-component-desc-linda"),
PlaqueType.Zas =>
Loc.GetString("atmos-plaque-component-desc-zas"),
PlaqueType.Unset => Loc.GetString("atmos-plaque-component-desc-unset"),
_ => Loc.GetString("atmos-plaque-component-desc-unset"),
};
_metaData.SetEntityDescription(uid, val, metaData);
var val1 = component.Type switch
{
PlaqueType.Zumos =>
Loc.GetString("atmos-plaque-component-name-zum"),
PlaqueType.Fea =>
Loc.GetString("atmos-plaque-component-name-fea"),
PlaqueType.Linda =>
Loc.GetString("atmos-plaque-component-name-linda"),
PlaqueType.Zas =>
Loc.GetString("atmos-plaque-component-name-zas"),
PlaqueType.Unset => Loc.GetString("atmos-plaque-component-name-unset"),
_ => Loc.GetString("atmos-plaque-component-name-unset"),
};
_metaData.SetEntityName(uid, val1, metaData);
if (TryComp<AppearanceComponent>(uid, out var appearance))
{
var state = component.Type == PlaqueType.Zumos ? "zumosplaque" : "atmosplaque";
_appearance.SetData(uid, AtmosPlaqueVisuals.State, state, appearance);
}
}
}
// If you get the ZUM plaque it means your round will be blessed with good engineering luck.
public enum PlaqueType : byte
{
Unset = 0,
Zumos,
Fea,
Linda,
Zas
}

View File

@@ -81,7 +81,10 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
private void OnTileChanged(ref TileChangedEvent ev)
{
InvalidateTile(ev.NewTile.GridUid, ev.NewTile.GridIndices);
foreach (var change in ev.Changes)
{
InvalidateTile(ev.Entity.Owner, change.GridIndices);
}
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)

View File

@@ -1,8 +1,7 @@
using Content.Server.Atmos.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.Maps;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Events;
namespace Content.Server.Atmos.EntitySystems;
@@ -12,40 +11,29 @@ namespace Content.Server.Atmos.EntitySystems;
/// </summary>
public sealed class AutomaticAtmosSystem : EntitySystem
{
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
SubscribeLocalEvent<MapGridComponent, MassDataChangedEvent>(OnMassDataChanged);
}
private void OnTileChanged(ref TileChangedEvent ev)
private void OnMassDataChanged(Entity<MapGridComponent> ent, ref MassDataChangedEvent ev)
{
// Only if a atmos-holding tile has been added or removed.
// Also, these calls are surprisingly slow.
// TODO: Make tiledefmanager cache the IsSpace property, and turn this lookup-through-two-interfaces into
// TODO: a simple array lookup, as tile IDs are likely contiguous, and there's at most 2^16 possibilities anyway.
var oldSpace = ev.OldTile.IsSpace(_tileDefinitionManager);
var newSpace = ev.NewTile.IsSpace(_tileDefinitionManager);
if (!(oldSpace && !newSpace ||
!oldSpace && newSpace) ||
_atmosphereSystem.HasAtmosphere(ev.Entity))
return;
if (!TryComp<PhysicsComponent>(ev.Entity, out var physics))
if (_atmosphereSystem.HasAtmosphere(ent))
return;
// We can't actually count how many tiles there are efficiently, so instead estimate with the mass.
if (physics.Mass / ShuttleSystem.TileMassMultiplier >= 7.0f)
if (ev.NewMass / ShuttleSystem.TileDensityMultiplier >= 7.0f)
{
AddComp<GridAtmosphereComponent>(ev.Entity);
Log.Info($"Giving grid {ev.Entity} GridAtmosphereComponent.");
AddComp<GridAtmosphereComponent>(ent);
Log.Info($"Giving grid {ent} GridAtmosphereComponent.");
}
// It's not super important to remove it should the grid become too small again.
// If explosions ever gain the ability to outright shatter grids, do rethink this.
return;
}
}

View File

@@ -47,4 +47,10 @@ public sealed partial class AirAlarmComponent : Component
/// </summary>
[DataField("normalPort", customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
public string NormalPort = "AirNormal";
/// <summary>
/// Whether the panic wire is cut, forcing the alarm into panic mode.
/// </summary>
[DataField, ViewVariables]
public bool PanicWireCut;
}

View File

@@ -466,11 +466,17 @@ public sealed class AirAlarmSystem : EntitySystem
/// <param name="uiOnly">Whether this change is for the UI only, or if it changes the air alarm's operating mode. Defaults to true.</param>
public void SetMode(EntityUid uid, string origin, AirAlarmMode mode, bool uiOnly = true, AirAlarmComponent? controller = null)
{
if (!Resolve(uid, ref controller) || controller.CurrentMode == mode)
if (!Resolve(uid, ref controller))
{
return;
}
if (controller.PanicWireCut)
{
mode = AirAlarmMode.Panic;
}
controller.CurrentMode = mode;
// setting it to UI only means we don't have
@@ -652,6 +658,7 @@ public sealed class AirAlarmSystem : EntitySystem
}
foreach (var (addr, data) in alarm.ScrubberData)
{
data.AirAlarmPanicWireCut = alarm.PanicWireCut;
dataToSend.Add((addr, data));
}
foreach (var (addr, data) in alarm.SensorData)
@@ -669,7 +676,7 @@ public sealed class AirAlarmSystem : EntitySystem
_ui.SetUiState(
uid,
SharedAirAlarmInterfaceKey.Key,
new AirAlarmUIState(devNet.Address, deviceCount, pressure, temperature, dataToSend, alarm.CurrentMode, highestAlarm.Value, alarm.AutoMode));
new AirAlarmUIState(devNet.Address, deviceCount, pressure, temperature, dataToSend, alarm.CurrentMode, highestAlarm.Value, alarm.AutoMode, alarm.PanicWireCut));
}
private const float Delay = 8f;

View File

@@ -30,6 +30,7 @@ public sealed partial class AirAlarmPanicWire : ComponentWireAction<AirAlarmComp
public override bool Cut(EntityUid user, Wire wire, AirAlarmComponent comp)
{
comp.PanicWireCut = true;
if (EntityManager.TryGetComponent<DeviceNetworkComponent>(wire.Owner, out var devNet))
{
_airAlarmSystem.SetMode(wire.Owner, devNet.Address, AirAlarmMode.Panic, false);
@@ -40,6 +41,7 @@ public sealed partial class AirAlarmPanicWire : ComponentWireAction<AirAlarmComp
public override bool Mend(EntityUid user, Wire wire, AirAlarmComponent alarm)
{
alarm.PanicWireCut = false;
if (EntityManager.TryGetComponent<DeviceNetworkComponent>(wire.Owner, out var devNet)
&& alarm.CurrentMode == AirAlarmMode.Panic)
{

View File

@@ -1,20 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Atmos.Piping.Binary.Components
{
[RegisterComponent]
public sealed partial class GasValveComponent : Component
{
[DataField("open")]
public bool Open { get; set; } = true;
[DataField("inlet")]
public string InletName { get; set; } = "inlet";
[DataField("outlet")]
public string OutletName { get; set; } = "outlet";
[DataField("valveSound")]
public SoundSpecifier ValveSound { get; private set; } = new SoundCollectionSpecifier("valveSqueak");
}
}

View File

@@ -1,93 +1,34 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Binary.Systems;
using Content.Shared.Audio;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
namespace Content.Server.Atmos.Piping.Binary.EntitySystems
namespace Content.Server.Atmos.Piping.Binary.EntitySystems;
public sealed class GasValveSystem : SharedGasValveSystem
{
[UsedImplicitly]
public sealed class GasValveSystem : EntitySystem
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
public override void Set(EntityUid uid, GasValveComponent component, bool value)
{
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
base.Set(uid, component, value);
public override void Initialize()
if (_nodeContainer.TryGetNodes(uid, component.InletName, component.OutletName, out PipeNode? inlet, out PipeNode? outlet))
{
base.Initialize();
SubscribeLocalEvent<GasValveComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<GasValveComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<GasValveComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(Entity<GasValveComponent> ent, ref ExaminedEvent args)
{
var valve = ent.Comp;
if (!Comp<TransformComponent>(ent).Anchored || !args.IsInDetailsRange) // Not anchored? Out of range? No status.
return;
if (Loc.TryGetString("gas-valve-system-examined", out var str,
("statusColor", valve.Open ? "green" : "orange"),
("open", valve.Open)))
if (component.Open)
{
args.PushMarkup(str);
inlet.AddAlwaysReachable(outlet);
outlet.AddAlwaysReachable(inlet);
_ambientSoundSystem.SetAmbience(uid, true);
}
}
private void OnStartup(EntityUid uid, GasValveComponent component, ComponentStartup args)
{
// We call set in startup so it sets the appearance, node state, etc.
Set(uid, component, component.Open);
}
private void OnActivate(EntityUid uid, GasValveComponent component, ActivateInWorldEvent args)
{
if (args.Handled || !args.Complex)
return;
Toggle(uid, component);
_audio.PlayPvs(component.ValveSound, uid, AudioParams.Default.WithVariation(0.25f));
args.Handled = true;
}
public void Set(EntityUid uid, GasValveComponent component, bool value)
{
component.Open = value;
if (_nodeContainer.TryGetNodes(uid, component.InletName, component.OutletName, out PipeNode? inlet, out PipeNode? outlet))
else
{
if (TryComp<AppearanceComponent>(uid, out var appearance))
{
_appearance.SetData(uid, FilterVisuals.Enabled, component.Open, appearance);
}
if (component.Open)
{
inlet.AddAlwaysReachable(outlet);
outlet.AddAlwaysReachable(inlet);
_ambientSoundSystem.SetAmbience(uid, true);
}
else
{
inlet.RemoveAlwaysReachable(outlet);
outlet.RemoveAlwaysReachable(inlet);
_ambientSoundSystem.SetAmbience(uid, false);
}
inlet.RemoveAlwaysReachable(outlet);
outlet.RemoveAlwaysReachable(inlet);
_ambientSoundSystem.SetAmbience(uid, false);
}
}
public void Toggle(EntityUid uid, GasValveComponent component)
{
Set(uid, component, !component.Open);
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.DeviceLinking.Events;
namespace Content.Server.Atmos.Piping.Binary.EntitySystems;

View File

@@ -11,6 +11,7 @@ namespace Content.Server.Atmos.Piping.EntitySystems;
public sealed class AtmosPipeAppearanceSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override void Initialize()
{
@@ -55,10 +56,11 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
// find the cardinal directions of any connected entities
var netConnectedDirections = PipeDirection.None;
var tile = grid.TileIndicesFor(xform.Coordinates);
var tile = _map.TileIndicesFor((xform.GridUid.Value, grid), xform.Coordinates);
foreach (var neighbour in connected)
{
var otherTile = grid.TileIndicesFor(Transform(neighbour).Coordinates);
// TODO z-levels, pipes across grids - we shouldn't assume that the neighboring tile's transform is on the same grid
var otherTile = _map.TileIndicesFor((xform.GridUid.Value, grid), Transform(neighbour).Coordinates);
netConnectedDirections |= (otherTile - tile) switch
{

View File

@@ -11,7 +11,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
{
[ViewVariables(VVAccess.ReadWrite)]
public bool Enabled { get; set; } = true;
public bool Enabled = true;
/// <summary>
/// Target volume to transfer. If <see cref="WideNet"/> is enabled, actual transfer rate will be much higher.
@@ -25,15 +25,14 @@ namespace Content.Server.Atmos.Piping.Unary.Components
private float _transferRate = 50;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxTransferRate")]
[DataField]
public float MaxTransferRate = Atmospherics.MaxTransferRate;
[DataField("maxPressure")]
[DataField]
[GuidebookData]
public float MaxPressure { get; set; } = GasVolumePumpComponent.DefaultHigherThreshold;
public float MaxPressure = GasVolumePumpComponent.DefaultHigherThreshold;
[DataField("inlet")]
public string InletName { get; set; } = "pipe";
public string InletName = "pipe";
}
}

View File

@@ -1,80 +0,0 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Unary.Components
{
[RegisterComponent]
public sealed partial class GasThermoMachineComponent : Component
{
[DataField("inlet")]
public string InletName = "pipe";
/// <summary>
/// Current electrical power consumption, in watts. Increasing power increases the ability of the
/// thermomachine to heat or cool air.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float HeatCapacity = 5000;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float TargetTemperature = Atmospherics.T20C;
/// <summary>
/// Tolerance for temperature setpoint hysteresis.
/// </summary>
[GuidebookData]
[DataField, ViewVariables(VVAccess.ReadOnly)]
public float TemperatureTolerance = 2f;
/// <summary>
/// Implements setpoint hysteresis to prevent heater from rapidly cycling on and off at setpoint.
/// If true, add Sign(Cp)*TemperatureTolerance to the temperature setpoint.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public bool HysteresisState;
/// <summary>
/// Coefficient of performance. Output power / input power.
/// Positive for heaters, negative for freezers.
/// </summary>
[DataField("coefficientOfPerformance")]
[ViewVariables(VVAccess.ReadWrite)]
public float Cp = 0.9f; // output power / input power, positive is heat
/// <summary>
/// Current minimum temperature
/// Ignored if heater.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MinTemperature = 73.15f;
/// <summary>
/// Current maximum temperature
/// Ignored if freezer.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MaxTemperature = 593.15f;
/// <summary>
/// Last amount of energy added/removed from the attached pipe network
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float LastEnergyDelta;
/// <summary>
/// An percentage of the energy change that is leaked into the surrounding environment rather than the inlet pipe.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float EnergyLeakPercentage;
/// <summary>
/// If true, heat is exclusively exchanged with the local atmosphere instead of the inlet pipe air
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool Atmospheric = false;
}
}

View File

@@ -9,11 +9,8 @@ using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Content.Server.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Atmos.Piping.Unary.Systems;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Examine;
@@ -22,36 +19,23 @@ using Content.Shared.DeviceNetwork.Components;
namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
[UsedImplicitly]
public sealed class GasThermoMachineSystem : EntitySystem
public sealed class GasThermoMachineSystem : SharedGasThermoMachineSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasThermoMachineComponent, AtmosDeviceUpdateEvent>(OnThermoMachineUpdated);
SubscribeLocalEvent<GasThermoMachineComponent, ExaminedEvent>(OnExamined);
// UI events
SubscribeLocalEvent<GasThermoMachineComponent, BeforeActivatableUIOpenEvent>(OnBeforeOpened);
SubscribeLocalEvent<GasThermoMachineComponent, GasThermomachineToggleMessage>(OnToggleMessage);
SubscribeLocalEvent<GasThermoMachineComponent, GasThermomachineChangeTemperatureMessage>(OnChangeTemperature);
// Device network
SubscribeLocalEvent<GasThermoMachineComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
}
private void OnBeforeOpened(Entity<GasThermoMachineComponent> ent, ref BeforeActivatableUIOpenEvent args)
{
DirtyUI(ent, ent.Comp);
}
private void OnThermoMachineUpdated(EntityUid uid, GasThermoMachineComponent thermoMachine, ref AtmosDeviceUpdateEvent args)
{
thermoMachine.LastEnergyDelta = 0f;
@@ -135,56 +119,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
}
private bool IsHeater(GasThermoMachineComponent comp)
{
return comp.Cp >= 0;
}
private void OnToggleMessage(EntityUid uid, GasThermoMachineComponent thermoMachine, GasThermomachineToggleMessage args)
{
var powerState = _power.TogglePower(uid);
_adminLogger.Add(LogType.AtmosPowerChanged, $"{ToPrettyString(args.Actor)} turned {(powerState ? "On" : "Off")} {ToPrettyString(uid)}");
DirtyUI(uid, thermoMachine);
}
private void OnChangeTemperature(EntityUid uid, GasThermoMachineComponent thermoMachine, GasThermomachineChangeTemperatureMessage args)
{
if (IsHeater(thermoMachine))
thermoMachine.TargetTemperature = MathF.Min(args.Temperature, thermoMachine.MaxTemperature);
else
thermoMachine.TargetTemperature = MathF.Max(args.Temperature, thermoMachine.MinTemperature);
thermoMachine.TargetTemperature = MathF.Max(thermoMachine.TargetTemperature, Atmospherics.TCMB);
_adminLogger.Add(LogType.AtmosTemperatureChanged, $"{ToPrettyString(args.Actor)} set temperature on {ToPrettyString(uid)} to {thermoMachine.TargetTemperature}");
DirtyUI(uid, thermoMachine);
}
private void DirtyUI(EntityUid uid, GasThermoMachineComponent? thermoMachine, UserInterfaceComponent? ui=null)
{
if (!Resolve(uid, ref thermoMachine, ref ui, false))
return;
ApcPowerReceiverComponent? powerReceiver = null;
if (!Resolve(uid, ref powerReceiver))
return;
_userInterfaceSystem.SetUiState(uid, ThermomachineUiKey.Key,
new GasThermomachineBoundUserInterfaceState(thermoMachine.MinTemperature, thermoMachine.MaxTemperature, thermoMachine.TargetTemperature, !powerReceiver.PowerDisabled, IsHeater(thermoMachine)));
}
private void OnExamined(EntityUid uid, GasThermoMachineComponent thermoMachine, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (Loc.TryGetString("gas-thermomachine-system-examined", out var str,
("machineName", !IsHeater(thermoMachine) ? "freezer" : "heater"),
("tempColor", !IsHeater(thermoMachine) ? "deepskyblue" : "red"),
("temp", Math.Round(thermoMachine.TargetTemperature,2))
))
args.PushMarkup(str);
}
private void OnPacketRecv(EntityUid uid, GasThermoMachineComponent component, DeviceNetworkPacketEvent args)
{
if (!TryComp(uid, out DeviceNetworkComponent? netConn)

View File

@@ -5,6 +5,7 @@ using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos.Piping.Portable.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Power;
using Content.Shared.UserInterface;
@@ -41,6 +42,7 @@ public sealed class SpaceHeaterSystem : EntitySystem
{
if (!TryComp<GasThermoMachineComponent>(uid, out var thermoMachine))
return;
thermoMachine.Cp = spaceHeater.HeatingCp;
thermoMachine.HeatCapacity = spaceHeater.PowerConsumption;
}

View File

@@ -1,9 +1,9 @@
using Content.Server.Actions;
using Content.Server.Bed.Components;
using Content.Server.Body.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Bed;
using Content.Shared.Bed.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
using Content.Shared.Buckle.Components;
@@ -11,49 +11,30 @@ using Content.Shared.Damage;
using Content.Shared.Emag.Systems;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Bed
{
public sealed class BedSystem : EntitySystem
public sealed class BedSystem : SharedBedSystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SleepingSystem _sleepingSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private EntityQuery<SleepingComponent> _sleepingQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HealOnBuckleComponent, StrappedEvent>(OnStrapped);
SubscribeLocalEvent<HealOnBuckleComponent, UnstrappedEvent>(OnUnstrapped);
_sleepingQuery = GetEntityQuery<SleepingComponent>();
SubscribeLocalEvent<StasisBedComponent, StrappedEvent>(OnStasisStrapped);
SubscribeLocalEvent<StasisBedComponent, UnstrappedEvent>(OnStasisUnstrapped);
SubscribeLocalEvent<StasisBedComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<StasisBedComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnStrapped(Entity<HealOnBuckleComponent> bed, ref StrappedEvent args)
{
EnsureComp<HealOnBuckleHealingComponent>(bed);
bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime);
_actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed);
// Single action entity, cannot strap multiple entities to the same bed.
DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1);
}
private void OnUnstrapped(Entity<HealOnBuckleComponent> bed, ref UnstrappedEvent args)
{
_actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction);
_sleepingSystem.TryWaking(args.Buckle.Owner);
RemComp<HealOnBuckleHealingComponent>(bed);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -61,7 +42,7 @@ namespace Content.Server.Bed
var query = EntityQueryEnumerator<HealOnBuckleHealingComponent, HealOnBuckleComponent, StrapComponent>();
while (query.MoveNext(out var uid, out _, out var bedComponent, out var strapComponent))
{
if (_timing.CurTime < bedComponent.NextHealTime)
if (Timing.CurTime < bedComponent.NextHealTime)
continue;
bedComponent.NextHealTime += TimeSpan.FromSeconds(bedComponent.HealTime);
@@ -76,7 +57,7 @@ namespace Content.Server.Bed
var damage = bedComponent.Damage;
if (HasComp<SleepingComponent>(healedEntity))
if (_sleepingQuery.HasComp(healedEntity))
damage *= bedComponent.SleepMultiplier;
_damageableSystem.TryChangeDamage(healedEntity, damage, true, origin: uid);

View File

@@ -1,30 +0,0 @@
using Content.Shared.Damage;
namespace Content.Server.Bed.Components
{
[RegisterComponent]
public sealed partial class HealOnBuckleComponent : Component
{
/// <summary>
/// Damage to apply to entities that are strapped to this entity.
/// </summary>
[DataField(required: true)]
public DamageSpecifier Damage = default!;
/// <summary>
/// How frequently the damage should be applied, in seconds.
/// </summary>
[DataField(required: false)]
public float HealTime = 1f;
/// <summary>
/// Damage multiplier that gets applied if the entity is sleeping.
/// </summary>
[DataField]
public float SleepMultiplier = 3f;
public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal
[DataField] public EntityUid? SleepAction;
}
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Bed.Components
{
// TODO rename this component
[RegisterComponent]
public sealed partial class HealOnBuckleHealingComponent : Component
{}
}

View File

@@ -47,6 +47,12 @@ namespace Content.Server.Body.Components
[DataField]
public EntityWhitelist? SpecialDigestible = null;
/// <summary>
/// Controls whitelist behavior. If true, this stomach can digest <i>only</i> food that passes the whitelist. If false, it can digest normal food <i>and</i> any food that passes the whitelist.
/// </summary>
[DataField]
public bool IsSpecialDigestibleExclusive = true;
/// <summary>
/// Used to track how long each reagent has been in the stomach
/// </summary>

View File

@@ -12,6 +12,7 @@ using Content.Shared.Movement.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Timing;
using System.Numerics;
using Content.Shared.Damage.Components;
namespace Content.Server.Body.Systems;
@@ -110,6 +111,9 @@ public sealed class BodySystem : SharedBodySystem
return new HashSet<EntityUid>();
}
if (HasComp<GodmodeComponent>(bodyId))
return new HashSet<EntityUid>();
var xform = Transform(bodyId);
if (xform.MapUid is null)
return new HashSet<EntityUid>();

View File

@@ -173,6 +173,20 @@ public sealed class RespiratorSystem : EntitySystem
_atmosSys.Merge(ev.Gas, outGas);
}
/// <summary>
/// Returns true if the entity is above their SuffocationThreshold and alive.
/// </summary>
public bool IsBreathing(Entity<RespiratorComponent?> ent)
{
if (_mobState.IsIncapacitated(ent))
return false;
if (!Resolve(ent, ref ent.Comp))
return false;
return (ent.Comp.Saturation > ent.Comp.SuffocationThreshold);
}
/// <summary>
/// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
/// gasses).

View File

@@ -1,4 +1,6 @@
using Content.Shared.Cargo;
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Cargo.Components;
@@ -41,6 +43,12 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component
[DataField]
public HashSet<string> CheckedBounties = new();
/// <summary>
/// The group that bounties are pulled from.
/// </summary>
[DataField]
public ProtoId<CargoBountyGroupPrototype> Group = "StationBounty";
/// <summary>
/// The time at which players will be able to skip the next bounty.
/// </summary>

View File

@@ -31,6 +31,16 @@ public sealed partial class StationCargoOrderDatabaseComponent : Component
[ViewVariables]
public int NumOrdersCreated;
/// <summary>
/// An all encompassing determiner of what markets can be ordered from.
/// Not every console can order from every market, but a console can't order from a market not on this list.
/// </summary>
[DataField]
public List<ProtoId<CargoMarketPrototype>> Markets = new()
{
"market",
};
// TODO: Can probably dump this
/// <summary>
/// The cargo shuttle assigned to this station.

View File

@@ -16,6 +16,7 @@ using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -292,6 +293,13 @@ public sealed partial class CargoSystem
return IsBountyComplete(container, proto.Entries);
}
public bool IsBountyComplete(EntityUid container, ProtoId<CargoBountyPrototype> prototypeId)
{
var prototype = _protoMan.Index(prototypeId);
return IsBountyComplete(container, prototype.Entries);
}
public bool IsBountyComplete(EntityUid container, CargoBountyPrototype prototype)
{
return IsBountyComplete(container, prototype.Entries);
@@ -392,7 +400,9 @@ public sealed partial class CargoSystem
return false;
// todo: consider making the cargo bounties weighted.
var allBounties = _protoMan.EnumeratePrototypes<CargoBountyPrototype>().ToList();
var allBounties = _protoMan.EnumeratePrototypes<CargoBountyPrototype>()
.Where(p => p.Group == component.Group)
.ToList();
var filteredBounties = new List<CargoBountyPrototype>();
foreach (var proto in allBounties)
{

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.Station.Components;
using Content.Shared.Cargo;
@@ -372,7 +373,7 @@ namespace Content.Server.Cargo.Systems
return;
}
if (!component.AllowedGroups.Contains(product.Group))
if (!GetAvailableProducts((uid, component)).Contains(args.CargoProductId))
return;
if (component.SlipPrinter)
@@ -421,7 +422,8 @@ namespace Content.Server.Cargo.Systems
GetOutstandingOrderCount(orderDatabase, console.Account),
orderDatabase.Capacity,
GetNetEntity(station.Value),
orderDatabase.Orders[console.Account]
orderDatabase.Orders[console.Account],
GetAvailableProducts((consoleUid, console))
));
}
}
@@ -617,6 +619,29 @@ namespace Content.Server.Cargo.Systems
}
public List<ProtoId<CargoProductPrototype>> GetAvailableProducts(Entity<CargoOrderConsoleComponent> ent)
{
if (_station.GetOwningStation(ent) is not { } station ||
!TryComp<StationCargoOrderDatabaseComponent>(station, out var db))
{
return new List<ProtoId<CargoProductPrototype>>();
}
var products = new List<ProtoId<CargoProductPrototype>>();
// Note that a market must be both on the station and on the console to be available.
var markets = ent.Comp.AllowedGroups.Intersect(db.Markets).ToList();
foreach (var product in _protoMan.EnumeratePrototypes<CargoProductPrototype>())
{
if (!markets.Contains(product.Group))
continue;
products.Add(product.ID);
}
return products;
}
#region Station
private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)

View File

@@ -131,14 +131,14 @@ public sealed partial class CargoSystem
#region Station
private bool SellPallets(EntityUid gridUid, out HashSet<(EntityUid, OverrideSellComponent?, double)> goods)
private bool SellPallets(EntityUid gridUid, EntityUid station, out HashSet<(EntityUid, OverrideSellComponent?, double)> goods)
{
GetPalletGoods(gridUid, out var toSell, out goods);
if (toSell.Count == 0)
return false;
var ev = new EntitySoldEvent(toSell);
var ev = new EntitySoldEvent(toSell, station);
RaiseLocalEvent(ref ev);
foreach (var ent in toSell)
@@ -230,7 +230,7 @@ public sealed partial class CargoSystem
return;
}
if (!SellPallets(gridUid, out var goods))
if (!SellPallets(gridUid, station, out var goods))
return;
var baseDistribution = CreateAccountDistribution((station, bankAccount));
@@ -267,4 +267,4 @@ public sealed partial class CargoSystem
/// deleted but after the price has been calculated.
/// </summary>
[ByRefEvent]
public readonly record struct EntitySoldEvent(HashSet<EntityUid> Sold);
public readonly record struct EntitySoldEvent(HashSet<EntityUid> Sold, EntityUid Station);

View File

@@ -38,7 +38,6 @@ public sealed partial class CargoSystem : SharedCargoSystem
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;

View File

@@ -1,4 +1,5 @@
using Content.Server.Popups;
using Content.Server.Salvage.JobBoard;
using Content.Shared.Cargo.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Timing;
@@ -13,6 +14,7 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
[Dependency] private readonly PricingSystem _pricingSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly CargoSystem _bountySystem = default!;
[Dependency] private readonly SalvageJobBoardSystem _salvageJobBoard = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
protected override bool GetPriceOrBounty(Entity<PriceGunComponent> entity, EntityUid target, EntityUid user)
@@ -24,6 +26,10 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), user, user);
}
else if (_salvageJobBoard.FulfillsSalvageJob(target, null, out _))
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-salvjob-complete"), user, user);
}
else // Otherwise appraise the price
{
var price = _pricingSystem.GetPrice(target);

View File

@@ -25,7 +25,6 @@ namespace Content.Server.Cargo.Systems;
/// </summary>
public sealed class PricingSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IConsoleHost _consoleHost = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
@@ -278,13 +277,13 @@ public sealed class PricingSystem : EntitySystem
{
double price = 0;
if (prototype.Components.ContainsKey(_factory.GetComponentName(typeof(MaterialComponent))) &&
prototype.Components.TryGetValue(_factory.GetComponentName(typeof(PhysicalCompositionComponent)), out var composition))
if (prototype.Components.ContainsKey(Factory.GetComponentName<MaterialComponent>()) &&
prototype.Components.TryGetValue(Factory.GetComponentName<PhysicalCompositionComponent>(), out var composition))
{
var compositionComp = (PhysicalCompositionComponent) composition.Component;
var matPrice = GetMaterialPrice(compositionComp);
if (prototype.Components.TryGetValue(_factory.GetComponentName(typeof(StackComponent)), out var stackProto))
if (prototype.Components.TryGetValue(Factory.GetComponentName<StackComponent>(), out var stackProto))
{
matPrice *= ((StackComponent) stackProto.Component).Count;
}
@@ -311,7 +310,7 @@ public sealed class PricingSystem : EntitySystem
{
var price = 0.0;
if (prototype.Components.TryGetValue(_factory.GetComponentName(typeof(SolutionContainerManagerComponent)), out var solManager))
if (prototype.Components.TryGetValue(Factory.GetComponentName<SolutionContainerManagerComponent>(), out var solManager))
{
var solComp = (SolutionContainerManagerComponent) solManager.Component;
price += GetSolutionPrice(solComp);
@@ -338,9 +337,9 @@ public sealed class PricingSystem : EntitySystem
{
var price = 0.0;
if (prototype.Components.TryGetValue(_factory.GetComponentName(typeof(StackPriceComponent)), out var stackpriceProto) &&
prototype.Components.TryGetValue(_factory.GetComponentName(typeof(StackComponent)), out var stackProto) &&
!prototype.Components.ContainsKey(_factory.GetComponentName(typeof(MaterialComponent))))
if (prototype.Components.TryGetValue(Factory.GetComponentName<StackPriceComponent>(), out var stackpriceProto) &&
prototype.Components.TryGetValue(Factory.GetComponentName<StackComponent>(), out var stackProto) &&
!prototype.Components.ContainsKey(Factory.GetComponentName<MaterialComponent>()))
{
var stackPrice = (StackPriceComponent) stackpriceProto.Component;
var stack = (StackComponent) stackProto.Component;
@@ -366,7 +365,7 @@ public sealed class PricingSystem : EntitySystem
{
var price = 0.0;
if (prototype.Components.TryGetValue(_factory.GetComponentName(typeof(StaticPriceComponent)), out var staticProto))
if (prototype.Components.TryGetValue(Factory.GetComponentName<StaticPriceComponent>(), out var staticProto))
{
var staticPrice = (StaticPriceComponent) staticProto.Component;
price += staticPrice.Price;

View File

@@ -2,7 +2,6 @@ using Content.Shared.Whitelist;
using Content.Shared.Containers.ItemSlots;
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Dispenser;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -15,47 +14,9 @@ namespace Content.Server.Chemistry.Components
[Access(typeof(ReagentDispenserSystem))]
public sealed partial class ReagentDispenserComponent : Component
{
/// <summary>
/// String with the pack name that stores the initial fill of the dispenser. The initial
/// fill is added to the dispenser on MapInit. Note that we don't use ContainerFill because
/// we have to generate the storage slots at MapInit first, then fill them.
/// </summary>
[DataField("pack", customTypeSerializer:typeof(PrototypeIdSerializer<ReagentDispenserInventoryPrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public string? PackPrototypeId = default!;
/// <summary>
/// Maximum number of internal storage slots. Dispenser can't store (or dispense) more than
/// this many chemicals (without unloading and reloading).
/// </summary>
[DataField("numStorageSlots")]
public int NumSlots = 25;
/// <summary>
/// For each created storage slot for the reagent containers being dispensed, apply this
/// entity whitelist. Makes sure weird containers don't fit in the dispenser and that beakers
/// don't accidentally get slotted into the source slots.
/// </summary>
[DataField]
public EntityWhitelist? StorageWhitelist;
[DataField]
public ItemSlot BeakerSlot = new();
/// <summary>
/// Prefix for automatically-generated slot name for storage, up to NumSlots.
/// </summary>
public static string BaseStorageSlotId = "ReagentDispenser-storageSlot";
/// <summary>
/// List of storage slots that were created at MapInit.
/// </summary>
[DataField]
public List<string> StorageSlotIds = new List<string>();
[DataField]
public List<ItemSlot> StorageSlots = new List<ItemSlot>();
[DataField("clickSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");

View File

@@ -1,11 +1,12 @@
using System.Linq;
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Dispenser;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Storage.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
@@ -13,6 +14,8 @@ using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Content.Shared.Labels.Components;
using Content.Shared.Storage;
using Content.Server.Hands.Systems;
namespace Content.Server.Chemistry.EntitySystems
{
@@ -30,6 +33,7 @@ namespace Content.Server.Chemistry.EntitySystems
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly HandsSystem _handsSystem = default!;
public override void Initialize()
{
@@ -37,15 +41,16 @@ namespace Content.Server.Chemistry.EntitySystems
SubscribeLocalEvent<ReagentDispenserComponent, ComponentStartup>(SubscribeUpdateUiState);
SubscribeLocalEvent<ReagentDispenserComponent, SolutionContainerChangedEvent>(SubscribeUpdateUiState);
SubscribeLocalEvent<ReagentDispenserComponent, EntInsertedIntoContainerMessage>(SubscribeUpdateUiState);
SubscribeLocalEvent<ReagentDispenserComponent, EntRemovedFromContainerMessage>(SubscribeUpdateUiState);
SubscribeLocalEvent<ReagentDispenserComponent, EntInsertedIntoContainerMessage>(SubscribeUpdateUiState, after: [typeof(SharedStorageSystem)]);
SubscribeLocalEvent<ReagentDispenserComponent, EntRemovedFromContainerMessage>(SubscribeUpdateUiState, after: [typeof(SharedStorageSystem)]);
SubscribeLocalEvent<ReagentDispenserComponent, BoundUIOpenedEvent>(SubscribeUpdateUiState);
SubscribeLocalEvent<ReagentDispenserComponent, ReagentDispenserSetDispenseAmountMessage>(OnSetDispenseAmountMessage);
SubscribeLocalEvent<ReagentDispenserComponent, ReagentDispenserDispenseReagentMessage>(OnDispenseReagentMessage);
SubscribeLocalEvent<ReagentDispenserComponent, ReagentDispenserEjectContainerMessage>(OnEjectReagentMessage);
SubscribeLocalEvent<ReagentDispenserComponent, ReagentDispenserClearContainerSolutionMessage>(OnClearContainerSolutionMessage);
SubscribeLocalEvent<ReagentDispenserComponent, MapInitEvent>(OnMapInit, before: new []{typeof(ItemSlotsSystem)});
SubscribeLocalEvent<ReagentDispenserComponent, MapInitEvent>(OnMapInit, before: new[] { typeof(ItemSlotsSystem) });
}
private void SubscribeUpdateUiState<T>(Entity<ReagentDispenserComponent> ent, ref T ev)
@@ -82,32 +87,31 @@ namespace Content.Server.Chemistry.EntitySystems
private List<ReagentInventoryItem> GetInventory(Entity<ReagentDispenserComponent> reagentDispenser)
{
if (!TryComp<StorageComponent>(reagentDispenser.Owner, out var storage))
{
return [];
}
var inventory = new List<ReagentInventoryItem>();
for (var i = 0; i < reagentDispenser.Comp.NumSlots; i++)
foreach (var (storedContainer, storageLocation) in storage.StoredItems)
{
var storageSlotId = ReagentDispenserComponent.BaseStorageSlotId + i;
var storedContainer = _itemSlotsSystem.GetItemOrNull(reagentDispenser.Owner, storageSlotId);
// Set label from manually-applied label, or metadata if unavailable
string reagentLabel;
if (TryComp<LabelComponent>(storedContainer, out var label) && !string.IsNullOrEmpty(label.CurrentLabel))
reagentLabel = label.CurrentLabel;
else if (storedContainer != null)
reagentLabel = Name(storedContainer.Value);
else
continue;
reagentLabel = Name(storedContainer);
// Get volume remaining and color of solution
FixedPoint2 quantity = 0f;
var reagentColor = Color.White;
if (storedContainer != null && _solutionContainerSystem.TryGetDrainableSolution(storedContainer.Value, out _, out var sol))
if (_solutionContainerSystem.TryGetDrainableSolution(storedContainer, out _, out var sol))
{
quantity = sol.Volume;
reagentColor = sol.GetColor(_prototypeManager);
}
inventory.Add(new ReagentInventoryItem(storageSlotId, reagentLabel, quantity, reagentColor));
inventory.Add(new ReagentInventoryItem(storageLocation, reagentLabel, quantity, reagentColor));
}
return inventory;
@@ -122,22 +126,28 @@ namespace Content.Server.Chemistry.EntitySystems
private void OnDispenseReagentMessage(Entity<ReagentDispenserComponent> reagentDispenser, ref ReagentDispenserDispenseReagentMessage message)
{
if (!TryComp<StorageComponent>(reagentDispenser.Owner, out var storage))
{
return;
}
// Ensure that the reagent is something this reagent dispenser can dispense.
var storedContainer = _itemSlotsSystem.GetItemOrNull(reagentDispenser, message.SlotId);
if (storedContainer == null)
var storageLocation = message.StorageLocation;
var storedContainer = storage.StoredItems.FirstOrDefault(kvp => kvp.Value == storageLocation).Key;
if (storedContainer == EntityUid.Invalid)
return;
var outputContainer = _itemSlotsSystem.GetItemOrNull(reagentDispenser, SharedReagentDispenser.OutputSlotName);
if (outputContainer is not { Valid: true } || !_solutionContainerSystem.TryGetFitsInDispenser(outputContainer.Value, out var solution, out _))
return;
if (_solutionContainerSystem.TryGetDrainableSolution(storedContainer.Value, out var src, out _) &&
if (_solutionContainerSystem.TryGetDrainableSolution(storedContainer, out var src, out _) &&
_solutionContainerSystem.TryGetRefillableSolution(outputContainer.Value, out var dst, out _))
{
// force open container, if applicable, to avoid confusing people on why it doesn't dispense
_openable.SetOpen(storedContainer.Value, true);
_openable.SetOpen(storedContainer, true);
_solutionTransferSystem.Transfer(reagentDispenser,
storedContainer.Value, src.Value,
storedContainer, src.Value,
outputContainer.Value, dst.Value,
(int)reagentDispenser.Comp.DispenseAmount);
}
@@ -146,6 +156,21 @@ namespace Content.Server.Chemistry.EntitySystems
ClickSound(reagentDispenser);
}
private void OnEjectReagentMessage(Entity<ReagentDispenserComponent> reagentDispenser, ref ReagentDispenserEjectContainerMessage message)
{
if (!TryComp<StorageComponent>(reagentDispenser.Owner, out var storage))
{
return;
}
var storageLocation = message.StorageLocation;
var storedContainer = storage.StoredItems.FirstOrDefault(kvp => kvp.Value == storageLocation).Key;
if (storedContainer == EntityUid.Invalid)
return;
_handsSystem.TryPickupAnyHand(message.Actor, storedContainer);
}
private void OnClearContainerSolutionMessage(Entity<ReagentDispenserComponent> reagentDispenser, ref ReagentDispenserClearContainerSolutionMessage message)
{
var outputContainer = _itemSlotsSystem.GetItemOrNull(reagentDispenser, SharedReagentDispenser.OutputSlotName);
@@ -163,39 +188,11 @@ namespace Content.Server.Chemistry.EntitySystems
}
/// <summary>
/// Automatically generate storage slots for all NumSlots, and fill them with their initial chemicals.
/// The actual spawning of entities happens in ItemSlotsSystem's MapInit.
/// Initializes the beaker slot
/// </summary>
private void OnMapInit(EntityUid uid, ReagentDispenserComponent component, MapInitEvent args)
private void OnMapInit(Entity<ReagentDispenserComponent> ent, ref MapInitEvent args)
{
// Get list of pre-loaded containers
List<string> preLoad = new List<string>();
if (component.PackPrototypeId is not null
&& _prototypeManager.TryIndex(component.PackPrototypeId, out ReagentDispenserInventoryPrototype? packPrototype))
{
preLoad.AddRange(packPrototype.Inventory);
}
// Populate storage slots with base storage slot whitelist
for (var i = 0; i < component.NumSlots; i++)
{
var storageSlotId = ReagentDispenserComponent.BaseStorageSlotId + i;
ItemSlot storageComponent = new();
storageComponent.Whitelist = component.StorageWhitelist;
storageComponent.Swap = false;
storageComponent.EjectOnBreak = true;
// Check corresponding index in pre-loaded container (if exists) and set starting item
if (i < preLoad.Count)
storageComponent.StartingItem = preLoad[i];
component.StorageSlotIds.Add(storageSlotId);
component.StorageSlots.Add(storageComponent);
component.StorageSlots[i].Name = "Storage Slot " + (i+1);
_itemSlotsSystem.AddItemSlot(uid, component.StorageSlotIds[i], component.StorageSlots[i]);
}
_itemSlotsSystem.AddItemSlot(uid, SharedReagentDispenser.OutputSlotName, component.BeakerSlot);
_itemSlotsSystem.AddItemSlot(ent.Owner, SharedReagentDispenser.OutputSlotName, ent.Comp.BeakerSlot);
}
}
}

View File

@@ -26,7 +26,6 @@ namespace Content.Server.Cloning;
/// </summary>
public sealed partial class CloningSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -106,7 +105,7 @@ public sealed partial class CloningSystem : EntitySystem
foreach (var componentName in componentsToCopy)
{
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
if (!Factory.TryGetRegistration(componentName, out var componentRegistration))
{
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
continue;
@@ -122,7 +121,7 @@ public sealed partial class CloningSystem : EntitySystem
foreach (var componentName in componentsToEvent)
{
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
if (!Factory.TryGetRegistration(componentName, out var componentRegistration))
{
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
continue;

View File

@@ -10,7 +10,6 @@ namespace Content.Server.Clothing.Systems;
public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
public override void Initialize()
@@ -68,7 +67,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
private void UpdateIdentityBlocker(EntityUid uid, ChameleonClothingComponent component, EntityPrototype proto)
{
if (proto.HasComponent<IdentityBlockerComponent>(_factory))
if (proto.HasComponent<IdentityBlockerComponent>(Factory))
EnsureComp<IdentityBlockerComponent>(uid);
else
RemComp<IdentityBlockerComponent>(uid);

View File

@@ -67,5 +67,12 @@ namespace Content.Server.Communications
/// </summary>
[DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Announcements/announce.ogg");
/// <summary>
/// Hides the sender identity (If they even have one).
/// In practise this removes the "Sent by ScugMcWawa (Slugcat Captain)" at the bottom of the announcement.
/// </summary>
[DataField]
public bool AnnounceSentBy = true;
}
}

View File

@@ -259,7 +259,9 @@ namespace Content.Server.Communications
Loc.TryGetString(comp.Title, out var title);
title ??= comp.Title;
msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author;
if (comp.AnnounceSentBy)
msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author;
if (comp.Global)
{
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color);

View File

@@ -25,6 +25,7 @@ public sealed class TileWallsCommand : IConsoleCommand
[ValidatePrototypeId<TagPrototype>]
public const string WallTag = "Wall";
public const string DiagonalTag = "Diagonal";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
@@ -85,6 +86,11 @@ public sealed class TileWallsCommand : IConsoleCommand
continue;
}
if (tagSystem.HasTag(child, DiagonalTag))
{
continue;
}
var childTransform = _entManager.GetComponent<TransformComponent>(child);
if (!childTransform.Anchored)

View File

@@ -25,7 +25,6 @@ namespace Content.Server.Construction
{
public sealed partial class ConstructionSystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
@@ -211,7 +210,7 @@ namespace Content.Server.Construction
case ArbitraryInsertConstructionGraphStep arbitraryStep:
foreach (var entity in new HashSet<EntityUid>(EnumerateNearby(user)))
{
if (!arbitraryStep.EntityValid(entity, EntityManager, _factory))
if (!arbitraryStep.EntityValid(entity, EntityManager, Factory))
continue;
if (used.Contains(entity))
@@ -541,7 +540,7 @@ namespace Content.Server.Construction
switch (step)
{
case EntityInsertConstructionGraphStep entityInsert:
if (entityInsert.EntityValid(holding, EntityManager, _factory))
if (entityInsert.EntityValid(holding, EntityManager, Factory))
valid = true;
break;
case ToolConstructionGraphStep _:

View File

@@ -8,8 +8,10 @@ using Content.Shared.Construction.EntitySystems;
using Content.Shared.Construction.Steps;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Prying.Systems;
using Content.Shared.Radio.EntitySystems;
using Content.Shared.Stacks;
using Content.Shared.Temperature;
using Content.Shared.Tools.Systems;
using Robust.Shared.Containers;
@@ -271,7 +273,11 @@ namespace Content.Server.Construction
// Since many things inherit this step, we delegate the "is this entity valid?" logic to them.
// While this is very OOP and I find it icky, I must admit that it simplifies the code here a lot.
if(!insertStep.EntityValid(insert, EntityManager, _factory))
if(!insertStep.EntityValid(insert, EntityManager, Factory))
return HandleResult.False;
// Unremovable items can't be inserted, unless they are a lingering stack
if(HasComp<UnremoveableComponent>(insert) && (!TryComp<StackComponent>(insert, out var comp) || !comp.Lingering))
return HandleResult.False;
// If we're only testing whether this step would be handled by the given event, then we're done.

View File

@@ -13,7 +13,6 @@ namespace Content.Server.Construction;
public sealed class MachineFrameSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly StackSystem _stack = default!;
@@ -75,7 +74,7 @@ public sealed class MachineFrameSystem : EntitySystem
if (component.ComponentProgress[compName] >= info.Amount)
continue;
var registration = _factory.GetRegistration(compName);
var registration = Factory.GetRegistration(compName);
if (!HasComp(args.Used, registration.Type))
continue;
@@ -294,7 +293,7 @@ public sealed class MachineFrameSystem : EntitySystem
// I have many regrets.
foreach (var (compName, _) in component.ComponentRequirements)
{
var registration = _factory.GetRegistration(compName);
var registration = Factory.GetRegistration(compName);
if (!HasComp(part, registration.Type))
continue;

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Shared.Administration.Logs;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
@@ -65,7 +66,11 @@ namespace Content.Server.Database
profiles[profile.Slot] = ConvertProfiles(profile);
}
return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor));
var constructionFavorites = new List<ProtoId<ConstructionPrototype>>(prefs.ConstructionFavorites.Count);
foreach (var favorite in prefs.ConstructionFavorites)
constructionFavorites.Add(new ProtoId<ConstructionPrototype>(favorite));
return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor), constructionFavorites);
}
public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
@@ -143,7 +148,8 @@ namespace Content.Server.Database
{
UserId = userId.UserId,
SelectedCharacterSlot = 0,
AdminOOCColor = Color.Red.ToHex()
AdminOOCColor = Color.Red.ToHex(),
ConstructionFavorites = [],
};
prefs.Profiles.Add(profile);
@@ -152,7 +158,7 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
return new PlayerPreferences(new[] {new KeyValuePair<int, ICharacterProfile>(0, defaultProfile)}, 0, Color.FromHex(prefs.AdminOOCColor));
return new PlayerPreferences(new[] { new KeyValuePair<int, ICharacterProfile>(0, defaultProfile) }, 0, Color.FromHex(prefs.AdminOOCColor), []);
}
public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
@@ -178,6 +184,19 @@ namespace Content.Server.Database
}
public async Task SaveConstructionFavoritesAsync(NetUserId userId, List<ProtoId<ConstructionPrototype>> constructionFavorites)
{
await using var db = await GetDb();
var prefs = await db.DbContext.Preference.SingleAsync(p => p.UserId == userId.UserId);
var favorites = new List<string>(constructionFavorites.Count);
foreach (var favorite in constructionFavorites)
favorites.Add(favorite.Id);
prefs.ConstructionFavorites = favorites;
await db.DbContext.SaveChangesAsync();
}
private static async Task SetSelectedCharacterSlotAsync(NetUserId userId, int newSlot, ServerDbContext db)
{
var prefs = await db.Preference.SingleAsync(p => p.UserId == userId.UserId);

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Database;
using Content.Shared.Preferences;
using Content.Shared.Roles;
@@ -42,6 +43,8 @@ namespace Content.Server.Database
Task SaveAdminOOCColorAsync(NetUserId userId, Color color);
Task SaveConstructionFavoritesAsync(NetUserId userId, List<ProtoId<ConstructionPrototype>> constructionFavorites);
// Single method for two operations for transaction.
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
@@ -489,6 +492,12 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color));
}
public Task SaveConstructionFavoritesAsync(NetUserId userId, List<ProtoId<ConstructionPrototype>> constructionFavorites)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SaveConstructionFavoritesAsync(userId, constructionFavorites));
}
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
{
DbReadOpsMetric.Inc();

View File

@@ -160,38 +160,45 @@ namespace Content.Server.Decals
private void OnTileChanged(ref TileChangedEvent args)
{
if (!args.NewTile.IsSpace(_tileDefMan))
return;
if (!TryComp(args.Entity, out DecalGridComponent? grid))
return;
var indices = GetChunkIndices(args.NewTile.GridIndices);
var toDelete = new HashSet<uint>();
if (!grid.ChunkCollection.ChunkCollection.TryGetValue(indices, out var chunk))
return;
foreach (var (uid, decal) in chunk.Decals)
foreach (var change in args.Changes)
{
if (new Vector2((int) Math.Floor(decal.Coordinates.X), (int) Math.Floor(decal.Coordinates.Y)) ==
args.NewTile.GridIndices)
if (!change.NewTile.IsSpace(_tileDefMan))
continue;
var indices = GetChunkIndices(change.GridIndices);
if (!grid.ChunkCollection.ChunkCollection.TryGetValue(indices, out var chunk))
continue;
toDelete.Clear();
foreach (var (uid, decal) in chunk.Decals)
{
toDelete.Add(uid);
if (new Vector2((int)Math.Floor(decal.Coordinates.X), (int)Math.Floor(decal.Coordinates.Y)) ==
change.GridIndices)
{
toDelete.Add(uid);
}
}
if (toDelete.Count == 0)
continue;
foreach (var decalId in toDelete)
{
grid.DecalIndex.Remove(decalId);
chunk.Decals.Remove(decalId);
}
DirtyChunk(args.Entity, indices, chunk);
if (chunk.Decals.Count == 0)
grid.ChunkCollection.ChunkCollection.Remove(indices);
}
if (toDelete.Count == 0)
return;
foreach (var decalId in toDelete)
{
grid.DecalIndex.Remove(decalId);
chunk.Decals.Remove(decalId);
}
DirtyChunk(args.Entity, indices, chunk);
if (chunk.Decals.Count == 0)
grid.ChunkCollection.ChunkCollection.Remove(indices);
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)

View File

@@ -43,7 +43,6 @@ namespace Content.Server.Destructible
[Dependency] public readonly PuddleSystem PuddleSystem = default!;
[Dependency] public readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] public readonly IPrototypeManager PrototypeManager = default!;
[Dependency] public readonly IComponentFactory ComponentFactory = default!;
[Dependency] public readonly IAdminLogManager _adminLogger = default!;
public override void Initialize()

View File

@@ -40,7 +40,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
if (toSpawn == 0) continue;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.ComponentFactory))
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory))
{
var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset)));
system.StackSystem.SetCount(spawned, toSpawn);

View File

@@ -53,7 +53,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
if (count == 0)
continue;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.ComponentFactory))
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory))
{
var spawned = SpawnInContainer
? system.EntityManager.SpawnNextToOrDrop(entityId, owner)

View File

@@ -49,7 +49,7 @@ public sealed partial class LogicGateComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)]
public ProtoId<SourcePortPrototype> OutputPort = "Output";
// Initial state
// Initial state, used to not spam invoke ports
[DataField]
public SignalState StateA = SignalState.Low;
@@ -59,13 +59,3 @@ public sealed partial class LogicGateComponent : Component
[DataField]
public bool LastOutput;
}
/// <summary>
/// Last state of a signal port, used to not spam invoking ports.
/// </summary>
public enum SignalState : byte
{
Momentary, // Instantaneous pulse high, compatibility behavior
Low,
High
}

View File

@@ -1,6 +1,7 @@
using Content.Server.DeviceLinking.Components;
using Content.Server.DeviceNetwork;
using Content.Server.Doors.Systems;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.DeviceNetwork;
using Content.Shared.Doors.Components;

View File

@@ -1,4 +1,5 @@
using Content.Server.DeviceLinking.Components;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.DeviceNetwork;

View File

@@ -0,0 +1,14 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Prototypes;
namespace Content.Server.Disposal.Tube;
/// <summary>
/// Disposal pipes with this component can be linked with devices to send a signal every time an item goes through the pipe
/// </summary>
[RegisterComponent, Access(typeof(DisposalSignallerSystem))]
public sealed partial class DisposalSignallerComponent : Component
{
[DataField]
public ProtoId<SourcePortPrototype> Port = "ItemDetected";
}

View File

@@ -0,0 +1,25 @@
using Content.Server.DeviceLinking.Systems;
namespace Content.Server.Disposal.Tube;
public sealed class DisposalSignallerSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _link = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalSignallerComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<DisposalSignallerComponent, GetDisposalsNextDirectionEvent>(OnGetNextDirection, after: new[] { typeof(DisposalTubeSystem) });
}
private void OnInit(EntityUid uid, DisposalSignallerComponent comp, ComponentInit args)
{
_link.EnsureSourcePorts(uid, comp.Port);
}
private void OnGetNextDirection(EntityUid uid, DisposalSignallerComponent comp, ref GetDisposalsNextDirectionEvent args)
{
_link.InvokePort(uid, comp.Port);
}
}

View File

@@ -0,0 +1,33 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.EffectConditions;
/// <summary>
/// Condition for if the entity is successfully breathing.
/// </summary>
public sealed partial class Breathing : EntityEffectCondition
{
/// <summary>
/// If true, the entity must not have trouble breathing to pass.
/// </summary>
[DataField]
public bool IsBreathing = true;
public override bool Condition(EntityEffectBaseArgs args)
{
if (!args.EntityManager.TryGetComponent(args.TargetEntity, out RespiratorComponent? respiratorComp))
return !IsBreathing; // They do not breathe.
var breathingState = args.EntityManager.System<RespiratorSystem>().IsBreathing((args.TargetEntity, respiratorComp));
return IsBreathing == breathingState;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-breathing",
("isBreathing", IsBreathing));
}
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Body.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.EffectConditions;
/// <summary>
/// Condition for if the entity is or isn't wearing internals.
/// </summary>
public sealed partial class Internals : EntityEffectCondition
{
/// <summary>
/// To pass, the entity's internals must have this same state.
/// </summary>
[DataField]
public bool UsingInternals = true;
public override bool Condition(EntityEffectBaseArgs args)
{
if (!args.EntityManager.TryGetComponent(args.TargetEntity, out InternalsComponent? internalsComp))
return !UsingInternals; // They have no internals to wear.
var internalsState = internalsComp.GasTankEntity == null;
return UsingInternals == internalsState;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-internals", ("usingInternals", UsingInternals));
}
}

View File

@@ -8,34 +8,49 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Tries to force someone to emote (scream, laugh, etc). Still respects whitelists/blacklists and other limits of the specified emote unless forced.
/// Tries to force someone to emote (scream, laugh, etc). Still respects whitelists/blacklists and other limits unless specially forced.
/// </summary>
[UsedImplicitly]
public sealed partial class Emote : EntityEffect
{
[DataField("emote", customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
public string? EmoteId;
/// <summary>
/// The emote the entity will preform.
/// </summary>
[DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
public string EmoteId;
/// <summary>
/// If the emote should be recorded in chat.
/// </summary>
[DataField]
public bool ShowInChat;
/// <summary>
/// If the forced emote will be listed in the guidebook.
/// </summary>
[DataField]
public bool ShowInGuidebook;
/// <summary>
/// If true, the entity will preform the emote even if they normally can't.
/// </summary>
[DataField]
public bool Force = false;
// JUSTIFICATION: Emoting is flavor, so same reason popup messages are not in here.
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> null;
{
if (!ShowInGuidebook)
return null; // JUSTIFICATION: Emoting is mostly flavor, so same reason popup messages are not in here.
return Loc.GetString("reagent-effect-guidebook-emote", ("chance", Probability), ("emote", EmoteId));
}
public override void Effect(EntityEffectBaseArgs args)
{
if (EmoteId == null)
return;
var chatSys = args.EntityManager.System<ChatSystem>();
if (ShowInChat)
chatSys.TryEmoteWithChat(args.TargetEntity, EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: Force);
else
chatSys.TryEmoteWithoutChat(args.TargetEntity, EmoteId);
}
}

View File

@@ -0,0 +1,141 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Version of <see cref="HealthChange"/> that distributes the healing to groups
/// </summary>
[UsedImplicitly]
public sealed partial class EvenHealthChange : EntityEffect
{
/// <summary>
/// Damage to heal, collected into entire damage groups.
/// </summary>
[DataField(required: true)]
public Dictionary<ProtoId<DamageGroupPrototype>, FixedPoint2> Damage = new();
/// <summary>
/// Should this effect scale the damage by the amount of chemical in the solution?
/// Useful for touch reactions, like styptic powder or acid.
/// Only usable if the EntityEffectBaseArgs is an EntityEffectReagentArgs.
/// </summary>
[DataField]
public bool ScaleByQuantity;
/// <summary>
/// Should this effect ignore damage modifiers?
/// </summary>
[DataField]
public bool IgnoreResistances = true;
protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
var damages = new List<string>();
var heals = false;
var deals = false;
var damagableSystem = entSys.GetEntitySystem<DamageableSystem>();
var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier;
var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier;
foreach (var (group, amount) in Damage)
{
var groupProto = prototype.Index(group);
var sign = FixedPoint2.Sign(amount);
var mod = 1f;
if (sign < 0)
{
heals = true;
mod = universalReagentHealModifier;
}
else if (sign > 0)
{
deals = true;
mod = universalReagentDamageModifier;
}
damages.Add(
Loc.GetString("health-change-display",
("kind", groupProto.LocalizedName),
("amount", MathF.Abs(amount.Float() * mod)),
("deltasign", sign)
));
}
var healsordeals = heals ? (deals ? "both" : "heals") : (deals ? "deals" : "none");
return Loc.GetString("reagent-effect-guidebook-even-health-change",
("chance", Probability),
("changes", ContentLocalizationManager.FormatList(damages)),
("healsordeals", healsordeals));
}
public override void Effect(EntityEffectBaseArgs args)
{
if (!args.EntityManager.TryGetComponent<DamageableComponent>(args.TargetEntity, out var damageable))
return;
var protoMan = IoCManager.Resolve<IPrototypeManager>();
var scale = FixedPoint2.New(1);
if (args is EntityEffectReagentArgs reagentArgs)
{
scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale;
}
var damagableSystem = args.EntityManager.System<DamageableSystem>();
var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier;
var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier;
var dspec = new DamageSpecifier();
foreach (var (group, amount) in Damage)
{
var groupProto = protoMan.Index(group);
var groupDamage = new Dictionary<string, FixedPoint2>();
foreach (var damageId in groupProto.DamageTypes)
{
var damageAmount = damageable.Damage.DamageDict.GetValueOrDefault(damageId);
if (damageAmount != FixedPoint2.Zero)
groupDamage.Add(damageId, damageAmount);
}
var sum = groupDamage.Values.Sum();
foreach (var (damageId, damageAmount) in groupDamage)
{
var existing = dspec.DamageDict.GetOrNew(damageId);
dspec.DamageDict[damageId] = existing + damageAmount / sum * amount;
}
}
if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1)
{
foreach (var (type, val) in dspec.DamageDict)
{
if (val < 0f)
{
dspec.DamageDict[type] = val * universalReagentHealModifier;
}
if (val > 0f)
{
dspec.DamageDict[type] = val * universalReagentDamageModifier;
}
}
}
damagableSystem.TryChangeDamage(
args.TargetEntity,
dspec * scale,
IgnoreResistances,
interruptsDoAfters: false);
}
}

View File

@@ -64,48 +64,6 @@ namespace Content.Server.EntityEffects.Effects
damageSpec = entSys.GetEntitySystem<DamageableSystem>().ApplyUniversalAllModifiers(damageSpec);
foreach (var group in prototype.EnumeratePrototypes<DamageGroupPrototype>())
{
if (!damageSpec.TryGetDamageInGroup(group, out var amount))
continue;
var relevantTypes = damageSpec.DamageDict
.Where(x => x.Value != FixedPoint2.Zero && group.DamageTypes.Contains(x.Key)).ToList();
if (relevantTypes.Count != group.DamageTypes.Count)
continue;
var sum = FixedPoint2.Zero;
foreach (var type in group.DamageTypes)
{
sum += damageSpec.DamageDict.GetValueOrDefault(type);
}
// if the total sum of all the types equal the damage amount,
// assume that they're evenly distributed.
if (sum != amount)
continue;
var sign = FixedPoint2.Sign(amount);
if (sign < 0)
heals = true;
if (sign > 0)
deals = true;
damages.Add(
Loc.GetString("health-change-display",
("kind", group.LocalizedName),
("amount", MathF.Abs(amount.Float())),
("deltasign", sign)
));
foreach (var type in group.DamageTypes)
{
damageSpec.DamageDict.Remove(type);
}
}
foreach (var (kind, amount) in damageSpec.DamageDict)
{
var sign = FixedPoint2.Sign(amount);

View File

@@ -233,65 +233,66 @@ public sealed partial class ExplosionSystem
/// </summary>
private void OnTileChanged(ref TileChangedEvent ev)
{
// only need to update the grid-edge map if a tile was added or removed from the grid.
if (!ev.NewTile.Tile.IsEmpty && !ev.OldTile.IsEmpty)
return;
if (!TryComp(ev.Entity, out MapGridComponent? grid))
return;
var tileRef = ev.NewTile;
if (!_gridEdges.TryGetValue(tileRef.GridUid, out var edges))
foreach (var change in ev.Changes)
{
edges = new();
_gridEdges[tileRef.GridUid] = edges;
}
// only need to update the grid-edge map if a tile was added or removed from the grid.
if (!change.NewTile.IsEmpty && !change.OldTile.IsEmpty)
continue;
if (tileRef.Tile.IsEmpty)
{
// if the tile is empty, it cannot itself be an edge tile.
edges.Remove(tileRef.GridIndices);
if (!_gridEdges.TryGetValue(ev.Entity, out var edges))
{
edges = new();
_gridEdges[ev.Entity] = edges;
}
// add any valid neighbours to the list of edge-tiles
if (change.NewTile.IsEmpty)
{
// if the tile is empty, it cannot itself be an edge tile.
edges.Remove(change.GridIndices);
// add any valid neighbours to the list of edge-tiles
for (var i = 0; i < NeighbourVectors.Length; i++)
{
var neighbourIndex = change.GridIndices + NeighbourVectors[i];
if (_mapSystem.TryGetTileRef(ev.Entity, grid, neighbourIndex, out var neighbourTile) && !neighbourTile.Tile.IsEmpty)
{
var oppositeDirection = (NeighborFlag)(1 << ((i + 4) % 8));
edges[neighbourIndex] = edges.GetValueOrDefault(neighbourIndex) | oppositeDirection;
}
}
continue;
}
// the tile is not empty space, but was previously. So update directly adjacent neighbours, which may no longer
// be edge tiles.
for (var i = 0; i < NeighbourVectors.Length; i++)
{
var neighbourIndex = tileRef.GridIndices + NeighbourVectors[i];
var neighbourIndex = change.GridIndices + NeighbourVectors[i];
if (_mapSystem.TryGetTileRef(ev.Entity, grid, neighbourIndex, out var neighbourTile) && !neighbourTile.Tile.IsEmpty)
if (edges.TryGetValue(neighbourIndex, out var neighborSpaceDir))
{
var oppositeDirection = (NeighborFlag) (1 << ((i + 4) % 8));
edges[neighbourIndex] = edges.GetValueOrDefault(neighbourIndex) | oppositeDirection;
var oppositeDirection = (NeighborFlag)(1 << ((i + 4) % 8));
neighborSpaceDir &= ~oppositeDirection;
if (neighborSpaceDir == NeighborFlag.Invalid)
{
// no longer an edge tile
edges.Remove(neighbourIndex);
continue;
}
edges[neighbourIndex] = neighborSpaceDir;
}
}
return;
// finally check if the new tile is itself an edge tile
if (IsEdge(grid, change.GridIndices, out var spaceDir))
edges.Add(change.GridIndices, spaceDir);
}
// the tile is not empty space, but was previously. So update directly adjacent neighbours, which may no longer
// be edge tiles.
for (var i = 0; i < NeighbourVectors.Length; i++)
{
var neighbourIndex = tileRef.GridIndices + NeighbourVectors[i];
if (edges.TryGetValue(neighbourIndex, out var neighborSpaceDir))
{
var oppositeDirection = (NeighborFlag) (1 << ((i + 4) % 8));
neighborSpaceDir &= ~oppositeDirection;
if (neighborSpaceDir == NeighborFlag.Invalid)
{
// no longer an edge tile
edges.Remove(neighbourIndex);
continue;
}
edges[neighbourIndex] = neighborSpaceDir;
}
}
// finally check if the new tile is itself an edge tile
if (IsEdge(grid, tileRef.GridIndices, out var spaceDir))
edges.Add(tileRef.GridIndices, spaceDir);
}
/// <summary>

View File

@@ -0,0 +1,79 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Explosion.EntitySystems;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
/// <summary>
/// Releases a gas mixture to the atmosphere when triggered.
/// Can also release gas over a set timespan to prevent trolling people
/// with the instant-wall-of-pressure-inator.
/// </summary>
public sealed partial class ReleaseGasOnTriggerSystem : SharedReleaseGasOnTriggerSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReleaseGasOnTriggerComponent, TriggerEvent>(OnTrigger);
}
/// <summary>
/// Shrimply sets the component to active when triggered, allowing it to release over time.
/// </summary>
private void OnTrigger(Entity<ReleaseGasOnTriggerComponent> ent, ref TriggerEvent args)
{
ent.Comp.Active = true;
ent.Comp.NextReleaseTime = _timing.CurTime;
ent.Comp.StartingTotalMoles = ent.Comp.Air.TotalMoles;
UpdateAppearance(ent.Owner, true);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<ReleaseGasOnTriggerComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (!comp.Active || comp.NextReleaseTime > curTime)
continue;
var giverGasMix = comp.Air.Remove(comp.StartingTotalMoles * comp.RemoveFraction);
var environment = _atmosphereSystem.GetContainingMixture(uid, false, true);
if (environment == null)
{
UpdateAppearance(uid, false);
RemCompDeferred<ReleaseGasOnTriggerComponent>(uid);
continue;
}
_atmosphereSystem.Merge(environment, giverGasMix);
comp.NextReleaseTime += comp.ReleaseInterval;
if (comp.PressureLimit != 0 && environment.Pressure >= comp.PressureLimit ||
comp.Air.TotalMoles <= 0)
{
UpdateAppearance(uid, false);
RemCompDeferred<ReleaseGasOnTriggerComponent>(uid);
continue;
}
}
}
private void UpdateAppearance(Entity<AppearanceComponent?> entity, bool state)
{
if (!Resolve(entity, ref entity.Comp, false))
return;
_appearance.SetData(entity, ReleaseGasOnTriggerVisuals.Key, state);
}
}

View File

@@ -6,7 +6,6 @@ namespace Content.Server.Explosion.EntitySystems;
public sealed class TwoStageTriggerSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly TriggerSystem _triggerSystem = default!;
@@ -30,7 +29,7 @@ public sealed class TwoStageTriggerSystem : EntitySystem
{
foreach (var (name, entry) in component.SecondStageComponents)
{
var comp = (Component)_factory.GetComponent(name);
var comp = (Component) Factory.GetComponent(name);
var temp = (object)comp;
if (EntityManager.TryGetComponent(uid, entry.Component.GetType(), out var c))

View File

@@ -1,7 +1,11 @@
using Content.Server.Antag;
using Content.Server.Dragon;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Roles;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.CharacterInfo;
using Content.Shared.Localizations;
using Robust.Server.GameObjects;
@@ -12,16 +16,37 @@ public sealed class DragonRuleSystem : GameRuleSystem<DragonRuleComponent>
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly RoleSystem _roleSystem = default!;
[Dependency] private readonly MindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DragonRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagEntitySelected);
SubscribeLocalEvent<DragonRoleComponent, GetBriefingEvent>(UpdateBriefing);
}
private void UpdateBriefing(Entity<DragonRoleComponent> entity, ref GetBriefingEvent args)
{
var ent = args.Mind.Comp.OwnedEntity;
if(ent is null)
return;
args.Append(MakeBriefing(ent.Value));
}
private void AfterAntagEntitySelected(Entity<DragonRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (!_mind.TryGetMind(args.EntityUid, out var mindId, out var mind))
return;
_roleSystem.MindHasRole<DragonRoleComponent>(mindId, out var dragonRole);
if(dragonRole is null)
return;
_antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
}

View File

@@ -44,6 +44,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
[ValidatePrototypeId<TagPrototype>]
private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
public override void Initialize()
{
base.Initialize();
@@ -104,7 +105,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
args.AddLine(Loc.GetString("nukeops-list-start"));
var antags =_antag.GetAntagIdentifiers(uid);
var antags = _antag.GetAntagIdentifiers(uid);
foreach (var (_, sessionData, name) in antags)
{
@@ -122,7 +123,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
if (ev.OwningStation == GetOutpost(uid))
{
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
SetWinType((uid, nukeops), WinType.CrewMajor);
SetWinType((uid, nukeops), WinType.CrewMajor, GameTicker.IsGameRuleActive("Nukeops")); // End the round ONLY if the actual gamemode is NukeOps.
if (!GameTicker.IsGameRuleActive("Nukeops")) // End the rule if the LoneOp shuttle got nuked, because that particular LoneOp clearly failed, and should not be considered a Syndie victory even if a future LoneOp wins.
GameTicker.EndGameRule(uid);
continue;
}
@@ -152,7 +155,27 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
}
_roundEndSystem.EndRound();
if (GameTicker.IsGameRuleActive("Nukeops")) // If it's Nukeops then end the round on any detonation
{
_roundEndSystem.EndRound();
}
else
{ // It's a LoneOp. Only end the round if the station was destroyed
var handled = false;
foreach (var cond in nukeops.WinConditions)
{
if (cond.ToString().ToLower() == "NukeExplodedOnCorrectStation") // If this is true, then the nuke destroyed the station! It's likely everyone is very dead so keeping the round going is pointless.
{
_roundEndSystem.EndRound(); // end the round!
handled = true;
break;
}
}
if (!handled) // The round didn't end, so end the rule so it doesn't get overridden by future LoneOps.
{
GameTicker.EndGameRule(uid);
}
}
}
}
@@ -411,10 +434,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
var nukeops = ent.Comp;
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
if (nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor) // Skip this if the round's victor has already been decided.
return;
// If there are any nuclear bombs that are active, immediately return. We're not over yet.
foreach (var nuke in EntityQuery<NukeComponent>())
{
@@ -462,11 +484,16 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
: WinCondition.AllNukiesDead);
SetWinType(ent, WinType.CrewMajor, false);
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing) // It's still worth checking if operatives have all died, even if the round-end behaviour is nothing.
return; // Shouldn't actually try to end the round in the case of nothing though.
_roundEndSystem.DoRoundEndBehavior(nukeops.RoundEndBehavior,
nukeops.EvacShuttleTime,
nukeops.RoundEndTextSender,
nukeops.RoundEndTextShuttleCall,
nukeops.RoundEndTextAnnouncement);
nukeops.EvacShuttleTime,
nukeops.RoundEndTextSender,
nukeops.RoundEndTextShuttleCall,
nukeops.RoundEndTextAnnouncement);
// prevent it called multiple times
nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;

View File

@@ -234,7 +234,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
continue;
// remove their antag role
_role.MindTryRemoveRole<RevolutionaryRoleComponent>(mindId);
_role.MindRemoveRole<RevolutionaryRoleComponent>(mindId);
// make it very obvious to the rev they've been deconverted since
// they may not see the popup due to antag and/or new player tunnel vision

View File

@@ -21,14 +21,13 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
private string _ruleCompName = default!;
public override void Initialize()
{
base.Initialize();
_ruleCompName = _compFact.GetComponentName(typeof(GameRuleComponent));
_ruleCompName = Factory.GetComponentName<GameRuleComponent>();
}
protected override void Added(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)

View File

@@ -1,6 +1,5 @@
using Content.Shared.EntityList;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
namespace Content.Server.Gatherable.Components;
@@ -25,11 +24,13 @@ public sealed partial class GatherableComponent : Component
/// - Tag1
/// - Tag2
/// loot:
/// Tag1: LootTableID1
/// Tag2: LootTableID2
/// Tag1: !type:NestedSelector
/// tableId: LootTableID1
/// Tag2: !type:NestedSelector
/// tableId: LootTableID2
/// </summary>
[DataField]
public Dictionary<string, ProtoId<EntityLootTablePrototype>>? Loot = new();
public Dictionary<string, EntityTableSelector>? Loot = new();
/// <summary>
/// Random shift of the appearing entity during gathering

View File

@@ -1,25 +1,25 @@
using Content.Server.Destructible;
using Content.Server.Gatherable.Components;
using Content.Shared.EntityTable;
using Content.Shared.Interaction;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Whitelist;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Gatherable;
public sealed partial class GatherableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DestructibleSystem _destructible = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly EntityTableSystem _entityTable = default!;
public override void Initialize()
{
@@ -76,8 +76,7 @@ public sealed partial class GatherableSystem : EntitySystem
if (gatherer != null && !_tagSystem.HasTag(gatherer.Value, tag))
continue;
}
var getLoot = _proto.Index(table);
var spawnLoot = getLoot.GetSpawns(_random);
var spawnLoot = _entityTable.GetSpawns(table);
foreach (var loot in spawnLoot)
{
var spawnPos = pos.Offset(_random.NextVector2(component.GatherOffset));

View File

@@ -33,7 +33,6 @@ namespace Content.Server.Hands.Systems
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
[Dependency] private readonly VirtualItemSystem _virtualItemSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PullingSystem _pullingSystem = default!;
@@ -54,9 +53,6 @@ namespace Content.Server.Hands.Systems
SubscribeLocalEvent<HandsComponent, DisarmedEvent>(OnDisarmed, before: new[] {typeof(StunSystem), typeof(SharedStaminaSystem)});
SubscribeLocalEvent<HandsComponent, PullStartedMessage>(HandlePullStarted);
SubscribeLocalEvent<HandsComponent, PullStoppedMessage>(HandlePullStopped);
SubscribeLocalEvent<HandsComponent, BodyPartAddedEvent>(HandleBodyPartAdded);
SubscribeLocalEvent<HandsComponent, BodyPartRemovedEvent>(HandleBodyPartRemoved);
@@ -142,45 +138,6 @@ namespace Content.Server.Hands.Systems
RemoveHand(uid, args.Slot);
}
#region pulling
private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args)
{
if (args.PullerUid != uid)
return;
if (TryComp<PullerComponent>(args.PullerUid, out var pullerComp) && !pullerComp.NeedsHands)
return;
if (!_virtualItemSystem.TrySpawnVirtualItemInHand(args.PulledUid, uid))
{
DebugTools.Assert("Unable to find available hand when starting pulling??");
}
}
private void HandlePullStopped(EntityUid uid, HandsComponent component, PullStoppedMessage args)
{
if (args.PullerUid != uid)
return;
// Try find hand that is doing this pull.
// and clear it.
foreach (var hand in component.Hands.Values)
{
if (hand.HeldEntity == null
|| !TryComp(hand.HeldEntity, out VirtualItemComponent? virtualItem)
|| virtualItem.BlockingEntity != args.PulledUid)
{
continue;
}
TryDrop(args.PullerUid, hand, handsComp: component);
break;
}
}
#endregion
#region interactions
private bool HandleThrowItem(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)

View File

@@ -20,7 +20,6 @@ public sealed class RandomGiftSystem : EntitySystem
{
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
@@ -93,9 +92,9 @@ public sealed class RandomGiftSystem : EntitySystem
{
_possibleGiftsSafe.Clear();
_possibleGiftsUnsafe.Clear();
var itemCompName = _componentFactory.GetComponentName(typeof(ItemComponent));
var mapGridCompName = _componentFactory.GetComponentName(typeof(MapGridComponent));
var physicsCompName = _componentFactory.GetComponentName(typeof(PhysicsComponent));
var itemCompName = Factory.GetComponentName<ItemComponent>();
var mapGridCompName = Factory.GetComponentName<MapGridComponent>();
var physicsCompName = Factory.GetComponentName<PhysicsComponent>();
if (!_prototype.TryIndex<EntityCategoryPrototype>("ForkFiltered", out var indexedFilter))
return;

View File

@@ -2,6 +2,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Shared.IgnitionSource;
namespace Content.Server.IgnitionSource;
public sealed partial class IgnitionSourceSystem : SharedIgnitionSourceSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;

View File

@@ -1,4 +1,6 @@
using Content.Server.Body.Systems;
using Content.Server.Destructible;
using Content.Server.Examine;
using Content.Server.Polymorph.Components;
using Content.Server.Popups;
using Content.Shared.Body.Components;
@@ -24,6 +26,7 @@ public sealed class ImmovableRodSystem : EntitySystem
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly DestructibleSystem _destructible = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
@@ -127,7 +130,7 @@ public sealed class ImmovableRodSystem : EntitySystem
return;
}
QueueDel(ent);
_destructible.DestroyEntity(ent);
}
private void OnExamined(EntityUid uid, ImmovableRodComponent component, ExaminedEvent args)

View File

@@ -547,6 +547,10 @@ namespace Content.Server.Kitchen.EntitySystems
var ev = new BeingMicrowavedEvent(uid, user);
RaiseLocalEvent(item, ev);
// TODO MICROWAVE SPARKS & EFFECTS
// Various microwaveable entities should probably spawn a spark, play a sound, and generate a pop=up.
// This should probably be handled by the microwave system, with fields in BeingMicrowavedEvent.
if (ev.Handled)
{
UpdateUserInterfaceState(uid, component);

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using System.Linq;
using Content.Server.Construction.Completions;
using Content.Server.Jittering;
using Content.Shared.Jittering;
using Content.Shared.Power;
@@ -38,6 +39,7 @@ namespace Content.Server.Kitchen.EntitySystems
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedDestructibleSystem _destructible = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly JitteringSystem _jitter = default!;
@@ -123,10 +125,7 @@ namespace Content.Server.Kitchen.EntitySystems
if (solution.Volume > containerSolution.AvailableVolume)
continue;
var dev = new DestructionEventArgs();
RaiseLocalEvent(item, dev);
QueueDel(item);
_destructible.DestroyEntity(item);
}
_solutionContainersSystem.TryAddSolution(containerSoln.Value, solution);

View File

@@ -12,6 +12,7 @@ using Content.Shared.Verbs;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Kitchen;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@@ -124,7 +125,7 @@ public sealed class SharpSystem : EntitySystem
if (hasBody)
popupType = PopupType.LargeCaution;
_popupSystem.PopupEntity(Loc.GetString("butcherable-knife-butchered-success", ("target", args.Args.Target.Value), ("knife", uid)),
_popupSystem.PopupEntity(Loc.GetString("butcherable-knife-butchered-success", ("target", args.Args.Target.Value), ("knife", Identity.Entity(uid, EntityManager))),
popupEnt, args.Args.User, popupType);
if (hasBody)
@@ -152,10 +153,10 @@ public sealed class SharpSystem : EntitySystem
var disabled = false;
string? message = null;
// if the user has hands
// and the item they're holding doesn't have the SharpComponent
// if the held item doesn't have SharpComponent
// and the user doesn't have SharpComponent
// disable the verb
if (!TryComp<SharpComponent>(args.Using, out var usingSharpComp) && args.Hands != null)
if (!TryComp<SharpComponent>(args.Using, out var usingSharpComp) && userSharpComp == null)
{
disabled = true;
message = Loc.GetString("butcherable-need-knife",

View File

@@ -1,24 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration.Logs;
using Content.Server.CartridgeLoader;
using Content.Server.CartridgeLoader.Cartridges;
using Content.Server.CartridgeLoader;
using Content.Server.Chat.Managers;
using Content.Server.Discord;
using Content.Server.GameTicking;
using Content.Server.MassMedia.Components;
using Content.Server.Popups;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CartridgeLoader;
using Content.Shared.CCVar;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.IdentityManagement;
using Content.Shared.MassMedia.Components;
using Content.Shared.MassMedia.Systems;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
using Robust.Server;
using Robust.Shared.Audio.Systems;
using Content.Shared.IdentityManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
namespace Content.Server.MassMedia.Systems;
@@ -34,11 +43,36 @@ public sealed class NewsSystem : SharedNewsSystem
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
private WebhookIdentifier? _webhookId = null;
private Color _webhookEmbedColor;
private bool _webhookSendDuringRound;
public override void Initialize()
{
base.Initialize();
// Discord hook
_cfg.OnValueChanged(CCVars.DiscordNewsWebhook,
value =>
{
if (!string.IsNullOrWhiteSpace(value))
_discord.GetWebhook(value, data => _webhookId = data.ToIdentifier());
}, true);
_cfg.OnValueChanged(CCVars.DiscordNewsWebhookEmbedColor, value =>
{
_webhookEmbedColor = Color.LawnGreen;
if (Color.TryParse(value, out var color))
_webhookEmbedColor = color;
}, true);
_cfg.OnValueChanged(CCVars.DiscordNewsWebhookSendDuringRound, value => _webhookSendDuringRound = value, true);
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEndMessageEvent);
// News writer
SubscribeLocalEvent<NewsWriterComponent, MapInitEvent>(OnMapInit);
@@ -177,6 +211,9 @@ public sealed class NewsSystem : SharedNewsSystem
RaiseLocalEvent(readerUid, ref args);
}
if (_webhookSendDuringRound)
Task.Run(async () => await SendArticleToDiscordWebhook(article));
UpdateWriterDevices();
}
#endregion
@@ -324,4 +361,62 @@ public sealed class NewsSystem : SharedNewsSystem
{
UpdateWriterUi(ent);
}
#region Discord Hook
private void OnRoundEndMessageEvent(RoundEndMessageEvent ev)
{
if (_webhookSendDuringRound)
return;
var query = EntityManager.EntityQueryEnumerator<StationNewsComponent>();
while (query.MoveNext(out _, out var comp))
{
SendArticlesListToDiscordWebhook(comp.Articles.OrderBy(article => article.ShareTime));
}
}
private async void SendArticlesListToDiscordWebhook(IOrderedEnumerable<NewsArticle> articles)
{
foreach (var article in articles)
{
await Task.Delay(TimeSpan.FromSeconds(1)); // TODO: proper discord rate limit handling
await SendArticleToDiscordWebhook(article);
}
}
private async Task SendArticleToDiscordWebhook(NewsArticle article)
{
if (_webhookId is null)
return;
try
{
var embed = new WebhookEmbed
{
Title = article.Title,
// There is no need to cut article content. It's MaxContentLength smaller then discord's limit (4096):
Description = FormattedMessage.RemoveMarkupPermissive(article.Content),
Color = _webhookEmbedColor.ToArgb() & 0xFFFFFF, // HACK: way to get hex without A (transparency)
Footer = new WebhookEmbedFooter
{
Text = Loc.GetString("news-discord-footer",
("server", _baseServer.ServerName),
("round", _ticker.RoundId),
("author", article.Author ?? Loc.GetString("news-discord-unknown-author")),
("time", article.ShareTime.ToString(@"hh\:mm\:ss")))
}
};
var payload = new WebhookPayload { Embeds = [embed] };
await _discord.CreateMessage(_webhookId.Value, payload);
Log.Info("Sent news article to Discord webhook");
}
catch (Exception e)
{
Log.Error($"Error while sending discord news article:\n{e}");
}
}
#endregion
}

View File

@@ -2,7 +2,9 @@ using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.Server.Mind.Commands;
@@ -10,6 +12,7 @@ namespace Content.Server.Mind.Commands;
[AdminCommand(AdminFlags.VarEdit)]
public sealed class RenameCommand : LocalizedEntityCommands
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
@@ -25,7 +28,7 @@ public sealed class RenameCommand : LocalizedEntityCommands
}
var name = args[1];
if (name.Length > IdCardConsoleComponent.MaxFullNameLength)
if (name.Length > _cfgManager.GetCVar(CCVars.MaxNameLength))
{
shell.WriteLine(Loc.GetString("cmd-rename-too-long"));
return;

View File

@@ -51,7 +51,7 @@ public sealed class MindShieldSystem : EntitySystem
}
if (_mindSystem.TryGetMind(implanted, out var mindId, out _) &&
_roleSystem.MindTryRemoveRole<RevolutionaryRoleComponent>(mindId))
_roleSystem.MindRemoveRole<RevolutionaryRoleComponent>(mindId))
{
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted)} was deconverted due to being implanted with a Mindshield.");
}

View File

@@ -11,7 +11,6 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
/// </summary>
public sealed partial class PickAccessibleComponentOperator : HTNOperator
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
private PathfindingSystem _pathfinding = default!;
private EntityLookupSystem _lookup = default!;
@@ -46,7 +45,7 @@ public sealed partial class PickAccessibleComponentOperator : HTNOperator
CancellationToken cancelToken)
{
// Check if the component exists
if (!_factory.TryGetRegistration(Component, out var registration))
if (!_entManager.ComponentFactory.TryGetRegistration(Component, out var registration))
{
return (false, null);
}

View File

@@ -50,10 +50,13 @@ public sealed partial class PathfindingSystem
private void OnTileChange(ref TileChangedEvent ev)
{
if (ev.OldTile.IsEmpty == ev.NewTile.Tile.IsEmpty)
return;
foreach (var change in ev.Changes)
{
if (change.OldTile.IsEmpty == change.NewTile.IsEmpty)
continue;
DirtyChunk(ev.Entity, Comp<MapGridComponent>(ev.Entity).GridTileToLocal(ev.NewTile.GridIndices));
DirtyChunk(ev.Entity, _maps.GridTileToLocal(ev.Entity, ev.Entity.Comp, change.GridIndices));
}
}

View File

@@ -0,0 +1,14 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Returns if the target is below a certain temperature.
/// </summary>
public sealed partial class TargetLowTempCon : UtilityConsideration
{
/// <summary>
/// The minimum temperature they must be.
/// </summary>
[DataField]
public float MinTemp;
}

View File

@@ -7,6 +7,7 @@ using Content.Server.NPC.Queries.Queries;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Storage.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Examine;
@@ -14,7 +15,6 @@ using Content.Shared.Fluids.Components;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.Components;
@@ -376,6 +376,13 @@ public sealed class NPCUtilitySystem : EntitySystem
return 0f;
}
case TargetLowTempCon con:
{
if (!TryComp<TemperatureComponent>(targetUid, out var temperature))
return 0f;
return temperature.CurrentTemperature <= con.MinTemp ? 1f : 0f;
}
default:
throw new NotImplementedException();
}

View File

@@ -13,7 +13,7 @@ namespace Content.Server.NodeContainer.EntitySystems
/// </summary>
/// <seealso cref="NodeGroupSystem"/>
[UsedImplicitly]
public sealed class NodeContainerSystem : EntitySystem
public sealed class NodeContainerSystem : SharedNodeContainerSystem
{
[Dependency] private readonly NodeGroupSystem _nodeGroupSystem = default!;
private EntityQuery<NodeContainerComponent> _query;

View File

@@ -21,8 +21,7 @@ namespace Content.Server.Nuke
/// <summary>
/// Default bomb timer value in seconds.
/// </summary>
[DataField("timer")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int Timer = 300;
/// <summary>
@@ -36,7 +35,7 @@ namespace Content.Server.Nuke
/// How long until the bomb can arm again after deactivation.
/// Used to prevent announcements spam.
/// </summary>
[DataField("cooldown")]
[DataField]
public int Cooldown = 30;
/// <summary>
@@ -143,32 +142,32 @@ namespace Content.Server.Nuke
/// </summary>
public (MapId, EntityUid?)? OriginMapGrid;
[DataField("codeLength")] public int CodeLength = 6;
[ViewVariables] public string Code = string.Empty;
[DataField] public int CodeLength = 6;
[DataField] public string Code = string.Empty;
/// <summary>
/// Time until explosion in seconds.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float RemainingTime;
/// <summary>
/// Time until bomb cooldown will expire in seconds.
/// </summary>
[ViewVariables]
[DataField]
public float CooldownTime;
/// <summary>
/// Current nuclear code buffer. Entered manually by players.
/// If valid it will allow arm/disarm bomb.
/// </summary>
[ViewVariables]
[DataField]
public string EnteredCode = "";
/// <summary>
/// Current status of a nuclear bomb.
/// </summary>
[ViewVariables]
[DataField]
public NukeStatus Status = NukeStatus.AWAIT_DISK;
/// <summary>

View File

@@ -2,6 +2,7 @@ using Content.Server.AlertLevel;
using Content.Server.Audio;
using Content.Server.Chat.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Pinpointer;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@@ -79,11 +80,12 @@ public sealed class NukeSystem : EntitySystem
// Doafter events
SubscribeLocalEvent<NukeComponent, NukeDisarmDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<NukeDiskComponent, BeingMicrowavedEvent>(OnMicrowaved);
}
private void OnInit(EntityUid uid, NukeComponent component, ComponentInit args)
{
component.RemainingTime = component.Timer;
_itemSlots.AddItemSlot(uid, SharedNukeComponent.NukeDiskSlotId, component.DiskSlot);
UpdateStatus(uid, component);
@@ -111,11 +113,13 @@ public sealed class NukeSystem : EntitySystem
private void OnMapInit(EntityUid uid, NukeComponent nuke, MapInitEvent args)
{
nuke.RemainingTime = nuke.Timer;
var originStation = _station.GetOwningStation(uid);
if (originStation != null)
{
nuke.OriginStation = originStation;
}
else
{
var transform = Transform(uid);
@@ -125,6 +129,19 @@ public sealed class NukeSystem : EntitySystem
nuke.Code = GenerateRandomNumberString(nuke.CodeLength);
}
/// <summary>
/// Slightly randomize nuke countdown timer
/// </summary>
private void OnMicrowaved(Entity<NukeDiskComponent> ent, ref BeingMicrowavedEvent args)
{
if (ent.Comp.TimeModifier != null)
return;
var seconds = _random.NextGaussian(ent.Comp.MicrowaveMean.TotalSeconds, ent.Comp.MicrowaveStd.TotalSeconds);
ent.Comp.TimeModifier = TimeSpan.FromSeconds(seconds);
_popups.PopupEntity(Loc.GetString("nuke-disk-component-microwave"), ent.Owner, PopupType.Medium);
}
private void OnRemove(EntityUid uid, NukeComponent component, ComponentRemove args)
{
_itemSlots.RemoveItemSlot(uid, component.DiskSlot);
@@ -346,11 +363,11 @@ public sealed class NukeSystem : EntitySystem
break;
}
// var isValid = _codes.IsCodeValid(uid, component.EnteredCode);
if (component.EnteredCode == component.Code)
{
component.Status = NukeStatus.AWAIT_ARM;
component.RemainingTime = component.Timer;
var modifier = CompOrNull<NukeDiskComponent>(component.DiskSlot.Item)?.TimeModifier ?? TimeSpan.Zero;
component.RemainingTime = MathF.Max(component.Timer + (float)modifier.TotalSeconds, component.MinimumTime);
_audio.PlayPvs(component.AccessGrantedSound, uid);
}
else

View File

@@ -336,6 +336,11 @@ public sealed class FoodSystem : EntitySystem
if (ev.Cancelled)
return;
var attemptEv = new DestructionAttemptEvent();
RaiseLocalEvent(food, attemptEv);
if (attemptEv.Cancelled)
return;
var afterEvent = new AfterFullyEatenEvent(user);
RaiseLocalEvent(food, ref afterEvent);
@@ -436,8 +441,10 @@ public sealed class FoodSystem : EntitySystem
// Check if the food is in the whitelist
if (_whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
return true;
// They can only eat whitelist food and the food isn't in the whitelist. It's not edible.
return false;
// If their diet is whitelist exclusive, then they cannot eat anything but what follows their whitelisted tags. Else, they can eat their tags AND human food.
if (ent.Comp1.IsSpecialDigestibleExclusive)
return false;
}
if (component.RequiresSpecialDigestion)

View File

@@ -178,17 +178,32 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
agentSummary.AppendLine(Loc.GetString(
"objectives-objective-success",
("objective", objectiveTitle),
("markupColor", "green")
("progress", progress)
));
completedObjectives++;
}
else if (progress <= 0.99f && progress >= 0.5f)
{
agentSummary.AppendLine(Loc.GetString(
"objectives-objective-partial-success",
("objective", objectiveTitle),
("progress", progress)
));
}
else if (progress < 0.5f && progress > 0f)
{
agentSummary.AppendLine(Loc.GetString(
"objectives-objective-partial-failure",
("objective", objectiveTitle),
("progress", progress)
));
}
else
{
agentSummary.AppendLine(Loc.GetString(
"objectives-objective-fail",
("objective", objectiveTitle),
("progress", (int) (progress * 100)),
("markupColor", "red")
("progress", progress)
));
}
}

View File

@@ -30,8 +30,9 @@ public sealed class EscapeShuttleConditionSystem : EntitySystem
return 0f;
// You're not escaping if you're restrained!
// Granting 50% as to allow for partial completion of the objective.
if (TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0)
return 0f;
return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 0.5f : 0f;
// Any emergency shuttle counts for this objective, but not pods.
return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 1f : 0f;

View File

@@ -53,6 +53,6 @@ public sealed class HelpProgressConditionSystem : EntitySystem
// require 50% completion for this one to be complete
var completion = total / max;
return completion >= 0.5f ? 1f : completion / 0.5f;
return completion;
}
}

View File

@@ -109,7 +109,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
if (biome.Template == null || !reloads.Modified.TryGetValue(biome.Template, out var proto))
continue;
SetTemplate(uid, biome, (BiomeTemplatePrototype) proto);
SetTemplate(uid, biome, (BiomeTemplatePrototype)proto);
}
}
@@ -257,7 +257,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
private void OnFTLStarted(ref FTLStartedEvent ev)
{
var targetMap = _transform.ToMapCoordinates(ev.TargetCoordinates);
var targetMapUid = _mapManager.GetMapEntityId(targetMap.MapId);
var targetMapUid = _mapSystem.GetMapOrInvalid(targetMap.MapId);
if (!TryComp<BiomeComponent>(targetMapUid, out var biome))
return;
@@ -283,12 +283,12 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
{
for (var y = Math.Floor(aabb.Bottom); y <= Math.Ceiling(aabb.Top); y++)
{
var index = new Vector2i((int) x, (int) y);
var index = new Vector2i((int)x, (int)y);
var chunk = SharedMapSystem.GetChunkIndices(index, ChunkSize);
var mod = biome.ModifiedTiles.GetOrNew(chunk * ChunkSize);
if (!mod.Add(index) || !TryGetBiomeTile(index, biome.Layers, biome.Seed, grid, out var tile))
if (!mod.Add(index) || !TryGetBiomeTile(index, biome.Layers, biome.Seed, (ev.MapUid, grid), out var tile))
continue;
// If we flag it as modified then the tile is never set so need to do it ourselves.
@@ -493,9 +493,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
var layerProto = ProtoManager.Index<BiomeMarkerLayerPrototype>(layer);
var markerSeed = seed + chunk.X * ChunkSize + chunk.Y + localIdx;
var rand = new Random(markerSeed);
var buffer = (int) (layerProto.Radius / 2f);
var buffer = (int)(layerProto.Radius / 2f);
var bounds = new Box2i(chunk + buffer, chunk + layerProto.Size - buffer);
var count = (int) (bounds.Area / (layerProto.Radius * layerProto.Radius));
var count = (int)(bounds.Area / (layerProto.Radius * layerProto.Radius));
count = Math.Min(count, layerProto.MaxCount);
GetMarkerNodes(gridUid, component, grid, layerProto, forced, bounds, count, rand,
@@ -607,7 +607,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
continue;
// Check if mask matches // anything blocking.
TryGetEntity(node, biome, grid, out var proto);
TryGetEntity(node, biome, (gridUid, grid), out var proto);
// If there's an existing entity and it doesn't match the mask then skip.
if (layerProto.EntityMask.Count > 0 &&
@@ -727,14 +727,14 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
continue;
// Need to ensure the tile under it has loaded for anchoring.
if (TryGetBiomeTile(node, component.Layers, seed, grid, out var tile))
if (TryGetBiomeTile(node, component.Layers, seed, (gridUid, grid), out var tile))
{
_mapSystem.SetTile(gridUid, grid, node, tile.Value);
}
string? prototype;
if (TryGetEntity(node, component, grid, out var proto) &&
if (TryGetEntity(node, component, (gridUid, grid), out var proto) &&
layerProto.EntityMask.TryGetValue(proto, out var maskedProto))
{
prototype = maskedProto;
@@ -793,7 +793,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
if (_mapSystem.TryGetTileRef(gridUid, grid, indices, out var tileRef) && !tileRef.Tile.IsEmpty)
continue;
if (!TryGetBiomeTile(indices, component.Layers, seed, grid, out var biomeTile))
if (!TryGetBiomeTile(indices, component.Layers, seed, (gridUid, grid), out var biomeTile))
continue;
_tiles.Add((indices, biomeTile.Value));
@@ -819,7 +819,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
// Don't mess with anything that's potentially anchored.
var anchored = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, indices);
if (anchored.MoveNext(out _) || !TryGetEntity(indices, component, grid, out var entPrototype))
if (anchored.MoveNext(out _) || !TryGetEntity(indices, component, (gridUid, grid), out var entPrototype))
continue;
// TODO: Fix non-anchored ents spawning.
@@ -852,7 +852,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
// Don't mess with anything that's potentially anchored.
var anchored = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, indices);
if (anchored.MoveNext(out _) || !TryGetDecals(indices, component.Layers, seed, grid, out var decals))
if (anchored.MoveNext(out _) || !TryGetDecals(indices, component.Layers, seed, (gridUid, grid), out var decals))
continue;
foreach (var decal in decals)
@@ -966,7 +966,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
continue;
// Don't mess with anything that's potentially anchored.
var anchored = grid.GetAnchoredEntitiesEnumerator(indices);
var anchored = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, indices);
if (anchored.MoveNext(out _))
{
@@ -1011,7 +1011,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
return;
EnsureComp<MapGridComponent>(mapUid);
var biome = (BiomeComponent) EntityManager.ComponentFactory.GetComponent(typeof(BiomeComponent));
var biome = EntityManager.ComponentFactory.GetComponent<BiomeComponent>();
seed ??= _random.Next();
SetSeed(mapUid, biome, seed.Value, false);
SetTemplate(mapUid, biome, biomeTemplate, false);
@@ -1040,8 +1040,8 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
EnsureComp<SunShadowCycleComponent>(mapUid);
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;
moles[(int)Gas.Oxygen] = 21.824779f;
moles[(int)Gas.Nitrogen] = 82.10312f;
var mixture = new GasMixture(moles, Atmospherics.T20C);
@@ -1070,7 +1070,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
continue;
}
if (!TryGetBiomeTile(tileSet.GridIndices, biome.Layers, biome.Seed, mapGrid, out var tile))
if (!TryGetBiomeTile(tileSet.GridIndices, biome.Layers, biome.Seed, (mapUid, mapGrid), out var tile))
{
continue;
}

View File

@@ -21,7 +21,6 @@ public sealed class PayloadSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
private static readonly ProtoId<TagPrototype> PayloadTag = "Payload";
@@ -92,13 +91,13 @@ public sealed class PayloadSystem : EntitySystem
// ANY payload trigger that gets inserted can grant components. It is up to the construction graphs to determine trigger capacity.
foreach (var (name, data) in trigger.Components)
{
if (!_componentFactory.TryGetRegistration(name, out var registration))
if (!Factory.TryGetRegistration(name, out var registration))
continue;
if (HasComp(uid, registration.Type))
continue;
if (_componentFactory.GetComponent(registration.Type) is not Component component)
if (Factory.GetComponent(registration.Type) is not Component component)
continue;
var temp = (object) component;

View File

@@ -101,30 +101,36 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
private void OnTileChanged(ref TileChangedEvent ev)
{
if (!ev.EmptyChanged || !_navQuery.TryComp(ev.NewTile.GridUid, out var navMap))
if (!_navQuery.TryComp(ev.Entity, out var navMap))
return;
var tile = ev.NewTile.GridIndices;
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
var chunk = EnsureChunk(navMap, chunkOrigin);
// This could be easily replaced in the future to accommodate diagonal tiles
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
ref var tileData = ref chunk.TileData[GetTileIndex(relative)];
if (ev.NewTile.IsSpace(_tileDefManager))
foreach (var change in ev.Changes)
{
tileData = 0;
if (PruneEmpty((ev.NewTile.GridUid, navMap), chunk))
return;
}
else
{
tileData = FloorMask;
}
if (!change.EmptyChanged)
continue;
DirtyChunk((ev.NewTile.GridUid, navMap), chunk);
var tile = change.GridIndices;
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
var chunk = EnsureChunk(navMap, chunkOrigin);
// This could be easily replaced in the future to accommodate diagonal tiles
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
ref var tileData = ref chunk.TileData[GetTileIndex(relative)];
if (change.NewTile.IsSpace(_tileDefManager))
{
tileData = 0;
if (PruneEmpty((ev.Entity, navMap), chunk))
continue;
}
else
{
tileData = FloorMask;
}
DirtyChunk((ev.Entity, navMap), chunk);
}
}
private void DirtyChunk(Entity<NavMapComponent> entity, NavMapChunk chunk)

View File

@@ -28,7 +28,6 @@ namespace Content.Server.Polymorph.Systems;
public sealed partial class PolymorphSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -213,7 +212,7 @@ public sealed partial class PolymorphSystem : EntitySystem
MakeSentientCommand.MakeSentient(child, EntityManager);
var polymorphedComp = _compFact.GetComponent<PolymorphedEntityComponent>();
var polymorphedComp = Factory.GetComponent<PolymorphedEntityComponent>();
polymorphedComp.Parent = uid;
polymorphedComp.Configuration = configuration;
AddComp(child, polymorphedComp);

View File

@@ -31,7 +31,6 @@ namespace Content.Server.Power.EntitySystems
private readonly HashSet<ApcNet> _apcNetReconnectQueue = new();
private EntityQuery<ApcPowerReceiverBatteryComponent> _apcBatteryQuery;
private EntityQuery<AppearanceComponent> _appearanceQuery;
private EntityQuery<BatteryComponent> _batteryQuery;
private BatteryRampPegSolver _solver = new();
@@ -41,7 +40,6 @@ namespace Content.Server.Power.EntitySystems
base.Initialize();
_apcBatteryQuery = GetEntityQuery<ApcPowerReceiverBatteryComponent>();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_batteryQuery = GetEntityQuery<BatteryComponent>();
UpdatesAfter.Add(typeof(NodeGroupSystem));
@@ -317,15 +315,25 @@ namespace Content.Server.Power.EntitySystems
_powerNetReconnectQueue.Clear();
}
private bool IsPoweredCalculate(ApcPowerReceiverComponent comp)
{
return !comp.PowerDisabled
&& (!comp.NeedsPower
|| MathHelper.CloseToPercent(comp.NetworkLoad.ReceivingPower,
comp.Load));
}
public override bool IsPoweredCalculate(SharedApcPowerReceiverComponent comp)
{
return IsPoweredCalculate((ApcPowerReceiverComponent)comp);
}
private void UpdateApcPowerReceiver(float frameTime)
{
var enumerator = AllEntityQuery<ApcPowerReceiverComponent>();
while (enumerator.MoveNext(out var uid, out var apcReceiver))
{
var powered = !apcReceiver.PowerDisabled
&& (!apcReceiver.NeedsPower
|| MathHelper.CloseToPercent(apcReceiver.NetworkLoad.ReceivingPower,
apcReceiver.Load));
var powered = IsPoweredCalculate(apcReceiver);
MetaDataComponent? metadata = null;
@@ -381,9 +389,6 @@ namespace Content.Server.Power.EntitySystems
var ev = new PowerChangedEvent(powered, apcReceiver.NetworkLoad.ReceivingPower);
RaiseLocalEvent(uid, ref ev);
if (_appearanceQuery.TryComp(uid, out var appearance))
_appearance.SetData(uid, PowerDeviceVisuals.Powered, powered, appearance);
}
}

View File

@@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Preferences;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Preferences.Managers
{
@@ -22,5 +24,6 @@ namespace Content.Server.Preferences.Managers
bool HavePreferencesLoaded(ICommonSession session);
Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile);
Task SetConstructionFavorites(NetUserId userId, List<ProtoId<ConstructionPrototype>> favorites);
}
}

View File

@@ -4,11 +4,13 @@ using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared.CCVar;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Preferences;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Preferences.Managers
@@ -26,6 +28,7 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
// Cache player prefs on the server so we don't need as much async hell related to them.
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
@@ -41,6 +44,7 @@ namespace Content.Server.Preferences.Managers
_netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage);
_netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateConstructionFavorites>(HandleUpdateConstructionFavoritesMessage);
_sawmill = _log.GetSawmill("prefs");
}
@@ -68,7 +72,7 @@ namespace Content.Server.Preferences.Managers
return;
}
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, index, curPrefs.AdminOOCColor);
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, index, curPrefs.AdminOOCColor, curPrefs.ConstructionFavorites);
if (ShouldStorePrefs(message.MsgChannel.AuthType))
{
@@ -108,12 +112,28 @@ namespace Content.Server.Preferences.Managers
[slot] = profile
};
prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor);
prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor, curPrefs.ConstructionFavorites);
if (ShouldStorePrefs(session.Channel.AuthType))
await _db.SaveCharacterSlotAsync(userId, profile, slot);
}
public async Task SetConstructionFavorites(NetUserId userId, List<ProtoId<ConstructionPrototype>> favorites)
{
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
{
_sawmill.Error($"Tried to modify user {userId} preferences before they loaded.");
return;
}
var curPrefs = prefsData.Prefs!;
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor, favorites);
var session = _playerManager.GetSessionById(userId);
if (ShouldStorePrefs(session.Channel.AuthType))
await _db.SaveConstructionFavoritesAsync(userId, favorites);
}
private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message)
{
var slot = message.Slot;
@@ -151,7 +171,7 @@ namespace Content.Server.Preferences.Managers
var arr = new Dictionary<int, ICharacterProfile>(curPrefs.Characters);
arr.Remove(slot);
prefsData.Prefs = new PlayerPreferences(arr, nextSlot ?? curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor);
prefsData.Prefs = new PlayerPreferences(arr, nextSlot ?? curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor, curPrefs.ConstructionFavorites);
if (ShouldStorePrefs(message.MsgChannel.AuthType))
{
@@ -166,6 +186,40 @@ namespace Content.Server.Preferences.Managers
}
}
private async void HandleUpdateConstructionFavoritesMessage(MsgUpdateConstructionFavorites message)
{
var userId = message.MsgChannel.UserId;
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
{
_sawmill.Warning($"User {userId} tried to modify preferences before they loaded.");
return;
}
// Validate items in the message so that a modified client cannot freely store a gigabyte of arbitrary data.
var validatedSet = new HashSet<ProtoId<ConstructionPrototype>>();
foreach (var favorite in message.Favorites)
{
if (_prototypeManager.HasIndex(favorite))
validatedSet.Add(favorite);
}
var validatedList = message.Favorites;
if (validatedSet.Count != message.Favorites.Count)
{
// A difference in counts indicates that unrecognized or duplicate IDs are present.
_sawmill.Warning($"User {userId} sent invalid construction favorites.");
validatedList = validatedSet.ToList();
}
var curPrefs = prefsData.Prefs!;
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor, validatedList);
if (ShouldStorePrefs(message.MsgChannel.AuthType))
{
await _db.SaveConstructionFavoritesAsync(userId, validatedList);
}
}
// Should only be called via UserDbDataManager.
public async Task LoadData(ICommonSession session, CancellationToken cancel)
{
@@ -176,8 +230,8 @@ namespace Content.Server.Preferences.Managers
{
PrefsLoaded = true,
Prefs = new PlayerPreferences(
new[] {new KeyValuePair<int, ICharacterProfile>(0, HumanoidCharacterProfile.Random())},
0, Color.Transparent)
new[] { new KeyValuePair<int, ICharacterProfile>(0, HumanoidCharacterProfile.Random()) },
0, Color.Transparent, [])
};
_cachedPlayerPrefs[session.UserId] = prefsData;
@@ -294,7 +348,7 @@ namespace Content.Server.Preferences.Managers
return new PlayerPreferences(prefs.Characters.Select(p =>
{
return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(session, collection));
}), prefs.SelectedCharacterIndex, prefs.AdminOOCColor);
}), prefs.SelectedCharacterIndex, prefs.AdminOOCColor, prefs.ConstructionFavorites);
}
public IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(

View File

@@ -13,14 +13,8 @@ public sealed partial class DungeonJob
/// <summary>
/// <see cref="AutoCablingDunGen"/>
/// </summary>
private async Task PostGen(AutoCablingDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
private async Task PostGen(AutoCablingDunGen gen, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Entities.TryGetValue(DungeonDataKey.Cabling, out var ent))
{
LogDataError(typeof(AutoCablingDunGen));
return;
}
// There's a lot of ways you could do this.
// For now we'll just connect every LV cable in the dungeon.
var cableTiles = new HashSet<Vector2i>();
@@ -157,7 +151,7 @@ public sealed partial class DungeonJob
if (found)
continue;
_entManager.SpawnEntity(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
_entManager.SpawnEntity(gen.Entity, _maps.GridTileToLocal(_gridUid, _grid, tile));
}
}
}

View File

@@ -14,7 +14,7 @@ public sealed partial class DungeonJob
/// <summary>
/// <see cref="BiomeDunGen"/>
/// </summary>
private async Task PostGen(BiomeDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
private async Task PostGen(BiomeDunGen dunGen, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!_prototype.TryIndex(dunGen.BiomeTemplate, out var indexedBiome))
return;
@@ -31,10 +31,10 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(node))
continue;
if (dunGen.TileMask is not null)
{
if (!dunGen.TileMask.Contains(((ContentTileDefinition) _tileDefManager[tileRef.Value.Tile.TypeId]).ID))
if (!dunGen.TileMask.Contains(((ContentTileDefinition)_tileDefManager[tileRef.Value.Tile.TypeId]).ID))
continue;
}
@@ -45,12 +45,12 @@ public sealed partial class DungeonJob
}
// Need to set per-tile to override data.
if (biomeSystem.TryGetTile(node, indexedBiome.Layers, seed, _grid, out var tile))
if (biomeSystem.TryGetTile(node, indexedBiome.Layers, seed, (_gridUid, _grid), out var tile))
{
_maps.SetTile(_gridUid, _grid, node, tile.Value);
}
if (biomeSystem.TryGetDecals(node, indexedBiome.Layers, seed, _grid, out var decals))
if (biomeSystem.TryGetDecals(node, indexedBiome.Layers, seed, (_gridUid, _grid), out var decals))
{
foreach (var decal in decals)
{
@@ -58,7 +58,7 @@ public sealed partial class DungeonJob
}
}
if (biomeSystem.TryGetEntity(node, indexedBiome.Layers, tile ?? tileRef.Value.Tile, seed, _grid, out var entityProto))
if (biomeSystem.TryGetEntity(node, indexedBiome.Layers, tile ?? tileRef.Value.Tile, seed, (_gridUid, _grid), out var entityProto))
{
var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
var xform = xformQuery.Get(ent);

View File

@@ -15,7 +15,7 @@ public sealed partial class DungeonJob
/// <summary>
/// <see cref="BiomeMarkerLayerDunGen"/>
/// </summary>
private async Task PostGen(BiomeMarkerLayerDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
private async Task PostGen(BiomeMarkerLayerDunGen dunGen, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
// If we're adding biome then disable it and just use for markers.
if (_entManager.EnsureComponent(_gridUid, out BiomeComponent biomeComp))

View File

@@ -12,27 +12,13 @@ public sealed partial class DungeonJob
/// <summary>
/// <see cref="BoundaryWallDunGen"/>
/// </summary>
private async Task PostGen(BoundaryWallDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
private async Task PostGen(BoundaryWallDunGen gen, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var protoTileDef) ||
!data.Entities.TryGetValue(DungeonDataKey.Walls, out var wall))
{
_sawmill.Error($"Error finding dungeon data for {nameof(gen)}");
return;
}
var tileDef = _tileDefManager[protoTileDef];
var tileDef = _tileDefManager[gen.Tile];
var tiles = new List<(Vector2i Index, Tile Tile)>(dungeon.RoomExteriorTiles.Count);
if (!data.Entities.TryGetValue(DungeonDataKey.CornerWalls, out var cornerWall))
{
cornerWall = wall;
}
if (cornerWall == default)
{
cornerWall = wall;
}
var wall = gen.Wall;
var cornerWall = gen.CornerWall ?? gen.Wall;
// Spawn wall outline
// - Tiles first

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