Merge remote-tracking branch 'refs/remotes/upstream/master' into ed-13-05-2024-upstream

# Conflicts:
#	Content.Shared/Lock/LockSystem.cs
#	Resources/Prototypes/Maps/oasis.yml
This commit is contained in:
Ed
2024-05-13 18:32:59 +03:00
624 changed files with 43352 additions and 25174 deletions

View File

@@ -65,7 +65,7 @@ namespace Content.Shared.APC
/// Bitmask for the full state for a given APC lock indicator.
/// </summary>
All = (Lock),
/// <summary>
/// The log 2 width in bits of the bitfields indicating the status of an APC lock indicator.
/// Used for bit shifting operations (Mask for the state for indicator i is (All << (i << LogWidth))).
@@ -175,7 +175,7 @@ namespace Content.Shared.APC
}
[Serializable, NetSerializable]
public sealed class ApcBoundInterfaceState : BoundUserInterfaceState
public sealed class ApcBoundInterfaceState : BoundUserInterfaceState, IEquatable<ApcBoundInterfaceState>
{
public readonly bool MainBreaker;
public readonly bool HasAccess;
@@ -191,6 +191,27 @@ namespace Content.Shared.APC
ApcExternalPower = apcExternalPower;
Charge = charge;
}
public bool Equals(ApcBoundInterfaceState? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return MainBreaker == other.MainBreaker &&
HasAccess == other.HasAccess &&
Power == other.Power &&
ApcExternalPower == other.ApcExternalPower &&
MathHelper.CloseTo(Charge, other.Charge);
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is ApcBoundInterfaceState other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(MainBreaker, HasAccess, Power, (int) ApcExternalPower, Charge);
}
}
[Serializable, NetSerializable]
@@ -198,7 +219,7 @@ namespace Content.Shared.APC
{
}
public enum ApcExternalPowerState
public enum ApcExternalPowerState : byte
{
None,
Low,
@@ -206,7 +227,7 @@ namespace Content.Shared.APC
}
[NetSerializable, Serializable]
public enum ApcUiKey
public enum ApcUiKey : byte
{
Key,
}

View File

@@ -40,4 +40,10 @@ public sealed partial class IdCardComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool BypassLogging;
[DataField]
public LocId NameLocId = "access-id-card-component-owner-name-job-title-text";
[DataField]
public LocId FullNameLocId = "access-id-card-component-owner-full-name-job-title-text";
}

View File

@@ -3,8 +3,6 @@ using Content.Shared.Containers.ItemSlots;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Prototypes;
namespace Content.Shared.Access.Components;

View File

@@ -0,0 +1,6 @@
using Content.Shared.Access.Systems;
namespace Content.Shared.Access.Components;
[RegisterComponent, Access(typeof(IdExaminableSystem))]
public sealed partial class IdExaminableComponent : Component;

View File

@@ -153,7 +153,7 @@ public sealed class AccessReaderSystem : EntitySystem
return IsAllowedInternal(access, stationKeys, reader);
if (!_containerSystem.TryGetContainer(target, reader.ContainerAccessProvider, out var container))
return false;
return Paused(target); // when mapping, containers with electronics arent spawned
foreach (var entity in container.ContainedEntities)
{

View File

@@ -0,0 +1,80 @@
using Content.Shared.Access.Components;
using Content.Shared.Examine;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared.Access.Systems;
public sealed class IdExaminableSystem : EntitySystem
{
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IdExaminableComponent, GetVerbsEvent<ExamineVerb>>(OnGetExamineVerbs);
}
private void OnGetExamineVerbs(EntityUid uid, IdExaminableComponent component, GetVerbsEvent<ExamineVerb> args)
{
var detailsRange = _examineSystem.IsInDetailsRange(args.User, uid);
var info = GetMessage(uid);
var verb = new ExamineVerb()
{
Act = () =>
{
var markup = FormattedMessage.FromMarkup(info);
_examineSystem.SendExamineTooltip(args.User, uid, markup, false, false);
},
Text = Loc.GetString("id-examinable-component-verb-text"),
Category = VerbCategory.Examine,
Disabled = !detailsRange,
Message = detailsRange ? null : Loc.GetString("id-examinable-component-verb-disabled"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/character.svg.192dpi.png"))
};
args.Verbs.Add(verb);
}
public string GetMessage(EntityUid uid)
{
return GetInfo(uid) ?? Loc.GetString("id-examinable-component-verb-no-id");
}
public string? GetInfo(EntityUid uid)
{
if (_inventorySystem.TryGetSlotEntity(uid, "id", out var idUid))
{
// PDA
if (EntityManager.TryGetComponent(idUid, out PdaComponent? pda) &&
TryComp<IdCardComponent>(pda.ContainedId, out var id))
{
return GetNameAndJob(id);
}
// ID Card
if (EntityManager.TryGetComponent(idUid, out id))
{
return GetNameAndJob(id);
}
}
return null;
}
private string GetNameAndJob(IdCardComponent id)
{
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString(id.NameLocId,
("jobSuffix", jobSuffix))
: Loc.GetString(id.FullNameLocId,
("fullName", id.FullName),
("jobSuffix", jobSuffix));
return val;
}
}

View File

@@ -207,9 +207,9 @@ public abstract class SharedIdCardSystem : EntitySystem
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
? Loc.GetString(id.NameLocId,
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
: Loc.GetString(id.FullNameLocId,
("fullName", id.FullName),
("jobSuffix", jobSuffix));
_metaSystem.SetEntityName(uid, val);

View File

@@ -169,8 +169,16 @@ namespace Content.Shared.ActionBlocker
public bool CanAttack(EntityUid uid, EntityUid? target = null, Entity<MeleeWeaponComponent>? weapon = null, bool disarm = false)
{
// If target is in a container can we attack
if (target != null && _container.IsEntityInContainer(target.Value))
{
return false;
}
_container.TryGetOuterContainer(uid, Transform(uid), out var outerContainer);
if (target != null && target != outerContainer?.Owner && _container.IsEntityInContainer(uid))
// If we're in a container can we attack the target.
if (target != null && target != outerContainer?.Owner && _container.IsEntityInContainer(uid))
{
var containerEv = new CanAttackFromContainerEvent(uid, target);
RaiseLocalEvent(uid, containerEv);

View File

@@ -155,4 +155,9 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs
/// The user performing the action.
/// </summary>
public EntityUid Performer;
/// <summary>
/// The action the event belongs to.
/// </summary>
public EntityUid Action;
}

View File

@@ -1,5 +1,4 @@
using Content.Shared.Mobs;
using Robust.Shared.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -25,6 +24,11 @@ public abstract partial class BaseActionComponent : Component
/// </summary>
[DataField("iconOn")] public SpriteSpecifier? IconOn;
/// <summary>
/// For toggle actions only, background to show when toggled on.
/// </summary>
[DataField] public SpriteSpecifier? BackgroundOn;
/// <summary>
/// If not null, this color will modulate the action icon color.
/// </summary>

View File

@@ -0,0 +1,8 @@
namespace Content.Shared.Actions.Events;
/// <summary>
/// Raised on the action entity when it is used and <see cref="BaseActionEvent.Handled"/>.
/// </summary>
/// <param name="Performer">The entity that performed this action.</param>
[ByRefEvent]
public readonly record struct ActionPerformedEvent(EntityUid Performer);

View File

@@ -144,9 +144,6 @@ public abstract class SharedActionsSystem : EntitySystem
public void SetCooldown(EntityUid? actionId, TimeSpan start, TimeSpan end)
{
if (actionId == null)
return;
if (!TryGetActionData(actionId, out var action))
return;
@@ -162,9 +159,6 @@ public abstract class SharedActionsSystem : EntitySystem
public void ClearCooldown(EntityUid? actionId)
{
if (actionId == null)
return;
if (!TryGetActionData(actionId, out var action))
return;
@@ -175,6 +169,27 @@ public abstract class SharedActionsSystem : EntitySystem
Dirty(actionId.Value, action);
}
/// <summary>
/// Sets the cooldown for this action only if it is bigger than the one it already has.
/// </summary>
public void SetIfBiggerCooldown(EntityUid? actionId, TimeSpan? cooldown)
{
if (cooldown == null ||
cooldown.Value <= TimeSpan.Zero ||
!TryGetActionData(actionId, out var action))
{
return;
}
var start = GameTiming.CurTime;
var end = start + cooldown;
if (action.Cooldown?.End > end)
return;
action.Cooldown = (start, end.Value);
Dirty(actionId.Value, action);
}
public void StartUseDelay(EntityUid? actionId)
{
if (actionId == null)
@@ -438,7 +453,10 @@ public abstract class SharedActionsSystem : EntitySystem
}
if (performEvent != null)
{
performEvent.Performer = user;
performEvent.Action = actionEnt;
}
// All checks passed. Perform the action!
PerformAction(user, component, actionEnt, action, performEvent, curTime);
@@ -551,13 +569,12 @@ public abstract class SharedActionsSystem : EntitySystem
handled = actionEvent.Handled;
}
_audio.PlayPredicted(action.Sound, performer,predicted ? performer : null);
handled |= action.Sound != null;
if (!handled)
return; // no interaction occurred.
// reduce charges, start cooldown, and mark as dirty (if required).
// play sound, reduce charges, start cooldown, and mark as dirty (if required).
_audio.PlayPredicted(action.Sound, performer,predicted ? performer : null);
var dirty = toggledBefore == action.Toggled;
@@ -580,6 +597,9 @@ public abstract class SharedActionsSystem : EntitySystem
if (dirty && component != null)
Dirty(performer, component);
var ev = new ActionPerformedEvent(performer);
RaiseLocalEvent(actionId, ref ev);
}
#endregion

View File

@@ -2,8 +2,13 @@
namespace Content.Shared.Administration;
[RegisterComponent, Access(typeof(AdminFrozenSystem))]
[NetworkedComponent]
[RegisterComponent, Access(typeof(SharedAdminFrozenSystem))]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AdminFrozenComponent : Component
{
/// <summary>
/// Whether the player is also muted.
/// </summary>
[DataField, AutoNetworkedField]
public bool Muted;
}

View File

@@ -1,15 +1,17 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Emoting;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Movement.Pulling.Systems;
using Content.Shared.Speech;
using Content.Shared.Throwing;
namespace Content.Shared.Administration;
public sealed class AdminFrozenSystem : EntitySystem
public abstract class SharedAdminFrozenSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly PullingSystem _pulling = default!;
@@ -28,6 +30,16 @@ public sealed class AdminFrozenSystem : EntitySystem
SubscribeLocalEvent<AdminFrozenComponent, PullAttemptEvent>(OnPullAttempt);
SubscribeLocalEvent<AdminFrozenComponent, AttackAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, ChangeDirectionAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, EmoteAttemptEvent>(OnEmoteAttempt);
SubscribeLocalEvent<AdminFrozenComponent, SpeakAttemptEvent>(OnSpeakAttempt);
}
private void OnSpeakAttempt(EntityUid uid, AdminFrozenComponent component, SpeakAttemptEvent args)
{
if (!component.Muted)
return;
args.Cancel();
}
private void OnAttempt(EntityUid uid, AdminFrozenComponent component, CancellableEntityEventArgs args)
@@ -62,4 +74,10 @@ public sealed class AdminFrozenSystem : EntitySystem
{
_blocker.UpdateCanMove(uid);
}
private void OnEmoteAttempt(EntityUid uid, AdminFrozenComponent component, EmoteAttemptEvent args)
{
if (component.Muted)
args.Cancel();
}
}

View File

@@ -0,0 +1,10 @@
namespace Content.Shared.Armor;
/// <summary>
/// Used on outerclothing to allow use of suit storage
/// </summary>
[RegisterComponent]
public sealed partial class AllowSuitStorageComponent : Component
{
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Inventory;
namespace Content.Shared.Atmos;
/// <summary>
/// Raised on a burning entity to check its fire protection.
/// Damage taken is multiplied by the final amount, but not temperature.
/// TemperatureProtection is needed for that.
/// </summary>
[ByRefEvent]
public sealed class GetFireProtectionEvent : EntityEventArgs, IInventoryRelayEvent
{
public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET;
/// <summary>
/// What to multiply the fire damage by.
/// If this is 0 then it's ignored
/// </summary>
public float Multiplier;
public GetFireProtectionEvent()
{
Multiplier = 1f;
}
/// <summary>
/// Reduce fire damage taken by a percentage.
/// </summary>
public void Reduce(float by)
{
Multiplier -= by;
}
}

View File

@@ -322,15 +322,17 @@ public partial class SharedBodySystem
launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
}
}
var bodyTransform = Transform(bodyId);
if (TryComp<InventoryComponent>(bodyId, out var inventory))
{
foreach (var item in _inventory.GetHandOrInventoryEntities(bodyId))
{
SharedTransform.AttachToGridOrMap(item);
SharedTransform.DropNextTo(item, (bodyId, bodyTransform));
gibs.Add(item);
}
}
_audioSystem.PlayPredicted(gibSoundOverride, Transform(bodyId).Coordinates, null);
_audioSystem.PlayPredicted(gibSoundOverride, bodyTransform.Coordinates, null);
return gibs;
}
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Maps;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
namespace Content.Shared.CCVar
{
@@ -1378,6 +1379,49 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> GridFill =
CVarDef.Create("shuttle.grid_fill", true, CVar.SERVERONLY);
/// <summary>
/// Whether to automatically preloading grids by GridPreloaderSystem
/// </summary>
public static readonly CVarDef<bool> PreloadGrids =
CVarDef.Create("shuttle.preload_grids", true, CVar.SERVERONLY);
/// <summary>
/// How long the warmup time before FTL start should be.
/// </summary>
public static readonly CVarDef<float> FTLStartupTime =
CVarDef.Create("shuttle.startup_time", 5.5f, CVar.SERVERONLY);
/// <summary>
/// How long a shuttle spends in FTL.
/// </summary>
public static readonly CVarDef<float> FTLTravelTime =
CVarDef.Create("shuttle.travel_time", 20f, CVar.SERVERONLY);
/// <summary>
/// How long the final stage of FTL before arrival should be.
/// </summary>
public static readonly CVarDef<float> FTLArrivalTime =
CVarDef.Create("shuttle.arrival_time", 5f, CVar.SERVERONLY);
/// <summary>
/// How much time needs to pass before a shuttle can FTL again.
/// </summary>
public static readonly CVarDef<float> FTLCooldown =
CVarDef.Create("shuttle.cooldown", 10f, CVar.SERVERONLY);
/// <summary>
/// The maximum <see cref="PhysicsComponent.Mass"/> a grid can have before it becomes unable to FTL.
/// Any value equal to or less than zero will disable this check.
/// </summary>
public static readonly CVarDef<float> FTLMassLimit =
CVarDef.Create("shuttle.mass_limit", 300f, CVar.SERVERONLY);
/// <summary>
/// How long to knock down entities for if they aren't buckled when FTL starts and stops.
/// </summary>
public static readonly CVarDef<float> HyperspaceKnockdownTime =
CVarDef.Create("shuttle.hyperspace_knockdown_time", 5f, CVar.SERVERONLY);
/*
* Emergency
*/

View File

@@ -60,7 +60,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
[Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
[Dependency] protected readonly SharedHandsSystem Hands = default!;
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] protected readonly MetaDataSystem MetaData = default!;
[Dependency] protected readonly MetaDataSystem MetaDataSys = default!;
[Dependency] protected readonly INetManager NetManager = default!;
public override void Initialize()
@@ -1123,7 +1123,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
Dirty(uid, container);
return solution;
}
private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype)
{
var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
@@ -1135,7 +1135,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation);
MetaData.SetEntityName(uid, $"solution - {name}");
MetaDataSys.SetEntityName(uid, $"solution - {name}");
ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation);

