Merge remote-tracking branch 'upstream/stable' into ed-29-10-2024-upstream

# Conflicts:
#	Content.Server/Chat/Managers/ChatSanitizationManager.cs
#	Content.Server/Temperature/Systems/TemperatureSystem.cs
#	Content.Shared/Localizations/ContentLocalizationManager.cs
This commit is contained in:
Ed
2024-10-29 11:16:56 +03:00
388 changed files with 34535 additions and 18822 deletions

View File

@@ -178,15 +178,13 @@ namespace Content.Shared.APC
public sealed class ApcBoundInterfaceState : BoundUserInterfaceState, IEquatable<ApcBoundInterfaceState>
{
public readonly bool MainBreaker;
public readonly bool HasAccess;
public readonly int Power;
public readonly ApcExternalPowerState ApcExternalPower;
public readonly float Charge;
public ApcBoundInterfaceState(bool mainBreaker, bool hasAccess, int power, ApcExternalPowerState apcExternalPower, float charge)
public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge)
{
MainBreaker = mainBreaker;
HasAccess = hasAccess;
Power = power;
ApcExternalPower = apcExternalPower;
Charge = charge;
@@ -197,7 +195,6 @@ namespace Content.Shared.APC
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);
@@ -210,7 +207,7 @@ namespace Content.Shared.APC
public override int GetHashCode()
{
return HashCode.Combine(MainBreaker, HasAccess, Power, (int) ApcExternalPower, Charge);
return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge);
}
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Actions;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Emoting;
using Content.Shared.Examine;
using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.IdentityManagement;
@@ -61,6 +62,7 @@ public sealed partial class SleepingSystem : EntitySystem
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
SubscribeLocalEvent<SleepingComponent, EmoteAttemptEvent>(OnEmoteAttempt);
}
private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
@@ -310,6 +312,14 @@ public sealed partial class SleepingSystem : EntitySystem
Wake((ent, ent.Comp));
return true;
}
/// <summary>
/// Prevents the use of emote actions while sleeping
/// </summary>
public void OnEmoteAttempt(Entity<SleepingComponent> ent, ref EmoteAttemptEvent args)
{
args.Cancel();
}
}

View File

@@ -45,6 +45,21 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> DefaultGuide =
CVarDef.Create("server.default_guide", "NewPlayer", CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// If greater than 0, automatically restart the server after this many minutes of uptime.
/// </summary>
/// <remarks>
/// <para>
/// This is intended to work around various bugs and performance issues caused by long continuous server uptime.
/// </para>
/// <para>
/// This uses the same non-disruptive logic as update restarts,
/// i.e. the game will only restart at round end or when there is nobody connected.
/// </para>
/// </remarks>
public static readonly CVarDef<int> ServerUptimeRestartMinutes =
CVarDef.Create("server.uptime_restart_minutes", 0, CVar.SERVERONLY);
/*
* Ambience
*/
@@ -449,6 +464,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<float> GameEntityMenuLookup =
CVarDef.Create("game.entity_menu_lookup", 0.25f, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Should the clients window show the server hostname in the title?
/// </summary>
public static readonly CVarDef<bool> GameHostnameInTitlebar =
CVarDef.Create("game.hostname_in_titlebar", true, CVar.SERVER | CVar.REPLICATED);
/*
* Discord
*/

View File

@@ -84,6 +84,35 @@ public abstract class SharedChatSystem : EntitySystem
return current ?? _prototypeManager.Index<SpeechVerbPrototype>(speech.SpeechVerb);
}
/// <summary>
/// Splits the input message into a radio prefix part and the rest to preserve it during sanitization.
/// </summary>
/// <remarks>
/// This is primarily for the chat emote sanitizer, which can match against ":b" as an emote, which is a valid radio keycode.
/// </remarks>
public void GetRadioKeycodePrefix(EntityUid source,
string input,
out string output,
out string prefix)
{
prefix = string.Empty;
output = input;
// If the string is less than 2, then it's probably supposed to be an emote.
// No one is sending empty radio messages!
if (input.Length <= 2)
return;
if (!(input.StartsWith(RadioChannelPrefix) || input.StartsWith(RadioChannelAltPrefix)))
return;
if (!_keyCodes.TryGetValue(char.ToLower(input[1]), out _))
return;
prefix = input[..2];
output = input[2..];
}
/// <summary>
/// Attempts to resolve radio prefixes in chat messages (e.g., remove a leading ":e" and resolve the requested
/// channel. Returns true if a radio message was attempted, even if the channel is invalid.

View File

@@ -0,0 +1,13 @@
namespace Content.Shared.Chemistry.Events;
/// <summary>
/// Raised directed on an entity when it embeds in another entity.
/// </summary>
[ByRefEvent]
public readonly record struct InjectOverTimeEvent(EntityUid embeddedIntoUid)
{
/// <summary>
/// Entity that is embedded in.
/// </summary>
public readonly EntityUid EmbeddedIntoUid = embeddedIntoUid;
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Ghost;
/// <summary>
/// Marker component to identify "ghostly" entities.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class SpectralComponent : Component { }

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Inventory.VirtualItem;
@@ -130,7 +131,7 @@ public abstract partial class SharedHandsSystem
TransformSystem.DropNextTo((entity, itemXform), (uid, userXform));
return true;
}
// drop the item with heavy calculations from their hands and place it at the calculated interaction range position
// The DoDrop is handle if there's no drop target
DoDrop(uid, hand, doDropInteraction: doDropInteraction, handsComp);
@@ -138,7 +139,7 @@ public abstract partial class SharedHandsSystem
// if there's no drop location stop here
if (targetDropLocation == null)
return true;
// otherwise, also move dropped item and rotate it properly according to grid/map
var (itemPos, itemRot) = TransformSystem.GetWorldPositionRotation(entity);
var origin = new MapCoordinates(itemPos, itemXform.MapID);
@@ -197,7 +198,7 @@ public abstract partial class SharedHandsSystem
/// <summary>
/// Removes the contents of a hand from its container. Assumes that the removal is allowed. In general, you should not be calling this directly.
/// </summary>
public virtual void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? handsComp = null)
public virtual void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? handsComp = null, bool log = true)
{
if (!Resolve(uid, ref handsComp))
return;
@@ -221,6 +222,9 @@ public abstract partial class SharedHandsSystem
if (doDropInteraction)
_interactionSystem.DroppedInteraction(uid, entity);
if (log)
_adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(uid):user} dropped {ToPrettyString(entity):entity}");
if (hand == handsComp.ActiveHand)
RaiseLocalEvent(entity, new HandDeselectedEvent(uid));
}

View File

@@ -178,8 +178,8 @@ public abstract partial class SharedHandsSystem : EntitySystem
if (!CanPickupToHand(uid, entity, handsComp.ActiveHand, checkActionBlocker, handsComp))
return false;
DoDrop(uid, hand, false, handsComp);
DoPickup(uid, handsComp.ActiveHand, entity, handsComp);
DoDrop(uid, hand, false, handsComp, log:false);
DoPickup(uid, handsComp.ActiveHand, entity, handsComp, log: false);
return true;
}

View File

@@ -220,7 +220,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
/// <summary>
/// Puts an entity into the player's hand, assumes that the insertion is allowed. In general, you should not be calling this function directly.
/// </summary>
public virtual void DoPickup(EntityUid uid, Hand hand, EntityUid entity, HandsComponent? hands = null)
public virtual void DoPickup(EntityUid uid, Hand hand, EntityUid entity, HandsComponent? hands = null, bool log = true)
{
if (!Resolve(uid, ref hands))
return;
@@ -235,7 +235,8 @@ public abstract partial class SharedHandsSystem : EntitySystem
return;
}
_adminLogger.Add(LogType.Pickup, LogImpact.Low, $"{ToPrettyString(uid):user} picked up {ToPrettyString(entity):entity}");
if (log)
_adminLogger.Add(LogType.Pickup, LogImpact.Low, $"{ToPrettyString(uid):user} picked up {ToPrettyString(entity):entity}");
Dirty(uid, hands);

