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

# Conflicts:
#	Resources/Prototypes/Maps/oasis.yml
This commit is contained in:
Ed
2024-06-17 14:20:38 +03:00
351 changed files with 27677 additions and 9596 deletions

View File

@@ -0,0 +1,31 @@
namespace Content.Shared.Atmos.Components;
// Unfortunately can't be friends yet due to magboots.
[RegisterComponent]
public sealed partial class MovedByPressureComponent : Component
{
public const float MoveForcePushRatio = 1f;
public const float MoveForceForcePushRatio = 1f;
public const float ProbabilityOffset = 25f;
public const float ProbabilityBasePercent = 10f;
public const float ThrowForce = 100f;
/// <summary>
/// Accumulates time when yeeted by high pressure deltas.
/// </summary>
[DataField]
public float Accumulator;
[DataField]
public bool Enabled { get; set; } = true;
[DataField]
public float PressureResistance { get; set; } = 1f;
[DataField]
public float MoveResist { get; set; } = 100f;
[ViewVariables(VVAccess.ReadWrite)]
public int LastHighPressureMovementAirCycle { get; set; } = 0;
}

View File

@@ -13,6 +13,7 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
public VentPressureBound PressureChecks { get; set; } = VentPressureBound.ExternalBound;
public float ExternalPressureBound { get; set; } = Atmospherics.OneAtmosphere;
public float InternalPressureBound { get; set; } = 0f;
public bool PressureLockoutOverride { get; set; } = false;
// Presets for 'dumb' air alarm modes
@@ -22,7 +23,8 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
PumpDirection = VentPumpDirection.Releasing,
PressureChecks = VentPressureBound.ExternalBound,
ExternalPressureBound = Atmospherics.OneAtmosphere,
InternalPressureBound = 0f
InternalPressureBound = 0f,
PressureLockoutOverride = false
};
public static GasVentPumpData FillModePreset = new GasVentPumpData
@@ -32,7 +34,8 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
PumpDirection = VentPumpDirection.Releasing,
PressureChecks = VentPressureBound.ExternalBound,
ExternalPressureBound = Atmospherics.OneAtmosphere * 50,
InternalPressureBound = 0f
InternalPressureBound = 0f,
PressureLockoutOverride = true
};
public static GasVentPumpData PanicModePreset = new GasVentPumpData
@@ -42,7 +45,8 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
PumpDirection = VentPumpDirection.Releasing,
PressureChecks = VentPressureBound.ExternalBound,
ExternalPressureBound = Atmospherics.OneAtmosphere,
InternalPressureBound = 0f
InternalPressureBound = 0f,
PressureLockoutOverride = false
};
public static GasVentPumpData ReplaceModePreset = new GasVentPumpData
@@ -53,7 +57,8 @@ namespace Content.Shared.Atmos.Piping.Unary.Components
PumpDirection = VentPumpDirection.Releasing,
PressureChecks = VentPressureBound.ExternalBound,
ExternalPressureBound = Atmospherics.OneAtmosphere,
InternalPressureBound = 0f
InternalPressureBound = 0f,
PressureLockoutOverride = false
};
}

View File

@@ -1,92 +0,0 @@
using Content.Shared.Actions;
using Content.Shared.Bed.Sleep;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.Pointing;
using Content.Shared.Speech;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Bed.Sleep
{
public abstract class SharedSleepingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly BlindableSystem _blindableSystem = default!;
[ValidatePrototypeId<EntityPrototype>] private const string WakeActionId = "ActionWake";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SleepingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SleepingComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SleepingComponent, SpeakAttemptEvent>(OnSpeakAttempt);
SubscribeLocalEvent<SleepingComponent, CanSeeAttemptEvent>(OnSeeAttempt);
SubscribeLocalEvent<SleepingComponent, PointAttemptEvent>(OnPointAttempt);
}
private void OnMapInit(EntityUid uid, SleepingComponent component, MapInitEvent args)
{
var ev = new SleepStateChangedEvent(true);
RaiseLocalEvent(uid, ev);
_blindableSystem.UpdateIsBlind(uid);
_actionsSystem.AddAction(uid, ref component.WakeAction, WakeActionId, uid);
// TODO remove hardcoded time.
_actionsSystem.SetCooldown(component.WakeAction, _gameTiming.CurTime, _gameTiming.CurTime + TimeSpan.FromSeconds(2f));
}
private void OnShutdown(EntityUid uid, SleepingComponent component, ComponentShutdown args)
{
_actionsSystem.RemoveAction(uid, component.WakeAction);
var ev = new SleepStateChangedEvent(false);
RaiseLocalEvent(uid, ev);
_blindableSystem.UpdateIsBlind(uid);
}
private void OnSpeakAttempt(EntityUid uid, SleepingComponent component, SpeakAttemptEvent args)
{
// TODO reduce duplication of this behavior with MobStateSystem somehow
if (HasComp<AllowNextCritSpeechComponent>(uid))
{
RemCompDeferred<AllowNextCritSpeechComponent>(uid);
return;
}
args.Cancel();
}
private void OnSeeAttempt(EntityUid uid, SleepingComponent component, CanSeeAttemptEvent args)
{
if (component.LifeStage <= ComponentLifeStage.Running)
args.Cancel();
}
private void OnPointAttempt(EntityUid uid, SleepingComponent component, PointAttemptEvent args)
{
args.Cancel();
}
}
}
public sealed partial class SleepActionEvent : InstantActionEvent {}
public sealed partial class WakeActionEvent : InstantActionEvent {}
/// <summary>
/// Raised on an entity when they fall asleep or wake up.
/// </summary>
public sealed class SleepStateChangedEvent : EntityEventArgs
{
public bool FellAsleep = false;
public SleepStateChangedEvent(bool fellAsleep)
{
FellAsleep = fellAsleep;
}
}

View File

