Merge branch 'master' into ion-storm-refactor

This commit is contained in:
metalgearsloth
2024-11-20 18:22:08 +11:00
committed by GitHub
150 changed files with 5700 additions and 2230 deletions

View File

@@ -33,6 +33,12 @@ public sealed partial class ToggleClothingComponent : Component
/// </summary>
[DataField]
public bool DisableOnUnequip;
/// <summary>
/// If true, the clothes must equip for adding action.
/// </summary>
[DataField]
public bool MustEquip = true;
}
/// <summary>

View File

@@ -39,8 +39,12 @@ public sealed class ToggleClothingSystem : EntitySystem
private void OnGetActions(Entity<ToggleClothingComponent> ent, ref GetItemActionsEvent args)
{
if (args.InHands && ent.Comp.MustEquip)
return;
var ev = new ToggleClothingCheckEvent(args.User);
RaiseLocalEvent(ent, ref ev);
if (!ev.Cancelled)
args.AddAction(ent.Comp.ActionEntity);
}

View File

@@ -3,12 +3,23 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Crayon
{
/// <summary>
/// Component holding the state of a crayon-like component
/// </summary>
[NetworkedComponent, ComponentProtoName("Crayon"), Access(typeof(SharedCrayonSystem))]
public abstract partial class SharedCrayonComponent : Component
{
/// <summary>
/// The ID of currently selected decal prototype that will be placed when the crayon is used
/// </summary>
public string SelectedState { get; set; } = string.Empty;
[DataField("color")] public Color Color;
/// <summary>
/// Color with which the crayon will draw
/// </summary>
[DataField("color")]
public Color Color;
[Serializable, NetSerializable]
public enum CrayonUiKey : byte
@@ -17,6 +28,9 @@ namespace Content.Shared.Crayon
}
}
/// <summary>
/// Used by the client to notify the server about the selected decal ID
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonSelectMessage : BoundUserInterfaceMessage
{
@@ -27,6 +41,9 @@ namespace Content.Shared.Crayon
}
}
/// <summary>
/// Sets the color of the crayon, used by Rainbow Crayon
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonColorMessage : BoundUserInterfaceMessage
{
@@ -37,13 +54,25 @@ namespace Content.Shared.Crayon
}
}
/// <summary>
/// Server to CLIENT. Notifies the BUI that a decal with given ID has been drawn.
/// Allows the client UI to advance forward in the client-only ephemeral queue,
/// preventing the crayon from becoming a magic text storage device.
/// </summary>
[Serializable, NetSerializable]
public enum CrayonVisuals
public sealed class CrayonUsedMessage : BoundUserInterfaceMessage
{
State,
Color
public readonly string DrawnDecal;
public CrayonUsedMessage(string drawn)
{
DrawnDecal = drawn;
}
}
/// <summary>
/// Component state, describes how many charges are left in the crayon in the near-hand UI
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonComponentState : ComponentState
{
@@ -60,10 +89,17 @@ namespace Content.Shared.Crayon
Capacity = capacity;
}
}
/// <summary>
/// The state of the crayon UI as sent by the server
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState
{
public string Selected;
/// <summary>
/// Whether or not the color can be selected
/// </summary>
public bool SelectableColor;
public Color Color;

View File

@@ -61,6 +61,12 @@ public sealed class DamageExamineSystem : EntitySystem
}
else
{
if (damageSpecifier.GetTotal() == FixedPoint2.Zero && !damageSpecifier.AnyPositive())
{
msg.AddMarkupOrThrow(Loc.GetString("damage-none"));
return msg;
}
msg.AddMarkupOrThrow(Loc.GetString("damage-examine-type", ("type", type)));
}

View File

@@ -96,6 +96,13 @@ public sealed class EmagSystem : EntitySystem
}
}
/// <summary>
/// Shows a popup to emag user (client side only!) and adds <see cref="EmaggedComponent"/> to the entity when handled
/// </summary>
/// <param name="UserUid">Emag user</param>
/// <param name="Handled">Did the emagging succeed? Causes a user-only popup to show on client side</param>
/// <param name="Repeatable">Can the entity be emagged more than once? Prevents adding of <see cref="EmaggedComponent"/></param>
/// <remarks>Needs to be handled in shared/client, not just the server, to actually show the emagging popup</remarks>
[ByRefEvent]
public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);