View File

@@ -17,7 +17,6 @@ namespace Content.Shared.Chemistry.EntitySystems;
/// </summary>
public sealed class SolutionTransferSystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;

View File

@@ -6,6 +6,9 @@ namespace Content.Shared.Chemistry.Reaction
{
public interface ITileReaction
{
FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume);
FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager);
}
}

View File

@@ -142,7 +142,7 @@ namespace Content.Shared.Chemistry.Reagent
[DataField]
public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepWater", AudioParams.Default.WithVolume(6));
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume)
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityManager entityManager)
{
var removed = FixedPoint2.Zero;
@@ -151,7 +151,7 @@ namespace Content.Shared.Chemistry.Reagent
foreach (var reaction in TileReactions)
{
removed += reaction.TileReact(tile, this, reactVolume - removed);
removed += reaction.TileReact(tile, this, reactVolume - removed, entityManager);
if (removed > reactVolume)
throw new Exception("Removed more than we have!");

View File

@@ -20,6 +20,46 @@ namespace Content.Shared.Chemistry
{
ReagentDispenserDispenseAmount = amount;
}
/// <summary>
/// Create a new instance from interpreting a String as an integer,
/// throwing an exception if it is unable to parse.
/// </summary>
public ReagentDispenserSetDispenseAmountMessage(String s)
{
switch (s)
{
case "1":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U1;
break;
case "5":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U5;
break;
case "10":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U10;
break;
case "15":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U15;
break;
case "20":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U20;
break;
case "25":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U25;
break;
case "30":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U30;
break;
case "50":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U50;
break;
case "100":
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U100;
break;
default:
throw new Exception($"Cannot convert the string `{s}` into a valid ReagentDispenser DispenseAmount");
}
}
}
[Serializable, NetSerializable]
@@ -52,20 +92,30 @@ namespace Content.Shared.Chemistry
U100 = 100,
}
[Serializable, NetSerializable]
public sealed class ReagentInventoryItem(string storageSlotId, string reagentLabel, string storedAmount, Color reagentColor)
{
public string StorageSlotId = storageSlotId;
public string ReagentLabel = reagentLabel;
public string StoredAmount = storedAmount;
public Color ReagentColor = reagentColor;
}
[Serializable, NetSerializable]
public sealed class ReagentDispenserBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly ContainerInfo? OutputContainer;
public readonly NetEntity? OutputContainerEntity;
/// <summary>
/// A list of the reagents which this dispenser can dispense.
/// </summary>
public readonly List<KeyValuePair<string, KeyValuePair<string, string>>> Inventory;
public readonly List<ReagentInventoryItem> Inventory;
public readonly ReagentDispenserDispenseAmount SelectedDispenseAmount;
public ReagentDispenserBoundUserInterfaceState(ContainerInfo? outputContainer, NetEntity? outputContainerEntity, List<KeyValuePair<string, KeyValuePair<string, string>>> inventory, ReagentDispenserDispenseAmount selectedDispenseAmount)
public ReagentDispenserBoundUserInterfaceState(ContainerInfo? outputContainer, NetEntity? outputContainerEntity, List<ReagentInventoryItem> inventory, ReagentDispenserDispenseAmount selectedDispenseAmount)
{
OutputContainer = outputContainer;
OutputContainerEntity = outputContainerEntity;

View File

@@ -0,0 +1,17 @@
using Content.Shared.Clothing.EntitySystems;
namespace Content.Shared.Clothing.Components;
/// <summary>
/// Makes this clothing reduce fire damage when worn.
/// </summary>
[RegisterComponent, Access(typeof(FireProtectionSystem))]
public sealed partial class FireProtectionComponent : Component
{
/// <summary>
/// Percentage to reduce fire damage by, subtracted not multiplicative.
/// 0.25 means 25% less fire damage.
/// </summary>
[DataField(required: true)]
public float Reduction;
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Prototypes;
namespace Content.Shared.Clothing.Components;
@@ -10,7 +11,20 @@ public sealed partial class LoadoutComponent : Component
/// <summary>
/// A list of starting gears, of which one will be given.
/// All elements are weighted the same in the list.
///
/// If not specified, <see cref="RoleLoadout"/> will be used instead.
/// </summary>
[DataField("prototypes", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<StartingGearPrototype>)), AutoNetworkedField]
public List<string>? Prototypes;
[DataField("prototypes")]
[AutoNetworkedField]
public List<ProtoId<StartingGearPrototype>>? StartingGear;
/// <summary>
/// A list of role loadouts, of which one will be given.
/// All elements are weighted the same in the list.
///
/// If not specified, <see cref="StartingGear"/> will be used instead.
/// </summary>
[DataField]
[AutoNetworkedField]
public List<ProtoId<RoleLoadoutPrototype>>? RoleLoadout;
}

View File

@@ -0,0 +1,23 @@
using Content.Shared.Atmos;
using Content.Shared.Clothing.Components;
using Content.Shared.Inventory;
namespace Content.Shared.Clothing.EntitySystems;
/// <summary>
/// Handles reducing fire damage when wearing clothing with <see cref="FireProtectionComponent"/>.
/// </summary>
public sealed class FireProtectionSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FireProtectionComponent, InventoryRelayedEvent<GetFireProtectionEvent>>(OnGetProtection);
}
private void OnGetProtection(Entity<FireProtectionComponent> ent, ref InventoryRelayedEvent<GetFireProtectionEvent> args)
{
args.Args.Reduce(ent.Comp.Reduction);
}
}

View File

@@ -9,7 +9,7 @@ using Robust.Shared.Random;
namespace Content.Shared.Clothing;
/// <summary>
/// Assigns a loadout to an entity based on the startingGear prototype
/// Assigns a loadout to an entity based on the RoleLoadout prototype
/// </summary>
public sealed class LoadoutSystem : EntitySystem
{
@@ -110,10 +110,22 @@ public sealed class LoadoutSystem : EntitySystem
private void OnMapInit(EntityUid uid, LoadoutComponent component, MapInitEvent args)
{
if (component.Prototypes == null)
// Use starting gear if specified
if (component.StartingGear != null)
{
var gear = _protoMan.Index(_random.Pick(component.StartingGear));
_station.EquipStartingGear(uid, gear);
return;
}
if (component.RoleLoadout == null)
return;
var proto = _protoMan.Index<StartingGearPrototype>(_random.Pick(component.Prototypes));
_station.EquipStartingGear(uid, proto);
// ...otherwise equip from role loadout
var id = _random.Pick(component.RoleLoadout);
var proto = _protoMan.Index(id);
var loadout = new RoleLoadout(id);
loadout.SetDefault(_protoMan, true);
_station.EquipRoleLoadout(uid, loadout, proto);
}
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.Dataset;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.CriminalRecords.Components;
/// <summary>
/// Lets the user hack a criminal records console, once.
/// Everyone is set to wanted with a randomly picked reason.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCriminalRecordsHackerSystem))]
public sealed partial class CriminalRecordsHackerComponent : Component
{
/// <summary>
/// How long the doafter is for hacking it.
/// </summary>
public TimeSpan Delay = TimeSpan.FromSeconds(20);
/// <summary>
/// Dataset of random reasons to use.
/// </summary>
[DataField]
public ProtoId<DatasetPrototype> Reasons = "CriminalRecordsWantedReasonPlaceholders";
/// <summary>
/// Announcement made after the console is hacked.
/// </summary>
[DataField]
public LocId Announcement = "ninja-criminal-records-hack-announcement";
}

View File

@@ -0,0 +1,48 @@
using Content.Shared.CriminalRecords.Components;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Systems;
using Robust.Shared.Serialization;
namespace Content.Shared.CriminalRecords.Systems;
public abstract class SharedCriminalRecordsHackerSystem : EntitySystem
{
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CriminalRecordsHackerComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
}
private void OnBeforeInteractHand(Entity<CriminalRecordsHackerComponent> ent, ref BeforeInteractHandEvent args)
{
// TODO: generic event
if (args.Handled || !_gloves.AbilityCheck(ent, args, out var target))
return;
if (!HasComp<CriminalRecordsConsoleComponent>(target))
return;
var doAfterArgs = new DoAfterArgs(EntityManager, ent, ent.Comp.Delay, new CriminalRecordsHackDoAfterEvent(), target: target, used: ent, eventTarget: ent)
{
BreakOnDamage = true,
BreakOnMove = true,
MovementThreshold = 0.5f
};
_doAfter.TryStartDoAfter(doAfterArgs);
args.Handled = true;
}
}
/// <summary>
/// Raised on the user when the doafter completes.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class CriminalRecordsHackDoAfterEvent : SimpleDoAfterEvent
{
}

View File

@@ -13,6 +13,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public const string InvokedPort = "link_port";
@@ -529,7 +530,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
private bool InRange(EntityUid sourceUid, EntityUid sinkUid, float range)
{
// TODO: This should be using an existing method and also coordinates inrange instead.
return Transform(sourceUid).MapPosition.InRange(Transform(sinkUid).MapPosition, range);
return _transform.GetMapCoordinates(sourceUid).InRange(_transform.GetMapCoordinates(sinkUid), range);
}
private void SendNewLinkEvent(EntityUid? user, EntityUid sourceUid, string source, EntityUid sinkUid, string sink)

View File

@@ -210,8 +210,8 @@ namespace Content.Shared.Examine
public bool InRangeUnOccluded(EntityUid origin, EntityUid other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
var otherPos = entMan.GetComponent<TransformComponent>(other).MapPosition;
var originPos = _transform.GetMapCoordinates(origin);
var otherPos = _transform.GetMapCoordinates(other);
return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
}
@@ -219,7 +219,7 @@ namespace Content.Shared.Examine
public bool InRangeUnOccluded(EntityUid origin, EntityCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
var originPos = _transform.GetMapCoordinates(origin);
var otherPos = other.ToMap(entMan, _transform);
return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
@@ -228,7 +228,7 @@ namespace Content.Shared.Examine
public bool InRangeUnOccluded(EntityUid origin, MapCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
var originPos = _transform.GetMapCoordinates(origin);
return InRangeUnOccluded(originPos, other, range, predicate, ignoreInsideBlocker);
}

View File

@@ -24,7 +24,11 @@ public sealed partial class BlindableComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("EyeDamage"), AutoNetworkedField]
public int EyeDamage = 0;
public const int MaxDamage = 9;
[ViewVariables(VVAccess.ReadOnly), DataField]
public int MaxDamage = 9;
[ViewVariables(VVAccess.ReadOnly), DataField]
public int MinDamage = 0;
/// <description>
/// Used to ensure that this doesn't break with sandbox or admin tools.

View File

@@ -37,7 +37,7 @@ public sealed class BlindableSystem : EntitySystem
var old = blindable.Comp.IsBlind;
// Don't bother raising an event if the eye is too damaged.
if (blindable.Comp.EyeDamage >= BlindableComponent.MaxDamage)
if (blindable.Comp.EyeDamage >= blindable.Comp.MaxDamage)
{
blindable.Comp.IsBlind = true;
}
@@ -62,13 +62,31 @@ public sealed class BlindableSystem : EntitySystem
return;
blindable.Comp.EyeDamage += amount;
blindable.Comp.EyeDamage = Math.Clamp(blindable.Comp.EyeDamage, 0, BlindableComponent.MaxDamage);
Dirty(blindable);
UpdateIsBlind(blindable);
UpdateEyeDamage(blindable, true);
}
private void UpdateEyeDamage(Entity<BlindableComponent?> blindable, bool isDamageChanged)
{
if (!Resolve(blindable, ref blindable.Comp, false))
return;
var previousDamage = blindable.Comp.EyeDamage;
blindable.Comp.EyeDamage = Math.Clamp(blindable.Comp.EyeDamage, blindable.Comp.MinDamage, blindable.Comp.MaxDamage);
Dirty(blindable);
if (!isDamageChanged && previousDamage == blindable.Comp.EyeDamage)
return;
UpdateIsBlind(blindable);
var ev = new EyeDamageChangedEvent(blindable.Comp.EyeDamage);
RaiseLocalEvent(blindable.Owner, ref ev);
}
public void SetMinDamage(Entity<BlindableComponent?> blindable, int amount)
{
if (!Resolve(blindable, ref blindable.Comp, false))
return;
blindable.Comp.MinDamage = amount;
UpdateEyeDamage(blindable, false);
}
}
/// <summary>