@@ -1,31 +1,42 @@
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Bed.Sleep;
/// <summary>
/// Added to entities when they go to sleep.
/// </summary>
[NetworkedComponent, RegisterComponent, AutoGenerateComponentPause(Dirty = true)]
[NetworkedComponent, RegisterComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause(Dirty = true)]
public sealed partial class SleepingComponent : Component
{
/// <summary>
/// How much damage of any type it takes to wake this entity.
/// </summary>
[DataField("wakeThreshold")]
[DataField]
public FixedPoint2 WakeThreshold = FixedPoint2.New(2);
/// <summary>
/// Cooldown time between users hand interaction.
/// </summary>
[DataField("cooldown")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan Cooldown = TimeSpan.FromSeconds(1f);
[DataField("cooldownEnd", customTypeSerializer:typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan CoolDownEnd;
[DataField]
[AutoNetworkedField, AutoPausedField]
public TimeSpan CooldownEnd;
[DataField("wakeAction")] public EntityUid? WakeAction;
[DataField]
[AutoNetworkedField]
public EntityUid? WakeAction;
/// <summary>
/// Sound to play when another player attempts to wake this entity.
/// </summary>
[DataField]
public SoundSpecifier WakeAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg")
{
Params = AudioParams.Default.WithVariation(0.05f)
};
}

View File

@@ -0,0 +1,314 @@
using Content.Shared.Actions;
using Content.Shared.Damage;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Examine;
using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Pointing;
using Content.Shared.Popups;
using Content.Shared.Slippery;
using Content.Shared.Sound;
using Content.Shared.Sound.Components;
using Content.Shared.Speech;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Bed.Sleep;
public sealed partial class SleepingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly BlindableSystem _blindableSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedEmitSoundSystem _emitSound = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
public static readonly ProtoId<EntityPrototype> SleepActionId = "ActionSleep";
public static readonly ProtoId<EntityPrototype> WakeActionId = "ActionWake";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActionsContainerComponent, SleepActionEvent>(OnBedSleepAction);
SubscribeLocalEvent<MobStateComponent, SleepStateChangedEvent>(OnSleepStateChanged);
SubscribeLocalEvent<MobStateComponent, WakeActionEvent>(OnWakeAction);
SubscribeLocalEvent<MobStateComponent, SleepActionEvent>(OnSleepAction);
SubscribeLocalEvent<SleepingComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<SleepingComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<SleepingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SleepingComponent, SpeakAttemptEvent>(OnSpeakAttempt);
SubscribeLocalEvent<SleepingComponent, CanSeeAttemptEvent>(OnSeeAttempt);
SubscribeLocalEvent<SleepingComponent, PointAttemptEvent>(OnPointAttempt);
SubscribeLocalEvent<SleepingComponent, SlipAttemptEvent>(OnSlip);
SubscribeLocalEvent<SleepingComponent, ConsciousAttemptEvent>(OnConsciousAttempt);
SubscribeLocalEvent<SleepingComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<SleepingComponent, GetVerbsEvent<AlternativeVerb>>(AddWakeVerb);
SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
}
private void OnBedSleepAction(Entity<ActionsContainerComponent> ent, ref SleepActionEvent args)
{
TrySleeping(args.Performer);
}
private void OnWakeAction(Entity<MobStateComponent> ent, ref WakeActionEvent args)
{
if (TryWakeWithCooldown(ent.Owner))
args.Handled = true;
}
private void OnSleepAction(Entity<MobStateComponent> ent, ref SleepActionEvent args)
{
TrySleeping((ent, ent.Comp));
}
/// <summary>
/// when sleeping component is added or removed, we do some stuff with other components.
/// </summary>
private void OnSleepStateChanged(Entity<MobStateComponent> ent, ref SleepStateChangedEvent args)
{
if (args.FellAsleep)
{
// Expiring status effects would remove the components needed for sleeping
_statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "Stun");
_statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "KnockedDown");
EnsureComp<StunnedComponent>(ent);
EnsureComp<KnockedDownComponent>(ent);
if (TryComp<SleepEmitSoundComponent>(ent, out var sleepSound))
{
var emitSound = EnsureComp<SpamEmitSoundComponent>(ent);
if (HasComp<SnoringComponent>(ent))
{
emitSound.Sound = sleepSound.Snore;
}
emitSound.MinInterval = sleepSound.Interval;
emitSound.MaxInterval = sleepSound.MaxInterval;
emitSound.PopUp = sleepSound.PopUp;
Dirty(ent.Owner, emitSound);
}
return;
}
RemComp<StunnedComponent>(ent);
RemComp<KnockedDownComponent>(ent);
RemComp<SpamEmitSoundComponent>(ent);
}
private void OnMapInit(Entity<SleepingComponent> ent, ref MapInitEvent args)
{
var ev = new SleepStateChangedEvent(true);
RaiseLocalEvent(ent, ref ev);
_blindableSystem.UpdateIsBlind(ent.Owner);
_actionsSystem.AddAction(ent, ref ent.Comp.WakeAction, WakeActionId, ent);
// TODO remove hardcoded time.
_actionsSystem.SetCooldown(ent.Comp.WakeAction, _gameTiming.CurTime, _gameTiming.CurTime + TimeSpan.FromSeconds(2f));
}
private void OnSpeakAttempt(Entity<SleepingComponent> ent, ref SpeakAttemptEvent args)
{
// TODO reduce duplication of this behavior with MobStateSystem somehow
if (HasComp<AllowNextCritSpeechComponent>(ent))
{
RemCompDeferred<AllowNextCritSpeechComponent>(ent);
return;
}
args.Cancel();
}
private void OnSeeAttempt(Entity<SleepingComponent> ent, ref CanSeeAttemptEvent args)
{
if (ent.Comp.LifeStage <= ComponentLifeStage.Running)
args.Cancel();
}
private void OnPointAttempt(Entity<SleepingComponent> ent, ref PointAttemptEvent args)
{
args.Cancel();
}
private void OnSlip(Entity<SleepingComponent> ent, ref SlipAttemptEvent args)
{
args.Cancel();
}
private void OnConsciousAttempt(Entity<SleepingComponent> ent, ref ConsciousAttemptEvent args)
{
args.Cancel();
}
private void OnExamined(Entity<SleepingComponent> ent, ref ExaminedEvent args)
{
if (args.IsInDetailsRange)
{
args.PushMarkup(Loc.GetString("sleep-examined", ("target", Identity.Entity(ent, EntityManager))));
}
}
private void AddWakeVerb(Entity<SleepingComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;
var target = args.Target;
var user = args.User;
AlternativeVerb verb = new()
{
Act = () =>
{
TryWakeWithCooldown((ent, ent.Comp), user: user);
},
Text = Loc.GetString("action-name-wake"),
Priority = 2
};
args.Verbs.Add(verb);
}
/// <summary>
/// When you click on a sleeping person with an empty hand, try to wake them.
/// </summary>
private void OnInteractHand(Entity<SleepingComponent> ent, ref InteractHandEvent args)
{
args.Handled = true;
TryWakeWithCooldown((ent, ent.Comp), args.User);
}
/// <summary>
/// Wake up on taking an instance of damage at least the value of WakeThreshold.
/// </summary>
private void OnDamageChanged(Entity<SleepingComponent> ent, ref DamageChangedEvent args)
{
if (!args.DamageIncreased || args.DamageDelta == null)
return;
if (args.DamageDelta.GetTotal() >= ent.Comp.WakeThreshold)
TryWaking((ent, ent.Comp));
}
/// <summary>
/// In crit, we wake up if we are not being forced to sleep.
/// And, you can't sleep when dead...
/// </summary>
private void OnMobStateChanged(Entity<SleepingComponent> ent, ref MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
{
RemComp<SpamEmitSoundComponent>(ent);
RemComp<SleepingComponent>(ent);
return;
}
if (TryComp<SpamEmitSoundComponent>(ent, out var spam))
_emitSound.SetEnabled((ent, spam), args.NewMobState == MobState.Alive);
}
private void OnInit(Entity<ForcedSleepingComponent> ent, ref ComponentInit args)
{
TrySleeping(ent.Owner);
}
private void Wake(Entity<SleepingComponent> ent)
{
RemComp<SleepingComponent>(ent);
_actionsSystem.RemoveAction(ent, ent.Comp.WakeAction);
var ev = new SleepStateChangedEvent(false);
RaiseLocalEvent(ent, ref ev);
_blindableSystem.UpdateIsBlind(ent.Owner);
}
/// <summary>
/// Try sleeping. Only mobs can sleep.
/// </summary>
public bool TrySleeping(Entity<MobStateComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false))
return false;
var tryingToSleepEvent = new TryingToSleepEvent(ent);
RaiseLocalEvent(ent, ref tryingToSleepEvent);
if (tryingToSleepEvent.Cancelled)
return false;
EnsureComp<SleepingComponent>(ent);
return true;
}
/// <summary>
/// Tries to wake up <paramref name="ent"/>, with a cooldown between attempts to prevent spam.
/// </summary>
public bool TryWakeWithCooldown(Entity<SleepingComponent?> ent, EntityUid? user = null)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
var curTime = _gameTiming.CurTime;
if (curTime < ent.Comp.CooldownEnd)
return false;
ent.Comp.CooldownEnd = curTime + ent.Comp.Cooldown;
Dirty(ent, ent.Comp);
return TryWaking(ent, user: user);
}
/// <summary>
/// Try to wake up <paramref name="ent"/>.
/// </summary>
public bool TryWaking(Entity<SleepingComponent?> ent, bool force = false, EntityUid? user = null)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
if (!force && HasComp<ForcedSleepingComponent>(ent))
{
if (user != null)
{
_audio.PlayPredicted(ent.Comp.WakeAttemptSound, ent, user);
_popupSystem.PopupClient(Loc.GetString("wake-other-failure", ("target", Identity.Entity(ent, EntityManager))), ent, user, PopupType.SmallCaution);
}
return false;
}
if (user != null)
{
_audio.PlayPredicted(ent.Comp.WakeAttemptSound, ent, user);
_popupSystem.PopupClient(Loc.GetString("wake-other-success", ("target", Identity.Entity(ent, EntityManager))), ent, user);
}
Wake((ent, ent.Comp));
return true;
}
}
public sealed partial class SleepActionEvent : InstantActionEvent;
public sealed partial class WakeActionEvent : InstantActionEvent;
/// <summary>
/// Raised on an entity when they fall asleep or wake up.
/// </summary>
[ByRefEvent]
public record struct SleepStateChangedEvent(bool FellAsleep);

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Bed.Sleep;
/// <summary>
/// This is used for the snoring trait.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class SnoringComponent : Component
{
}

View File