View File

@@ -66,5 +66,5 @@ public record struct GenerateDnaEvent()
/// <summary>
/// The generated DNA.
/// </summary>
public string DNA;
public required string DNA;
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Player;
namespace Content.Shared.GameTicking;
/// <summary>
/// Event raised broadcast before a player is spawned by the GameTicker.
/// You can use this event to spawn a player off-station on late-join but also at round start.
/// When this event is handled, the GameTicker will not perform its own player-spawning logic.
/// </summary>
[PublicAPI]
public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
{
public ICommonSession Player { get; }
public HumanoidCharacterProfile Profile { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public EntityUid Station { get; }
public PlayerBeforeSpawnEvent(ICommonSession player,
HumanoidCharacterProfile profile,
string? jobId,
bool lateJoin,
EntityUid station)
{
Player = player;
Profile = profile;
JobId = jobId;
LateJoin = lateJoin;
Station = station;
}
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Player;
namespace Content.Shared.GameTicking;
/// <summary>
/// Event raised both directed and broadcast when a player has been spawned by the GameTicker.
/// You can use this to handle people late-joining, or to handle people being spawned at round start.
/// Can be used to give random players a role, modify their equipment, etc.
/// </summary>
[PublicAPI]
public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
{
public EntityUid Mob { get; }
public ICommonSession Player { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public bool Silent { get; }
public EntityUid Station { get; }
public HumanoidCharacterProfile Profile { get; }
// Ex. If this is the 27th person to join, this will be 27.
public int JoinOrder { get; }
public PlayerSpawnCompleteEvent(EntityUid mob,
ICommonSession player,
string? jobId,
bool lateJoin,
bool silent,
int joinOrder,
EntityUid station,
HumanoidCharacterProfile profile)
{
Mob = mob;
Player = player;
JobId = jobId;
LateJoin = lateJoin;
Silent = silent;
Station = station;
Profile = profile;
JoinOrder = joinOrder;
}
}

View File

@@ -32,6 +32,18 @@ public sealed partial class ItemToggleComponent : Component
[DataField]
public bool OnUse = true;
/// <summary>
/// The localized text to display in the verb to activate.
/// </summary>
[DataField]
public string VerbToggleOn = "item-toggle-activate";
/// <summary>
/// The localized text to display in the verb to de-activate.
/// </summary>
[DataField]
public string VerbToggleOff = "item-toggle-deactivate";
/// <summary>
/// Whether the item's toggle can be predicted by the client.
/// </summary>

View File

@@ -1,18 +0,0 @@
using Content.Shared.Item.ItemToggle;
using Robust.Shared.GameStates;
namespace Content.Shared.Item.ItemToggle.Components;
/// <summary>
/// Adds a verb for toggling something, requires <see cref="ItemToggleComponent"/>.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(ToggleVerbSystem))]
public sealed partial class ToggleVerbComponent : Component
{
/// <summary>
/// Text the verb will have.
/// Gets passed "entity" as the entity's identity string.
/// </summary>
[DataField(required: true)]
public LocId Text = string.Empty;
}

View File

@@ -78,7 +78,7 @@ public sealed class ItemToggleSystem : EntitySystem
args.Verbs.Add(new ActivationVerb()
{
Text = !ent.Comp.Activated ? Loc.GetString("item-toggle-activate") : Loc.GetString("item-toggle-deactivate"),
Text = !ent.Comp.Activated ? Loc.GetString(ent.Comp.VerbToggleOn) : Loc.GetString(ent.Comp.VerbToggleOff),
Act = () =>
{
Toggle((ent.Owner, ent.Comp), user, predicted: ent.Comp.Predictable);

View File

@@ -1,34 +0,0 @@
using Content.Shared.IdentityManagement;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Verbs;
namespace Content.Shared.Item.ItemToggle;
/// <summary>
/// Adds a verb for toggling something with <see cref="ToggleVerbComponent"/>.
/// </summary>
public sealed class ToggleVerbSystem : EntitySystem
{
[Dependency] private readonly ItemToggleSystem _toggle = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ToggleVerbComponent, GetVerbsEvent<ActivationVerb>>(OnGetVerbs);
}
private void OnGetVerbs(Entity<ToggleVerbComponent> ent, ref GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
var name = Identity.Entity(ent, EntityManager);
var user = args.User;
args.Verbs.Add(new ActivationVerb()
{
Text = Loc.GetString(ent.Comp.Text, ("entity", name)),
Act = () => _toggle.Toggle(ent.Owner, user)
});
}
}

View File

@@ -0,0 +1,23 @@
using Content.Shared.Actions;
using Content.Shared.Storage;
using Robust.Shared.Audio;
namespace Content.Shared.Magic.Events;
public sealed partial class RandomGlobalSpawnSpellEvent : InstantActionEvent, ISpeakSpell
{
/// <summary>
/// The list of prototypes this spell can spawn, will select one randomly
/// </summary>
[DataField]
public List<EntitySpawnEntry> Spawns = new();
/// <summary>
/// Sound that will play globally when cast
/// </summary>
[DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Magic/staff_animation.ogg");
[DataField]
public string? Speech { get; private set; }
}

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Content.Shared.Actions;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
@@ -7,12 +7,17 @@ using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.Magic.Components;
using Content.Shared.Magic.Events;
using Content.Shared.Maps;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Speech.Muting;
@@ -20,6 +25,7 @@ using Content.Shared.Storage;
using Content.Shared.Tag;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
@@ -53,6 +59,9 @@ public abstract class SharedMagicSystem : EntitySystem
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
@@ -67,6 +76,7 @@ public abstract class SharedMagicSystem : EntitySystem
SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
SubscribeLocalEvent<ChargeSpellEvent>(OnChargeSpell);
SubscribeLocalEvent<RandomGlobalSpawnSpellEvent>(OnRandomGlobalSpawnSpell);
// Spell wishlist
// A wishlish of spells that I'd like to implement or planning on implementing in a future PR
@@ -501,6 +511,37 @@ public abstract class SharedMagicSystem : EntitySystem
_gunSystem.UpdateBasicEntityAmmoCount(wand.Value, basicAmmoComp.Count.Value + ev.Charge, basicAmmoComp);
}
// End Charge Spells
#endregion
#region Global Spells
private void OnRandomGlobalSpawnSpell(RandomGlobalSpawnSpellEvent ev)
{
if (!_net.IsServer || ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || ev.Spawns is not { } spawns)
return;
ev.Handled = true;
Speak(ev);
var allHumans = _mind.GetAliveHumans();
foreach (var human in allHumans)
{
if (!human.Comp.OwnedEntity.HasValue)
continue;
var ent = human.Comp.OwnedEntity.Value;
var mapCoords = _transform.GetMapCoordinates(ent);
foreach (var spawn in EntitySpawnCollection.GetSpawns(spawns, _random))
{
var spawned = Spawn(spawn, mapCoords);
_hands.PickupOrDrop(ent, spawned);
}
}
_audio.PlayGlobal(ev.Sound, ev.Performer);
}
#endregion
// End Spells
#endregion

View File

@@ -532,22 +532,19 @@ public abstract class SharedMindSystem : EntitySystem
/// <summary>
/// Returns a list of every living humanoid player's minds, except for a single one which is exluded.
/// </summary>
public List<EntityUid> GetAliveHumansExcept(EntityUid exclude)
public HashSet<Entity<MindComponent>> GetAliveHumans(EntityUid? exclude = null)
{
var mindQuery = EntityQuery<MindComponent>();
var allHumans = new List<EntityUid>();
var allHumans = new HashSet<Entity<MindComponent>>();
// HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen
var query = EntityQueryEnumerator<MindContainerComponent, MobStateComponent, HumanoidAppearanceComponent>();
while (query.MoveNext(out var uid, out var mc, out var mobState, out _))
var query = EntityQueryEnumerator<MobStateComponent, HumanoidAppearanceComponent>();
while (query.MoveNext(out var uid, out var mobState, out _))
{
// the player needs to have a mind and not be the excluded one
if (mc.Mind == null || mc.Mind == exclude)
// the player needs to have a mind and not be the excluded one +
// the player has to be alive
if (!TryGetMind(uid, out var mind, out var mindComp) || mind == exclude || !_mobState.IsAlive(uid, mobState))
continue;
// the player has to be alive
if (_mobState.IsAlive(uid, mobState))
allHumans.Add(mc.Mind.Value);
allHumans.Add(new Entity<MindComponent>(mind, mindComp));
}
return allHumans;

View File

@@ -26,10 +26,10 @@ public abstract partial class SharedSalvageSystem : EntitySystem
[ValidatePrototypeId<SalvageLootPrototype>]
public const string ExpeditionsLootProto = "SalvageLoot";
public static string GetFTLName(DatasetPrototype dataset, int seed)
public string GetFTLName(LocalizedDatasetPrototype dataset, int seed)
{
var random = new System.Random(seed);
return $"{dataset.Values[random.Next(dataset.Values.Count)]}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}";
return $"{Loc.GetString(dataset.Values[random.Next(dataset.Values.Count)])}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}";
}
public SalvageMission GetMission(SalvageDifficultyPrototype difficulty, int seed)

View File

@@ -56,6 +56,7 @@ public sealed class StationAiVisionSystem : EntitySystem
EntManager = EntityManager,
Maps = _maps,
System = this,
VisibleTiles = _singleTiles,
};
}
@@ -278,7 +279,7 @@ public sealed class StationAiVisionSystem : EntitySystem
/// </summary>
private record struct SeedJob() : IRobustJob
{
public StationAiVisionSystem System;
public required StationAiVisionSystem System;
public Entity<MapGridComponent> Grid;
public Box2 ExpandedBounds;
@@ -293,14 +294,14 @@ public sealed class StationAiVisionSystem : EntitySystem
{
public int BatchSize => 1;
public IEntityManager EntManager;
public SharedMapSystem Maps;
public StationAiVisionSystem System;
public required IEntityManager EntManager;
public required SharedMapSystem Maps;
public required StationAiVisionSystem System;
public Entity<MapGridComponent> Grid;
public List<Entity<StationAiVisionComponent>> Data = new();
public HashSet<Vector2i> VisibleTiles;
public required HashSet<Vector2i> VisibleTiles;
public readonly List<Dictionary<Vector2i, int>> Vis1 = new();
public readonly List<Dictionary<Vector2i, int>> Vis2 = new();

View File

@@ -0,0 +1,69 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Shared.Physics;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.GameStates;
namespace Content.Shared.Singularity.Components;
[RegisterComponent, AutoGenerateComponentPause, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class SingularityGeneratorComponent : Component
{
/// <summary>
/// The amount of power this generator has accumulated.
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetPower"/>
/// </summary>
[DataField]
public float Power = 0;
/// <summary>
/// The power threshold at which this generator will spawn a singularity.
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetThreshold"/>
/// </summary>
[DataField]
public float Threshold = 16;
/// <summary>
/// Allows the generator to ignore all the failsafe stuff, e.g. when emagged
/// </summary>
[DataField, AutoNetworkedField]
public bool FailsafeDisabled = false;
/// <summary>
/// Maximum distance at which the generator will check for a field at
/// </summary>
[DataField]
public float FailsafeDistance = 16;
/// <summary>
/// The prototype ID used to spawn a singularity.
/// </summary>
[DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? SpawnPrototype = "Singularity";
/// <summary>
/// The masks the raycast should not go through
/// </summary>
[DataField]
public int CollisionMask = (int)CollisionGroup.FullTileMask;
/// <summary>
/// Message to use when there's no containment field on cardinal directions
/// </summary>
[DataField]
public LocId ContainmentFailsafeMessage = "comp-generator-failsafe";
/// <summary>
/// For how long the failsafe will cause the generator to stop working and not issue a failsafe warning
/// </summary>
[DataField]
public TimeSpan FailsafeCooldown = TimeSpan.FromSeconds(10);
/// <summary>
/// How long until the generator can issue a failsafe warning again
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextFailsafe = TimeSpan.Zero;
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Emag.Systems;
using Content.Shared.Popups;
using Content.Shared.Singularity.Components;
namespace Content.Shared.Singularity.EntitySystems;
/// <summary>
/// Shared part of SingularitySingularityGeneratorSystem
/// </summary>
public abstract class SharedSingularityGeneratorSystem : EntitySystem
{
#region Dependencies
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
#endregion Dependencies
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SingularityGeneratorComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnEmagged(EntityUid uid, SingularityGeneratorComponent component, ref GotEmaggedEvent args)
{
component.FailsafeDisabled = true;
args.Handled = true;
}
}

View File

@@ -129,7 +129,6 @@ public abstract class SharedStorageSystem : EntitySystem
SubscribeAllEvent<StorageInteractWithItemEvent>(OnInteractWithItem);
SubscribeAllEvent<StorageSetItemLocationEvent>(OnSetItemLocation);
SubscribeAllEvent<StorageInsertItemIntoLocationEvent>(OnInsertItemIntoLocation);
SubscribeAllEvent<StorageRemoveItemEvent>(OnRemoveItem);
SubscribeAllEvent<StorageSaveItemLocationEvent>(OnSaveItemLocation);
SubscribeLocalEvent<StorageComponent, GotReclaimedEvent>(OnReclaimed);
@@ -639,19 +638,6 @@ public abstract class SharedStorageSystem : EntitySystem
TrySetItemStorageLocation(item!, storage!, msg.Location);
}
private void OnRemoveItem(StorageRemoveItemEvent msg, EntitySessionEventArgs args)
{
if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item))
return;
_adminLog.Add(
LogType.Storage,
LogImpact.Low,
$"{ToPrettyString(player):player} is removing {ToPrettyString(item):item} from {ToPrettyString(storage):storage}");
TransformSystem.DropNextTo(item.Owner, player.Owner);
Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams);
}
private void OnInsertItemIntoLocation(StorageInsertItemIntoLocationEvent msg, EntitySessionEventArgs args)
{
if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item, held: true))

View File

@@ -169,20 +169,6 @@ namespace Content.Shared.Storage
}
}
[Serializable, NetSerializable]
public sealed class StorageRemoveItemEvent : EntityEventArgs
{
public readonly NetEntity ItemEnt;
public readonly NetEntity StorageEnt;
public StorageRemoveItemEvent(NetEntity itemEnt, NetEntity storageEnt)
{
ItemEnt = itemEnt;
StorageEnt = storageEnt;
}
}
[Serializable, NetSerializable]
public sealed class StorageInsertItemIntoLocationEvent : EntityEventArgs
{

View File

@@ -39,7 +39,8 @@ public partial class ListingData : IEquatable<ListingData>
other.Categories,
other.OriginalCost,
other.RestockTime,
other.DiscountDownTo
other.DiscountDownTo,
other.DisableRefund
)
{
@@ -63,7 +64,8 @@ public partial class ListingData : IEquatable<ListingData>
HashSet<ProtoId<StoreCategoryPrototype>> categories,
IReadOnlyDictionary<ProtoId<CurrencyPrototype>, FixedPoint2> originalCost,
TimeSpan restockTime,
Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> dataDiscountDownTo
Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> dataDiscountDownTo,
bool disableRefund
)
{
Name = name;
@@ -84,6 +86,7 @@ public partial class ListingData : IEquatable<ListingData>
OriginalCost = originalCost;
RestockTime = restockTime;
DiscountDownTo = new Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2>(dataDiscountDownTo);
DisableRefund = disableRefund;
}
[ViewVariables]
@@ -194,6 +197,12 @@ public partial class ListingData : IEquatable<ListingData>
[DataField]
public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> DiscountDownTo = new();
/// <summary>
/// Whether or not to disable refunding for the store when the listing is purchased from it.
/// </summary>
[DataField]
public bool DisableRefund = false;
public bool Equals(ListingData? listing)
{
if (listing == null)
@@ -287,7 +296,8 @@ public sealed partial class ListingDataWithCostModifiers : ListingData
listingData.Categories,
listingData.OriginalCost,
listingData.RestockTime,
listingData.DiscountDownTo
listingData.DiscountDownTo,
listingData.DisableRefund
)
{
}