View File

@@ -1,4 +1,3 @@
using Content.Shared.Actions;
using Content.Shared.Eye.Blinding.Components;
using Robust.Shared.Audio.Systems;
@@ -124,7 +123,7 @@ public sealed class EyeClosingSystem : EntitySystem
if (_entityManager.TryGetComponent<EyeClosingComponent>(blindable, out var eyelids) && !eyelids.NaturallyCreated)
return;
if (ev.Blur < BlurryVisionComponent.MaxMagnitude || ev.Blur >= BlindableComponent.MaxDamage)
if (ev.Blur < BlurryVisionComponent.MaxMagnitude || ev.Blur >= blindable.Comp.MaxDamage)
{
RemCompDeferred<EyeClosingComponent>(blindable);
return;

View File

@@ -101,4 +101,6 @@ public sealed partial class ToggleLightingActionEvent : InstantActionEvent { }
public sealed partial class ToggleGhostHearingActionEvent : InstantActionEvent { }
public sealed partial class ToggleGhostVisibilityToAllEvent : InstantActionEvent { }
public sealed partial class BooActionEvent : InstantActionEvent { }

View File

@@ -0,0 +1,21 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.GridPreloader.Prototypes;
/// <summary>
/// Creating this prototype will automatically load the grid at the specified path at the beginning of the round,
/// and allow the GridPreloader system to load them in the middle of the round. This is needed for optimization,
/// because loading grids in the middle of a round causes the server to lag.
/// </summary>
[Prototype("preloadedGrid")]
public sealed partial class PreloadedGridPrototype : IPrototype
{
[IdDataField] public string ID { get; } = string.Empty;
[DataField(required: true)]
public ResPath Path;
[DataField]
public int Copies = 1;
}

View File

@@ -0,0 +1,4 @@
namespace Content.Shared.GridPreloader.Systems;
public abstract class SharedGridPreloaderSystem : EntitySystem
{
}

View File

@@ -120,17 +120,12 @@ public abstract partial class SharedHandsSystem
return true;
var userXform = Transform(uid);
var isInContainer = ContainerSystem.IsEntityInContainer(uid);
var isInContainer = ContainerSystem.IsEntityOrParentInContainer(uid, xform: userXform);
if (targetDropLocation == null || isInContainer)
{
// If user is in a container, drop item into that container. Otherwise, attach to grid or map.\
// TODO recursively check upwards for containers
if (!isInContainer
|| !ContainerSystem.TryGetContainingContainer(userXform.ParentUid, uid, out var container, skipExistCheck: true)
|| !ContainerSystem.Insert((entity, itemXform), container))
TransformSystem.AttachToGridOrMap(entity, itemXform);
// If user is in a container, drop item into that container. Otherwise, attach to grid or map.
TransformSystem.DropNextTo((entity, itemXform), (uid, userXform));
return true;
}

View File

@@ -108,10 +108,10 @@ public abstract partial class SharedHandsSystem : EntitySystem
var xform = Transform(uid);
var coordinateEntity = xform.ParentUid.IsValid() ? xform.ParentUid : uid;
var itemXform = Transform(entity);
var itemPos = itemXform.MapPosition;
var itemPos = TransformSystem.GetMapCoordinates(entity, xform: itemXform);
if (itemPos.MapId == xform.MapID
&& (itemPos.Position - xform.MapPosition.Position).Length() <= MaxAnimationRange
&& (itemPos.Position - TransformSystem.GetMapCoordinates(uid, xform: xform).Position).Length() <= MaxAnimationRange
&& MetaData(entity).VisibilityMask == MetaData(uid).VisibilityMask) // Don't animate aghost pickups.
{
var initialPosition = EntityCoordinates.FromMap(coordinateEntity, itemPos, TransformSystem, EntityManager);

View File

@@ -0,0 +1,23 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.HealthExaminable;
[RegisterComponent, Access(typeof(HealthExaminableSystem))]
public sealed partial class HealthExaminableComponent : Component
{
public List<FixedPoint2> Thresholds = new()
{ FixedPoint2.New(10), FixedPoint2.New(25), FixedPoint2.New(50), FixedPoint2.New(75) };
[DataField(required: true)]
public HashSet<ProtoId<DamageTypePrototype>> ExaminableTypes = default!;
/// <summary>
/// Health examine text is automatically generated through creating loc string IDs, in the form:
/// `health-examine-[prefix]-[type]-[threshold]`
/// This part determines the prefix.
/// </summary>
[DataField]
public string LocPrefix = "carbon";
}

View File

@@ -0,0 +1,116 @@
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared.HealthExaminable;
public sealed class HealthExaminableSystem : EntitySystem
{
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HealthExaminableComponent, GetVerbsEvent<ExamineVerb>>(OnGetExamineVerbs);
}
private void OnGetExamineVerbs(EntityUid uid, HealthExaminableComponent component, GetVerbsEvent<ExamineVerb> args)
{
if (!TryComp<DamageableComponent>(uid, out var damage))
return;
var detailsRange = _examineSystem.IsInDetailsRange(args.User, uid);
var verb = new ExamineVerb()
{
Act = () =>
{
var markup = CreateMarkup(uid, component, damage);
_examineSystem.SendExamineTooltip(args.User, uid, markup, false, false);
},
Text = Loc.GetString("health-examinable-verb-text"),
Category = VerbCategory.Examine,
Disabled = !detailsRange,
Message = detailsRange ? null : Loc.GetString("health-examinable-verb-disabled"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/rejuvenate.svg.192dpi.png"))
};
args.Verbs.Add(verb);
}
public FormattedMessage CreateMarkup(EntityUid uid, HealthExaminableComponent component, DamageableComponent damage)
{
var msg = new FormattedMessage();
var first = true;
foreach (var type in component.ExaminableTypes)
{
if (!damage.Damage.DamageDict.TryGetValue(type, out var dmg))
continue;
if (dmg == FixedPoint2.Zero)
continue;
FixedPoint2 closest = FixedPoint2.Zero;
string chosenLocStr = string.Empty;
foreach (var threshold in component.Thresholds)
{
var str = $"health-examinable-{component.LocPrefix}-{type}-{threshold}";
var tempLocStr = Loc.GetString($"health-examinable-{component.LocPrefix}-{type}-{threshold}", ("target", Identity.Entity(uid, EntityManager)));
// i.e., this string doesn't exist, because theres nothing for that threshold
if (tempLocStr == str)
continue;
if (dmg > threshold && threshold > closest)
{
chosenLocStr = tempLocStr;
closest = threshold;
}
}
if (closest == FixedPoint2.Zero)
continue;
if (!first)
{
msg.PushNewline();
}
else
{
first = false;
}
msg.AddMarkup(chosenLocStr);
}
if (msg.IsEmpty)
{
msg.AddMarkup(Loc.GetString($"health-examinable-{component.LocPrefix}-none"));
}
// Anything else want to add on to this?
RaiseLocalEvent(uid, new HealthBeingExaminedEvent(msg), true);
return msg;
}
}
/// <summary>
/// A class raised on an entity whose health is being examined
/// in order to add special text that is not handled by the
/// damage thresholds.
/// </summary>
public sealed class HealthBeingExaminedEvent
{
public FormattedMessage Message;
public HealthBeingExaminedEvent(FormattedMessage message)
{
Message = message;
}
}

View File

@@ -9,8 +9,29 @@ namespace Content.Shared.Humanoid;
[DataDefinition]
[Serializable, NetSerializable]
public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, IEquatable<HumanoidCharacterAppearance>
{
[DataField("hair")]
public string HairStyleId { get; set; } = HairStyles.DefaultHairStyle;
[DataField]
public Color HairColor { get; set; } = Color.Black;
[DataField("facialHair")]
public string FacialHairStyleId { get; set; } = HairStyles.DefaultFacialHairStyle;
[DataField]
public Color FacialHairColor { get; set; } = Color.Black;
[DataField]
public Color EyeColor { get; set; } = Color.Black;
[DataField]
public Color SkinColor { get; set; } = Humanoid.SkinColor.ValidHumanSkinTone;
[DataField]
public List<Marking> Markings { get; set; } = new();
public HumanoidCharacterAppearance(string hairStyleId,
Color hairColor,
string facialHairStyleId,
@@ -28,26 +49,11 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
Markings = markings;
}
[DataField("hair")]
public string HairStyleId { get; private set; }
public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) :
this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings))
{
[DataField("hairColor")]
public Color HairColor { get; private set; }
[DataField("facialHair")]
public string FacialHairStyleId { get; private set; }
[DataField("facialHairColor")]
public Color FacialHairColor { get; private set; }
[DataField("eyeColor")]
public Color EyeColor { get; private set; }
[DataField("skinColor")]
public Color SkinColor { get; private set; }
[DataField("markings")]
public List<Marking> Markings { get; private set; }
}
public HumanoidCharacterAppearance WithHairStyleName(string newName)
{
@@ -84,18 +90,6 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings);
}
public HumanoidCharacterAppearance() : this(
HairStyles.DefaultHairStyle,
Color.Black,
HairStyles.DefaultFacialHairStyle,
Color.Black,
Color.Black,
Humanoid.SkinColor.ValidHumanSkinTone,
new ()
)
{
}
public static HumanoidCharacterAppearance DefaultWithSpecies(string species)
{
var speciesPrototype = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species);
@@ -245,4 +239,32 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
if (!Markings.SequenceEqual(other.Markings)) return false;
return true;
}
public bool Equals(HumanoidCharacterAppearance? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return HairStyleId == other.HairStyleId &&
HairColor.Equals(other.HairColor) &&
FacialHairStyleId == other.FacialHairStyleId &&
FacialHairColor.Equals(other.FacialHairColor) &&
EyeColor.Equals(other.EyeColor) &&
SkinColor.Equals(other.SkinColor) &&
Markings.SequenceEqual(other.Markings);
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is HumanoidCharacterAppearance other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
}
public HumanoidCharacterAppearance Clone()
{
return new(this);
}
}

View File

@@ -0,0 +1,19 @@
using Content.Shared.Preferences;
namespace Content.Shared.Humanoid;
/// <summary>
/// Holds all of the data for importing / exporting character profiles.
/// </summary>
[DataDefinition]
public sealed partial class HumanoidProfileExport
{
[DataField]
public string ForkId;
[DataField]
public int Version = 1;
[DataField(required: true)]
public HumanoidCharacterProfile Profile = default!;
}

View File

@@ -1,13 +1,22 @@
using System.IO;
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Decals;
using Content.Shared.Examine;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement;
using Content.Shared.Preferences;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects.Components.Localization;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Humanoid;
@@ -22,8 +31,10 @@ namespace Content.Shared.Humanoid;
/// </summary>
public abstract class SharedHumanoidAppearanceSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISerializationManager _serManager = default!;
[Dependency] private readonly MarkingManager _markingManager = default!;
[ValidatePrototypeId<SpeciesPrototype>]
@@ -37,6 +48,37 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
SubscribeLocalEvent<HumanoidAppearanceComponent, ExaminedEvent>(OnExamined);
}
public DataNode ToDataNode(HumanoidCharacterProfile profile)
{
var export = new HumanoidProfileExport()
{
ForkId = _cfgManager.GetCVar(CVars.BuildForkId),
Profile = profile,
};
var dataNode = _serManager.WriteValue(export, alwaysWrite: true, notNullableOverride: true);
return dataNode;
}
public HumanoidCharacterProfile FromStream(Stream stream, ICommonSession session)
{
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
var root = yamlStream.Documents[0].RootNode;
var export = _serManager.Read<HumanoidProfileExport>(root.ToDataNode(), notNullableOverride: true);
/*
* Add custom handling here for forks / version numbers if you care.
*/
var profile = export.Profile;
var collection = IoCManager.Instance;
profile.EnsureValid(session, collection!);
return profile;
}
private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, ComponentInit args)
{
if (string.IsNullOrEmpty(humanoid.Species) || _netManager.IsClient && !IsClientSide(uid))

View File

@@ -70,7 +70,7 @@ namespace Content.Shared.Interaction
if (!Resolve(user, ref xform))
return false;
var diff = coordinates - xform.MapPosition.Position;
var diff = coordinates - _transform.GetMapCoordinates(user, xform: xform).Position;
if (diff.LengthSquared() <= 0.01f)
return true;

View File

@@ -665,14 +665,14 @@ namespace Content.Shared.Interaction
else
{
// We'll still do the raycast from the centres but we'll bump the range as we know they're in range.
originPos = xformA.MapPosition;
originPos = _transform.GetMapCoordinates(origin, xform: xformA);
range = (originPos.Position - targetPos.Position).Length();
}
}
// No fixtures, e.g. wallmounts.
else
{
originPos = Transform(origin).MapPosition;
originPos = _transform.GetMapCoordinates(origin);
var otherParent = Transform(other).ParentUid;
targetRot = otherParent.IsValid() ? Transform(otherParent).LocalRotation + otherAngle : otherAngle;
}
@@ -826,7 +826,7 @@ namespace Content.Shared.Interaction
bool popup = false)
{
Ignored combinedPredicate = e => e == origin || (predicate?.Invoke(e) ?? false);
var originPosition = Transform(origin).MapPosition;
var originPosition = _transform.GetMapCoordinates(origin);
var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, combinedPredicate, ShouldCheckAccess(origin));
if (!inRange && popup && _gameTiming.IsFirstTimePredicted)

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Armor;
using Content.Shared.Clothing.Components;
using Content.Shared.DoAfter;
using Content.Shared.Hands;
@@ -114,7 +115,7 @@ public abstract partial class InventorySystem
if (!_handsSystem.CanDropHeld(actor, hands.ActiveHand!, checkActionBlocker: false))
return;
RaiseLocalEvent(held.Value, new HandDeselectedEvent(actor), false);
RaiseLocalEvent(held.Value, new HandDeselectedEvent(actor));
TryEquip(actor, actor, held.Value, ev.Slot, predicted: true, inventory: inventory, force: true, checkDoafter:true);
}
@@ -243,8 +244,16 @@ public abstract partial class InventorySystem
return false;
DebugTools.Assert(slotDefinition.Name == slot);
if (slotDefinition.DependsOn != null && !TryGetSlotEntity(target, slotDefinition.DependsOn, out _, inventory))
return false;
if (slotDefinition.DependsOn != null)
{
if (!TryGetSlotEntity(target, slotDefinition.DependsOn, out EntityUid? slotEntity, inventory))
return false;
if (slotDefinition.DependsOnComponents is { } componentRegistry)
foreach (var (_, entry) in componentRegistry)
if (!HasComp(slotEntity, entry.Component.GetType()))
return false;
}
var fittingInPocket = slotDefinition.SlotFlags.HasFlag(SlotFlags.POCKET) &&
item != null &&
@@ -301,7 +310,6 @@ public abstract partial class InventorySystem
reason = itemAttemptEvent.Reason ?? reason;
return false;
}
return true;
}