View File

@@ -94,22 +94,38 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// </summary>
public void AddImplants(EntityUid uid, IEnumerable<String> implants)
{
var coords = Transform(uid).Coordinates;
foreach (var id in implants)
{
var ent = Spawn(id, coords);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
}
AddImplant(uid, id);
}
}
/// <summary>
/// Adds a single implant to a person, and returns the implant.
/// Logs any implant ids that don't have <see cref="SubdermalImplantComponent"/>.
/// </summary>
/// <returns>
/// The implant, if it was successfully created. Otherwise, null.
/// </returns>>
public EntityUid? AddImplant(EntityUid uid, String implantId)
{
var coords = Transform(uid).Coordinates;
var ent = Spawn(implantId, coords);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
return null;
}
return ent;
}
/// <summary>
/// Forces an implant into a person
/// Good for on spawn related code or admin additions

View File

@@ -8,7 +8,7 @@ namespace Content.Shared.Interaction.Events;
/// </remarks>
public sealed class ContactInteractionEvent : HandledEntityEventArgs
{
public readonly EntityUid Other;
public EntityUid Other;
public ContactInteractionEvent(EntityUid other)
{

View File

@@ -3,5 +3,7 @@ namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised on the target when failing to pet/hug something.
/// </summary>
// TODO INTERACTION
// Rename this, or move it to another namespace to make it clearer that this is specific to "petting/hugging" (InteractionPopupSystem)
[ByRefEvent]
public readonly record struct InteractionFailureEvent(EntityUid User);

View File

@@ -3,5 +3,7 @@ namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised on the target when successfully petting/hugging something.
/// </summary>
// TODO INTERACTION
// Rename this, or move it to another namespace to make it clearer that this is specific to "petting/hugging" (InteractionPopupSystem)
[ByRefEvent]
public readonly record struct InteractionSuccessEvent(EntityUid User);

View File

@@ -456,8 +456,22 @@ namespace Content.Shared.Interaction
inRangeUnobstructed);
}
private bool IsDeleted(EntityUid uid)
{
return TerminatingOrDeleted(uid) || EntityManager.IsQueuedForDeletion(uid);
}
private bool IsDeleted(EntityUid? uid)
{
//optional / null entities can pass this validation check. I.e., is-deleted returns false for null uids
return uid != null && IsDeleted(uid.Value);
}
public void InteractHand(EntityUid user, EntityUid target)
{
if (IsDeleted(user) || IsDeleted(target))
return;
var complexInteractions = _actionBlockerSystem.CanComplexInteract(user);
if (!complexInteractions)
{
@@ -466,7 +480,8 @@ namespace Content.Shared.Interaction
checkCanInteract: false,
checkUseDelay: true,
checkAccess: false,
complexInteractions: complexInteractions);
complexInteractions: complexInteractions,
checkDeletion: false);
return;
}
@@ -479,6 +494,7 @@ namespace Content.Shared.Interaction
return;
}
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target, message, true);
@@ -487,18 +503,23 @@ namespace Content.Shared.Interaction
if (message.Handled)
return;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
// Else we run Activate.
InteractionActivate(user,
target,
checkCanInteract: false,
checkUseDelay: true,
checkAccess: false,
complexInteractions: complexInteractions);
complexInteractions: complexInteractions,
checkDeletion: false);
}
public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target,
EntityCoordinates clickLocation, bool inRangeUnobstructed)
{
if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
return;
if (target != null)
{
_adminLogger.Add(
@@ -514,9 +535,10 @@ namespace Content.Shared.Interaction
$"{ToPrettyString(user):user} interacted with *nothing* using {ToPrettyString(used):used}");
}
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed))
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false))
return;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
if (target != null)
{
var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation);
@@ -524,12 +546,12 @@ namespace Content.Shared.Interaction
// We contact the USED entity, but not the target.
DoContactInteraction(user, used, rangedMsg);
if (rangedMsg.Handled)
return;
}
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed);
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false);
}
protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates)
@@ -933,11 +955,18 @@ namespace Content.Shared.Interaction
EntityUid used,
EntityUid? target,
EntityCoordinates clickLocation,
bool canReach)
bool canReach,
bool checkDeletion = true)
{
if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
return false;
var ev = new BeforeRangedInteractEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(used, ev);
if (!ev.Handled)
return false;
// We contact the USED entity, but not the target.
DoContactInteraction(user, used, ev);
return ev.Handled;
@@ -966,6 +995,9 @@ namespace Content.Shared.Interaction
bool checkCanInteract = true,
bool checkCanUse = true)
{
if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
return false;
if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
return false;
@@ -977,9 +1009,10 @@ namespace Content.Shared.Interaction
LogImpact.Low,
$"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}");
if (RangedInteractDoBefore(user, used, target, clickLocation, true))
if (RangedInteractDoBefore(user, used, target, clickLocation, canReach: true, checkDeletion: false))
return true;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
// all interactions should only happen when in range / unobstructed, so no range check is needed
var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(target, interactUsingEvent, true);
@@ -989,8 +1022,10 @@ namespace Content.Shared.Interaction
if (interactUsingEvent.Handled)
return true;
if (InteractDoAfter(user, used, target, clickLocation, canReach: true))
if (InteractDoAfter(user, used, target, clickLocation, canReach: true, checkDeletion: false))
return true;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
return false;
}
@@ -1004,11 +1039,14 @@ namespace Content.Shared.Interaction
/// <param name="canReach">Whether the <paramref name="user"/> is in range of the <paramref name="target"/>.
/// </param>
/// <returns>True if the interaction was handled. Otherwise, false.</returns>
public bool InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach)
public bool InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach, bool checkDeletion = true)
{
if (target is { Valid: false })
target = null;
if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
return false;
var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(used, afterInteractEvent);
DoContactInteraction(user, used, afterInteractEvent);
@@ -1024,6 +1062,7 @@ namespace Content.Shared.Interaction
if (target == null)
return false;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
var afterInteractUsingEvent = new AfterInteractUsingEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(target.Value, afterInteractUsingEvent);
@@ -1034,9 +1073,7 @@ namespace Content.Shared.Interaction
// Contact interactions are currently only used for forensics, so we don't raise used -> target
}
if (afterInteractUsingEvent.Handled)
return true;
return false;
return afterInteractUsingEvent.Handled;
}
#region ActivateItemInWorld
@@ -1068,8 +1105,13 @@ namespace Content.Shared.Interaction
bool checkCanInteract = true,
bool checkUseDelay = true,
bool checkAccess = true,
bool? complexInteractions = null)
bool? complexInteractions = null,
bool checkDeletion = true)
{
if (checkDeletion && (IsDeleted(user) || IsDeleted(used)))
return false;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
_delayQuery.TryComp(used, out var delayComponent);
if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
return false;
@@ -1085,21 +1127,32 @@ namespace Content.Shared.Interaction
if (checkAccess && !IsAccessible(user, used))
return false;
complexInteractions ??= SupportsComplexInteractions(user);
complexInteractions ??= _actionBlockerSystem.CanComplexInteract(user);
var activateMsg = new ActivateInWorldEvent(user, used, complexInteractions.Value);
RaiseLocalEvent(used, activateMsg, true);
if (activateMsg.Handled)
{
DoContactInteraction(user, used);
if (!activateMsg.WasLogged)
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
if (delayComponent != null)
_useDelay.TryResetDelay(used, component: delayComponent);
return true;
}
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
var userEv = new UserActivateInWorldEvent(user, used, complexInteractions.Value);
RaiseLocalEvent(user, userEv, true);
if (!activateMsg.Handled && !userEv.Handled)
if (!userEv.Handled)
return false;
DoContactInteraction(user, used, activateMsg);
DoContactInteraction(user, used);
// Still need to call this even without checkUseDelay in case this gets relayed from Activate.
if (delayComponent != null)
_useDelay.TryResetDelay(used, component: delayComponent);
if (!activateMsg.WasLogged)
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
return true;
}
#endregion
@@ -1118,6 +1171,9 @@ namespace Content.Shared.Interaction
bool checkCanInteract = true,
bool checkUseDelay = true)
{
if (IsDeleted(user) || IsDeleted(used))
return false;
_delayQuery.TryComp(used, out var delayComponent);
if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
return true; // if the item is on cooldown, we consider this handled.
@@ -1138,8 +1194,9 @@ namespace Content.Shared.Interaction
return true;
}
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
// else, default to activating the item
return InteractionActivate(user, used, false, false, false);
return InteractionActivate(user, used, false, false, false, checkDeletion: false);
}
/// <summary>
@@ -1164,10 +1221,11 @@ namespace Content.Shared.Interaction
public void DroppedInteraction(EntityUid user, EntityUid item)
{
if (IsDeleted(user) || IsDeleted(item))
return;
var dropMsg = new DroppedEvent(user);
RaiseLocalEvent(item, dropMsg, true);
if (dropMsg.Handled)
_adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}");
// If the dropper is rotated then use their targetrelativerotation as the drop rotation
var rotation = Angle.Zero;
@@ -1314,15 +1372,21 @@ namespace Content.Shared.Interaction
if (uidB == null || args?.Handled == false)
return;
// Entities may no longer exist (banana was eaten, or human was exploded)?
if (!Exists(uidA) || !Exists(uidB))
if (uidA == uidB.Value)
return;
if (Paused(uidA) || Paused(uidB.Value))
if (!TryComp(uidA, out MetaDataComponent? metaA) || metaA.EntityPaused)
return;
RaiseLocalEvent(uidA, new ContactInteractionEvent(uidB.Value));
RaiseLocalEvent(uidB.Value, new ContactInteractionEvent(uidA));
if (!TryComp(uidB, out MetaDataComponent? metaB) || metaB.EntityPaused)
return ;
// TODO Struct event
var ev = new ContactInteractionEvent(uidB.Value);
RaiseLocalEvent(uidA, ev);
ev.Other = uidA;
RaiseLocalEvent(uidB.Value, ev);
}