@@ -124,6 +124,18 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<float>
EventsRampingAverageChaos = CVarDef.Create("events.ramping_average_chaos", 6f, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// Minimum time between meteor swarms in minutes.
/// </summary>
public static readonly CVarDef<float>
MeteorSwarmMinTime = CVarDef.Create("events.meteor_swarm_min_time", 7.5f, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// Maximum time between meteor swarms in minutes.
/// </summary>
public static readonly CVarDef<float>
MeteorSwarmMaxTime = CVarDef.Create("events.meteor_swarm_max_time", 12.5f, CVar.ARCHIVE | CVar.SERVERONLY);
/*
* Game
*/
@@ -1428,6 +1440,18 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> ArrivalsReturns =
CVarDef.Create("shuttle.arrivals_returns", false, CVar.SERVERONLY);
/// <summary>
/// Should all players be forced to spawn at departures, even on roundstart, even if their loadout says they spawn in cryo?
/// </summary>
public static readonly CVarDef<bool> ForceArrivals =
CVarDef.Create("shuttle.force_arrivals", false, CVar.SERVERONLY);
/// <summary>
/// Should all players who spawn at arrivals have godmode until they leave the map?
/// </summary>
public static readonly CVarDef<bool> GodmodeArrivals =
CVarDef.Create("shuttle.godmode_arrivals", false, CVar.SERVERONLY);
/// <summary>
/// Whether to automatically spawn escape shuttles.
/// </summary>
@@ -1847,7 +1871,7 @@ namespace Content.Shared.CCVar
/// Don't show rules to localhost/loopback interface.
/// </summary>
public static readonly CVarDef<bool> RulesExemptLocal =
CVarDef.Create("rules.exempt_local", false, CVar.SERVERONLY);
CVarDef.Create("rules.exempt_local", true, CVar.SERVERONLY);
/*
@@ -1927,6 +1951,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<float> GhostRoleTime =
CVarDef.Create("ghost.role_time", 3f, CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Whether or not to kill the player's mob on ghosting, when it is in a critical health state.
/// </summary>
public static readonly CVarDef<bool> GhostKillCrit =
CVarDef.Create("ghost.kill_crit", true, CVar.REPLICATED | CVar.SERVER);
/*
* Fire alarm
*/

View File

@@ -193,7 +193,7 @@ public sealed class SolutionTransferSystem : EntitySystem
var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
var solution = _solution.SplitSolution(source, actualAmount);
_solution.Refill(targetEntity, target, solution);
_solution.AddSolution(target, solution);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Chemistry.Components;
using Content.Shared.Database;
@@ -28,7 +28,7 @@ namespace Content.Shared.Chemistry.Reagent
public virtual string ReagentEffectFormat => "guidebook-reagent-effect-description";
protected abstract string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys); // => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability));
protected abstract string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys);
/// <summary>
/// What's the chance, from 0 to 1, that this effect will occur?

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Clothing.Components;
/// <summary>
/// This is used for clothing that makes an entity weightless when worn.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class AntiGravityClothingComponent : Component;

View File

@@ -16,9 +16,14 @@ namespace Content.Shared.Clothing.Components;
public sealed partial class ClothingComponent : Component
{
[DataField("clothingVisuals")]
[Access(typeof(ClothingSystem), typeof(InventorySystem), Other = AccessPermissions.ReadExecute)] // TODO remove execute permissions.
public Dictionary<string, List<PrototypeLayerData>> ClothingVisuals = new();
/// <summary>
/// The name of the layer in the user that this piece of clothing will map to
/// </summary>
[DataField]
public string? MappedLayer;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("quickEquip")]
public bool QuickEquip = true;
@@ -124,4 +129,3 @@ public sealed partial class ClothingUnequipDoAfterEvent : DoAfterEvent
public override DoAfterEvent Clone() => this;
}

View File

@@ -0,0 +1,27 @@
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.NPC.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Clothing.Components;
/// <summary>
/// When equipped, adds the wearer to a faction.
/// When removed, removes the wearer from a faction.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(FactionClothingSystem))]
public sealed partial class FactionClothingComponent : Component
{
/// <summary>
/// Faction to add and remove.
/// </summary>
[DataField(required: true)]
public ProtoId<NpcFactionPrototype> Faction = string.Empty;
/// <summary>
/// If true, the wearer was already part of the faction.
/// This prevents wrongly removing them after removing the item.
/// </summary>
[DataField]
public bool AlreadyMember;
}

View File

@@ -0,0 +1,23 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Gravity;
using Content.Shared.Inventory;
namespace Content.Shared.Clothing.EntitySystems;
public sealed class AntiGravityClothingSystem : EntitySystem
{
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<IsWeightlessEvent>>(OnIsWeightless);
}
private void OnIsWeightless(Entity<AntiGravityClothingComponent> ent, ref InventoryRelayedEvent<IsWeightlessEvent> args)
{
if (args.Args.Handled)
return;
args.Args.Handled = true;
args.Args.IsWeightless = true;
}
}

View File

@@ -92,26 +92,29 @@ public abstract class ClothingSystem : EntitySystem
InventorySystem.InventorySlotEnumerator enumerator = _invSystem.GetSlotEnumerator(equipee);
bool shouldLayerShow = true;
while (enumerator.NextItem(out EntityUid item))
while (enumerator.NextItem(out EntityUid item, out SlotDefinition? slot))
{
if (TryComp(item, out HideLayerClothingComponent? comp))
{
if (comp.Slots.Contains(layer))
{
//Checks for mask toggling. TODO: Make a generic system for this
if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask) && TryComp(item, out ClothingComponent? clothing))
if (TryComp(item, out ClothingComponent? clothing) && clothing.Slots == slot.SlotFlags)
{
if (clothing.EquippedPrefix != mask.EquippedPrefix)
//Checks for mask toggling. TODO: Make a generic system for this
if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask))
{
if (clothing.EquippedPrefix != mask.EquippedPrefix)
{
shouldLayerShow = false;
break;
}
}
else
{
shouldLayerShow = false;
break;
}
}
else
{
shouldLayerShow = false;
break;
}
}
}
}
@@ -238,9 +241,6 @@ public abstract class ClothingSystem : EntitySystem
public void SetLayerColor(ClothingComponent clothing, string slot, string mapKey, Color? color)
{
if (clothing.ClothingVisuals == null)
return;
foreach (var layer in clothing.ClothingVisuals[slot])
{
if (layer.MapKeys == null)
@@ -254,9 +254,6 @@ public abstract class ClothingSystem : EntitySystem
}
public void SetLayerState(ClothingComponent clothing, string slot, string mapKey, string state)
{
if (clothing.ClothingVisuals == null)
return;
foreach (var layer in clothing.ClothingVisuals[slot])
{
if (layer.MapKeys == null)

View File

@@ -0,0 +1,42 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Inventory.Events;
using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
namespace Content.Shared.Clothing.EntitySystems;
/// <summary>
/// Handles <see cref="FactionClothingComponent"/> faction adding and removal.
/// </summary>
public sealed class FactionClothingSystem : EntitySystem
{
[Dependency] private readonly NpcFactionSystem _faction = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FactionClothingComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<FactionClothingComponent, GotUnequippedEvent>(OnUnequipped);
}
private void OnEquipped(Entity<FactionClothingComponent> ent, ref GotEquippedEvent args)
{
TryComp<NpcFactionMemberComponent>(args.Equipee, out var factionComp);
var faction = (args.Equipee, factionComp);
ent.Comp.AlreadyMember = _faction.IsMember(faction, ent.Comp.Faction);
_faction.AddFaction(faction, ent.Comp.Faction);
}
private void OnUnequipped(Entity<FactionClothingComponent> ent, ref GotUnequippedEvent args)
{
if (ent.Comp.AlreadyMember)
{
ent.Comp.AlreadyMember = false;
return;
}
_faction.RemoveFaction(args.Equipee, ent.Comp.Faction);
}
}

View File

@@ -33,32 +33,32 @@ public sealed class FoldableClothingSystem : EntitySystem
private void OnFolded(Entity<FoldableClothingComponent> ent, ref FoldedEvent args)
{
if (TryComp<ClothingComponent>(ent.Owner, out var clothingComp) &&
TryComp<ItemComponent>(ent.Owner, out var itemComp))
if (!TryComp<ClothingComponent>(ent.Owner, out var clothingComp) ||
!TryComp<ItemComponent>(ent.Owner, out var itemComp))
return;
if (args.IsFolded)
{
if (args.IsFolded)
{
if (ent.Comp.FoldedSlots.HasValue)
_clothingSystem.SetSlots(ent.Owner, ent.Comp.FoldedSlots.Value, clothingComp);
if (ent.Comp.FoldedSlots.HasValue)
_clothingSystem.SetSlots(ent.Owner, ent.Comp.FoldedSlots.Value, clothingComp);
if (ent.Comp.FoldedEquippedPrefix != null)
_clothingSystem.SetEquippedPrefix(ent.Owner, ent.Comp.FoldedEquippedPrefix, clothingComp);
if (ent.Comp.FoldedEquippedPrefix != null)
_clothingSystem.SetEquippedPrefix(ent.Owner, ent.Comp.FoldedEquippedPrefix, clothingComp);
if (ent.Comp.FoldedHeldPrefix != null)
_itemSystem.SetHeldPrefix(ent.Owner, ent.Comp.FoldedHeldPrefix, false, itemComp);
}
else
{
if (ent.Comp.UnfoldedSlots.HasValue)
_clothingSystem.SetSlots(ent.Owner, ent.Comp.UnfoldedSlots.Value, clothingComp);
if (ent.Comp.FoldedHeldPrefix != null)
_itemSystem.SetHeldPrefix(ent.Owner, ent.Comp.FoldedHeldPrefix, false, itemComp);
}
else
{
if (ent.Comp.UnfoldedSlots.HasValue)
_clothingSystem.SetSlots(ent.Owner, ent.Comp.UnfoldedSlots.Value, clothingComp);
if (ent.Comp.FoldedEquippedPrefix != null)
_clothingSystem.SetEquippedPrefix(ent.Owner, null, clothingComp);
if (ent.Comp.FoldedEquippedPrefix != null)
_clothingSystem.SetEquippedPrefix(ent.Owner, null, clothingComp);
if (ent.Comp.FoldedHeldPrefix != null)
_itemSystem.SetHeldPrefix(ent.Owner, null, false, itemComp);
if (ent.Comp.FoldedHeldPrefix != null)
_itemSystem.SetHeldPrefix(ent.Owner, null, false, itemComp);
}
}
}
}

View File

@@ -1,8 +1,11 @@
using System.Linq;
using Content.Shared.Clothing.Components;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -15,6 +18,7 @@ public sealed class LoadoutSystem : EntitySystem
{
// Shared so we can predict it for placement manager.
[Dependency] private readonly ActorSystem _actors = default!;
[Dependency] private readonly SharedStationSpawningSystem _station = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -125,7 +129,17 @@ public sealed class LoadoutSystem : EntitySystem
var id = _random.Pick(component.RoleLoadout);
var proto = _protoMan.Index(id);
var loadout = new RoleLoadout(id);
loadout.SetDefault(_protoMan, true);
loadout.SetDefault(GetProfile(uid), _actors.GetSession(uid), _protoMan, true);
_station.EquipRoleLoadout(uid, loadout, proto);
}
public HumanoidCharacterProfile GetProfile(EntityUid? uid)
{
if (TryComp(uid, out HumanoidAppearanceComponent? appearance))
{
return HumanoidCharacterProfile.DefaultWithSpecies(appearance.Species);
}
return HumanoidCharacterProfile.Random();
}
}

