Merge remote-tracking branch 'upstream/master' into ed-04-06-2024-upstream

# Conflicts:
#	Resources/Prototypes/Accents/word_replacements.yml
This commit is contained in:
Ed
2024-06-04 18:09:38 +03:00
418 changed files with 3648 additions and 3761 deletions

View File

@@ -1,5 +1,7 @@
using Content.Shared.Access.Systems;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Access.UI
{
@@ -40,7 +42,7 @@ namespace Content.Client.Access.UI
SendMessage(new AgentIDCardJobChangedMessage(newJob));
}
public void OnJobIconChanged(string newJobIconId)
public void OnJobIconChanged(ProtoId<StatusIconPrototype> newJobIconId)
{
SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
}

View File

@@ -38,7 +38,7 @@ namespace Content.Client.Access.UI
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
}
public void SetAllowedIcons(HashSet<string> icons, string currentJobIconId)
public void SetAllowedIcons(HashSet<ProtoId<StatusIconPrototype>> icons, string currentJobIconId)
{
IconGrid.DisposeAllChildren();
@@ -46,10 +46,8 @@ namespace Content.Client.Access.UI
var i = 0;
foreach (var jobIconId in icons)
{
if (!_prototypeManager.TryIndex<StatusIconPrototype>(jobIconId, out var jobIcon))
{
if (!_prototypeManager.TryIndex(jobIconId, out var jobIcon))
continue;
}
String styleBase = StyleBase.ButtonOpenBoth;
var modulo = i % JobIconColumnCount;
@@ -77,7 +75,7 @@ namespace Content.Client.Access.UI
};
jobIconButton.AddChild(jobIconTexture);
jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIcon.ID);
jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIconId);
IconGrid.AddChild(jobIconButton);
if (jobIconId.Equals(currentJobIconId))

View File

@@ -1,55 +0,0 @@
using Content.Shared.Antag;
using Content.Shared.Revolutionary.Components;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Zombies;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Antag;
/// <summary>
/// Used for assigning specified icons for antags.
/// </summary>
public sealed class AntagStatusIconSystem : SharedStatusIconSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RevolutionaryComponent, GetStatusIconsEvent>(GetRevIcon);
SubscribeLocalEvent<ZombieComponent, GetStatusIconsEvent>(GetIcon);
SubscribeLocalEvent<HeadRevolutionaryComponent, GetStatusIconsEvent>(GetIcon);
SubscribeLocalEvent<InitialInfectedComponent, GetStatusIconsEvent>(GetIcon);
}
/// <summary>
/// Adds a Status Icon on an entity if the player is supposed to see it.
/// </summary>
private void GetIcon<T>(EntityUid uid, T comp, ref GetStatusIconsEvent ev) where T: IAntagStatusIconComponent
{
var ent = _player.LocalSession?.AttachedEntity;
var canEv = new CanDisplayStatusIconsEvent(ent);
RaiseLocalEvent(uid, ref canEv);
if (!canEv.Cancelled)
ev.StatusIcons.Add(_prototype.Index(comp.StatusIcon));
}
/// <summary>
/// Adds the Rev Icon on an entity if the player is supposed to see it. This additional function is needed to deal
/// with a special case where if someone is a head rev we only want to display the headrev icon.
/// </summary>
private void GetRevIcon(EntityUid uid, RevolutionaryComponent comp, ref GetStatusIconsEvent ev)
{
if (HasComp<HeadRevolutionaryComponent>(uid))
return;
GetIcon(uid, comp, ref ev);
}
}

View File

@@ -3,6 +3,7 @@ using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Hands;
using Content.Shared.Item;
using Content.Shared.Rounding;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
@@ -150,6 +151,9 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
if (!TryComp<ItemComponent>(uid, out var item))
return;
if (!AppearanceSystem.TryGetData<float>(uid, SolutionContainerVisuals.FillFraction, out var fraction, appearance))
return;
@@ -159,7 +163,8 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
{
var layer = new PrototypeLayerData();
var key = "inhand-" + args.Location.ToString().ToLowerInvariant() + component.InHandsFillBaseName + closestFillSprite;
var heldPrefix = item.HeldPrefix == null ? "inhand-" : $"{item.HeldPrefix}-inhand-";
var key = heldPrefix + args.Location.ToString().ToLowerInvariant() + component.InHandsFillBaseName + closestFillSprite;
layer.State = key;

View File

@@ -35,6 +35,7 @@ public sealed class ShowHealthBarsCommand : LocalizedCommands
var showHealthBarsComponent = new ShowHealthBarsComponent
{
DamageContainers = args.ToList(),
HealthStatusIcon = null,
NetSyncEnabled = false
};

View File

@@ -244,7 +244,7 @@ namespace Content.Client.LateJoin
VerticalAlignment = VAlignment.Center
};
var jobIcon = _prototypeManager.Index<StatusIconPrototype>(prototype.Icon);
var jobIcon = _prototypeManager.Index(prototype.Icon);
icon.Texture = _sprites.Frame0(jobIcon.Icon);
jobSelector.AddChild(icon);

View File

@@ -7,6 +7,7 @@ using Content.Client.Lobby.UI.Loadouts;
using Content.Client.Lobby.UI.Roles;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared._CP14.Humanoid;
using Content.Shared.CCVar;
@@ -467,38 +468,96 @@ namespace Content.Client.Lobby.UI
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
if (traits.Count > 0)
{
foreach (var trait in traits)
{
var selector = new TraitPreferenceSelector(trait);
if (Profile?.TraitPreferences.Contains(trait.ID) == true)
{
selector.Preference = true;
}
else
{
selector.Preference = false;
}
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithTraitPreference(trait.ID, preference);
SetDirty();
};
TraitsList.AddChild(selector);
}
}
else
if (traits.Count < 1)
{
TraitsList.AddChild(new Label
{
// TODO: Localise
Text = "No traits available :(",
Text = Loc.GetString("humanoid-profile-editor-no-traits"),
FontColorOverride = Color.Gray,
});
return;
}
//Setup model
Dictionary<string, List<string>> model = new();
List<string> defaultTraits = new();
model.Add("default", defaultTraits);
foreach (var trait in traits)
{
if (trait.Category == null)
{
defaultTraits.Add(trait.ID);
continue;
}
if (!model.ContainsKey(trait.Category))
{
model.Add(trait.Category, new());
}
model[trait.Category].Add(trait.ID);
}
//Create UI view from model
foreach (var (categoryId, traitId) in model)
{
TraitCategoryPrototype? category = null;
if (categoryId != "default")
{
category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
// Label
TraitsList.AddChild(new Label
{
Text = Loc.GetString(category.Name),
Margin = new Thickness(0, 10, 0, 0),
StyleClasses = { StyleBase.StyleClassLabelHeading },
});
}
List<TraitPreferenceSelector?> selectors = new();
var selectionCount = 0;
foreach (var traitProto in traitId)
{
var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
var selector = new TraitPreferenceSelector(trait);
selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
if (selector.Preference)
selectionCount += trait.Cost;
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithTraitPreference(trait.ID, categoryId, preference);
SetDirty();
RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
};
selectors.Add(selector);
}
// Selection counter
if (category is { MaxTraitPoints: >= 0 })
{
TraitsList.AddChild(new Label
{
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
FontColorOverride = Color.Gray
});
}
foreach (var selector in selectors)
{
if (selector == null)
continue;
if (category is { MaxTraitPoints: >= 0 } &&
selector.Cost + selectionCount > category.MaxTraitPoints)
{
selector.Checkbox.Label.FontColorOverride = Color.Red;
}
TraitsList.AddChild(selector);
}
}
}
@@ -807,7 +866,7 @@ namespace Content.Client.Lobby.UI
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = _prototypeManager.Index<StatusIconPrototype>(job.Icon);
var jobIcon = _prototypeManager.Index(job.Icon);
icon.Texture = jobIcon.Icon.Frame0();
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);

View File

@@ -2,6 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<BoxContainer Name="Container"
Orientation="Horizontal">
<CheckBox Name="Checkbox"/>
<CheckBox Name="Checkbox" Access="Public"/>
</BoxContainer>
</Control>

View File

@@ -9,6 +9,8 @@ namespace Content.Client.Lobby.UI.Roles;
[GenerateTypedNameReferences]
public sealed partial class TraitPreferenceSelector : Control
{
public int Cost;
public bool Preference
{
get => Checkbox.Pressed;
@@ -20,7 +22,12 @@ public sealed partial class TraitPreferenceSelector : Control
public TraitPreferenceSelector(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
Checkbox.Text = Loc.GetString(trait.Name);
var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
text += Loc.GetString(trait.Name);
Cost = trait.Cost;
Checkbox.Text = text;
Checkbox.OnToggled += OnCheckBoxToggled;
if (trait.Description is { } desc)

View File

@@ -2,94 +2,115 @@
using Content.Client.Buckle;
using Content.Client.Gravity;
using Content.Shared.ActionBlocker;
using Content.Shared.Buckle.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Movement.Systems;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Timing;
namespace Content.Client.Movement.Systems;
public sealed class WaddleAnimationSystem : EntitySystem
public sealed class WaddleAnimationSystem : SharedWaddleAnimationSystem
{
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly GravitySystem _gravity = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly BuckleSystem _buckle = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
public override void Initialize()
{
SubscribeLocalEvent<WaddleAnimationComponent, MoveInputEvent>(OnMovementInput);
SubscribeLocalEvent<WaddleAnimationComponent, StartedWaddlingEvent>(OnStartedWalking);
SubscribeLocalEvent<WaddleAnimationComponent, StoppedWaddlingEvent>(OnStoppedWalking);
base.Initialize();
SubscribeAllEvent<StartedWaddlingEvent>(OnStartWaddling);
SubscribeLocalEvent<WaddleAnimationComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<WaddleAnimationComponent, StunnedEvent>(OnStunned);
SubscribeLocalEvent<WaddleAnimationComponent, KnockedDownEvent>(OnKnockedDown);
SubscribeLocalEvent<WaddleAnimationComponent, BuckleChangeEvent>(OnBuckleChange);
SubscribeAllEvent<StoppedWaddlingEvent>(OnStopWaddling);
}
private void OnMovementInput(EntityUid entity, WaddleAnimationComponent component, MoveInputEvent args)
private void OnStartWaddling(StartedWaddlingEvent msg, EntitySessionEventArgs args)
{
// Prediction mitigation. Prediction means that MoveInputEvents are spammed repeatedly, even though you'd assume
// they're once-only for the user actually doing something. As such do nothing if we're just repeating this FoR.
if (!_timing.IsFirstTimePredicted)
{
return;
}
if (!args.HasDirectionalMovement && component.IsCurrentlyWaddling)
{
var stopped = new StoppedWaddlingEvent(entity);
RaiseLocalEvent(entity, ref stopped);
return;
}
// Only start waddling if we're not currently AND we're actually moving.
if (component.IsCurrentlyWaddling || !args.HasDirectionalMovement)
return;
var started = new StartedWaddlingEvent(entity);
RaiseLocalEvent(entity, ref started);
if (TryComp<WaddleAnimationComponent>(GetEntity(msg.Entity), out var comp))
StartWaddling((GetEntity(msg.Entity), comp));
}
private void OnStartedWalking(EntityUid uid, WaddleAnimationComponent component, StartedWaddlingEvent args)
private void OnStopWaddling(StoppedWaddlingEvent msg, EntitySessionEventArgs args)
{
if (_animation.HasRunningAnimation(uid, component.KeyName))
if (TryComp<WaddleAnimationComponent>(GetEntity(msg.Entity), out var comp))
StopWaddling((GetEntity(msg.Entity), comp));
}
private void StartWaddling(Entity<WaddleAnimationComponent> entity)
{
if (_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName))
return;
if (!TryComp<InputMoverComponent>(uid, out var mover))
if (!TryComp<InputMoverComponent>(entity.Owner, out var mover))
return;
if (_gravity.IsWeightless(uid))
if (_gravity.IsWeightless(entity.Owner))
return;
if (!_actionBlocker.CanMove(uid, mover))
if (!_actionBlocker.CanMove(entity.Owner, mover))
return;
// Do nothing if buckled in
if (_buckle.IsBuckled(uid))
if (_buckle.IsBuckled(entity.Owner))
return;
// Do nothing if crit or dead (for obvious reasons)
if (_mobState.IsIncapacitated(uid))
if (_mobState.IsIncapacitated(entity.Owner))
return;
var tumbleIntensity = component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity;
var len = mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength;
PlayWaddleAnimationUsing(
(entity.Owner, entity.Comp),
CalculateAnimationLength(entity.Comp, mover),
CalculateTumbleIntensity(entity.Comp)
);
}
component.LastStep = !component.LastStep;
component.IsCurrentlyWaddling = true;
private static float CalculateTumbleIntensity(WaddleAnimationComponent component)
{
return component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity;
}
private static float CalculateAnimationLength(WaddleAnimationComponent component, InputMoverComponent mover)
{
return mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength;
}
private void OnAnimationCompleted(Entity<WaddleAnimationComponent> entity, ref AnimationCompletedEvent args)
{
if (args.Key != entity.Comp.KeyName)
return;
if (!TryComp<InputMoverComponent>(entity.Owner, out var mover))
return;
PlayWaddleAnimationUsing(
(entity.Owner, entity.Comp),
CalculateAnimationLength(entity.Comp, mover),
CalculateTumbleIntensity(entity.Comp)
);
}
private void StopWaddling(Entity<WaddleAnimationComponent> entity)
{
if (!_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName))
return;
_animation.Stop(entity.Owner, entity.Comp.KeyName);
if (!TryComp<SpriteComponent>(entity.Owner, out var sprite))
return;
sprite.Offset = new Vector2();
sprite.Rotation = Angle.FromDegrees(0);
}
private void PlayWaddleAnimationUsing(Entity<WaddleAnimationComponent> entity, float len, float tumbleIntensity)
{
entity.Comp.LastStep = !entity.Comp.LastStep;
var anim = new Animation()
{
@@ -116,58 +137,13 @@ public sealed class WaddleAnimationSystem : EntitySystem
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(new Vector2(), 0),
new AnimationTrackProperty.KeyFrame(component.HopIntensity, len/2),
new AnimationTrackProperty.KeyFrame(entity.Comp.HopIntensity, len/2),
new AnimationTrackProperty.KeyFrame(new Vector2(), len/2),
}
}
}
};
_animation.Play(uid, anim, component.KeyName);
}
private void OnStoppedWalking(EntityUid uid, WaddleAnimationComponent component, StoppedWaddlingEvent args)
{
StopWaddling(uid, component);
}
private void OnAnimationCompleted(EntityUid uid, WaddleAnimationComponent component, AnimationCompletedEvent args)
{
var started = new StartedWaddlingEvent(uid);
RaiseLocalEvent(uid, ref started);
}
private void OnStunned(EntityUid uid, WaddleAnimationComponent component, StunnedEvent args)
{
StopWaddling(uid, component);
}
private void OnKnockedDown(EntityUid uid, WaddleAnimationComponent component, KnockedDownEvent args)
{
StopWaddling(uid, component);
}
private void OnBuckleChange(EntityUid uid, WaddleAnimationComponent component, BuckleChangeEvent args)
{
StopWaddling(uid, component);
}
private void StopWaddling(EntityUid uid, WaddleAnimationComponent component)
{
if (!component.IsCurrentlyWaddling)
return;
_animation.Stop(uid, component.KeyName);
if (!TryComp<SpriteComponent>(uid, out var sprite))
{
return;
}
sprite.Offset = new Vector2();
sprite.Rotation = Angle.FromDegrees(0);
component.IsCurrentlyWaddling = false;
_animation.Play(entity.Owner, anim, entity.Comp.KeyName);
}
}

View File

@@ -22,6 +22,7 @@ public sealed class TargetOutlineSystem : EntitySystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private bool _enabled = false;
@@ -137,7 +138,7 @@ public sealed class TargetOutlineSystem : EntitySystem
// check the entity whitelist
if (valid && Whitelist != null)
valid = Whitelist.IsValid(entity);
valid = _whitelistSystem.IsWhitelistPass(Whitelist, entity);
// and check the cancellable event
if (valid && ValidationEvent != null)

