merge remote wizden/master

This commit is contained in:
Dmitry
2026-04-30 02:49:26 +07:00
668 changed files with 16562 additions and 15153 deletions
@@ -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 },
+5 -5
View File
@@ -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];
}
+22 -7
View File
@@ -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;
}
}
+94 -26
View File
@@ -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)))
+5
View File
@@ -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
+2 -2
View File
@@ -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);
}
}
+455 -208
View File
@@ -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);
}
}
+29 -30
View File
@@ -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);
}
+3 -3
View File
@@ -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;
}
+13 -16
View File
@@ -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