View File

@@ -15,7 +15,7 @@ public partial class InventorySystem : EntitySystem
private void InitializeSlots()
{
SubscribeLocalEvent<InventoryComponent, ComponentInit>(OnInit);
SubscribeNetworkEvent<OpenSlotStorageNetworkMessage>(OnOpenSlotStorage);
SubscribeAllEvent<OpenSlotStorageNetworkMessage>(OnOpenSlotStorage);
_vvm.GetTypeHandler<InventoryComponent>()
.AddHandler(HandleViewVariablesSlots, ListViewVariablesSlots);

View File

@@ -34,6 +34,8 @@ public sealed partial class SlotDefinition
[DataField("dependsOn")] public string? DependsOn { get; private set; }
[DataField("dependsOnComponents")] public ComponentRegistry? DependsOnComponents { get; private set; }
[DataField("displayName", required: true)]
public string DisplayName { get; private set; } = string.Empty;

View File

@@ -21,12 +21,18 @@ public sealed partial class LockComponent : Component
public bool Locked = true;
/// <summary>
/// Whether or not the lock is toggled by simply clicking.
/// Whether or not the lock is locked by simply clicking.
/// </summary>
[DataField("lockOnClick"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public bool LockOnClick;
/// <summary>
/// Whether or not the lock is unlocked by simply clicking.
/// </summary>
[DataField, AutoNetworkedField]
public bool UnlockOnClick = true;
/// <summary>
/// The sound played when unlocked.
/// </summary>

View File

@@ -64,12 +64,12 @@ public sealed class LockSystem : EntitySystem
//CrystallPunk LockSystem Adapt
// Only attempt an unlock by default on Activate
//if (lockComp.Locked)
//if (lockComp.Locked && lockComp.UnlockOnClick)
//{
// TryUnlock(uid, args.User, lockComp);
// args.Handled = true;
//}
//else if (lockComp.LockOnClick)
//else if (!lockComp.Locked && lockComp.LockOnClick)
//{
// TryLock(uid, args.User, lockComp);
// args.Handled = true;
@@ -228,6 +228,18 @@ public sealed class LockSystem : EntitySystem
return true;
}
/// <summary>
/// Returns true if the entity is locked.
/// Entities with no lock component are considered unlocked.
/// </summary>
public bool IsLocked(Entity<LockComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
return ent.Comp.Locked;
}
/// <summary>
/// Raises an event for other components to check whether or not
/// the entity can be locked in its current state.

View File

@@ -0,0 +1,39 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Magic.Components;
// TODO: Rename to MagicActionComponent or MagicRequirementsComponent
[RegisterComponent, NetworkedComponent, Access(typeof(SharedMagicSystem))]
public sealed partial class MagicComponent : Component
{
// TODO: Split into different components?
// This could be the MagicRequirementsComp - which just is requirements for the spell
// Magic comp could be on the actual entities itself
// Could handle lifetime, ignore caster, etc?
// Magic caster comp would be on the caster, used for what I'm not sure
// TODO: Do After here or in actions
// TODO: Spell requirements
// A list of requirements to cast the spell
// Hands
// Any item in hand
// Spell takes up an inhand slot
// May be an action toggle or something
// TODO: List requirements in action desc
/// <summary>
/// Does this spell require Wizard Robes & Hat?
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool RequiresClothes;
/// <summary>
/// Does this spell require the user to speak?
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool RequiresSpeech;
// TODO: FreeHand - should check if toggleable action
// Check which hand is free to toggle action in
}

View File

@@ -0,0 +1,36 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Magic.Components;
/// <summary>
/// Spellbooks can grant one or more spells to the user. If marked as <see cref="LearnPermanently"/> it will teach
/// the performer the spells and wipe the book.
/// Default behavior requires the book to be held in hand
/// </summary>
[RegisterComponent, Access(typeof(SpellbookSystem))]
public sealed partial class SpellbookComponent : Component
{
/// <summary>
/// List of spells that this book has. This is a combination of the WorldSpells, EntitySpells, and InstantSpells.
/// </summary>
[ViewVariables]
public readonly List<EntityUid> Spells = new();
/// <summary>
/// The three fields below is just used for initialization.
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<EntProtoId, int> SpellActions = new();
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public float LearnTime = .75f;
/// <summary>
/// If true, the spell action stays even after the book is removed
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public bool LearnPermanently;
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Magic.Components;
/// <summary>
/// The <see cref="SharedMagicSystem"/> checks this if a spell requires wizard clothes
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedMagicSystem))]
public sealed partial class WizardClothesComponent : Component;

View File