View File

@@ -1,14 +1,17 @@
using System.Numerics;
using Content.Client.StatusIcon;
using Content.Client.UserInterface.Systems;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using static Robust.Shared.Maths.Color;
namespace Content.Client.Overlays;
@@ -19,19 +22,27 @@ namespace Content.Client.Overlays;
public sealed class EntityHealthBarOverlay : Overlay
{
private readonly IEntityManager _entManager;
private readonly IPrototypeManager _prototype;
private readonly SharedTransformSystem _transform;
private readonly MobStateSystem _mobStateSystem;
private readonly MobThresholdSystem _mobThresholdSystem;
private readonly StatusIconSystem _statusIconSystem;
private readonly ProgressColorSystem _progressColor;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
public HashSet<string> DamageContainers = new();
public ProtoId<StatusIconPrototype>? StatusIcon;
public EntityHealthBarOverlay(IEntityManager entManager)
public EntityHealthBarOverlay(IEntityManager entManager, IPrototypeManager prototype)
{
_entManager = entManager;
_prototype = prototype;
_transform = _entManager.System<SharedTransformSystem>();
_mobStateSystem = _entManager.System<MobStateSystem>();
_mobThresholdSystem = _entManager.System<MobThresholdSystem>();
_statusIconSystem = _entManager.System<StatusIconSystem>();
_progressColor = _entManager.System<ProgressColorSystem>();
}
@@ -44,6 +55,7 @@ public sealed class EntityHealthBarOverlay : Overlay
const float scale = 1f;
var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
_prototype.TryIndex(StatusIcon, out var statusIcon);
var query = _entManager.AllEntityQueryEnumerator<MobThresholdsComponent, MobStateComponent, DamageableComponent, SpriteComponent>();
while (query.MoveNext(out var uid,
@@ -52,31 +64,23 @@ public sealed class EntityHealthBarOverlay : Overlay
out var damageableComponent,
out var spriteComponent))
{
if (_entManager.TryGetComponent<MetaDataComponent>(uid, out var metaDataComponent) &&
metaDataComponent.Flags.HasFlag(MetaDataFlags.InContainer))
{
if (statusIcon != null && !_statusIconSystem.IsVisible((uid, _entManager.GetComponent<MetaDataComponent>(uid)), statusIcon))
continue;
}
// We want the stealth user to still be able to see his health bar himself
if (!xformQuery.TryGetComponent(uid, out var xform) ||
xform.MapID != args.MapId)
{
continue;
}
if (damageableComponent.DamageContainerID == null || !DamageContainers.Contains(damageableComponent.DamageContainerID))
{
continue;
}
// we use the status icon component bounds if specified otherwise use sprite
var bounds = _entManager.GetComponentOrNull<StatusIconComponent>(uid)?.Bounds ?? spriteComponent.Bounds;
var worldPos = _transform.GetWorldPosition(xform, xformQuery);
if (!bounds.Translated(worldPos).Intersects(args.WorldAABB))
{
continue;
}
// we are all progressing towards death every day
if (CalcProgress(uid, mobStateComponent, damageableComponent, mobThresholdsComponent) is not { } deathProgress)

View File

@@ -19,10 +19,10 @@ public sealed class ShowCriminalRecordIconsSystem : EquipmentHudSystem<ShowCrimi
private void OnGetStatusIconsEvent(EntityUid uid, CriminalRecordComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
if (!IsActive)
return;
if (_prototype.TryIndex<StatusIconPrototype>(component.StatusIcon.Id, out var iconPrototype))
if (_prototype.TryIndex(component.StatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -2,6 +2,8 @@ using Content.Shared.Inventory.Events;
using Content.Shared.Overlays;
using Robust.Client.Graphics;
using System.Linq;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
@@ -11,6 +13,7 @@ namespace Content.Client.Overlays;
public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComponent>
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private EntityHealthBarOverlay _overlay = default!;
@@ -18,16 +21,21 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
{
base.Initialize();
_overlay = new(EntityManager);
_overlay = new(EntityManager, _prototype);
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthBarsComponent> component)
{
base.UpdateInternal(component);
foreach (var damageContainerId in component.Components.SelectMany(x => x.DamageContainers))
foreach (var comp in component.Components)
{
_overlay.DamageContainers.Add(damageContainerId);
foreach (var damageContainerId in comp.DamageContainers)
{
_overlay.DamageContainers.Add(damageContainerId);
}
_overlay.StatusIcon = comp.HealthStatusIcon;
}
if (!_overlayMan.HasOverlay<EntityHealthBarOverlay>())

View File

@@ -24,7 +24,6 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
base.Initialize();
SubscribeLocalEvent<DamageableComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthIconsComponent> component)
@@ -46,7 +45,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetStatusIconsEvent args)
{
if (!IsActive || args.InContainer)
if (!IsActive)
return;
var healthIcons = DecideHealthIcons(entity);

View File

@@ -18,7 +18,7 @@ public sealed class ShowHungerIconsSystem : EquipmentHudSystem<ShowHungerIconsCo
private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
if (!IsActive)
return;
if (_hunger.TryGetStatusIconPrototype(component, out var iconPrototype))

View File

@@ -25,7 +25,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponen
private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
if (!IsActive)
return;
var iconId = JobIconForNoId;

View File

@@ -19,10 +19,10 @@ public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem<ShowMindShiel
private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
if (!IsActive)
return;
if (_prototype.TryIndex<StatusIconPrototype>(component.MindShieldStatusIcon.Id, out var iconPrototype))
if (_prototype.TryIndex(component.MindShieldStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -19,11 +19,10 @@ public sealed class ShowSyndicateIconsSystem : EquipmentHudSystem<ShowSyndicateI
private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
if (!IsActive)
return;
if (_prototype.TryIndex<StatusIconPrototype>(component.SyndStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -18,7 +18,7 @@ public sealed class ShowThirstIconsSystem : EquipmentHudSystem<ShowThirstIconsCo
private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent component, ref GetStatusIconsEvent ev)
{
if (!IsActive || ev.InContainer)
if (!IsActive)
return;
if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype))

View File

@@ -1,44 +1,37 @@
using Content.Shared.Antag;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Ghost;
using Content.Shared.Revolutionary;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Revolutionary;
/// <summary>
/// Used for the client to get status icons from other revs.
/// </summary>
public sealed class RevolutionarySystem : EntitySystem
public sealed class RevolutionarySystem : SharedRevolutionarySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RevolutionaryComponent, CanDisplayStatusIconsEvent>(OnCanShowRevIcon);
SubscribeLocalEvent<HeadRevolutionaryComponent, CanDisplayStatusIconsEvent>(OnCanShowRevIcon);
SubscribeLocalEvent<RevolutionaryComponent, GetStatusIconsEvent>(GetRevIcon);
SubscribeLocalEvent<HeadRevolutionaryComponent, GetStatusIconsEvent>(GetHeadRevIcon);
}
/// <summary>
/// Determine whether a client should display the rev icon.
/// </summary>
private void OnCanShowRevIcon<T>(EntityUid uid, T comp, ref CanDisplayStatusIconsEvent args) where T : IAntagStatusIconComponent
private void GetRevIcon(Entity<RevolutionaryComponent> ent, ref GetStatusIconsEvent args)
{
args.Cancelled = !CanDisplayIcon(args.User, comp.IconVisibleToGhost);
if (HasComp<HeadRevolutionaryComponent>(ent))
return;
if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
/// <summary>
/// The criteria that determine whether a client should see Rev/Head rev icons.
/// </summary>
private bool CanDisplayIcon(EntityUid? uid, bool visibleToGhost)
private void GetHeadRevIcon(Entity<HeadRevolutionaryComponent> ent, ref GetStatusIconsEvent args)
{
if (HasComp<HeadRevolutionaryComponent>(uid) || HasComp<RevolutionaryComponent>(uid))
return true;
if (visibleToGhost && HasComp<GhostComponent>(uid))
return true;
return HasComp<ShowRevIconsComponent>(uid);
if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -30,13 +30,12 @@ public sealed class SSDIndicatorSystem : EntitySystem
{
if (component.IsSSD &&
_cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
!args.InContainer &&
!_mobState.IsDead(uid) &&
!HasComp<ActiveNPCComponent>(uid) &&
TryComp<MindContainerComponent>(uid, out var mindContainer) &&
mindContainer.ShowExamineInfo)
{
args.StatusIcons.Add(_prototype.Index<StatusIconPrototype>(component.Icon));
args.StatusIcons.Add(_prototype.Index(component.Icon));
}
}
}

View File

@@ -45,7 +45,7 @@ public sealed class StatusIconOverlay : Overlay
var query = _entity.AllEntityQueryEnumerator<StatusIconComponent, SpriteComponent, TransformComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var sprite, out var xform, out var meta))
{
if (xform.MapID != args.MapId)
if (xform.MapID != args.MapId || !sprite.Visible)
continue;
var bounds = comp.Bounds ?? sprite.Bounds;
@@ -72,6 +72,8 @@ public sealed class StatusIconOverlay : Overlay
foreach (var proto in icons)
{
if (!_statusIcon.IsVisible((uid, meta), proto))
continue;
var curTime = _timing.RealTime;
var texture = _sprite.GetFrame(proto.Icon, curTime);

View File

@@ -1,7 +1,11 @@
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Stealth.Components;
using Content.Shared.Whitelist;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
namespace Content.Client.StatusIcon;
@@ -13,6 +17,8 @@ public sealed class StatusIconSystem : SharedStatusIconSystem
{
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
private bool _globalEnabled;
private bool _localEnabled;
@@ -54,10 +60,34 @@ public sealed class StatusIconSystem : SharedStatusIconSystem
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
return list;
var inContainer = (meta.Flags & MetaDataFlags.InContainer) != 0;
var ev = new GetStatusIconsEvent(list, inContainer);
var ev = new GetStatusIconsEvent(list);
RaiseLocalEvent(uid, ref ev);
return ev.StatusIcons;
}
}
/// <summary>
/// For overlay to check if an entity can be seen.
/// </summary>
public bool IsVisible(Entity<MetaDataComponent> ent, StatusIconData data)
{
var viewer = _playerManager.LocalSession?.AttachedEntity;
// Always show our icons to our entity
if (viewer == ent.Owner)
return true;
if (data.VisibleToGhosts && HasComp<GhostComponent>(viewer))
return true;
if (data.HideInContainer && (ent.Comp.Flags & MetaDataFlags.InContainer) != 0)
return false;
if (data.HideOnStealth && TryComp<StealthComponent>(ent, out var stealth) && stealth.Enabled)
return false;
if (data.ShowTo != null && !_entityWhitelist.IsValid(data.ShowTo, viewer))
return false;
return true;
}
}

View File

@@ -1,4 +1,5 @@
using Content.Client.Interactable.Components;
using Content.Client.StatusIcon;
using Content.Shared.Stealth;
using Content.Shared.Stealth.Components;
using Robust.Client.GameObjects;
@@ -18,6 +19,7 @@ public sealed class StealthSystem : SharedStealthSystem
base.Initialize();
_shader = _protoMan.Index<ShaderPrototype>("Stealth").InstanceUnique();
SubscribeLocalEvent<StealthComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<StealthComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<StealthComponent, BeforePostShaderRenderEvent>(OnShaderRender);
@@ -93,4 +95,3 @@ public sealed class StealthSystem : SharedStealthSystem
args.Sprite.Color = new Color(visibility, visibility, 1, 1);
}
}

View File

@@ -1,6 +1,5 @@
using Content.Shared.Atmos.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.UserInterface.Systems.Atmos.GasTank
{
@@ -30,7 +29,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
protected override void Open()
{
base.Open();
_window = new GasTankWindow(this);
_window = new GasTankWindow(this, EntMan.GetComponent<MetaDataComponent>(Owner).EntityName);
_window.OnClose += Close;
_window.OpenCentered();
}

View File

@@ -10,201 +10,194 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.UserInterface.Systems.Atmos.GasTank
namespace Content.Client.UserInterface.Systems.Atmos.GasTank;
public sealed class GasTankWindow
: BaseWindow
{
public sealed class GasTankWindow
: BaseWindow
private readonly RichTextLabel _lblPressure;
private readonly FloatSpinBox _spbPressure;
private readonly RichTextLabel _lblInternals;
private readonly Button _btnInternals;
public GasTankWindow(GasTankBoundUserInterface owner, string uidName)
{
private GasTankBoundUserInterface _owner;
private readonly Label _lblName;
private readonly BoxContainer _topContainer;
private readonly Control _contentContainer;
Control contentContainer;
BoxContainer topContainer;
TextureButton btnClose;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var rootContainer = new LayoutContainer { Name = "GasTankRoot" };
AddChild(rootContainer);
MouseFilter = MouseFilterMode.Stop;
private readonly IResourceCache _resourceCache = default!;
private readonly RichTextLabel _lblPressure;
private readonly FloatSpinBox _spbPressure;
private readonly RichTextLabel _lblInternals;
private readonly Button _btnInternals;
public GasTankWindow(GasTankBoundUserInterface owner)
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
{
TextureButton btnClose;
_resourceCache = IoCManager.Resolve<IResourceCache>();
_owner = owner;
var rootContainer = new LayoutContainer {Name = "GasTankRoot"};
AddChild(rootContainer);
Texture = panelTex,
Modulate = Color.FromHex("#25252A"),
};
MouseFilter = MouseFilterMode.Stop;
back.SetPatchMargin(StyleBox.Margin.All, 10);
var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
var topPanel = new PanelContainer
{
PanelOverride = back,
MouseFilter = MouseFilterMode.Pass
};
var bottomWrap = new LayoutContainer
{
Name = "BottomWrap"
};
rootContainer.AddChild(topPanel);
rootContainer.AddChild(bottomWrap);
LayoutContainer.SetAnchorPreset(topPanel, LayoutContainer.LayoutPreset.Wide);
LayoutContainer.SetMarginBottom(topPanel, -85);
LayoutContainer.SetAnchorPreset(bottomWrap, LayoutContainer.LayoutPreset.VerticalCenterWide);
LayoutContainer.SetGrowHorizontal(bottomWrap, LayoutContainer.GrowDirection.Both);
var topContainerWrap = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Children =
{
Texture = panelTex,
Modulate = Color.FromHex("#25252A"),
};
back.SetPatchMargin(StyleBox.Margin.All, 10);
var topPanel = new PanelContainer
{
PanelOverride = back,
MouseFilter = MouseFilterMode.Pass
};
var bottomWrap = new LayoutContainer
{
Name = "BottomWrap"
};
rootContainer.AddChild(topPanel);
rootContainer.AddChild(bottomWrap);
LayoutContainer.SetAnchorPreset(topPanel, LayoutContainer.LayoutPreset.Wide);
LayoutContainer.SetMarginBottom(topPanel, -85);
LayoutContainer.SetAnchorPreset(bottomWrap, LayoutContainer.LayoutPreset.VerticalCenterWide);
LayoutContainer.SetGrowHorizontal(bottomWrap, LayoutContainer.GrowDirection.Both);
var topContainerWrap = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Children =
(topContainer = new BoxContainer
{
(_topContainer = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
}),
new Control {MinSize = new Vector2(0, 110)}
}
};
Orientation = LayoutOrientation.Vertical
}),
new Control {MinSize = new Vector2(0, 110)}
}
};
rootContainer.AddChild(topContainerWrap);
rootContainer.AddChild(topContainerWrap);
LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide);
LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide);
var font = _resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
var font = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
var topRow = new BoxContainer
var topRow = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Margin = new Thickness(4, 2, 12, 2),
Children =
{
(new Label
{
Text = uidName,
FontOverride = font,
FontColorOverride = StyleNano.NanoGold,
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0, 0, 20, 0),
}),
(btnClose = new TextureButton
{
StyleClasses = {DefaultWindow.StyleClassWindowCloseButton},
VerticalAlignment = VAlignment.Center
})
}
};
var middle = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#202025") },
Children =
{
(contentContainer = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Margin = new Thickness(8, 4),
})
}
};
topContainer.AddChild(topRow);
topContainer.AddChild(new PanelContainer
{
MinSize = new Vector2(0, 2),
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") }
});
topContainer.AddChild(middle);
topContainer.AddChild(new PanelContainer
{
MinSize = new Vector2(0, 2),
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") }
});
_lblPressure = new RichTextLabel();
contentContainer.AddChild(_lblPressure);
//internals
_lblInternals = new RichTextLabel
{ MinSize = new Vector2(200, 0), VerticalAlignment = VAlignment.Center };
_btnInternals = new Button { Text = Loc.GetString("gas-tank-window-internals-toggle-button") };
contentContainer.AddChild(
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Margin = new Thickness(4, 2, 12, 2),
Children =
{
(_lblName = new Label
{
Text = Loc.GetString("gas-tank-window-label"),
FontOverride = font,
FontColorOverride = StyleNano.NanoGold,
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0, 0, 20, 0),
}),
(btnClose = new TextureButton
{
StyleClasses = {DefaultWindow.StyleClassWindowCloseButton},
VerticalAlignment = VAlignment.Center
})
}
};
var middle = new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#202025")},
Children =
{
(_contentContainer = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Margin = new Thickness(8, 4),
})
}
};
_topContainer.AddChild(topRow);
_topContainer.AddChild(new PanelContainer
{
MinSize = new Vector2(0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")}
});
_topContainer.AddChild(middle);
_topContainer.AddChild(new PanelContainer
{
MinSize = new Vector2(0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")}
Margin = new Thickness(0, 7, 0, 0),
Children = { _lblInternals, _btnInternals }
});
_lblPressure = new RichTextLabel();
_contentContainer.AddChild(_lblPressure);
//internals
_lblInternals = new RichTextLabel
{MinSize = new Vector2(200, 0), VerticalAlignment = VAlignment.Center};
_btnInternals = new Button {Text = Loc.GetString("gas-tank-window-internals-toggle-button") };
_contentContainer.AddChild(
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Margin = new Thickness(0, 7, 0, 0),
Children = {_lblInternals, _btnInternals}
});
// Separator
_contentContainer.AddChild(new Control
{
MinSize = new Vector2(0, 10)
});
_contentContainer.AddChild(new Label
{
Text = Loc.GetString("gas-tank-window-output-pressure-label"),
Align = Label.AlignMode.Center
});
_spbPressure = new FloatSpinBox
{
IsValid = f => f >= 0 || f <= 3000,
Margin = new Thickness(25, 0, 25, 7)
};
_contentContainer.AddChild(_spbPressure);
// Handlers
_spbPressure.OnValueChanged += args =>
{
_owner.SetOutputPressure(args.Value);
};
_btnInternals.OnPressed += args =>
{
_owner.ToggleInternals();
};
btnClose.OnPressed += _ => Close();
}
public void UpdateState(GasTankBoundUserInterfaceState state)
// Separator
contentContainer.AddChild(new Control
{
_lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}")));
_btnInternals.Disabled = !state.CanConnectInternals;
_lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text",
("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
if (state.OutputPressure.HasValue)
{
_spbPressure.Value = state.OutputPressure.Value;
}
}
MinSize = new Vector2(0, 10)
});
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
contentContainer.AddChild(new Label
{
return DragMode.Move;
}
Text = Loc.GetString("gas-tank-window-output-pressure-label"),
Align = Label.AlignMode.Center
});
_spbPressure = new FloatSpinBox
{
IsValid = f => f >= 0 || f <= 3000,
Margin = new Thickness(25, 0, 25, 7)
};
contentContainer.AddChild(_spbPressure);
protected override bool HasPoint(Vector2 point)
// Handlers
_spbPressure.OnValueChanged += args =>
{
return false;
owner.SetOutputPressure(args.Value);
};
_btnInternals.OnPressed += args =>
{
owner.ToggleInternals();
};
btnClose.OnPressed += _ => Close();
}
public void UpdateState(GasTankBoundUserInterfaceState state)
{
_lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}")));
_btnInternals.Disabled = !state.CanConnectInternals;
_lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text",
("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
if (state.OutputPressure.HasValue)
{
_spbPressure.Value = state.OutputPressure.Value;
}
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
return DragMode.Move;
}
protected override bool HasPoint(Vector2 point)
{
return false;
}
}