View File

@@ -20,4 +20,10 @@ public sealed partial class MagbootsComponent : Component
[DataField]
public ProtoId<AlertPrototype> MagbootsAlert = "Magboots";
/// <summary>
/// If true, the user must be standing on a grid or planet map to experience the weightlessness-canceling effect
/// </summary>
[DataField]
public bool RequiresGrid = true;
}

View File

@@ -1,5 +1,8 @@
using Content.Shared.Actions;
using Content.Shared.Alert;
using Content.Shared.Atmos.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Gravity;
using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Slippery;
@@ -9,10 +12,12 @@ using Robust.Shared.Containers;
namespace Content.Shared.Clothing;
public abstract class SharedMagbootsSystem : EntitySystem
public sealed class SharedMagbootsSystem : EntitySystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly ClothingSpeedModifierSystem _clothingSpeedModifier = default!;
[Dependency] private readonly ClothingSystem _clothing = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedActionsSystem _sharedActions = default!;
[Dependency] private readonly SharedActionsSystem _actionContainer = default!;
@@ -29,6 +34,11 @@ public abstract class SharedMagbootsSystem : EntitySystem
SubscribeLocalEvent<MagbootsComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<MagbootsComponent, ToggleMagbootsEvent>(OnToggleMagboots);
SubscribeLocalEvent<MagbootsComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<MagbootsComponent, ClothingGotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<MagbootsComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<MagbootsComponent, InventoryRelayedEvent<IsWeightlessEvent>>(OnIsWeightless);
}
private void OnMapInit(EntityUid uid, MagbootsComponent component, MapInitEvent args)
@@ -37,6 +47,16 @@ public abstract class SharedMagbootsSystem : EntitySystem
Dirty(uid, component);
}
private void OnGotUnequipped(EntityUid uid, MagbootsComponent component, ref ClothingGotUnequippedEvent args)
{
UpdateMagbootEffects(args.Wearer, uid, false, component);
}
private void OnGotEquipped(EntityUid uid, MagbootsComponent component, ref ClothingGotEquippedEvent args)
{
UpdateMagbootEffects(args.Wearer, uid, true, component);
}
private void OnToggleMagboots(EntityUid uid, MagbootsComponent component, ToggleMagbootsEvent args)
{
if (args.Handled)
@@ -51,9 +71,11 @@ public abstract class SharedMagbootsSystem : EntitySystem
{
magboots.On = !magboots.On;
if (_sharedContainer.TryGetContainingContainer(uid, out var container) &&
if (_sharedContainer.TryGetContainingContainer((uid, Transform(uid)), out var container) &&
_inventory.TryGetSlotEntity(container.Owner, "shoes", out var entityUid) && entityUid == uid)
{
UpdateMagbootEffects(container.Owner, uid, true, magboots);
}
if (TryComp<ItemComponent>(uid, out var item))
{
@@ -66,9 +88,28 @@ public abstract class SharedMagbootsSystem : EntitySystem
Dirty(uid, magboots);
}
protected virtual void UpdateMagbootEffects(EntityUid parent, EntityUid uid, bool state, MagbootsComponent? component) { }
public void UpdateMagbootEffects(EntityUid parent, EntityUid uid, bool state, MagbootsComponent? component)
{
if (!Resolve(uid, ref component))
return;
state = state && component.On;
protected void OnChanged(EntityUid uid, MagbootsComponent component)
if (TryComp(parent, out MovedByPressureComponent? movedByPressure))
{
movedByPressure.Enabled = !state;
}
if (state)
{
_alerts.ShowAlert(parent, component.MagbootsAlert);
}
else
{
_alerts.ClearAlert(parent, component.MagbootsAlert);
}
}
private void OnChanged(EntityUid uid, MagbootsComponent component)
{
_sharedActions.SetToggled(component.ToggleActionEntity, component.On);
_clothingSpeedModifier.SetClothingSpeedModifierEnabled(uid, component.On);
@@ -79,10 +120,12 @@ public abstract class SharedMagbootsSystem : EntitySystem
if (!args.CanAccess || !args.CanInteract)
return;
ActivationVerb verb = new();
verb.Text = Loc.GetString("toggle-magboots-verb-get-data-text");
verb.Act = () => ToggleMagboots(uid, component);
// TODO VERB ICON add toggle icon? maybe a computer on/off symbol?
ActivationVerb verb = new()
{
Text = Loc.GetString("toggle-magboots-verb-get-data-text"),
Act = () => ToggleMagboots(uid, component),
// TODO VERB ICON add toggle icon? maybe a computer on/off symbol?
};
args.Verbs.Add(verb);
}
@@ -96,6 +139,22 @@ public abstract class SharedMagbootsSystem : EntitySystem
{
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnIsWeightless(Entity<MagbootsComponent> ent, ref InventoryRelayedEvent<IsWeightlessEvent> args)
{
if (args.Args.Handled)
return;
if (!ent.Comp.On)
return;
// do not cancel weightlessness if the person is in off-grid.
if (ent.Comp.RequiresGrid && !_gravity.EntityOnGravitySupportingGridOrMap(ent.Owner))
return;
args.Args.IsWeightless = false;
args.Args.Handled = true;
}
}
public sealed partial class ToggleMagbootsEvent : InstantActionEvent {}
public sealed partial class ToggleMagbootsEvent : InstantActionEvent;

View File

@@ -105,3 +105,9 @@ public record struct UncuffAttemptEvent(EntityUid User, EntityUid Target)
public readonly EntityUid Target = Target;
public bool Cancelled = false;
}
/// <summary>
/// Event raised on an entity being uncuffed to determine any modifiers to the amount of time it takes to uncuff them.
/// </summary>
[ByRefEvent]
public record struct ModifyUncuffDurationEvent(EntityUid User, EntityUid Target, float Duration);

View File

@@ -561,7 +561,10 @@ namespace Content.Shared.Cuffs
return;
}
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.BreakoutTime : cuff.UncuffTime);
RaiseLocalEvent(user, ref ev);
var uncuffTime = ev.Duration;
if (isOwner)
{

View File

@@ -23,6 +23,13 @@ public sealed partial class GameRuleComponent : Component
[DataField]
public int MinPlayers;
/// <summary>
/// If true, this rule not having enough players will cancel the preset selection.
/// If false, it will simply not run silently.
/// </summary>
[DataField]
public bool CancelPresetOnTooFewPlayers = true;
/// <summary>
/// A delay for when the rule the is started and when the starting logic actually runs.
/// </summary>

View File

@@ -6,11 +6,6 @@ namespace Content.Shared.Glue;
[Access(typeof(SharedGlueSystem))]
public sealed partial class GluedComponent : Component
{
/// <summary>
/// Reverts name to before prefix event (essentially removes prefix).
/// </summary>
[DataField("beforeGluedEntityName"), ViewVariables(VVAccess.ReadOnly)]
public string BeforeGluedEntityName = string.Empty;
[DataField("until", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan Until;

View File

@@ -1,9 +1,7 @@
using Content.Shared.Alert;
using Content.Shared.Clothing;
using Content.Shared.Inventory;
using Content.Shared.Movement.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Serialization;
@@ -15,11 +13,12 @@ namespace Content.Shared.Gravity
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[ValidatePrototypeId<AlertPrototype>]
public const string WeightlessAlert = "Weightless";
private EntityQuery<GravityComponent> _gravityQuery;
public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, TransformComponent? xform = null)
{
Resolve(uid, ref body, false);
@@ -30,31 +29,44 @@ namespace Content.Shared.Gravity
if (TryComp<MovementIgnoreGravityComponent>(uid, out var ignoreGravityComponent))
return ignoreGravityComponent.Weightless;
var ev = new IsWeightlessEvent(uid);
RaiseLocalEvent(uid, ref ev);
if (ev.Handled)
return ev.IsWeightless;
if (!Resolve(uid, ref xform))
return true;
// If grid / map has gravity
if (TryComp<GravityComponent>(xform.GridUid, out var gravity) && gravity.Enabled ||
TryComp<GravityComponent>(xform.MapUid, out var mapGravity) && mapGravity.Enabled)
{
if (EntityGridOrMapHaveGravity((uid, xform)))
return false;
}
var hasGrav = gravity != null || mapGravity != null;
// Check for something holding us down
// If the planet has gravity component and no gravity it will still give gravity
// If there's no gravity comp at all (i.e. space) then they don't work.
if (hasGrav && _inventory.TryGetSlotEntity(uid, "shoes", out var ent))
{
// TODO this should just be a event that gets relayed instead of a specific slot & component check.
if (TryComp<MagbootsComponent>(ent, out var boots) && boots.On)
return false;
}
return true;
}
/// <summary>
/// Checks if a given entity is currently standing on a grid or map that supports having gravity at all.
/// </summary>
public bool EntityOnGravitySupportingGridOrMap(Entity<TransformComponent?> entity)
{
entity.Comp ??= Transform(entity);
return _gravityQuery.HasComp(entity.Comp.GridUid) ||
_gravityQuery.HasComp(entity.Comp.MapUid);
}
/// <summary>
/// Checks if a given entity is currently standing on a grid or map that has gravity of some kind.
/// </summary>
public bool EntityGridOrMapHaveGravity(Entity<TransformComponent?> entity)
{
entity.Comp ??= Transform(entity);
return _gravityQuery.TryComp(entity.Comp.GridUid, out var gravity) && gravity.Enabled ||
_gravityQuery.TryComp(entity.Comp.MapUid, out var mapGravity) && mapGravity.Enabled;
}
public override void Initialize()
{
base.Initialize();
@@ -64,6 +76,8 @@ namespace Content.Shared.Gravity
SubscribeLocalEvent<GravityChangedEvent>(OnGravityChange);
SubscribeLocalEvent<GravityComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<GravityComponent, ComponentHandleState>(OnHandleState);
_gravityQuery = GetEntityQuery<GravityComponent>();
}
public override void Update(float frameTime)
@@ -74,9 +88,11 @@ namespace Content.Shared.Gravity
private void OnHandleState(EntityUid uid, GravityComponent component, ref ComponentHandleState args)
{
if (args.Current is not GravityComponentState state) return;
if (args.Current is not GravityComponentState state)
return;
if (component.EnabledVV == state.Enabled) return;
if (component.EnabledVV == state.Enabled)
return;
component.EnabledVV = state.Enabled;
var ev = new GravityChangedEvent(uid, component.EnabledVV);
RaiseLocalEvent(uid, ref ev, true);
@@ -90,9 +106,10 @@ namespace Content.Shared.Gravity
private void OnGravityChange(ref GravityChangedEvent ev)
{
var alerts = AllEntityQuery<AlertsComponent, TransformComponent>();
while(alerts.MoveNext(out var uid, out var comp, out var xform))
while(alerts.MoveNext(out var uid, out _, out var xform))
{
if (xform.GridUid != ev.ChangedGridIndex) continue;
if (xform.GridUid != ev.ChangedGridIndex)
continue;
if (!ev.HasGravity)
{
@@ -145,4 +162,10 @@ namespace Content.Shared.Gravity
}
}
}
[ByRefEvent]
public record struct IsWeightlessEvent(EntityUid Entity, bool IsWeightless = false, bool Handled = false) : IInventoryRelayEvent
{
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
}
}

View File

@@ -5,22 +5,28 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Info;
/// <summary>
/// Sent by the server to show the rules to the client instantly.
/// Sent by the server when the client connects to sync the client rules and displaying a popup with them if necessitated.
/// </summary>
public sealed class ShowRulesPopupMessage : NetMessage
public sealed class SendRulesInformationMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public float PopupTime { get; set; }
public string CoreRules { get; set; } = string.Empty;
public bool ShouldShowRules { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
PopupTime = buffer.ReadFloat();
CoreRules = buffer.ReadString();
ShouldShowRules = buffer.ReadBoolean();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(PopupTime);
buffer.Write(CoreRules);
buffer.Write(ShouldShowRules);
}
}

View File

@@ -4,9 +4,11 @@ using Content.Shared.Damage;
using Content.Shared.Electrocution;
using Content.Shared.Explosion;
using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.Gravity;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Overlays;
using Content.Shared.Radio;
using Content.Shared.Slippery;
@@ -28,9 +30,11 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, SeeIdentityAttemptEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ModifyChangedTemperatureEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent);
// by-ref events
SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, IsWeightlessEvent>(RefRelayInventoryEvent);
// Eye/vision events
SubscribeLocalEvent<InventoryComponent, CanSeeAttemptEvent>(RelayInventoryEvent);

View File

@@ -14,11 +14,4 @@ public sealed partial class LabelComponent : Component
/// </summary>
[DataField, AutoNetworkedField]
public string? CurrentLabel { get; set; }
/// <summary>
/// The original name of the entity
/// Used for reverting the modified entity name when the label is removed
/// </summary>
[DataField, AutoNetworkedField]
public string? OriginalName { get; set; }
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Examine;
using Content.Shared.Labels.Components;
using Content.Shared.NameModifier.EntitySystems;
using Robust.Shared.Utility;
namespace Content.Shared.Labels.EntitySystems;
@@ -11,6 +12,7 @@ public abstract partial class SharedLabelSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<LabelComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<LabelComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
}
public virtual void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null){}
@@ -27,4 +29,10 @@ public abstract partial class SharedLabelSystem : EntitySystem
message.AddText(Loc.GetString("hand-labeler-has-label", ("label", label.CurrentLabel)));
args.PushMessage(message);
}
private void OnRefreshNameModifiers(Entity<LabelComponent> entity, ref RefreshNameModifiersEvent args)
{
if (!string.IsNullOrEmpty(entity.Comp.CurrentLabel))
args.AddModifier("comp-label-format", extraArgs: ("label", entity.Comp.CurrentLabel));
}
}