@@ -0,0 +1,12 @@
namespace Content.Shared.Magic.Events;
[ByRefEvent]
public struct BeforeCastSpellEvent(EntityUid performer)
{
/// <summary>
/// The Performer of the event, to check if they meet the requirements.
/// </summary>
public EntityUid Performer = performer;
public bool Cancelled;
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.Actions;
namespace Content.Shared.Magic.Events;
/// <summary>
/// Adds provided Charge to the held wand
/// </summary>
public sealed partial class ChargeSpellEvent : InstantActionEvent, ISpeakSpell
{
[DataField(required: true)]
public int Charge;
[DataField]
public string WandTag = "WizardWand";
[DataField]
public string? Speech { get; private set; }
}

View File

@@ -1,6 +1,5 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Magic.Events;
@@ -9,17 +8,18 @@ public sealed partial class InstantSpawnSpellEvent : InstantActionEvent, ISpeakS
/// <summary>
/// What entity should be spawned.
/// </summary>
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype = default!;
[DataField(required: true)]
public EntProtoId Prototype;
[DataField("preventCollide")]
[DataField]
public bool PreventCollideWithCaster = true;
[DataField("speech")]
[DataField]
public string? Speech { get; private set; }
/// <summary>
/// Gets the targeted spawn positons; may lead to multiple entities being spawned.
/// </summary>
[DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
[DataField]
public MagicInstantSpawnData PosData = new TargetCasterPos();
}

View File

@@ -1,5 +1,4 @@
using Content.Shared.Actions;
using Robust.Shared.Audio;
namespace Content.Shared.Magic.Events;
@@ -7,20 +6,12 @@ public sealed partial class KnockSpellEvent : InstantActionEvent, ISpeakSpell
{
/// <summary>
/// The range this spell opens doors in
/// 4f is the default
/// 10f is the default
/// Should be able to open all doors/lockers in visible sight
/// </summary>
[DataField("range")]
public float Range = 4f;
[DataField]
public float Range = 10f;
[DataField("knockSound")]
public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Magic/knock.ogg");
/// <summary>
/// Volume control for the spell.
/// </summary>
[DataField("knockVolume")]
public float KnockVolume = 5f;
[DataField("speech")]
[DataField]
public string? Speech { get; private set; }
}

View File

@@ -1,6 +1,5 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Magic.Events;
@@ -9,14 +8,9 @@ public sealed partial class ProjectileSpellEvent : WorldTargetActionEvent, ISpea
/// <summary>
/// What entity should be spawned.
/// </summary>
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype = default!;
[DataField(required: true)]
public EntProtoId Prototype;
/// <summary>
/// Gets the targeted spawn positions; may lead to multiple entities being spawned.
/// </summary>
[DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
[DataField("speech")]
[DataField]
public string? Speech { get; private set; }
}

View File

@@ -4,12 +4,13 @@ namespace Content.Shared.Magic.Events;
public sealed partial class SmiteSpellEvent : EntityTargetActionEvent, ISpeakSpell
{
// TODO: Make part of gib method
/// <summary>
/// Should this smite delete all parts/mechanisms gibbed except for the brain?
/// Should this smite delete all parts/mechanisms gibbed except for the brain?
/// </summary>
[DataField("deleteNonBrainParts")]
[DataField]
public bool DeleteNonBrainParts = true;
[DataField("speech")]
[DataField]
public string? Speech { get; private set; }
}

View File

@@ -0,0 +1,8 @@
namespace Content.Shared.Magic.Events;
[ByRefEvent]
public readonly struct SpeakSpellEvent(EntityUid performer, string speech)
{
public readonly EntityUid Performer = performer;
public readonly string Speech = speech;
}

View File

@@ -1,19 +1,19 @@
using Content.Shared.Actions;
using Robust.Shared.Audio;
namespace Content.Shared.Magic.Events;
// TODO: Can probably just be an entity or something
public sealed partial class TeleportSpellEvent : WorldTargetActionEvent, ISpeakSpell
{
[DataField("blinkSound")]
public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg");
[DataField("speech")]
[DataField]
public string? Speech { get; private set; }
// TODO: Move to magic component
// TODO: Maybe not since sound specifier is a thing
// Keep here to remind what the volume was set as
/// <summary>
/// Volume control for the spell.
/// </summary>
[DataField("blinkVolume")]
[DataField]
public float BlinkVolume = 5f;
}

View File

@@ -4,29 +4,31 @@ using Content.Shared.Storage;
namespace Content.Shared.Magic.Events;
// TODO: This class needs combining with InstantSpawnSpellEvent
public sealed partial class WorldSpawnSpellEvent : WorldTargetActionEvent, ISpeakSpell
{
// TODO:This class needs combining with InstantSpawnSpellEvent
/// <summary>
/// The list of prototypes this spell will spawn
/// </summary>
[DataField("prototypes")]
public List<EntitySpawnEntry> Contents = new();
[DataField]
public List<EntitySpawnEntry> Prototypes = new();
// TODO: This offset is liable for deprecation.
// TODO: Target tile via code instead?
/// <summary>
/// The offset the prototypes will spawn in on relative to the one prior.
/// Set to 0,0 to have them spawn on the same tile.
/// </summary>
[DataField("offset")]
[DataField]
public Vector2 Offset;
/// <summary>
/// Lifetime to set for the entities to self delete
/// </summary>
[DataField("lifetime")] public float? Lifetime;
[DataField]
public float? Lifetime;
[DataField("speech")]
[DataField]
public string? Speech { get; private set; }
}

View File

@@ -0,0 +1,25 @@
namespace Content.Shared.Magic;
// TODO: If still needed, move to magic component
[ImplicitDataDefinitionForInheritors]
public abstract partial class MagicInstantSpawnData;
/// <summary>
/// Spawns underneath caster.
/// </summary>
public sealed partial class TargetCasterPos : MagicInstantSpawnData;
/// <summary>
/// Spawns 3 tiles wide in front of the caster.
/// </summary>
public sealed partial class TargetInFront : MagicInstantSpawnData
{
[DataField]
public int Width = 3;
}
/// <summary>
/// Spawns 1 tile in front of caster
/// </summary>
public sealed partial class TargetInFrontSingle : MagicInstantSpawnData;

View File

@@ -1,20 +0,0 @@
namespace Content.Shared.Magic;
[ImplicitDataDefinitionForInheritors]
public abstract partial class MagicSpawnData
{
}
/// <summary>
/// Spawns 1 at the caster's feet.
/// </summary>
public sealed partial class TargetCasterPos : MagicSpawnData {}
/// <summary>
/// Targets the 3 tiles in front of the caster.
/// </summary>
public sealed partial class TargetInFront : MagicSpawnData
{
[DataField("width")] public int Width = 3;
}

View File

@@ -0,0 +1,519 @@
using System.Numerics;
using Content.Shared.Actions;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
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.Physics;
using Content.Shared.Popups;
using Content.Shared.Speech.Muting;
using Content.Shared.Storage;
using Content.Shared.Tag;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Spawners;
namespace Content.Shared.Magic;
/// <summary>
/// Handles learning and using spells (actions)
/// </summary>
public abstract class SharedMagicSystem : EntitySystem
{
[Dependency] private readonly ISerializationManager _seriMan = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedGunSystem _gunSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedDoorSystem _door = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly TagSystem _tag = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MagicComponent, BeforeCastSpellEvent>(OnBeforeCastSpell);
SubscribeLocalEvent<InstantSpawnSpellEvent>(OnInstantSpawn);
SubscribeLocalEvent<TeleportSpellEvent>(OnTeleportSpell);
SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
SubscribeLocalEvent<ChangeComponentsSpellEvent>(OnChangeComponentsSpell);
SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
SubscribeLocalEvent<ChargeSpellEvent>(OnChargeSpell);
// Spell wishlist
// A wishlish of spells that I'd like to implement or planning on implementing in a future PR
// TODO: InstantDoAfterSpell and WorldDoafterSpell
// Both would be an action that take in an event, that passes an event to trigger once the doafter is done
// This would be three events:
// 1 - Event that triggers from the action that starts the doafter
// 2 - The doafter event itself, which passes the event with it
// 3 - The event to trigger once the do-after finishes
// TODO: Inanimate objects to life ECS
// AI sentience
// TODO: Flesh2Stone
// Entity Target spell
// Synergy with Inanimate object to life (detects player and allows player to move around)
// TODO: Lightning Spell
// Should just fire lightning, try to prevent arc back to caster
// TODO: Magic Missile (homing projectile ecs)
// Instant action, target any player (except self) on screen
// TODO: Random projectile ECS for magic-carp, wand of magic
// TODO: Recall Spell
// mark any item in hand to recall
// ItemRecallComponent
// Event adds the component if it doesn't exist and the performer isn't stored in the comp
// 2nd firing of the event checks to see if the recall comp has this uid, and if it does it calls it
// if no free hands, summon at feet
// if item deleted, clear stored item
// TODO: Jaunt (should be its own ECS)
// Instant action
// When clicked, disappear/reappear (goes to paused map)
// option to restrict to tiles
// option for requiring entry/exit (blood jaunt)
// speed option
// TODO: Summon Events
// List of wizard events to add into the event pool that frequently activate
// floor is lava
// change places
// ECS that when triggered, will periodically trigger a random GameRule
// Would need a controller/controller entity?
// TODO: Summon Guns
// Summon a random gun at peoples feet
// Get every alive player (not in cryo, not a simplemob)
// TODO: After Antag Rework - Rare chance of giving gun collector status to people
// TODO: Summon Magic
// Summon a random magic wand at peoples feet
// Get every alive player (not in cryo, not a simplemob)
// TODO: After Antag Rework - Rare chance of giving magic collector status to people
// TODO: Bottle of Blood
// Summons Slaughter Demon
// TODO: Slaughter Demon
// Also see Jaunt
// TODO: Field Spells
// Should be able to specify a grid of tiles (3x3 for example) that it effects
// Timed despawn - so it doesn't last forever
// Ignore caster - for spells that shouldn't effect the caster (ie if timestop should effect the caster)
// TODO: Touch toggle spell
// 1 - When toggled on, show in hand
// 2 - Block hand when toggled on
// - Require free hand
// 3 - use spell event when toggled & click
}
private void OnBeforeCastSpell(Entity<MagicComponent> ent, ref BeforeCastSpellEvent args)
{
var comp = ent.Comp;
var hasReqs = true;
if (comp.RequiresClothes)
{
var enumerator = _inventory.GetSlotEnumerator(args.Performer, SlotFlags.OUTERCLOTHING | SlotFlags.HEAD);
while (enumerator.MoveNext(out var containerSlot))
{
if (containerSlot.ContainedEntity is { } item)
hasReqs = HasComp<WizardClothesComponent>(item);
else
hasReqs = false;
if (!hasReqs)
break;
}
}
if (comp.RequiresSpeech && HasComp<MutedComponent>(args.Performer))
hasReqs = false;
if (hasReqs)
return;
args.Cancelled = true;
_popup.PopupClient(Loc.GetString("spell-requirements-failed"), args.Performer, args.Performer);
// TODO: Pre-cast do after, either here or in SharedActionsSystem
}
private bool PassesSpellPrerequisites(EntityUid spell, EntityUid performer)
{
var ev = new BeforeCastSpellEvent(performer);
RaiseLocalEvent(spell, ref ev);
return !ev.Cancelled;
}
#region Spells
#region Instant Spawn Spells
/// <summary>
/// Handles the instant action (i.e. on the caster) attempting to spawn an entity.
/// </summary>
private void OnInstantSpawn(InstantSpawnSpellEvent args)
{
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
return;
var transform = Transform(args.Performer);
foreach (var position in GetInstantSpawnPositions(transform, args.PosData))
{
SpawnSpellHelper(args.Prototype, position, args.Performer, preventCollide: args.PreventCollideWithCaster);
}
Speak(args);
args.Handled = true;
}
/// <summary>
/// Gets spawn positions listed on <see cref="InstantSpawnSpellEvent"/>
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
private List<EntityCoordinates> GetInstantSpawnPositions(TransformComponent casterXform, MagicInstantSpawnData data)
{
switch (data)
{
case TargetCasterPos:
return new List<EntityCoordinates>(1) {casterXform.Coordinates};
case TargetInFrontSingle:
{
var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
return new List<EntityCoordinates>();
if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
return new List<EntityCoordinates>();
var tileIndex = tileReference.Value.GridIndices;
return new List<EntityCoordinates>(1) { _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex) };
}
case TargetInFront:
{
var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
return new List<EntityCoordinates>();
if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
return new List<EntityCoordinates>();
var tileIndex = tileReference.Value.GridIndices;
var coords = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex);
EntityCoordinates coordsPlus;
EntityCoordinates coordsMinus;
var dir = casterXform.LocalRotation.GetCardinalDir();
switch (dir)
{
case Direction.North:
case Direction.South:
{
coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (1, 0));
coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (-1, 0));
return new List<EntityCoordinates>(3)
{
coords,
coordsPlus,
coordsMinus,
};
}
case Direction.East:
case Direction.West:
{
coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, 1));
coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, -1));
return new List<EntityCoordinates>(3)
{
coords,
coordsPlus,
coordsMinus,
};
}
}
return new List<EntityCoordinates>();
}
default:
throw new ArgumentOutOfRangeException();
}
}
// End Instant Spawn Spells
#endregion
#region World Spawn Spells
/// <summary>
/// Spawns entities from a list within range of click.
/// </summary>
/// <remarks>
/// It will offset entities after the first entity based on the OffsetVector2.
/// </remarks>
/// <param name="args"> The Spawn Spell Event args.</param>
private void OnWorldSpawn(WorldSpawnSpellEvent args)
{
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
return;
var targetMapCoords = args.Target;
WorldSpawnSpellHelper(args.Prototypes, targetMapCoords, args.Performer, args.Lifetime, args.Offset);
Speak(args);
args.Handled = true;
}
/// <summary>
/// Loops through a supplied list of entity prototypes and spawns them
/// </summary>
/// <remarks>
/// If an offset of 0, 0 is supplied then the entities will all spawn on the same tile.
/// Any other offset will spawn entities starting from the source Map Coordinates and will increment the supplied
/// offset
/// </remarks>
/// <param name="entityEntries"> The list of Entities to spawn in</param>
/// <param name="entityCoords"> Map Coordinates where the entities will spawn</param>
/// <param name="lifetime"> Check to see if the entities should self delete</param>
/// <param name="offsetVector2"> A Vector2 offset that the entities will spawn in</param>
private void WorldSpawnSpellHelper(List<EntitySpawnEntry> entityEntries, EntityCoordinates entityCoords, EntityUid performer, float? lifetime, Vector2 offsetVector2)
{
var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random);
var offsetCoords = entityCoords;
foreach (var proto in getProtos)
{
SpawnSpellHelper(proto, offsetCoords, performer, lifetime);
offsetCoords = offsetCoords.Offset(offsetVector2);
}
}
// End World Spawn Spells
#endregion
#region Projectile Spells
private void OnProjectileSpell(ProjectileSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !_net.IsServer)
return;
ev.Handled = true;
Speak(ev);
var xform = Transform(ev.Performer);
var fromCoords = xform.Coordinates;
var toCoords = ev.Target;
var userVelocity = _physics.GetMapLinearVelocity(ev.Performer);
// If applicable, this ensures the projectile is parented to grid on spawn, instead of the map.
var fromMap = fromCoords.ToMap(EntityManager, _transform);
var spawnCoords = _mapManager.TryFindGridAt(fromMap, out var gridUid, out _)
? fromCoords.WithEntityId(gridUid, EntityManager)
: new(_mapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
var ent = Spawn(ev.Prototype, spawnCoords);
var direction = toCoords.ToMapPos(EntityManager, _transform) -
spawnCoords.ToMapPos(EntityManager, _transform);
_gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, ev.Performer);
}
// End Projectile Spells
#endregion
#region Change Component Spells
// staves.yml ActionRGB light
private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
return;
ev.Handled = true;
Speak(ev);
foreach (var toRemove in ev.ToRemove)
{
if (_compFact.TryGetRegistration(toRemove, out var registration))
RemComp(ev.Target, registration.Type);
}
foreach (var (name, data) in ev.ToAdd)
{
if (HasComp(ev.Target, data.Component.GetType()))
continue;
var component = (Component) _compFact.GetComponent(name);
component.Owner = ev.Target;
var temp = (object) component;
_seriMan.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(ev.Target, (Component) temp!);
}
}
// End Change Component Spells
#endregion
#region Teleport Spells
// TODO: Rename to teleport clicked spell?
/// <summary>
/// Teleports the user to the clicked location
/// </summary>
/// <param name="args"></param>
private void OnTeleportSpell(TeleportSpellEvent args)
{
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
return;
var transform = Transform(args.Performer);
if (transform.MapID != args.Target.GetMapId(EntityManager) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true))
return;
_transform.SetCoordinates(args.Performer, args.Target);
_transform.AttachToGridOrMap(args.Performer, transform);
Speak(args);
args.Handled = true;
}
// End Teleport Spells
#endregion
#region Spell Helpers
private void SpawnSpellHelper(string? proto, EntityCoordinates position, EntityUid performer, float? lifetime = null, bool preventCollide = false)
{
if (!_net.IsServer)
return;
var ent = Spawn(proto, position.SnapToGrid(EntityManager, _mapManager));
if (lifetime != null)
{
var comp = EnsureComp<TimedDespawnComponent>(ent);
comp.Lifetime = lifetime.Value;
}
if (preventCollide)
{
var comp = EnsureComp<PreventCollideComponent>(ent);
comp.Uid = performer;
}
}
// End Spell Helpers
#endregion
#region Smite Spells
private void OnSmiteSpell(SmiteSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
return;
ev.Handled = true;
Speak(ev);
var direction = _transform.GetMapCoordinates(ev.Target, Transform(ev.Target)).Position - _transform.GetMapCoordinates(ev.Performer, Transform(ev.Performer)).Position;
var impulseVector = direction * 10000;
_physics.ApplyLinearImpulse(ev.Target, impulseVector);
if (!TryComp<BodyComponent>(ev.Target, out var body))
return;
_body.GibBody(ev.Target, true, body);
}
// End Smite Spells
#endregion
#region Knock Spells
/// <summary>
/// Opens all doors and locks within range
/// </summary>
/// <param name="args"></param>
private void OnKnockSpell(KnockSpellEvent args)
{
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
return;
args.Handled = true;
Speak(args);
var transform = Transform(args.Performer);
// Look for doors and lockers, and don't open/unlock them if they're already opened/unlocked.
foreach (var target in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(args.Performer, transform), args.Range, flags: LookupFlags.Dynamic | LookupFlags.Static))
{
if (!_interaction.InRangeUnobstructed(args.Performer, target, range: 0, collisionMask: CollisionGroup.Opaque))
continue;
if (TryComp<DoorBoltComponent>(target, out var doorBoltComp) && doorBoltComp.BoltsDown)
_door.SetBoltsDown((target, doorBoltComp), false, predicted: true);
if (TryComp<DoorComponent>(target, out var doorComp) && doorComp.State is not DoorState.Open)
_door.StartOpening(target);
if (TryComp<LockComponent>(target, out var lockComp) && lockComp.Locked)
_lock.Unlock(target, args.Performer, lockComp);
}
}
// End Knock Spells
#endregion
#region Charge Spells
// TODO: Future support to charge other items
private void OnChargeSpell(ChargeSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !TryComp<HandsComponent>(ev.Performer, out var handsComp))
return;
EntityUid? wand = null;
foreach (var item in _hands.EnumerateHeld(ev.Performer, handsComp))
{
if (!_tag.HasTag(item, ev.WandTag))
continue;
wand = item;
}
ev.Handled = true;
Speak(ev);
if (wand == null || !TryComp<BasicEntityAmmoProviderComponent>(wand, out var basicAmmoComp) || basicAmmoComp.Count == null)
return;
_gunSystem.UpdateBasicEntityAmmoCount(wand.Value, basicAmmoComp.Count.Value + ev.Charge, basicAmmoComp);
}
// End Charge Spells
#endregion
// End Spells
#endregion
// When any spell is cast it will raise this as an event, so then it can be played in server or something. At least until chat gets moved to shared
// TODO: Temp until chat is in shared
private void Speak(BaseActionEvent args)
{
if (args is not ISpeakSpell speak || string.IsNullOrWhiteSpace(speak.Speech))
return;
var ev = new SpeakSpellEvent(args.Performer, speak.Speech);
RaiseLocalEvent(ref ev);
}
}

View File