View File

@@ -1,21 +1,40 @@
using System.Linq;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Zombies;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Zombies;
public sealed class ZombieSystem : EntitySystem
public sealed class ZombieSystem : SharedZombieSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ZombieComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ZombieComponent, CanDisplayStatusIconsEvent>(OnCanDisplayStatusIcons);
SubscribeLocalEvent<InitialInfectedComponent, CanDisplayStatusIconsEvent>(OnCanDisplayStatusIcons);
SubscribeLocalEvent<ZombieComponent, GetStatusIconsEvent>(GetZombieIcon);
SubscribeLocalEvent<InitialInfectedComponent, GetStatusIconsEvent>(GetInitialInfectedIcon);
}
private void GetZombieIcon(Entity<ZombieComponent> ent, ref GetStatusIconsEvent args)
{
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
private void GetInitialInfectedIcon(Entity<InitialInfectedComponent> ent, ref GetStatusIconsEvent args)
{
if (HasComp<ZombieComponent>(ent))
return;
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args)
@@ -31,29 +50,4 @@ public sealed class ZombieSystem : EntitySystem
sprite.LayerSetColor(i, component.SkinColor);
}
}
/// <summary>
/// Determines whether a player should be able to see the StatusIcon for zombies.
/// </summary>
private void OnCanDisplayStatusIcons(EntityUid uid, ZombieComponent component, ref CanDisplayStatusIconsEvent args)
{
if (HasComp<ZombieComponent>(args.User) || HasComp<InitialInfectedComponent>(args.User) || HasComp<ShowZombieIconsComponent>(args.User))
return;
if (component.IconVisibleToGhost && HasComp<GhostComponent>(args.User))
return;
args.Cancelled = true;
}
private void OnCanDisplayStatusIcons(EntityUid uid, InitialInfectedComponent component, ref CanDisplayStatusIconsEvent args)
{
if (HasComp<InitialInfectedComponent>(args.User) && !HasComp<ZombieComponent>(args.User))
return;
if (component.IconVisibleToGhost && HasComp<GhostComponent>(args.User))
return;
args.Cancelled = true;
}
}

View File

@@ -3,8 +3,13 @@ using System.Linq;
using System.Numerics;
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.IdentityManagement;
using Content.Shared.Stacks;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -149,6 +154,80 @@ public sealed class CargoTest
await pair.CleanReturnAsync();
}
/// <summary>
/// Tests to see if any items that are valid for cargo bounties can be sliced into items that
/// are also valid for the same bounty entry.
/// </summary>
[Test]
public async Task NoSliceableBountyArbitrageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var testMap = await pair.CreateTestMap();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var componentFactory = server.ResolveDependency<IComponentFactory>();
var whitelist = entManager.System<EntityWhitelistSystem>();
var cargo = entManager.System<CargoSystem>();
var sliceableSys = entManager.System<SliceableFoodSystem>();
var bounties = protoManager.EnumeratePrototypes<CargoBountyPrototype>().ToList();
await server.WaitAssertion(() =>
{
var mapId = testMap.MapId;
var grid = mapManager.CreateGridEntity(mapId);
var coord = new EntityCoordinates(grid.Owner, 0, 0);
var sliceableEntityProtos = protoManager.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => p.TryGetComponent<SliceableFoodComponent>(out _, componentFactory))
.Select(p => p.ID)
.ToList();
foreach (var proto in sliceableEntityProtos)
{
var ent = entManager.SpawnEntity(proto, coord);
var sliceable = entManager.GetComponent<SliceableFoodComponent>(ent);
// Check each bounty
foreach (var bounty in bounties)
{
// Check each entry in the bounty
foreach (var entry in bounty.Entries)
{
// See if the entity counts as part of this bounty entry
if (!cargo.IsValidBountyEntry(ent, entry))
continue;
// Spawn a slice
var slice = entManager.SpawnEntity(sliceable.Slice, coord);
// See if the slice also counts for this bounty entry
if (!cargo.IsValidBountyEntry(slice, entry))
{
entManager.DeleteEntity(slice);
continue;
}
entManager.DeleteEntity(slice);
// If for some reason it can only make one slice, that's okay, I guess
Assert.That(sliceable.TotalCount, Is.EqualTo(1), $"{proto} counts as part of cargo bounty {bounty.ID} and slices into {sliceable.TotalCount} slices which count for the same bounty!");
}
}
entManager.DeleteEntity(ent);
}
mapManager.DeleteMap(mapId);
});
await pair.CleanReturnAsync();
}
[TestPrototypes]
private const string StackProto = @"

View File

@@ -18,7 +18,7 @@ public sealed class DispenserTest : InteractionTest
ToggleNeedPower();
// Insert beaker
await Interact("Beaker");
await InteractUsing("Beaker");
Assert.That(Hands.ActiveHandEntity, Is.Null);
// Open BUI

View File

@@ -16,10 +16,8 @@ public sealed class ComputerConstruction : InteractionTest
await StartConstruction(Computer);
// Initial interaction (ghost turns into real entity)
await Interact(Steel, 5);
ClientAssertPrototype(ComputerFrame, ClientTarget);
Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
ClientTarget = null;
await InteractUsing(Steel, 5);
ClientAssertPrototype(ComputerFrame, Target);
// Perform construction steps
await Interact(
@@ -41,7 +39,7 @@ public sealed class ComputerConstruction : InteractionTest
await StartDeconstruction(ComputerId);
// Initial interaction turns id computer into generic computer
await Interact(Screw);
await InteractUsing(Screw);
AssertPrototype(ComputerFrame);
// Perform deconstruction steps
@@ -71,7 +69,7 @@ public sealed class ComputerConstruction : InteractionTest
await SpawnTarget(ComputerId);
// Initial interaction turns id computer into generic computer
await Interact(Screw);
await InteractUsing(Screw);
AssertPrototype(ComputerFrame);
// Perform partial deconstruction steps

View File

@@ -17,17 +17,14 @@ public sealed class GrilleWindowConstruction : InteractionTest
{
// Construct Grille
await StartConstruction(Grille);
await Interact(Rod, 10);
ClientAssertPrototype(Grille, ClientTarget);
Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
await InteractUsing(Rod, 10);
ClientAssertPrototype(Grille, Target);
var grille = Target;
// Construct Window
await StartConstruction(Window);
await Interact(Glass, 10);
ClientAssertPrototype(Window, ClientTarget);
Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
await InteractUsing(Glass, 10);
ClientAssertPrototype(Window, Target);
// Deconstruct Window
await Interact(Screw, Wrench);
@@ -35,7 +32,7 @@ public sealed class GrilleWindowConstruction : InteractionTest
// Deconstruct Grille
Target = grille;
await Interact(Cut);
await InteractUsing(Cut);
AssertDeleted();
}

View File

@@ -14,9 +14,8 @@ public sealed class MachineConstruction : InteractionTest
public async Task ConstructProtolathe()
{
await StartConstruction(MachineFrame);
await Interact(Steel, 5);
ClientAssertPrototype(Unfinished, ClientTarget);
Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
await InteractUsing(Steel, 5);
ClientAssertPrototype(Unfinished, Target);
await Interact(Wrench, Cable);
AssertPrototype(MachineFrame);
await Interact(ProtolatheBoard, Bin1, Bin1, Manipulator1, Manipulator1, Beaker, Beaker, Screw);
@@ -51,7 +50,7 @@ public sealed class MachineConstruction : InteractionTest
AssertPrototype(MachineFrame);
// Change it into an autolathe
await Interact("AutolatheMachineCircuitboard");
await InteractUsing("AutolatheMachineCircuitboard");
AssertPrototype(MachineFrame);
await Interact(Bin1, Bin1, Bin1, Manipulator1, Glass, Screw);
AssertPrototype("Autolathe");

View File

@@ -19,21 +19,21 @@ public sealed class PanelScrewing : InteractionTest
// Open & close panel
Assert.That(comp.Open, Is.False);
await Interact(Screw);
await InteractUsing(Screw);
Assert.That(comp.Open, Is.True);
await Interact(Screw);
await InteractUsing(Screw);
Assert.That(comp.Open, Is.False);
// Interrupted DoAfters
await Interact(Screw, awaitDoAfters: false);
await InteractUsing(Screw, awaitDoAfters: false);
await CancelDoAfters();
Assert.That(comp.Open, Is.False);
await Interact(Screw);
await InteractUsing(Screw);
Assert.That(comp.Open, Is.True);
await Interact(Screw, awaitDoAfters: false);
await InteractUsing(Screw, awaitDoAfters: false);
await CancelDoAfters();
Assert.That(comp.Open, Is.True);
await Interact(Screw);
await InteractUsing(Screw);
Assert.That(comp.Open, Is.False);
}
}

View File

@@ -13,9 +13,9 @@ public sealed class PlaceableDeconstruction : InteractionTest
{
await StartDeconstruction("Table");
Assert.That(Comp<PlaceableSurfaceComponent>().IsPlaceable);
await Interact(Wrench);
await InteractUsing(Wrench);
AssertPrototype("TableFrame");
await Interact(Wrench);
await InteractUsing(Wrench);
AssertDeleted();
await AssertEntityLookup((Steel, 1), (Rod, 2));
}

View File

@@ -12,11 +12,10 @@ public sealed class WallConstruction : InteractionTest
public async Task ConstructWall()
{
await StartConstruction(Wall);
await Interact(Steel, 2);
await InteractUsing(Steel, 2);
Assert.That(Hands.ActiveHandEntity, Is.Null);
ClientAssertPrototype(Girder, ClientTarget);
Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
await Interact(Steel, 2);
ClientAssertPrototype(Girder, Target);
await InteractUsing(Steel, 2);
Assert.That(Hands.ActiveHandEntity, Is.Null);
AssertPrototype(WallSolid);
}
@@ -25,7 +24,7 @@ public sealed class WallConstruction : InteractionTest
public async Task DeconstructWall()
{
await StartDeconstruction(WallSolid);
await Interact(Weld);
await InteractUsing(Weld);
AssertPrototype(Girder);
await Interact(Wrench, Screw);
AssertDeleted();

View File

@@ -11,8 +11,8 @@ public sealed class WindowConstruction : InteractionTest
public async Task ConstructWindow()
{
await StartConstruction(Window);
await Interact(Glass, 5);
ClientAssertPrototype(Window, ClientTarget);
await InteractUsing(Glass, 5);
ClientAssertPrototype(Window, Target);
}
[Test]
@@ -28,8 +28,8 @@ public sealed class WindowConstruction : InteractionTest
public async Task ConstructReinforcedWindow()
{
await StartConstruction(RWindow);
await Interact(RGlass, 5);
ClientAssertPrototype(RWindow, ClientTarget);
await InteractUsing(RGlass, 5);
ClientAssertPrototype(RWindow, Target);
}
[Test]

View File

@@ -24,7 +24,7 @@ public sealed class WindowRepair : InteractionTest
Assert.That(comp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero));
// Repair the entity
await Interact(Weld);
await InteractUsing(Weld);
Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero));
// Validate that we can still deconstruct the entity (i.e., that welding deconstruction is not blocked).

View File

@@ -3,6 +3,7 @@ using Content.Server.Destructible.Thresholds;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Destructible.Thresholds;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes;

View File

