mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-06-09 13:26:34 +02:00
merge remote wizden/master
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Changeling.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Changeling.Systems;
|
||||
|
||||
@@ -12,11 +13,37 @@ public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnAfterAutoHandleState(Entity<ChangelingIdentityComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
private void OnHandleState(Entity<ChangelingIdentityComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not ChangelingIdentityComponentState state)
|
||||
return;
|
||||
|
||||
ent.Comp.ConsumedIdentities = new List<ChangelingIdentityData>();
|
||||
|
||||
foreach (var identity in state.ConsumedIdentities)
|
||||
{
|
||||
ChangelingIdentityData data = new()
|
||||
{
|
||||
Identity = EnsureEntity<ChangelingIdentityComponent>(identity.Identity, ent),
|
||||
Original = EnsureEntity<ChangelingIdentityComponent>(identity.Original, ent),
|
||||
OriginalMind = null, // Don't network the mind!
|
||||
OriginalJob = identity.OriginalJob,
|
||||
OriginalName = identity.OriginalName,
|
||||
Starting = identity.Starting,
|
||||
GrantedDna = identity.GrantedDna,
|
||||
};
|
||||
|
||||
ent.Comp.ConsumedIdentities.Add(data);
|
||||
}
|
||||
|
||||
ent.Comp.CurrentIdentity = EnsureEntity<ChangelingStoredIdentityComponent>(state.CurrentIdentity, ent);
|
||||
|
||||
ent.Comp.IdentityCloningSettings = state.IdentityCloningSettings;
|
||||
ent.Comp.MaxStoredDisguises = state.MaxStoredDisguises;
|
||||
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,13 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
|
||||
if (!EntMan.TryGetComponent<ChangelingIdentityComponent>(Owner, out var lingIdentity))
|
||||
return;
|
||||
|
||||
var models = ConvertToButtons(lingIdentity.ConsumedIdentities.Keys, lingIdentity?.CurrentIdentity);
|
||||
var models = ConvertToButtons(lingIdentity.ConsumedIdentities, lingIdentity.CurrentIdentity);
|
||||
|
||||
_menu.SetButtons(models);
|
||||
}
|
||||
|
||||
private IEnumerable<RadialMenuOptionBase> ConvertToButtons(
|
||||
IEnumerable<EntityUid> identities,
|
||||
IEnumerable<ChangelingIdentityData> identities,
|
||||
EntityUid? currentIdentity
|
||||
)
|
||||
{
|
||||
@@ -49,25 +49,28 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
|
||||
|
||||
foreach (var identity in identities)
|
||||
{
|
||||
if (identity.Identity == null)
|
||||
continue;
|
||||
|
||||
// Options for selecting identities.
|
||||
var option = new RadialMenuActionOption<NetEntity>(SendIdentitySelect, EntMan.GetNetEntity(identity))
|
||||
var option = new RadialMenuActionOption<NetEntity>(SendIdentitySelect, EntMan.GetNetEntity(identity.Identity.Value))
|
||||
{
|
||||
IconSpecifier = RadialMenuIconSpecifier.With(identity),
|
||||
ToolTip = Loc.GetString("changeling-transform-bui-select-entity", ("entity", identity)),
|
||||
BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null, // mark as selected
|
||||
HoverBackgroundColor = (currentIdentity == identity) ? SelectedOptionHoverBackground : null
|
||||
IconSpecifier = RadialMenuIconSpecifier.With(identity.Identity.Value),
|
||||
ToolTip = Loc.GetString("changeling-transform-bui-select-entity", ("entity", identity.Identity)),
|
||||
BackgroundColor = (currentIdentity == identity.Identity) ? SelectedOptionBackground : null, // mark as selected
|
||||
HoverBackgroundColor = (currentIdentity == identity.Identity) ? SelectedOptionHoverBackground : null
|
||||
};
|
||||
buttons.Add(option);
|
||||
|
||||
// Options for dropping identities.
|
||||
var dropOption = new RadialMenuActionOption<NetEntity>(SendIdentityDrop, EntMan.GetNetEntity(identity))
|
||||
var dropOption = new RadialMenuActionOption<NetEntity>(SendIdentityDrop, EntMan.GetNetEntity(identity.Identity.Value))
|
||||
{
|
||||
IconSpecifier = RadialMenuIconSpecifier.With(identity),
|
||||
ToolTip = (currentIdentity == identity)
|
||||
IconSpecifier = RadialMenuIconSpecifier.With(identity.Identity.Value),
|
||||
ToolTip = (currentIdentity == identity.Identity)
|
||||
? Loc.GetString("changeling-transform-bui-drop-identity-cannot-drop")
|
||||
: Loc.GetString("changeling-transform-bui-drop-identity-entity", ("entity", identity)),
|
||||
BackgroundColor = (currentIdentity == identity) ? DisabledOptionBackground : null, // cannot drop your current identity
|
||||
HoverBackgroundColor = (currentIdentity == identity) ? DisabledOptionHoverBackground : null
|
||||
: Loc.GetString("changeling-transform-bui-drop-identity-entity", ("entity", identity.Identity)),
|
||||
BackgroundColor = (currentIdentity == identity.Identity) ? DisabledOptionBackground : null, // cannot drop your current identity
|
||||
HoverBackgroundColor = (currentIdentity == identity.Identity) ? DisabledOptionHoverBackground : null
|
||||
};
|
||||
dropButtons.Add(dropOption);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Chemistry.Containers.EntitySystems;
|
||||
|
||||
public sealed partial class SolutionContainerSystem : SharedSolutionContainerSystem
|
||||
{
|
||||
}
|
||||
public sealed partial class SolutionContainerSystem : SharedSolutionContainerSystem;
|
||||
|
||||
@@ -117,8 +117,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
|
||||
|
||||
|
||||
if (extractableComponent.GrindableSolutionName is { } grindableSolutionId &&
|
||||
entProto.TryGetComponent<SolutionContainerManagerComponent>(out var manager, EntityManager.ComponentFactory) &&
|
||||
_solutionContainer.TryGetSolution(manager, grindableSolutionId, out var grindableSolution))
|
||||
_solutionContainer.TryGetSolution(entProto, grindableSolutionId, out var grindableSolution))
|
||||
{
|
||||
var data = new ReagentEntitySourceData(
|
||||
new() { DefaultGrindCategory },
|
||||
|
||||
@@ -136,7 +136,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
private void InitializeVisualizer(EntityUid entity, DamageVisualsComponent damageVisComp)
|
||||
{
|
||||
if (!TryComp(entity, out SpriteComponent? spriteComponent)
|
||||
|| !TryComp<DamageableComponent>(entity, out var damageComponent)
|
||||
|| !TryComp<InjurableComponent>(entity, out var injurableComponent)
|
||||
|| !HasComp<AppearanceComponent>(entity))
|
||||
return;
|
||||
|
||||
@@ -152,8 +152,8 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
|
||||
// If the damage container on our entity's DamageableComponent
|
||||
// is not null, we can try to check through its groups.
|
||||
if (damageComponent.DamageContainerID != null
|
||||
&& _prototypeManager.Resolve<DamageContainerPrototype>(damageComponent.DamageContainerID, out var damageContainer))
|
||||
if (injurableComponent.DamageContainer != null
|
||||
&& _prototypeManager.Resolve<DamageContainerPrototype>(injurableComponent.DamageContainer, out var damageContainer))
|
||||
{
|
||||
// Are we using damage overlay sprites by group?
|
||||
// Check if the container matches the supported groups,
|
||||
@@ -559,12 +559,12 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
damageTotal = damageTotal / damageVisComp.Divisor;
|
||||
var thresholdIndex = damageVisComp.Thresholds.BinarySearch(damageTotal);
|
||||
|
||||
if (thresholdIndex < 0)
|
||||
if (thresholdIndex < -1)
|
||||
{
|
||||
thresholdIndex = ~thresholdIndex;
|
||||
threshold = damageVisComp.Thresholds[thresholdIndex - 1];
|
||||
}
|
||||
else
|
||||
else if (thresholdIndex >= 0)
|
||||
{
|
||||
threshold = damageVisComp.Thresholds[thresholdIndex];
|
||||
}
|
||||
|
||||
@@ -81,7 +81,10 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
|
||||
private void OnAnimationCompleted(Entity<DoorComponent> ent, ref AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != DoorComponent.OpenCloseKey || !TryComp<SpriteComponent>(ent, out var sprite))
|
||||
if (args.Key != DoorComponent.OpenKey && args.Key != DoorComponent.CloseKey)
|
||||
return;
|
||||
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
return;
|
||||
|
||||
switch (ent.Comp.State)
|
||||
@@ -130,9 +133,15 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
switch (state)
|
||||
{
|
||||
case DoorState.Open:
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenKey))
|
||||
return;
|
||||
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.CloseKey))
|
||||
{
|
||||
_animationSystem.Stop(entity, null, DoorComponent.CloseKey);
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.OpenKey);
|
||||
}
|
||||
|
||||
foreach (var (layer, layerState) in entity.Comp.OpenSpriteStates)
|
||||
{
|
||||
// Allow animations to play while it's open (e.g., pinion);
|
||||
@@ -143,9 +152,15 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
|
||||
return;
|
||||
case DoorState.Closed:
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.CloseKey))
|
||||
return;
|
||||
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenKey))
|
||||
{
|
||||
_animationSystem.Stop(entity, null, DoorComponent.OpenKey);
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.CloseKey);
|
||||
}
|
||||
|
||||
foreach (var (layer, layerState) in entity.Comp.ClosedSpriteStates)
|
||||
{
|
||||
_sprite.LayerSetAutoAnimated((entity.Owner, sprite), layer, true);
|
||||
@@ -157,20 +172,20 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
if (entity.Comp.OpeningAnimationTime == TimeSpan.Zero)
|
||||
return;
|
||||
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenKey))
|
||||
return;
|
||||
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.OpenCloseKey);
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.OpenKey);
|
||||
|
||||
return;
|
||||
case DoorState.Closing:
|
||||
if (entity.Comp.ClosingAnimationTime == TimeSpan.Zero)
|
||||
return;
|
||||
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.CloseKey))
|
||||
return;
|
||||
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.OpenCloseKey);
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.CloseKey);
|
||||
|
||||
return;
|
||||
case DoorState.Denying:
|
||||
|
||||
@@ -8,7 +8,7 @@ using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Cuffs;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Ensnaring;
|
||||
using Content.Shared.Ensnaring.Components;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
@@ -39,6 +39,7 @@ namespace Content.Client.Inventory
|
||||
private readonly InventorySystem _inv;
|
||||
private readonly SharedCuffableSystem _cuffable;
|
||||
private readonly StrippableSystem _strippable;
|
||||
private readonly SharedEnsnareableSystem _snare;
|
||||
|
||||
[ViewVariables]
|
||||
private const int ButtonSeparation = 4;
|
||||
@@ -71,6 +72,7 @@ namespace Content.Client.Inventory
|
||||
_inv = EntMan.System<InventorySystem>();
|
||||
_cuffable = EntMan.System<SharedCuffableSystem>();
|
||||
_strippable = EntMan.System<StrippableSystem>();
|
||||
_snare = EntMan.System<SharedEnsnareableSystem>();
|
||||
|
||||
_virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace);
|
||||
}
|
||||
@@ -149,7 +151,7 @@ namespace Content.Client.Inventory
|
||||
}
|
||||
|
||||
// snare-removal button. This is just the old button before the change to item slots. It is pretty out of place.
|
||||
if (EntMan.TryGetComponent<EnsnareableComponent>(Owner, out var snare) && snare.IsEnsnared)
|
||||
if (EntMan.TryGetComponent<EnsnareableComponent>(Owner, out var snare) && _snare.IsEnsnared((Owner, snare)))
|
||||
{
|
||||
var button = new Button()
|
||||
{
|
||||
@@ -175,7 +177,7 @@ namespace Content.Client.Inventory
|
||||
// +27 vertically from the window header
|
||||
var horizontalMenuSize = Math.Max(200, Math.Max(_handCount, _inventoryDimensions.X + 1) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 20);
|
||||
var verticalMenuSize = Math.Max(200, (_inventoryDimensions.Y + (_handCount > 0 ? 2 : 1)) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 53);
|
||||
if (snare?.IsEnsnared == true)
|
||||
if (_snare.IsEnsnared(Owner))
|
||||
verticalMenuSize += 20;
|
||||
_strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize);
|
||||
}
|
||||
|
||||
@@ -56,18 +56,19 @@ public sealed class EntityHealthBarOverlay : Overlay
|
||||
var handle = args.WorldHandle;
|
||||
var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero;
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
var spriteQuery = _entManager.GetEntityQuery<SpriteComponent>();
|
||||
|
||||
const float scale = 1f;
|
||||
var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
|
||||
var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
|
||||
_prototype.Resolve(StatusIcon, out var statusIcon);
|
||||
|
||||
var query = _entManager.AllEntityQueryEnumerator<MobThresholdsComponent, MobStateComponent, DamageableComponent, SpriteComponent>();
|
||||
var query = _entManager.AllEntityQueryEnumerator<MobThresholdsComponent, MobStateComponent, DamageableComponent, InjurableComponent>();
|
||||
while (query.MoveNext(out var uid,
|
||||
out var mobThresholdsComponent,
|
||||
out var mobStateComponent,
|
||||
out var damageableComponent,
|
||||
out var spriteComponent))
|
||||
out var injurableComponent))
|
||||
{
|
||||
if (statusIcon != null && !_statusIconSystem.IsVisible((uid, _entManager.GetComponent<MetaDataComponent>(uid)), statusIcon))
|
||||
continue;
|
||||
@@ -77,11 +78,15 @@ public sealed class EntityHealthBarOverlay : Overlay
|
||||
xform.MapID != args.MapId)
|
||||
continue;
|
||||
|
||||
if (damageableComponent.DamageContainerID == null || !DamageContainers.Contains(damageableComponent.DamageContainerID))
|
||||
if (injurableComponent.DamageContainer == null || !DamageContainers.Contains(injurableComponent.DamageContainer))
|
||||
continue;
|
||||
|
||||
if (!spriteQuery.TryGetComponent(uid, out var sprite))
|
||||
continue;
|
||||
|
||||
// we use the status icon component bounds if specified otherwise use sprite
|
||||
var bounds = _entManager.GetComponentOrNull<StatusIconComponent>(uid)?.Bounds ?? _spriteSystem.GetLocalBounds((uid, spriteComponent));
|
||||
var bounds = _entManager.GetComponentOrNull<StatusIconComponent>(uid)?.Bounds ?? _spriteSystem.GetLocalBounds(
|
||||
(uid, sprite));
|
||||
var worldPos = _transform.GetWorldPosition(xform, xformQuery);
|
||||
|
||||
if (!bounds.Translated(worldPos).Intersects(args.WorldAABB))
|
||||
@@ -135,9 +140,6 @@ public sealed class EntityHealthBarOverlay : Overlay
|
||||
var totalDamage = _damageable.GetTotalDamage((uid, dmg));
|
||||
if (_mobStateSystem.IsAlive(uid, component))
|
||||
{
|
||||
if (dmg.HealthBarThreshold != null && totalDamage < dmg.HealthBarThreshold)
|
||||
return null;
|
||||
|
||||
if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var threshold, thresholds) &&
|
||||
!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Dead, out threshold, thresholds))
|
||||
return (1, false);
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DamageableComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
|
||||
SubscribeLocalEvent<InjurableComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
|
||||
SubscribeLocalEvent<ShowHealthIconsComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetStatusIconsEvent args)
|
||||
private void OnGetStatusIconsEvent(Entity<InjurableComponent> entity, ref GetStatusIconsEvent args)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
@@ -63,12 +63,12 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
args.StatusIcons.AddRange(healthIcons);
|
||||
}
|
||||
|
||||
private IReadOnlyList<HealthIconPrototype> DecideHealthIcons(Entity<DamageableComponent> entity)
|
||||
private IReadOnlyList<HealthIconPrototype> DecideHealthIcons(Entity<InjurableComponent> entity)
|
||||
{
|
||||
var damageableComponent = entity.Comp;
|
||||
var injurableComp = entity.Comp;
|
||||
|
||||
if (damageableComponent.DamageContainerID == null ||
|
||||
!DamageContainers.Contains(damageableComponent.DamageContainerID))
|
||||
if (injurableComp.DamageContainer == null ||
|
||||
!DamageContainers.Contains(injurableComp.DamageContainer))
|
||||
{
|
||||
return Array.Empty<HealthIconPrototype>();
|
||||
}
|
||||
@@ -76,14 +76,14 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
var result = new List<HealthIconPrototype>();
|
||||
|
||||
// Here you could check health status, diseases, mind status, etc. and pick a good icon, or multiple depending on whatever.
|
||||
if (damageableComponent?.DamageContainerID == "Biological")
|
||||
if (injurableComp?.DamageContainer == "Biological")
|
||||
{
|
||||
if (TryComp<MobStateComponent>(entity, out var state))
|
||||
{
|
||||
// Since there is no MobState for a rotting mob, we have to deal with this case first.
|
||||
if (HasComp<RottingComponent>(entity) && _prototypeMan.Resolve(damageableComponent.RottingIcon, out var rottingIcon))
|
||||
if (HasComp<RottingComponent>(entity) && _prototypeMan.Resolve(injurableComp.RottingIcon, out var rottingIcon))
|
||||
result.Add(rottingIcon);
|
||||
else if (damageableComponent.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.Resolve(value, out var icon))
|
||||
else if (injurableComp.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.Resolve(value, out var icon))
|
||||
result.Add(icon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Client.Speech.EntitySystems;
|
||||
|
||||
public sealed class SlurredSystem : SharedSlurredSystem
|
||||
{
|
||||
protected override string AccentuateInternal(EntityUid uid, SlurredAccentComponent comp, string message)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Client.Speech.EntitySystems
|
||||
{
|
||||
public sealed class StutteringSystem : SharedStutteringSystem
|
||||
{
|
||||
namespace Content.Client.Speech.EntitySystems;
|
||||
|
||||
public sealed class StutteringSystem : SharedStutteringSystem
|
||||
{
|
||||
protected override string AccentuateInternal(EntityUid uid, StutteringAccentComponent comp, string message)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Client.Items;
|
||||
using Content.Client.Items.UI;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Power.Visualizers;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Disposal.Components;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.SubFloor;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.SubFloor;
|
||||
@@ -16,10 +26,10 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly TrayScanRevealSystem _trayScanReveal = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly EntityQuery<TrayScannerComponent> _trayScannerQuery = default!;
|
||||
[Dependency] private readonly EntityQuery<SubFloorHideComponent> _subFloorHideQuery = default!;
|
||||
|
||||
@@ -28,6 +38,12 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
|
||||
|
||||
public const LookupFlags Flags = LookupFlags.Static | LookupFlags.Sundries | LookupFlags.Approximate;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
Subs.ItemStatus<TrayScannerComponent>(OnCollectItemStatus);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
@@ -44,37 +60,20 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
|
||||
var playerPos = _transform.GetWorldPosition(playerXform);
|
||||
var playerMap = playerXform.MapID;
|
||||
var range = 0f;
|
||||
var mode = TrayScannerMode.All;
|
||||
HashSet<Entity<SubFloorHideComponent>> inRange;
|
||||
|
||||
// TODO: Should probably sub to player attached changes / inventory changes but inventory's
|
||||
// API is extremely skrungly. If this ever shows up on dottrace ping me and laugh.
|
||||
var canSee = false;
|
||||
|
||||
// TODO: Common iterator for both systems.
|
||||
if (_inventory.TryGetContainerSlotEnumerator(player.Value, out var enumerator))
|
||||
foreach (var item in _inventory.GetHandOrInventoryEntities(player.Value, SlotFlags.POCKET))
|
||||
{
|
||||
while (enumerator.MoveNext(out var slot))
|
||||
{
|
||||
foreach (var ent in slot.ContainedEntities)
|
||||
{
|
||||
if (!_trayScannerQuery.TryGetComponent(ent, out var sneakScanner) || !sneakScanner.Enabled)
|
||||
continue;
|
||||
|
||||
canSee = true;
|
||||
range = MathF.Max(range, sneakScanner.Range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var hand in _hands.EnumerateHands(player.Value))
|
||||
{
|
||||
if (!_hands.TryGetHeldItem(player.Value, hand, out var heldEntity))
|
||||
if (!_trayScannerQuery.TryGetComponent(item, out var scanner) || !scanner.Enabled)
|
||||
continue;
|
||||
|
||||
if (!_trayScannerQuery.TryGetComponent(heldEntity, out var heldScanner) || !heldScanner.Enabled)
|
||||
continue;
|
||||
|
||||
range = MathF.Max(heldScanner.Range, range);
|
||||
range = MathF.Max(scanner.Range, range);
|
||||
mode = scanner.Mode;
|
||||
canSee = true;
|
||||
break;
|
||||
}
|
||||
@@ -83,10 +82,16 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
|
||||
|
||||
if (canSee)
|
||||
{
|
||||
_lookup.GetEntitiesInRange(playerMap, playerPos, range, inRange, flags: Flags);
|
||||
var entitiesInRange = new HashSet<Entity<SubFloorHideComponent>>();
|
||||
_lookup.GetEntitiesInRange(playerMap, playerPos, range, entitiesInRange, flags: Flags);
|
||||
|
||||
foreach (var (uid, comp) in inRange)
|
||||
foreach (var (uid, comp) in entitiesInRange)
|
||||
{
|
||||
if (!MatchesMode(uid, mode))
|
||||
continue;
|
||||
|
||||
inRange.Add((uid, comp));
|
||||
|
||||
if (comp.IsUnderCover || _trayScanReveal.IsUnderRevealingEntity(uid))
|
||||
EnsureComp<TrayRevealedComponent>(uid);
|
||||
}
|
||||
@@ -173,4 +178,67 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
|
||||
{
|
||||
_appearance.SetData(uid, SubFloorVisuals.ScannerRevealed, value);
|
||||
}
|
||||
|
||||
private bool MatchesMode(EntityUid uid, TrayScannerMode mode)
|
||||
{
|
||||
return mode switch
|
||||
{
|
||||
TrayScannerMode.All => true,
|
||||
TrayScannerMode.Wiring => HasComp<CableVisualizerComponent>(uid),
|
||||
// TODO: proper comp query after disposals refactor
|
||||
TrayScannerMode.Piping => HasComp<AtmosPipeLayersComponent>(uid) || _appearance.TryGetData(uid, DisposalTubeVisuals.VisualState, out _),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
#region UI
|
||||
private Control OnCollectItemStatus(Entity<TrayScannerComponent> entity)
|
||||
{
|
||||
_inputManager.TryGetKeyBinding((ContentKeyFunctions.AltUseItemInHand), out var binding);
|
||||
return new StatusControl(entity, binding?.GetKeyString() ?? "");
|
||||
}
|
||||
|
||||
private sealed class StatusControl : PollingItemStatusControl<StatusControl.TRayData>
|
||||
{
|
||||
private readonly RichTextLabel _label;
|
||||
private readonly TrayScannerComponent _scanner;
|
||||
private readonly string _keyBindingName;
|
||||
|
||||
public StatusControl(TrayScannerComponent scanner, string keyBindingName)
|
||||
{
|
||||
_scanner = scanner;
|
||||
_keyBindingName = keyBindingName;
|
||||
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
protected override TRayData PollData()
|
||||
{
|
||||
return new TRayData(_scanner.Enabled, _scanner.Mode);
|
||||
}
|
||||
|
||||
protected override void Update(in TRayData data)
|
||||
{
|
||||
if (!data.Enabled)
|
||||
{
|
||||
_label.SetMarkup(string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var modeLocString = data.Mode switch
|
||||
{
|
||||
TrayScannerMode.All => "tray-scanner-examine-mode-all",
|
||||
TrayScannerMode.Wiring => "tray-scanner-examine-mode-wiring",
|
||||
TrayScannerMode.Piping => "tray-scanner-examine-mode-piping",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
_label.SetMarkup(Robust.Shared.Localization.Loc.GetString("tray-scanner-item-status-label",
|
||||
("mode", Robust.Shared.Localization.Loc.GetString(modeLocString)),
|
||||
("keybinding", _keyBindingName)));
|
||||
}
|
||||
|
||||
public readonly record struct TRayData(bool Enabled, TrayScannerMode Mode);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -76,11 +76,12 @@ public sealed class DamageOverlayUiController : UIController
|
||||
}
|
||||
|
||||
//TODO: Jezi: adjust oxygen and hp overlays to use appropriate systems once bodysim is implemented
|
||||
private void UpdateOverlays(EntityUid entity, MobStateComponent? mobState, DamageableComponent? damageable = null, MobThresholdsComponent? thresholds = null)
|
||||
private void UpdateOverlays(EntityUid entity, MobStateComponent? mobState, DamageableComponent? damageable = null, MobThresholdsComponent? thresholds = null, InjurableComponent? injurable = null)
|
||||
{
|
||||
if (mobState == null && !EntityManager.TryGetComponent(entity, out mobState) ||
|
||||
thresholds == null && !EntityManager.TryGetComponent(entity, out thresholds) ||
|
||||
damageable == null && !EntityManager.TryGetComponent(entity, out damageable))
|
||||
damageable == null && !EntityManager.TryGetComponent(entity, out damageable) ||
|
||||
injurable == null && !EntityManager.TryGetComponent(entity, out injurable))
|
||||
return;
|
||||
|
||||
if (!_mobThresholdSystem.TryGetIncapThreshold(entity, out var foundThreshold, thresholds))
|
||||
@@ -105,7 +106,7 @@ public sealed class DamageOverlayUiController : UIController
|
||||
|
||||
if (!_statusEffects.TryEffectsWithComp<PainNumbnessStatusEffectComponent>(entity, out _))
|
||||
{
|
||||
foreach (var painDamageType in damageable.PainDamageGroups)
|
||||
foreach (var painDamageType in injurable.PainDamageGroups)
|
||||
{
|
||||
|
||||
damagePerGroup.TryGetValue(painDamageType, out var painDamage);
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
|
||||
return;
|
||||
}
|
||||
|
||||
_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide, cast.Voice);//cast.Voice Corvax-TTS
|
||||
_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide, cast.TitleText, cast.Voice);//cast.Voice Corvax-TTS
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -23,11 +23,9 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
|
||||
private List<TTSVoicePrototype> _voices = new(); // Corvax-TTS
|
||||
|
||||
private string? _verb;
|
||||
|
||||
public VoiceMaskNameChangeWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
NameSelectorSet.OnPressed += _ =>
|
||||
{
|
||||
OnNameChange?.Invoke(NameSelector.Text);
|
||||
@@ -105,13 +103,13 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
|
||||
}
|
||||
// Corvax-TTS-End
|
||||
|
||||
public void UpdateState(string name, string? verb, bool active, bool accentHide, string voice) // Corvax-TTS
|
||||
public void UpdateState(string name, string? verb, bool active, bool accentHide, LocId titleText, string voice) // Corvax-TTS
|
||||
{
|
||||
NameSelector.Text = name;
|
||||
_verb = verb;
|
||||
ToggleButton.Pressed = active;
|
||||
ToggleAccentButton.Pressed = accentHide;
|
||||
|
||||
Title = Loc.GetString(titleText);
|
||||
for (int id = 0; id < SpeechVerbSelector.ItemCount; id++)
|
||||
{
|
||||
if (string.Equals(verb, SpeechVerbSelector.GetItemMetadata(id)))
|
||||
|
||||
@@ -13,6 +13,11 @@ public static partial class PoolManager
|
||||
public static readonly ContentPoolManager Instance = new();
|
||||
public const string TestMap = "Empty";
|
||||
|
||||
/// <summary>
|
||||
/// Designated load bearing station. Sometimes you need a station for a test.
|
||||
/// </summary>
|
||||
public const string TestStation = "Saltern";
|
||||
|
||||
/// <summary>
|
||||
/// Runs a server, or a client until a condition is true
|
||||
/// </summary>
|
||||
|
||||
@@ -52,6 +52,7 @@ public sealed class DeltaPressureTest : AtmosTest
|
||||
types:
|
||||
Structural: 1000
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
|
||||
@@ -23,15 +23,15 @@ public sealed class DrainTest : InteractionTest
|
||||
- type: entity
|
||||
parent: Puddle
|
||||
id: PuddleBloodTest
|
||||
suffix: Blood (30u)
|
||||
suffix: Blood
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
puddle:
|
||||
maxVol: 1000
|
||||
reagents:
|
||||
- ReagentId: {BloodReagent}
|
||||
Quantity: {PuddleVolume}
|
||||
- type: Solution
|
||||
id: puddle
|
||||
solution:
|
||||
maxVol: 1000
|
||||
reagents:
|
||||
- ReagentId: {BloodReagent}
|
||||
Quantity: {PuddleVolume}
|
||||
";
|
||||
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ public sealed class SolutionRoundingTest : GameTest
|
||||
- type: entity
|
||||
id: SolutionRoundingTestContainer
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
beaker:
|
||||
maxVol: 100
|
||||
- type: Solution
|
||||
id: beaker
|
||||
solution:
|
||||
maxVol: 100
|
||||
|
||||
# This is the Chloral Hydrate recipe fyi.
|
||||
- type: reagent
|
||||
|
||||
@@ -20,10 +20,10 @@ public sealed class SolutionSystemTests : GameTest
|
||||
- type: entity
|
||||
id: SolutionTarget
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
beaker:
|
||||
maxVol: 50
|
||||
- type: Solution
|
||||
id: beaker
|
||||
solution:
|
||||
maxVol: 50
|
||||
|
||||
- type: reagent
|
||||
id: TestReagentA
|
||||
|
||||
@@ -20,11 +20,10 @@ namespace Content.IntegrationTests.Tests.Chemistry
|
||||
- type: entity
|
||||
id: TestSolutionContainer
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
beaker:
|
||||
maxVol: 50
|
||||
canMix: true";
|
||||
- type: Solution
|
||||
id: beaker
|
||||
solution:
|
||||
maxVol: 120";
|
||||
|
||||
private static string[] _reactions = GameDataScrounger.PrototypesOfKind<ReactionPrototype>();
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
id: DamageableDummy
|
||||
components:
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
damageContainer: Biological
|
||||
- type: MobState
|
||||
- type: MobThresholds
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Utility;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Damageable;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(DamageableComponent))]
|
||||
[TestOf(typeof(DamageableSystem))]
|
||||
public sealed class DamageAllPrototypesTest : GameTest
|
||||
{
|
||||
[SidedDependency(Side.Server)] private readonly DamageableSystem _damageableSystem = default!;
|
||||
|
||||
private static string[] _damageables = GameDataScrounger.EntitiesWithComponent("Damageable");
|
||||
|
||||
[Test]
|
||||
[TestOf(typeof(DamageableSystem))]
|
||||
[TestCaseSource(nameof(_damageables))]
|
||||
[Description("Ensures all Entity Prototypes with damageable can be damaged.")]
|
||||
public async Task TestDamageableComponents(string damageable)
|
||||
{
|
||||
var map = await Pair.CreateTestMap();
|
||||
|
||||
var entity = await SpawnAtPosition(damageable, map.GridCoords);
|
||||
|
||||
// Intentionally cannot take damage, ignore it.
|
||||
if (SEntMan.HasComponent<GodmodeComponent>(entity))
|
||||
return;
|
||||
|
||||
var canBeDamaged = false;
|
||||
|
||||
foreach (var type in SProtoMan.EnumeratePrototypes<DamageTypePrototype>())
|
||||
{
|
||||
if (!_damageableSystem.CanBeDamagedBy(entity, type))
|
||||
continue;
|
||||
|
||||
canBeDamaged = true;
|
||||
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
var damage = new DamageSpecifier(type, FixedPoint2.Epsilon);
|
||||
var previousDamage = _damageableSystem.GetTotalDamage(entity);
|
||||
_damageableSystem.ChangeDamage(entity, damage, ignoreResistances: true);
|
||||
Assert.That(_damageableSystem.GetTotalDamage(entity) == FixedPoint2.Epsilon + previousDamage);
|
||||
_damageableSystem.ClearAllDamage(entity);
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that this entity can actually be damaged.
|
||||
Assert.That(canBeDamaged);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,7 @@ namespace Content.IntegrationTests.Tests.Damageable
|
||||
name: {TestDamageableEntityId}
|
||||
components:
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
damageContainer: testDamageContainer
|
||||
";
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace Content.IntegrationTests.Tests.Destructible
|
||||
name: {DestructibleEntityId}
|
||||
components:
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -94,6 +95,7 @@ namespace Content.IntegrationTests.Tests.Destructible
|
||||
name: {DestructibleDestructionEntityId}
|
||||
components:
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -116,6 +118,7 @@ namespace Content.IntegrationTests.Tests.Destructible
|
||||
name: {DestructibleDamageTypeEntityId}
|
||||
components:
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -133,6 +136,7 @@ namespace Content.IntegrationTests.Tests.Destructible
|
||||
name: {DestructibleDamageGroupEntityId}
|
||||
components:
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
|
||||
@@ -85,6 +85,7 @@ namespace Content.IntegrationTests.Tests.Disposal
|
||||
0: Alive
|
||||
200: Dead
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
damageContainer: Biological
|
||||
- type: Physics
|
||||
bodyType: KinematicController
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Content.IntegrationTests.Tests
|
||||
[TestOf(typeof(EntityUid))]
|
||||
public sealed class EntityTest : GameTest
|
||||
{
|
||||
private static readonly ProtoId<EntityCategoryPrototype> SpawnerCategory = "Spawner";
|
||||
private static readonly HashSet<ProtoId<EntityCategoryPrototype>> IgnoredCategories = ["Spawner", "Debug"];
|
||||
|
||||
public override PoolSettings PoolSettings => new()
|
||||
{
|
||||
@@ -255,7 +255,7 @@ namespace Content.IntegrationTests.Tests
|
||||
.Where(p => !p.Abstract)
|
||||
.Where(p => !pair.IsTestPrototype(p))
|
||||
.Where(p => !excluded.Any(p.Components.ContainsKey))
|
||||
.Where(p => p.Categories.All(x => x.ID != SpawnerCategory))
|
||||
.Where(p => p.Categories.All(x => !IgnoredCategories.Contains(x.ID)))
|
||||
.Select(p => p.ID)
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -35,19 +35,19 @@ public sealed class AbsorbentTest : GameTest
|
||||
components:
|
||||
- type: Absorbent
|
||||
useAbsorberSolution: true
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
absorbed:
|
||||
maxVol: 100
|
||||
- type: Solution
|
||||
id: absorbed
|
||||
solution:
|
||||
maxVol: 100
|
||||
|
||||
- type: entity
|
||||
name: {RefillableDummyId}
|
||||
id: {RefillableDummyId}
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
refillable:
|
||||
maxVol: 200
|
||||
- type: Solution
|
||||
id: refillable
|
||||
solution:
|
||||
maxVol: 200
|
||||
- type: RefillableSolution
|
||||
solution: refillable
|
||||
|
||||
@@ -55,10 +55,10 @@ public sealed class AbsorbentTest : GameTest
|
||||
name: {SmallRefillableDummyId}
|
||||
id: {SmallRefillableDummyId}
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
refillable:
|
||||
maxVol: 20
|
||||
- type: Solution
|
||||
id: refillable
|
||||
solution:
|
||||
maxVol: 20
|
||||
- type: RefillableSolution
|
||||
solution: refillable
|
||||
";
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Utility;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameRules;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class AllGamePresetsStartTest : GameTest
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of blacklisted <see cref="GamePresetPrototype"/> for this test. Some down streams might make changes which nuke upstream game modes they don't use.
|
||||
/// This prevents them from being tested. If you use this to silence valid test fails and your game fails to start. Skill issue. Do 100 push-ups.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> IgnoredPresets = []; // Is a string to prevent YAML Linter from freaking if this is empty.
|
||||
|
||||
private static string[] _gamePresets = GameDataScrounger.PrototypesOfKind<GamePresetPrototype>().Where(p => !IgnoredPresets.Contains(p)).ToArray();
|
||||
|
||||
public override PoolSettings PoolSettings => new()
|
||||
{
|
||||
Dirty = true,
|
||||
DummyTicker = false,
|
||||
Connected = true,
|
||||
InLobby = true
|
||||
};
|
||||
|
||||
// Tests that all game modes can start given ideal circumstances.
|
||||
[Test]
|
||||
[TestOf(typeof(GameTicker)), TestOf(typeof(AntagSelectionSystem)), TestOf(typeof(AntagSelectionComponent))]
|
||||
[TestCaseSource(nameof(_gamePresets))]
|
||||
[Description("Ensures all Game Presets are able to start and assign all antags correctly without spawning anyone in nullspace.")]
|
||||
public async Task TestAllGamemodesCanStart(string presetId)
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var client = Pair.Client;
|
||||
var protoMan = server.ProtoMan;
|
||||
var entMan = server.EntMan;
|
||||
var ticker = server.System<GameTicker>();
|
||||
var antagSys = server.System<AntagSelectionSystem>();
|
||||
var mind = server.System<SharedMindSystem>();
|
||||
|
||||
// Initially in the lobby
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
|
||||
Assert.That(client.AttachedEntity, Is.Null);
|
||||
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
|
||||
|
||||
// Don't start dummy antag game rule because we want our antags to be predictable for the test.
|
||||
server.CfgMan.SetCVar(CCVars.GameTickerIgnoredPresets, GameTicker.DummyGameRule);
|
||||
|
||||
var preset = protoMan.Index<GamePresetPrototype>(presetId);
|
||||
|
||||
// Spawn the minimum number of players.
|
||||
var players = new List<ICommonSession>();
|
||||
players.Add(client.Session);
|
||||
var min = 0;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
min = ticker.GetMinimumPlayerCount(preset);
|
||||
});
|
||||
|
||||
// We should already have one client connected, and we need to check the min
|
||||
|
||||
// If we have antags, make sure that those with the correct preferences can spawn with them!
|
||||
List<(AntagSpecifierPrototype, int)> rules = [];
|
||||
|
||||
var antags = 0;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
foreach (var ruleId in preset.Rules)
|
||||
{
|
||||
if (ruleId == GameTicker.DummyGameRule)
|
||||
continue;
|
||||
|
||||
if (!protoMan.Resolve(ruleId, out var rule ))
|
||||
continue; // Bruh moment
|
||||
|
||||
// Ignore non-antag game-rules.
|
||||
if (!rule.TryGetComponent<AntagSelectionComponent>(out var antag, entMan.ComponentFactory))
|
||||
continue;
|
||||
|
||||
var runningCount = 0;
|
||||
|
||||
foreach (var selector in antag.Antags)
|
||||
{
|
||||
// Throw on invalid prototypes, skip roundstart ghost roles.
|
||||
if (!protoMan.Resolve(selector.Proto, out var definition) || definition.PrefRoles.Count == 0)
|
||||
continue;
|
||||
|
||||
var count = antagSys.GetTargetAntagCount(selector, min, ref runningCount);
|
||||
antags += count;
|
||||
rules.Add((definition, count));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// No preset should ever try to spawn more antags roundstart than it can spawn players.
|
||||
Assert.That(antags <= min, Is.True);
|
||||
if (min > 1)
|
||||
{
|
||||
var dummies = await server.AddDummySessions(min - 1);
|
||||
// Put our client at the front of the list.
|
||||
players = players.Union(dummies).ToList();
|
||||
}
|
||||
|
||||
await Pair.RunUntilSynced();
|
||||
|
||||
// This also ensures that admin commands work properly :P
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
ticker.ToggleReadyAll(true);
|
||||
});
|
||||
|
||||
var i = 0;
|
||||
foreach (var (antag, amount) in rules)
|
||||
{
|
||||
for (var count = 0; count < amount; count++)
|
||||
{
|
||||
await Pair.SetAntagPreference(antag.PrefRoles.FirstOrDefault(), true, players[i++].UserId);
|
||||
Assert.That(i < min, $"Tried to assign more antags than there were players");
|
||||
}
|
||||
}
|
||||
|
||||
await Pair.RunUntilSynced();
|
||||
await Pair.WaitCommand($"setgamepreset {presetId}");
|
||||
await Pair.WaitCommand("startround");
|
||||
await Pair.RunUntilSynced();
|
||||
|
||||
// Game should have started
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
|
||||
Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.JoinedGame));
|
||||
Assert.That(ticker.PlayerGameStatuses.Count == players.Count);
|
||||
Assert.That(client.EntMan.EntityExists(client.AttachedEntity));
|
||||
|
||||
var player = Pair.Player!.AttachedEntity!.Value;
|
||||
Assert.That(entMan.EntityExists(player));
|
||||
|
||||
// Start all game presets so antags spawn!
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
ticker.StartGamePresetRules();
|
||||
});
|
||||
await Pair.RunUntilSynced();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var j = 0;
|
||||
foreach (var (antag, amount) in rules)
|
||||
{
|
||||
for (var count = 0; count < amount; count++)
|
||||
{
|
||||
AssertAntagInitialized(antag, players[j++]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Maps now exist
|
||||
Assert.That(entMan.Count<MapComponent>(), Is.GreaterThan(0));
|
||||
Assert.That(entMan.Count<MapGridComponent>(), Is.GreaterThan(0));
|
||||
Assert.That(entMan.Count<StationCentcommComponent>(), Is.EqualTo(1));
|
||||
|
||||
// Clear game preset and return to lobby
|
||||
await Pair.WaitCommand("golobby");
|
||||
ticker.SetGamePreset((GamePresetPrototype) null);
|
||||
await Pair.RunUntilSynced();
|
||||
void AssertAntagInitialized(AntagSpecifierPrototype antag, ICommonSession session)
|
||||
{
|
||||
Assert.That(mind.TryGetMind(session, out var mindEnt, out var mindComp),
|
||||
$"Session {session} spawned into the game as an antag but had no mind!");
|
||||
Assert.That(entMan.EntityExists(mindComp!.CurrentEntity),
|
||||
$"Session {session} spawned into the game as an antag, but had no entity!");
|
||||
var ent = mindComp.CurrentEntity!.Value;
|
||||
|
||||
// Make sure all components were added
|
||||
foreach (var comp in antag.Components)
|
||||
{
|
||||
Assert.That(entMan.HasComponent(ent, comp.Value.Component.GetType()),
|
||||
$"Entity {entMan.ToPrettyString(ent)} owned by {session} failed to acquire {comp.Key} component, while becoming {antag.ID}");
|
||||
}
|
||||
|
||||
// Make sure all mind components were added
|
||||
foreach (var comp in antag.MindComponents)
|
||||
{
|
||||
Assert.That(entMan.HasComponent(mindEnt, comp.Value.Component.GetType()),
|
||||
$"Mind {entMan.ToPrettyString(mindEnt)} owned by {session} failed to acquire {comp.Key} component, while becoming {antag.ID}");
|
||||
}
|
||||
|
||||
if (antag.MindRoles != null)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var role in antag.MindRoles)
|
||||
{
|
||||
Assert.That(mindComp!.MindRoleContainer.ContainedEntities.Any(x => entMan.MetaQuery.Comp(x).EntityPrototype?.ID == role),
|
||||
$"{SToPrettyString(mindEnt)} owned by {session}, failed to acquire role {role} for antagonist {antag}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameRules;
|
||||
|
||||
// Once upon a time, players in the lobby weren't ever considered eligible for antag roles.
|
||||
// Lets not let that happen again.
|
||||
[TestFixture]
|
||||
public sealed class AntagPreferenceTest : GameTest
|
||||
{
|
||||
public override PoolSettings PoolSettings => new PoolSettings
|
||||
{
|
||||
DummyTicker = false,
|
||||
Connected = true,
|
||||
InLobby = true
|
||||
};
|
||||
|
||||
[Test]
|
||||
public async Task TestLobbyPlayersValid()
|
||||
{
|
||||
var pair = Pair;
|
||||
|
||||
var server = pair.Server;
|
||||
var client = pair.Client;
|
||||
var ticker = server.System<GameTicker>();
|
||||
var sys = server.System<AntagSelectionSystem>();
|
||||
|
||||
// Initially in the lobby
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
|
||||
Assert.That(client.AttachedEntity, Is.Null);
|
||||
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
|
||||
|
||||
EntityUid uid = default;
|
||||
await server.WaitPost(() => uid = server.EntMan.Spawn("Traitor"));
|
||||
var rule = new Entity<AntagSelectionComponent>(uid, server.EntMan.GetComponent<AntagSelectionComponent>(uid));
|
||||
var def = rule.Comp.Definitions.Single();
|
||||
|
||||
// IsSessionValid & IsEntityValid are preference agnostic and should always be true for players in the lobby.
|
||||
// Though maybe that will change in the future, but then GetPlayerPool() needs to be updated to reflect that.
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
|
||||
// By default, traitor/antag preferences are disabled, so the pool should be empty.
|
||||
var sessions = new List<ICommonSession> { pair.Player! };
|
||||
var pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(0));
|
||||
|
||||
// Opt into the traitor role.
|
||||
await pair.SetAntagPreference("Traitor", true);
|
||||
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(1));
|
||||
pool.TryPickAndTake(pair.Server.ResolveDependency<IRobustRandom>(), out var picked);
|
||||
Assert.That(picked, Is.EqualTo(pair.Player));
|
||||
Assert.That(sessions.Count, Is.EqualTo(1));
|
||||
|
||||
// opt back out
|
||||
await pair.SetAntagPreference("Traitor", false);
|
||||
|
||||
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
|
||||
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
|
||||
pool = sys.GetPlayerPool(rule, sessions, def);
|
||||
Assert.That(pool.Count, Is.EqualTo(0));
|
||||
|
||||
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ public sealed class StartEndGameRulesTest : GameTest
|
||||
public override PoolSettings PoolSettings => new PoolSettings
|
||||
{
|
||||
Dirty = true,
|
||||
DummyTicker = false
|
||||
DummyTicker = false,
|
||||
Map = PoolManager.TestStation
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Utility;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Speech.Components;
|
||||
@@ -13,7 +17,14 @@ namespace Content.IntegrationTests.Tests.Humanoid;
|
||||
[TestOf(typeof(HumanoidProfileSystem))]
|
||||
public sealed class HumanoidProfileTests : GameTest
|
||||
{
|
||||
private static readonly EntProtoId BaseSpecies = "MobHuman";
|
||||
private static readonly ProtoId<SpeciesPrototype> Vox = "Vox";
|
||||
private static string[] _species = GameDataScrounger.PrototypesOfKind<SpeciesPrototype>();
|
||||
|
||||
private BodySystem _bodySystem;
|
||||
private HumanoidProfileSystem _humanoidProfile;
|
||||
private MarkingManager _markingManager;
|
||||
private SharedVisualBodySystem _visualBody;
|
||||
|
||||
[Test]
|
||||
public async Task EnsureValidLoading()
|
||||
@@ -27,8 +38,9 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
{
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var humanoidProfile = entityManager.System<HumanoidProfileSystem>();
|
||||
var human = entityManager.Spawn("MobHuman");
|
||||
humanoidProfile.ApplyProfileTo(human, new HumanoidCharacterProfile()
|
||||
var human = entityManager.Spawn(BaseSpecies);
|
||||
humanoidProfile.ApplyProfileTo(human,
|
||||
new HumanoidCharacterProfile()
|
||||
.WithSex(Sex.Female)
|
||||
.WithAge(67)
|
||||
.WithGender(Gender.Neuter)
|
||||
@@ -45,4 +57,123 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
Assert.That(voiceComponent.Sounds![Sex.Female], Is.EqualTo(voiceComponent.EmoteSounds));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestOf(typeof(HumanoidCharacterProfile)), TestOf(typeof(VisualBodyComponent))]
|
||||
[Description("Tests that the game can generate a completely random profile with a completely random species and apply it to a blank body.")]
|
||||
public async Task EnsureValidRandom()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
LoadDependencies(out var body, out var humanoidComponent);
|
||||
var profile = HumanoidCharacterProfile.Random();
|
||||
_humanoidProfile.ApplyProfileTo(body, profile);
|
||||
_visualBody.ApplyProfileTo(body, profile);
|
||||
|
||||
AssertValidProfile((body, humanoidComponent), profile);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestOf(typeof(HumanoidCharacterProfile)), TestOf(typeof(VisualBodyComponent))]
|
||||
[TestCaseSource(nameof(_species))]
|
||||
[Description("Tests that every species is able to randomly generate a valid appearance without issues.")]
|
||||
public async Task EnsureValidRandomSpecies(string species)
|
||||
{
|
||||
await Server.WaitIdleAsync();
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
LoadDependencies(out var body, out var humanoidComponent);
|
||||
|
||||
var proto = Server.ProtoMan.Index<SpeciesPrototype>(species);
|
||||
var profile = HumanoidCharacterProfile.RandomWithSpecies(species);
|
||||
_humanoidProfile.ApplyProfileTo(body, profile);
|
||||
_visualBody.ApplyProfileTo(body, profile);
|
||||
|
||||
Assert.That(humanoidComponent.Age, Is.LessThanOrEqualTo(proto.MaxAge));
|
||||
Assert.That(humanoidComponent.Age, Is.GreaterThanOrEqualTo(proto.MinAge));
|
||||
Assert.That(proto.Sexes.Contains(humanoidComponent.Sex), Is.True);
|
||||
Assert.That(humanoidComponent.Species, Is.EqualTo(species));
|
||||
var strategy = Server.ProtoMan.Index(proto.SkinColoration).Strategy;
|
||||
Assert.That(strategy.VerifySkinColor(profile.Appearance.SkinColor), Is.True);
|
||||
|
||||
AssertValidProfile((body, humanoidComponent), profile);
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadDependencies(out EntityUid body, out HumanoidProfileComponent humanoidComponent)
|
||||
{
|
||||
var entityManager = Server.ResolveDependency<IEntityManager>();
|
||||
_humanoidProfile = entityManager.System<HumanoidProfileSystem>();
|
||||
_markingManager = Server.ResolveDependency<MarkingManager>();
|
||||
_visualBody = entityManager.System<SharedVisualBodySystem>();
|
||||
_bodySystem = entityManager.System<BodySystem>();
|
||||
body = entityManager.Spawn(BaseSpecies);
|
||||
humanoidComponent = entityManager.GetComponent<HumanoidProfileComponent>(body);
|
||||
}
|
||||
|
||||
private void AssertValidProfile(Entity<HumanoidProfileComponent> body, HumanoidCharacterProfile profile)
|
||||
{
|
||||
_bodySystem.TryGetOrgansWithComponent<VisualOrganComponent>(body.Owner, out var organs);
|
||||
|
||||
foreach (var (_, visualOrgan) in organs)
|
||||
{
|
||||
Assert.That(visualOrgan.Profile.Sex, Is.EqualTo(profile.Sex));
|
||||
Assert.That(visualOrgan.Profile.EyeColor, Is.EqualTo(profile.Appearance.EyeColor));
|
||||
Assert.That(visualOrgan.Profile.SkinColor, Is.EqualTo(profile.Appearance.SkinColor));
|
||||
}
|
||||
|
||||
_bodySystem.TryGetOrgansWithComponent<VisualOrganMarkingsComponent>(body.Owner, out var markings);
|
||||
|
||||
foreach (var (_, markingOrgan) in markings)
|
||||
{
|
||||
// Needed to avoid access restrictions
|
||||
var data = markingOrgan.MarkingData;
|
||||
var groupProto = Server.ProtoMan.Index(data.Group);
|
||||
var counts = new Dictionary<HumanoidVisualLayers, int>();
|
||||
var freeMarkings = new List<Marking>();
|
||||
|
||||
foreach (var marking in markingOrgan.AppliedMarkings)
|
||||
{
|
||||
var markingProto = Server.ProtoMan.Index(marking.MarkingId);
|
||||
|
||||
Assert.That(markingProto.Sprites.Count, Is.EqualTo(marking.MarkingColors.Count));
|
||||
Assert.That(_markingManager.CanBeApplied(data.Group, profile.Sex, markingProto), Is.True);
|
||||
Assert.That(data.Layers.Contains(markingProto.BodyPart), Is.True);
|
||||
if (!markingProto.ForcedColoring && groupProto.Appearances.GetValueOrDefault(markingProto.BodyPart)?.MatchSkin != true)
|
||||
freeMarkings.Add(marking);
|
||||
|
||||
if (!groupProto.Limits.TryGetValue(markingProto.BodyPart, out var limits))
|
||||
continue;
|
||||
|
||||
var count = counts.GetValueOrDefault(markingProto.BodyPart);
|
||||
Assert.That(count, Is.LessThanOrEqualTo(limits.Limit));
|
||||
counts[markingProto.BodyPart] = count + 1;
|
||||
}
|
||||
|
||||
if (freeMarkings.Count == markingOrgan.AppliedMarkings.Count)
|
||||
continue;
|
||||
|
||||
// Go through the whole list a second time just for the colors!
|
||||
foreach (var marking in markingOrgan.AppliedMarkings)
|
||||
{
|
||||
if (freeMarkings.Contains(marking))
|
||||
continue;
|
||||
|
||||
var markingProto = Server.ProtoMan.Index(marking.MarkingId);
|
||||
|
||||
Assert.That(marking.MarkingColors,
|
||||
Is.EqualTo(MarkingColoring.GetMarkingLayerColors(markingProto, profile.Appearance.SkinColor, profile.Appearance.EyeColor, markingOrgan.AppliedMarkings)));
|
||||
|
||||
if (markingProto.SexRestriction != null)
|
||||
Assert.That(markingProto.SexRestriction, Is.EqualTo(profile.Sex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed partial class MindTests : GameTest
|
||||
components:
|
||||
- type: MindContainer
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
damageContainer: Biological
|
||||
- type: Body
|
||||
prototype: Human
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class EvacShuttleTest : GameTest
|
||||
pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, true);
|
||||
pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false);
|
||||
var gameMap = pair.Server.CfgMan.GetCVar(CCVars.GameMap);
|
||||
pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Saltern");
|
||||
pair.Server.CfgMan.SetCVar(CCVars.GameMap, PoolManager.TestStation);
|
||||
|
||||
await server.WaitPost(() => ticker.RestartRound());
|
||||
await pair.RunTicksSync(25);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Content.Shared.Roles;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -105,16 +106,25 @@ public sealed class StationJobsTest : GameTest
|
||||
}
|
||||
});
|
||||
|
||||
var dummies = await server.AddDummySessions(TotalPlayers);
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var fakePlayers = new Dictionary<NetUserId, HumanoidCharacterProfile>()
|
||||
.AddJob("TAssistant", JobPriority.Medium, PlayerCount)
|
||||
.AddPreference("TClown", JobPriority.Low)
|
||||
.AddPreference("TMime", JobPriority.High)
|
||||
.WithPlayers(
|
||||
new Dictionary<NetUserId, HumanoidCharacterProfile>()
|
||||
.AddJob("TCaptain", JobPriority.High, CaptainCount)
|
||||
);
|
||||
var fakePlayers = new Dictionary<NetUserId, HumanoidCharacterProfile>(TotalPlayers);
|
||||
var i = 0;
|
||||
foreach (var dummy in dummies)
|
||||
{
|
||||
if (i < PlayerCount)
|
||||
{
|
||||
fakePlayers.AddJob(dummy, "TAssistant", JobPriority.Medium)
|
||||
.AddPreference("TClown", JobPriority.Low)
|
||||
.AddPreference("TMime", JobPriority.High);
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
fakePlayers.AddJob(dummy, "TCaptain", JobPriority.High);
|
||||
}
|
||||
}
|
||||
Assert.That(fakePlayers, Is.Not.Empty);
|
||||
|
||||
var start = new Stopwatch();
|
||||
@@ -252,13 +262,9 @@ public sealed class StationJobsTest : GameTest
|
||||
internal static class JobExtensions
|
||||
{
|
||||
public static Dictionary<NetUserId, HumanoidCharacterProfile> AddJob(
|
||||
this Dictionary<NetUserId, HumanoidCharacterProfile> inp, string jobId, JobPriority prio = JobPriority.Medium,
|
||||
int amount = 1)
|
||||
this Dictionary<NetUserId, HumanoidCharacterProfile> inp, ICommonSession session, string jobId, JobPriority prio = JobPriority.Medium)
|
||||
{
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
inp.Add(new NetUserId(Guid.NewGuid()), HumanoidCharacterProfile.Random().WithJobPriority(jobId, prio));
|
||||
}
|
||||
inp.Add(session.UserId, HumanoidCharacterProfile.Random().WithJobPriority(jobId, prio));
|
||||
|
||||
return inp;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class EntityStorageTests : GameTest
|
||||
components:
|
||||
- type: EntityStorage
|
||||
- type: Damageable
|
||||
- type: Injurable
|
||||
damageContainer: Inorganic
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
|
||||
@@ -81,8 +81,9 @@ namespace Content.IntegrationTests.Tests
|
||||
name: TestRestockExplode
|
||||
components:
|
||||
- type: Damageable
|
||||
damageContainer: Inorganic
|
||||
damageModifierSet: Metallic
|
||||
- type: Injurable
|
||||
damageContainer: Inorganic
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
|
||||
@@ -36,16 +36,17 @@ namespace Content.Server.Administration.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.TryGetComponent(uid, out SolutionContainerManagerComponent? man))
|
||||
{
|
||||
shell.WriteLine($"Entity does not have any solutions.");
|
||||
return;
|
||||
}
|
||||
|
||||
var solutionContainerSystem = _entManager.System<SharedSolutionContainerSystem>();
|
||||
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solution))
|
||||
if (!solutionContainerSystem.TryGetSolution(uid.Value, args[1], out var solution))
|
||||
{
|
||||
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
|
||||
var solutions = solutionContainerSystem.EnumerateSolutions(uid.Value).ToArray();
|
||||
if (!solutions.Any())
|
||||
{
|
||||
shell.WriteLine("Entity does not have any solutions!");
|
||||
return;
|
||||
}
|
||||
|
||||
var validSolutions = string.Join(", ", solutions.Select(s => s.Name));
|
||||
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
@@ -41,7 +40,6 @@ public sealed class OwoifyCommand : IConsoleCommand
|
||||
|
||||
var owoSys = _entManager.System<OwOAccentSystem>();
|
||||
var metaDataSys = _entManager.System<MetaDataSystem>();
|
||||
|
||||
metaDataSys.SetEntityName(eUid.Value, owoSys.Accentuate(meta.EntityName), meta);
|
||||
metaDataSys.SetEntityDescription(eUid.Value, owoSys.Accentuate(meta.EntityDescription), meta);
|
||||
}
|
||||
|
||||
@@ -24,22 +24,23 @@ namespace Content.Server.Administration.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var uidNet))
|
||||
if (!NetEntity.TryParse(args[0], out var uidNet) || !_entManager.TryGetEntity(uidNet, out var uid))
|
||||
{
|
||||
shell.WriteLine($"Invalid entity id.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.TryGetEntity(uidNet, out var uid) || !_entManager.TryGetComponent(uid, out SolutionContainerManagerComponent? man))
|
||||
{
|
||||
shell.WriteLine($"Entity does not have any solutions.");
|
||||
return;
|
||||
}
|
||||
|
||||
var solutionContainerSystem = _entManager.System<SharedSolutionContainerSystem>();
|
||||
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solution))
|
||||
if (!solutionContainerSystem.TryGetSolution(uid.Value, args[1], out var solution))
|
||||
{
|
||||
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
|
||||
var solutions = solutionContainerSystem.EnumerateSolutions(uid.Value).ToArray();
|
||||
if (!solutions.Any())
|
||||
{
|
||||
shell.WriteLine("Entity does not have any solutions!");
|
||||
return;
|
||||
}
|
||||
|
||||
var validSolutions = string.Join(", ", solutions.Select(s => s.Name));
|
||||
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,16 +29,17 @@ namespace Content.Server.Administration.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.TryGetComponent(uid, out SolutionContainerManagerComponent? man))
|
||||
{
|
||||
shell.WriteLine($"Entity does not have any solutions.");
|
||||
return;
|
||||
}
|
||||
|
||||
var solutionContainerSystem = _entManager.System<SharedSolutionContainerSystem>();
|
||||
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solution))
|
||||
if (!solutionContainerSystem.TryGetSolution(uid.Value, args[1], out var solution))
|
||||
{
|
||||
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
|
||||
var solutions = solutionContainerSystem.EnumerateSolutions(uid.Value).ToArray();
|
||||
if (!solutions.Any())
|
||||
{
|
||||
shell.WriteLine("Entity does not have any solutions!");
|
||||
return;
|
||||
}
|
||||
|
||||
var validSolutions = string.Join(", ", solutions.Select(s => s.Name));
|
||||
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,16 +29,17 @@ namespace Content.Server.Administration.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.TryGetComponent(uid, out SolutionContainerManagerComponent? man))
|
||||
{
|
||||
shell.WriteLine($"Entity does not have any solutions.");
|
||||
return;
|
||||
}
|
||||
|
||||
var solutionContainerSystem = _entManager.System<SharedSolutionContainerSystem>();
|
||||
if (!solutionContainerSystem.TryGetSolution((uid.Value, man), args[1], out var solutionEnt, out var solution))
|
||||
if (!solutionContainerSystem.TryGetSolution(uid.Value, args[1], out var solutionEnt, out var solution))
|
||||
{
|
||||
var validSolutions = string.Join(", ", solutionContainerSystem.EnumerateSolutions((uid.Value, man)).Select(s => s.Name));
|
||||
var solutions = solutionContainerSystem.EnumerateSolutions(uid.Value).ToArray();
|
||||
if (!solutions.Any())
|
||||
{
|
||||
shell.WriteLine("Entity does not have any solutions!");
|
||||
return;
|
||||
}
|
||||
|
||||
var validSolutions = string.Join(", ", solutions.Select(s => s.Name));
|
||||
shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -441,17 +441,17 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
: null;
|
||||
}
|
||||
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs)
|
||||
public bool IsRoleBanned(ICommonSession player, params List<ProtoId<JobPrototype>> jobs)
|
||||
{
|
||||
return IsRoleBanned<JobPrototype>(player, jobs);
|
||||
}
|
||||
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags)
|
||||
public bool IsRoleBanned(ICommonSession player, params List<ProtoId<AntagPrototype>> antags)
|
||||
{
|
||||
return IsRoleBanned<AntagPrototype>(player, antags);
|
||||
}
|
||||
|
||||
private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles) where T : class, IPrototype
|
||||
private bool IsRoleBanned<T>(ICommonSession player, params List<ProtoId<T>> roles) where T : class, IPrototype
|
||||
{
|
||||
var bans = GetRoleBans(player.UserId);
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ public interface IBanManager
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="antags">A list of valid antag prototype IDs.</param>
|
||||
/// <returns>Returns True if an active role ban is found for this player for any of the listed roles.</returns>
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags);
|
||||
public bool IsRoleBanned(ICommonSession player, params List<ProtoId<AntagPrototype>> antags);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player is currently banned from any of the listed roles.
|
||||
@@ -82,7 +82,7 @@ public interface IBanManager
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="jobs">A list of valid job prototype IDs.</param>
|
||||
/// <returns>Returns True if an active role ban is found for this player for any of the listed roles.</returns>
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs);
|
||||
public bool IsRoleBanned(ICommonSession player, params List<ProtoId<JobPrototype>> jobs);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of prototype IDs with the player's job bans.
|
||||
|
||||
@@ -13,6 +13,7 @@ using Content.Server.Polymorph.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Server.Tabletop;
|
||||
using Content.Server.Tabletop.Components;
|
||||
|
||||
@@ -35,6 +35,7 @@ using Robust.Shared.Timing;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using static Content.Shared.Configurable.ConfigurationComponent;
|
||||
|
||||
namespace Content.Server.Administration.Systems
|
||||
@@ -73,7 +74,10 @@ namespace Content.Server.Administration.Systems
|
||||
{
|
||||
SubscribeLocalEvent<GetVerbsEvent<Verb>>(GetVerbs);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<SolutionContainerManagerComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
|
||||
|
||||
// TODO: This is genuinely terrible, solutions are already networked and we shouldn't need to update the BUI like this.
|
||||
SubscribeLocalEvent<SolutionComponent, SolutionChangedEvent>((x, ref _) => OnSolutionChanged(x.Owner));
|
||||
SubscribeLocalEvent<SolutionManagerComponent, SolutionChangedEvent>((x, ref _) => OnSolutionChanged(x.Owner));
|
||||
}
|
||||
|
||||
private void GetVerbs(GetVerbsEvent<Verb> ev)
|
||||
@@ -445,7 +449,7 @@ namespace Content.Server.Administration.Systems
|
||||
}
|
||||
|
||||
// Control mob verb
|
||||
if ((_toolshed.ActivePermissionController?.CheckInvokable(new CommandSpec(_toolshed.DefaultEnvironment.GetCommand("mind"), "control"), player, out _) ?? false) &&
|
||||
if (_toolshed.ActivePermissionController?.CheckInvokable(new CommandSpec(_toolshed.DefaultEnvironment.GetCommand("mind"), "control"), player, out _) ?? false &&
|
||||
args.User != args.Target)
|
||||
{
|
||||
Verb verb = new()
|
||||
@@ -573,7 +577,7 @@ namespace Content.Server.Administration.Systems
|
||||
|
||||
// Add verb to open Solution Editor
|
||||
if (_groupController.CanCommand(player, "addreagent") &&
|
||||
HasComp<SolutionContainerManagerComponent>(args.Target))
|
||||
(HasComp<SolutionManagerComponent>(args.Target) || HasComp<SolutionComponent>(args.Target)))
|
||||
{
|
||||
Verb verb = new()
|
||||
{
|
||||
@@ -588,13 +592,13 @@ namespace Content.Server.Administration.Systems
|
||||
}
|
||||
|
||||
#region SolutionsEui
|
||||
private void OnSolutionChanged(Entity<SolutionContainerManagerComponent> entity, ref SolutionContainerChangedEvent args)
|
||||
private void OnSolutionChanged(EntityUid uid)
|
||||
{
|
||||
foreach (var list in _openSolutionUis.Values)
|
||||
{
|
||||
foreach (var eui in list)
|
||||
{
|
||||
if (eui.Target == entity.Owner)
|
||||
if (eui.Target == uid)
|
||||
eui.StateDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed.Syntax;
|
||||
using Robust.Shared.Toolshed.TypeParsers;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
@@ -42,21 +41,15 @@ namespace Content.Server.Administration.UI
|
||||
|
||||
public override EuiStateBase GetNewState()
|
||||
{
|
||||
List<(string Name, NetEntity Solution)>? netSolutions;
|
||||
List<(string Name, NetEntity Solution)>? netSolutions = new();
|
||||
|
||||
if (_entityManager.TryGetComponent(Target, out SolutionContainerManagerComponent? container) && container.Containers.Count > 0)
|
||||
foreach (var (name, solution) in _solutionContainerSystem.EnumerateSolutions(Target))
|
||||
{
|
||||
netSolutions = new();
|
||||
foreach (var (name, solution) in _solutionContainerSystem.EnumerateSolutions((Target, container)))
|
||||
{
|
||||
if (name is null || !_entityManager.TryGetNetEntity(solution, out var netSolution))
|
||||
continue;
|
||||
if (name is null || !_entityManager.TryGetNetEntity(solution, out var netSolution))
|
||||
continue;
|
||||
|
||||
netSolutions.Add((name, netSolution.Value));
|
||||
}
|
||||
netSolutions.Add((name, netSolution.Value));
|
||||
}
|
||||
else
|
||||
netSolutions = null;
|
||||
|
||||
return new EditSolutionsEuiState(_entityManager.GetNetEntity(Target), netSolutions, _gameTiming.CurTick);
|
||||
}
|
||||
|
||||
@@ -6,35 +6,22 @@ namespace Content.Server.Antag;
|
||||
public sealed class AntagMultipleRoleSpawnerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AntagMultipleRoleSpawnerComponent, AntagSelectEntityEvent>(OnSelectEntity);
|
||||
|
||||
_sawmill = _log.GetSawmill("antag_multiple_spawner");
|
||||
}
|
||||
|
||||
private void OnSelectEntity(Entity<AntagMultipleRoleSpawnerComponent> ent, ref AntagSelectEntityEvent args)
|
||||
{
|
||||
// If its more than one the logic breaks
|
||||
if (args.AntagRoles.Count != 1)
|
||||
{
|
||||
_sawmill.Fatal($"Antag multiple role spawner had more than one antag ({args.AntagRoles.Count})");
|
||||
return;
|
||||
}
|
||||
|
||||
var role = args.AntagRoles[0];
|
||||
|
||||
var entProtos = ent.Comp.AntagRoleToPrototypes[role];
|
||||
var entProtos = ent.Comp.AntagRoleToPrototypes[args.Antag];
|
||||
|
||||
if (entProtos.Count == 0)
|
||||
return; // You will just get a normal job
|
||||
|
||||
args.Entity = Spawn(ent.Comp.PickAndTake ? _random.PickAndTake(entProtos) : _random.Pick(entProtos));
|
||||
// TODO: Could probably turn this into a dictionary that takes an antag prototype and spits out an entity?
|
||||
args.Entity = Spawn(ent.Comp.PickAndTake ? _random.PickAndTake(entProtos) : _random.Pick(entProtos), args.Coords);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
public sealed class AntagSelectionPlayerPool (List<List<ICommonSession>> orderedPools)
|
||||
{
|
||||
public bool TryPickAndTake(IRobustRandom random, [NotNullWhen(true)] out ICommonSession? session)
|
||||
{
|
||||
session = null;
|
||||
|
||||
foreach (var pool in orderedPools)
|
||||
{
|
||||
if (pool.Count == 0)
|
||||
continue;
|
||||
|
||||
session = random.PickAndTake(pool);
|
||||
break;
|
||||
}
|
||||
|
||||
return session != null;
|
||||
}
|
||||
|
||||
public int Count => orderedPools.Sum(p => p.Count);
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Players;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using static Content.Server.Antag.Components.AntagSelectionTime;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
|
||||
public sealed partial class AntagSelectionSystem
|
||||
{
|
||||
/// <inhereitdoc cref="CanBeAntag(ICommonSession,Entity{AntagSelectionComponent},AntagSpecifierPrototype,bool)"/>
|
||||
[PublicAPI]
|
||||
public bool CanBeAntag(ICommonSession player, AntagRule antagRule, bool checkPref = true)
|
||||
{
|
||||
return CanBeAntag(player, antagRule.GameRule, antagRule.Definition, checkPref);
|
||||
}
|
||||
|
||||
/// <inhereitdoc cref="CanBeAntag(ICommonSession,Entity{AntagSelectionComponent},AntagSpecifierPrototype,bool)"/>
|
||||
[PublicAPI]
|
||||
public bool CanBeAntag(ICommonSession player,
|
||||
Entity<AntagSelectionComponent> gameRule,
|
||||
ProtoId<AntagSpecifierPrototype> proto,
|
||||
bool checkPref = true)
|
||||
{
|
||||
// Can't be this antag if it doesn't exist :)
|
||||
if (!Proto.Resolve(proto, out var antag))
|
||||
return false;
|
||||
|
||||
return CanBeAntag(player, gameRule, antag, checkPref);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a player is able to be an antag performing a wide variety of checks.
|
||||
/// </summary>
|
||||
/// <param name="player">Player we're checking</param>
|
||||
/// <param name="gameRule">Game rule which we want to be an antag for, needed to ensure we haven't already been selected.</param>
|
||||
/// <param name="def">Antag definition we want to become</param>
|
||||
/// <param name="checkPref">Whether we want to check our antag preferences or not.</param>
|
||||
/// <returns>True if this player can be an antagonist.</returns>
|
||||
[PublicAPI]
|
||||
public bool CanBeAntag(ICommonSession player,
|
||||
Entity<AntagSelectionComponent> gameRule,
|
||||
AntagSpecifierPrototype def,
|
||||
bool checkPref = true)
|
||||
{
|
||||
if (!IsSessionValid(player, gameRule, def))
|
||||
return false;
|
||||
|
||||
if (IsAssignedAntag(gameRule, def, player))
|
||||
return false;
|
||||
|
||||
// Add player to the appropriate antag pool
|
||||
if (checkPref && !TryGetValidAntagPreferences(player, def.PrefRoles))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inhereitdoc cref="IsSessionValid(ICommonSession,Entity{AntagSelectionComponent},ProtoId{AntagSpecifierPrototype})"/>
|
||||
public bool IsSessionValid(ICommonSession player,
|
||||
Entity<AntagSelectionComponent> gameRule,
|
||||
ProtoId<AntagSpecifierPrototype> def)
|
||||
{
|
||||
if (!Proto.Resolve(def, out var antag))
|
||||
return false;
|
||||
|
||||
return IsSessionValid(player, gameRule, antag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if our session can play a given antagonist, checking if the session is role banned from the antag,
|
||||
/// </summary>
|
||||
/// <param name="player">Player which we are checking antag viability for</param>
|
||||
/// <param name="gameRule">Game that's trying to make this player an antag.</param>
|
||||
/// <param name="def">Antag definition we're checking against.</param>
|
||||
/// <returns>True if there is nothing stopping this session from becoming this antagonist.</returns>
|
||||
[PublicAPI]
|
||||
public bool IsSessionValid(ICommonSession player,
|
||||
Entity<AntagSelectionComponent> gameRule,
|
||||
AntagSpecifierPrototype def)
|
||||
{
|
||||
// Cannot be antag if you're not in the game.
|
||||
if (IsDisconnected(player))
|
||||
return false;
|
||||
|
||||
if (IsAntagBanned(player, def))
|
||||
return false;
|
||||
|
||||
// If our antag is mutually exclusive with other antags, yell about it!
|
||||
switch (def.MultiAntagSetting)
|
||||
{
|
||||
case AntagAcceptability.None:
|
||||
{
|
||||
if (IsAssignedAntag(player, gameRule))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case AntagAcceptability.NotExclusive:
|
||||
{
|
||||
if (IsAssignedExclusiveAntag(player, gameRule))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return player.AttachedEntity == null || HasComp<GhostComponent>(player.AttachedEntity) || IsEntityValid(player, def);
|
||||
}
|
||||
|
||||
/// <inhereitdoc cref="IsMindValid(EntityUid?,AntagSpecifierPrototype)"/>
|
||||
public bool IsMindValid(ICommonSession session, AntagSpecifierPrototype def)
|
||||
{
|
||||
return IsMindValid(session.GetMind(), def);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given mind entity is valid for the specified antag.
|
||||
/// </summary>
|
||||
/// <param name="mind">Mind we are checking</param>
|
||||
/// <param name="def">Antag definition we want to give this mind.</param>
|
||||
/// <returns>True if there is nothing stopping this mind entity from being this antag.</returns>
|
||||
private bool IsMindValid([NotNullWhen(true)] EntityUid? mind, AntagSpecifierPrototype def)
|
||||
{
|
||||
// The jobless can always be antag!
|
||||
if (!_jobs.MindTryGetJob(mind, out var job))
|
||||
return true;
|
||||
|
||||
// "Sorry buddy, but you can't be a traitor and the head of security" - Urist 1984
|
||||
// This checks nullability for our mind for free as well!
|
||||
if (def.JobBlacklist?.Contains(job) ?? false)
|
||||
return false;
|
||||
|
||||
if (!def.JobWhitelist?.Contains(job) ?? false)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks both the mind and attached entity of the given session to see if anything is blocking it from being converted to an antag.
|
||||
/// </summary>
|
||||
/// <param name="session">Entity whose validity we're checking.</param>
|
||||
/// <param name="def">Antag definition we want to give them.</param>
|
||||
/// <returns>True if there is nothing stopping this entity from being this antag. Or if there is no entity.</returns>
|
||||
public bool IsEntityValid(ICommonSession session, AntagSpecifierPrototype def)
|
||||
{
|
||||
return IsMindValid(session, def) && IsEntityValid(session.AttachedEntity, def);
|
||||
}
|
||||
|
||||
/// <inhereitdoc cref="IsEntityValid(EntityUid?,AntagSpecifierPrototype)"/>
|
||||
public bool IsEntityValid([NotNullWhen(true)] EntityUid? uid, ProtoId<AntagSpecifierPrototype> def)
|
||||
{
|
||||
if (!Proto.Resolve(def, out var antag))
|
||||
return false;
|
||||
|
||||
return IsEntityValid(uid, antag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given entity is able to become the given antagonist.
|
||||
/// Note that this does not check if the entity had a mind or if that mind can become an antag.
|
||||
/// </summary>
|
||||
/// <param name="uid">Entity whose validity we're checking.</param>
|
||||
/// <param name="def">Antag definition we want to give them.</param>
|
||||
/// <returns>True if there is nothing stopping this entity from being this antag. Or if there is no entity.</returns>
|
||||
public bool IsEntityValid([NotNullWhen(true)] EntityUid? uid, AntagSpecifierPrototype def)
|
||||
{
|
||||
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
|
||||
if (!_whitelist.CheckBoth(uid, def.Blacklist, def.Whitelist))
|
||||
return false;
|
||||
|
||||
if (_arrivals.IsOnArrivals((uid.Value, null)))
|
||||
return false;
|
||||
|
||||
// No ghosts!!!
|
||||
if (HasComp<GhostComponent>(uid))
|
||||
return false;
|
||||
|
||||
if (!def.AllowNonHumans && !HasComp<HumanoidProfileComponent>(uid))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if our session is banned from playing this antag. If so returns true.
|
||||
/// This is separate so that methods which normally force antag can return early.
|
||||
/// </summary>
|
||||
/// <param name="session">Player who may or may not be banned from an antagonist</param>
|
||||
/// <param name="definition">The definition which a player may or may not be banned from</param>
|
||||
/// <returns>True if any of the preferred roles within the definition hit a ban.</returns>
|
||||
[PublicAPI]
|
||||
public bool IsAntagBanned(ICommonSession session, AntagSpecifierPrototype definition)
|
||||
{
|
||||
if (_ban.GetAntagBans(session.UserId) is not { } bans)
|
||||
return false;
|
||||
|
||||
foreach (var role in definition.PrefRoles)
|
||||
{
|
||||
// banned!
|
||||
if (bans.Contains(role))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryMakeAntag(Entity{AntagSelectionComponent},AntagSpecifierPrototype,ICommonSession,bool)"/>
|
||||
[PublicAPI]
|
||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> gameRule,
|
||||
ProtoId<AntagSpecifierPrototype> proto,
|
||||
ICommonSession session,
|
||||
bool checkPref = true)
|
||||
{
|
||||
if (!Proto.Resolve(proto, out var def))
|
||||
return false;
|
||||
|
||||
return TryMakeAntag(gameRule, def, session, checkPref);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to make a given player into the specified antagonist for the given game rule.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> gameRule,
|
||||
AntagSpecifierPrototype prototype,
|
||||
ICommonSession session,
|
||||
bool checkPref = true)
|
||||
{
|
||||
_adminLogger.Add(LogType.AntagSelection,
|
||||
$"Start trying to make {session} become the antagonist: {ToPrettyString(gameRule)}, {prototype.ID}");
|
||||
|
||||
if (!CanBeAntag(session, gameRule, prototype, checkPref))
|
||||
return false;
|
||||
|
||||
PreSelectSession(gameRule, prototype, session);
|
||||
return TryInitializeAntag(gameRule, prototype, session);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryAssignNextAvailableAntag(Entity{AntagSelectionComponent},ICommonSession,int)"/>
|
||||
public bool TryAssignNextAvailableAntag(Entity<AntagSelectionComponent> gameRule, ICommonSession session)
|
||||
{
|
||||
return TryAssignNextAvailableAntag(gameRule, session, GetActivePlayerCount());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find an open antag slot for a given player and assign it to that player.
|
||||
/// </summary>
|
||||
/// <param name="gameRule">GameRule we are checking for antags.</param>
|
||||
/// <param name="session">Player we're trying to assign antag to.</param>
|
||||
/// <param name="players">Current number of players in the round. Used to determine antag count.</param>
|
||||
/// <returns>Returns true if an open antag slot was found and successfully assigned, false otherwise.</returns>
|
||||
public bool TryAssignNextAvailableAntag(Entity<AntagSelectionComponent> gameRule,
|
||||
ICommonSession session,
|
||||
int players)
|
||||
{
|
||||
foreach (var selector in gameRule.Comp.Antags)
|
||||
{
|
||||
if (!Proto.Resolve(selector.Proto, out var antag))
|
||||
continue;
|
||||
|
||||
// Because this value can theoretically fluctuate as players leave and join, we don't want to cache it.
|
||||
if (AllAntagsAssigned(gameRule, antag, players))
|
||||
continue;
|
||||
|
||||
// Try and assign this antag, if we fail, then try the next definition!
|
||||
if (TryMakeAntag(gameRule, antag, session))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to make this player be a late-join antag.
|
||||
/// </summary>
|
||||
/// <param name="session">The session to attempt to make antag.</param>
|
||||
[PublicAPI]
|
||||
public bool TryMakeLateJoinAntag(ICommonSession session)
|
||||
{
|
||||
// Sorry buddy, no antag for you!
|
||||
if (!RobustRandom.Prob(LateJoinRandomChance))
|
||||
return false;
|
||||
|
||||
// TODO: We may want to query all rules to add late joins to pre-selections?
|
||||
// This logic is effectively copy-pasted from the old system with some fixes.
|
||||
var query = QueryActiveRules();
|
||||
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
||||
while (query.MoveNext(out var uid, out _, out var antag, out _))
|
||||
{
|
||||
// This is intended to only be used for ghost roles so it shouldn't be assigned for late joins
|
||||
if (antag.SelectionTime == Never || !antag.LateJoinAdditional)
|
||||
continue;
|
||||
|
||||
rules.Add((uid, antag));
|
||||
}
|
||||
|
||||
RobustRandom.Shuffle(rules);
|
||||
|
||||
var players = GetActivePlayerCount();
|
||||
|
||||
foreach (var (uid, antag) in rules)
|
||||
{
|
||||
if (TryAssignNextAvailableAntag((uid, antag), session, players))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a list of AntagRules and tries to make ghost roles out of them.
|
||||
/// </summary>
|
||||
/// <param name="antagRules">list of antag rules we wish to turn into ghost roles.
|
||||
/// Note, a ghost role can only be created if the rule has the ghost role spawner protoId set to a valid prototype.</param>
|
||||
/// <param name="assert">Whether we should throw if the spawner prototype doesn't exist.</param>
|
||||
[PublicAPI]
|
||||
public void SpawnGhostRoles(List<AntagRule> antagRules, bool assert = false)
|
||||
{
|
||||
foreach (var rule in antagRules)
|
||||
{
|
||||
SpawnGhostRoles(rule.GameRule, rule.Definition, rule.Count, assert);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SpawnGhostRoles(Entity{AntagSelectionComponent},AntagCount[],bool)"/>
|
||||
[PublicAPI]
|
||||
public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule, int playerCount, bool assert = false)
|
||||
{
|
||||
SpawnGhostRoles(gameRule, GetAntags(gameRule, playerCount), assert);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a list of AntagCounts and tries to make ghost roles out of them
|
||||
/// </summary>
|
||||
/// <param name="gameRule">Game rule with the associated antags we're spawning</param>
|
||||
/// <param name="antagRules">Antags we want to make into ghost roles, with paired counts we need to spawn</param>
|
||||
/// <param name="assert">Whether we should throw if the spawner prototype doesn't exist.</param>
|
||||
[PublicAPI]
|
||||
public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule, AntagCount[] antagRules, bool assert = false)
|
||||
{
|
||||
foreach (var rule in antagRules)
|
||||
{
|
||||
SpawnGhostRoles(gameRule, rule.Definition, rule.Count, assert);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SpawnGhostRoles(Entity{AntagSelectionComponent},AntagSpecifierPrototype,int,bool)"/>
|
||||
[PublicAPI]
|
||||
public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule,
|
||||
ProtoId<AntagSpecifierPrototype> protoId,
|
||||
int count,
|
||||
bool assert = false)
|
||||
{
|
||||
if (!Proto.Resolve(protoId, out var antag))
|
||||
return;
|
||||
|
||||
SpawnGhostRoles(gameRule, antag, count, assert);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates ghost role spawners for a given antag definition equivalent to the count.
|
||||
/// </summary>
|
||||
/// <param name="gameRule">Game rule with the associated antags we're spawning</param>
|
||||
/// <param name="proto">Antag prototype we're spawning.</param>
|
||||
/// <param name="count">Amount of ghost roles we are spawning.</param>
|
||||
/// <param name="assert">Whether we should throw if the spawner prototype doesn't exist.</param>
|
||||
[PublicAPI]
|
||||
public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule, AntagSpecifierPrototype proto, int count, bool assert = false)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
SpawnGhostRole(gameRule, proto, assert);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ghost role spawner of a given antag for a given game rule.
|
||||
/// </summary>
|
||||
/// <param name="gameRule">Game rule with the associated antags we're spawning</param>
|
||||
/// <param name="proto">Antag prototype we're spawning.</param>
|
||||
/// <param name="assert">Whether we should throw if the spawner prototype doesn't exist.</param>
|
||||
[PublicAPI]
|
||||
public void SpawnGhostRole(Entity<AntagSelectionComponent> gameRule, AntagSpecifierPrototype proto, bool assert = false)
|
||||
{
|
||||
if (proto.SpawnerPrototype is not { } spawnerPrototype)
|
||||
{
|
||||
Debug.Assert(!assert, $"Tried to spawn a ghost role for {proto.ID}, but it had no prototype!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!TryGetValidSpawnPosition(gameRule, proto, out var coordinates))
|
||||
{
|
||||
Log.Error(
|
||||
$"Found no valid positions to place antag spawner for game rule: {ToPrettyString(gameRule)}, antag: {proto.ID}");
|
||||
return;
|
||||
}
|
||||
|
||||
var spawner = Spawn(spawnerPrototype, coordinates.Value);
|
||||
if (!TryComp<GhostRoleAntagSpawnerComponent>(spawner, out var spawnerComp))
|
||||
{
|
||||
Log.Error($"Antag spawner {spawner} does not have a {nameof(GhostRoleAntagSpawnerComponent)}.");
|
||||
_adminLogger.Add(LogType.AntagSelection,
|
||||
$"Antag spawner {spawner} in game rule {ToPrettyString(gameRule)} failed due to not having {nameof(GhostRoleAntagSpawnerComponent)}.");
|
||||
Del(spawner);
|
||||
return;
|
||||
}
|
||||
|
||||
spawnerComp.Rule = gameRule;
|
||||
spawnerComp.Definition = proto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find a valid existing game rule for our antag, creating a new one if none exist.
|
||||
/// Then attempts to ticket an existing antag slot to our player, forcing one if there are no open slots.
|
||||
/// You shouldn't be using this basically ever except for debug and admin stuff.
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public void ForceMakeAntag<T>(ICommonSession player, EntProtoId defaultRule) where T : Component
|
||||
{
|
||||
var rule = ForceGetGameRuleEnt<T>(defaultRule);
|
||||
|
||||
if (TryAssignNextAvailableAntag(rule, player))
|
||||
return;
|
||||
|
||||
if (rule.Comp.Antags.LastOrDefault() is not { } antag || !Proto.Resolve(antag.Proto, out var proto))
|
||||
return;
|
||||
|
||||
PreSelectSession(rule, proto, player);
|
||||
TryInitializeAntag(rule, proto, player);
|
||||
}
|
||||
|
||||
/// <inhereitdoc cref="ForceMakeAntag{T}(ICommonSession,EntProtoId,AntagSpecifierPrototype)"/>
|
||||
public void ForceMakeAntag<T>(ICommonSession player, EntProtoId ruleProto, ProtoId<AntagSpecifierPrototype> antagProto) where T : Component
|
||||
{
|
||||
if (!Proto.Resolve(antagProto, out var antag))
|
||||
return;
|
||||
|
||||
ForceMakeAntag<T>(player, ruleProto, antag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to create a specific antag from a specific game rule prototype. Checking if the game rule already exists first.
|
||||
/// </summary>
|
||||
/// <param name="player">Player we are making into an antag</param>
|
||||
/// <param name="ruleProto">Game rule prototype associated with the antag we are creating.</param>
|
||||
/// <param name="proto">Prototype for the antag we are creating.</param>
|
||||
/// <typeparam name="T">Component from the game rule we are creating, for faster querying.</typeparam>
|
||||
/// <remarks>
|
||||
/// Do not use this method for anything other than debugging purposes.
|
||||
/// This ignores antag bans and the like so genuinely *do not use this unless it's for debugging purposes*
|
||||
/// </remarks>
|
||||
public void ForceMakeAntag<T>(ICommonSession player, EntProtoId ruleProto, AntagSpecifierPrototype proto) where T : Component
|
||||
{
|
||||
var rule = ForceGetGameRuleEnt<T>(ruleProto);
|
||||
|
||||
foreach (var antag in rule.Comp.Antags)
|
||||
{
|
||||
if (antag.Proto != proto)
|
||||
continue;
|
||||
|
||||
// Try and assign this antag, if we fail, then try the next definition!
|
||||
PreSelectSession(rule, proto, player);
|
||||
if (TryInitializeAntag(rule, proto, player))
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Error($"Antag Prototype {proto.ID} does not exist in {ToPrettyString(rule)}, {ruleProto}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find a valid gamerule which matches a specific prototype and component.
|
||||
/// Note that this is private because you generally should not be forcing a gamerule and this code is evil.
|
||||
/// I'm not touching it any more than I have to.
|
||||
/// </summary>
|
||||
private Entity<AntagSelectionComponent> ForceGetGameRuleEnt<T>(string id) where T : Component
|
||||
{
|
||||
var query = EntityQueryEnumerator<T, AntagSelectionComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
if (MetaData(uid).EntityPrototype?.ID == id)
|
||||
return (uid, comp);
|
||||
}
|
||||
var ruleEnt = GameTicker.AddGameRule(id);
|
||||
RemComp<LoadMapRuleComponent>(ruleEnt);
|
||||
var antag = RuleQuery.Comp(ruleEnt);
|
||||
antag.AssignmentHandled = true; // don't do normal selection.
|
||||
GameTicker.StartGameRule(ruleEnt);
|
||||
return (ruleEnt, antag);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Antag.Selectors;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
@@ -18,62 +17,29 @@ namespace Content.Server.Antag;
|
||||
|
||||
public sealed partial class AntagSelectionSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
|
||||
/// </summary>
|
||||
public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> ent,
|
||||
[NotNullWhen(true)] out AntagSelectionDefinition? definition,
|
||||
int? players = null)
|
||||
/// <inhereitdoc cref="GetActivePlayerCount(IList{ICommonSession})"/>
|
||||
[PublicAPI]
|
||||
public int GetActivePlayerCount()
|
||||
{
|
||||
definition = null;
|
||||
|
||||
var totalTargetCount = GetTargetAntagCount(ent, players);
|
||||
var mindCount = ent.Comp.AssignedMinds.Count;
|
||||
if (mindCount >= totalTargetCount)
|
||||
return false;
|
||||
|
||||
// TODO ANTAG fix this
|
||||
// If here are two definitions with 1/10 and 10/10 slots filled, this will always return the second definition
|
||||
// even though it has already met its target
|
||||
// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA I fucking hate game ticker code.
|
||||
// It needs to track selected minds for each definition independently.
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
{
|
||||
var target = GetTargetAntagCount(ent, null, def);
|
||||
|
||||
if (mindCount < target)
|
||||
{
|
||||
definition = def;
|
||||
return true;
|
||||
}
|
||||
|
||||
mindCount -= target;
|
||||
}
|
||||
|
||||
return false;
|
||||
return GetActivePlayerCount(_playerManager.Sessions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of antagonists that should be present for a given rule based on the provided pool.
|
||||
/// A null pool will simply use the player count.
|
||||
/// Returns the total number of valid players from the given player pool.
|
||||
/// For a player to be valid, they must have a connection to the server, be in the round, and have a non-ghost entity.
|
||||
/// </summary>
|
||||
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, int? playerCount = null)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
{
|
||||
count += GetTargetAntagCount(ent, playerCount, def);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public int GetTotalPlayerCount(IList<ICommonSession> pool)
|
||||
/// <param name="pool">Player pool we're querying, this typically includes all players connected to the server.</param>
|
||||
/// <returns>The number of valid players</returns>
|
||||
[PublicAPI]
|
||||
public int GetActivePlayerCount(IList<ICommonSession> pool)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var session in pool)
|
||||
{
|
||||
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
|
||||
if (IsDisconnected(session))
|
||||
continue;
|
||||
|
||||
if (session.AttachedEntity is not { } uid || HasComp<GhostComponent>(uid))
|
||||
continue;
|
||||
|
||||
count++;
|
||||
@@ -82,28 +48,129 @@ public sealed partial class AntagSelectionSystem
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of antagonists that should be present for a given antag definition based on the provided pool.
|
||||
/// A null pool will simply use the player count.
|
||||
/// </summary>
|
||||
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, int? playerCount, AntagSelectionDefinition def)
|
||||
[PublicAPI]
|
||||
public IEnumerable<ICommonSession> GetActivePlayers()
|
||||
{
|
||||
// TODO ANTAG
|
||||
// make pool non-nullable
|
||||
// Review uses and ensure that people are INTENTIONALLY including players in the lobby if this is a mid-round
|
||||
// antag selection.
|
||||
var poolSize = playerCount ?? GetTotalPlayerCount(_playerManager.Sessions);
|
||||
return GetActivePlayers(_playerManager.Sessions);
|
||||
}
|
||||
|
||||
// factor in other definitions' affect on the count.
|
||||
var countOffset = 0;
|
||||
foreach (var otherDef in ent.Comp.Definitions)
|
||||
[PublicAPI]
|
||||
public IEnumerable<ICommonSession> GetActivePlayers(IList<ICommonSession> pool)
|
||||
{
|
||||
foreach (var session in pool)
|
||||
{
|
||||
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio; // Note: Is the PlayerRatio necessary here? Seems like it can cause issues for defs with varied PlayerRatio.
|
||||
}
|
||||
// make sure we don't double-count the current selection
|
||||
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
|
||||
if (IsDisconnected(session))
|
||||
continue;
|
||||
|
||||
return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max);
|
||||
if (session.AttachedEntity is not { } uid || HasComp<GhostComponent>(uid))
|
||||
continue;
|
||||
|
||||
yield return session;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDisconnected(ICommonSession session)
|
||||
{
|
||||
return session.Status is SessionStatus.Disconnected or SessionStatus.Zombie;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of antags a game rule wishes to spawn.
|
||||
/// </summary>
|
||||
/// <param name="gameRule">Game rule which is spawning antags</param>
|
||||
/// <param name="playerCount">Simulated player count</param>
|
||||
/// <returns>Total number of antags this gamerule will spawn</returns>
|
||||
[PublicAPI]
|
||||
public int GetTotalAntagCount(Entity<AntagSelectionComponent> gameRule, int playerCount)
|
||||
{
|
||||
var runningCount = 0;
|
||||
var count = 0;
|
||||
|
||||
// We assume that antag definitions are prioritized by order, and take up slots that other roles may take.
|
||||
// I.E for Nukies, it selects 1 commander which takes up 10 players, then one corpsman which takes up another 10, then we select X nukies based on the remaining player count.
|
||||
// This is how the system worked when I got here, and I decided not to change it to avoid fucking with team antag balance
|
||||
foreach (var antag in gameRule.Comp.Antags)
|
||||
{
|
||||
if (!Proto.Resolve(antag.Proto, out _))
|
||||
continue;
|
||||
|
||||
count += GetTargetAntagCount(antag, playerCount, ref runningCount);
|
||||
runningCount += count * antag.PlayerRatio;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of antags of a given type this game rule is attempting to spawn, for a given player count.
|
||||
/// </summary>
|
||||
/// <param name="gameRule">Game rule which is spawning the antags</param>
|
||||
/// <param name="playerCount">Current player count</param>
|
||||
/// <param name="proto">Antag prototype we're spawning</param>
|
||||
/// <returns>Number of antags of this type we're spawning.</returns>
|
||||
[PublicAPI]
|
||||
public int GetTargetAntagCount(Entity<AntagSelectionComponent> gameRule, int playerCount, ProtoId<AntagSpecifierPrototype> proto)
|
||||
{
|
||||
if (!Proto.Resolve(proto, out var antag))
|
||||
return 0;
|
||||
|
||||
return GetTargetAntagCount(gameRule, playerCount, antag);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetTargetAntagCount(Entity{AntagSelectionComponent},int,ProtoId{AntagSpecifierPrototype})"/>
|
||||
[PublicAPI]
|
||||
public int GetTargetAntagCount(Entity<AntagSelectionComponent> gameRule, int playerCount, AntagSpecifierPrototype proto)
|
||||
{
|
||||
var runningCount = 0;
|
||||
|
||||
// We assume that antag definitions are prioritized by order, and take up slots that other roles may take.
|
||||
// I.E for Nukies, it selects 1 commander which takes up 10 players, then one corpsman which takes up another 10, then we select X nukies based on the remaining player count.
|
||||
// This is how the system worked when I got here, and I decided not to change it to avoid fucking with team antag balance
|
||||
foreach (var antag in gameRule.Comp.Antags)
|
||||
{
|
||||
if (!Proto.Resolve(antag.Proto, out _))
|
||||
continue;
|
||||
|
||||
// We need to update our running count which is why we get the count for definitions we may not be assigning.
|
||||
var count = GetTargetAntagCount(antag, playerCount, ref runningCount);
|
||||
|
||||
if (antag.Proto == proto)
|
||||
return count;
|
||||
}
|
||||
|
||||
Log.Error($"Error, attempted to get the antag count for an antagonist, {proto.ID} not included in gamerule: {ToPrettyString(gameRule)}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not use this if you don't know what you're doing. This is public for test purposes only.
|
||||
/// </summary>
|
||||
public int GetTargetAntagCount(AntagCountSelector selector, int playerCount, ref int runningCount)
|
||||
{
|
||||
var count = selector.GetTargetAntagCount(RobustRandom, playerCount - runningCount);
|
||||
runningCount += count * selector.PlayerRatio;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of assigned antags of a given type from a game rule.
|
||||
/// </summary>
|
||||
/// <param name="gameRule">Game rule entity</param>
|
||||
/// <param name="proto">The antag prototype we're checking</param>
|
||||
/// <returns>The amount of sessions which this game rule has assigned our given prototype to.</returns>
|
||||
[PublicAPI]
|
||||
public int GetAssignedAntagCount(Entity<AntagSelectionComponent> gameRule, ProtoId<AntagSpecifierPrototype> proto)
|
||||
{
|
||||
return !gameRule.Comp.AssignedMinds.TryGetValue(proto, out var assigned) ? 0 : assigned.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all antags of this specific type from this specific game rule have been assigned.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public bool AllAntagsAssigned(Entity<AntagSelectionComponent> gameRule, AntagSpecifierPrototype proto, int players)
|
||||
{
|
||||
return GetAssignedAntagCount(gameRule, proto) < GetTargetAntagCount(gameRule, players, proto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -112,95 +179,76 @@ public sealed partial class AntagSelectionSystem
|
||||
/// <returns>
|
||||
/// A list containing, in order, the antag's mind, the session data, and the original name stored as a string.
|
||||
/// </returns>
|
||||
public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity<AntagSelectionComponent?> ent)
|
||||
[PublicAPI]
|
||||
public IEnumerable<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return new List<(EntityUid, SessionData, string)>();
|
||||
if (!RuleQuery.Resolve(ent, ref ent.Comp, false))
|
||||
yield break;
|
||||
|
||||
var output = new List<(EntityUid, SessionData, string)>();
|
||||
foreach (var (mind, name) in ent.Comp.AssignedMinds)
|
||||
foreach (var (_, minds) in ent.Comp.AssignedMinds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
foreach (var (mind, name) in minds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
|
||||
if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data))
|
||||
continue;
|
||||
if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data))
|
||||
continue;
|
||||
|
||||
output.Add((mind, data, name));
|
||||
yield return (mind, data, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns identifiable information for all antagonists to be used in a round end summary.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A list containing, in order, the antag's mind, the session data, and the original name stored as a string.
|
||||
/// </returns>
|
||||
[PublicAPI]
|
||||
public IEnumerable<(EntityUid, string)> GetAntagIdentities(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!RuleQuery.Resolve(ent, ref ent.Comp, false))
|
||||
yield break;
|
||||
|
||||
foreach (var (_, minds) in ent.Comp.AssignedMinds)
|
||||
{
|
||||
foreach (var identity in minds)
|
||||
{
|
||||
yield return identity;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the minds of antagonists.
|
||||
/// </summary>
|
||||
public List<Entity<MindComponent>> GetAntagMinds(Entity<AntagSelectionComponent?> ent)
|
||||
[PublicAPI]
|
||||
public IEnumerable<Entity<MindComponent>> GetAntagMinds(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return new();
|
||||
if (!RuleQuery.Resolve(ent, ref ent.Comp, false))
|
||||
yield break;
|
||||
|
||||
var output = new List<Entity<MindComponent>>();
|
||||
foreach (var (mind, _) in ent.Comp.AssignedMinds)
|
||||
foreach (var (_, minds) in ent.Comp.AssignedMinds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
foreach (var (mind, _) in minds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
|
||||
output.Add((mind, mindComp));
|
||||
yield return (mind, mindComp);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Helper to get just the mind entities and not names.
|
||||
/// </remarks>
|
||||
public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return new();
|
||||
|
||||
return ent.Comp.AssignedMinds.Select(p => p.Item1).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given session has enabled the antag preferences for a given definition,
|
||||
/// and if it is blocked by any requirements or bans.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if at least one role from the provided list passes every condition</returns>>
|
||||
public bool ValidAntagPreference(ICommonSession? session, List<ProtoId<AntagPrototype>> roles)
|
||||
{
|
||||
if (session == null)
|
||||
return true;
|
||||
|
||||
if (roles.Count == 0)
|
||||
return false;
|
||||
|
||||
if (!_pref.TryGetCachedPreferences(session.UserId, out var pref))
|
||||
return false;
|
||||
|
||||
var character = (HumanoidCharacterProfile) pref.SelectedCharacter;
|
||||
|
||||
var valid = false;
|
||||
|
||||
// Check each individual antag role
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var list = new List<ProtoId<AntagPrototype>>{role};
|
||||
|
||||
if (character.AntagPreferences.Contains(role)
|
||||
&& !_ban.IsRoleBanned(session, list)
|
||||
&& _playTime.IsAllowed(session, list))
|
||||
valid = true;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the antagonists for this rule who are currently alive
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public IEnumerable<EntityUid> GetAliveAntags(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
if (!RuleQuery.Resolve(ent, ref ent.Comp, false))
|
||||
yield break;
|
||||
|
||||
var minds = GetAntagMinds(ent);
|
||||
@@ -217,9 +265,10 @@ public sealed partial class AntagSelectionSystem
|
||||
/// <summary>
|
||||
/// Returns the number of alive antagonists for this rule.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public int GetAliveAntagCount(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
if (!RuleQuery.Resolve(ent, ref ent.Comp, false))
|
||||
return 0;
|
||||
|
||||
var numbah = 0;
|
||||
@@ -238,9 +287,10 @@ public sealed partial class AntagSelectionSystem
|
||||
/// <summary>
|
||||
/// Returns if there are any remaining antagonists alive for this rule.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public bool AnyAliveAntags(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
if (!RuleQuery.Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
return GetAliveAntags(ent).Any();
|
||||
@@ -249,9 +299,10 @@ public sealed partial class AntagSelectionSystem
|
||||
/// <summary>
|
||||
/// Checks if all the antagonists for this rule are alive.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public bool AllAntagsAlive(Entity<AntagSelectionComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
if (!RuleQuery.Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
return GetAliveAntagCount(ent) == ent.Comp.AssignedMinds.Count;
|
||||
@@ -264,6 +315,7 @@ public sealed partial class AntagSelectionSystem
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
[PublicAPI]
|
||||
public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
|
||||
{
|
||||
if (!_mind.TryGetMind(entity, out _, out var mindComponent))
|
||||
@@ -296,7 +348,7 @@ public sealed partial class AntagSelectionSystem
|
||||
/// </summary>
|
||||
/// <param name="session">The player chosen to be an antag</param>
|
||||
/// <param name="data">The briefing data</param>
|
||||
public void SendBriefing(
|
||||
private void SendBriefing(
|
||||
ICommonSession? session,
|
||||
BriefingData? data)
|
||||
{
|
||||
@@ -314,6 +366,7 @@ public sealed partial class AntagSelectionSystem
|
||||
/// <param name="briefing">The briefing text to send</param>
|
||||
/// <param name="briefingColor">The color the briefing should be, null for default</param>
|
||||
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
|
||||
// TODO: It might take a bit of effort but this can probably be privated.
|
||||
public void SendBriefing(
|
||||
ICommonSession? session,
|
||||
string briefing,
|
||||
@@ -327,98 +380,292 @@ public sealed partial class AntagSelectionSystem
|
||||
if (!string.IsNullOrEmpty(briefing))
|
||||
{
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
|
||||
_chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel,
|
||||
briefingColor);
|
||||
_chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This technically is a gamerule-ent-less way to make an entity an antag.
|
||||
/// You should almost never be using this.
|
||||
/// Returns a list of all antag players who are constrained by a job whitelist or blacklist from an antag.
|
||||
/// </summary>
|
||||
public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule) where T : Component
|
||||
/// <param name="except">Antag prototypes we're excluding for our returned job whitelist/blacklist.</param>
|
||||
/// <returns>A dictionary of antag sessions, and their job blacklists.</returns>
|
||||
[PublicAPI]
|
||||
public Dictionary<ICommonSession, (HashSet<ProtoId<JobPrototype>>? Whitelist, HashSet<ProtoId<JobPrototype>>? Blacklist)>
|
||||
GetAntagJobs(params HashSet<ProtoId<AntagSpecifierPrototype>> except)
|
||||
{
|
||||
var rule = ForceGetGameRuleEnt<T>(defaultRule);
|
||||
|
||||
if (!TryGetNextAvailableDefinition(rule, out var def))
|
||||
def = rule.Comp.Definitions.Last();
|
||||
MakeAntag(rule, player, def.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to grab one of the weird specific antag gamerule ents or starts a new one.
|
||||
/// This is gross code but also most of this is pretty gross to begin with.
|
||||
/// </summary>
|
||||
public Entity<AntagSelectionComponent> ForceGetGameRuleEnt<T>(string id) where T : Component
|
||||
{
|
||||
var query = EntityQueryEnumerator<T, AntagSelectionComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
return (uid, comp);
|
||||
}
|
||||
var ruleEnt = GameTicker.AddGameRule(id);
|
||||
RemComp<LoadMapRuleComponent>(ruleEnt);
|
||||
var antag = Comp<AntagSelectionComponent>(ruleEnt);
|
||||
antag.AssignmentComplete = true; // don't do normal selection.
|
||||
GameTicker.StartGameRule(ruleEnt);
|
||||
return (ruleEnt, antag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all sessions that have been preselected for antag.
|
||||
/// </summary>
|
||||
/// <param name="except">A specific definition to be excluded from the check.</param>
|
||||
public HashSet<ICommonSession> GetPreSelectedAntagSessions(AntagSelectionDefinition? except = null)
|
||||
{
|
||||
var result = new HashSet<ICommonSession>();
|
||||
var result = new Dictionary<ICommonSession, (HashSet<ProtoId<JobPrototype>>? Whitelist, HashSet<ProtoId<JobPrototype>>? Blacklist)>();
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var def in comp.Definitions)
|
||||
foreach (var antag in comp.Antags)
|
||||
{
|
||||
if (def.Equals(except))
|
||||
if (except.Contains(antag))
|
||||
continue;
|
||||
|
||||
if (comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
result.UnionWith(set);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all sessions that have been preselected for antag and are exclusive, i.e. should not be paired with other antags.
|
||||
/// </summary>
|
||||
/// <param name="except">A specific definition to be excluded from the check.</param>
|
||||
// Note: This is a bit iffy since technically this exclusive definition is defined via the MultiAntagSetting, while there's a separately tracked antagExclusive variable in the mindrole.
|
||||
// We can't query that however since there's no guarantee the mindrole has been given out yet when checking pre-selected antags.
|
||||
// I don't think there's any instance where they differ, but it's something to be aware of for a potential future refactor.
|
||||
public HashSet<ICommonSession> GetPreSelectedExclusiveAntagSessions(AntagSelectionDefinition? except = null)
|
||||
{
|
||||
var result = new HashSet<ICommonSession>();
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var def in comp.Definitions)
|
||||
{
|
||||
if (def.Equals(except))
|
||||
if (!comp.PreSelectedSessions.TryGetValue(antag, out var set) || !Proto.Resolve(antag.Proto, out var proto))
|
||||
continue;
|
||||
|
||||
if (def.MultiAntagSetting == AntagAcceptability.None && comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
// Check this here so we don't make a dictionary entry for a bunch of players, with empty blacklists and whitelists.
|
||||
if (proto.JobBlacklist == null && proto.JobWhitelist == null)
|
||||
continue;
|
||||
|
||||
foreach (var player in set)
|
||||
{
|
||||
result.UnionWith(set);
|
||||
break;
|
||||
if (result.TryGetValue(player, out var jobs))
|
||||
{
|
||||
if (proto.JobWhitelist != null)
|
||||
{
|
||||
if (jobs.Whitelist == null)
|
||||
jobs.Whitelist = proto.JobWhitelist;
|
||||
else
|
||||
jobs.Whitelist.UnionWith(proto.JobWhitelist);
|
||||
}
|
||||
|
||||
if (proto.JobBlacklist != null)
|
||||
{
|
||||
if (jobs.Blacklist == null)
|
||||
jobs.Blacklist = proto.JobBlacklist;
|
||||
else
|
||||
jobs.Blacklist.UnionWith(proto.JobBlacklist);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(player, (proto.JobWhitelist, proto.JobBlacklist));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all blacklisted and whitelisted jobs for this player
|
||||
/// </summary>
|
||||
/// <param name="player">Player we're checking the blocked jobs of</param>
|
||||
/// <param name="except">Antag prototypes we're excluding in our search</param>
|
||||
/// <returns>A tuple of a whitelist and blacklist hashset for this player.</returns>
|
||||
[PublicAPI]
|
||||
public (HashSet<ProtoId<JobPrototype>>? Whitelist, HashSet<ProtoId<JobPrototype>>? Blacklist)
|
||||
GetAntagJobs(ICommonSession player, params HashSet<ProtoId<AntagSpecifierPrototype>> except)
|
||||
{
|
||||
HashSet<ProtoId<JobPrototype>>? whitelist = null;
|
||||
HashSet<ProtoId<JobPrototype>>? blacklist = null;
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var antag in comp.Antags)
|
||||
{
|
||||
if (except.Contains(antag))
|
||||
continue;
|
||||
|
||||
if (!comp.PreSelectedSessions.TryGetValue(antag, out var set) || !set.Contains(player))
|
||||
continue;
|
||||
|
||||
if (!Proto.Resolve(antag.Proto, out var proto))
|
||||
continue;
|
||||
|
||||
if (proto.JobWhitelist != null)
|
||||
{
|
||||
if (whitelist == null)
|
||||
whitelist = proto.JobWhitelist;
|
||||
else
|
||||
whitelist.UnionWith(proto.JobWhitelist);
|
||||
}
|
||||
|
||||
if (proto.JobBlacklist != null)
|
||||
{
|
||||
if (blacklist == null)
|
||||
blacklist = proto.JobBlacklist;
|
||||
else
|
||||
blacklist.UnionWith(proto.JobBlacklist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (whitelist, blacklist);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all sessions that have been preselected for antag.
|
||||
/// </summary>
|
||||
/// <param name="except">A specific definition to be excluded from the check.</param>
|
||||
[PublicAPI]
|
||||
public HashSet<ICommonSession> GetPreSelectedAntagSessions(params HashSet<ProtoId<AntagSpecifierPrototype>> except)
|
||||
{
|
||||
var result = new HashSet<ICommonSession>();
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var antag in comp.Antags)
|
||||
{
|
||||
if (except.Contains(antag))
|
||||
continue;
|
||||
|
||||
if (comp.PreSelectedSessions.TryGetValue(antag, out var set))
|
||||
result.UnionWith(set);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryGetValidAntagPreferences(ICommonSession session, List<ProtoId<AntagPrototype>>? filter = null)
|
||||
{
|
||||
return TryGetValidAntagPreferences(session, out _, filter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the antag preferences for a specific session, excluding banned antags or antags this player lacks the playtime for.
|
||||
/// </summary>
|
||||
/// <param name="session">Session we want the antag preferences for</param>
|
||||
/// <param name="antags">List of valid antag prototypes this player can play as.</param>
|
||||
/// <param name="filter">Optional list of antag prototypes we're specifically looking for.</param>
|
||||
/// <returns>True if this player has any antags enabled they can play and pass our filter</returns>
|
||||
[PublicAPI]
|
||||
public bool TryGetValidAntagPreferences(ICommonSession session, out List<ProtoId<AntagPrototype>> antags, List<ProtoId<AntagPrototype>>? filter = null)
|
||||
{
|
||||
antags = new List<ProtoId<AntagPrototype>>(GetValidAntagPreferences(session, filter));
|
||||
return antags.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the antag preferences for a specific session, excluding banned antags or antags this player lacks the playtime for.
|
||||
/// Optionally takes a filter for antag preferences we're specifically looking for.
|
||||
/// </summary>
|
||||
/// <param name="session">Session we want the antag preferences for</param>
|
||||
/// <param name="filter">Optional list of antag preferences we're specifically looking for.</param>
|
||||
/// <returns>A list of all antags which the player meets the requirement for, and are contained in the filter</returns>
|
||||
[PublicAPI]
|
||||
public IEnumerable<ProtoId<AntagPrototype>> GetValidAntagPreferences(ICommonSession session, List<ProtoId<AntagPrototype>>? filter = null)
|
||||
{
|
||||
if (!_pref.TryGetCachedPreferences(session.UserId, out var prefs))
|
||||
yield break;
|
||||
|
||||
foreach (var antag in prefs.SelectedCharacter.AntagPreferences)
|
||||
{
|
||||
// We also check this in IsSessionValid, but we also check it here since this is public API.
|
||||
if (_ban.IsRoleBanned(session, antag) || !_playTime.IsAllowed(session, antag))
|
||||
continue;
|
||||
|
||||
if (filter != null && !filter.Contains(antag))
|
||||
continue;
|
||||
|
||||
yield return antag;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a player has been assigned antag for a specific game rule.
|
||||
/// Does not check if that game rule is active or ended so check that beforehand if it matters.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[PublicAPI]
|
||||
public bool IsAssignedAntag(Entity<AntagSelectionComponent> gameRule, ICommonSession player)
|
||||
{
|
||||
// First check our mindroles.
|
||||
if (_role.PlayerIsAntagonist(player))
|
||||
return true;
|
||||
|
||||
foreach (var (_, sessions) in gameRule.Comp.PreSelectedSessions)
|
||||
{
|
||||
// Session has already been preselected as antagonist, and therefore *has* been assigned antag!
|
||||
if (sessions.Contains(player))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a player has been assigned a specific antag for a specific game rule.
|
||||
/// Does not check if that game rule is active or ended so check that beforehand if it matters.
|
||||
/// Also does not check mind roles, but if the game rule data is messed up you have bigger problems.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[PublicAPI]
|
||||
public bool IsAssignedAntag(Entity<AntagSelectionComponent> gameRule, ProtoId<AntagSpecifierPrototype> antag, ICommonSession player)
|
||||
{
|
||||
if (!gameRule.Comp.PreSelectedSessions.TryGetValue(antag, out var sessions))
|
||||
return false;
|
||||
|
||||
// Session has already been preselected as antagonist, and therefore *has* been assigned antag!
|
||||
return sessions.Contains(player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given player is currently assigned antag for any game rule.
|
||||
/// </summary>
|
||||
/// <param name="player">Player who may or may not be the antagonist.</param>
|
||||
/// <param name="ignored">Game rule entities we're ignoring.
|
||||
/// You can only get one antag per game rule so it's fine to ignore by uid.</param>
|
||||
/// <returns>True if there is a game rule giving this player antag status</returns>
|
||||
[PublicAPI]
|
||||
public bool IsAssignedAntag(ICommonSession player, params HashSet<EntityUid> ignored)
|
||||
{
|
||||
// First check our mindroles.
|
||||
if (_role.PlayerIsAntagonist(player))
|
||||
return true;
|
||||
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (ignored.Contains(uid) || HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var (_, sessions) in comp.PreSelectedSessions)
|
||||
{
|
||||
// Session has already been preselected as antagonist, and therefore *has* been assigned antag!
|
||||
if (sessions.Contains(player))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given player is currently assigned antag for any game rule that is incompatible with other antag prototypes.
|
||||
/// </summary>
|
||||
/// <param name="player">Player who may or may not be the antagonist.</param>
|
||||
/// <param name="ignored">Game rule entities we're ignoring.</param>
|
||||
/// <returns>True if there is a game rule giving this player antag status that is exclusive with other antags</returns>
|
||||
[PublicAPI]
|
||||
public bool IsAssignedExclusiveAntag(ICommonSession player, params HashSet<EntityUid> ignored)
|
||||
{
|
||||
// First check our mindroles.
|
||||
if (_role.MindIsExclusiveAntagonist(player.AttachedEntity))
|
||||
return true;
|
||||
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (ignored.Contains(uid) || HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var (proto, sessions) in comp.PreSelectedSessions)
|
||||
{
|
||||
if (!Proto.Resolve(proto, out var def))
|
||||
continue; // How did you even get here?
|
||||
|
||||
if (!sessions.Contains(player))
|
||||
continue;
|
||||
|
||||
if (def.MultiAntagSetting == AntagAcceptability.None)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -13,7 +14,7 @@ public sealed partial class AntagMultipleRoleSpawnerComponent : Component
|
||||
/// antag prototype -> list of possible entities to spawn for that antag prototype. Will choose from the list randomly once with replacement unless <see cref="PickAndTake"/> is set to true
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<ProtoId<AntagPrototype>, List<EntProtoId>> AntagRoleToPrototypes;
|
||||
public Dictionary<ProtoId<AntagSpecifierPrototype>, List<EntProtoId>> AntagRoleToPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// Should you remove ent prototypes from the list after spawning one.
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.Antag.Selectors;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Destructible.Thresholds;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -14,10 +12,12 @@ namespace Content.Server.Antag.Components;
|
||||
public sealed partial class AntagSelectionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Has the primary assignment of antagonists finished yet?
|
||||
/// Has the primary assignment of antagonists been handled yet?
|
||||
/// This is typically set to true at the start of antag assignment for a game rule.
|
||||
/// Note that this can be true even before all antags have been assigned.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool AssignmentComplete;
|
||||
public bool AssignmentHandled;
|
||||
|
||||
/// <summary>
|
||||
/// Has the antagonists been preselected but yet to be fully assigned?
|
||||
@@ -26,34 +26,35 @@ public sealed partial class AntagSelectionComponent : Component
|
||||
public bool PreSelectionsComplete;
|
||||
|
||||
/// <summary>
|
||||
/// The definitions for the antagonists
|
||||
/// If true, players that late join into a round have a chance of being converted into antagonists for this game rule.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<AntagSelectionDefinition> Definitions = new();
|
||||
public bool LateJoinAdditional;
|
||||
|
||||
/// <summary>
|
||||
/// The minds and original names of the players assigned to be antagonists.
|
||||
/// The antag specifiers for the antagonists
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public AntagCountSelector[] Antags;
|
||||
|
||||
/// <summary>
|
||||
/// Cached sessions of antag definitions and selected players.
|
||||
/// Players in this dict are not guaranteed to have been assigned the role yet, and may be removed if they fail to initialize as an antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<(EntityUid, string)> AssignedMinds = new();
|
||||
public Dictionary<ProtoId<AntagSpecifierPrototype>, HashSet<ICommonSession>> PreSelectedSessions = new();
|
||||
|
||||
/// <summary>
|
||||
/// The minds and original names of the players assigned to be antagonists, as well as their assigned antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<ProtoId<AntagSpecifierPrototype>, HashSet<(EntityUid uid, string name)>> AssignedMinds = new();
|
||||
|
||||
/// <summary>
|
||||
/// When the antag selection will occur.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
|
||||
|
||||
/// <summary>
|
||||
/// Cached sessions of antag definitions and selected players. Players in this dict are not guaranteed to have been assigned the role yet.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<AntagSelectionDefinition, HashSet<ICommonSession>>PreSelectedSessions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick.
|
||||
/// Is not serialized.
|
||||
/// </summary>
|
||||
public HashSet<ICommonSession> AssignedSessions = new();
|
||||
public AntagSelectionTime SelectionTime = AntagSelectionTime.RuleStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Locale id for the name of the antag.
|
||||
@@ -70,170 +71,32 @@ public sealed partial class AntagSelectionComponent : Component
|
||||
public bool RemoveUponFailedSpawn = true;
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
public partial struct AntagSelectionDefinition()
|
||||
/// <remarks>
|
||||
/// Regardless of this value, antags are only initialized after the game rule activates.
|
||||
/// If a game rule does not have a delayed activation, the antag will be initialized at the same time as this enum.
|
||||
/// Otherwise, it will not be initialized until the game rule becomes active.
|
||||
/// </remarks>
|
||||
public enum AntagSelectionTime : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of antagonist roles that are used for selecting which players will be antagonists.
|
||||
/// Antag roles are selected at <see cref="RulePlayerSpawningEvent"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<AntagPrototype>> PrefRoles = new();
|
||||
PrePlayerSpawn,
|
||||
|
||||
/// <summary>
|
||||
/// Fallback for <see cref="PrefRoles"/>. Useful if you need multiple role preferences for a team antagonist.
|
||||
/// Antag roles are selected at <see cref="RulePlayerJobsAssignedEvent"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<AntagPrototype>> FallbackRoles = new();
|
||||
JobsAssigned,
|
||||
|
||||
/// <summary>
|
||||
/// Should we allow people who already have an antagonist role?
|
||||
/// Antag roles are selected at <see cref="GameRuleStartedEvent"/>
|
||||
/// or <see cref="RulePlayerJobsAssignedEvent"/> if the game rule was started before spawning.
|
||||
/// This is the latest an antag can be selected.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public AntagAcceptability MultiAntagSetting = AntagAcceptability.None;
|
||||
RuleStarted,
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of this antag.
|
||||
/// Antag roles are *never* selected. Instead, this definition only makes ghost roles.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Min = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of this antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Max = 1;
|
||||
|
||||
/// <summary>
|
||||
/// A range used to randomly select <see cref="Min"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public MinMax? MinRange;
|
||||
|
||||
/// <summary>
|
||||
/// A range used to randomly select <see cref="Max"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public MinMax? MaxRange;
|
||||
|
||||
/// <summary>
|
||||
/// a player to antag ratio: used to determine the amount of antags that will be present.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int PlayerRatio = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not players should be picked to inhabit this antag or not.
|
||||
/// If no players are left and <see cref="SpawnerPrototype"/> is set, it will make a ghost role.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool PickPlayer = true;
|
||||
|
||||
// Corvax-start
|
||||
/// <summary>
|
||||
/// The entity that will spawn the antagonist.
|
||||
/// Works if you select <see cref="SpawnerPrototype"/> PrePlayerSpawn
|
||||
/// If null, the player character will spawn (if you haven't added other components)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? RoundstartEntity = null;
|
||||
// Corvax-end
|
||||
|
||||
/// <summary>
|
||||
/// If true, players that latejoin into a round have a chance of being converted into antagonists.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool LateJoinAdditional = false;
|
||||
|
||||
//todo: find out how to do this with minimal boilerplate: filler department, maybe?
|
||||
//public HashSet<ProtoId<JobPrototype>> JobBlacklist = new()
|
||||
|
||||
/// <remarks>
|
||||
/// Mostly just here for legacy compatibility and reducing boilerplate
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public bool AllowNonHumans = false;
|
||||
|
||||
/// <summary>
|
||||
/// A whitelist for selecting which players can become this antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// A blacklist for selecting which players can become this antag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
/// <summary>
|
||||
/// Components added to the player.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry Components = new();
|
||||
|
||||
/// <summary>
|
||||
/// Components added to the player's mind.
|
||||
/// Do NOT use this to add role-type components. Add those as MindRoles instead
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry MindComponents = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of Mind Role Prototypes to be added to the player's mind.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntProtoId>? MindRoles;
|
||||
|
||||
/// <summary>
|
||||
/// A set of starting gear that's equipped to the player.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<StartingGearPrototype>? StartingGear;
|
||||
|
||||
/// <summary>
|
||||
/// A list of role loadouts, from which a randomly selected one will be equipped.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<RoleLoadoutPrototype>>? RoleLoadout;
|
||||
|
||||
/// <summary>
|
||||
/// A briefing shown to the player.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public BriefingData? Briefing;
|
||||
|
||||
/// <summary>
|
||||
/// A spawner used to defer the selection of this particular definition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not the cleanest way of doing this code but it's just an odd specific behavior.
|
||||
/// Sue me.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public EntProtoId? SpawnerPrototype;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains data used to generate a briefing.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public partial struct BriefingData
|
||||
{
|
||||
/// <summary>
|
||||
/// The text shown
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? Text;
|
||||
|
||||
/// <summary>
|
||||
/// The color of the text.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color? Color;
|
||||
|
||||
/// <summary>
|
||||
/// The sound played.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? Sound;
|
||||
Never,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Content.Shared.Antag;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Ghost role spawner that creates an antag for the associated gamerule.
|
||||
/// Ghost role spawner that creates an antag for the associated game rule.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(AntagSelectionSystem))]
|
||||
public sealed partial class GhostRoleAntagSpawnerComponent : Component
|
||||
@@ -10,5 +13,5 @@ public sealed partial class GhostRoleAntagSpawnerComponent : Component
|
||||
public EntityUid? Rule;
|
||||
|
||||
[DataField]
|
||||
public AntagSelectionDefinition? Definition;
|
||||
public ProtoId<AntagSpecifierPrototype>? Definition;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Destructible.Thresholds;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Antag.Selectors;
|
||||
|
||||
/// <summary>
|
||||
/// An abstract class meant to return the amount of antags to spawn.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract partial class AntagCountSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// How many players does this antag count as?
|
||||
/// Each antag spawned by a game rule "takes" a select group of players from the pool.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int PlayerRatio = 10;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<AntagSpecifierPrototype> Proto;
|
||||
|
||||
public abstract int GetTargetAntagCount(IRobustRandom random, int playerCount);
|
||||
|
||||
public static implicit operator ProtoId<AntagSpecifierPrototype>(AntagCountSelector selector)
|
||||
{
|
||||
return selector.Proto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An abstract version of <see cref="AntagCountSelector"/> which constrains the amount of antags spawned to a minimum and maximum.
|
||||
/// </summary>
|
||||
public abstract partial class MinMaxAntagCountSelector : AntagCountSelector
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public MinMax Range;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Antag.Selectors;
|
||||
|
||||
/// <summary>
|
||||
/// Always spawns this many antags.
|
||||
/// </summary>
|
||||
public sealed partial class FixedAntagCount : AntagCountSelector
|
||||
{
|
||||
[DataField]
|
||||
public int Count = 1;
|
||||
|
||||
public override int GetTargetAntagCount(IRobustRandom random, int playerCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(Count);
|
||||
return Count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Antag.Selectors;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a constrained number of antags that scales linearly.
|
||||
/// </summary>
|
||||
public sealed partial class LinearAntagCount : MinMaxAntagCountSelector
|
||||
{
|
||||
public override int GetTargetAntagCount(IRobustRandom random, int playerCount)
|
||||
{
|
||||
return Math.Clamp(playerCount / PlayerRatio, Range.Min, Range.Max);
|
||||
}
|
||||
}
|
||||
@@ -11,38 +11,9 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BloodstreamComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<BloodstreamComponent, GenerateDnaEvent>(OnDnaGenerated);
|
||||
}
|
||||
|
||||
// not sure if we can move this to shared or not
|
||||
// it would certainly help if SolutionContainer was documented
|
||||
// but since we usually don't add the component dynamically to entities we can keep this unpredicted for now
|
||||
private void OnComponentInit(Entity<BloodstreamComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (!SolutionContainer.EnsureSolution(entity.Owner,
|
||||
entity.Comp.BloodSolutionName,
|
||||
out var bloodSolution) ||
|
||||
!SolutionContainer.EnsureSolution(entity.Owner,
|
||||
entity.Comp.BloodTemporarySolutionName,
|
||||
out var tempSolution) ||
|
||||
!SolutionContainer.EnsureSolution(entity.Owner,
|
||||
entity.Comp.MetabolitesSolutionName,
|
||||
out var metabolitesSolution))
|
||||
return;
|
||||
|
||||
bloodSolution.MaxVolume = entity.Comp.BloodReferenceSolution.Volume * entity.Comp.MaxVolumeModifier;
|
||||
metabolitesSolution.MaxVolume = bloodSolution.MaxVolume;
|
||||
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
||||
entity.Comp.BloodReferenceSolution.SetReagentData(GetEntityBloodData((entity, entity.Comp)));
|
||||
|
||||
// Fill blood solution with BLOOD
|
||||
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
|
||||
var solution = entity.Comp.BloodReferenceSolution.Clone();
|
||||
solution.ScaleTo(entity.Comp.BloodReferenceSolution.Volume - bloodSolution.Volume);
|
||||
bloodSolution.AddSolution(solution, PrototypeManager);
|
||||
}
|
||||
|
||||
// forensics is not predicted yet
|
||||
private void OnDnaGenerated(Entity<BloodstreamComponent> entity, ref GenerateDnaEvent args)
|
||||
{
|
||||
|
||||
@@ -20,21 +20,17 @@ public sealed partial class BotanySystem
|
||||
_entityEffects.TryApplyEffect(uid, mutation.Effect);
|
||||
}
|
||||
|
||||
if (!_solutionContainerSystem.EnsureSolution(uid,
|
||||
produce.SolutionName,
|
||||
out var solutionContainer,
|
||||
FixedPoint2.Zero))
|
||||
return;
|
||||
_solutionContainerSystem.EnsureSolution(uid, produce.SolutionName, out var solution);
|
||||
|
||||
solutionContainer.RemoveAllSolution();
|
||||
solution.Comp.Solution.RemoveAllSolution();
|
||||
foreach (var (chem, quantity) in seed.Chemicals)
|
||||
{
|
||||
var amount = quantity.Min;
|
||||
if (quantity.PotencyDivisor > 0 && seed.Potency > 0)
|
||||
amount += seed.Potency / quantity.PotencyDivisor;
|
||||
amount = FixedPoint2.Clamp(amount, quantity.Min, quantity.Max);
|
||||
solutionContainer.MaxVolume += amount;
|
||||
solutionContainer.AddReagent(chem, amount);
|
||||
solution.Comp.Solution.MaxVolume += amount;
|
||||
solution.Comp.Solution.AddReagent(chem, amount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,14 +130,11 @@ public sealed class PricingSystem : EntitySystem
|
||||
args.Price += entity.Comp.RandomPrice ?? 0;
|
||||
}
|
||||
|
||||
private double GetSolutionPrice(Entity<SolutionContainerManagerComponent> entity)
|
||||
private double GetSolutionPrice(EntityUid entity)
|
||||
{
|
||||
if (Comp<MetaDataComponent>(entity).EntityLifeStage < EntityLifeStage.MapInitialized)
|
||||
return GetSolutionPrice(entity.Comp);
|
||||
|
||||
var price = 0.0;
|
||||
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((entity.Owner, entity.Comp)))
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(entity))
|
||||
{
|
||||
var solution = soln.Comp.Solution;
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
@@ -153,25 +150,6 @@ public sealed class PricingSystem : EntitySystem
|
||||
return price;
|
||||
}
|
||||
|
||||
private double GetSolutionPrice(SolutionContainerManagerComponent component)
|
||||
{
|
||||
var price = 0.0;
|
||||
|
||||
foreach (var (_, prototype) in _solutionContainerSystem.EnumerateSolutions(component))
|
||||
{
|
||||
foreach (var (reagent, quantity) in prototype.Contents)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var reagentProto))
|
||||
continue;
|
||||
|
||||
// TODO check ReagentData for price information?
|
||||
price += (float) quantity * reagentProto.PricePerUnit;
|
||||
}
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
private double GetMaterialPrice(PhysicalCompositionComponent component)
|
||||
{
|
||||
double price = 0;
|
||||
@@ -319,22 +297,43 @@ public sealed class PricingSystem : EntitySystem
|
||||
{
|
||||
var price = 0.0;
|
||||
|
||||
if (TryComp<SolutionContainerManagerComponent>(uid, out var solComp))
|
||||
var meta = MetaData(uid);
|
||||
if (meta.EntityLifeStage < EntityLifeStage.MapInitialized)
|
||||
return GetSolutionsPrice(meta.EntityPrototype);
|
||||
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(uid))
|
||||
{
|
||||
price += GetSolutionPrice((uid, solComp));
|
||||
var solution = soln.Comp.Solution;
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var reagentProto))
|
||||
continue;
|
||||
|
||||
// TODO check ReagentData for price information?
|
||||
price += (float) quantity * reagentProto.PricePerUnit;
|
||||
}
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
private double GetSolutionsPrice(EntityPrototype prototype)
|
||||
private double GetSolutionsPrice(EntityPrototype? prototype)
|
||||
{
|
||||
var price = 0.0;
|
||||
|
||||
if (prototype.Components.TryGetValue(Factory.GetComponentName<SolutionContainerManagerComponent>(), out var solManager))
|
||||
if (prototype == null)
|
||||
return price;
|
||||
|
||||
foreach (var (_, solution) in _solutionContainerSystem.EnumerateSolutions(prototype))
|
||||
{
|
||||
var solComp = (SolutionContainerManagerComponent) solManager.Component;
|
||||
price += GetSolutionPrice(solComp);
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var reagentProto))
|
||||
continue;
|
||||
|
||||
// TODO check ReagentData for price information?
|
||||
price += (float) quantity * reagentProto.PricePerUnit;
|
||||
}
|
||||
}
|
||||
|
||||
return price;
|
||||
|
||||
@@ -1,5 +1,45 @@
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Changeling.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Server.Changeling.Systems;
|
||||
|
||||
public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem;
|
||||
public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, ComponentGetState>(OnGetState);
|
||||
}
|
||||
|
||||
private void OnGetState(Entity<ChangelingIdentityComponent> entity, ref ComponentGetState args)
|
||||
{
|
||||
List<ChangelingNetworkedIdentityData> sentIdentities = new();
|
||||
|
||||
foreach (var identity in entity.Comp.ConsumedIdentities)
|
||||
{
|
||||
ChangelingNetworkedIdentityData netData = new()
|
||||
{
|
||||
Identity = GetNetEntity(identity.Identity),
|
||||
Original = GetNetEntity(identity.Original),
|
||||
OriginalJob = identity.OriginalJob,
|
||||
OriginalName = identity.OriginalName,
|
||||
Starting = identity.Starting,
|
||||
GrantedDna = identity.GrantedDna,
|
||||
};
|
||||
|
||||
sentIdentities.Add(netData);
|
||||
}
|
||||
|
||||
var current = entity.Comp.CurrentIdentity;
|
||||
|
||||
var netCurrent = GetNetEntity(current);
|
||||
|
||||
args.State = new ChangelingIdentityComponentState(
|
||||
sentIdentities,
|
||||
netCurrent,
|
||||
entity.Comp.IdentityCloningSettings,
|
||||
entity.Comp.MaxStoredDisguises);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Server.Chemistry.Containers.EntitySystems;
|
||||
|
||||
[Obsolete("This is being depreciated. Use SharedSolutionContainerSystem instead!")]
|
||||
public sealed partial class SolutionContainerSystem : SharedSolutionContainerSystem
|
||||
{
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name)
|
||||
=> EnsureSolution(entity, name, out _);
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, out bool existed)
|
||||
=> EnsureSolution(entity, name, FixedPoint2.Zero, out existed);
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, out bool existed)
|
||||
=> EnsureSolution(entity, name, maxVol, null, out existed);
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
|
||||
{
|
||||
EnsureSolution(entity, name, maxVol, prototype, out existed, out var solution);
|
||||
return solution!;//solution is only ever null on the client, so we can suppress this
|
||||
}
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Entity<SolutionComponent> EnsureSolutionEntity(
|
||||
Entity<SolutionContainerManagerComponent?> entity,
|
||||
string name,
|
||||
FixedPoint2 maxVol,
|
||||
Solution? prototype,
|
||||
out bool existed)
|
||||
{
|
||||
EnsureSolutionEntity(entity, name, out existed, out var solEnt, maxVol, prototype);
|
||||
return solEnt!.Value;//solEnt is only ever null on the client, so we can suppress this
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ namespace Content.Server.Chemistry.EntitySystems
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ChemMasterComponent, ComponentStartup>(SubscribeUpdateUiState);
|
||||
SubscribeLocalEvent<ChemMasterComponent, SolutionContainerChangedEvent>(SubscribeUpdateUiState);
|
||||
SubscribeLocalEvent<ChemMasterComponent, SolutionChangedEvent>(SubscribeUpdateUiState);
|
||||
SubscribeLocalEvent<ChemMasterComponent, EntInsertedIntoContainerMessage>(SubscribeUpdateUiState);
|
||||
SubscribeLocalEvent<ChemMasterComponent, EntRemovedFromContainerMessage>(SubscribeUpdateUiState);
|
||||
// Subscribing to DragDropTargetEvent is a quick fix to ensure the UI updates when fluids are dragged and dropped into the ChemMaster, since Shared.Fluids.EntitySystems.SolutionDumpingSystem.cs bypasses UpdateChemicals().
|
||||
@@ -235,22 +235,17 @@ namespace Content.Server.Chemistry.EntitySystems
|
||||
_storageSystem.Insert(container, item, out _, user: user, storage);
|
||||
_labelSystem.Label(item, message.Label);
|
||||
|
||||
_solutionContainerSystem.EnsureSolutionEntity(item,
|
||||
SharedChemMaster.PillSolutionName,
|
||||
out var itemSolution,
|
||||
message.Dosage);
|
||||
if (!itemSolution.HasValue)
|
||||
return;
|
||||
_solutionContainerSystem.EnsureSolution(item, SharedChemMaster.PillSolutionName, out var itemSolution);
|
||||
itemSolution.Comp.Solution.MaxVolume = message.Dosage;
|
||||
|
||||
_solutionContainerSystem.TryAddSolution(itemSolution.Value, withdrawal.SplitSolution(message.Dosage));
|
||||
_solutionContainerSystem.TryAddSolution(itemSolution, withdrawal.SplitSolution(message.Dosage));
|
||||
|
||||
var pill = EnsureComp<PillComponent>(item);
|
||||
pill.PillType = chemMaster.Comp.PillType;
|
||||
Dirty(item, pill);
|
||||
|
||||
// Log pill creation by a user
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low,
|
||||
$"{ToPrettyString(user):user} printed {ToPrettyString(item):pill} {SharedSolutionContainerSystem.ToPrettyString(itemSolution.Value.Comp.Solution)}");
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} printed {ToPrettyString(item):pill} {SharedSolutionContainerSystem.ToPrettyString(itemSolution.Comp.Solution)}");
|
||||
}
|
||||
|
||||
UpdateUiState(chemMaster);
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Content.Server.Chemistry.EntitySystems.DeleteOnSolutionEmptySystem
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DeleteOnSolutionEmptyComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<DeleteOnSolutionEmptyComponent, SolutionContainerChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<DeleteOnSolutionEmptyComponent, SolutionChangedEvent>(OnSolutionChange);
|
||||
}
|
||||
|
||||
public void OnStartup(Entity<DeleteOnSolutionEmptyComponent> entity, ref ComponentStartup args)
|
||||
@@ -20,19 +20,23 @@ namespace Content.Server.Chemistry.EntitySystems.DeleteOnSolutionEmptySystem
|
||||
CheckSolutions(entity);
|
||||
}
|
||||
|
||||
public void OnSolutionChange(Entity<DeleteOnSolutionEmptyComponent> entity, ref SolutionContainerChangedEvent args)
|
||||
public void OnSolutionChange(Entity<DeleteOnSolutionEmptyComponent> entity, ref SolutionChangedEvent args)
|
||||
{
|
||||
CheckSolutions(entity);
|
||||
var solution = args.Solution.Comp.Solution;
|
||||
if (args.Solution.Comp.Id != entity.Comp.Solution)
|
||||
return;
|
||||
|
||||
if (solution.Volume <= 0)
|
||||
QueueDel(entity);
|
||||
}
|
||||
|
||||
public void CheckSolutions(Entity<DeleteOnSolutionEmptyComponent> entity)
|
||||
{
|
||||
if (!TryComp(entity, out SolutionContainerManagerComponent? solutions))
|
||||
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
|
||||
return;
|
||||
|
||||
if (_solutionContainerSystem.TryGetSolution((entity.Owner, solutions), entity.Comp.Solution, out _, out var solution))
|
||||
if (solution.Volume <= 0)
|
||||
QueueDel(entity);
|
||||
if (solution.Volume <= 0)
|
||||
QueueDel(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
@@ -40,7 +39,7 @@ namespace Content.Server.Chemistry.EntitySystems
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ReagentDispenserComponent, ComponentStartup>(SubscribeUpdateUiState);
|
||||
SubscribeLocalEvent<ReagentDispenserComponent, SolutionContainerChangedEvent>(SubscribeUpdateUiState);
|
||||
SubscribeLocalEvent<ReagentDispenserComponent, SolutionChangedEvent>(SubscribeUpdateUiState);
|
||||
SubscribeLocalEvent<ReagentDispenserComponent, EntInsertedIntoContainerMessage>(SubscribeUpdateUiState, after: [typeof(SharedStorageSystem)]);
|
||||
SubscribeLocalEvent<ReagentDispenserComponent, EntRemovedFromContainerMessage>(SubscribeUpdateUiState, after: [typeof(SharedStorageSystem)]);
|
||||
SubscribeLocalEvent<ReagentDispenserComponent, BoundUIOpenedEvent>(SubscribeUpdateUiState);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
public sealed class SolutionContainerSystem : SharedSolutionContainerSystem;
|
||||
@@ -81,13 +81,10 @@ public sealed class SolutionHeaterSystem : EntitySystem
|
||||
var query = EntityQueryEnumerator<ActiveSolutionHeaterComponent, SolutionHeaterComponent, ItemPlacerComponent>();
|
||||
while (query.MoveNext(out _, out _, out var heater, out var placer))
|
||||
{
|
||||
var energy = heater.HeatPerSecond * frameTime;
|
||||
foreach (var heatingEntity in placer.PlacedEntities)
|
||||
{
|
||||
if (!TryComp<SolutionContainerManagerComponent>(heatingEntity, out var container))
|
||||
continue;
|
||||
|
||||
var energy = heater.HeatPerSecond * frameTime;
|
||||
foreach (var (_, soln) in _solutionContainer.EnumerateSolutions((heatingEntity, container)))
|
||||
foreach (var (_, soln) in _solutionContainer.EnumerateSolutions(heatingEntity))
|
||||
{
|
||||
_solutionContainer.AddThermalEnergy(soln, energy);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -37,8 +38,10 @@ public sealed class SolutionRandomFillSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
_solutionsSystem.EnsureSolutionEntity(entity.Owner, entity.Comp.Solution, out var target , pick.quantity);
|
||||
if(target.HasValue)
|
||||
_solutionsSystem.TryAddReagent(target.Value, reagent, quantity);
|
||||
_solutionsSystem.EnsureSolution(entity.Owner, entity.Comp.Solution, out var target);
|
||||
if (target.Comp.Solution.AvailableVolume < quantity)
|
||||
Log.Error($"A random solution fill {entity.Comp.WeightedRandomId} tried to put {pick.quantity} of {pick.reagent} into {ToPrettyString(target)} but there was not enough space!");
|
||||
|
||||
_solutionsSystem.TryAddReagent(target, reagent, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class TransformableContainerSystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<TransformableContainerComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<TransformableContainerComponent, SolutionContainerChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<TransformableContainerComponent, SolutionChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<TransformableContainerComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class TransformableContainerSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSolutionChange(Entity<TransformableContainerComponent> entity, ref SolutionContainerChangedEvent args)
|
||||
private void OnSolutionChange(Entity<TransformableContainerComponent> entity, ref SolutionChangedEvent args)
|
||||
{
|
||||
if (!_solutionsSystem.TryGetFitsInDispenser(entity.Owner, out _, out var solution))
|
||||
return;
|
||||
|
||||
@@ -39,9 +39,7 @@ namespace Content.Server.Chemistry.EntitySystems
|
||||
|
||||
private void HandleCollide(Entity<VaporComponent> entity, ref StartCollideEvent args)
|
||||
{
|
||||
if (!TryComp(entity.Owner, out SolutionContainerManagerComponent? contents)) return;
|
||||
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((entity.Owner, contents)))
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(entity.Owner))
|
||||
{
|
||||
var solution = soln.Comp.Solution;
|
||||
_reactive.DoEntityReaction(args.OtherEntity, solution, ReactionMethod.Touch);
|
||||
@@ -102,7 +100,8 @@ namespace Content.Server.Chemistry.EntitySystems
|
||||
base.Update(frameTime);
|
||||
|
||||
// Enumerate over all VaporComponents
|
||||
var query = EntityQueryEnumerator<VaporComponent, SolutionContainerManagerComponent, TransformComponent>();
|
||||
// TODO: Vapor should just use SolutionComponent and not be capable of having multiple solutions.
|
||||
var query = EntityQueryEnumerator<VaporComponent, SolutionManagerComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var vaporComp, out var container, out var xform))
|
||||
{
|
||||
// Return early if we're not active
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Construction;
|
||||
|
||||
public sealed class AnchorOnlyOnStationSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly StationSystem _stationSystem = null!;
|
||||
[Dependency] private readonly TransformSystem _transform = null!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
|
||||
}
|
||||
|
||||
private void OnGridSplit(ref GridSplitEvent args)
|
||||
{
|
||||
var allGrids = args.NewGrids.ToList();
|
||||
|
||||
var query = AllEntityQuery<AnchorOnlyOnStationComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var anchorOnlyOnStationComp, out var entXform))
|
||||
{
|
||||
if (entXform.GridUid == null)
|
||||
continue;
|
||||
|
||||
if (!allGrids.Contains(entXform.GridUid.Value))
|
||||
continue;
|
||||
|
||||
if (_stationSystem.IsOnStation(ent, anchorOnlyOnStationComp.OnlyCountLargestGrid))
|
||||
continue;
|
||||
|
||||
_transform.Unanchor(ent, entXform);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Shared.Player;
|
||||
@@ -88,6 +89,9 @@ public sealed class DamageForceSaySystem : EntitySystem
|
||||
if (!args.FellAsleep)
|
||||
return;
|
||||
|
||||
if (Comp<MobStateComponent>(uid).CurrentState != MobState.Alive)
|
||||
return;
|
||||
|
||||
TryForceSay(uid, component);
|
||||
AllowNextSpeech(uid);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ public sealed partial class SpillBehavior : IThresholdBehavior
|
||||
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
|
||||
|
||||
// Spill the solution that was drained/split
|
||||
if (solutionContainer.TryGetSolution(owner, Solution, out _, out var solution))
|
||||
// TODO: ??? Top 10 reasons for solution entity prototypes right here bruh.
|
||||
if (Solution != null && solutionContainer.TryGetSolution(owner, Solution, out _, out var solution))
|
||||
puddleSystem.TrySplashSpillAt(owner, coordinates, solution, out _, false, cause);
|
||||
else
|
||||
puddleSystem.TrySplashSpillAt(owner, coordinates, out _, out _, false, cause);
|
||||
|
||||
@@ -219,7 +219,7 @@ public sealed partial class ExplosionSystem
|
||||
totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible);
|
||||
}
|
||||
|
||||
if (totalDamageTarget == FixedPoint2.MaxValue || !_damageableQuery.TryGetComponent(uid, out var damageable))
|
||||
if (totalDamageTarget == FixedPoint2.MaxValue || !_injurableQuery.TryGetComponent(uid, out var injurable))
|
||||
{
|
||||
for (var i = 0; i < explosionTolerance.Length; i++)
|
||||
{
|
||||
@@ -245,7 +245,7 @@ public sealed partial class ExplosionSystem
|
||||
var damagePerIntensity = FixedPoint2.Zero;
|
||||
foreach (var (type, value) in explosionType.DamagePerIntensity.DamageDict)
|
||||
{
|
||||
if (!_damageableSystem.CanBeDamagedBy((uid, damageable), type))
|
||||
if (!_damageableSystem.CanBeDamagedBy((uid, injurable), type))
|
||||
continue;
|
||||
|
||||
// TODO EXPLOSION SYSTEM
|
||||
@@ -260,7 +260,7 @@ public sealed partial class ExplosionSystem
|
||||
}
|
||||
|
||||
var toleranceValue = damagePerIntensity > 0
|
||||
? (float) ((totalDamageTarget - _damageableSystem.GetTotalDamage((uid, damageable))) / damagePerIntensity)
|
||||
? (float) ((totalDamageTarget - _damageableSystem.GetTotalDamage(uid)) / damagePerIntensity)
|
||||
: ToleranceValues.Invulnerable;
|
||||
|
||||
explosionTolerance[index] = toleranceValue;
|
||||
|
||||
@@ -65,6 +65,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
|
||||
private EntityQuery<ActorComponent> _actorQuery;
|
||||
private EntityQuery<DestructibleComponent> _destructibleQuery;
|
||||
private EntityQuery<DamageableComponent> _damageableQuery;
|
||||
[Dependency] private readonly EntityQuery<InjurableComponent> _injurableQuery = default!;
|
||||
private EntityQuery<AirtightComponent> _airtightQuery;
|
||||
private EntityQuery<TileHistoryComponent> _tileHistoryQuery;
|
||||
|
||||
|
||||
@@ -294,20 +294,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
Solution addedSolution,
|
||||
bool sound = true,
|
||||
bool checkForOverflow = true,
|
||||
PuddleComponent? puddleComponent = null,
|
||||
SolutionContainerManagerComponent? sol = null)
|
||||
PuddleComponent? puddleComponent = null)
|
||||
{
|
||||
if (!Resolve(puddleUid, ref puddleComponent, ref sol))
|
||||
if (!Resolve(puddleUid, ref puddleComponent))
|
||||
return false;
|
||||
|
||||
_solutionContainerSystem.EnsureAllSolutions((puddleUid, sol));
|
||||
|
||||
if (addedSolution.Volume == 0 ||
|
||||
!_solutionContainerSystem.ResolveSolution(puddleUid, puddleComponent.SolutionName,
|
||||
ref puddleComponent.Solution))
|
||||
{
|
||||
if (addedSolution.Volume == 0 || !_solutionContainerSystem.ResolveSolution(puddleUid, puddleComponent.SolutionName, ref puddleComponent.Solution))
|
||||
return false;
|
||||
}
|
||||
|
||||
_solutionContainerSystem.AddSolution(puddleComponent.Solution.Value, addedSolution);
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ public sealed class SmokeSystem : EntitySystem
|
||||
|
||||
private void OnReactionAttempt(Entity<SmokeComponent> entity, ref SolutionRelayEvent<ReactionAttemptEvent> args)
|
||||
{
|
||||
if (args.Name == SmokeComponent.SolutionName)
|
||||
if (args.Solution.Comp.Id == SmokeComponent.SolutionName)
|
||||
OnReactionAttempt(entity, ref args.Event);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,7 @@ using Content.Shared.Tag;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Robust.Shared.Prototypes;
|
||||
// todo: remove this stinky LINQy
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ namespace Content.Server.Forensics
|
||||
SubscribeLocalEvent<CleansForensicsComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(AbsorbentSystem) });
|
||||
SubscribeLocalEvent<ForensicsComponent, CleanForensicsDoAfterEvent>(OnCleanForensicsDoAfter);
|
||||
SubscribeLocalEvent<DnaComponent, TransferDnaEvent>(OnTransferDnaEvent);
|
||||
SubscribeLocalEvent<DnaSubstanceTraceComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
|
||||
SubscribeLocalEvent<DnaSubstanceTraceComponent, SolutionChangedEvent>(OnSolutionChanged);
|
||||
SubscribeLocalEvent<CleansForensicsComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
|
||||
}
|
||||
|
||||
private void OnSolutionChanged(Entity<DnaSubstanceTraceComponent> ent, ref SolutionContainerChangedEvent ev)
|
||||
private void OnSolutionChanged(Entity<DnaSubstanceTraceComponent> ent, ref SolutionChangedEvent ev)
|
||||
{
|
||||
var soln = GetSolutionsDNA(ev.Solution);
|
||||
if (soln.Count > 0)
|
||||
@@ -152,12 +152,9 @@ namespace Content.Server.Forensics
|
||||
public List<string> GetSolutionsDNA(EntityUid uid)
|
||||
{
|
||||
List<string> list = new();
|
||||
if (TryComp<SolutionContainerManagerComponent>(uid, out var comp))
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(uid))
|
||||
{
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((uid, comp)))
|
||||
{
|
||||
list.AddRange(GetSolutionsDNA(soln.Comp.Solution));
|
||||
}
|
||||
list.AddRange(GetSolutionsDNA(soln.Comp.Solution));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ namespace Content.Server.GameTicking
|
||||
#if EXCEPTION_TOLERANCE
|
||||
Subs.CVar(_cfg, CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true);
|
||||
#endif
|
||||
Subs.CVar(_cfg, CCVars.GameTickerIgnoredPresets, value => _ignoredRules = value.Split(","));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ using System.Threading.Tasks;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Maps;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking;
|
||||
|
||||
@@ -193,7 +195,6 @@ public sealed partial class GameTicker
|
||||
_gameMapManager.SelectMapRandom();
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
private bool AddGamePresetRules()
|
||||
{
|
||||
if (DummyTicker || Preset == null)
|
||||
@@ -202,7 +203,7 @@ public sealed partial class GameTicker
|
||||
CurrentPreset = Preset;
|
||||
foreach (var rule in Preset.Rules)
|
||||
{
|
||||
AddGameRule(rule);
|
||||
AddFilteredGameRule(rule);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -227,6 +228,40 @@ public sealed partial class GameTicker
|
||||
}
|
||||
}
|
||||
|
||||
/// <inhereitdoc cref="GetMinimumPlayerCount(GamePresetPrototype)"/>
|
||||
[PublicAPI]
|
||||
public int GetMinimumPlayerCount(ProtoId<GamePresetPrototype> proto)
|
||||
{
|
||||
if (!_prototypeManager.Resolve(proto, out var preset))
|
||||
return 0;
|
||||
|
||||
return GetMinimumPlayerCount(preset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum number of players required for a game preset to start.
|
||||
/// Checks both the preset itself, and all rules to find the minimum.
|
||||
/// </summary>
|
||||
/// <param name="proto">Game preset prototype we're checking.</param>
|
||||
/// <returns>Minimum number of players required for the rule to start.</returns>
|
||||
[PublicAPI]
|
||||
public int GetMinimumPlayerCount(GamePresetPrototype proto)
|
||||
{
|
||||
var min = proto.MinPlayers ?? 0;
|
||||
foreach (var entProto in proto.Rules)
|
||||
{
|
||||
if (!_prototypeManager.Resolve(entProto, out var ent))
|
||||
continue;
|
||||
|
||||
if (!ent.TryGetComponent<GameRuleComponent>(out var rule, Factory))
|
||||
continue;
|
||||
|
||||
min = Math.Max(min, rule.MinPlayers);
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
private void IncrementRoundNumber()
|
||||
{
|
||||
var playerIds = _playerGameStatuses.Keys.Select(player => player.UserId).ToArray();
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Prototypes;
|
||||
@@ -10,14 +11,24 @@ using JetBrains.Annotations;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Server.GameTicking;
|
||||
|
||||
public sealed partial class GameTicker
|
||||
{
|
||||
/// <summary>
|
||||
/// Designated game rule that spawns a fake antagonist to discourage metagaming.
|
||||
/// </summary>
|
||||
public static readonly EntProtoId DummyGameRule = "DummyNonAntag";
|
||||
|
||||
[ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of ignored game rules, these rules won't be spawned by normal means.
|
||||
/// This list is populated by <see cref="CCVars.GameTickerIgnoredPresets"/>
|
||||
/// </summary>
|
||||
[ViewVariables] private string[] _ignoredRules = [];
|
||||
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = null!;
|
||||
|
||||
/// <summary>
|
||||
@@ -55,6 +66,8 @@ public sealed partial class GameTicker
|
||||
string.Empty,
|
||||
$"listgamerules - {localizedHelp}",
|
||||
ListGameRuleCommand);
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
}
|
||||
|
||||
private void ShutdownGameRules()
|
||||
@@ -98,6 +111,29 @@ public sealed partial class GameTicker
|
||||
return ruleEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a gamerule to the current round, but ignores any <see cref="_ignoredRules"/>
|
||||
/// </summary>
|
||||
/// <param name="gameRule">Game rule entity that we are trying to spawn</param>
|
||||
/// <returns>The entityUid of the spawned game rule, if it wasn't ignored.</returns>
|
||||
public EntityUid? AddFilteredGameRule(EntProtoId gameRule)
|
||||
{
|
||||
if (IsIgnored(gameRule))
|
||||
return null;
|
||||
|
||||
return AddGameRule(gameRule);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this GameRule should be ignored before a spawning attempt.
|
||||
/// </summary>
|
||||
/// <param name="gameRule">GameRule we are trying to validate</param>
|
||||
/// <returns>True if the gamerule should be ignored and not spawned.</returns>
|
||||
public bool IsIgnored(EntProtoId gameRule)
|
||||
{
|
||||
return _ignoredRules.Contains(gameRule);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Game rules can be 'started' separately from being added. 'Starting' them usually
|
||||
/// happens at round start while they can be added and removed before then.
|
||||
@@ -386,6 +422,37 @@ public sealed partial class GameTicker
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartAttempt(RoundStartAttemptEvent args)
|
||||
{
|
||||
if (args.Forced || args.Cancelled)
|
||||
return;
|
||||
|
||||
var query = EntityQueryEnumerator<GameRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var gameRule))
|
||||
{
|
||||
var minPlayers = gameRule.MinPlayers;
|
||||
var name = ToPrettyString(uid);
|
||||
|
||||
if (args.Players.Length >= minPlayers)
|
||||
continue;
|
||||
|
||||
if (gameRule.CancelPresetOnTooFewPlayers)
|
||||
{
|
||||
_chatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
|
||||
("readyPlayersCount", args.Players.Length),
|
||||
("minimumPlayers", minPlayers),
|
||||
("presetName", name)));
|
||||
args.Cancel();
|
||||
//TODO remove this once announcements are logged
|
||||
Log.Info($"Rule '{name}' requires {minPlayers} players, but only {args.Players.Length} are ready.");
|
||||
}
|
||||
else
|
||||
{
|
||||
EndGameRule(uid, gameRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Command Implementations
|
||||
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace Content.Server.GameTicking.Rules;
|
||||
public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfileRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
|
||||
|
||||
@@ -34,18 +33,18 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfile
|
||||
: HumanoidCharacterProfile.RandomWithSpecies();
|
||||
|
||||
|
||||
if (profile?.Species is not { } speciesId || !_proto.Resolve(speciesId, out var species))
|
||||
if (profile?.Species is not { } speciesId || !Proto.Resolve(speciesId, out var species))
|
||||
{
|
||||
species = _proto.Index<SpeciesPrototype>(HumanoidCharacterProfile.DefaultSpecies);
|
||||
species = Proto.Index(HumanoidCharacterProfile.DefaultSpecies);
|
||||
}
|
||||
|
||||
if (ent.Comp.SpeciesOverride != null
|
||||
&& (ent.Comp.SpeciesOverrideBlacklist?.Contains(new ProtoId<SpeciesPrototype>(species.ID)) ?? false))
|
||||
{
|
||||
species = _proto.Index(ent.Comp.SpeciesOverride.Value);
|
||||
species = Proto.Index(ent.Comp.SpeciesOverride.Value);
|
||||
}
|
||||
|
||||
args.Entity = Spawn(species.Prototype);
|
||||
args.Entity = Spawn(species.Prototype, args.Coords);
|
||||
if (profile?.WithSpecies(species.ID) is { } humanoidProfile)
|
||||
{
|
||||
_visualBody.ApplyProfileTo(args.Entity.Value, humanoidProfile);
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -9,86 +10,57 @@ namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public abstract partial class GameRuleSystem<T> : EntitySystem where T : IComponent
|
||||
{
|
||||
[Dependency] protected readonly IRobustRandom RobustRandom = default!;
|
||||
[Dependency] protected readonly IChatManager ChatManager = default!;
|
||||
[Dependency] protected readonly GameTicker GameTicker = default!;
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] protected readonly IPrototypeManager Proto = default!;
|
||||
[Dependency] protected readonly IRobustRandom RobustRandom = default!;
|
||||
[Dependency] protected readonly GameTicker GameTicker = default!;
|
||||
|
||||
// Not protected, just to be used in utility methods
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly MapSystem _map = default!;
|
||||
|
||||
[Dependency] protected readonly EntityQuery<GameRuleComponent> GameRuleQuery = default!;
|
||||
[Dependency] protected readonly EntityQuery<T> RuleQuery = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
|
||||
SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
|
||||
SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
|
||||
}
|
||||
|
||||
private void OnStartAttempt(RoundStartAttemptEvent args)
|
||||
{
|
||||
if (args.Forced || args.Cancelled)
|
||||
return;
|
||||
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out _, out var gameRule))
|
||||
{
|
||||
var minPlayers = gameRule.MinPlayers;
|
||||
var name = ToPrettyString(uid);
|
||||
|
||||
if (args.Players.Length >= minPlayers)
|
||||
continue;
|
||||
|
||||
if (gameRule.CancelPresetOnTooFewPlayers)
|
||||
{
|
||||
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
|
||||
("readyPlayersCount", args.Players.Length),
|
||||
("minimumPlayers", minPlayers),
|
||||
("presetName", name)));
|
||||
args.Cancel();
|
||||
//TODO remove this once announcements are logged
|
||||
Log.Info($"Rule '{name}' requires {minPlayers} players, but only {args.Players.Length} are ready.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ForceEndSelf(uid, gameRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args)
|
||||
{
|
||||
if (!TryComp<GameRuleComponent>(uid, out var ruleData))
|
||||
if (!GameRuleQuery.TryComp(uid, out var ruleData))
|
||||
return;
|
||||
|
||||
Added(uid, component, ruleData, args);
|
||||
}
|
||||
|
||||
private void OnGameRuleStarted(EntityUid uid, T component, ref GameRuleStartedEvent args)
|
||||
{
|
||||
if (!TryComp<GameRuleComponent>(uid, out var ruleData))
|
||||
if (!GameRuleQuery.TryComp(uid, out var ruleData))
|
||||
return;
|
||||
|
||||
Started(uid, component, ruleData, args);
|
||||
}
|
||||
|
||||
private void OnGameRuleEnded(EntityUid uid, T component, ref GameRuleEndedEvent args)
|
||||
{
|
||||
if (!TryComp<GameRuleComponent>(uid, out var ruleData))
|
||||
if (!GameRuleQuery.TryComp(uid, out var ruleData))
|
||||
return;
|
||||
|
||||
Ended(uid, component, ruleData, args);
|
||||
}
|
||||
|
||||
private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
|
||||
{
|
||||
var query = AllEntityQuery<T>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out var ruleData))
|
||||
{
|
||||
if (!TryComp<GameRuleComponent>(uid, out var ruleData))
|
||||
continue;
|
||||
|
||||
AppendRoundEndText(uid, comp, ruleData, ref ev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.EUI;
|
||||
@@ -104,7 +105,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
|
||||
args.AddLine(Loc.GetString(Outcomes[index]));
|
||||
|
||||
var sessionData = _antag.GetAntagIdentifiers(uid);
|
||||
var sessionData = _antag.GetAntagIdentifiers(uid).ToList();
|
||||
args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
|
||||
foreach (var (mind, data, name) in sessionData)
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class RoundstartStationVariationRuleSystem : GameRuleSystem<Rounds
|
||||
var spawns = EntitySpawnCollection.GetSpawns(component.Rules, _random);
|
||||
foreach (var rule in spawns)
|
||||
{
|
||||
GameTicker.AddGameRule(rule);
|
||||
GameTicker.AddFilteredGameRule(rule);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,9 +65,9 @@ public sealed class RuleGridsSystem : GameRuleSystem<RuleGridsComponent>
|
||||
if (_whitelist.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid))
|
||||
continue;
|
||||
|
||||
if (TryComp<GridSpawnPointWhitelistComponent>(uid, out var gridSpawnPointWhitelistComponent))
|
||||
if (TryComp<AntagGridSpawnPointComponent>(uid, out var comp))
|
||||
{
|
||||
if (!_whitelist.CheckBoth(args.Entity, gridSpawnPointWhitelistComponent.Blacklist, gridSpawnPointWhitelistComponent.Whitelist))
|
||||
if (args.Antag == null || !comp.Whitelist.Contains(args.Antag))
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
|
||||
|
||||
foreach (var rule in preset.Rules)
|
||||
{
|
||||
if (GameTicker.IsIgnored(rule))
|
||||
continue;
|
||||
|
||||
EntityUid ruleEnt;
|
||||
|
||||
// if we're pre-round (i.e. will only be added)
|
||||
@@ -153,19 +156,6 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
|
||||
if (selected == null)
|
||||
return false;
|
||||
|
||||
foreach (var ruleId in selected.Rules)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(ruleId, out EntityPrototype? rule)
|
||||
|| !rule.TryGetComponent(_ruleCompName, out GameRuleComponent? ruleComp))
|
||||
{
|
||||
Log.Error($"Encountered invalid rule {ruleId} in preset {selected.ID}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ruleComp.MinPlayers > players && ruleComp.CancelPresetOnTooFewPlayers)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return players >= GameTicker.GetMinimumPlayerCount(selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ public sealed class SubGamemodesSystem : GameRuleSystem<SubGamemodesComponent>
|
||||
var picked = EntitySpawnCollection.GetSpawns(comp.Rules, RobustRandom);
|
||||
foreach (var id in picked)
|
||||
{
|
||||
if (GameTicker.IsIgnored(id))
|
||||
continue;
|
||||
|
||||
Log.Info($"Starting gamerule {id} as a subgamemode of {ToPrettyString(uid):rule}");
|
||||
GameTicker.AddGameRule(id);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using Content.Shared.Zombies;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
@@ -76,7 +77,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
else
|
||||
args.AddLine(Loc.GetString("zombie-round-end-amount-all"));
|
||||
|
||||
var antags = _antag.GetAntagIdentifiers(uid);
|
||||
var antags = _antag.GetAntagIdentifiers(uid).ToList();
|
||||
args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count)));
|
||||
foreach (var (_, data, entName) in antags)
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace Content.Server.Kitchen.EntitySystems
|
||||
|
||||
SubscribeLocalEvent<MicrowaveComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<MicrowaveComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<MicrowaveComponent, SolutionContainerChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<MicrowaveComponent, SolutionChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<MicrowaveComponent, EntInsertedIntoContainerMessage>(OnContentUpdate);
|
||||
SubscribeLocalEvent<MicrowaveComponent, EntRemovedFromContainerMessage>(OnContentUpdate);
|
||||
SubscribeLocalEvent<MicrowaveComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(AnchorableSystem) });
|
||||
@@ -181,9 +181,7 @@ namespace Content.Server.Kitchen.EntitySystems
|
||||
if (TryComp<TemperatureComponent>(entity, out var tempComp))
|
||||
_temperature.ChangeHeat(entity, heatToAdd * component.ObjectHeatMultiplier, false, tempComp);
|
||||
|
||||
if (!TryComp<SolutionContainerManagerComponent>(entity, out var solutions))
|
||||
continue;
|
||||
foreach (var (_, soln) in _solutionContainer.EnumerateSolutions((entity, solutions)))
|
||||
foreach (var (_, soln) in _solutionContainer.EnumerateSolutions(entity))
|
||||
{
|
||||
var solution = soln.Comp.Solution;
|
||||
if (solution.Temperature > component.TemperatureUpperThreshold)
|
||||
@@ -319,7 +317,7 @@ namespace Content.Server.Kitchen.EntitySystems
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSolutionChange(Entity<MicrowaveComponent> ent, ref SolutionContainerChangedEvent args)
|
||||
private void OnSolutionChange(Entity<MicrowaveComponent> ent, ref SolutionChangedEvent args)
|
||||
{
|
||||
UpdateUserInterfaceState(ent, ent.Comp);
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ public sealed class MappingSystem : EntitySystem
|
||||
}
|
||||
|
||||
_currentlyAutosaving[uid] = (CalculateNextTime(), name);
|
||||
var saveDir = Path.Combine(_cfg.GetCVar(CCVars.AutosaveDirectory), name).Replace(Path.DirectorySeparatorChar, '/');
|
||||
_resMan.UserData.CreateDir(new ResPath(saveDir).ToRootedPath());
|
||||
var saveDir = new ResPath(Path.Combine(_cfg.GetCVar(CCVars.AutosaveDirectory), name).Replace(Path.DirectorySeparatorChar, '/'));
|
||||
_resMan.UserData.CreateDir(saveDir.ToRootedPath());
|
||||
|
||||
var path = new ResPath(Path.Combine(saveDir, $"{DateTime.Now:yyyy-M-dd_HH.mm.ss}-AUTO.yml"));
|
||||
var path = saveDir / new ResPath($"{DateTime.Now:yyyy-M-dd_HH.mm.ss}-AUTO.yml");
|
||||
Log.Info($"Autosaving map {name} ({uid}) to {path}. Next save in {ReadableTimeLeft(uid)} seconds.");
|
||||
|
||||
if (HasComp<MapComponent>(uid))
|
||||
|
||||
@@ -69,20 +69,19 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
|
||||
|
||||
private void OnInteractUsing(Entity<MaterialReclaimerComponent> entity, ref InteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
if (args.Handled || entity.Comp.SolutionContainerId == null)
|
||||
return;
|
||||
|
||||
// if we're trying to get a solution out of the reclaimer, don't destroy it
|
||||
if (_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.SolutionContainerId, out _, out var outputSolution) && outputSolution.Contents.Any())
|
||||
{
|
||||
if (TryComp<SolutionContainerManagerComponent>(args.Used, out var managerComponent) &&
|
||||
_solutionContainer.EnumerateSolutions((args.Used, managerComponent)).Any(s => s.Solution.Comp.Solution.AvailableVolume > 0))
|
||||
if (_solutionContainer.EnumerateSolutions(args.Used).Any(s => s.Solution.Comp.Solution.AvailableVolume > 0))
|
||||
{
|
||||
if (_openable.IsClosed(args.Used))
|
||||
return;
|
||||
|
||||
if (TryComp<SolutionTransferComponent>(args.Used, out var transfer) &&
|
||||
transfer.CanReceive)
|
||||
transfer.CanSend)
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -250,7 +249,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
|
||||
TransformComponent? xform = null,
|
||||
PhysicalCompositionComponent? composition = null)
|
||||
{
|
||||
if (!Resolve(reclaimer, ref reclaimerComponent, ref xform))
|
||||
if (!Resolve(reclaimer, ref reclaimerComponent, ref xform) || reclaimerComponent.SolutionContainerId == null)
|
||||
return;
|
||||
|
||||
efficiency *= reclaimerComponent.Efficiency;
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<SliceableFoodComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<SliceableFoodComponent, SliceFoodDoAfterEvent>(OnSlicedoAfter);
|
||||
SubscribeLocalEvent<SliceableFoodComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<SliceableFoodComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(Entity<SliceableFoodComponent> entity, ref InteractUsingEvent args)
|
||||
@@ -156,10 +156,9 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
_solutionContainer.TryAddSolution(itsSoln.Value, lostSolutionPart);
|
||||
}
|
||||
|
||||
private void OnComponentStartup(Entity<SliceableFoodComponent> entity, ref ComponentStartup args)
|
||||
private void OnMapInit(Entity<SliceableFoodComponent> entity, ref MapInitEvent args)
|
||||
{
|
||||
// TODO: When Food Component is fully kill delete this awful method
|
||||
// This exists just to make tests fail I guess, awesome!
|
||||
// This exists just to make tests fail!
|
||||
// If you're here because your test just failed, make sure that:
|
||||
// Your food has the edible component
|
||||
// The solution listed in the edible component exists
|
||||
|
||||
@@ -81,11 +81,10 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
|
||||
EntityUid contents = entity.Comp.BowlSlot.Item.Value;
|
||||
|
||||
if (!TryComp<SolutionContainerManagerComponent>(contents, out var reagents) ||
|
||||
!_solutionContainerSystem.TryGetSolution(smokable.Owner, smokable.Comp.Solution, out var pipeSolution, out _))
|
||||
if (!_solutionContainerSystem.TryGetSolution(smokable.Owner, smokable.Comp.Solution, out var pipeSolution, out _))
|
||||
return false;
|
||||
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions((contents, reagents)))
|
||||
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(contents))
|
||||
{
|
||||
var reagentSolution = soln.Comp.Solution;
|
||||
_solutionContainerSystem.TryAddSolution(pipeSolution.Value, reagentSolution);
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<TrashOnSolutionEmptyComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<TrashOnSolutionEmptyComponent, SolutionContainerChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<TrashOnSolutionEmptyComponent, SolutionChangedEvent>(OnSolutionChange);
|
||||
}
|
||||
|
||||
public void OnMapInit(Entity<TrashOnSolutionEmptyComponent> entity, ref MapInitEvent args)
|
||||
@@ -26,16 +26,13 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
CheckSolutions(entity);
|
||||
}
|
||||
|
||||
public void OnSolutionChange(Entity<TrashOnSolutionEmptyComponent> entity, ref SolutionContainerChangedEvent args)
|
||||
public void OnSolutionChange(Entity<TrashOnSolutionEmptyComponent> entity, ref SolutionChangedEvent args)
|
||||
{
|
||||
CheckSolutions(entity);
|
||||
}
|
||||
|
||||
public void CheckSolutions(Entity<TrashOnSolutionEmptyComponent> entity)
|
||||
{
|
||||
if (!HasComp<SolutionContainerManagerComponent>(entity))
|
||||
return;
|
||||
|
||||
if (_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
|
||||
UpdateTags(entity, solution);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Requires that a changeling has obtained X unique identities.
|
||||
/// Depends on <see cref="NumberObjectiveComponent"/> to function.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(ChangelingObjectiveSystem))]
|
||||
public sealed partial class ChangelingUniqueIdentityConditionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of identities that have been already devoured.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int UniqueIdentities;
|
||||
}
|
||||
@@ -11,6 +11,8 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.Objectives.Commands;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Prototypes;
|
||||
@@ -23,13 +25,13 @@ namespace Content.Server.Objectives;
|
||||
|
||||
public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
{
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly SharedJobSystem _job = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private IEnumerable<string>? _objectives;
|
||||
|
||||
@@ -60,19 +62,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
{
|
||||
// go through each gamerule getting data for the roundend summary.
|
||||
var summaries = new Dictionary<string, Dictionary<string, List<(EntityUid, string)>>>();
|
||||
var query = EntityQueryEnumerator<GameRuleComponent>();
|
||||
while (query.MoveNext(out var uid, out var gameRule))
|
||||
var query = EntityQueryEnumerator<ActiveGameRuleComponent, AntagSelectionComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
if (!_gameTicker.IsGameRuleAdded(uid, gameRule))
|
||||
if (comp.AgentName is not { } agent)
|
||||
continue;
|
||||
|
||||
var info = new ObjectivesTextGetInfoEvent(new List<(EntityUid, string)>(), string.Empty);
|
||||
RaiseLocalEvent(uid, ref info);
|
||||
if (info.Minds.Count == 0)
|
||||
continue;
|
||||
var minds = _antag.GetAntagIdentities((uid, comp));
|
||||
|
||||
// first group the gamerules by their agents, for example 2 different dragons
|
||||
var agent = info.AgentName;
|
||||
if (!summaries.ContainsKey(agent))
|
||||
summaries[agent] = new Dictionary<string, List<(EntityUid, string)>>();
|
||||
|
||||
@@ -85,11 +82,11 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
if (summary.ContainsKey(prepend.Text))
|
||||
{
|
||||
// same prepended text (usually empty) so combine them
|
||||
summary[prepend.Text].AddRange(info.Minds);
|
||||
summary[prepend.Text].AddRange(minds);
|
||||
}
|
||||
else
|
||||
{
|
||||
summary[prepend.Text] = info.Minds;
|
||||
summary[prepend.Text] = minds.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +99,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
foreach (var (_, minds) in summary)
|
||||
{
|
||||
total += minds.Count;
|
||||
totalInCustody += minds.Where(pair => IsInCustody(pair.Item1)).Count();
|
||||
totalInCustody += minds.Count(pair => IsInCustody(pair.Item1));
|
||||
}
|
||||
|
||||
var result = new StringBuilder();
|
||||
@@ -258,7 +255,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
||||
/// <summary>
|
||||
/// Returns whether a target is considered 'in custody' (cuffed on the shuttle).
|
||||
/// </summary>
|
||||
private bool IsInCustody(EntityUid mindId, MindComponent? mind = null)
|
||||
public bool IsInCustody(EntityUid mindId, MindComponent? mind = null)
|
||||
{
|
||||
if (!Resolve(mindId, ref mind))
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user