@@ -0,0 +1,96 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Content.Shared.Interaction.Events;
using Content.Shared.Magic.Components;
using Content.Shared.Mind;
using Robust.Shared.Network;
namespace Content.Shared.Magic;
public sealed class SpellbookSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly INetManager _netManager = default!;
public override void Initialize()
{
SubscribeLocalEvent<SpellbookComponent, MapInitEvent>(OnInit, before: [typeof(SharedMagicSystem)]);
SubscribeLocalEvent<SpellbookComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<SpellbookComponent, SpellbookDoAfterEvent>(OnDoAfter);
}
private void OnInit(Entity<SpellbookComponent> ent, ref MapInitEvent args)
{
foreach (var (id, charges) in ent.Comp.SpellActions)
{
var spell = _actionContainer.AddAction(ent, id);
if (spell == null)
continue;
int? charge = charges;
if (_actions.GetCharges(spell) != null)
charge = _actions.GetCharges(spell);
_actions.SetCharges(spell, charge < 0 ? null : charge);
ent.Comp.Spells.Add(spell.Value);
}
}
private void OnUse(Entity<SpellbookComponent> ent, ref UseInHandEvent args)
{
if (args.Handled)
return;
AttemptLearn(ent, args);
args.Handled = true;
}
private void OnDoAfter<T>(Entity<SpellbookComponent> ent, ref T args) where T : DoAfterEvent // Sometimes i despise this language
{
if (args.Handled || args.Cancelled)
return;
args.Handled = true;
if (!ent.Comp.LearnPermanently)
{
_actions.GrantActions(args.Args.User, ent.Comp.Spells, ent);
return;
}
if (_mind.TryGetMind(args.Args.User, out var mindId, out _))
{
var mindActionContainerComp = EnsureComp<ActionsContainerComponent>(mindId);
if (_netManager.IsServer)
_actionContainer.TransferAllActionsWithNewAttached(ent, mindId, args.Args.User, newContainer: mindActionContainerComp);
}
else
{
foreach (var (id, charges) in ent.Comp.SpellActions)
{
EntityUid? actionId = null;
if (_actions.AddAction(args.Args.User, ref actionId, id))
_actions.SetCharges(actionId, charges < 0 ? null : charges);
}
}
ent.Comp.SpellActions.Clear();
}
private void AttemptLearn(Entity<SpellbookComponent> ent, UseInHandEvent args)
{
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, ent.Comp.LearnTime, new SpellbookDoAfterEvent(), ent, target: ent)
{
BreakOnMove = true,
BreakOnDamage = true,
NeedHand = true //What, are you going to read with your eyes only??
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
}
}

View File

@@ -26,7 +26,6 @@ public abstract class SharedMindSystem : EntitySystem
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedPlayerSystem _player = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly ISharedPlayerManager _playerMan = default!;
[ViewVariables]
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
@@ -383,6 +382,30 @@ public abstract class SharedMindSystem : EntitySystem
return false;
}
/// <summary>
/// Tries to find an objective that has the same prototype as the argument.
/// </summary>
/// <remarks>
/// Will not work for objectives that have no prototype, or duplicate objectives with the same prototype.
/// <//remarks>
public bool TryFindObjective(Entity<MindComponent?> mind, string prototype, [NotNullWhen(true)] out EntityUid? objective)
{
objective = null;
if (!Resolve(mind, ref mind.Comp))
return false;
foreach (var uid in mind.Comp.Objectives)
{
if (MetaData(uid).EntityPrototype?.ID == prototype)
{
objective = uid;
return true;
}
}
return false;
}
public bool TryGetSession(EntityUid? mindId, [NotNullWhen(true)] out ICommonSession? session)
{
session = null;

View File

@@ -10,12 +10,15 @@ using Content.Shared.Item;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Pointing;
using Content.Shared.Projectiles;
using Content.Shared.Pulling.Events;
using Content.Shared.Speech;
using Content.Shared.Standing;
using Content.Shared.Strip.Components;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
namespace Content.Shared.Mobs.Systems;
@@ -43,6 +46,7 @@ public partial class MobStateSystem
SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
SubscribeLocalEvent<MobStateComponent, PreventCollideEvent>(OnPreventCollide);
}
private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state)
@@ -175,5 +179,21 @@ public partial class MobStateSystem
args.Cancelled = true;
}
private void OnPreventCollide(Entity<MobStateComponent> ent, ref PreventCollideEvent args)
{
if (args.Cancelled)
return;
if (IsAlive(ent, ent))
return;
var other = args.OtherEntity;
if (HasComp<ProjectileComponent>(other) &&
CompOrNull<TargetedProjectileComponent>(other)?.Target != ent.Owner)
{
args.Cancelled = true;
}
}
#endregion
}

View File

@@ -14,6 +14,15 @@ namespace Content.Shared.Movement.Components
[DataField] public float PushStrength = 600f;
[DataField, AutoNetworkedField]
public float StepSoundMoveDistanceRunning = 2;
[DataField, AutoNetworkedField]
public float StepSoundMoveDistanceWalking = 1.5f;
[DataField, AutoNetworkedField]
public float FootstepVariation;
[ViewVariables(VVAccess.ReadWrite)]
public EntityCoordinates LastPosition { get; set; }

View File

@@ -58,11 +58,6 @@ namespace Content.Shared.Movement.Systems
protected EntityQuery<CanMoveInAirComponent> CanMoveInAirQuery;
protected EntityQuery<NoRotateOnMoveComponent> NoRotateQuery;
private const float StepSoundMoveDistanceRunning = 2;
private const float StepSoundMoveDistanceWalking = 1.5f;
private const float FootstepVariation = 0f;
/// <summary>
/// <see cref="CCVars.StopSpeed"/>
/// </summary>
@@ -258,7 +253,7 @@ namespace Content.Shared.Movement.Systems
var audioParams = sound.Params
.WithVolume(sound.Params.Volume + soundModifier)
.WithVariation(sound.Params.Variation ?? FootstepVariation);
.WithVariation(sound.Params.Variation ?? mobMover.FootstepVariation);
// If we're a relay target then predict the sound for all relays.
if (relayTarget != null)
@@ -404,7 +399,9 @@ namespace Content.Shared.Movement.Systems
return false;
var coordinates = xform.Coordinates;
var distanceNeeded = mover.Sprinting ? StepSoundMoveDistanceRunning : StepSoundMoveDistanceWalking;
var distanceNeeded = mover.Sprinting
? mobMover.StepSoundMoveDistanceRunning
: mobMover.StepSoundMoveDistanceWalking;
// Handle footsteps.
if (!weightless)

View File

@@ -1,6 +1,6 @@
using Content.Shared.Ninja.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
namespace Content.Shared.Ninja.Components;
@@ -35,4 +35,22 @@ public sealed partial class SpaceNinjaComponent : Component
/// </summary>
[DataField("katana"), AutoNetworkedField]
public EntityUid? Katana;
/// <summary>
/// Objective to complete after calling in a threat.
/// </summary>
[DataField]
public EntProtoId TerrorObjective = "TerrorObjective";
/// <summary>
/// Objective to complete after setting everyone to arrest.
/// </summary>
[DataField]
public EntProtoId MassArrestObjective = "MassArrestObjective";
/// <summary>
/// Objective to complete after the spider charge detonates.
/// </summary>
[DataField]
public EntProtoId SpiderChargeObjective = "SpiderChargeObjective";
}

View File

@@ -4,8 +4,8 @@ using Content.Shared.Charges.Systems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Examine;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
@@ -20,7 +20,7 @@ public sealed class DashAbilitySystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
@@ -79,11 +79,10 @@ public sealed class DashAbilitySystem : EntitySystem
_popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
return;
}
var origin = Transform(user).MapPosition;
var origin = _transform.GetMapCoordinates(user);
var target = args.Target.ToMap(EntityManager, _transform);
// prevent collision with the user duh
if (!_interaction.InRangeUnobstructed(origin, target, 0f, CollisionGroup.Opaque, uid => uid == user))
if (!_examine.InRangeUnOccluded(origin, target, SharedInteractionSystem.MaxRaycastRange, null))
{
// can only dash if the destination is visible on screen
_popup.PopupClient(Loc.GetString("dash-ability-cant-see", ("item", uid)), user, user);

View File

@@ -1,6 +1,7 @@
using Content.Shared.Actions;
using Content.Shared.CombatMode;
using Content.Shared.Communications;
using Content.Shared.CriminalRecords.Components;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
@@ -62,6 +63,7 @@ public abstract class SharedNinjaGlovesSystem : EntitySystem
RemComp<StunProviderComponent>(user);
RemComp<ResearchStealerComponent>(user);
RemComp<CommsHackerComponent>(user);
RemComp<CriminalRecordsHackerComponent>(user);
}
/// <summary>

View File

@@ -23,7 +23,6 @@ public abstract class SharedNavMapSystem : EntitySystem
public const int FloorMask = AllDirMask << (int) NavMapChunkType.Floor;
[Robust.Shared.IoC.Dependency] private readonly TagSystem _tagSystem = default!;
[Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
private readonly string[] _wallTags = ["Wall", "Window"];
private EntityQuery<NavMapDoorComponent> _doorQuery;

View File

@@ -17,8 +17,8 @@ namespace Content.Shared.Plunger.Systems;
/// </summary>
public sealed class PlungerSystem : EntitySystem
{
[Dependency] protected readonly IPrototypeManager _proto = default!;
[Dependency] protected readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;

View File

@@ -5,7 +5,6 @@ using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.Traits;
using Robust.Shared.Collections;
@@ -29,16 +28,101 @@ namespace Content.Shared.Preferences
public const int MaxNameLength = 32;
public const int MaxDescLength = 512;
private readonly Dictionary<string, JobPriority> _jobPriorities;
private readonly List<string> _antagPreferences;
private readonly List<string> _traitPreferences;
/// <summary>
/// Job preferences for initial spawn.
/// </summary>
[DataField]
private Dictionary<string, JobPriority> _jobPriorities = new()
{
{
SharedGameTicker.FallbackOverflowJob, JobPriority.High
}
};
/// <summary>
/// Antags we have opted in to.
/// </summary>
[DataField]
private HashSet<string> _antagPreferences = new();
/// <summary>
/// Enabled traits.
/// </summary>
[DataField]
private HashSet<string> _traitPreferences = new();
/// <summary>
/// <see cref="_loadouts"/>
/// </summary>
public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
private Dictionary<string, RoleLoadout> _loadouts;
[DataField]
private Dictionary<string, RoleLoadout> _loadouts = new();
// What in the lord is happening here.
private HumanoidCharacterProfile(
[DataField]
public string Name { get; set; } = "John Doe";
/// <summary>
/// Detailed text that can appear for the character if <see cref="CCVars.FlavorText"/> is enabled.
/// </summary>
[DataField]
public string FlavorText { get; set; } = string.Empty;
/// <summary>
/// Associated <see cref="SpeciesPrototype"/> for this profile.
/// </summary>
[DataField]
public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
[DataField]
public int Age { get; set; } = 18;
[DataField]
public Sex Sex { get; private set; } = Sex.Male;
[DataField]
public Gender Gender { get; private set; } = Gender.Male;
/// <summary>
/// <see cref="Appearance"/>
/// </summary>
public ICharacterAppearance CharacterAppearance => Appearance;
/// <summary>
/// Stores markings, eye colors, etc for the profile.
/// </summary>
[DataField]
public HumanoidCharacterAppearance Appearance { get; set; } = new();
/// <summary>
/// When spawning into a round what's the preferred spot to spawn.
/// </summary>
[DataField]
public SpawnPriorityPreference SpawnPriority { get; private set; } = SpawnPriorityPreference.None;
/// <summary>
/// <see cref="_jobPriorities"/>
/// </summary>
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
/// <summary>
/// <see cref="_antagPreferences"/>
/// </summary>
public IReadOnlySet<string> AntagPreferences => _antagPreferences;
/// <summary>
/// <see cref="_traitPreferences"/>
/// </summary>
public IReadOnlySet<string> TraitPreferences => _traitPreferences;
/// <summary>
/// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
/// </summary>
[DataField]
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } =
PreferenceUnavailableMode.SpawnAsOverflow;
public HumanoidCharacterProfile(
string name,
string flavortext,
string species,
@@ -49,8 +133,8 @@ namespace Content.Shared.Preferences
SpawnPriorityPreference spawnPriority,
Dictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
List<string> antagPreferences,
List<string> traitPreferences,
HashSet<string> antagPreferences,
HashSet<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
{
Name = name;
@@ -68,40 +152,21 @@ namespace Content.Shared.Preferences
_loadouts = loadouts;
}
/// <summary>Copy constructor but with overridable references (to prevent useless copies)</summary>
private HumanoidCharacterProfile(
HumanoidCharacterProfile other,
Dictionary<string, JobPriority> jobPriorities,
List<string> antagPreferences,
List<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.SpawnPriority,
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences, loadouts)
{
}
/// <summary>Copy constructor</summary>
private HumanoidCharacterProfile(HumanoidCharacterProfile other)
: this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences), new Dictionary<string, RoleLoadout>(other.Loadouts))
{
}
public HumanoidCharacterProfile(
string name,
string flavortext,
string species,
int age,
Sex sex,
Gender gender,
HumanoidCharacterAppearance appearance,
SpawnPriorityPreference spawnPriority,
IReadOnlyDictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
IReadOnlyList<string> antagPreferences,
IReadOnlyList<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(name, flavortext, species, age, sex, gender, appearance, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences), new Dictionary<string, RoleLoadout>(loadouts))
public HumanoidCharacterProfile(HumanoidCharacterProfile other)
: this(other.Name,
other.FlavorText,
other.Species,
other.Age,
other.Sex,
other.Gender,
other.Appearance.Clone(),
other.SpawnPriority,
new Dictionary<string, JobPriority>(other.JobPriorities),
other.PreferenceUnavailable,
new HashSet<string>(other.AntagPreferences),
new HashSet<string>(other.TraitPreferences),
new Dictionary<string, RoleLoadout>(other.Loadouts))
{
}
@@ -110,23 +175,7 @@ namespace Content.Shared.Preferences
/// Defaults to <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/> for the species.
/// </summary>
/// <returns></returns>
public HumanoidCharacterProfile() : this(
"John Doe",
"",
SharedHumanoidAppearanceSystem.DefaultSpecies,
18,
Sex.Male,
Gender.Male,
new HumanoidCharacterAppearance(),
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>(),
new Dictionary<string, RoleLoadout>())
public HumanoidCharacterProfile()
{
}
@@ -137,23 +186,10 @@ namespace Content.Shared.Preferences
/// <returns>Humanoid character profile with default settings.</returns>
public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies)
{
return new(
"John Doe",
"",
species,
18,
Sex.Male,
Gender.Male,
HumanoidCharacterAppearance.DefaultWithSpecies(species),
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>(),
new Dictionary<string, RoleLoadout>());
return new()
{
Species = species,
};
}
// TODO: This should eventually not be a visual change only.
@@ -198,36 +234,17 @@ namespace Content.Shared.Preferences
var name = GetName(species, gender);
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High},
}, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>(), new Dictionary<string, RoleLoadout>());
return new HumanoidCharacterProfile()
{
Name = name,
Sex = sex,
Age = age,
Gender = gender,
Species = species,
Appearance = HumanoidCharacterAppearance.Random(species, sex),
};
}
public string Name { get; private set; }
public string FlavorText { get; private set; }
public string Species { get; private set; }
[DataField("age")]
public int Age { get; private set; }
[DataField("sex")]
public Sex Sex { get; private set; }
[DataField("gender")]
public Gender Gender { get; private set; }
public ICharacterAppearance CharacterAppearance => Appearance;
[DataField("appearance")]
public HumanoidCharacterAppearance Appearance { get; private set; }
public SpawnPriorityPreference SpawnPriority { get; private set; }
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
public IReadOnlyList<string> AntagPreferences => _antagPreferences;
public IReadOnlyList<string> TraitPreferences => _traitPreferences;
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; }
public HumanoidCharacterProfile WithName(string name)
{
return new(this) { Name = name };
@@ -271,7 +288,10 @@ namespace Content.Shared.Preferences
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
{
return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences, _loadouts);
return new(this)
{
_jobPriorities = new Dictionary<string, JobPriority>(jobPriorities),
};
}
public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
@@ -285,7 +305,11 @@ namespace Content.Shared.Preferences
{
dictionary[jobId] = priority;
}
return new(this, dictionary, _antagPreferences, _traitPreferences, _loadouts);
return new(this)
{
_jobPriorities = dictionary,
};
}
public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
@@ -295,50 +319,47 @@ namespace Content.Shared.Preferences
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
{
return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences, _loadouts);
return new(this)
{
_antagPreferences = new HashSet<string>(antagPreferences),
};
}
public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
{
var list = new List<string>(_antagPreferences);
var list = new HashSet<string>(_antagPreferences);
if (pref)
{
if (!list.Contains(antagId))
{
list.Add(antagId);
}
list.Add(antagId);
}
else
{
if (list.Contains(antagId))
{
list.Remove(antagId);
}
list.Remove(antagId);
}
return new(this, _jobPriorities, list, _traitPreferences, _loadouts);
return new(this)
{
_antagPreferences = list,
};
}
public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref)
{
var list = new List<string>(_traitPreferences);
var list = new HashSet<string>(_traitPreferences);
// TODO: Maybe just refactor this to HashSet? Same with _antagPreferences
if (pref)
{
if (!list.Contains(traitId))
{
list.Add(traitId);
}
list.Add(traitId);
}
else
{
if (list.Contains(traitId))
{
list.Remove(traitId);
}
list.Remove(traitId);
}
return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
return new(this)
{
_traitPreferences = list,
};
}
public string Summary =>
@@ -497,10 +518,10 @@ namespace Content.Shared.Preferences
PreferenceUnavailable = prefsUnavailableMode;
_antagPreferences.Clear();
_antagPreferences.AddRange(antags);
_antagPreferences.UnionWith(antags);
_traitPreferences.Clear();
_traitPreferences.AddRange(traits);
_traitPreferences.UnionWith(traits);
// Checks prototypes exist for all loadouts and dump / set to default if not.
var toRemove = new ValueList<string>();
@@ -513,7 +534,7 @@ namespace Content.Shared.Preferences
continue;
}
loadouts.EnsureValid(session, collection);
loadouts.EnsureValid(this, session, collection);
}
foreach (var value in toRemove)
@@ -539,27 +560,26 @@ namespace Content.Shared.Preferences
public override bool Equals(object? obj)
{
return obj is HumanoidCharacterProfile other && MemberwiseEquals(other);
return ReferenceEquals(this, obj) || obj is HumanoidCharacterProfile other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(
HashCode.Combine(
Name,
Species,
Age,
Sex,
Gender,
Appearance
),
SpawnPriority,
PreferenceUnavailable,
_jobPriorities,
_antagPreferences,
_traitPreferences,
_loadouts
);
var hashCode = new HashCode();
hashCode.Add(_jobPriorities);
hashCode.Add(_antagPreferences);
hashCode.Add(_traitPreferences);
hashCode.Add(_loadouts);
hashCode.Add(Name);
hashCode.Add(FlavorText);
hashCode.Add(Species);
hashCode.Add(Age);
hashCode.Add((int)Sex);
hashCode.Add((int)Gender);
hashCode.Add(Appearance);
hashCode.Add((int)SpawnPriority);
hashCode.Add((int)PreferenceUnavailable);
return hashCode.ToHashCode();
}
public void SetLoadout(RoleLoadout loadout)
@@ -581,10 +601,12 @@ namespace Content.Shared.Preferences
}
copied[loadout.Role] = loadout.Clone();
return new(this, _jobPriorities, _antagPreferences, _traitPreferences, copied);
var profile = Clone();
profile._loadouts = copied;
return profile;
}
public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager)
public RoleLoadout GetLoadoutOrDefault(string id, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
{
if (!_loadouts.TryGetValue(id, out var loadout))
{
@@ -595,5 +617,10 @@ namespace Content.Shared.Preferences
loadout.SetDefault(protoManager);
return loadout;
}
public HumanoidCharacterProfile Clone()
{
return new HumanoidCharacterProfile(this);
}
}
}