@@ -16,31 +16,31 @@ public sealed class DoAfterCancellationTests : InteractionTest
public async Task CancelWallDeconstruct()
{
await StartDeconstruction(WallConstruction.WallSolid);
await Interact(Weld, awaitDoAfters: false);
await InteractUsing(Weld, awaitDoAfters: false);
// Failed do-after has no effect
await CancelDoAfters();
AssertPrototype(WallConstruction.WallSolid);
// Second attempt works fine
await Interact(Weld);
await InteractUsing(Weld);
AssertPrototype(WallConstruction.Girder);
// Repeat for wrenching interaction
AssertAnchored();
await Interact(Wrench, awaitDoAfters: false);
await InteractUsing(Wrench, awaitDoAfters: false);
await CancelDoAfters();
AssertAnchored();
AssertPrototype(WallConstruction.Girder);
await Interact(Wrench);
await InteractUsing(Wrench);
AssertAnchored(false);
// Repeat for screwdriver interaction.
AssertExists();
await Interact(Screw, awaitDoAfters: false);
await InteractUsing(Screw, awaitDoAfters: false);
await CancelDoAfters();
AssertExists();
await Interact(Screw);
await InteractUsing(Screw);
AssertDeleted();
}
@@ -48,17 +48,16 @@ public sealed class DoAfterCancellationTests : InteractionTest
public async Task CancelWallConstruct()
{
await StartConstruction(WallConstruction.Wall);
await Interact(Steel, 5, awaitDoAfters: false);
await InteractUsing(Steel, 5, awaitDoAfters: false);
await CancelDoAfters();
await Interact(Steel, 5);
ClientAssertPrototype(WallConstruction.Girder, ClientTarget);
Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
await Interact(Steel, 5, awaitDoAfters: false);
await InteractUsing(Steel, 5);
ClientAssertPrototype(WallConstruction.Girder, Target);
await InteractUsing(Steel, 5, awaitDoAfters: false);
await CancelDoAfters();
AssertPrototype(WallConstruction.Girder);
await Interact(Steel, 5);
await InteractUsing(Steel, 5);
AssertPrototype(WallConstruction.WallSolid);
}
@@ -66,11 +65,11 @@ public sealed class DoAfterCancellationTests : InteractionTest
public async Task CancelTilePry()
{
await SetTile(Floor);
await Interact(Pry, awaitDoAfters: false);
await InteractUsing(Pry, awaitDoAfters: false);
await CancelDoAfters();
await AssertTile(Floor);
await Interact(Pry);
await InteractUsing(Pry);
await AssertTile(Plating);
}
@@ -78,7 +77,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
public async Task CancelRepeatedTilePry()
{
await SetTile(Floor);
await Interact(Pry, awaitDoAfters: false);
await InteractUsing(Pry, awaitDoAfters: false);
await RunTicks(1);
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
await AssertTile(Floor);
@@ -89,7 +88,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
await AssertTile(Floor);
// Third do after will work fine
await Interact(Pry);
await InteractUsing(Pry);
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
await AssertTile(Plating);
}
@@ -102,7 +101,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
Assert.That(comp.IsWelded, Is.False);
await Interact(Weld, awaitDoAfters: false);
await InteractUsing(Weld, awaitDoAfters: false);
await RunTicks(1);
Assert.Multiple(() =>
{
@@ -120,7 +119,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
});
// Third do after will work fine
await Interact(Weld);
await InteractUsing(Weld);
Assert.Multiple(() =>
{
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
@@ -128,7 +127,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
});
// Repeat test for un-welding
await Interact(Weld, awaitDoAfters: false);
await InteractUsing(Weld, awaitDoAfters: false);
await RunTicks(1);
Assert.Multiple(() =>
{
@@ -141,7 +140,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
Assert.That(comp.IsWelded, Is.True);
});
await Interact(Weld);
await InteractUsing(Weld);
Assert.Multiple(() =>
{
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));

View File

@@ -22,7 +22,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
});
// Remove the key
await Interact(Screw);
await InteractUsing(Screw);
Assert.Multiple(() =>
{
Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -34,7 +34,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
await AssertEntityLookup(("EncryptionKeyCommon", 1));
// Re-insert a key.
await Interact("EncryptionKeyCentCom");
await InteractUsing("EncryptionKeyCentCom");
Assert.Multiple(() =>
{
Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1));
@@ -59,7 +59,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
});
// cannot remove keys without opening panel
await Interact(Pry);
await InteractUsing(Pry);
Assert.Multiple(() =>
{
Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.GreaterThan(0));
@@ -68,7 +68,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
});
// Open panel
await Interact(Screw);
await InteractUsing(Screw);
Assert.Multiple(() =>
{
Assert.That(panel.Open, Is.True);
@@ -79,7 +79,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
});
// Now remove the keys
await Interact(Pry);
await InteractUsing(Pry);
Assert.Multiple(() =>
{
Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -87,7 +87,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
});
// Reinsert a key
await Interact("EncryptionKeyCentCom");
await InteractUsing("EncryptionKeyCentCom");
Assert.Multiple(() =>
{
Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1));
@@ -97,7 +97,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
});
// Remove it again
await Interact(Pry);
await InteractUsing(Pry);
Assert.Multiple(() =>
{
Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -106,7 +106,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
// Prying again will start deconstructing the machine.
AssertPrototype("TelecomServerFilled");
await Interact(Pry);
await InteractUsing(Pry);
AssertPrototype("MachineFrame");
}
}

View File

@@ -0,0 +1,71 @@
using System.Linq;
using Content.Shared.Chemistry.Components;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests;
/// <summary>
/// Tests to see if any entity prototypes specify solution fill level sprites that don't exist.
/// </summary>
[TestFixture]
public sealed class FillLevelSpriteTest
{
private static readonly string[] HandStateNames = ["left", "right"];
[Test]
public async Task FillLevelSpritesExist()
{
await using var pair = await PoolManager.GetServerClient();
var client = pair.Client;
var protoMan = client.ResolveDependency<IPrototypeManager>();
var componentFactory = client.ResolveDependency<IComponentFactory>();
await client.WaitAssertion(() =>
{
var protos = protoMan.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => p.TryGetComponent<SolutionContainerVisualsComponent>(out _, componentFactory))
.OrderBy(p => p.ID)
.ToList();
foreach (var proto in protos)
{
Assert.That(proto.TryGetComponent<SolutionContainerVisualsComponent>(out var visuals, componentFactory));
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory));
var rsi = sprite.BaseRSI;
// Test base sprite fills
if (!string.IsNullOrEmpty(visuals.FillBaseName))
{
for (var i = 1; i <= visuals.MaxFillLevels; i++)
{
var state = $"{visuals.FillBaseName}{i}";
Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
MaxFillLevels = {visuals.MaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
}
}
// Test inhand sprite fills
if (!string.IsNullOrEmpty(visuals.InHandsFillBaseName))
{
for (var i = 1; i <= visuals.InHandsMaxFillLevels; i++)
{
foreach (var handname in HandStateNames)
{
var state = $"inhand-{handname}{visuals.InHandsFillBaseName}{i}";
Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
InHandsMaxFillLevels = {visuals.InHandsMaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
}
}
}
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -1,9 +1,9 @@
#nullable enable
using Content.Server.GameTicking;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Presets;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.GameRules;

View File

@@ -114,8 +114,8 @@ public sealed class NukeOpsTest
// The game rule exists, and all the stations/shuttles/maps are properly initialized
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
var mapRule = entMan.AllComponents<LoadMapRuleComponent>().Single().Component;
foreach (var grid in mapRule.MapGrids)
var gridsRule = entMan.AllComponents<RuleGridsComponent>().Single().Component;
foreach (var grid in gridsRule.MapGrids)
{
Assert.That(entMan.EntityExists(grid));
Assert.That(entMan.HasComponent<MapGridComponent>(grid));
@@ -129,7 +129,7 @@ public sealed class NukeOpsTest
Assert.That(entMan.EntityExists(nukieShuttlEnt));
EntityUid? nukieStationEnt = null;
foreach (var grid in mapRule.MapGrids)
foreach (var grid in gridsRule.MapGrids)
{
if (entMan.HasComponent<StationMemberComponent>(grid))
{
@@ -144,8 +144,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.EntityExists(nukieStation.Station));
Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
Assert.That(server.MapMan.MapExists(mapRule.Map));
var nukieMap = mapSys.GetMap(mapRule.Map!.Value);
Assert.That(server.MapMan.MapExists(gridsRule.Map));
var nukieMap = mapSys.GetMap(gridsRule.Map!.Value);
var targetStation = entMan.GetComponent<StationDataComponent>(rule.TargetStation!.Value);
var targetGrid = targetStation.Grids.First();

View File

@@ -1,9 +1,9 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking.Commands;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.CCVar;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;

View File

@@ -33,7 +33,7 @@ public abstract partial class InteractionTest
public int Quantity;
/// <summary>
/// If true, a check has been performed to see if the prototype ia an entity prototype with a stack component,
/// If true, a check has been performed to see if the prototype is an entity prototype with a stack component,
/// in which case the specifier was converted into a stack-specifier
/// </summary>
public bool Converted;
@@ -100,7 +100,7 @@ public abstract partial class InteractionTest
if (!ProtoMan.TryIndex<EntityPrototype>(spec.Prototype, out var entProto))
{
Assert.Fail($"Unkown prototype: {spec.Prototype}");
Assert.Fail($"Unknown prototype: {spec.Prototype}");
return default;
}
@@ -120,7 +120,7 @@ public abstract partial class InteractionTest
/// <summary>
/// Convert an entity-uid to a matching entity specifier. Useful when doing entity lookups & checking that the
/// right quantity of entities/materials werre produced. Returns null if passed an entity with a null prototype.
/// right quantity of entities/materials were produced. Returns null if passed an entity with a null prototype.
/// </summary>
protected EntitySpecifier? ToEntitySpecifier(EntityUid uid)
{

View File

@@ -14,6 +14,7 @@ using Content.Shared.Construction.Prototypes;
using Content.Shared.Gravity;
using Content.Shared.Item;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
@@ -44,8 +45,9 @@ public abstract partial class InteractionTest
return;
var comp = CEntMan.GetComponent<ConstructionGhostComponent>(clientTarget!.Value);
ClientTarget = clientTarget;
ConstructionGhostId = comp.Owner.Id;
Target = CEntMan.GetNetEntity(clientTarget.Value);
Assert.That(Target.Value.IsClientSide());
ConstructionGhostId = clientTarget.Value.GetHashCode();
});
await RunTicks(1);
@@ -129,21 +131,20 @@ public abstract partial class InteractionTest
/// <summary>
/// Place an entity prototype into the players hand. Deletes any currently held entity.
/// </summary>
/// <remarks>
/// Automatically enables welders.
/// </remarks>
protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableWelder = true)
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableToggleable = true)
{
return await PlaceInHands((id, quantity), enableWelder);
return await PlaceInHands((id, quantity), enableToggleable);
}
/// <summary>
/// Place an entity prototype into the players hand. Deletes any currently held entity.
/// </summary>
/// <remarks>
/// Automatically enables welders.
/// </remarks>
protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableWelder = true)
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
{
if (Hands.ActiveHand == null)
{
@@ -165,7 +166,7 @@ public abstract partial class InteractionTest
Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
// turn on welders
if (enableWelder && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
{
Assert.That(ItemToggleSys.TryActivate(item, playerEnt, itemToggle: itemToggle));
}
@@ -173,7 +174,7 @@ public abstract partial class InteractionTest
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
if (enableWelder && itemToggle != null)
if (enableToggleable && itemToggle != null)
Assert.That(itemToggle.Activated);
return SEntMan.GetNetEntity(item);
@@ -254,21 +255,20 @@ public abstract partial class InteractionTest
/// <summary>
/// Place an entity prototype into the players hand and interact with the given entity (or target position)
/// </summary>
/// <remarks>
/// Empty strings imply empty hands.
/// </remarks>
protected async Task Interact(string id, int quantity = 1, bool shouldSucceed = true, bool awaitDoAfters = true)
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
{
await Interact((id, quantity), shouldSucceed, awaitDoAfters);
await InteractUsing((id, quantity), awaitDoAfters);
}
/// <summary>
/// Place an entity prototype into the players hand and interact with the given entity (or target position)
/// Place an entity prototype into the players hand and interact with the given entity (or target position).
/// </summary>
/// <remarks>
/// Empty strings imply empty hands.
/// </remarks>
protected async Task Interact(EntitySpecifier entity, bool shouldSucceed = true, bool awaitDoAfters = true)
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
{
// For every interaction, we will also examine the entity, just in case this breaks something, somehow.
// (e.g., servers attempt to assemble construction examine hints).
@@ -278,38 +278,80 @@ public abstract partial class InteractionTest
}
await PlaceInHands(entity);
await Interact(shouldSucceed, awaitDoAfters);
await Interact(awaitDoAfters);
}
/// <summary>
/// Interact with an entity using the currently held entity.
/// </summary>
protected async Task Interact(bool shouldSucceed = true, bool awaitDoAfters = true)
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task Interact(bool awaitDoAfters = true)
{
var clientTarget = ClientTarget;
if ((clientTarget?.IsValid() != true || CEntMan.Deleted(clientTarget)) && (Target == null || Target.Value.IsValid()))
if (Target == null || !Target.Value.IsClientSide())
{
await Server.WaitPost(() => InteractSys.UserInteraction(SEntMan.GetEntity(Player), SEntMan.GetCoordinates(TargetCoords), SEntMan.GetEntity(Target)));
await RunTicks(1);
await Interact(Target, TargetCoords, awaitDoAfters);
return;
}
else
{
// The entity is client-side, so attempt to start construction
var clientEnt = ClientTarget ?? CEntMan.GetEntity(Target);
await Client.WaitPost(() => CConSys.TryStartConstruction(clientEnt!.Value));
await RunTicks(5);
}
// The target is a client-side entity, so we will just attempt to start construction under the assumption that
// it is a construction ghost.
await Client.WaitPost(() => CConSys.TryStartConstruction(CTarget!.Value));
await RunTicks(5);
if (awaitDoAfters)
await AwaitDoAfters(shouldSucceed);
await AwaitDoAfters();
await CheckTargetChange(shouldSucceed && awaitDoAfters);
await CheckTargetChange();
}
/// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool)"/>
protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
{
Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
var coords = SEntMan.GetCoordinates(coordinates);
Assert.That(coords.IsValid(SEntMan));
await Interact(sTarget, coords, awaitDoAfters);
}
/// <summary>
/// Variant of <see cref="InteractUsing"/> that performs several interactions using different entities.
/// Interact with an entity using the currently held entity.
/// </summary>
protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
{
Assert.That(SEntMan.TryGetEntity(Player, out var player));
await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
await RunTicks(1);
if (awaitDoAfters)
await AwaitDoAfters();
await CheckTargetChange();
}
/// <summary>
/// Activate an entity.
/// </summary>
protected async Task Activate(NetEntity? target = null, bool awaitDoAfters = true)
{
target ??= Target;
Assert.That(target, Is.Not.Null);
Assert.That(SEntMan.TryGetEntity(target!.Value, out var sTarget));
Assert.That(SEntMan.TryGetEntity(Player, out var player));
await Server.WaitPost(() => InteractSys.InteractionActivate(player!.Value, sTarget!.Value));
await RunTicks(1);
if (awaitDoAfters)
await AwaitDoAfters();
await CheckTargetChange();
}
/// <summary>
/// Variant of <see cref="InteractUsing(string,int,bool)"/> that performs several interactions using different entities.
/// Useful for quickly finishing multiple construction steps.
/// </summary>
/// <remarks>
/// Empty strings imply empty hands.
@@ -318,7 +360,7 @@ public abstract partial class InteractionTest
{
foreach (var spec in specifiers)
{
await Interact(spec);
await InteractUsing(spec);
}
}
@@ -338,7 +380,7 @@ public abstract partial class InteractionTest
/// <summary>
/// Wait for any currently active DoAfters to finish.
/// </summary>
protected async Task AwaitDoAfters(bool shouldSucceed = true, int maxExpected = 1)
protected async Task AwaitDoAfters(int maxExpected = 1)
{
if (!ActiveDoAfters.Any())
return;
@@ -353,13 +395,12 @@ public abstract partial class InteractionTest
await RunTicks(10);
}
if (!shouldSucceed)
return;
foreach (var doAfter in doAfters)
{
Assert.That(!doAfter.Cancelled);
}
await RunTicks(5);
}
/// <summary>
@@ -398,39 +439,28 @@ public abstract partial class InteractionTest
/// Check if the test's target entity has changed. E.g., construction interactions will swap out entities while
/// a structure is being built.
/// </summary>
protected async Task CheckTargetChange(bool shouldSucceed)
protected async Task CheckTargetChange()
{
if (Target == null)
return;
var target = Target.Value;
var originalTarget = Target.Value;
await RunTicks(5);
if (ClientTarget != null && CEntMan.IsClientSide(ClientTarget.Value))
if (Target.Value.IsClientSide() && CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh))
{
Assert.That(CEntMan.Deleted(ClientTarget.Value), Is.EqualTo(shouldSucceed),
$"Construction ghost was {(shouldSucceed ? "not deleted" : "deleted")}.");
if (shouldSucceed)
{
Assert.That(CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh),
$"Failed to get construction entity from ghost Id");
await Client.WaitPost(() => CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}"));
Target = newWeh;
}
CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}");
Target = newWeh;
}
if (STestSystem.EntChanges.TryGetValue(Target.Value, out var newServerWeh))
{
await Server.WaitPost(
() => SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}"));
SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}");
Target = newServerWeh;
}
if (Target != target)
await CheckTargetChange(shouldSucceed);
if (Target != originalTarget)
await CheckTargetChange();
}
#region Asserts
@@ -444,16 +474,10 @@ public abstract partial class InteractionTest
return;
}
var meta = SEntMan.GetComponent<MetaDataComponent>(SEntMan.GetEntity(target.Value));
var meta = CEntMan.GetComponent<MetaDataComponent>(CEntMan.GetEntity(target.Value));
Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
}
protected void ClientAssertPrototype(string? prototype, EntityUid? target)
{
var netEnt = CTestSystem.Ghosts[target.GetHashCode()];
AssertPrototype(prototype, netEnt);
}
protected void AssertPrototype(string? prototype, NetEntity? target = null)
{
target ??= Target;
@@ -699,6 +723,8 @@ public abstract partial class InteractionTest
protected IEnumerable<Shared.DoAfter.DoAfter> ActiveDoAfters
=> DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed);
#region Component
/// <summary>
/// Convenience method to get components on the target. Returns SERVER-SIDE components.
/// </summary>
@@ -708,9 +734,23 @@ public abstract partial class InteractionTest
if (target == null)
Assert.Fail("No target specified");
return SEntMan.GetComponent<T>(SEntMan.GetEntity(target!.Value));
return SEntMan.GetComponent<T>(ToServer(target!.Value));
}
/// <inheritdoc cref="Comp{T}"/>
protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
{
return SEntMan.TryGetComponent(ToServer(target), out comp);
}
/// <inheritdoc cref="Comp{T}"/>
protected bool TryComp<T>([NotNullWhen(true)] out T? comp) where T : IComponent
{
return SEntMan.TryGetComponent(STarget, out comp);
}
#endregion
/// <summary>
/// Set the tile at the target position to some prototype.
/// </summary>
@@ -833,23 +873,70 @@ public abstract partial class InteractionTest
return true;
}
protected bool IsUiOpen(Enum key)
{
if (!TryComp(Player, out UserInterfaceUserComponent? user))
return false;
foreach (var keys in user.OpenInterfaces.Values)
{
if (keys.Contains(key))
return true;
}
return false;
}
#endregion
#region UI
/// <summary>
/// Presses and releases a button on some client-side window. Will fail if the button cannot be found.
/// Attempts to find, and then presses and releases a control on some client-side window.
/// Will fail if the control cannot be found.
/// </summary>
protected async Task ClickControl<TWindow>(string name) where TWindow : BaseWindow
protected async Task ClickControl<TWindow, TControl>(string name, BoundKeyFunction? function = null)
where TWindow : BaseWindow
where TControl : Control
{
await ClickControl(GetControl<TWindow, Control>(name));
var window = GetWindow<TWindow>();
var control = GetControlFromField<TControl>(name, window);
await ClickControl(control, function);
}
/// <summary>
/// Simulates a click and release at the center of some UI Constrol.
/// Attempts to find, and then presses and releases a control on some client-side widget.
/// Will fail if the control cannot be found.
/// </summary>
protected async Task ClickControl(Control control)
protected async Task ClickWidgetControl<TWidget, TControl>(string name, BoundKeyFunction? function = null)
where TWidget : UIWidget, new()
where TControl : Control
{
var widget = GetWidget<TWidget>();
var control = GetControlFromField<TControl>(name, widget);
await ClickControl(control, function);
}
/// <inheritdoc cref="ClickControl{TWindow,TControl}"/>
protected async Task ClickControl<TWindow>(string name, BoundKeyFunction? function = null)
where TWindow : BaseWindow
{
await ClickControl<TWindow, Control>(name, function);
}
/// <inheritdoc cref="ClickWidgetControl{TWidget,TControl}"/>
protected async Task ClickWidgetControl<TWidget>(string name, BoundKeyFunction? function = null)
where TWidget : UIWidget, new()
{
await ClickWidgetControl<TWidget, Control>(name, function);
}
/// <summary>
/// Simulates a click and release at the center of some UI control.
/// </summary>
protected async Task ClickControl(Control control, BoundKeyFunction? function = null)
{
function ??= EngineKeyFunctions.UIClick;
var screenCoords = new ScreenCoordinates(
control.GlobalPixelPosition + control.PixelSize / 2,
control.Window?.Id ?? default);
@@ -858,7 +945,7 @@ public abstract partial class InteractionTest
var relativePixelPos = screenCoords.Position - control.GlobalPixelPosition;
var args = new GUIBoundKeyEventArgs(
EngineKeyFunctions.UIClick,
function.Value,
BoundKeyState.Down,
screenCoords,
default,
@@ -869,7 +956,7 @@ public abstract partial class InteractionTest
await RunTicks(1);
args = new GUIBoundKeyEventArgs(
EngineKeyFunctions.UIClick,
function.Value,
BoundKeyState.Up,
screenCoords,
default,
@@ -881,31 +968,26 @@ public abstract partial class InteractionTest
}
/// <summary>
/// Attempts to find a control on some client-side window. Will fail if the control cannot be found.
/// Attempt to retrieve a control by looking for a field on some other control.
/// </summary>
protected TControl GetControl<TWindow, TControl>(string name)
where TWindow : BaseWindow
/// <remarks>
/// Will fail if the control cannot be found.
/// </remarks>
protected TControl GetControlFromField<TControl>(string name, Control parent)
where TControl : Control
{
var control = GetControl<TWindow>(name);
Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
return (TControl) control;
}
protected Control GetControl<TWindow>(string name) where TWindow : BaseWindow
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
var field = typeof(TWindow).GetField(name, flags);
var prop = typeof(TWindow).GetProperty(name, flags);
var parentType = parent.GetType();
var field = parentType.GetField(name, flags);
var prop = parentType.GetProperty(name, flags);
if (field == null && prop == null)
{
Assert.Fail($"Window {typeof(TWindow).Name} does not have a field or property named {name}");
Assert.Fail($"Window {parentType.Name} does not have a field or property named {name}");
return default!;
}
var window = GetWindow<TWindow>();
var fieldOrProp = field?.GetValue(window) ?? prop?.GetValue(window);
var fieldOrProp = field?.GetValue(parent) ?? prop?.GetValue(parent);
if (fieldOrProp is not Control control)
{
@@ -913,7 +995,59 @@ public abstract partial class InteractionTest
return default!;
}
return control;
Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
return (TControl) control;
}
/// <summary>
/// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
/// </summary>
/// <remarks>
/// Will fail if the control cannot be found.
/// </remarks>
protected TControl GetControlFromChildren<TControl>(Func<TControl, bool> predicate, Control parent, bool recursive = true)
where TControl : Control
{
if (TryGetControlFromChildren(predicate, parent, out var control, recursive))
return control;
Assert.Fail($"Failed to find a {nameof(TControl)} that satisfies the predicate in {parent.Name}");
return default!;
}
/// <summary>
/// Attempt to retrieve a control of a given type by iterating through a control's children.
/// </summary>
protected TControl GetControlFromChildren<TControl>(Control parent, bool recursive = false)
where TControl : Control
{
return GetControlFromChildren<TControl>(static _ => true, parent, recursive);
}
/// <summary>
/// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
/// </summary>
protected bool TryGetControlFromChildren<TControl>(
Func<TControl, bool> predicate,
Control parent,
[NotNullWhen(true)] out TControl? control,
bool recursive = true)
where TControl : Control
{
foreach (var ctrl in parent.Children)
{
if (ctrl is TControl cast && predicate(cast))
{
control = cast;
return true;
}
if (recursive && TryGetControlFromChildren(predicate, ctrl, out control))
return true;
}
control = null;
return false;
}
/// <summary>
@@ -944,7 +1078,6 @@ public abstract partial class InteractionTest
return window != null;
}
/// <summary>
/// Attempts to find a currently open client-side window.
/// </summary>
@@ -962,6 +1095,34 @@ public abstract partial class InteractionTest
return window != null;
}
/// <summary>
/// Attempts to find client-side UI widget.
/// </summary>
protected UIWidget GetWidget<TWidget>()
where TWidget : UIWidget, new()
{
if (TryFindWidget(out TWidget? widget))
return widget;
Assert.Fail($"Could not find a {typeof(TWidget).Name} widget");
return default!;
}
/// <summary>
/// Attempts to find client-side UI widget.
/// </summary>
private bool TryFindWidget<TWidget>([NotNullWhen(true)] out TWidget? uiWidget)
where TWidget : UIWidget, new()
{
uiWidget = null;
var screen = UiMan.ActiveScreen;
if (screen == null)
return false;
return screen.TryGetWidget(out uiWidget);
}
#endregion
#region Power