View File

@@ -129,6 +129,20 @@ namespace Content.Shared.Localizations
};
}
/// <summary>
/// Formats a list as per english grammar rules, but uses or instead of and.
/// </summary>
public static string FormatListToOr(List<string> list)
{
return list.Count switch
{
<= 0 => string.Empty,
1 => list[0],
2 => $"{list[0]} or {list[1]}",
_ => $"{string.Join(" or ", list)}"
};
}
/// <summary>
/// Formats a direction struct as a human-readable string.
/// </summary>

View File

@@ -3,12 +3,6 @@ namespace Content.Shared.Lube;
[RegisterComponent]
public sealed partial class LubedComponent : Component
{
/// <summary>
/// Reverts name to before prefix event (essentially removes prefix).
/// </summary>
[DataField("beforeLubedEntityName")]
public string BeforeLubedEntityName = string.Empty;
[DataField("slipsLeft"), ViewVariables(VVAccess.ReadWrite)]
public int SlipsLeft;

View File

@@ -16,7 +16,8 @@ public partial class MobStateSystem
/// <returns>If the entity can be set to that MobState</returns>
public bool HasState(EntityUid entity, MobState mobState, MobStateComponent? component = null)
{
return Resolve(entity, ref component, false) && component.AllowedStates.Contains(mobState);
return _mobStateQuery.Resolve(entity, ref component, false) &&
component.AllowedStates.Contains(mobState);
}
/// <summary>
@@ -27,7 +28,7 @@ public partial class MobStateSystem
/// <param name="origin">Entity that caused the state update (if applicable)</param>
public void UpdateMobState(EntityUid entity, MobStateComponent? component = null, EntityUid? origin = null)
{
if (!Resolve(entity, ref component))
if (!_mobStateQuery.Resolve(entity, ref component))
return;
var ev = new UpdateMobStateEvent {Target = entity, Component = component, Origin = origin};
@@ -46,7 +47,7 @@ public partial class MobStateSystem
public void ChangeMobState(EntityUid entity, MobState mobState, MobStateComponent? component = null,
EntityUid? origin = null)
{
if (!Resolve(entity, ref component))
if (!_mobStateQuery.Resolve(entity, ref component))
return;
ChangeState(entity, component, mobState, origin: origin);

View File

@@ -2,7 +2,6 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Mobs.Components;
using Content.Shared.Standing;
using Robust.Shared.GameStates;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
@@ -20,9 +19,12 @@ public partial class MobStateSystem : EntitySystem
[Dependency] private readonly IGameTiming _timing = default!;
private ISawmill _sawmill = default!;
private EntityQuery<MobStateComponent> _mobStateQuery;
public override void Initialize()
{
_sawmill = _logManager.GetSawmill("MobState");
_mobStateQuery = GetEntityQuery<MobStateComponent>();
base.Initialize();
SubscribeEvents();
}
@@ -37,7 +39,7 @@ public partial class MobStateSystem : EntitySystem
/// <returns>If the entity is alive</returns>
public bool IsAlive(EntityUid target, MobStateComponent? component = null)
{
if (!Resolve(target, ref component, false))
if (!_mobStateQuery.Resolve(target, ref component, false))
return false;
return component.CurrentState == MobState.Alive;
}
@@ -50,7 +52,7 @@ public partial class MobStateSystem : EntitySystem
/// <returns>If the entity is Critical</returns>
public bool IsCritical(EntityUid target, MobStateComponent? component = null)
{
if (!Resolve(target, ref component, false))
if (!_mobStateQuery.Resolve(target, ref component, false))
return false;
return component.CurrentState == MobState.Critical;
}
@@ -63,7 +65,7 @@ public partial class MobStateSystem : EntitySystem
/// <returns>If the entity is Dead</returns>
public bool IsDead(EntityUid target, MobStateComponent? component = null)
{
if (!Resolve(target, ref component, false))
if (!_mobStateQuery.Resolve(target, ref component, false))
return false;
return component.CurrentState == MobState.Dead;
}
@@ -76,7 +78,7 @@ public partial class MobStateSystem : EntitySystem
/// <returns>If the entity is Critical or Dead</returns>
public bool IsIncapacitated(EntityUid target, MobStateComponent? component = null)
{
if (!Resolve(target, ref component, false))
if (!_mobStateQuery.Resolve(target, ref component, false))
return false;
return component.CurrentState is MobState.Critical or MobState.Dead;
}
@@ -89,14 +91,10 @@ public partial class MobStateSystem : EntitySystem
/// <returns>If the entity is in an Invalid State</returns>
public bool IsInvalidState(EntityUid target, MobStateComponent? component = null)
{
if (!Resolve(target, ref component, false))
if (!_mobStateQuery.Resolve(target, ref component, false))
return false;
return component.CurrentState is MobState.Invalid;
}
#endregion
#region Private Implementation
#endregion
}

View File

@@ -212,7 +212,7 @@ public sealed class MobThresholdSystem : EntitySystem
MobThresholdsComponent? thresholdComponent = null)
{
threshold = null;
if (!Resolve(target, ref thresholdComponent))
if (!Resolve(target, ref thresholdComponent, false))
return false;
return TryGetThresholdForState(target, MobState.Dead, out threshold, thresholdComponent);

View File

@@ -0,0 +1,7 @@
namespace Content.Shared.Movement.Pulling.Components;
/// <summary>
/// Component that indicates that an entity is currently pulling some other entity.
/// </summary>
[RegisterComponent]
public sealed partial class ActivePullerComponent : Component;

View File

@@ -9,7 +9,7 @@ namespace Content.Shared.Movement.Pulling.Components;
/// <summary>
/// Specifies an entity as being able to pull another entity with <see cref="PullableComponent"/>
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(PullingSystem))]
public sealed partial class PullerComponent : Component
{

View File

@@ -3,6 +3,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.Cuffs.Components;
using Content.Shared.Database;
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems;
@@ -12,6 +13,7 @@ using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Pulling.Events;
using Content.Shared.Standing;
using Content.Shared.Throwing;
@@ -43,6 +45,7 @@ public sealed class PullingSystem : EntitySystem
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
@@ -56,7 +59,9 @@ public sealed class PullingSystem : EntitySystem
SubscribeLocalEvent<PullableComponent, JointRemovedEvent>(OnJointRemoved);
SubscribeLocalEvent<PullableComponent, GetVerbsEvent<Verb>>(AddPullVerbs);
SubscribeLocalEvent<PullableComponent, EntGotInsertedIntoContainerMessage>(OnPullableContainerInsert);
SubscribeLocalEvent<PullableComponent, ModifyUncuffDurationEvent>(OnModifyUncuffDuration);
SubscribeLocalEvent<PullerComponent, AfterAutoHandleStateEvent>(OnAfterState);
SubscribeLocalEvent<PullerComponent, EntGotInsertedIntoContainerMessage>(OnPullerContainerInsert);
SubscribeLocalEvent<PullerComponent, EntityUnpausedEvent>(OnPullerUnpaused);
SubscribeLocalEvent<PullerComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
@@ -68,6 +73,14 @@ public sealed class PullingSystem : EntitySystem
.Register<PullingSystem>();
}
private void OnAfterState(Entity<PullerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.Pulling == null)
RemComp<ActivePullerComponent>(ent.Owner);
else
EnsureComp<ActivePullerComponent>(ent.Owner);
}
private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHandItemsEvent args)
{
if (pullerComp.Pulling == null || pullerComp.NeedsHands)
@@ -94,6 +107,18 @@ public sealed class PullingSystem : EntitySystem
TryStopPull(ent.Owner, ent.Comp);
}
private void OnModifyUncuffDuration(Entity<PullableComponent> ent, ref ModifyUncuffDurationEvent args)
{
if (!ent.Comp.BeingPulled)
return;
// We don't care if the person is being uncuffed by someone else
if (args.User != args.Target)
return;
args.Duration *= 2;
}
public override void Shutdown()
{
base.Shutdown();
@@ -212,6 +237,9 @@ public sealed class PullingSystem : EntitySystem
}
var oldPuller = pullableComp.Puller;
if (oldPuller != null)
RemComp<ActivePullerComponent>(oldPuller.Value);
pullableComp.PullJointId = null;
pullableComp.Puller = null;
Dirty(pullableUid, pullableComp);
@@ -394,6 +422,7 @@ public sealed class PullingSystem : EntitySystem
// Use net entity so it's consistent across client and server.
pullableComp.PullJointId = $"pull-joint-{GetNetEntity(pullableUid)}";
EnsureComp<ActivePullerComponent>(pullerUid);
pullerComp.Pulling = pullableUid;
pullableComp.Puller = pullerUid;

