Merge remote-tracking branch 'upstream/master' into ed-04-06-2024-upstream
# Conflicts: # Resources/Prototypes/Accents/word_replacements.yml
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class ShowHealthBarsCommand : LocalizedCommands
|
||||
var showHealthBarsComponent = new ShowHealthBarsComponent
|
||||
{
|
||||
DamageContainers = args.ToList(),
|
||||
HealthStatusIcon = null,
|
||||
NetSyncEnabled = false
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = @"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
71
Content.IntegrationTests/Tests/FillLevelSpriteTest.cs
Normal file
71
Content.IntegrationTests/Tests/FillLevelSpriteTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
78
Content.Server/GameTicking/Rules/RuleGridsSystem.cs
Normal file
78
Content.Server/GameTicking/Rules/RuleGridsSystem.cs
Normal 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);
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user