View File

@@ -1,8 +1,10 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Client.Construction;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.IntegrationTests.Pair;
using Content.Server.Body.Systems;
using Content.Server.Hands.Systems;
@@ -24,6 +26,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.UnitTesting;
using Content.Shared.Item.ItemToggle;
using Robust.Client.State;
namespace Content.IntegrationTests.Tests.Interaction;
@@ -64,15 +67,12 @@ public abstract partial class InteractionTest
/// The player entity that performs all these interactions. Defaults to an admin-observer with 1 hand.
/// </summary>
protected NetEntity Player;
protected EntityUid SPlayer => ToServer(Player);
protected EntityUid CPlayer => ToClient(Player);
protected EntityUid SPlayer;
protected EntityUid CPlayer;
protected ICommonSession ClientSession = default!;
protected ICommonSession ServerSession = default!;
public EntityUid? ClientTarget;
/// <summary>
/// The current target entity. This is the default entity for various helper functions.
/// </summary>
@@ -108,6 +108,7 @@ public abstract partial class InteractionTest
protected InteractionTestSystem STestSystem = default!;
protected SharedTransformSystem Transform = default!;
protected ISawmill SLogger = default!;
protected SharedUserInterfaceSystem SUiSys = default!;
// CLIENT dependencies
protected IEntityManager CEntMan = default!;
@@ -119,6 +120,7 @@ public abstract partial class InteractionTest
protected ExamineSystem ExamineSys = default!;
protected InteractionTestSystem CTestSystem = default!;
protected ISawmill CLogger = default!;
protected SharedUserInterfaceSystem CUiSys = default!;
// player components
protected HandsComponent Hands = default!;
@@ -168,6 +170,7 @@ public abstract partial class InteractionTest
STestSystem = SEntMan.System<InteractionTestSystem>();
Stack = SEntMan.System<StackSystem>();
SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
SUiSys = Client.System<SharedUserInterfaceSystem>();
// client dependencies
CEntMan = Client.ResolveDependency<IEntityManager>();
@@ -179,6 +182,7 @@ public abstract partial class InteractionTest
CConSys = CEntMan.System<ConstructionSystem>();
ExamineSys = CEntMan.System<ExamineSystem>();
CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
CUiSys = Client.System<SharedUserInterfaceSystem>();
// Setup map.
await Pair.CreateTestMap();
@@ -204,15 +208,16 @@ public abstract partial class InteractionTest
old = cPlayerMan.LocalEntity;
Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)));
var serverPlayerEnt = SEntMan.GetEntity(Player);
Server.PlayerMan.SetAttachedEntity(ServerSession, serverPlayerEnt);
Hands = SEntMan.GetComponent<HandsComponent>(serverPlayerEnt);
DoAfters = SEntMan.GetComponent<DoAfterComponent>(serverPlayerEnt);
SPlayer = SEntMan.GetEntity(Player);
Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
Hands = SEntMan.GetComponent<HandsComponent>(SPlayer);
DoAfters = SEntMan.GetComponent<DoAfterComponent>(SPlayer);
});
// Check player got attached.
await RunTicks(5);
Assert.That(CEntMan.GetNetEntity(cPlayerMan.LocalEntity), Is.EqualTo(Player));
CPlayer = ToClient(Player);
Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(CPlayer));
// Delete old player entity.
await Server.WaitPost(() =>
@@ -235,6 +240,10 @@ public abstract partial class InteractionTest
}
});
// Change UI state to in-game.
var state = Client.ResolveDependency<IStateManager>();
await Client.WaitPost(() => state.RequestStateChange<GameplayState>());
// Final player asserts/checks.
await Pair.ReallyBeIdle(5);
Assert.Multiple(() =>

View File

@@ -0,0 +1,35 @@
using System.Linq;
using Content.Shared.Dataset;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Localization;
[TestFixture]
public sealed class LocalizedDatasetPrototypeTest
{
[Test]
public async Task ValidProtoIdsTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoMan = server.ResolveDependency<IPrototypeManager>();
var localizationMan = server.ResolveDependency<ILocalizationManager>();
var protos = protoMan.EnumeratePrototypes<LocalizedDatasetPrototype>().OrderBy(p => p.ID);
// Check each prototype
foreach (var proto in protos)
{
// Check each value in the prototype
foreach (var locId in proto.Values)
{
// Make sure the localization manager has a string for the LocId
Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
}
}
await pair.CleanReturnAsync();
}
}

View File

@@ -22,32 +22,32 @@ public sealed class ModularGrenadeTests : InteractionTest
Target = SEntMan.GetNetEntity(await FindEntity("ModularGrenade"));
await Drop();
await Interact(Cable);
await InteractUsing(Cable);
// Insert & remove trigger
AssertComp<OnUseTimerTriggerComponent>(false);
await Interact(Trigger);
await InteractUsing(Trigger);
AssertComp<OnUseTimerTriggerComponent>();
await FindEntity(Trigger, LookupFlags.Uncontained, shouldSucceed: false);
await Interact(Pry);
await InteractUsing(Pry);
AssertComp<OnUseTimerTriggerComponent>(false);
// Trigger was dropped to floor, not deleted.
await FindEntity(Trigger, LookupFlags.Uncontained);
// Re-insert
await Interact(Trigger);
await InteractUsing(Trigger);
AssertComp<OnUseTimerTriggerComponent>();
// Insert & remove payload.
await Interact(Payload);
await InteractUsing(Payload);
await FindEntity(Payload, LookupFlags.Uncontained, shouldSucceed: false);
await Interact(Pry);
await InteractUsing(Pry);
var ent = await FindEntity(Payload, LookupFlags.Uncontained);
await Delete(ent);
// successfully insert a second time
await Interact(Payload);
await InteractUsing(Payload);
ent = await FindEntity(Payload);
var sys = SEntMan.System<SharedContainerSystem>();
Assert.That(sys.IsEntityInContainer(ent));

View File