View File

@@ -0,0 +1,25 @@
using Robust.Shared.GameStates;
namespace Content.Shared.NameModifier.Components;
/// <summary>
/// Adds a modifier to the wearer's name when this item is equipped,
/// and removes it when it is unequipped.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class ModifyWearerNameComponent : Component
{
/// <summary>
/// The localization ID of the text to be used as the modifier.
/// The base name will be passed in as <c>$baseName</c>
/// </summary>
[DataField, AutoNetworkedField]
public LocId LocId = string.Empty;
/// <summary>
/// Priority of the modifier. See <see cref="EntitySystems.RefreshNameModifiersEvent"/> for more information.
/// </summary>
[DataField, AutoNetworkedField]
public int Priority;
}

View File

@@ -0,0 +1,20 @@
using Content.Shared.NameModifier.EntitySystems;
using Robust.Shared.GameStates;
namespace Content.Shared.NameModifier.Components;
/// <summary>
/// Used to manage modifiers on an entity's name and handle renaming in a way
/// that survives being renamed by multiple systems.
/// </summary>
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(NameModifierSystem))]
public sealed partial class NameModifierComponent : Component
{
/// <summary>
/// The entity's name without any modifiers applied.
/// </summary>
[DataField, AutoNetworkedField]
public string BaseName = string.Empty;
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Clothing;
using Content.Shared.Inventory;
using Content.Shared.NameModifier.Components;
namespace Content.Shared.NameModifier.EntitySystems;
public sealed partial class ModifyWearerNameSystem : EntitySystem
{
[Dependency] private readonly NameModifierSystem _nameMod = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ModifyWearerNameComponent, InventoryRelayedEvent<RefreshNameModifiersEvent>>(OnRefreshNameModifiers);
SubscribeLocalEvent<ModifyWearerNameComponent, ClothingGotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<ModifyWearerNameComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
}
private void OnGotEquipped(Entity<ModifyWearerNameComponent> entity, ref ClothingGotEquippedEvent args)
{
_nameMod.RefreshNameModifiers(args.Wearer);
}
private void OnGotUnequipped(Entity<ModifyWearerNameComponent> entity, ref ClothingGotUnequippedEvent args)
{
_nameMod.RefreshNameModifiers(args.Wearer);
}
private void OnRefreshNameModifiers(Entity<ModifyWearerNameComponent> entity, ref InventoryRelayedEvent<RefreshNameModifiersEvent> args)
{
args.Args.AddModifier(entity.Comp.LocId, entity.Comp.Priority);
}
}