View File

@@ -13,13 +13,13 @@ public sealed partial class GroupLoadoutEffect : LoadoutEffect
[DataField(required: true)]
public ProtoId<LoadoutEffectGroupPrototype> Proto;
public override bool Validate(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);
foreach (var effect in effectsProto.Effects)
{
if (!effect.Validate(loadout, session, collection, out reason))
if (!effect.Validate(profile, loadout, session, collection, out reason))
return false;
}

View File

@@ -15,7 +15,7 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
[DataField(required: true)]
public JobRequirement Requirement = default!;
public override bool Validate(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 manager = collection.Resolve<ISharedPlaytimeManager>();
var playtimes = manager.GetPlayTimes(session);

View File

@@ -11,6 +11,7 @@ public abstract partial class LoadoutEffect
/// Tries to validate the effect.
/// </summary>
public abstract bool Validate(
HumanoidCharacterProfile profile,
RoleLoadout loadout,
ICommonSession session,
IDependencyCollection collection,

View File

@@ -11,6 +11,7 @@ public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
public int Cost = 1;
public override bool Validate(
HumanoidCharacterProfile profile,
RoleLoadout loadout,
ICommonSession session,
IDependencyCollection collection,

View File

@@ -1,6 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
public sealed class SpeciesLoadoutEffect
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,
[NotNullWhen(false)] out FormattedMessage? reason)
{
if (Species.Contains(profile.Species))
{
reason = null;
return true;
}
reason = FormattedMessage.FromUnformatted(Loc.GetString("loadout-group-species-restriction"));
return false;
}
}

View File

@@ -6,8 +6,9 @@ namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Specifies the selected prototype and custom data for a loadout.
/// </summary>
[Serializable, NetSerializable]
public sealed class Loadout
[Serializable, NetSerializable, DataDefinition]
public sealed partial class Loadout
{
[DataField]
public ProtoId<LoadoutPrototype> Prototype;
}

View File

@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Random;
using Robust.Shared.Collections;
using Robust.Shared.Player;
@@ -11,11 +13,13 @@ namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Contains all of the selected data for a role's loadout.
/// </summary>
[Serializable, NetSerializable]
public sealed class RoleLoadout
[Serializable, NetSerializable, DataDefinition]
public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
{
public readonly ProtoId<RoleLoadoutPrototype> Role;
[DataField]
public ProtoId<RoleLoadoutPrototype> Role;
[DataField]
public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
/*
@@ -44,7 +48,7 @@ public sealed class RoleLoadout
/// <summary>
/// Ensures all prototypes exist and effects can be applied.
/// </summary>
public void EnsureValid(ICommonSession session, IDependencyCollection collection)
public void EnsureValid(HumanoidCharacterProfile profile, ICommonSession session, IDependencyCollection collection)
{
var groupRemove = new ValueList<string>();
var protoManager = collection.Resolve<IPrototypeManager>();
@@ -81,7 +85,7 @@ public sealed class RoleLoadout
}
// Validate the loadout can be applied (e.g. points).
if (!IsValid(session, loadout.Prototype, collection, out _))
if (!IsValid(profile, session, loadout.Prototype, collection, out _))
{
loadouts.RemoveAt(i);
continue;
@@ -167,7 +171,7 @@ public sealed class RoleLoadout
/// <summary>
/// Returns whether a loadout is valid or not.
/// </summary>
public bool IsValid(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;
@@ -180,7 +184,7 @@ public sealed class RoleLoadout
return false;
}
if (!protoManager.TryIndex(Role, out var roleProto))
if (!protoManager.HasIndex(Role))
{
reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
return false;
@@ -190,7 +194,7 @@ public sealed class RoleLoadout
foreach (var effect in loadoutProto.Effects)
{
valid = valid && effect.Validate(this, session, collection, out reason);
valid = valid && effect.Validate(profile, this, session, collection, out reason);
}
return valid;
@@ -257,4 +261,21 @@ public sealed class RoleLoadout
return false;
}
public bool Equals(RoleLoadout? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Role.Equals(other.Role) && SelectedLoadouts.SequenceEqual(other.SelectedLoadouts) && Points == other.Points;
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is RoleLoadout other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Role, SelectedLoadouts, Points);
}
}

View File

@@ -0,0 +1,53 @@
using Content.Shared.Radio;
using Content.Shared.Robotics;
using Content.Shared.Robotics.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Robotics.Components;
/// <summary>
/// Robotics console for managing borgs.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedRoboticsConsoleSystem))]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class RoboticsConsoleComponent : Component
{
/// <summary>
/// Address and data of each cyborg.
/// </summary>
[DataField]
public Dictionary<string, CyborgControlData> Cyborgs = new();
/// <summary>
/// After not responding for this length of time borgs are removed from the console.
/// </summary>
[DataField]
public TimeSpan Timeout = TimeSpan.FromSeconds(10);
/// <summary>
/// Radio channel to send messages on.
/// </summary>
[DataField]
public ProtoId<RadioChannelPrototype> RadioChannel = "Science";
/// <summary>
/// Radio message sent when destroying a borg.
/// </summary>
[DataField]
public LocId DestroyMessage = "robotics-console-cyborg-destroyed";
/// <summary>
/// Cooldown on destroying borgs to prevent complete abuse.
/// </summary>
[DataField]
public TimeSpan DestroyCooldown = TimeSpan.FromSeconds(30);
/// <summary>
/// When a borg can next be destroyed.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan NextDestroy = TimeSpan.Zero;
}

View File

@@ -0,0 +1,126 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
namespace Content.Shared.Robotics;
[Serializable, NetSerializable]
public enum RoboticsConsoleUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class RoboticsConsoleState : BoundUserInterfaceState
{
/// <summary>
/// Map of device network addresses to cyborg data.
/// </summary>
public Dictionary<string, CyborgControlData> Cyborgs;
public RoboticsConsoleState(Dictionary<string, CyborgControlData> cyborgs)
{
Cyborgs = cyborgs;
}
}
/// <summary>
/// Message to disable the selected cyborg.
/// </summary>
[Serializable, NetSerializable]
public sealed class RoboticsConsoleDisableMessage : BoundUserInterfaceMessage
{
public readonly string Address;
public RoboticsConsoleDisableMessage(string address)
{
Address = address;
}
}
/// <summary>
/// Message to destroy the selected cyborg.
/// </summary>
[Serializable, NetSerializable]
public sealed class RoboticsConsoleDestroyMessage : BoundUserInterfaceMessage
{
public readonly string Address;
public RoboticsConsoleDestroyMessage(string address)
{
Address = address;
}
}
/// <summary>
/// All data a client needs to render the console UI for a single cyborg.
/// Created by <c>BorgTransponderComponent</c> and sent to clients by <c>RoboticsConsoleComponent</c>.
/// </summary>
[DataRecord, Serializable, NetSerializable]
public record struct CyborgControlData
{
/// <summary>
/// Texture of the borg chassis.
/// </summary>
[DataField(required: true)]
public SpriteSpecifier? ChassisSprite;
/// <summary>
/// Name of the borg chassis.
/// </summary>
[DataField(required: true)]
public string ChassisName = string.Empty;
/// <summary>
/// Name of the borg's entity, including its silicon id.
/// </summary>
[DataField(required: true)]
public string Name = string.Empty;
/// <summary>
/// Battery charge from 0 to 1.
/// </summary>
[DataField]
public float Charge;
/// <summary>
/// How many modules this borg has, just useful information for roboticists.
/// Lets them keep track of the latejoin borgs that need new modules and stuff.
/// </summary>
[DataField]
public int ModuleCount;
/// <summary>
/// Whether the borg has a brain installed or not.
/// </summary>
[DataField]
public bool HasBrain;
/// <summary>
/// When this cyborg's data will be deleted.
/// Set by the console when receiving the packet.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan Timeout = TimeSpan.Zero;
public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain)
{
ChassisSprite = chassisSprite;
ChassisName = chassisName;
Name = name;
Charge = charge;
ModuleCount = moduleCount;
HasBrain = hasBrain;
}
}
public static class RoboticsConsoleConstants
{
// broadcast by cyborgs on Robotics Console frequency
public const string NET_CYBORG_DATA = "cyborg-data";
// sent by robotics console to cyborgs on Cyborg Control frequency
public const string NET_DISABLE_COMMAND = "cyborg-disable";
public const string NET_DESTROY_COMMAND = "cyborg-destroy";
}

View File

@@ -0,0 +1,8 @@
namespace Content.Shared.Robotics.Systems;
/// <summary>
/// Does nothing, only exists for access right now.
/// </summary>
public abstract class SharedRoboticsConsoleSystem : EntitySystem
{
}

View File

@@ -0,0 +1,43 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
namespace Content.Shared.Silicons.Borgs.Components;
/// <summary>
/// Periodically broadcasts borg data to robotics consoles.
/// When not emagged, handles disabling and destroying commands as expected.
/// </summary>
[RegisterComponent, Access(typeof(SharedBorgSystem))]
public sealed partial class BorgTransponderComponent : Component
{
/// <summary>
/// Sprite of the chassis to send.
/// </summary>
[DataField(required: true)]
public SpriteSpecifier? Sprite;
/// <summary>
/// Name of the chassis to send.
/// </summary>
[DataField(required: true)]
public string Name = string.Empty;
/// <summary>
/// Popup shown to everyone when a borg is disabled.
/// Gets passed a string "name".
/// </summary>
[DataField]
public LocId DisabledPopup = "borg-transponder-disabled-popup";
/// <summary>
/// How long to wait between each broadcast.
/// </summary>
[DataField]
public TimeSpan BroadcastDelay = TimeSpan.FromSeconds(5);
/// <summary>
/// When to next broadcast data.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextBroadcast = TimeSpan.Zero;
}

View File

@@ -39,6 +39,13 @@ public partial class SiliconLaw : IComparable<SiliconLaw>
return Order.CompareTo(other.Order);
}
public bool Equals(SiliconLaw other)
{
return LawString == other.LawString
&& Order == other.Order
&& LawIdentifierOverride == other.LawIdentifierOverride;
}
/// <summary>
/// Return a shallow clone of this law.
/// </summary>

View File

@@ -67,7 +67,7 @@ public sealed class SlipperySystem : EntitySystem
&& _statusEffects.CanApplyEffect(toSlip, "Stun"); //Should be KnockedDown instead?
}
private void TrySlip(EntityUid uid, SlipperyComponent component, EntityUid other)
public void TrySlip(EntityUid uid, SlipperyComponent component, EntityUid other, bool requiresContact = true)
{
if (HasComp<KnockedDownComponent>(other) && !component.SuperSlippery)
return;
@@ -89,7 +89,7 @@ public sealed class SlipperySystem : EntitySystem
{
_physics.SetLinearVelocity(other, physics.LinearVelocity * component.LaunchForwardsMultiplier, body: physics);
if (component.SuperSlippery)
if (component.SuperSlippery && requiresContact)
{
var sliding = EnsureComp<SlidingComponent>(other);
sliding.CollidingEntities.Add(uid);

View File

@@ -1,6 +1,8 @@
using System.Linq;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
@@ -31,6 +33,34 @@ public abstract class SharedStationSpawningSystem : EntitySystem
_xformQuery = GetEntityQuery<TransformComponent>();
}
/// <summary>
/// Equips the given starting gears from a `RoleLoadout` onto an entity.
/// </summary>
public void EquipRoleLoadout(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto)
{
// Order loadout selections by the order they appear on the prototype.
foreach (var group in loadout.SelectedLoadouts.OrderBy(x => roleProto.Groups.FindIndex(e => e == x.Key)))
{
foreach (var items in group.Value)
{
if (!PrototypeManager.TryIndex(items.Prototype, out var loadoutProto))
{
Log.Error($"Unable to find loadout prototype for {items.Prototype}");
continue;
}
if (!PrototypeManager.TryIndex(loadoutProto.Equipment, out var startingGear))
{
Log.Error($"Unable to find starting gear {loadoutProto.Equipment} for loadout {loadoutProto}");
continue;
}
// Handle any extra data here.
EquipStartingGear(entity, startingGear, raiseEvent: false);
}
}
}
/// <summary>
/// <see cref="EquipStartingGear(Robust.Shared.GameObjects.EntityUid,System.Nullable{Robust.Shared.Prototypes.ProtoId{Content.Shared.Roles.StartingGearPrototype}},bool)"/>
/// </summary>

View File

@@ -37,17 +37,22 @@ public sealed class GeneralStationRecordConsoleState : BoundUserInterfaceState
public readonly GeneralStationRecord? Record;
public readonly Dictionary<uint, string>? RecordListing;
public readonly StationRecordsFilter? Filter;
public readonly bool CanDeleteEntries;
public GeneralStationRecordConsoleState(uint? key, GeneralStationRecord? record,
Dictionary<uint, string>? recordListing, StationRecordsFilter? newFilter)
public GeneralStationRecordConsoleState(uint? key,
GeneralStationRecord? record,
Dictionary<uint, string>? recordListing,
StationRecordsFilter? newFilter,
bool canDeleteEntries)
{
SelectedKey = key;
Record = record;
RecordListing = recordListing;
Filter = newFilter;
CanDeleteEntries = canDeleteEntries;
}
public GeneralStationRecordConsoleState() : this(null, null, null, null)
public GeneralStationRecordConsoleState() : this(null, null, null, null, false)
{
}
@@ -69,3 +74,15 @@ public sealed class SelectStationRecord : BoundUserInterfaceMessage
SelectedKey = selectedKey;
}
}
[Serializable, NetSerializable]
public sealed class DeleteStationRecord : BoundUserInterfaceMessage
{
public DeleteStationRecord(uint id)
{
Id = id;
}
public readonly uint Id;
}

View File

@@ -77,7 +77,7 @@ public sealed class MagnetPickupSystem : EntitySystem
// the problem is that stack pickups delete the original entity, which is fine, but due to
// game state handling we can't show a lerp animation for it.
var nearXform = Transform(near);
var nearMap = nearXform.MapPosition;
var nearMap = _transform.GetMapCoordinates(near, xform: nearXform);
var nearCoords = EntityCoordinates.FromMap(moverCoords.EntityId, nearMap, _transform, EntityManager);
if (!_storage.Insert(uid, near, out var stacked, storageComp: storage, playSound: !playedSound))

View File

@@ -111,6 +111,7 @@ public abstract class SharedStorageSystem : EntitySystem
SubscribeLocalEvent<StorageComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<StorageComponent, DestructionEventArgs>(OnDestroy);
SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
SubscribeLocalEvent<StorageComponent, LockToggledEvent>(OnLockToggled);
SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
@@ -1406,6 +1407,25 @@ public abstract class SharedStorageSystem : EntitySystem
return _nextSmallest[item.Size];
}
/// <summary>
/// Checks if a storage's UI is open by anyone when locked, and closes it unless they're an admin.
/// </summary>
private void OnLockToggled(EntityUid uid, StorageComponent component, ref LockToggledEvent args)
{
if (!args.Locked)
return;
// Gets everyone looking at the UI
foreach (var actor in _ui.GetActors(uid, StorageComponent.StorageUiKey.Key).ToList())
{
if (_admin.HasAdminFlag(actor, AdminFlags.Admin))
continue;
// And closes it unless they're an admin
_ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor);
}
}
private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args)
{
if (_containerSystem.TryGetContainingContainer(uid, out var container, component) &&
@@ -1418,15 +1438,15 @@ public abstract class SharedStorageSystem : EntitySystem
private void HandleOpenBackpack(ICommonSession? session)
{
HandleOpenSlotUI(session, "back");
HandleToggleSlotUI(session, "back");
}
private void HandleOpenBelt(ICommonSession? session)
{
HandleOpenSlotUI(session, "belt");
HandleToggleSlotUI(session, "belt");
}
private void HandleOpenSlotUI(ICommonSession? session, string slot)
private void HandleToggleSlotUI(ICommonSession? session, string slot)
{
if (session is not { } playerSession)
return;
@@ -1440,7 +1460,14 @@ public abstract class SharedStorageSystem : EntitySystem
if (!ActionBlocker.CanInteract(playerEnt, storageEnt))
return;
OpenStorageUI(storageEnt.Value, playerEnt);
if (!_ui.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt))
{
OpenStorageUI(storageEnt.Value, playerEnt);
}
else
{
_ui.CloseUi(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt);
}
}
protected void ClearCantFillReasons()

View File

@@ -75,14 +75,14 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
public EntProtoId? ProductAction;
/// <summary>
/// The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
/// upgrade or to use standalone as an upgrade
/// The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
/// upgrade or to use standalone as an upgrade
/// </summary>
[DataField]
public ProtoId<ListingPrototype>? ProductUpgradeID;
public ProtoId<ListingPrototype>? ProductUpgradeId;
/// <summary>
/// Keeps track of the current action entity this is tied to, for action upgrades
/// Keeps track of the current action entity this is tied to, for action upgrades
/// </summary>
[DataField]
[NonSerialized]
@@ -161,7 +161,7 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
Priority = Priority,
ProductEntity = ProductEntity,
ProductAction = ProductAction,
ProductUpgradeID = ProductUpgradeID,
ProductUpgradeId = ProductUpgradeId,
ProductActionEntity = ProductActionEntity,
ProductEvent = ProductEvent,
PurchaseAmount = PurchaseAmount,

View File

@@ -16,7 +16,7 @@ namespace Content.Shared.Tabletop
[Dependency] protected readonly ActionBlockerSystem ActionBlockerSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _transforms = default!;
[Dependency] protected readonly SharedTransformSystem Transforms = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
public override void Initialize()
@@ -41,8 +41,8 @@ namespace Content.Shared.Tabletop
// Move the entity and dirty it (we use the map ID from the entity so noone can try to be funny and move the item to another map)
var transform = EntityManager.GetComponent<TransformComponent>(moved);
_transforms.SetParent(moved, transform, _mapMan.GetMapEntityId(transform.MapID));
_transforms.SetLocalPositionNoLerp(transform, msg.Coordinates.Position);
Transforms.SetParent(moved, transform, _mapMan.GetMapEntityId(transform.MapID));
Transforms.SetLocalPositionNoLerp(transform, msg.Coordinates.Position);
}
private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg, EntitySessionEventArgs args)

View File

@@ -8,5 +8,7 @@ namespace Content.Shared.Traits.Assorted;
[RegisterComponent, NetworkedComponent]
public sealed partial class PermanentBlindnessComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField]
public int Blindness = 0; // How damaged should their eyes be. Set 0 for maximum damage.
}

View File

@@ -18,15 +18,14 @@ public sealed class PermanentBlindnessSystem : EntitySystem
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<PermanentBlindnessComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<PermanentBlindnessComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PermanentBlindnessComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<PermanentBlindnessComponent, EyeDamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<PermanentBlindnessComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(Entity<PermanentBlindnessComponent> blindness, ref ExaminedEvent args)
{
if (args.IsInDetailsRange && !_net.IsClient)
if (args.IsInDetailsRange && !_net.IsClient && blindness.Comp.Blindness == 0)
{
args.PushMarkup(Loc.GetString("permanent-blindness-trait-examined", ("target", Identity.Entity(blindness, EntityManager))));
}
@@ -37,28 +36,17 @@ public sealed class PermanentBlindnessSystem : EntitySystem
_blinding.UpdateIsBlind(blindness.Owner);
}
private void OnStartup(Entity<PermanentBlindnessComponent> blindness, ref ComponentStartup args)
private void OnMapInit(Entity<PermanentBlindnessComponent> blindness, ref MapInitEvent args)
{
if (!_entityManager.TryGetComponent<BlindableComponent>(blindness, out var blindable))
return;
var damageToDeal = (int) BlurryVisionComponent.MaxMagnitude - blindable.EyeDamage;
if (damageToDeal <= 0)
return;
_blinding.AdjustEyeDamage(blindness.Owner, damageToDeal);
}
private void OnDamageChanged(Entity<PermanentBlindnessComponent> blindness, ref EyeDamageChangedEvent args)
{
if (args.Damage >= BlurryVisionComponent.MaxMagnitude)
return;
if (!_entityManager.TryGetComponent<BlindableComponent>(blindness, out var blindable))
return;
var damageRestoration = (int) BlurryVisionComponent.MaxMagnitude - args.Damage;
_blinding.AdjustEyeDamage(blindness.Owner, damageRestoration);
if (blindness.Comp.Blindness != 0)
_blinding.SetMinDamage(new Entity<BlindableComponent?>(blindness.Owner, blindable), blindness.Comp.Blindness);
else
{
var maxMagnitudeInt = (int) BlurryVisionComponent.MaxMagnitude;
_blinding.SetMinDamage(new Entity<BlindableComponent?>(blindness.Owner, blindable), maxMagnitudeInt);
}
}
}

View File

@@ -220,7 +220,7 @@ public abstract partial class SharedTetherGunSystem : EntitySystem
_blocker.UpdateCanMove(target);
// Invisible tether entity
var tether = Spawn("TetherEntity", Transform(target).MapPosition);
var tether = Spawn("TetherEntity", TransformSystem.GetMapCoordinates(target));
var tetherPhysics = Comp<PhysicsComponent>(tether);
component.TetherEntity = tether;
_physics.WakeBody(tether);

View File

@@ -1,5 +1,3 @@
using Content.Shared.Damage;
using Content.Shared.Tag;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio;
@@ -139,6 +137,12 @@ public sealed partial class GunComponent : Component
[ViewVariables]
public EntityCoordinates? ShootCoordinates = null;
/// <summary>
/// Who the gun is being requested to shoot at directly.
/// </summary>
[ViewVariables]
public EntityUid? Target = null;
/// <summary>
/// The base value for how many shots to fire per burst.
/// </summary>

Some files were not shown because too many files have changed in this diff Show More