@@ -0,0 +1,85 @@
using Content.Shared.Tag;
using Robust.Client.Upload.Commands;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Upload;
namespace Content.IntegrationTests.Tests.PrototypeTests;
public sealed class PrototypeUploadTest
{
public const string IdA = "UploadTestPrototype";
public const string IdB = $"{IdA}NoParent";
public const string IdC = $"{IdA}Abstract";
public const string IdD = $"{IdA}UploadedParent";
private const string File = $@"
- type: entity
parent: BaseStructure # BaseItem can cause AllItemsHaveSpritesTest to fail
id: {IdA}
- type: entity
id: {IdB}
- type: entity
id: {IdC}
abstract: true
components:
- type: Tag
- type: entity
id: {IdD}
parent: {IdC}
";
[Test]
[TestOf(typeof(LoadPrototypeCommand))]
public async Task TestFileUpload()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings {Connected = true});
var sCompFact = pair.Server.ResolveDependency<IComponentFactory>();
var cCompFact = pair.Client.ResolveDependency<IComponentFactory>();
Assert.That(!pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdA, out _));
Assert.That(!pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdB, out _));
Assert.That(!pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdC, out _));
Assert.That(!pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdD, out _));
Assert.That(!pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdA, out _));
Assert.That(!pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdB, out _));
Assert.That(!pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdC, out _));
Assert.That(!pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdD, out _));
var protoLoad = pair.Client.ResolveDependency<IGamePrototypeLoadManager>();
await pair.Client.WaitPost(() => protoLoad.SendGamePrototype(File));
await pair.RunTicksSync(10);
Assert.That(pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdA, out var sProtoA));
Assert.That(pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdB, out var sProtoB));
Assert.That(!pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdC, out _));
Assert.That(pair.Server.ProtoMan.TryIndex<EntityPrototype>(IdD, out var sProtoD));
Assert.That(pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdA, out var cProtoA));
Assert.That(pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdB, out var cProtoB));
Assert.That(!pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdC, out _));
Assert.That(pair.Client.ProtoMan.TryIndex<EntityPrototype>(IdD, out var cProtoD));
// Arbitrarily choosing TagComponent to check that inheritance works for uploaded prototypes.
await pair.Server.WaitPost(() =>
{
Assert.That(sProtoA!.TryGetComponent<TagComponent>(out _, sCompFact), Is.True);
Assert.That(sProtoB!.TryGetComponent<TagComponent>(out _, sCompFact), Is.False);
Assert.That(sProtoD!.TryGetComponent<TagComponent>(out _, sCompFact), Is.True);
});
await pair.Client.WaitPost(() =>
{
Assert.That(cProtoA!.TryGetComponent<TagComponent>(out _, cCompFact), Is.True);
Assert.That(cProtoB!.TryGetComponent<TagComponent>(out _, cCompFact), Is.False);
Assert.That(cProtoD!.TryGetComponent<TagComponent>(out _, cCompFact), Is.True);
});
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,72 @@
using System.Linq;
using Content.Shared.Roles;
using Content.Server.Storage.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Collections;
namespace Content.IntegrationTests.Tests.Roles;
[TestFixture]
public sealed class StartingGearPrototypeStorageTest
{
/// <summary>
/// Checks that a storage fill on a StartingGearPrototype will properly fill
/// </summary>
[Test]
public async Task TestStartingGearStorage()
{
var settings = new PoolSettings { Connected = true, Dirty = true };
await using var pair = await PoolManager.GetServerClient(settings);
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var storageSystem = server.System<StorageSystem>();
var protos = server.ProtoMan
.EnumeratePrototypes<StartingGearPrototype>()
.Where(p => !p.Abstract)
.ToList()
.OrderBy(p => p.ID);
var testMap = await pair.CreateTestMap();
var coords = testMap.GridCoords;
await server.WaitAssertion(() =>
{
foreach (var gearProto in protos)
{
var backpackProto = gearProto.GetGear("back");
if (backpackProto == string.Empty)
continue;
var bag = server.EntMan.SpawnEntity(backpackProto, coords);
var ents = new ValueList<EntityUid>();
foreach (var (slot, entProtos) in gearProto.Storage)
{
if (entProtos.Count == 0)
continue;
foreach (var ent in entProtos)
{
ents.Add(server.EntMan.SpawnEntity(ent, coords));
}
foreach (var ent in ents)
{
if (!storageSystem.CanInsert(bag, ent, out _))
Assert.Fail($"StartingGearPrototype {gearProto.ID} could not successfully put items into storage {bag.Id}");
server.EntMan.DeleteEntity(ent);
}
}
server.EntMan.DeleteEntity(bag);
}
mapManager.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,75 @@
using Content.Client.UserInterface.Systems.Hotbar.Widgets;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Input;
using Content.Shared.PDA;
using Content.Shared.Storage;
using Robust.Client.UserInterface;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Storage;
public sealed class StorageInteractionTest : InteractionTest
{
/// <summary>
/// Check that players can interact with items in storage if the storage UI is open
/// </summary>
[Test]
public async Task UiInteractTest()
{
var sys = Server.System<SharedContainerSystem>();
await SpawnTarget("ClothingBackpack");
var backpack = ToServer(Target);
// Initially no BUI is open.
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.False);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
// Activating the backpack opens the UI
await Activate();
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
// Pick up a PDA
var pda = await PlaceInHands("PassengerPDA");
var sPda = ToServer(pda);
Assert.That(sys.IsEntityInContainer(sPda), Is.True);
Assert.That(sys.TryGetContainingContainer((sPda, null), out var container));
Assert.That(container!.Owner, Is.EqualTo(SPlayer));
// Insert the PDA into the backpack
await Interact();
Assert.That(sys.TryGetContainingContainer((sPda, null), out container));
Assert.That(container!.Owner, Is.EqualTo(backpack));
// Use "e" / ActivateInWorld to open the PDA UI while it is still in the backpack.
var ctrl = GetStorageControl(pda);
await ClickControl(ctrl, ContentKeyFunctions.ActivateItemInWorld);
await RunTicks(10);
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
// Click on the pda to pick it up and remove it from the backpack.
await ClickControl(ctrl, ContentKeyFunctions.MoveStoredItem);
await RunTicks(10);
Assert.That(sys.TryGetContainingContainer((sPda, null), out container));
Assert.That(container!.Owner, Is.EqualTo(SPlayer));
// UIs should still be open
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
}
/// <summary>
/// Retrieve the control that corresponds to the given entity in the currently open storage UI.
/// </summary>
private ItemGridPiece GetStorageControl(NetEntity target)
{
var uid = ToClient(target);
var hotbar = GetWidget<HotbarGui>();
var storageContainer = GetControlFromField<Control>(nameof(HotbarGui.StorageContainer), hotbar);
return GetControlFromChildren<ItemGridPiece>(c => c.Entity == uid, storageContainer);
}
}

View File

@@ -15,10 +15,10 @@ public sealed class TileConstructionTests : InteractionTest
await AssertTile(Plating, PlayerCoords);
AssertGridCount(1);
await SetTile(null);
await Interact(Rod);
await InteractUsing(Rod);
await AssertTile(Lattice);
Assert.That(Hands.ActiveHandEntity, Is.Null);
await Interact(Cut);
await InteractUsing(Cut);
await AssertTile(null);
await AssertEntityLookup((Rod, 1));
AssertGridCount(1);
@@ -43,14 +43,14 @@ public sealed class TileConstructionTests : InteractionTest
// Place Lattice
var oldPos = TargetCoords;
TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
await Interact(Rod);
await InteractUsing(Rod);
TargetCoords = oldPos;
await AssertTile(Lattice);
AssertGridCount(1);
// Cut lattice
Assert.That(Hands.ActiveHandEntity, Is.Null);
await Interact(Cut);
await InteractUsing(Cut);
await AssertTile(null);
AssertGridCount(0);
@@ -76,25 +76,25 @@ public sealed class TileConstructionTests : InteractionTest
// Space -> Lattice
var oldPos = TargetCoords;
TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
await Interact(Rod);
await InteractUsing(Rod);
TargetCoords = oldPos;
await AssertTile(Lattice);
AssertGridCount(1);
// Lattice -> Plating
await Interact(Steel);
await InteractUsing(Steel);
Assert.That(Hands.ActiveHandEntity, Is.Null);
await AssertTile(Plating);
AssertGridCount(1);
// Plating -> Tile
await Interact(FloorItem);
await InteractUsing(FloorItem);
Assert.That(Hands.ActiveHandEntity, Is.Null);
await AssertTile(Floor);
AssertGridCount(1);
// Tile -> Plating
await Interact(Pry);
await InteractUsing(Pry);
await AssertTile(Plating);
AssertGridCount(1);

View File

@@ -18,7 +18,7 @@ public sealed class WeldableTests : InteractionTest
Assert.That(comp.IsWelded, Is.False);
await Interact(Weld);
await InteractUsing(Weld);
Assert.That(comp.IsWelded, Is.True);
AssertPrototype(Locker); // Prototype did not change.
}

View File

@@ -1,5 +1,5 @@
using Content.Shared.StatusIcon;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Robust.Shared.Prototypes;
namespace Content.Server.Access.Components
{
@@ -9,7 +9,7 @@ namespace Content.Server.Access.Components
/// <summary>
/// Set of job icons that the agent ID card can show.
/// </summary>
[DataField("icons", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StatusIconPrototype>))]
public HashSet<string> Icons = new();
[DataField]
public HashSet<ProtoId<StatusIconPrototype>> Icons;
}
}

View File