View File

@@ -0,0 +1,143 @@
using System.Linq;
using Content.Shared.Inventory;
using Content.Shared.NameModifier.Components;
namespace Content.Shared.NameModifier.EntitySystems;
/// <inheritdoc cref="NameModifierComponent"/>
public sealed partial class NameModifierSystem : EntitySystem
{
[Dependency] private readonly MetaDataSystem _metaData = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NameModifierComponent, EntityRenamedEvent>(OnEntityRenamed);
}
private void OnEntityRenamed(Entity<NameModifierComponent> entity, ref EntityRenamedEvent args)
{
SetBaseName((entity, entity.Comp), args.NewName);
RefreshNameModifiers((entity, entity.Comp));
}
private void SetBaseName(Entity<NameModifierComponent> entity, string name)
{
if (name == entity.Comp.BaseName)
return;
// Set the base name to the new name
entity.Comp.BaseName = name;
Dirty(entity);
}
/// <summary>
/// Raises a <see cref="RefreshNameModifiersEvent"/> to gather modifiers and
/// updates the entity's name to its base name with modifiers applied.
/// This will add a <see cref="NameModifierComponent"/> if any modifiers are added.
/// </summary>
/// <remarks>
/// Call this to update the entity's name when adding or removing a modifier.
/// </remarks>
public void RefreshNameModifiers(Entity<NameModifierComponent?> entity)
{
var meta = MetaData(entity);
var baseName = meta.EntityName;
if (Resolve(entity, ref entity.Comp, logMissing: false))
baseName = entity.Comp.BaseName;
// Raise an event to get any modifiers
// If the entity already has the component, use its BaseName, otherwise use the entity's name from metadata
var modifierEvent = new RefreshNameModifiersEvent(baseName);
RaiseLocalEvent(entity, ref modifierEvent);
// Nothing added a modifier, so we can just use the base name
if (modifierEvent.ModifierCount == 0)
{
// If the entity doesn't have the component, we're done
if (entity.Comp == null)
return;
// Restore the base name
_metaData.SetEntityName(entity, entity.Comp.BaseName, meta, raiseEvents: false);
// The component isn't doing anything anymore, so remove it
RemComp<NameModifierComponent>(entity);
return;
}
// We have at least one modifier, so we need to apply it to the entity.
// Get the final name with modifiers applied
var modifiedName = modifierEvent.GetModifiedName();
// Add the component if needed, and initialize it with the base name
if (!EnsureComp<NameModifierComponent>(entity, out var comp))
SetBaseName((entity, comp), meta.EntityName);
// Set the entity's name with modifiers applied
_metaData.SetEntityName(entity, modifiedName, meta, raiseEvents: false);
}
}
/// <summary>
/// Raised on an entity when <see cref="NameModifierSystem.RefreshNameModifiers"/> is called.
/// Subscribe to this event and use its methods to add modifiers to the entity's name.
/// </summary>
[ByRefEvent]
public sealed class RefreshNameModifiersEvent : IInventoryRelayEvent
{
/// <summary>
/// The entity's name without any modifiers applied.
/// If you want to base a modifier on the entity's name, use
/// this so you don't include other modifiers.
/// </summary>
public readonly string BaseName;
private readonly List<(LocId LocId, int Priority, (string, object)[] ExtraArgs)> _modifiers = [];
/// <inheritdoc/>
public SlotFlags TargetSlots => ~SlotFlags.POCKET;
/// <summary>
/// How many modifiers have been added to this event.
/// </summary>
public int ModifierCount => _modifiers.Count;
public RefreshNameModifiersEvent(string baseName)
{
BaseName = baseName;
}
/// <summary>
/// Adds a modifier to the entity's name.
/// The original name will be passed to Fluent as <c>$baseName</c> along with any <paramref name="extraArgs"/>.
/// Modifiers with a higher <paramref name="priority"/> will be applied later.
/// </summary>
public void AddModifier(LocId locId, int priority = 0, params (string, object)[] extraArgs)
{
_modifiers.Add((locId, priority, extraArgs));
}
/// <summary>
/// Returns the final name with all modifiers applied.
/// </summary>
public string GetModifiedName()
{
// Start out with the entity's name name
var name = BaseName;
// Iterate through all the modifiers in priority order
foreach (var modifier in _modifiers.OrderBy(n => n.Priority))
{
// Grab any extra args needed by the Loc string
var args = modifier.ExtraArgs;
// Add the current version of the entity name as an arg
Array.Resize(ref args, args.Length + 1);
args[^1] = ("baseName", name);
// Resolve the Loc string and use the result as the base in the next iteration.
name = Loc.GetString(modifier.LocId, args);
}
return name;
}
}

View File

@@ -35,10 +35,4 @@ public sealed partial class InfantComponent : Component
[DataField("infantEndTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan InfantEndTime;
/// <summary>
/// The entity's name before the "baby" prefix is added.
/// </summary>
[DataField("originalName")]
public string OriginalName = string.Empty;
}

View File

@@ -62,7 +62,7 @@ public sealed partial class OpenableSystem : EntitySystem
if (args.Handled || !ent.Comp.OpenableByHand)
return;
args.Handled = TryToggle(ent, args.User);
args.Handled = TryOpen(ent, ent, args.User);
}
private void OnActivated(Entity<OpenableComponent> ent, ref ActivateInWorldEvent args)

View File

@@ -690,15 +690,15 @@ namespace Content.Shared.Preferences
return profile;
}
public RoleLoadout GetLoadoutOrDefault(string id, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
public RoleLoadout GetLoadoutOrDefault(string id, ICommonSession? session, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
{
if (!_loadouts.TryGetValue(id, out var loadout))
{
loadout = new RoleLoadout(id);
loadout.SetDefault(protoManager, force: true);
loadout.SetDefault(this, session, protoManager, force: true);
}
loadout.SetDefault(protoManager);
loadout.SetDefault(this, session, protoManager);
return loadout;
}

View File

@@ -13,17 +13,20 @@ public sealed partial class GroupLoadoutEffect : LoadoutEffect
[DataField(required: true)]
public ProtoId<LoadoutEffectGroupPrototype> Proto;
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession? session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto);
var reasons = new List<string>();
foreach (var effect in effectsProto.Effects)
{
if (!effect.Validate(profile, loadout, session, collection, out reason))
return false;
if (effect.Validate(profile, loadout, session, collection, out reason))
continue;
reasons.Add(reason.ToMarkup());
}
reason = null;
return true;
reason = reasons.Count == 0 ? null : FormattedMessage.FromMarkup(string.Join('\n', reasons));
return reason == null;
}
}