View File

@@ -41,6 +41,8 @@ namespace Content.Shared.Localizations
_loc.AddFunction(culture, "LOC", FormatLoc);
_loc.AddFunction(culture, "NATURALFIXED", FormatNaturalFixed);
_loc.AddFunction(culture, "NATURALPERCENT", FormatNaturalPercent);
_loc.AddFunction(culture, "PLAYTIME", FormatPlaytime);
_loc.AddFunction(culture, "MANY", FormatMany); // TODO: Temporary fix for MANY() fluent errors. Remove after resolve errors.
/*
@@ -151,6 +153,16 @@ namespace Content.Shared.Localizations
return Loc.GetString($"zzzz-fmt-direction-{dir.ToString()}");
}
/// <summary>
/// Formats playtime as hours and minutes.
/// </summary>
public static string FormatPlaytime(TimeSpan time)
{
var hours = (int)time.TotalHours;
var minutes = time.Minutes;
return Loc.GetString($"zzzz-fmt-playtime", ("hours", hours), ("minutes", minutes));
}
private static ILocValue FormatLoc(LocArgs args)
{
var id = ((LocValueString) args.Args[0]).Value;
@@ -239,5 +251,15 @@ namespace Content.Shared.Localizations
return new LocValueString(res);
}
private static ILocValue FormatPlaytime(LocArgs args)
{
var time = TimeSpan.Zero;
if (args.Args is { Count: > 0 } && args.Args[0].Value is TimeSpan timeArg)
{
time = timeArg;
}
return new LocValueString(FormatPlaytime(time));
}
}
}

View File

@@ -483,19 +483,6 @@ public abstract class SharedMindSystem : EntitySystem
return false;
}
/// <summary>
/// Gets a role component from a player's mind.
/// </summary>
/// <returns>Whether a role was found</returns>
public bool TryGetRole<T>(EntityUid user, [NotNullWhen(true)] out T? role) where T : IComponent
{
role = default;
if (!TryComp<MindContainerComponent>(user, out var mindContainer) || mindContainer.Mind == null)
return false;
return TryComp(mindContainer.Mind, out role);
}
/// <summary>
/// Sets the Mind's UserId, Session, and updates the player's PlayerData. This should have no direct effect on the
/// entity that any mind is connected to, except as a side effect of the fact that it may change a player's

View File

@@ -27,6 +27,50 @@ public sealed partial class NavMapComponent : Component
/// </summary>
[ViewVariables]
public Dictionary<NetEntity, SharedNavMapSystem.NavMapBeacon> Beacons = new();
/// <summary>
/// Describes the properties of a region on the station.
/// It is indexed by the entity assigned as the region owner.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<NetEntity, SharedNavMapSystem.NavMapRegionProperties> RegionProperties = new();
/// <summary>
/// All flood filled regions, ready for display on a NavMapControl.
/// It is indexed by the entity assigned as the region owner.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<NetEntity, NavMapRegionOverlay> RegionOverlays = new();
/// <summary>
/// A queue of all region owners that are waiting their associated regions to be floodfilled.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Queue<NetEntity> QueuedRegionsToFlood = new();
/// <summary>
/// A look up table to get a list of region owners associated with a flood filled chunk.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<Vector2i, HashSet<NetEntity>> ChunkToRegionOwnerTable = new();
/// <summary>
/// A look up table to find flood filled chunks associated with a given region owner.
/// </summary>
/// <remarks>
/// For client use only
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<NetEntity, HashSet<Vector2i>> RegionOwnerToChunkTable = new();
}
[Serializable, NetSerializable]
@@ -51,10 +95,30 @@ public sealed class NavMapChunk(Vector2i origin)
public GameTick LastUpdate;
}
[Serializable, NetSerializable]
public sealed class NavMapRegionOverlay(Enum uiKey, List<(Vector2i, Vector2i)> gridCoords)
{
/// <summary>
/// The key to the UI that will be displaying this region on its navmap
/// </summary>
public Enum UiKey = uiKey;
/// <summary>
/// The local grid coordinates of the rectangles that make up the region
/// Item1 is the top left corner, Item2 is the bottom right corner
/// </summary>
public List<(Vector2i, Vector2i)> GridCoords = gridCoords;
/// <summary>
/// Color of the region
/// </summary>
public Color Color = Color.White;
}
public enum NavMapChunkType : byte
{
// Values represent bit shift offsets when retrieving data in the tile array.
Invalid = byte.MaxValue,
Invalid = byte.MaxValue,
Floor = 0, // I believe floors have directional information for diagonal tiles?
Wall = SharedNavMapSystem.Directions,
Airlock = 2 * SharedNavMapSystem.Directions,

View File

@@ -3,10 +3,9 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using Content.Shared.Tag;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Pinpointer;
@@ -16,7 +15,7 @@ public abstract class SharedNavMapSystem : EntitySystem
public const int Directions = 4; // Not directly tied to number of atmos directions
public const int ChunkSize = 8;
public const int ArraySize = ChunkSize* ChunkSize;
public const int ArraySize = ChunkSize * ChunkSize;
public const int AllDirMask = (1 << Directions) - 1;
public const int AirlockMask = AllDirMask << (int) NavMapChunkType.Airlock;
@@ -24,6 +23,7 @@ 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 INetManager _net = default!;
private static readonly ProtoId<TagPrototype>[] WallTags = {"Wall", "Window"};
private EntityQuery<NavMapDoorComponent> _doorQuery;
@@ -57,7 +57,7 @@ public abstract class SharedNavMapSystem : EntitySystem
public NavMapChunkType GetEntityType(EntityUid uid)
{
if (_doorQuery.HasComp(uid))
return NavMapChunkType.Airlock;
return NavMapChunkType.Airlock;
if (_tagSystem.HasAnyTag(uid, WallTags))
return NavMapChunkType.Wall;
@@ -81,6 +81,57 @@ public abstract class SharedNavMapSystem : EntitySystem
return true;
}
public void AddOrUpdateNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner, NavMapRegionProperties regionProperties)
{
// Check if a new region has been added or an existing one has been altered
var isDirty = !component.RegionProperties.TryGetValue(regionOwner, out var oldProperties) || oldProperties != regionProperties;
if (isDirty)
{
component.RegionProperties[regionOwner] = regionProperties;
if (_net.IsServer)
Dirty(uid, component);
}
}
public void RemoveNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner)
{
bool regionOwnerRemoved = component.RegionProperties.Remove(regionOwner) | component.RegionOverlays.Remove(regionOwner);
if (regionOwnerRemoved)
{
if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var affectedChunks))
{
foreach (var affectedChunk in affectedChunks)
{
if (component.ChunkToRegionOwnerTable.TryGetValue(affectedChunk, out var regionOwners))
regionOwners.Remove(regionOwner);
}
component.RegionOwnerToChunkTable.Remove(regionOwner);
}
if (_net.IsServer)
Dirty(uid, component);
}
}
public Dictionary<NetEntity, NavMapRegionOverlay> GetNavMapRegionOverlays(EntityUid uid, NavMapComponent component, Enum uiKey)
{
var regionOverlays = new Dictionary<NetEntity, NavMapRegionOverlay>();
foreach (var (regionOwner, regionOverlay) in component.RegionOverlays)
{
if (!regionOverlay.UiKey.Equals(uiKey))
continue;
regionOverlays.Add(regionOwner, regionOverlay);
}
return regionOverlays;
}
#region: Event handling
private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
@@ -97,7 +148,7 @@ public abstract class SharedNavMapSystem : EntitySystem
chunks.Add(origin, chunk.TileData);
}
args.State = new NavMapState(chunks, component.Beacons);
args.State = new NavMapState(chunks, component.Beacons, component.RegionProperties);
return;
}
@@ -110,7 +161,7 @@ public abstract class SharedNavMapSystem : EntitySystem
chunks.Add(origin, chunk.TileData);
}
args.State = new NavMapDeltaState(chunks, component.Beacons, new(component.Chunks.Keys));
args.State = new NavMapDeltaState(chunks, component.Beacons, component.RegionProperties, new(component.Chunks.Keys));
}
#endregion
@@ -120,22 +171,26 @@ public abstract class SharedNavMapSystem : EntitySystem
[Serializable, NetSerializable]
protected sealed class NavMapState(
Dictionary<Vector2i, int[]> chunks,
Dictionary<NetEntity, NavMapBeacon> beacons)
Dictionary<NetEntity, NavMapBeacon> beacons,
Dictionary<NetEntity, NavMapRegionProperties> regions)
: ComponentState
{
public Dictionary<Vector2i, int[]> Chunks = chunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
}
[Serializable, NetSerializable]
protected sealed class NavMapDeltaState(
Dictionary<Vector2i, int[]> modifiedChunks,
Dictionary<NetEntity, NavMapBeacon> beacons,
Dictionary<NetEntity, NavMapRegionProperties> regions,
HashSet<Vector2i> allChunks)
: ComponentState, IComponentDeltaState<NavMapState>
{
public Dictionary<Vector2i, int[]> ModifiedChunks = modifiedChunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
public HashSet<Vector2i> AllChunks = allChunks;
public void ApplyToFullState(NavMapState state)
@@ -159,11 +214,18 @@ public abstract class SharedNavMapSystem : EntitySystem
{
state.Beacons.Add(nuid, beacon);
}
state.Regions.Clear();
foreach (var (nuid, region) in Regions)
{
state.Regions.Add(nuid, region);
}
}
public NavMapState CreateNewFullState(NavMapState state)
{
var chunks = new Dictionary<Vector2i, int[]>(state.Chunks.Count);
foreach (var (index, data) in state.Chunks)
{
if (!AllChunks!.Contains(index))
@@ -177,12 +239,25 @@ public abstract class SharedNavMapSystem : EntitySystem
Array.Copy(newData, data, ArraySize);
}
return new NavMapState(chunks, new(Beacons));
return new NavMapState(chunks, new(Beacons), new(Regions));
}
}
[Serializable, NetSerializable]
public record struct NavMapBeacon(NetEntity NetEnt, Color Color, string Text, Vector2 Position);
[Serializable, NetSerializable]
public record struct NavMapRegionProperties(NetEntity Owner, Enum UiKey, HashSet<Vector2i> Seeds)
{
// Server defined color for the region
public Color Color = Color.White;
// The maximum number of tiles that can be assigned to this region
public int MaxArea = 625;
// The maximum distance this region can propagate from its seeds
public int MaxRadius = 25;
}
#endregion
}

View File

@@ -3,7 +3,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Power.Generator;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActiveGeneratorRevvingComponent: Component
public sealed partial class ActiveGeneratorRevvingComponent : Component
{
[DataField, ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public TimeSpan CurrentTime = TimeSpan.Zero;

View File

@@ -1,6 +1,6 @@
namespace Content.Shared.Power.Generator;
public sealed class ActiveGeneratorRevvingSystem: EntitySystem
public sealed class ActiveGeneratorRevvingSystem : EntitySystem
{
public override void Initialize()
{
@@ -25,7 +25,7 @@ public sealed class ActiveGeneratorRevvingSystem: EntitySystem
/// <param name="component">ActiveGeneratorRevvingComponent of the generator entity.</param>
public void StartAutoRevving(EntityUid uid, ActiveGeneratorRevvingComponent? component = null)
{
if (Resolve(uid, ref component))
if (Resolve(uid, ref component, false))
{
// reset the revving
component.CurrentTime = TimeSpan.FromSeconds(0);

View File

@@ -13,37 +13,43 @@ public sealed partial class EmbeddableProjectileComponent : Component
/// <summary>
/// Minimum speed of the projectile to embed.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public float MinimumSpeed = 5f;
/// <summary>
/// Delete the entity on embedded removal?
/// Does nothing if there's no RemovalTime.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public bool DeleteOnRemove;
/// <summary>
/// How long it takes to remove the embedded object.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public float? RemovalTime = 3f;
/// <summary>
/// Whether this entity will embed when thrown, or only when shot as a projectile.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public bool EmbedOnThrow = true;
/// <summary>
/// How far into the entity should we offset (0 is wherever we collided).
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public Vector2 Offset = Vector2.Zero;
/// <summary>
/// Sound to play after embedding into a hit target.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
[DataField, AutoNetworkedField]
public SoundSpecifier? Sound;
/// <summary>
/// Uid of the entity the projectile is embed into.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? EmbeddedIntoUid;
}

View File

@@ -71,6 +71,8 @@ public abstract partial class SharedProjectileSystem : EntitySystem
TryComp<PhysicsComponent>(uid, out var physics);
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
_transform.AttachToGridOrMap(uid, xform);
component.EmbeddedIntoUid = null;
Dirty(uid, component);
// Reset whether the projectile has damaged anything if it successfully was removed
if (TryComp<ProjectileComponent>(uid, out var projectile))
@@ -127,8 +129,10 @@ public abstract partial class SharedProjectileSystem : EntitySystem
}
_audio.PlayPredicted(component.Sound, uid, null);
component.EmbeddedIntoUid = target;
var ev = new EmbedEvent(user, target);
RaiseLocalEvent(uid, ref ev);
Dirty(uid, component);
}
private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)

View File

@@ -42,10 +42,12 @@ public abstract class SharedJammerSystem : EntitySystem
{
entity.Comp.SelectedPowerLevel = currIndex;
Dirty(entity);
if (_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity)))
{
Popup.PopupPredicted(Loc.GetString(setting.Message), user, user);
}
// If the jammer is off, this won't do anything which is fine.
// The range should be updated when it turns on again!
_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity));
Popup.PopupClient(Loc.GetString(setting.Message), user, user);
},
Text = Loc.GetString(setting.Name),
};

View File

@@ -30,7 +30,7 @@ public sealed partial class AgeRequirement : JobRequirement
if (!Inverted)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-young",
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-too-young",
("age", RequiredAge)));
if (profile.Age < RequiredAge)
@@ -38,7 +38,7 @@ public sealed partial class AgeRequirement : JobRequirement
}
else
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-old",
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-too-old",
("age", RequiredAge)));
if (profile.Age > RequiredAge)

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Localizations;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
@@ -15,7 +16,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
/// Which department needs the required amount of time.
/// </summary>
[DataField(required: true)]
public ProtoId<DepartmentPrototype> Department = default!;
public ProtoId<DepartmentPrototype> Department;
/// <summary>
/// How long (in seconds) this requirement is.
@@ -47,7 +48,9 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
playtime += otherTime;
}
var deptDiff = Time.TotalMinutes - playtime.TotalMinutes;
var deptDiffSpan = Time - playtime;
var deptDiff = deptDiffSpan.TotalMinutes;
var formattedDeptDiff = ContentLocalizationManager.FormatPlaytime(deptDiffSpan);
var nameDepartment = "role-timer-department-unknown";
if (protoManager.TryIndex(Department, out var departmentIndexed))
@@ -62,7 +65,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-insufficient",
("time", Math.Ceiling(deptDiff)),
("time", formattedDeptDiff),
("department", Loc.GetString(nameDepartment)),
("departmentColor", department.Color.ToHex())));
return false;
@@ -72,7 +75,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-too-high",
("time", -deptDiff),
("time", formattedDeptDiff),
("department", Loc.GetString(nameDepartment)),
("departmentColor", department.Color.ToHex())));
return false;

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Localizations;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using JetBrains.Annotations;
@@ -25,7 +26,9 @@ public sealed partial class OverallPlaytimeRequirement : JobRequirement
reason = new FormattedMessage();
var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall);
var overallDiff = Time.TotalMinutes - overallTime.TotalMinutes;
var overallDiffSpan = Time - overallTime;
var overallDiff = overallDiffSpan.TotalMinutes;
var formattedOverallDiff = ContentLocalizationManager.FormatPlaytime(overallDiffSpan);
if (!Inverted)
{
@@ -34,14 +37,14 @@ public sealed partial class OverallPlaytimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-overall-insufficient",
("time", Math.Ceiling(overallDiff))));
("time", formattedOverallDiff)));
return false;
}
if (overallDiff <= 0 || overallTime >= Time)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high",
("time", -overallDiff)));
("time", formattedOverallDiff)));
return false;
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Localizations;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using Content.Shared.Roles.Jobs;
@@ -17,7 +18,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
/// What particular role they need the time requirement with.
/// </summary>
[DataField(required: true)]
public ProtoId<PlayTimeTrackerPrototype> Role = default!;
public ProtoId<PlayTimeTrackerPrototype> Role;
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
[DataField(required: true)]
@@ -34,7 +35,9 @@ public sealed partial class RoleTimeRequirement : JobRequirement
string proto = Role;
playTimes.TryGetValue(proto, out var roleTime);
var roleDiff = Time.TotalMinutes - roleTime.TotalMinutes;
var roleDiffSpan = Time - roleTime;
var roleDiff = roleDiffSpan.TotalMinutes;
var formattedRoleDiff = ContentLocalizationManager.FormatPlaytime(roleDiffSpan);
var departmentColor = Color.Yellow;
if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
@@ -52,7 +55,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-insufficient",
("time", Math.Ceiling(roleDiff)),
("time", formattedRoleDiff),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;
@@ -62,7 +65,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-too-high",
("time", -roleDiff),
("time", formattedRoleDiff),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;

View File

@@ -103,7 +103,6 @@ public abstract class SharedJobSystem : EntitySystem
public bool MindHasJobWithId(EntityUid? mindId, string prototypeId)
{
MindRoleComponent? comp = null;
if (mindId is null)
return false;
@@ -112,9 +111,7 @@ public abstract class SharedJobSystem : EntitySystem
if (role is null)
return false;
comp = role.Value.Comp;
return (comp.JobPrototype == prototypeId);
return role.Value.Comp1.JobPrototype == prototypeId;
}
public bool MindTryGetJob(
@@ -124,7 +121,7 @@ public abstract class SharedJobSystem : EntitySystem
prototype = null;
MindTryGetJobId(mindId, out var protoId);
return (_prototypes.TryIndex<JobPrototype>(protoId, out prototype) || prototype is not null);
return _prototypes.TryIndex(protoId, out prototype) || prototype is not null;
}
public bool MindTryGetJobId(
@@ -137,9 +134,9 @@ public abstract class SharedJobSystem : EntitySystem
return false;
if (_roles.MindHasRole<JobRoleComponent>(mindId.Value, out var role))
job = role.Value.Comp.JobPrototype;
job = role.Value.Comp1.JobPrototype;
return (job is not null);
return job is not null;
}
/// <summary>

View File

@@ -42,6 +42,8 @@ public sealed partial class MindRoleComponent : BaseMindRoleComponent
public ProtoId<JobPrototype>? JobPrototype { get; set; }
}
// Why does this base component actually exist? It does make auto-categorization easy, but before that it was useless?
[EntityCategory("Roles")]
public abstract partial class BaseMindRoleComponent : Component
{

View File

@@ -10,6 +10,7 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
@@ -92,19 +93,18 @@ public abstract class SharedRoleSystem : EntitySystem
bool silent = false,
string? jobPrototype = null)
{
// Can't have someone get paid for two jobs now, can we
if (MindHasRole<JobRoleComponent>(mindId, out var jobRole)
&& jobRole.Value.Comp.JobPrototype != jobPrototype)
{
Resolve(mindId, ref mind);
if (mind is not null)
{
_adminLogger.Add(LogType.Mind,
LogImpact.Low,
$"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp.JobPrototype}' to '{jobPrototype}'");
}
if (!Resolve(mindId, ref mind))
return;
jobRole.Value.Comp.JobPrototype = jobPrototype;
// Can't have someone get paid for two jobs now, can we
if (MindHasRole<JobRoleComponent>((mindId, mind), out var jobRole)
&& jobRole.Value.Comp1.JobPrototype != jobPrototype)
{
_adminLogger.Add(LogType.Mind,
LogImpact.Low,
$"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp1.JobPrototype}' to '{jobPrototype}'");
jobRole.Value.Comp1.JobPrototype = jobPrototype;
}
else
MindAddRoleDo(mindId, "MindRoleJob", mind, silent, jobPrototype);
@@ -146,11 +146,12 @@ public abstract class SharedRoleSystem : EntitySystem
{
mindRoleComp.JobPrototype = jobPrototype;
EnsureComp<JobRoleComponent>(mindRoleId);
DebugTools.AssertNull(mindRoleComp.AntagPrototype);
DebugTools.Assert(!mindRoleComp.Antag);
DebugTools.Assert(!mindRoleComp.ExclusiveAntag);
}
if (mindRoleComp.Antag || mindRoleComp.ExclusiveAntag)
antagonist = true;
antagonist |= mindRoleComp.Antag;
mind.MindRoles.Add(mindRoleId);
var mindEv = new MindRoleAddedEvent(silent);
@@ -182,51 +183,55 @@ public abstract class SharedRoleSystem : EntitySystem
/// <summary>
/// Removes all instances of a specific role from this mind.
/// </summary>
/// <param name="mindId">The mind to remove the role from.</param>
/// <param name="mind">The mind to remove the role from.</param>
/// <typeparam name="T">The type of the role to remove.</typeparam>
/// <exception cref="ArgumentException">Thrown if the mind does not exist or does not have this role.</exception>
/// <returns>Returns False if there was something wrong with the mind or the removal. True if successful</returns>>
public bool MindRemoveRole<T>(EntityUid mindId) where T : IComponent
/// <returns>Returns false if the role did not exist. True if successful</returns>>
public bool MindRemoveRole<T>(Entity<MindComponent?> mind) where T : IComponent
{
if (!TryComp<MindComponent>(mindId, out var mind) )
throw new ArgumentException($"{mindId} does not exist or does not have mind component");
if (typeof(T) == typeof(MindRoleComponent))
throw new InvalidOperationException();
if (!Resolve(mind.Owner, ref mind.Comp))
return false;
var found = false;
var antagonist = false;
var delete = new List<EntityUid>();
foreach (var role in mind.MindRoles)
foreach (var role in mind.Comp.MindRoles)
{
if (!HasComp<T>(role))
continue;
var roleComp = Comp<MindRoleComponent>(role);
antagonist = roleComp.Antag;
_entityManager.DeleteEntity(role);
if (!TryComp(role, out MindRoleComponent? roleComp))
{
Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
continue;
}
antagonist |= roleComp.Antag | roleComp.ExclusiveAntag;
_entityManager.DeleteEntity(role);
delete.Add(role);
found = true;
}
foreach (var role in delete)
{
mind.MindRoles.Remove(role);
}
if (!found)
return false;
foreach (var role in delete)
{
throw new ArgumentException($"{mindId} does not have this role: {typeof(T)}");
mind.Comp.MindRoles.Remove(role);
}
var message = new RoleRemovedEvent(mindId, mind, antagonist);
if (mind.OwnedEntity != null)
if (mind.Comp.OwnedEntity != null)
{
RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
var message = new RoleRemovedEvent(mind.Owner, mind.Comp, antagonist);
RaiseLocalEvent(mind.Comp.OwnedEntity.Value, message, true);
}
_adminLogger.Add(LogType.Mind,
LogImpact.Low,
$"'Role {typeof(T).Name}' removed from mind of {ToPrettyString(mind.OwnedEntity)}");
$"All roles of type '{typeof(T).Name}' removed from mind of {ToPrettyString(mind.Comp.OwnedEntity)}");
return true;
}
@@ -238,16 +243,14 @@ public abstract class SharedRoleSystem : EntitySystem
/// <returns>True if the role existed and was removed</returns>
public bool MindTryRemoveRole<T>(EntityUid mindId) where T : IComponent
{
if (!MindHasRole<T>(mindId))
{
Log.Warning($"Failed to remove role {typeof(T)} from {mindId} : mind does not have role ");
return false;
}
if (typeof(T) == typeof(MindRoleComponent))
return false;
return MindRemoveRole<T>(mindId);
if (MindRemoveRole<T>(mindId))
return true;
Log.Warning($"Failed to remove role {typeof(T)} from {ToPrettyString(mindId)} : mind does not have role ");
return false;
}
/// <summary>
@@ -259,30 +262,29 @@ public abstract class SharedRoleSystem : EntitySystem
/// <param name="role">The Mind Role entity component</param>
/// <param name="roleT">The Mind Role's entity component for T</param>
/// <returns>True if the role is found</returns>
public bool MindHasRole<T>(EntityUid mindId,
[NotNullWhen(true)] out Entity<MindRoleComponent>? role,
[NotNullWhen(true)] out Entity<T>? roleT) where T : IComponent
public bool MindHasRole<T>(Entity<MindComponent?> mind,
[NotNullWhen(true)] out Entity<MindRoleComponent, T>? role) where T : IComponent
{
role = null;
roleT = null;
if (!TryComp<MindComponent>(mindId, out var mind))
if (!Resolve(mind.Owner, ref mind.Comp))
return false;
var found = false;
foreach (var roleEnt in mind.MindRoles)
foreach (var roleEnt in mind.Comp.MindRoles)
{
if (!HasComp<T>(roleEnt))
if (!TryComp(roleEnt, out T? tcomp))
continue;
role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
roleT = (roleEnt,Comp<T>(roleEnt));
found = true;
break;
if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
{
Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
continue;
}
role = (roleEnt, roleComp, tcomp);
return true;
}
return found;
return false;
}
/// <summary>
@@ -317,7 +319,13 @@ public abstract class SharedRoleSystem : EntitySystem
if (!HasComp(roleEnt, type))
continue;
role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
{
Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
continue;
}
role = (roleEnt, roleComp);
found = true;
break;
}
@@ -325,20 +333,6 @@ public abstract class SharedRoleSystem : EntitySystem
return found;
}
/// <summary>
/// Finds the first mind role of a specific type on a mind entity.
/// Outputs an entity component for the mind role's MindRoleComponent
/// </summary>
/// <param name="mindId">The mind entity</param>
/// <param name="role">The Mind Role entity component</param>
/// <typeparam name="T">The type of the role to find.</typeparam>
/// <returns>True if the role is found</returns>
public bool MindHasRole<T>(EntityUid mindId,
[NotNullWhen(true)] out Entity<MindRoleComponent>? role) where T : IComponent
{
return MindHasRole<T>(mindId, out role, out _);
}
/// <summary>
/// Finds the first mind role of a specific type on a mind entity.
/// </summary>
@@ -347,7 +341,7 @@ public abstract class SharedRoleSystem : EntitySystem
/// <returns>True if the role is found</returns>
public bool MindHasRole<T>(EntityUid mindId) where T : IComponent
{
return MindHasRole<T>(mindId, out _, out _);
return MindHasRole<T>(mindId, out _);
}
//TODO: Delete this later
@@ -374,28 +368,31 @@ public abstract class SharedRoleSystem : EntitySystem
/// <summary>
/// Reads all Roles of a mind Entity and returns their data as RoleInfo
/// </summary>
/// <param name="mindId">The mind entity</param>
/// <param name="mind">The mind entity</param>
/// <returns>RoleInfo list</returns>
public List<RoleInfo> MindGetAllRoleInfo(EntityUid mindId)
public List<RoleInfo> MindGetAllRoleInfo(Entity<MindComponent?> mind)
{
var roleInfo = new List<RoleInfo>();
if (!TryComp<MindComponent>(mindId, out var mind))
if (!Resolve(mind.Owner, ref mind.Comp))
return roleInfo;
foreach (var role in mind.MindRoles)
foreach (var role in mind.Comp.MindRoles)
{
var valid = false;
var name = "game-ticker-unknown-role";
var prototype = "";
string? playTimeTracker = null;
string? playTimeTracker = null;
var comp = Comp<MindRoleComponent>(role);
if (comp.AntagPrototype is not null)
if (!TryComp(role, out MindRoleComponent? comp))
{
prototype = comp.AntagPrototype;
Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
continue;
}
if (comp.AntagPrototype is not null)
prototype = comp.AntagPrototype;
if (comp.JobPrototype is not null && comp.AntagPrototype is null)
{
prototype = comp.JobPrototype;
@@ -429,7 +426,7 @@ public abstract class SharedRoleSystem : EntitySystem
}
if (valid)
roleInfo.Add(new RoleInfo(name, comp.Antag || comp.ExclusiveAntag , playTimeTracker, prototype));
roleInfo.Add(new RoleInfo(name, comp.Antag, playTimeTracker, prototype));
}
return roleInfo;
}
@@ -442,12 +439,9 @@ public abstract class SharedRoleSystem : EntitySystem
public bool MindIsAntagonist(EntityUid? mindId)
{
if (mindId is null)
{
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
return false;
}
return CheckAntagonistStatus(mindId.Value).Item1;
return CheckAntagonistStatus(mindId.Value).Antag;
}
/// <summary>
@@ -458,37 +452,28 @@ public abstract class SharedRoleSystem : EntitySystem
public bool MindIsExclusiveAntagonist(EntityUid? mindId)
{
if (mindId is null)
{
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
return false;
}
return CheckAntagonistStatus(mindId.Value).Item2;
return CheckAntagonistStatus(mindId.Value).ExclusiveAntag;
}
private (bool, bool) CheckAntagonistStatus(EntityUid mindId)
public (bool Antag, bool ExclusiveAntag) CheckAntagonistStatus(Entity<MindComponent?> mind)
{
if (!TryComp<MindComponent>(mindId, out var mind))
{
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind component not found");
if (!Resolve(mind.Owner, ref mind.Comp))
return (false, false);
}
var antagonist = false;
var exclusiveAntag = false;
foreach (var role in mind.MindRoles)
foreach (var role in mind.Comp.MindRoles)
{
if (!TryComp<MindRoleComponent>(role, out var roleComp))
{
//If this ever shows up outside of an integration test, then we need to look into this further.
Log.Warning($"Mind Role Entity {role} does not have MindRoleComponent!");
Log.Error($"Mind Role Entity {ToPrettyString(role)} does not have a MindRoleComponent, despite being listed as a role belonging to {ToPrettyString(mind)}|");
continue;
}
if (roleComp.Antag || exclusiveAntag)
antagonist = true;
if (roleComp.ExclusiveAntag)
exclusiveAntag = true;
antagonist |= roleComp.Antag;
exclusiveAntag |= roleComp.ExclusiveAntag;
}
return (antagonist, exclusiveAntag);
@@ -504,6 +489,9 @@ public abstract class SharedRoleSystem : EntitySystem
_audio.PlayGlobal(sound, mind.Session);
}
// TODO ROLES Change to readonly.
// Passing around a reference to a prototype's hashset makes me uncomfortable because it might be accidentally
// mutated.
public HashSet<JobRequirement>? GetJobRequirement(JobPrototype job)
{
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req))
@@ -512,6 +500,7 @@ public abstract class SharedRoleSystem : EntitySystem
return job.Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetJobRequirement(ProtoId<JobPrototype> job)
{
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job, out var req))
@@ -520,6 +509,7 @@ public abstract class SharedRoleSystem : EntitySystem
return _prototypes.Index(job).Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetAntagRequirement(ProtoId<AntagPrototype> antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag, out var req))
@@ -528,6 +518,7 @@ public abstract class SharedRoleSystem : EntitySystem
return _prototypes.Index(antag).Requirements;
}
// TODO ROLES Change to readonly.
public HashSet<JobRequirement>? GetAntagRequirement(AntagPrototype antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))

View File

@@ -0,0 +1,20 @@
//using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Shared.Silicons.Borgs.Components;
/// <summary>
/// This is used to override the action icon for cyborg actions.
/// Without this component the no-action state will be used.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class BorgModuleIconComponent : Component
{
/// <summary>
/// The action icon for this module
/// </summary>
[DataField]
public SpriteSpecifier.Rsi Icon = default!;
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Audio;
namespace Content.Shared.Silicons.Laws.Components;
@@ -20,4 +21,12 @@ public sealed partial class SiliconLawProviderComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SiliconLawset? Lawset;
/// <summary>
/// The sound that plays for the Silicon player
/// when the particular lawboard has been inserted.
/// </summary>
[DataField]
public SoundSpecifier? LawUploadSound = new SoundPathSpecifier("/Audio/Misc/cryo_warning.ogg");
}

View File

@@ -285,6 +285,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private bool SetupEye(Entity<StationAiCoreComponent> ent)
{
if (_net.IsClient)
return false;
if (ent.Comp.RemoteEntity != null)
return false;
@@ -299,8 +301,11 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private void ClearEye(Entity<StationAiCoreComponent> ent)
{
if (_net.IsClient)
return;
QueueDel(ent.Comp.RemoteEntity);
ent.Comp.RemoteEntity = null;
Dirty(ent);
}
private void AttachEye(Entity<StationAiCoreComponent> ent)
@@ -330,6 +335,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
if (_timing.ApplyingState)
return;
SetupEye(ent);
// Just so text and the likes works properly
_metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName);
@@ -351,6 +358,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
{
_eye.SetTarget(args.Entity, null, eyeComp);
}
ClearEye(ent);
}
private void UpdateAppearance(Entity<StationAiHolderComponent?> entity)

View File

@@ -1,3 +1,4 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Sound.Components;
@@ -8,4 +9,9 @@ namespace Content.Shared.Sound.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnUIOpenComponent : BaseEmitSoundComponent
{
/// <summary>
/// Blacklist for making the sound not play if certain entities open the UI
/// </summary>
[DataField]
public EntityWhitelist Blacklist = new();
}

View File

@@ -58,7 +58,10 @@ public abstract class SharedEmitSoundSystem : EntitySystem
private void HandleEmitSoundOnUIOpen(EntityUid uid, EmitSoundOnUIOpenComponent component, AfterActivatableUIOpenEvent args)
{
TryEmitSound(uid, component, args.User);
if (_whitelistSystem.IsBlacklistFail(component.Blacklist, args.User))
{
TryEmitSound(uid, component, args.User);
}
}
private void OnMobState(Entity<SoundWhileAliveComponent> entity, ref MobStateChangedEvent args)

View File

@@ -150,6 +150,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
foreach (var (slot, entProtos) in startingGear.Storage)
{
ents.Clear();
if (entProtos.Count == 0)
continue;

View File

@@ -14,6 +14,8 @@ using Content.Shared.Verbs;
using Content.Shared.IdentityManagement;
using Content.Shared.Tools.EntitySystems;
using Content.Shared.Whitelist;
using Content.Shared.Materials;
using Robust.Shared.Map;
namespace Content.Shared.Storage.EntitySystems;
@@ -35,6 +37,7 @@ public sealed class SecretStashSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<SecretStashComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SecretStashComponent, DestructionEventArgs>(OnDestroyed);
SubscribeLocalEvent<SecretStashComponent, GotReclaimedEvent>(OnReclaimed);
SubscribeLocalEvent<SecretStashComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem) });
SubscribeLocalEvent<SecretStashComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<SecretStashComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
@@ -47,12 +50,12 @@ public sealed class SecretStashSystem : EntitySystem
private void OnDestroyed(Entity<SecretStashComponent> entity, ref DestructionEventArgs args)
{
var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer);
if (storedInside != null && storedInside.Count >= 1)
{
var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity)));
_popupSystem.PopupEntity(popup, storedInside[0], PopupType.MediumCaution);
}
DropContentsAndAlert(entity);
}
private void OnReclaimed(Entity<SecretStashComponent> entity, ref GotReclaimedEvent args)
{
DropContentsAndAlert(entity, args.ReclaimerCoordinates);
}
private void OnInteractUsing(Entity<SecretStashComponent> entity, ref InteractUsingEvent args)
@@ -211,5 +214,18 @@ public sealed class SecretStashSystem : EntitySystem
return entity.Comp.ItemContainer.ContainedEntity != null;
}
/// <summary>
/// Drop the item stored in the stash and alert all nearby players with a popup.
/// </summary>
private void DropContentsAndAlert(Entity<SecretStashComponent> entity, EntityCoordinates? cords = null)
{
var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer, true, cords);
if (storedInside != null && storedInside.Count >= 1)
{
var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity)));
_popupSystem.PopupPredicted(popup, storedInside[0], null, PopupType.MediumCaution);
}
}
#endregion
}

View File

@@ -673,7 +673,7 @@ public abstract class SharedStorageSystem : EntitySystem
private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args)
{
if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item, held: true))
if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item))
return;
SaveItemLocation(storage!, item.Owner);

View File

@@ -103,7 +103,7 @@ public abstract class SharedStrippableSystem : EntitySystem
if (userHands.ActiveHandEntity != null && !hasEnt)
StartStripInsertInventory((user, userHands), strippable.Owner, userHands.ActiveHandEntity.Value, args.Slot);
else if (userHands.ActiveHandEntity == null && hasEnt)
else if (hasEnt)
StartStripRemoveInventory(user, strippable.Owner, held!.Value, args.Slot);
}
@@ -135,7 +135,7 @@ public abstract class SharedStrippableSystem : EntitySystem
if (user.Comp.ActiveHandEntity != null && handSlot.HeldEntity == null)
StartStripInsertHand(user, target, user.Comp.ActiveHandEntity.Value, handId, targetStrippable);
else if (user.Comp.ActiveHandEntity == null && handSlot.HeldEntity != null)
else if (handSlot.HeldEntity != null)
StartStripRemoveHand(user, target, handSlot.HeldEntity.Value, handId, targetStrippable);
}

View File

@@ -10,6 +10,7 @@ public sealed class IntrinsicUISystem : EntitySystem
public override void Initialize()
{
SubscribeLocalEvent<IntrinsicUIComponent, MapInitEvent>(InitActions);
SubscribeLocalEvent<IntrinsicUIComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<IntrinsicUIComponent, ToggleIntrinsicUIEvent>(OnActionToggle);
}
@@ -21,6 +22,15 @@ public sealed class IntrinsicUISystem : EntitySystem
args.Handled = InteractUI(uid, args.Key, component);
}
private void OnShutdown(EntityUid uid, IntrinsicUIComponent component, ref ComponentShutdown args)
{
foreach (var actionEntry in component.UIs.Values)
{
var actionId = actionEntry.ToggleActionEntity;
_actionsSystem.RemoveAction(uid, actionId);
}
}
private void InitActions(EntityUid uid, IntrinsicUIComponent component, MapInitEvent args)
{
foreach (var entry in component.UIs.Values)

View File

@@ -87,12 +87,13 @@ namespace Content.Shared.VendingMachines
/// Sound that plays when ejecting an item
/// </summary>
[DataField("soundVend")]
// Grabbed from: https://github.com/discordia-space/CEV-Eris/blob/f702afa271136d093ddeb415423240a2ceb212f0/sound/machines/vending_drop.ogg
// Grabbed from: https://github.com/tgstation/tgstation/blob/d34047a5ae911735e35cd44a210953c9563caa22/sound/machines/machine_vend.ogg
public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg")
{
Params = new AudioParams
{
Volume = -2f
Volume = -4f,
Variation = 0.15f
}
};

View File

@@ -440,7 +440,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
throw new NotImplementedException();
}
DoLungeAnimation(user, weaponUid, weapon.Angle, GetCoordinates(attack.Coordinates).ToMap(EntityManager, TransformSystem), weapon.Range, animation);
DoLungeAnimation(user, weaponUid, weapon.Angle, TransformSystem.ToMapCoordinates(GetCoordinates(attack.Coordinates)), weapon.Range, animation);
}
var attackEv = new MeleeAttackEvent(weaponUid);
@@ -472,12 +472,14 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
// TODO: This needs fixing
if (meleeUid == user)
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Low,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (light) using their hands and missed");
}
else
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Low,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (light) using {ToPrettyString(meleeUid):tool} and missed");
}
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, null);
@@ -526,12 +528,14 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
if (meleeUid == user)
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Medium,
$"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using their hands and dealt {damageResult.GetTotal():damage} damage");
}
else
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Medium,
$"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage");
}
@@ -553,7 +557,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
if (!TryComp(user, out TransformComponent? userXform))
return false;
var targetMap = GetCoordinates(ev.Coordinates).ToMap(EntityManager, TransformSystem);
var targetMap = TransformSystem.ToMapCoordinates(GetCoordinates(ev.Coordinates));
if (targetMap.MapId != userXform.MapID)
return false;
@@ -569,12 +573,14 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
{
if (meleeUid == user)
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Low,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (heavy) using their hands and missed");
}
else
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Low,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (heavy) using {ToPrettyString(meleeUid):tool} and missed");
}
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, direction);
@@ -595,8 +601,14 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
// Validate client
for (var i = entities.Count - 1; i >= 0; i--)
{
if (ArcRaySuccessful(entities[i], userPos, direction.ToWorldAngle(), component.Angle, distance,
userXform.MapID, user, session))
if (ArcRaySuccessful(entities[i],
userPos,
direction.ToWorldAngle(),
component.Angle,
distance,
userXform.MapID,
user,
session))
{
continue;
}
@@ -663,16 +675,24 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero)
{
// If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor
if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage))
{
_stamina.TakeStaminaDamage(entity, (bluntDamage * component.BluntStaminaDamageFactor).Float(), visual: false, source: user, with: meleeUid == user ? null : meleeUid);
}
appliedDamage += damageResult;
if (meleeUid == user)
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Medium,
$"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.GetTotal():damage} damage");
}
else
{
AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium,
AdminLogger.Add(LogType.MeleeHit,
LogImpact.Medium,
$"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.GetTotal():damage} damage");
}
}
@@ -706,8 +726,13 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
{
var castAngle = new Angle(baseAngle + increment * i);
var res = _physics.IntersectRay(mapId,
new CollisionRay(position, castAngle.ToWorldVec(),
AttackMask), range, ignore, false).ToList();
new CollisionRay(position,
castAngle.ToWorldVec(),
AttackMask),
range,
ignore,
false)
.ToList();
if (res.Count != 0)
{
@@ -718,8 +743,14 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
return resSet;
}
protected virtual bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range,
MapId mapId, EntityUid ignore, ICommonSession? session)
protected virtual bool ArcRaySuccessful(EntityUid targetUid,
Vector2 position,
Angle angle,
Angle arcWidth,
float range,
MapId mapId,
EntityUid ignore,
ICommonSession? session)
{
// Only matters for server.
return true;

View File

@@ -114,19 +114,14 @@ public abstract class SharedGrapplingGunSystem : EntitySystem
private void OnGunActivate(EntityUid uid, GrapplingGunComponent component, ActivateInWorldEvent args)
{
if (!Timing.IsFirstTimePredicted || args.Handled || !args.Complex)
return;
if (Deleted(component.Projectile))
if (!Timing.IsFirstTimePredicted || args.Handled || !args.Complex || component.Projectile is not {} projectile)
return;
_audio.PlayPredicted(component.CycleSound, uid, args.User);
_appearance.SetData(uid, SharedTetherGunSystem.TetherVisualsStatus.Key, true);
if (_netManager.IsServer)
{
QueueDel(component.Projectile.Value);
}
QueueDel(projectile);
component.Projectile = null;
SetReeling(uid, component, false, args.User);