@@ -90,14 +90,10 @@ namespace Content.Server.Access.Systems
private void OnJobIconChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardJobIconChangedMessage args)
{
if (!TryComp<IdCardComponent>(uid, out var idCard))
{
return;
}
if (!_prototypeManager.TryIndex<StatusIconPrototype>(args.JobIconId, out var jobIcon))
{
if (!_prototypeManager.TryIndex(args.JobIconId, out var jobIcon))
return;
}
_cardSystem.TryChangeJobIcon(uid, jobIcon, idCard);
@@ -109,7 +105,7 @@ namespace Content.Server.Access.Systems
{
foreach (var jobPrototype in _prototypeManager.EnumeratePrototypes<JobPrototype>())
{
if(jobPrototype.Icon == jobIcon.ID)
if (jobPrototype.Icon == jobIcon.ID)
{
job = jobPrototype;
return true;

View File

@@ -129,7 +129,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
_idCard.TryChangeJobTitle(targetId, newJobTitle, player: player);
if (_prototype.TryIndex<JobPrototype>(newJobProto, out var job)
&& _prototype.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
&& _prototype.TryIndex(job.Icon, out var jobIcon))
{
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
_idCard.TryChangeJobDepartment(targetId, job);

View File

@@ -82,9 +82,7 @@ public sealed class PresetIdCardSystem : EntitySystem
_cardSystem.TryChangeJobTitle(uid, job.LocalizedName);
_cardSystem.TryChangeJobDepartment(uid, job);
if (_prototypeManager.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
{
if (_prototypeManager.TryIndex(job.Icon, out var jobIcon))
_cardSystem.TryChangeJobIcon(uid, jobIcon);
}
}
}

View File

@@ -8,13 +8,13 @@ using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Maps;
using Content.Server.RoundEnd;
using Content.Shared.Administration.Managers;
using Content.Shared.CCVar;
using Content.Shared.GameTicking.Components;
using Content.Shared.Prototypes;
using Robust.Server.ServerStatus;
using Robust.Shared.Asynchronous;

View File

@@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Antag.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
@@ -15,6 +14,7 @@ using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
using Content.Shared.Antag;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Players;
@@ -182,20 +182,20 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
.Where(x => GameTicker.PlayerGameStatuses[x.UserId] == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players);
ChooseAntags((uid, component), players, midround: true);
}
/// <summary>
/// Chooses antagonists from the given selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool)
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.SelectionsComplete)
return;
foreach (var def in ent.Comp.Definitions)
{
ChooseAntags(ent, pool, def);
ChooseAntags(ent, pool, def, midround: midround);
}
ent.Comp.SelectionsComplete = true;
@@ -204,17 +204,28 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def)
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def, bool midround = false)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
// if there is both a spawner and players getting picked, let it fall back to a spawner.
var noSpawner = def.SpawnerPrototype == null;
var picking = def.PickPlayer;
if (midround && ent.Comp.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
{
// prevent antag selection from happening if the round is on-going, requiring a spawner if used midround.
// this is so rules like nukies, if added by an admin midround, dont make random living people nukies
Log.Info($"Antags for rule {ent:?} get picked pre-spawn so only spawners will be made.");
DebugTools.Assert(def.SpawnerPrototype != null, $"Rule {ent:?} had no spawner for pre-spawn rule added mid-round!");
picking = false;
}
for (var i = 0; i < count; i++)
{
var session = (ICommonSession?) null;
if (def.PickPlayer)
if (picking)
{
if (!playerPool.TryPickAndTake(RobustRandom, out session) && noSpawner)
{

View File

@@ -1,6 +1,6 @@
using Content.Server.Administration.Systems;
using Content.Server.Destructible.Thresholds;
using Content.Shared.Antag;
using Content.Shared.Destructible.Thresholds;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Whitelist;

View File

@@ -1,7 +1,7 @@
using Content.Server.Antag.Mimic;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.VendingMachines;
using Robust.Shared.Map;
using Robust.Shared.Random;

View File

@@ -300,6 +300,21 @@ public sealed partial class CargoSystem
return IsBountyComplete(GetBountyEntities(container), entries, out bountyEntities);
}
/// <summary>
/// Determines whether the <paramref name="entity"/> meets the criteria for the bounty <paramref name="entry"/>.
/// </summary>
/// <returns>true if <paramref name="entity"/> is a valid item for the bounty entry, otherwise false</returns>
public bool IsValidBountyEntry(EntityUid entity, CargoBountyItemEntry entry)
{
if (!_whitelistSys.IsValid(entry.Whitelist, entity))
return false;
if (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity))
return false;
return true;
}
public bool IsBountyComplete(HashSet<EntityUid> entities, IEnumerable<CargoBountyItemEntry> entries, out HashSet<EntityUid> bountyEntities)
{
bountyEntities = new();
@@ -313,7 +328,7 @@ public sealed partial class CargoSystem
var temp = new HashSet<EntityUid>();
foreach (var entity in entities)
{
if (!_whitelistSys.IsValid(entry.Whitelist, entity) || (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity)))
if (!IsValidBountyEntry(entity, entry))
continue;
count += _stackQuery.CompOrNull(entity)?.Count ?? 1;

View File

@@ -49,18 +49,20 @@ public partial class ChatSystem
/// <param name="hideLog">Whether or not this message should appear in the adminlog window</param>
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
public void TryEmoteWithChat(
EntityUid source,
string emoteId,
ChatTransmitRange range = ChatTransmitRange.Normal,
bool hideLog = false,
string? nameOverride = null,
bool ignoreActionBlocker = false
bool ignoreActionBlocker = false,
bool forceEmote = false
)
{
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
return;
TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker);
TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
}
/// <summary>
@@ -72,21 +74,18 @@ public partial class ChatSystem
/// <param name="hideChat">Whether or not this message should appear in the chat window</param>
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
public void TryEmoteWithChat(
EntityUid source,
EmotePrototype emote,
ChatTransmitRange range = ChatTransmitRange.Normal,
bool hideLog = false,
string? nameOverride = null,
bool ignoreActionBlocker = false
bool ignoreActionBlocker = false,
bool forceEmote = false
)
{
if (_whitelistSystem.IsWhitelistFailOrNull(emote.Whitelist, source) || _whitelistSystem.IsBlacklistPass(emote.Blacklist, source))
return;
if (!emote.Available &&
TryComp<SpeechComponent>(source, out var speech) &&
!speech.AllowedEmotes.Contains(emote.ID))
if (!forceEmote && !AllowedToUseEmote(source, emote))
return;
// check if proto has valid message for chat
@@ -155,15 +154,40 @@ public partial class ChatSystem
_audio.PlayPvs(sound, uid, param);
return true;
}
/// <summary>
/// Checks if a valid emote was typed, to play sounds and etc and invokes an event.
/// </summary>
/// <param name="uid"></param>
/// <param name="textInput"></param>
private void TryEmoteChatInput(EntityUid uid, string textInput)
{
var actionLower = textInput.ToLower();
if (!_wordEmoteDict.TryGetValue(actionLower, out var emote))
return;
if (!AllowedToUseEmote(uid, emote))
return;
InvokeEmoteEvent(uid, emote);
}
/// <summary>
/// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity.
/// </summary>
/// <param name="source">The entity that is speaking</param>
/// <param name="emote">The emote being used</param>
/// <returns></returns>
private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
{
if ((_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) || _whitelistSystem.IsBlacklistPass(emote.Blacklist, source)))
return false;
if (!emote.Available &&
TryComp<SpeechComponent>(source, out var speech) &&
!speech.AllowedEmotes.Contains(emote.ID))
return false;
return true;
}
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
{

View File

@@ -8,7 +8,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
namespace Content.Server.Chemistry.ReagentEffects;
/// <summary>
/// Tries to force someone to emote (scream, laugh, etc).
/// Tries to force someone to emote (scream, laugh, etc). Still respects whitelists/blacklists and other limits of the specified emote unless forced.
/// </summary>
[UsedImplicitly]
public sealed partial class Emote : ReagentEffect
@@ -19,6 +19,9 @@ public sealed partial class Emote : ReagentEffect
[DataField]
public bool ShowInChat;
[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;
@@ -30,7 +33,7 @@ public sealed partial class Emote : ReagentEffect
var chatSys = args.EntityManager.System<ChatSystem>();
if (ShowInChat)
chatSys.TryEmoteWithChat(args.SolutionEntity, EmoteId, ChatTransmitRange.GhostRangeLimit);
chatSys.TryEmoteWithChat(args.SolutionEntity, EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: Force);
else
chatSys.TryEmoteWithoutChat(args.SolutionEntity, EmoteId);

View File

@@ -1,4 +1,4 @@
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Maps;
@@ -47,7 +47,8 @@ public sealed partial class CreateEntityTileReaction : ITileReaction
int acc = 0;
foreach (var ent in tile.GetEntitiesInTile())
{
if (Whitelist.IsValid(ent))
var whitelistSystem = entityManager.System<EntityWhitelistSystem>();
if (whitelistSystem.IsWhitelistPass(Whitelist, ent))
acc += 1;
if (acc >= MaxOnTile)

View File

@@ -1,6 +1,7 @@
using Content.Shared.Configurable;
using Content.Shared.Interaction;
using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Player;
@@ -11,6 +12,7 @@ namespace Content.Server.Configurable;
public sealed class ConfigurationSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
public override void Initialize()
{
@@ -28,7 +30,7 @@ public sealed class ConfigurationSystem : EntitySystem
if (args.Handled)
return;
if (!TryComp(args.Used, out ToolComponent? tool) || !tool.Qualities.Contains(component.QualityNeeded))
if (!_toolSystem.HasQuality(args.Used, component.QualityNeeded))
return;
args.Handled = _uiSystem.TryOpenUi(uid, ConfigurationUiKey.Key, args.User);
@@ -68,7 +70,7 @@ public sealed class ConfigurationSystem : EntitySystem
private void OnInsert(EntityUid uid, ConfigurationComponent component, ContainerIsInsertingAttemptEvent args)
{
if (!TryComp(args.EntityUid, out ToolComponent? tool) || !tool.Qualities.Contains(component.QualityNeeded))
if (!_toolSystem.HasQuality(args.EntityUid, component.QualityNeeded))
return;
args.Cancel();

View File

@@ -14,6 +14,7 @@ using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Player;
@@ -30,6 +31,7 @@ namespace Content.Server.Construction
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
// --- WARNING! LEGACY CODE AHEAD! ---
// This entire file contains the legacy code for initial construction.
@@ -337,7 +339,7 @@ namespace Content.Server.Construction
return false;
}
if (constructionPrototype.EntityWhitelist != null && !constructionPrototype.EntityWhitelist.IsValid(user))
if (_whitelistSystem.IsWhitelistFail(constructionPrototype.EntityWhitelist, user))
{
_popup.PopupEntity(Loc.GetString("construction-system-cannot-start"), user, user);
return false;
@@ -422,7 +424,7 @@ namespace Content.Server.Construction
return;
}
if (constructionPrototype.EntityWhitelist != null && !constructionPrototype.EntityWhitelist.IsValid(user))
if (_whitelistSystem.IsWhitelistFail(constructionPrototype.EntityWhitelist, user))
{
_popup.PopupEntity(Loc.GetString("construction-system-cannot-start"), user, user);
return;

View File

@@ -1,22 +1,19 @@
using Content.Shared.Damage;
using Content.Shared.Tools;
using Robust.Shared.Utility;
using Robust.Shared.Prototypes;
namespace Content.Server.Damage.Components
namespace Content.Server.Damage.Components;
[RegisterComponent]
public sealed partial class DamageOnToolInteractComponent : Component
{
[RegisterComponent]
public sealed partial class DamageOnToolInteractComponent : Component
{
[DataField("tools")]
public PrototypeFlags<ToolQualityPrototype> Tools { get; private set; } = new ();
[DataField]
public ProtoId<ToolQualityPrototype> Tools { get; private set; }
// TODO: Remove this snowflake stuff, make damage per-tool quality perhaps?
[DataField("weldingDamage")]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier? WeldingDamage { get; private set; }
// TODO: Remove this snowflake stuff, make damage per-tool quality perhaps?
[DataField]
public DamageSpecifier? WeldingDamage { get; private set; }
[DataField("defaultDamage")]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier? DefaultDamage { get; private set; }
}
[DataField]
public DamageSpecifier? DefaultDamage { get; private set; }
}

View File

@@ -4,6 +4,7 @@ using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems;
using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
namespace Content.Server.Damage.Systems
@@ -12,6 +13,7 @@ namespace Content.Server.Damage.Systems
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
public override void Initialize()
{
@@ -42,8 +44,7 @@ namespace Content.Server.Damage.Systems
args.Handled = true;
}
else if (component.DefaultDamage is {} damage
&& EntityManager.TryGetComponent(args.Used, out ToolComponent? tool)
&& tool.Qualities.ContainsAny(component.Tools))
&& _toolSystem.HasQuality(args.Used, component.Tools))
{
var dmg = _damageableSystem.TryChangeDamage(args.Target, damage, origin: args.User);

View File

@@ -1,12 +1,12 @@
using System.Numerics;
using Content.Server.Forensics;
using Content.Server.Stack;
using Content.Shared.Destructible.Thresholds;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
@@ -17,8 +17,8 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
/// <summary>
/// Entities spawned on reaching this threshold, from a min to a max.
/// </summary>
[DataField("spawn", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<MinMax, EntityPrototype>))]
public Dictionary<string, MinMax> Spawn { get; set; } = new();
[DataField]
public Dictionary<EntProtoId, MinMax> Spawn = new();
[DataField("offset")]
public float Offset { get; set; } = 0.5f;

View File

@@ -1,26 +0,0 @@
using Robust.Shared.Random;
namespace Content.Server.Destructible.Thresholds
{
[Serializable]
[DataDefinition]
public partial struct MinMax
{
[DataField("min")]
public int Min;
[DataField("max")]
public int Max;
public MinMax(int min, int max)
{
Min = min;
Max = max;
}
public int Next(IRobustRandom random)
{
return random.Next(Min, Max + 1);
}
}
}

View File

@@ -1,9 +1,9 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.GameTicking.Components;
using Content.Shared.Prototypes;
using JetBrains.Annotations;
using Robust.Shared.Console;

View File

@@ -1,8 +1,6 @@
using Content.Server.GameTicking.Rules;
using Content.Server.Maps;
using Content.Shared.GridPreloader.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -10,25 +8,27 @@ namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// This is used for a game rule that loads a map when activated.
/// Works with <see cref="RuleGridsComponent"/>.
/// </summary>
[RegisterComponent]
[RegisterComponent, Access(typeof(LoadMapRuleSystem))]
public sealed partial class LoadMapRuleComponent : Component
{
[DataField]
public MapId? Map;
/// <summary>
/// A <see cref="GameMapPrototype"/> to load on a new map.
/// </summary>
[DataField]
public ProtoId<GameMapPrototype>? GameMap;
/// <summary>
/// A map path to load on a new map.
/// </summary>
[DataField]
public ResPath? MapPath;
/// <summary>
/// A <see cref="PreloadedGridPrototype"/> to move to a new map.
/// If there are no instances left nothing is done.
/// </summary>
[DataField]
public ProtoId<PreloadedGridPrototype>? PreloadedGrid;
[DataField]
public List<EntityUid> MapGrids = new();
[DataField]
public EntityWhitelist? SpawnerWhitelist;
}

View File

@@ -6,4 +6,9 @@
[RegisterComponent, Access(typeof(RespawnRuleSystem))]
public sealed partial class RespawnDeadRuleComponent : Component
{
/// <summary>
/// Whether or not we want to add everyone who dies to the respawn tracker
/// </summary>
[DataField]
public bool AlwaysRespawnDead;
}

View File

@@ -13,18 +13,24 @@ public sealed partial class RespawnTrackerComponent : Component
/// A list of the people that should be respawned.
/// Used to make sure that we don't respawn aghosts or observers.
/// </summary>
[DataField("players")]
[DataField]
public HashSet<NetUserId> Players = new();
/// <summary>
/// The delay between dying and respawning.
/// </summary>
[DataField("respawnDelay")]
[DataField]
public TimeSpan RespawnDelay = TimeSpan.Zero;
/// <summary>
/// A dictionary of player netuserids and when they will respawn.
/// </summary>
[DataField("respawnQueue")]
[DataField]
public Dictionary<NetUserId, TimeSpan> RespawnQueue = new();
/// <summary>
/// Whether or not to delete the original body when respawning
/// </summary>
[DataField]
public bool DeleteBody = true;
}

View File

@@ -0,0 +1,30 @@
using Content.Server.GameTicking.Rules;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
/// <summary>
/// Stores grids created by another gamerule component.
/// With <c>AntagSelection</c>, spawners on these grids can be used for its antags.
/// </summary>
[RegisterComponent, Access(typeof(RuleGridsSystem))]
public sealed partial class RuleGridsComponent : Component
{
/// <summary>
/// The map that was loaded.
/// </summary>
[DataField]
public MapId? Map;
/// <summary>
/// The grid entities that have been loaded.
/// </summary>
[DataField]
public List<EntityUid> MapGrids = new();
/// <summary>
/// Whitelist for a spawner to be considered for an antag.
/// All spawners must have <c>SpawnPointComponent</c> regardless to be found.
/// </summary>
[DataField]
public EntityWhitelist? SpawnerWhitelist;
}

View File

@@ -1,12 +1,12 @@
using System.Linq;
using Content.Server.Administration.Commands;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Server.Mind;
using Content.Server.Points;
using Content.Server.RoundEnd;
using Content.Server.Station.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Points;
using Content.Shared.Storage;
using Robust.Server.GameObjects;
@@ -56,7 +56,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
_mind.TransferTo(newMind, mob);
SetOutfitCommand.SetOutfit(mob, dm.Gear, EntityManager);
EnsureComp<KillTrackerComponent>(mob);
_respawn.AddToTracker(ev.Player.UserId, uid, tracker);
_respawn.AddToTracker(ev.Player.UserId, (uid, tracker));
_point.EnsurePlayer(ev.Player.UserId, uid, point);
@@ -73,7 +73,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
{
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
_respawn.AddToTracker(ev.Mob, uid, tracker);
_respawn.AddToTracker((ev.Mob, null), (uid, tracker));
}
}

View File

@@ -1,15 +0,0 @@
namespace Content.Server.GameTicking.Rules;
/*
[Prototype("gameRule")]
public sealed partial class GameRulePrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("config", required: true)]
public GameRuleConfiguration Configuration { get; private set; } = default!;
}
*/

View File

@@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
@@ -126,4 +126,8 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
return found;
}
protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null)
{
GameTicker.EndGameRule(uid, component);
}
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Components;
using Content.Shared.GameTicking.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Robust.Shared.Timing;

View File

@@ -1,7 +1,7 @@
using System.Threading;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.GameTicking.Components;
using Robust.Server.Player;
using Robust.Shared.Player;
using Timer = Robust.Shared.Timing.Timer;

View File

@@ -1,8 +1,8 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Shared.Chat;
using Content.Shared.GameTicking.Components;
using Robust.Server.Player;
using Robust.Shared.Player;
using Robust.Shared.Random;

View File

@@ -1,8 +1,6 @@
using Content.Server.Antag;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GridPreloader;
using Content.Server.Spawners.Components;
using Content.Shared.GameTicking.Components;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Map;
@@ -13,96 +11,70 @@ namespace Content.Server.GameTicking.Rules;
public sealed class LoadMapRuleSystem : GameRuleSystem<LoadMapRuleComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly GridPreloaderSystem _gridPreloader = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LoadMapRuleComponent, AntagSelectLocationEvent>(OnSelectLocation);
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
}
private void OnGridSplit(ref GridSplitEvent args)
{
var rule = QueryActiveRules();
while (rule.MoveNext(out _, out var mapComp, out _))
{
if (!mapComp.MapGrids.Contains(args.Grid))
continue;
mapComp.MapGrids.AddRange(args.NewGrids);
break;
}
}
protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args)
{
if (comp.Map != null)
if (comp.PreloadedGrid != null && !_gridPreloader.PreloadingEnabled)
{
// Preloading will never work if it's disabled, duh
Log.Debug($"Immediately ending {ToPrettyString(uid):rule} as preloading grids is disabled by cvar.");
ForceEndSelf(uid, rule);
return;
}
// grid preloading needs map to init after moving it
var mapUid = comp.PreloadedGrid != null ? _map.CreateMap(out var mapId, false) : _map.CreateMap(out mapId);
_metaData.SetEntityName(mapUid, $"LoadMapRule destination for rule {ToPrettyString(uid)}");
comp.Map = mapId;
var mapUid = _map.CreateMap(out var mapId, runMapInit: comp.PreloadedGrid == null);
Log.Info($"Created map {mapId} for {ToPrettyString(uid):rule}");
IReadOnlyList<EntityUid> grids;
if (comp.GameMap != null)
{
var gameMap = _prototypeManager.Index(comp.GameMap.Value);
comp.MapGrids.AddRange(GameTicker.LoadGameMap(gameMap, comp.Map.Value, new MapLoadOptions()));
grids = GameTicker.LoadGameMap(gameMap, mapId, new MapLoadOptions());
}
else if (comp.MapPath != null)
else if (comp.MapPath is {} path)
{
if (!_mapLoader.TryLoad(comp.Map.Value,
comp.MapPath.Value.ToString(),
out var roots,
new MapLoadOptions { LoadMap = true }))
var options = new MapLoadOptions { LoadMap = true };
if (!_mapLoader.TryLoad(mapId, path.ToString(), out var roots, options))
{
_mapManager.DeleteMap(mapId);
Log.Error($"Failed to load map from {path}!");
Del(mapUid);
ForceEndSelf(uid, rule);
return;
}
comp.MapGrids.AddRange(roots);
grids = roots;
}
else if (comp.PreloadedGrid != null)
else if (comp.PreloadedGrid is {} preloaded)
{
// TODO: If there are no preloaded grids left, any rule announcements will still go off!
if (!_gridPreloader.TryGetPreloadedGrid(comp.PreloadedGrid.Value, out var loadedShuttle))
if (!_gridPreloader.TryGetPreloadedGrid(preloaded, out var loadedShuttle))
{
_mapManager.DeleteMap(mapId);
Log.Error($"Failed to get a preloaded grid with {preloaded}!");
Del(mapUid);
ForceEndSelf(uid, rule);
return;
}
_transform.SetParent(loadedShuttle.Value, mapUid);
comp.MapGrids.Add(loadedShuttle.Value);
_map.InitializeMap(mapId);
grids = new List<EntityUid>() { loadedShuttle.Value };
_map.InitializeMap(mapUid);
}
else
{
Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}");
Del(mapUid);
ForceEndSelf(uid, rule);
return;
}
}
private void OnSelectLocation(Entity<LoadMapRuleComponent> ent, ref AntagSelectLocationEvent args)
{
var query = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
while (query.MoveNext(out var uid, out _, out var xform))
{
if (xform.MapID != ent.Comp.Map)
continue;
if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value))
continue;
if (ent.Comp.SpawnerWhitelist != null && !ent.Comp.SpawnerWhitelist.IsValid(uid, EntityManager))
continue;
args.Coordinates.Add(_transform.GetMapCoordinates(xform));
}
var ev = new RuleLoadedGridsEvent(mapId, grids);
RaiseLocalEvent(uid, ref ev);
}
}