View File

@@ -15,8 +15,14 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
[DataField(required: true)]
public JobRequirement Requirement = default!;
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession? session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
if (session == null)
{
reason = FormattedMessage.Empty;
return true;
}
var manager = collection.Resolve<ISharedPlaytimeManager>();
var playtimes = manager.GetPlayTimes(session);
return JobRequirements.TryRequirementMet(Requirement, playtimes, out reason,

View File

@@ -13,7 +13,7 @@ public abstract partial class LoadoutEffect
public abstract bool Validate(
HumanoidCharacterProfile profile,
RoleLoadout loadout,
ICommonSession session,
ICommonSession? session,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason);

View File

@@ -13,7 +13,7 @@ public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
public override bool Validate(
HumanoidCharacterProfile profile,
RoleLoadout loadout,
ICommonSession session,
ICommonSession? session,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason)
{

View File

@@ -11,7 +11,7 @@ public sealed partial class SpeciesLoadoutEffect : LoadoutEffect
[DataField(required: true)]
public List<ProtoId<SpeciesPrototype>> Species = new();
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection,
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession? session, IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason)
{
if (Species.Contains(profile.Species))

View File

@@ -7,8 +7,25 @@ namespace Content.Shared.Preferences.Loadouts;
/// Specifies the selected prototype and custom data for a loadout.
/// </summary>
[Serializable, NetSerializable, DataDefinition]
public sealed partial class Loadout
public sealed partial class Loadout : IEquatable<Loadout>
{
[DataField]
public ProtoId<LoadoutPrototype> Prototype;
public bool Equals(Loadout? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Prototype.Equals(other.Prototype);
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is Loadout other && Equals(other);
}
public override int GetHashCode()
{
return Prototype.GetHashCode();
}
}

View File

@@ -166,12 +166,15 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
/// <summary>
/// Resets the selected loadouts to default if no data is present.
/// </summary>
/// <param name="force">Clear existing data first</param>
public void SetDefault(IPrototypeManager protoManager, bool force = false)
public void SetDefault(HumanoidCharacterProfile? profile, ICommonSession? session, IPrototypeManager protoManager, bool force = false)
{
if (profile == null)
return;
if (force)
SelectedLoadouts.Clear();
var collection = IoCManager.Instance!;
var roleProto = protoManager.Index(Role);
for (var i = roleProto.Groups.Count - 1; i >= 0; i--)
@@ -184,14 +187,28 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
if (SelectedLoadouts.ContainsKey(group))
continue;
SelectedLoadouts[group] = new List<Loadout>();
var loadouts = new List<Loadout>();
SelectedLoadouts[group] = loadouts;
if (groupProto.MinLimit > 0)
{
// Apply any loadouts we can.
for (var j = 0; j < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); j++)
{
AddLoadout(group, groupProto.Loadouts[j], protoManager);
if (!protoManager.TryIndex(groupProto.Loadouts[j], out var loadoutProto))
continue;
var defaultLoadout = new Loadout()
{
Prototype = loadoutProto.ID,
};
// Not valid so don't default to it anyway.
if (!IsValid(profile, session, defaultLoadout.Prototype, collection, out _))
continue;
loadouts.Add(defaultLoadout);
Apply(loadoutProto);
}
}
}
@@ -200,7 +217,7 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
/// <summary>
/// Returns whether a loadout is valid or not.
/// </summary>
public bool IsValid(HumanoidCharacterProfile profile, ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
public bool IsValid(HumanoidCharacterProfile profile, ICommonSession? session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
@@ -295,7 +312,25 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Role.Equals(other.Role) && SelectedLoadouts.SequenceEqual(other.SelectedLoadouts) && Points == other.Points;
if (!Role.Equals(other.Role) ||
SelectedLoadouts.Count != other.SelectedLoadouts.Count ||
Points != other.Points)
{
return false;
}
// Tried using SequenceEqual but it stinky so.
foreach (var (key, value) in SelectedLoadouts)
{
if (!other.SelectedLoadouts.TryGetValue(key, out var otherValue) ||
!otherValue.SequenceEqual(value))
{
return false;
}
}
return true;
}
public override bool Equals(object? obj)

View File

@@ -36,7 +36,7 @@ public sealed partial class RoboticsConsoleComponent : Component
/// Radio message sent when destroying a borg.
/// </summary>
[DataField]
public LocId DestroyMessage = "robotics-console-cyborg-destroyed";
public LocId DestroyMessage = "robotics-console-cyborg-destroying";
/// <summary>
/// Cooldown on destroying borgs to prevent complete abuse.

View File

@@ -97,6 +97,13 @@ public record struct CyborgControlData
[DataField]
public bool HasBrain;
/// <summary>
/// Whether the borg can currently be disabled if the brain is installed,
/// if on cooldown then can't queue up multiple disables.
/// </summary>
[DataField]
public bool CanDisable;
/// <summary>
/// When this cyborg's data will be deleted.
/// Set by the console when receiving the packet.
@@ -104,7 +111,7 @@ public record struct CyborgControlData
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan Timeout = TimeSpan.Zero;
public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain)
public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain, bool canDisable)
{
ChassisSprite = chassisSprite;
ChassisName = chassisName;
@@ -112,6 +119,7 @@ public record struct CyborgControlData
Charge = charge;
ModuleCount = moduleCount;
HasBrain = hasBrain;
CanDisable = canDisable;
}
}

View File

@@ -23,12 +23,25 @@ public sealed partial class BorgTransponderComponent : Component
public string Name = string.Empty;
/// <summary>
/// Popup shown to everyone when a borg is disabled.
/// Popup shown to everyone after a borg is disabled.
/// Gets passed a string "name".
/// </summary>
[DataField]
public LocId DisabledPopup = "borg-transponder-disabled-popup";
/// <summary>
/// Popup shown to the borg when it is being disabled.
/// </summary>
[DataField]
public LocId DisablingPopup = "borg-transponder-disabling-popup";
/// <summary>
/// Popup shown to everyone when a borg is being destroyed.
/// Gets passed a string "name".
/// </summary>
[DataField]
public LocId DestroyingPopup = "borg-transponder-destroying-popup";
/// <summary>
/// How long to wait between each broadcast.
/// </summary>
@@ -40,4 +53,28 @@ public sealed partial class BorgTransponderComponent : Component
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextBroadcast = TimeSpan.Zero;
/// <summary>
/// When to next disable the borg.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? NextDisable;
/// <summary>
/// How long to wait to disable the borg after RD has ordered it.
/// </summary>
[DataField]
public TimeSpan DisableDelay = TimeSpan.FromSeconds(5);
/// <summary>
/// Pretend that the borg cannot be disabled due to being on delay.
/// </summary>
[DataField]
public bool FakeDisabling;
/// <summary>
/// Pretend that the borg has no brain inserted.
/// </summary>
[DataField]
public bool FakeDisabled;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Movement.Systems;
using Content.Shared.NameModifier.EntitySystems;
namespace Content.Shared.Zombies;
@@ -10,6 +11,7 @@ public abstract class SharedZombieSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<ZombieComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshSpeed);
SubscribeLocalEvent<ZombieComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
}
private void OnRefreshSpeed(EntityUid uid, ZombieComponent component, RefreshMovementSpeedModifiersEvent args)
@@ -17,4 +19,9 @@ public abstract class SharedZombieSystem : EntitySystem
var mod = component.ZombieMovementSpeedDebuff;
args.ModifySpeed(mod, mod);
}
private void OnRefreshNameModifiers(Entity<ZombieComponent> entity, ref RefreshNameModifiersEvent args)
{
args.AddModifier("zombie-name-prefix");
}
}

View File

@@ -62,12 +62,6 @@ public sealed partial class ZombieComponent : Component
[DataField("zombieRoleId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string ZombieRoleId = "Zombie";
/// <summary>
/// The EntityName of the humanoid to restore in case of cloning
/// </summary>
[DataField("beforeZombifiedEntityName"), ViewVariables(VVAccess.ReadOnly)]
public string BeforeZombifiedEntityName = string.Empty;
/// <summary>
/// The CustomBaseLayers of the humanoid to restore in case of cloning
/// </summary>