View File

@@ -1,7 +1,7 @@
using System.Threading;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.GameTicking.Components;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameTicking.Rules;

View File

@@ -11,6 +11,7 @@ using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC.Components;
@@ -24,7 +25,6 @@ using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Shared.Store.Components;
namespace Content.Server.GameTicking.Rules;
@@ -260,10 +260,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
var map = Transform(ent).MapID;
var rules = EntityQueryEnumerator<NukeopsRuleComponent, LoadMapRuleComponent>();
while (rules.MoveNext(out var uid, out _, out var mapRule))
var rules = EntityQueryEnumerator<NukeopsRuleComponent, RuleGridsComponent>();
while (rules.MoveNext(out var uid, out _, out var grids))
{
if (map != mapRule.Map)
if (map != grids.Map)
continue;
ent.Comp.AssociatedRule = uid;
break;
@@ -324,7 +324,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
if (nukeops.WarDeclaredTime != null)
continue;
if (TryComp<LoadMapRuleComponent>(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map)
if (TryComp<RuleGridsComponent>(uid, out var grids) && Transform(ev.DeclaratorEntity).MapID != grids.Map)
continue;
var newStatus = GetWarCondition(nukeops, ev.Status);
@@ -445,7 +445,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
// Check that there are spawns available and that they can access the shuttle.
var spawnsAvailable = EntityQuery<NukeOperativeSpawnerComponent>(true).Any();
if (spawnsAvailable && CompOrNull<LoadMapRuleComponent>(ent)?.Map == shuttleMapId)
if (spawnsAvailable && CompOrNull<RuleGridsComponent>(ent)?.Map == shuttleMapId)
return; // Ghost spawns can still access the shuttle. Continue the round.
// The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives,
@@ -478,7 +478,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
/// Is this method the shitty glue holding together the last of my sanity? yes.
/// Do i have a better solution? not presently.
/// </remarks>
private EntityUid? GetOutpost(Entity<LoadMapRuleComponent?> ent)
private EntityUid? GetOutpost(Entity<RuleGridsComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return null;

View File

@@ -1,8 +1,9 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Components;
using Content.Server.Database.Migrations.Postgres;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.GameTicking.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind;
using Content.Shared.Mobs;
@@ -34,38 +35,6 @@ public sealed class RespawnRuleSystem : GameRuleSystem<RespawnDeadRuleComponent>
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
}
private void OnSuicide(SuicideEvent ev)
{
if (!TryComp<ActorComponent>(ev.Victim, out var actor))
return;
var query = EntityQueryEnumerator<RespawnTrackerComponent>();
while (query.MoveNext(out _, out var respawn))
{
if (respawn.Players.Remove(actor.PlayerSession.UserId))
QueueDel(ev.Victim);
}
}
private void OnMobStateChanged(MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Alive)
return;
if (!TryComp<ActorComponent>(args.Target, out var actor))
return;
var query = EntityQueryEnumerator<RespawnDeadRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out _, out var rule))
{
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
if (RespawnPlayer(args.Target, uid, actor: actor))
break;
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -75,8 +44,7 @@ public sealed class RespawnRuleSystem : GameRuleSystem<RespawnDeadRuleComponent>
foreach (var tracker in EntityQuery<RespawnTrackerComponent>())
{
var queue = new Dictionary<NetUserId, TimeSpan>(tracker.RespawnQueue);
foreach (var (player, time) in queue)
foreach (var (player, time) in tracker.RespawnQueue)
{
if (_timing.CurTime < time)
continue;
@@ -92,53 +60,84 @@ public sealed class RespawnRuleSystem : GameRuleSystem<RespawnDeadRuleComponent>
}
}
/// <summary>
/// Adds a given player to the respawn tracker, ensuring that they are respawned if they die.
/// </summary>
public void AddToTracker(EntityUid player, EntityUid tracker, RespawnTrackerComponent? component = null, ActorComponent? actor = null)
private void OnSuicide(SuicideEvent ev)
{
if (!Resolve(tracker, ref component) || !Resolve(player, ref actor, false))
return;
if (!TryComp<ActorComponent>(ev.Victim, out var actor))
return;
AddToTracker(actor.PlayerSession.UserId, tracker, component);
var query = EntityQueryEnumerator<RespawnTrackerComponent>();
while (query.MoveNext(out _, out var respawn))
{
if (respawn.Players.Remove(actor.PlayerSession.UserId))
QueueDel(ev.Victim);
}
}
/// <summary>
/// Adds a given player to the respawn tracker, ensuring that they are respawned if they die.
/// </summary>
public void AddToTracker(NetUserId id, EntityUid tracker, RespawnTrackerComponent? component = null)
private void OnMobStateChanged(MobStateChangedEvent args)
{
if (!Resolve(tracker, ref component))
if (args.NewMobState != MobState.Dead)
return;
component.Players.Add(id);
if (!TryComp<ActorComponent>(args.Target, out var actor))
return;
var query = EntityQueryEnumerator<RespawnDeadRuleComponent, RespawnTrackerComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var respawnRule, out var tracker, out var rule))
{
if (!GameTicker.IsGameRuleActive(uid, rule))
continue;
if (respawnRule.AlwaysRespawnDead)
AddToTracker(actor.PlayerSession.UserId, (uid, tracker));
if (RespawnPlayer((args.Target, actor), (uid, tracker)))
break;
}
}
/// <summary>
/// Attempts to directly respawn a player, skipping the lobby screen.
/// </summary>
public bool RespawnPlayer(EntityUid player, EntityUid respawnTracker, RespawnTrackerComponent? component = null, ActorComponent? actor = null)
public bool RespawnPlayer(Entity<ActorComponent> player, Entity<RespawnTrackerComponent> respawnTracker)
{
if (!Resolve(respawnTracker, ref component) || !Resolve(player, ref actor, false))
if (!respawnTracker.Comp.Players.Contains(player.Comp.PlayerSession.UserId) || respawnTracker.Comp.RespawnQueue.ContainsKey(player.Comp.PlayerSession.UserId))
return false;
if (!component.Players.Contains(actor.PlayerSession.UserId) || component.RespawnQueue.ContainsKey(actor.PlayerSession.UserId))
return false;
if (component.RespawnDelay == TimeSpan.Zero)
if (respawnTracker.Comp.RespawnDelay == TimeSpan.Zero)
{
if (_station.GetStations().FirstOrNull() is not { } station)
return false;
QueueDel(player);
GameTicker.MakeJoinGame(actor.PlayerSession, station, silent: true);
if (respawnTracker.Comp.DeleteBody)
QueueDel(player);
GameTicker.MakeJoinGame(player.Comp.PlayerSession, station, silent: true);
return false;
}
var msg = Loc.GetString("rule-respawn-in-seconds", ("second", component.RespawnDelay.TotalSeconds));
var msg = Loc.GetString("rule-respawn-in-seconds", ("second", respawnTracker.Comp.RespawnDelay.TotalSeconds));
var wrappedMsg = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMsg, respawnTracker, false, actor.PlayerSession.Channel, Color.LimeGreen);
component.RespawnQueue[actor.PlayerSession.UserId] = _timing.CurTime + component.RespawnDelay;
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMsg, respawnTracker, false, player.Comp.PlayerSession.Channel, Color.LimeGreen);
respawnTracker.Comp.RespawnQueue[player.Comp.PlayerSession.UserId] = _timing.CurTime + respawnTracker.Comp.RespawnDelay;
return true;
}
/// <summary>
/// Adds a given player to the respawn tracker, ensuring that they are respawned if they die.
/// </summary>
public void AddToTracker(Entity<ActorComponent?> player, Entity<RespawnTrackerComponent?> respawnTracker)
{
if (!Resolve(respawnTracker, ref respawnTracker.Comp) || !Resolve(player, ref player.Comp, false))
return;
AddToTracker(player.Comp.PlayerSession.UserId, (respawnTracker, respawnTracker.Comp));
}
/// <summary>
/// Adds a given player to the respawn tracker, ensuring that they are respawned if they die.
/// </summary>
public void AddToTracker(NetUserId id, Entity<RespawnTrackerComponent> tracker)
{
tracker.Comp.Players.Add(id);
}
}

View File

@@ -12,6 +12,7 @@ using Content.Server.RoundEnd;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Database;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Mind;
@@ -27,7 +28,6 @@ using Content.Shared.Stunnable;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Server.GameTicking.Components;
using Content.Shared.Cuffs.Components;
namespace Content.Server.GameTicking.Rules;

View File

@@ -1,9 +1,9 @@
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Content.Shared.GameTicking.Components;
using Content.Shared.Storage;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;

View File

@@ -0,0 +1,78 @@
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Spawners.Components;
using Content.Shared.Whitelist;
using Robust.Server.Physics;
using Robust.Shared.Map;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Handles storing grids from <see cref="RuleLoadedGridsEvent"/> and antags spawning on their spawners.
/// </summary>
public sealed class RuleGridsSystem : GameRuleSystem<RuleGridsComponent>
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
SubscribeLocalEvent<RuleGridsComponent, RuleLoadedGridsEvent>(OnLoadedGrids);
SubscribeLocalEvent<RuleGridsComponent, AntagSelectLocationEvent>(OnSelectLocation);
}
private void OnGridSplit(ref GridSplitEvent args)
{
var rule = QueryActiveRules();
while (rule.MoveNext(out _, out var comp, out _))
{
if (!comp.MapGrids.Contains(args.Grid))
continue;
comp.MapGrids.AddRange(args.NewGrids);
break; // only 1 rule can own a grid, not multiple
}
}
private void OnLoadedGrids(Entity<RuleGridsComponent> ent, ref RuleLoadedGridsEvent args)
{
var (uid, comp) = ent;
if (comp.Map != null && args.Map != comp.Map)
{
Log.Warning($"{ToPrettyString(uid):rule} loaded grids on multiple maps {comp.Map} and {args.Map}, the second will be ignored.");
return;
}
comp.Map = args.Map;
comp.MapGrids.AddRange(args.Grids);
}
private void OnSelectLocation(Entity<RuleGridsComponent> ent, ref AntagSelectLocationEvent args)
{
var query = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
while (query.MoveNext(out var uid, out _, out var xform))
{
if (xform.MapID != ent.Comp.Map)
continue;
if (xform.GridUid is not {} grid || !ent.Comp.MapGrids.Contains(grid))
continue;
if (_whitelist.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid))
continue;
args.Coordinates.Add(_transform.GetMapCoordinates(xform));
}
}
}
/// <summary>
/// Raised by another gamerule system to store loaded grids, and have other systems work with it.
/// A single rule can only load grids for a single map, attempts to load more are ignored.
/// </summary>
[ByRefEvent]
public record struct RuleLoadedGridsEvent(MapId Map, IReadOnlyList<EntityUid> Grids);

View File

@@ -1,6 +1,6 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Sandbox;
using Content.Shared.GameTicking.Components;
namespace Content.Server.GameTicking.Rules;

View File

@@ -1,10 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.GameTicking.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Random;
using Content.Shared.CCVar;
using Content.Shared.Database;

View File

@@ -1,5 +1,5 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Storage;
namespace Content.Server.GameTicking.Rules;

View File

@@ -5,6 +5,7 @@ using Content.Server.Objectives;
using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components;
@@ -15,7 +16,6 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
using System.Text;
using Content.Server.GameTicking.Components;
namespace Content.Server.GameTicking.Rules;

View File

@@ -1,5 +1,6 @@
using Content.Server.GameTicking.Rules.VariationPass.Components;
using Content.Server.Wires;
using Content.Shared.Whitelist;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules.VariationPass;
@@ -11,6 +12,8 @@ namespace Content.Server.GameTicking.Rules.VariationPass;
/// </summary>
public sealed class CutWireVariationPassSystem : VariationPassSystem<CutWireVariationPassComponent>
{
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
protected override void ApplyVariation(Entity<CutWireVariationPassComponent> ent, ref StationVariationPassEvent args)
{
var wiresCut = 0;
@@ -22,7 +25,7 @@ public sealed class CutWireVariationPassSystem : VariationPassSystem<CutWireVari
continue;
// Check against blacklist
if (ent.Comp.Blacklist.IsValid(uid))
if (_whitelistSystem.IsBlacklistPass(ent.Comp.Blacklist, uid))
continue;
if (Random.Prob(ent.Comp.WireCutChance))

View File

@@ -6,6 +6,7 @@ using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Zombies;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs;
@@ -15,7 +16,6 @@ using Content.Shared.Zombies;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using System.Globalization;
using Content.Server.GameTicking.Components;
namespace Content.Server.GameTicking.Rules;

View File

@@ -24,12 +24,19 @@ public sealed class GridPreloaderSystem : SharedGridPreloaderSystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
/// <summary>
/// Whether the preloading CVar is set or not.
/// </summary>
public bool PreloadingEnabled;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
SubscribeLocalEvent<PostGameMapLoad>(OnPostGameMapLoad);
Subs.CVar(_cfg, CCVars.PreloadGrids, value => PreloadingEnabled = value, true);
}
private void OnRoundRestart(RoundRestartCleanupEvent ev)
@@ -52,7 +59,7 @@ public sealed class GridPreloaderSystem : SharedGridPreloaderSystem
if (GetPreloaderEntity() != null)
return;
if (!_cfg.GetCVar(CCVars.PreloadGrids))
if (!PreloadingEnabled)
return;
var mapUid = _map.CreateMap(out var mapId, false);

View File

@@ -1,9 +1,10 @@
using Content.Server.Administration.Logs;
using Content.Server.Administration.Logs;
using Content.Server.Hands.Systems;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Whitelist;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
@@ -24,6 +25,7 @@ public sealed class RandomGiftSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private readonly List<string> _possibleGiftsSafe = new();
private readonly List<string> _possibleGiftsUnsafe = new();
@@ -40,7 +42,7 @@ public sealed class RandomGiftSystem : EntitySystem
private void OnExamined(EntityUid uid, RandomGiftComponent component, ExaminedEvent args)
{
if (!component.ContentsViewers.IsValid(args.Examiner, EntityManager) || component.SelectedEntity is null)
if (_whitelistSystem.IsWhitelistFail(component.ContentsViewers, args.Examiner) || component.SelectedEntity is null)
return;
var name = _prototype.Index<EntityPrototype>(component.SelectedEntity).Name;

View File

@@ -2,6 +2,7 @@ using Content.Server.NPC.HTN;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Player;
namespace Content.Server.KillTracking;
@@ -14,7 +15,8 @@ public sealed class KillTrackingSystem : EntitySystem
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<KillTrackerComponent, DamageChangedEvent>(OnDamageChanged);
// Add damage to LifetimeDamage before MobStateChangedEvent gets raised
SubscribeLocalEvent<KillTrackerComponent, DamageChangedEvent>(OnDamageChanged, before: [ typeof(MobThresholdSystem) ]);
SubscribeLocalEvent<KillTrackerComponent, MobStateChangedEvent>(OnMobStateChanged);
}
@@ -50,7 +52,7 @@ public sealed class KillTrackingSystem : EntitySystem
var largestSource = GetLargestSource(component.LifetimeDamage);
largestSource ??= killImpulse;
KillSource? killSource;
KillSource killSource;
KillSource? assistSource = null;
if (killImpulse is KillEnvironmentSource)
@@ -69,13 +71,13 @@ public sealed class KillTrackingSystem : EntitySystem
killSource = killImpulse;
// no assist is given to environmental kills
if (largestSource is not KillEnvironmentSource)
if (largestSource is not KillEnvironmentSource
&& component.LifetimeDamage.TryGetValue(largestSource, out var largestDamage))
{
// you have to do at least 50% of largest source's damage to get the assist.
if (component.LifetimeDamage[largestSource] >= component.LifetimeDamage[killSource] / 2)
{
var killDamage = component.LifetimeDamage.GetValueOrDefault(killSource);
// you have to do at least twice as much damage as the killing source to get the assist.
if (largestDamage >= killDamage / 2)
assistSource = largestSource;
}
}
}

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