upstream remote merge

This commit is contained in:
Dmitry
2025-10-02 04:36:42 +07:00
128 changed files with 1248 additions and 813 deletions

View File

@@ -25,7 +25,7 @@ namespace Content.Client.Access.UI
public void SetAccessLevels(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels)
{
_accessButtons.Clear();
AccessLevelGrid.DisposeAllChildren();
AccessLevelGrid.RemoveAllChildren();
foreach (var access in accessLevels)
{

View File

@@ -41,7 +41,7 @@ namespace Content.Client.Access.UI
public void SetAllowedIcons(string currentJobIconId)
{
IconGrid.DisposeAllChildren();
IconGrid.RemoveAllChildren();
var jobIconButtonGroup = new ButtonGroup();
var i = 0;

View File

@@ -99,8 +99,8 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
private bool TryRebuildAccessGroupControls()
{
AccessGroupList.DisposeAllChildren();
AccessLevelChecklist.DisposeAllChildren();
AccessGroupList.RemoveAllChildren();
AccessLevelChecklist.RemoveAllChildren();
// No access level prototypes were assigned to any of the access level groups.
// Either the turret controller has no assigned access levels or their names were invalid.
@@ -165,7 +165,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
/// </summary>
public void RebuildAccessLevelsControls()
{
AccessLevelChecklist.DisposeAllChildren();
AccessLevelChecklist.RemoveAllChildren();
_accessLevelEntries.Clear();
// No access level prototypes were assigned to any of the access level groups

View File

@@ -33,6 +33,7 @@ namespace Content.Client.Actions
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
public event Action<EntityUid>? OnActionAdded;
public event Action<EntityUid>? OnActionRemoved;
@@ -286,8 +287,27 @@ namespace Content.Client.Actions
continue;
}
if (assignmentNode is SequenceDataNode sequenceAssignments)
{
try
{
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(sequenceAssignments, notNullableOverride: true);
foreach (var index in nodeAssignments)
{
assignments.Add(new SlotAssignment(index.Hotbar, index.Slot, actionId));
}
}
catch (Exception ex)
{
Log.Error($"Failed to parse action assignments: {ex}");
}
}
AddActionDirect((user, actions), actionId);
}
AssignSlot?.Invoke(assignments);
}
private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
@@ -309,10 +329,10 @@ namespace Content.Client.Actions
// this is the actual entity-world targeting magic
EntityUid? targetEnt = null;
if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
args.Input.EntityUid != null &&
ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
args.Input.EntityUid is { Valid: true } entityUid &&
ValidateEntityTarget(user, entityUid, (uid, entity)))
{
targetEnt = args.Input.EntityUid;
targetEnt = entityUid;
}
if (action.ClientExclusive)

View File

@@ -1,6 +1,5 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc admin-camera-window-title-placeholder}"
SetSize="425 550"
MinSize="200 225"
Name="Window">
MinSize="200 225">
</DefaultWindow>

View File

@@ -67,7 +67,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// </summary>
public void UpdateReagents()
{
ReagentList.DisposeAllChildren();
ReagentList.RemoveAllChildren();
if (_selectedSolution == null || _solutions == null)
return;
@@ -92,7 +92,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// <param name="solution">The selected solution.</param>
private void UpdateVolumeBox(Solution solution)
{
VolumeBox.DisposeAllChildren();
VolumeBox.RemoveAllChildren();
var volumeLabel = new Label();
volumeLabel.HorizontalExpand = true;
@@ -131,7 +131,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// <param name="solution">The selected solution.</param>
private void UpdateThermalBox(Solution solution)
{
ThermalBox.DisposeAllChildren();
ThermalBox.RemoveAllChildren();
var heatCap = solution.GetHeatCapacity(null);
var specificHeatLabel = new Label();
specificHeatLabel.HorizontalExpand = true;

View File

@@ -206,7 +206,7 @@ namespace Content.Client.Cargo.UI
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
return;
Requests.DisposeAllChildren();
Requests.RemoveAllChildren();
foreach (var order in orders)
{

View File

@@ -30,7 +30,7 @@ namespace Content.Client.Cargo.UI
public void SetOrders(SpriteSystem sprites, IPrototypeManager protoManager, List<CargoOrderData> orders)
{
Orders.DisposeAllChildren();
Orders.RemoveAllChildren();
foreach (var order in orders)
{

View File

@@ -1,4 +1,4 @@
using Content.Client.CrewManifest.UI;
using Content.Client.CrewManifest.UI;
using Content.Shared.CrewManifest;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
@@ -21,7 +21,6 @@ public sealed partial class CrewManifestUiFragment : BoxContainer
public void UpdateState(string stationName, CrewManifestEntries? entries)
{
CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;

View File

@@ -55,7 +55,7 @@ namespace Content.Client.Changelog
// Changelog is not kept in memory so load it again.
var changelogs = await _changelog.LoadChangelog();
Tabs.DisposeAllChildren();
Tabs.RemoveAllChildren();
var i = 0;
foreach (var changelog in changelogs)

View File

@@ -95,7 +95,7 @@ namespace Content.Client.ContextMenu.UI
/// </summary>
public void Close()
{
RootMenu.MenuBody.DisposeAllChildren();
RootMenu.MenuBody.RemoveAllChildren();
CancelOpen?.Cancel();
CancelClose?.Cancel();
OnContextClosed?.Invoke();

View File

@@ -293,7 +293,7 @@ namespace Content.Client.ContextMenu.UI
var element = new EntityMenuElement(entity);
element.SubMenu = new ContextMenuPopup(_context, element);
element.SubMenu.OnPopupOpen += () => _verb.OpenVerbMenu(entity, popup: element.SubMenu);
element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.DisposeAllChildren;
element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.RemoveAllChildren;
_context.AddElement(menu, element);
Elements.TryAdd(entity, element);
}

View File

@@ -53,7 +53,7 @@ namespace Content.Client.Crayon.UI
private void RefreshList()
{
// Clear
Grids.DisposeAllChildren();
Grids.RemoveAllChildren();
if (_decals == null || _allDecals == null)
return;

View File

@@ -18,7 +18,6 @@ public sealed partial class CrewManifestUi : DefaultWindow
public void Populate(string name, CrewManifestEntries? entries)
{
CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;

View File

@@ -72,7 +72,7 @@ public sealed partial class GatewayWindow : FancyWindow,
_isUnlockPending = _nextUnlock >= _timing.CurTime;
_isCooldownPending = _nextReady >= _timing.CurTime;
Container.DisposeAllChildren();
Container.RemoveAllChildren();
if (_destinations.Count == 0)
{

View File

@@ -116,7 +116,7 @@ namespace Content.Client.HealthAnalyzer.UI
AlertsContainer.Visible = showAlerts;
if (showAlerts)
AlertsContainer.DisposeAllChildren();
AlertsContainer.RemoveAllChildren();
if (msg.Unrevivable == true)
AlertsContainer.AddChild(new RichTextLabel

View File

@@ -416,7 +416,7 @@ public sealed partial class MarkingPicker : Control
var stateNames = GetMarkingStateNames(prototype);
_currentMarkingColors.Clear();
CMarkingColors.DisposeAllChildren();
CMarkingColors.RemoveAllChildren();
List<ColorSelectorSliders> colorSliders = new();
for (int i = 0; i < prototype.Sprites.Count; i++)
{

View File

@@ -226,7 +226,6 @@ public sealed partial class SingleMarkingPicker : BoxContainer
var marking = _markings[Slot];
ColorSelectorContainer.DisposeAllChildren();
ColorSelectorContainer.RemoveAllChildren();
if (marking.MarkingColors.Count != proto.Sprites.Count)

View File

@@ -1,7 +0,0 @@
using Content.Shared.IdentityManagement;
namespace Content.Client.IdentityManagement;
public sealed class IdentitySystem : SharedIdentitySystem
{
}

View File

@@ -80,7 +80,7 @@ namespace Content.Client.Lobby.UI
public void ReloadCharacterPickers()
{
_createNewCharacterButton.Orphan();
Characters.DisposeAllChildren();
Characters.RemoveAllChildren();
var numberOfFullSlots = 0;
var characterButtonsGroup = new ButtonGroup();

View File

@@ -505,7 +505,7 @@ namespace Content.Client.Lobby.UI
/// </summary>
public void RefreshTraits()
{
TraitsList.DisposeAllChildren();
TraitsList.RemoveAllChildren();
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
@@ -650,7 +650,7 @@ namespace Content.Client.Lobby.UI
public void RefreshAntags()
{
AntagList.DisposeAllChildren();
AntagList.RemoveAllChildren();
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
@@ -845,7 +845,7 @@ namespace Content.Client.Lobby.UI
/// </summary>
public void RefreshJobs()
{
JobList.DisposeAllChildren();
JobList.RemoveAllChildren();
_jobCategories.Clear();
_jobPriorities.Clear();
var firstCategory = true;

View File

@@ -43,7 +43,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
{
var protoMan = collection.Resolve<IPrototypeManager>();
var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
RestrictionsContainer.DisposeAllChildren();
RestrictionsContainer.RemoveAllChildren();
if (_groupProto.MinLimit > 0)
{
@@ -72,7 +72,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
});
}
LoadoutsContainer.DisposeAllChildren();
LoadoutsContainer.RemoveAllChildren();
// Corvax-Loadouts-Start
var groupLoadouts = _groupProto.Loadouts;

View File

@@ -43,7 +43,7 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
_previewDummy = uid;
ViewBox.DisposeAllChildren();
ViewBox.RemoveAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -37,7 +37,7 @@ public sealed partial class MappingPrototypeList : Control
{
_prototypes.Clear();
PrototypeList.DisposeAllChildren();
PrototypeList.RemoveAllChildren();
_prototypes.AddRange(prototypes);
@@ -99,7 +99,7 @@ public sealed partial class MappingPrototypeList : Control
public void Search(List<MappingPrototype> prototypes)
{
_search.Clear();
SearchList.DisposeAllChildren();
SearchList.RemoveAllChildren();
_lastIndices = (0, -1);
_search.AddRange(prototypes);

View File

@@ -861,7 +861,7 @@ public sealed class MappingState : GameplayStateBase
}
else
{
button.ChildrenPrototypes.DisposeAllChildren();
button.ChildrenPrototypes.RemoveAllChildren();
button.CollapseButton.Label.Text = "▶";
}
}

View File

@@ -53,8 +53,8 @@ public sealed partial class PowerMonitoringWindow
// Selection action
windowEntry.Button.OnButtonUp += args =>
{
windowEntry.SourcesContainer.DisposeAllChildren();
windowEntry.LoadsContainer.DisposeAllChildren();
windowEntry.SourcesContainer.RemoveAllChildren();
windowEntry.LoadsContainer.RemoveAllChildren();
ButtonAction(windowEntry, masterContainer);
};
}

View File

@@ -70,7 +70,7 @@ public sealed partial class OfferingWindow : FancyWindow,
public void ClearOptions()
{
Container.DisposeAllChildren();
Container.RemoveAllChildren();
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -67,8 +67,8 @@ public sealed partial class DockingScreen : BoxContainer
{
DockingControl.BuildDocks(shuttle);
var currentDock = DockingControl.ViewedDock;
// DockedWith.DisposeAllChildren();
DockPorts.DisposeAllChildren();
// DockedWith.RemoveAllChildren();
DockPorts.RemoveAllChildren();
_ourDockButtons.Clear();
if (shuttle == null)

View File

@@ -59,7 +59,7 @@ public sealed partial class EmergencyConsoleWindow : FancyWindow,
// TODO: Loc and cvar for this.
_earlyLaunchTime = scc.EarlyLaunchTime;
AuthorizationsContainer.DisposeAllChildren();
AuthorizationsContainer.RemoveAllChildren();
var remainingAuths = scc.AuthorizationsRequired - scc.Authorizations.Count;
AuthorizationCount.Text = Loc.GetString("emergency-shuttle-ui-remaining", ("remaining", remainingAuths));

View File

@@ -237,7 +237,7 @@ public sealed partial class MapScreen : BoxContainer
private void ClearMapObjects()
{
_mapObjectControls.Clear();
HyperspaceDestinations.DisposeAllChildren();
HyperspaceDestinations.RemoveAllChildren();
_pendingMapObjects.Clear();
_mapObjects.Clear();
_mapHeadings.Clear();

View File

@@ -25,9 +25,9 @@ namespace Content.Client.Strip
public void ClearButtons()
{
InventoryContainer.DisposeAllChildren();
HandsContainer.DisposeAllChildren();
SnareContainer.DisposeAllChildren();
InventoryContainer.RemoveAllChildren();
HandsContainer.RemoveAllChildren();
SnareContainer.RemoveAllChildren();
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -29,7 +29,7 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
public void UpdateState(ThiefBackpackBoundUserInterfaceState state)
{
SetsGrid.DisposeAllChildren();
SetsGrid.RemoveAllChildren();
var selectedNumber = 0;
foreach (var (set, info) in state.Sets)
{

View File

@@ -66,7 +66,8 @@ namespace Content.Client.UserInterface.Controls
Viewport.StretchMode = filterMode switch
{
"nearest" => ScalingViewportStretchMode.Nearest,
"bilinear" => ScalingViewportStretchMode.Bilinear
"bilinear" => ScalingViewportStretchMode.Bilinear,
_ => ScalingViewportStretchMode.Nearest
};
Viewport.IgnoreDimension = verticalFit ? ScalingViewportIgnoreDimension.Horizontal : ScalingViewportIgnoreDimension.None;

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -19,7 +19,7 @@ namespace Content.Client.UserInterface.Controls
public void Clear()
{
DisposeAllChildren();
RemoveAllChildren();
}
public void AddEntry(float amount, Color color, string? tooltip = null)

View File

@@ -17,7 +17,7 @@ namespace Content.Client.UserInterface
public void UpdateValues(List<string> headers, List<string[]> values)
{
Values.DisposeAllChildren();
Values.RemoveAllChildren();
Values.Columns = headers.Count;
for (var i = 0; i < headers.Count; i++)

View File

@@ -45,7 +45,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
public void Populate()
{
ButtonContainer.DisposeAllChildren();
ButtonContainer.RemoveAllChildren();
AddButtons();
}

View File

@@ -26,7 +26,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
public void ClearEntries()
{
NoRolesMessage.Visible = true;
EntryContainer.DisposeAllChildren();
EntryContainer.RemoveAllChildren();
_collapsibleBoxes.Clear();
}

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Content.Client.UserInterface.Systems.Inventory.Controls;
using Robust.Client.UserInterface.Controls;
@@ -74,7 +74,7 @@ public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
public void Clear()
{
ClearButtons();
_grid.DisposeAllChildren();
_grid.RemoveAllChildren();
}
public IEnumerable<HandButton> GetButtons()

View File

@@ -109,7 +109,7 @@ namespace Content.Client.Verbs.UI
Close();
var menu = popup ?? _context.RootMenu;
menu.MenuBody.DisposeAllChildren();
menu.MenuBody.RemoveAllChildren();
CurrentTarget = target;
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force);
@@ -207,7 +207,7 @@ namespace Content.Client.Verbs.UI
/// </summary>
public void AddServerVerbs(List<Verb>? verbs, ContextMenuPopup popup)
{
popup.MenuBody.DisposeAllChildren();
popup.MenuBody.RemoveAllChildren();
// Verbs may be null if the server does not think we can see the target entity. This **should** not happen.
if (verbs == null)

View File

@@ -271,7 +271,7 @@ namespace Content.IntegrationTests.Tests
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>);
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e));
await Assert.MultipleAsync(async () =>
{
@@ -311,8 +311,8 @@ namespace Content.IntegrationTests.Tests
// Check that the number of entities has gone back to the original value.
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(client.EntMan.EntityCount, Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
}

View File

@@ -1,9 +1,9 @@
using System.Linq;
using Content.Server.Emp;
using Content.Server.IdentityManagement;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Emp;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory;
using Content.Shared.Prototypes;

View File

@@ -269,31 +269,4 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
mob = user;
return true;
}
/// <summary>
/// Checks if the new identity's name has a criminal record attached to it, and gives the entity the icon that
/// belongs to the status if it does.
/// </summary>
public void CheckNewIdentity(EntityUid uid)
{
var name = Identity.Name(uid, EntityManager);
var xform = Transform(uid);
// TODO use the entity's station? Not the station of the map that it happens to currently be on?
var station = _station.GetStationInMap(xform.MapID);
if (station != null && _records.GetRecordByName(station.Value, name) is { } id)
{
if (_records.TryGetRecord<CriminalRecord>(new StationRecordKey(id, station.Value),
out var record))
{
if (record.Status != SecurityStatus.None)
{
_criminalRecords.SetCriminalIcon(name, record.Status, uid);
return;
}
}
}
RemComp<CriminalRecordComponent>(uid);
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Delivery;
using Content.Shared.Power.EntitySystems;
using Content.Server.StationRecords;
using Content.Shared.EntityTable;
using Content.Shared.StationRecords;
using Robust.Shared.Random;
using Robust.Shared.Timing;

View File

@@ -30,6 +30,10 @@ public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem
if (!_jammer.GetJammableNetworks((uid, jammerComp)).Contains(ev.NetworkId))
continue;
if (jammerComp.FrequenciesExcluded != null &&
jammerComp.FrequenciesExcluded.Contains(ev.Frequency))
continue;
if (_transform.InRange(jammerXform.Coordinates, ev.SenderTransform.Coordinates, jammerComp.Range)
|| _transform.InRange(jammerXform.Coordinates, xform.Comp.Coordinates, jammerComp.Range))
{

View File

@@ -349,7 +349,7 @@ namespace Content.Server.DeviceNetwork.Systems
if (connection.Owner == packet.Sender)
continue;
BeforePacketSentEvent beforeEv = new(packet.Sender, xform, senderPos, connection.NetIdEnum.ToString());
BeforePacketSentEvent beforeEv = new(packet.Sender, xform, senderPos, connection.NetIdEnum.ToString(), packet.Frequency);
RaiseLocalEvent(connection.Owner, beforeEv, false);
if (!beforeEv.Cancelled)

View File

@@ -63,16 +63,6 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
public const int MaxExplosionAudioRange = 30;
/// <summary>
/// The "default" explosion prototype.
/// </summary>
/// <remarks>
/// Generally components should specify an explosion prototype via a yaml datafield, so that the yaml-linter can
/// find errors. However some components, like rogue arrows, or some commands like the admin-smite need to have
/// a "default" option specified outside of yaml data-fields. Hence this const string.
/// </remarks>
public static readonly ProtoId<ExplosionPrototype> DefaultExplosionPrototypeId = "Default";
public override void Initialize()
{
base.Initialize();
@@ -222,10 +212,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
return r0 * (MathF.Sqrt(12 * totalIntensity / v0 - 3) / 6 + 0.5f);
}
/// <summary>
/// Queue an explosions, centered on some entity.
/// </summary>
public void QueueExplosion(EntityUid uid,
/// <inheritdoc />
public override void QueueExplosion(EntityUid uid,
string typeId,
float totalIntensity,
float slope,

View File

@@ -1,180 +0,0 @@
using Content.Server.Access.Systems;
using Content.Server.Administration.Logs;
using Content.Server.CriminalRecords.Systems;
using Content.Server.Humanoid;
using Content.Shared.Clothing;
using Content.Shared.Database;
using Content.Shared.Hands;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects.Components.Localization;
namespace Content.Server.IdentityManagement;
/// <summary>
/// Responsible for updating the identity of an entity on init or clothing equip/unequip.
/// </summary>
public sealed class IdentitySystem : SharedIdentitySystem
{
[Dependency] private readonly IdCardSystem _idCard = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[Dependency] private readonly CriminalRecordsConsoleSystem _criminalRecordsConsole = default!;
[Dependency] private readonly GrammarSystem _grammarSystem = default!;
private HashSet<EntityUid> _queuedIdentityUpdates = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IdentityComponent, DidEquipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidEquipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var ent in _queuedIdentityUpdates)
{
if (!TryComp<IdentityComponent>(ent, out var identity))
continue;
UpdateIdentityInfo(ent, identity);
}
_queuedIdentityUpdates.Clear();
}
// This is where the magic happens
private void OnMapInit(EntityUid uid, IdentityComponent component, MapInitEvent args)
{
var ident = Spawn(null, Transform(uid).Coordinates);
_metaData.SetEntityName(ident, "identity");
QueueIdentityUpdate(uid);
_container.Insert(ident, component.IdentityEntitySlot);
}
/// <summary>
/// Queues an identity update to the start of the next tick.
/// </summary>
public override void QueueIdentityUpdate(EntityUid uid)
{
_queuedIdentityUpdates.Add(uid);
}
#region Private API
/// <summary>
/// Updates the metadata name for the id(entity) from the current state of the character.
/// </summary>
private void UpdateIdentityInfo(EntityUid uid, IdentityComponent identity)
{
if (identity.IdentityEntitySlot.ContainedEntity is not { } ident)
return;
var representation = GetIdentityRepresentation(uid);
var name = GetIdentityName(uid, representation);
// Clone the old entity's grammar to the identity entity, for loc purposes.
if (TryComp<GrammarComponent>(uid, out var grammar))
{
var identityGrammar = EnsureComp<GrammarComponent>(ident);
identityGrammar.Attributes.Clear();
foreach (var (k, v) in grammar.Attributes)
{
identityGrammar.Attributes.Add(k, v);
}
// If presumed name is null and we're using that, we set proper noun to be false ("the old woman")
if (name != representation.TrueName && representation.PresumedName == null)
_grammarSystem.SetProperNoun((ident, identityGrammar), false);
Dirty(ident, identityGrammar);
}
if (name == Name(ident))
return;
_metaData.SetEntityName(ident, name);
_adminLog.Add(LogType.Identity, LogImpact.Medium, $"{ToPrettyString(uid)} changed identity to {name}");
var identityChangedEvent = new IdentityChangedEvent(uid, ident);
RaiseLocalEvent(uid, ref identityChangedEvent);
SetIdentityCriminalIcon(uid);
}
private string GetIdentityName(EntityUid target, IdentityRepresentation representation)
{
var ev = new SeeIdentityAttemptEvent();
RaiseLocalEvent(target, ev);
return representation.ToStringKnown(!ev.Cancelled);
}
/// <summary>
/// When the identity of a person is changed, searches the criminal records to see if the name of the new identity
/// has a record. If the new name has a criminal status attached to it, the person will get the criminal status
/// until they change identity again.
/// </summary>
private void SetIdentityCriminalIcon(EntityUid uid)
{
_criminalRecordsConsole.CheckNewIdentity(uid);
}
/// <summary>
/// Gets an 'identity representation' of an entity, with their true name being the entity name
/// and their 'presumed name' and 'presumed job' being the name/job on their ID card, if they have one.
/// </summary>
private IdentityRepresentation GetIdentityRepresentation(EntityUid target,
InventoryComponent? inventory=null,
HumanoidAppearanceComponent? appearance=null)
{
int age = 18;
Gender gender = Gender.Epicene;
string species = SharedHumanoidAppearanceSystem.DefaultSpecies;
// Always use their actual age and gender, since that can't really be changed by an ID.
if (Resolve(target, ref appearance, false))
{
gender = appearance.Gender;
age = appearance.Age;
species = appearance.Species;
}
var ageString = _humanoid.GetAgeRepresentation(species, age);
var trueName = Name(target);
if (!Resolve(target, ref inventory, false))
return new(trueName, gender, ageString, string.Empty);
string? presumedJob = null;
string? presumedName = null;
// Get their name and job from their ID for their presumed name.
if (_idCard.TryFindIdCard(target, out var id))
{
presumedName = string.IsNullOrWhiteSpace(id.Comp.FullName) ? null : id.Comp.FullName;
presumedJob = id.Comp.LocalizedJobTitle?.ToLowerInvariant();
}
// If it didn't find a job, that's fine.
return new(trueName, gender, ageString, presumedName, presumedJob);
}
#endregion
}

View File

@@ -72,6 +72,15 @@ public sealed class JammerSystem : SharedJammerSystem
EnsureComp<DeviceNetworkJammerComponent>(ent, out var jammingComp);
_jammer.SetRange((ent, jammingComp), GetCurrentRange(ent));
_jammer.AddJammableNetwork((ent, jammingComp), DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString());
// Add excluded frequencies using the system method
if (ent.Comp.FrequenciesExcluded != null)
{
foreach (var freq in ent.Comp.FrequenciesExcluded)
{
_jammer.AddExcludedFrequency((ent, jammingComp), (uint)freq);
}
}
}
else
{
@@ -96,19 +105,23 @@ public sealed class JammerSystem : SharedJammerSystem
private void OnRadioSendAttempt(ref RadioSendAttemptEvent args)
{
if (ShouldCancelSend(args.RadioSource))
if (ShouldCancelSend(args.RadioSource, args.Channel.Frequency))
{
args.Cancelled = true;
}
}
private bool ShouldCancelSend(EntityUid sourceUid)
private bool ShouldCancelSend(EntityUid sourceUid, int frequency)
{
var source = Transform(sourceUid).Coordinates;
var query = EntityQueryEnumerator<ActiveRadioJammerComponent, RadioJammerComponent, TransformComponent>();
while (query.MoveNext(out var uid, out _, out var jam, out var transform))
{
// Check if this jammer excludes the frequency
if (jam.FrequenciesExcluded != null && jam.FrequenciesExcluded.Contains(frequency))
continue;
if (_transform.InRange(source, transform.Coordinates, GetCurrentRange((uid, jam))))
{
return true;

View File

@@ -1,6 +1,5 @@
using Content.Server.Access.Systems;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Mind;
using Content.Server.PDA;
using Content.Server.Station.Components;
@@ -11,6 +10,7 @@ using Content.Shared.Clothing;
using Content.Shared.DetailExaminable;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;

View File

@@ -35,7 +35,8 @@ public sealed class ImmovableRodRule : StationEventSystem<ImmovableRodRuleCompon
var speed = RobustRandom.NextFloat(rod.MinSpeed, rod.MaxSpeed);
var angle = RobustRandom.NextAngle();
var direction = angle.ToVec();
var spawnCoords = targetCoords.ToMap(EntityManager, _transform).Offset(-direction * speed * despawn.Lifetime / 2);
var mapCoords = _transform.ToMapCoordinates(targetCoords);
var spawnCoords = mapCoords.Offset(-direction * speed * despawn.Lifetime / 2);
var ent = Spawn(protoName, spawnCoords);
_gun.ShootProjectile(ent, direction, Vector2.Zero, uid, speed: speed);
}

View File

@@ -215,26 +215,6 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
return false;
}
/// <summary>
/// Try to get a record from this station's record entries,
/// from the provided station record key. Will always return
/// null if the key does not match the station.
/// </summary>
/// <param name="key">Station and key to try and index from the record set.</param>
/// <param name="entry">The resulting entry.</param>
/// <param name="records">Station record component.</param>
/// <typeparam name="T">Type to get from the record set.</typeparam>
/// <returns>True if the record was obtained, false otherwise.</returns>
public bool TryGetRecord<T>(StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
{
entry = default;
if (!Resolve(key.OriginStation, ref records))
return false;
return records.Records.TryGetRecordEntry(key.Id, out entry);
}
/// <summary>
/// Gets a random record from the station's record entries.
/// </summary>
@@ -257,26 +237,6 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
return ent.Comp.Records.TryGetRecordEntry(key, out entry);
}
/// <summary>
/// Returns an id if a record with the same name exists.
/// </summary>
/// <remarks>
/// Linear search so O(n) time complexity.
/// </remarks>
public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records, false))
return null;
foreach (var (id, record) in GetRecordsOfType<GeneralStationRecord>(station, records))
{
if (record.Name == name)
return id;
}
return null;
}
/// <summary>
/// Get the name for a record, or an empty string if it has no record.
/// </summary>
@@ -288,21 +248,6 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
return record.Name;
}
/// <summary>
/// Gets all records of a specific type from a station.
/// </summary>
/// <param name="station">The station to get the records from.</param>
/// <param name="records">Station records component.</param>
/// <typeparam name="T">Type of record to fetch</typeparam>
/// <returns>Enumerable of pairs with a station record key, and the entry in question of type T.</returns>
public IEnumerable<(uint, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
return Array.Empty<(uint, T)>();
return records.Records.GetRecordsOfType<T>();
}
/// <summary>
/// Adds a new record entry to a station's record set.
/// </summary>

View File

@@ -6,7 +6,6 @@ using Content.Server.Chat.Managers;
using Content.Server.Ghost;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Inventory;
using Content.Server.Mind;
using Content.Server.NPC;
@@ -40,6 +39,7 @@ using Content.Shared.Prying.Components;
using Content.Shared.Traits.Assorted;
using Robust.Shared.Audio.Systems;
using Content.Shared.Ghost.Roles.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Tag;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;

View File

@@ -840,7 +840,7 @@ public abstract partial class SharedActionsSystem : EntitySystem
if (!_actionsQuery.Resolve(performer, ref performer.Comp, false))
{
DebugTools.Assert(performer == null || TerminatingOrDeleted(performer));
DebugTools.Assert(TerminatingOrDeleted(performer));
ent.Comp.AttachedEntity = null;
// TODO: should this delete the action since it's now orphaned?
return;

View File

@@ -14,7 +14,6 @@ namespace Content.Shared.Clothing;
public sealed class SharedMagbootsSystem : EntitySystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;

View File

@@ -9,12 +9,12 @@ namespace Content.Shared.Coordinates.Helpers
public static EntityCoordinates SnapToGrid(this EntityCoordinates coordinates, IEntityManager? entMan = null, IMapManager? mapManager = null)
{
IoCManager.Resolve(ref entMan, ref mapManager);
var xformSys = entMan.System<SharedTransformSystem>();
var gridId = coordinates.GetGridUid(entMan);
var gridId = xformSys.GetGrid(coordinates.EntityId);
if (gridId == null)
{
var xformSys = entMan.System<SharedTransformSystem>();
var mapPos = xformSys.ToMapCoordinates(coordinates);
var mapX = (int)Math.Floor(mapPos.X) + 0.5f;
var mapY = (int)Math.Floor(mapPos.Y) + 0.5f;
@@ -24,11 +24,11 @@ namespace Content.Shared.Coordinates.Helpers
var grid = entMan.GetComponent<MapGridComponent>(gridId.Value);
var tileSize = grid.TileSize;
var localPos = coordinates.WithEntityId(gridId.Value).Position;
var localPos = xformSys.WithEntityId(coordinates, gridId.Value).Position;
var x = (int)Math.Floor(localPos.X / tileSize) + tileSize / 2f;
var y = (int)Math.Floor(localPos.Y / tileSize) + tileSize / 2f;
var gridPos = new EntityCoordinates(gridId.Value, new Vector2(x, y));
return gridPos.WithEntityId(coordinates.EntityId);
return xformSys.WithEntityId(gridPos, coordinates.EntityId);
}
public static EntityCoordinates SnapToGrid(this EntityCoordinates coordinates, MapGridComponent grid)

View File

@@ -1,6 +1,44 @@
using Content.Shared.IdentityManagement;
using Content.Shared.Security;
using Content.Shared.Security.Components;
using Content.Shared.Station;
using Content.Shared.StationRecords;
namespace Content.Shared.CriminalRecords.Systems;
/// <summary>
/// Station records aren't predicted, just exists for access.
/// </summary>
public abstract class SharedCriminalRecordsConsoleSystem : EntitySystem;
public abstract class SharedCriminalRecordsConsoleSystem : EntitySystem
{
[Dependency] private readonly SharedCriminalRecordsSystem _criminalRecords = default!;
[Dependency] private readonly SharedStationRecordsSystem _records = default!;
[Dependency] private readonly SharedStationSystem _station = default!;
/// <summary>
/// Checks if the new identity's name has a criminal record attached to it, and gives the entity the icon that
/// belongs to the status if it does.
/// </summary>
public void CheckNewIdentity(EntityUid uid)
{
var name = Identity.Name(uid, EntityManager);
var xform = Transform(uid);
// TODO use the entity's station? Not the station of the map that it happens to currently be on?
var station = _station.GetStationInMap(xform.MapID);
if (station != null && _records.GetRecordByName(station.Value, name) is { } id)
{
if (_records.TryGetRecord<CriminalRecord>(new StationRecordKey(id, station.Value),
out var record))
{
if (record.Status != SecurityStatus.None)
{
_criminalRecords.SetCriminalIcon(name, record.Status, uid);
return;
}
}
}
RemComp<CriminalRecordComponent>(uid);
}
}

View File

@@ -31,7 +31,8 @@ public sealed class DamagePopupSystem : EntitySystem
_ => "Invalid type",
};
_popupSystem.PopupPredicted(msg, ent.Owner, args.Origin);
// Turn this back into (msg, ent.Owner, args.Origin) when shooting gets predicted.
_popupSystem.PopupPredicted(msg, ent.Owner, null);
}
}

View File

@@ -23,4 +23,10 @@ public sealed partial class DeviceNetworkJammerComponent : Component
[DataField, AutoNetworkedField]
public HashSet<string> JammableNetworks = [];
/// <summary>
/// Device networks frequencies that wont be jammed.
/// </summary>
[DataField]
public HashSet<uint> FrequenciesExcluded = [];
}

View File

@@ -25,11 +25,17 @@ public sealed class BeforePacketSentEvent : CancellableEntityEventArgs
/// </summary>
public readonly string NetworkId;
public BeforePacketSentEvent(EntityUid sender, TransformComponent xform, Vector2 senderPosition, string networkId)
/// <summary>
/// The frequency the packet is sent on.
/// </summary>
public readonly uint Frequency;
public BeforePacketSentEvent(EntityUid sender, TransformComponent xform, Vector2 senderPosition, string networkId, uint frequency)
{
Sender = sender;
SenderTransform = xform;
SenderPosition = senderPosition;
NetworkId = networkId;
Frequency = frequency;
}
}
}

View File

@@ -60,4 +60,34 @@ public abstract class SharedDeviceNetworkJammerSystem : EntitySystem
ent.Comp.JammableNetworks.Clear();
Dirty(ent);
}
/// <summary>
/// Enables this entity to stop packets with the specified frequency from being jammmed.
/// </summary>
public void AddExcludedFrequency(Entity<DeviceNetworkJammerComponent> ent, uint frequency)
{
if (ent.Comp.FrequenciesExcluded.Add(frequency))
Dirty(ent);
}
/// <summary>
/// Stops this entity to stop packets with the specified frequency from being jammmed.
/// </summary>
public void RemoveExcludedFrequency(Entity<DeviceNetworkJammerComponent> ent, uint frequency)
{
if (ent.Comp.FrequenciesExcluded.Remove(frequency))
Dirty(ent);
}
/// <summary>
/// Stops this entity to stop packets with any frequency from being jammmed.
/// </summary>
public void ClearExcludedFrequency(Entity<DeviceNetworkJammerComponent> ent)
{
if (ent.Comp.FrequenciesExcluded.Count == 0)
return;
ent.Comp.FrequenciesExcluded.Clear();
Dirty(ent);
}
}

View File

@@ -1,14 +1,26 @@
using Content.Shared.Armor;
using Content.Shared.Explosion.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.Explosion.EntitySystems;
// TODO some sort of struct like DamageSpecifier but for explosions.
/// <summary>
/// Lets code in shared trigger explosions and handles explosion resistance examining.
/// All processing is still done clientside.
/// </summary>
public abstract class SharedExplosionSystem : EntitySystem
{
/// <summary>
/// The "default" explosion prototype.
/// </summary>
/// <remarks>
/// Generally components should specify an explosion prototype via a yaml datafield, so that the yaml-linter can
/// find errors. However some components, like rogue arrows, or some commands like the admin-smite need to have
/// a "default" option specified outside of yaml data-fields. Hence this const string.
/// </remarks>
public static readonly ProtoId<ExplosionPrototype> DefaultExplosionPrototypeId = "Default";
public override void Initialize()
{
base.Initialize();
@@ -37,4 +49,24 @@ public abstract class SharedExplosionSystem : EntitySystem
public virtual void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null)
{
}
/// <summary>
/// Queue an explosion centered on some entity. Bypasses needing <see cref="ExplosiveComponent"/>.
/// </summary>
/// <param name="uid">Where the explosion happens.</param>
/// <param name="typeId">A ProtoId of type <see cref="ExplosionPrototype"/>.</param>
/// <param name="user">The entity which caused the explosion.</param>
/// <param name="addLog">Whether to add an admin log about this explosion. Includes user.</param>
public virtual void QueueExplosion(EntityUid uid,
string typeId,
float totalIntensity,
float slope,
float maxTileIntensity,
float tileBreakScale = 1f,
int maxTileBreak = int.MaxValue,
bool canCreateVacuum = true,
EntityUid? user = null,
bool addLog = true)
{
}
}

View File

@@ -42,7 +42,7 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
[Dependency] private readonly ISerializationManager _serManager = default!;
[Dependency] private readonly MarkingManager _markingManager = default!;
[Dependency] private readonly GrammarSystem _grammarSystem = default!;
[Dependency] private readonly SharedIdentitySystem _identity = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
private ISharedSponsorsManager? _sponsors;
public static readonly ProtoId<SpeciesPrototype> DefaultSpecies = "Human";

View File

@@ -1,12 +1,13 @@
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.IdentityManagement.Components;
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class IdentityBlockerComponent : Component
{
[DataField]
[DataField, AutoNetworkedField]
public bool Enabled = true;
/// <summary>
@@ -16,6 +17,8 @@ public sealed partial class IdentityBlockerComponent : Component
public IdentityBlockerCoverage Coverage = IdentityBlockerCoverage.FULL;
}
[Flags]
[Serializable, NetSerializable]
public enum IdentityBlockerCoverage
{
NONE = 0,

View File

@@ -1,5 +1,6 @@
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
namespace Content.Shared.IdentityManagement.Components;
@@ -10,7 +11,7 @@ namespace Content.Shared.IdentityManagement.Components;
/// <remarks>
/// This is a <see cref="ContainerSlot"/> and not just a datum entity because we do sort of care that it gets deleted and sent with the user.
/// </remarks>
[RegisterComponent]
[RegisterComponent, NetworkedComponent]
public sealed partial class IdentityComponent : Component
{
[ViewVariables]

View File

@@ -0,0 +1,241 @@
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Clothing;
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.Database;
using Content.Shared.Hands;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects.Components.Localization;
using Robust.Shared.Timing;
namespace Content.Shared.IdentityManagement;
/// <summary>
/// Responsible for updating the identity of an entity on init or clothing equip/unequip.
/// </summary>
public sealed class IdentitySystem : EntitySystem
{
[Dependency] private readonly GrammarSystem _grammarSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedCriminalRecordsConsoleSystem _criminalRecordsConsole = default!;
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
[Dependency] private readonly SharedIdCardSystem _idCard = default!;
// The name of the container holding the identity entity
private const string SlotName = "identity";
// Recycled hashset for tracking identities each tick that need to update
private readonly HashSet<EntityUid> _queuedIdentityUpdates = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IdentityBlockerComponent, SeeIdentityAttemptEvent>(OnSeeIdentity);
SubscribeLocalEvent<IdentityBlockerComponent, InventoryRelayedEvent<SeeIdentityAttemptEvent>>(OnRelaySeeIdentity);
SubscribeLocalEvent<IdentityBlockerComponent, ItemMaskToggledEvent>(OnMaskToggled);
SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<IdentityComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<IdentityComponent, DidEquipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidEquipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
}
/// <summary>
/// Iterates through all identities that need to be updated.
/// </summary>
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var ent in _queuedIdentityUpdates)
{
if (!TryComp<IdentityComponent>(ent, out var identity))
continue;
UpdateIdentityInfo((ent, identity));
}
_queuedIdentityUpdates.Clear();
}
#region Event Handlers
// Creates an identity entity, and store it in the identity container
private void OnMapInit(Entity<IdentityComponent> ent, ref MapInitEvent args)
{
var ident = Spawn(null, Transform(ent).Coordinates);
_metaData.SetEntityName(ident, "identity");
QueueIdentityUpdate(ent);
_container.Insert(ident, ent.Comp.IdentityEntitySlot);
}
private void OnComponentInit(Entity<IdentityComponent> ent, ref ComponentInit args)
{
ent.Comp.IdentityEntitySlot = _container.EnsureContainer<ContainerSlot>(ent, SlotName);
}
// Adds an identity blocker's coverage, and cancels the event if coverage is complete.
private void OnSeeIdentity(Entity<IdentityBlockerComponent> ent, ref SeeIdentityAttemptEvent args)
{
if (ent.Comp.Enabled)
{
args.TotalCoverage |= ent.Comp.Coverage;
if (args.TotalCoverage == IdentityBlockerCoverage.FULL)
args.Cancel();
}
}
private void OnRelaySeeIdentity(Entity<IdentityBlockerComponent> ent, ref InventoryRelayedEvent<SeeIdentityAttemptEvent> args)
{
OnSeeIdentity(ent, ref args.Args);
}
// Toggles if a mask is hiding the identity.
private void OnMaskToggled(Entity<IdentityBlockerComponent> ent, ref ItemMaskToggledEvent args)
{
ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
Dirty(ent);
}
#endregion
/// <summary>
/// Queues an identity update to the start of the next tick.
/// </summary>
public void QueueIdentityUpdate(EntityUid uid)
{
if (_timing.ApplyingState)
return;
_queuedIdentityUpdates.Add(uid);
}
#region Private API
/// <summary>
/// Updates the metadata name for the id(entity) from the current state of the character.
/// </summary>
private void UpdateIdentityInfo(Entity<IdentityComponent> ent)
{
if (ent.Comp.IdentityEntitySlot.ContainedEntity is not { } ident)
return;
var representation = GetIdentityRepresentation(ent.Owner);
var name = GetIdentityName(ent, representation);
// Clone the old entity's grammar to the identity entity, for loc purposes.
if (TryComp<GrammarComponent>(ent, out var grammar))
{
var identityGrammar = EnsureComp<GrammarComponent>(ident);
identityGrammar.Attributes.Clear();
foreach (var (k, v) in grammar.Attributes)
{
identityGrammar.Attributes.Add(k, v);
}
// If presumed name is null and we're using that, we set proper noun to be false ("the old woman")
if (name != representation.TrueName && representation.PresumedName == null)
_grammarSystem.SetProperNoun((ident, identityGrammar), false);
Dirty(ident, identityGrammar);
}
if (name == Name(ident))
return;
_metaData.SetEntityName(ident, name);
_adminLog.Add(LogType.Identity, LogImpact.Medium, $"{ToPrettyString(ent)} changed identity to {name}");
var identityChangedEvent = new IdentityChangedEvent(ent, ident);
RaiseLocalEvent(ent, ref identityChangedEvent);
SetIdentityCriminalIcon(ent);
}
/// <summary>
/// When the identity of a person is changed, searches the criminal records to see if the name of the new identity
/// has a record. If the new name has a criminal status attached to it, the person will get the criminal status
/// until they change identity again.
/// </summary>
private void SetIdentityCriminalIcon(EntityUid uid)
{
_criminalRecordsConsole.CheckNewIdentity(uid);
}
/// <summary>
/// Attempts to get an entity's name. Cancelled if the entity has full coverage from <see cref="IdentityBlockerComponent"/>.
/// </summary>
/// <param name="target">The entity being targeted.</param>
/// <param name="representation">The data structure containing an entity's identities.</param>
/// <returns>
/// An entity's real name if <see cref="SeeIdentityAttemptEvent"/> isn't cancelled,
/// or a hidden identity such as a fake ID or fully hidden identity like "middle-aged man".
/// </returns>
private string GetIdentityName(EntityUid target, IdentityRepresentation representation)
{
var ev = new SeeIdentityAttemptEvent();
RaiseLocalEvent(target, ev);
return representation.ToStringKnown(!ev.Cancelled);
}
/// <summary>
/// Gets an 'identity representation' of an entity, with their true name being the entity name
/// and their 'presumed name' and 'presumed job' being the name/job on their ID card, if they have one.
/// </summary>
private IdentityRepresentation GetIdentityRepresentation(Entity<InventoryComponent?, HumanoidAppearanceComponent?> target)
{
var age = 18;
var gender = Gender.Epicene;
var species = SharedHumanoidAppearanceSystem.DefaultSpecies;
// Always use their actual age and gender, since that can't really be changed by an ID.
if (Resolve(target, ref target.Comp2, false))
{
gender = target.Comp2.Gender;
age = target.Comp2.Age;
species = target.Comp2.Species;
}
var ageString = _humanoid.GetAgeRepresentation(species, age);
var trueName = Name(target);
if (!Resolve(target, ref target.Comp1, false))
return new(trueName, gender, ageString, string.Empty);
string? presumedJob = null;
string? presumedName = null;
// Get their name and job from their ID for their presumed name.
if (_idCard.TryFindIdCard(target, out var id))
{
presumedName = string.IsNullOrWhiteSpace(id.Comp.FullName) ? null : id.Comp.FullName;
presumedJob = id.Comp.LocalizedJobTitle?.ToLowerInvariant();
}
// If it didn't find a job, that's fine.
return new(trueName, gender, ageString, presumedName, presumedJob);
}
#endregion
}
/// <summary>
/// Gets called whenever an entity changes their identity.
/// </summary>
[ByRefEvent]
public record struct IdentityChangedEvent(EntityUid CharacterEntity, EntityUid IdentityEntity);

View File

@@ -1,52 +0,0 @@
using Content.Shared.Clothing;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory;
using Robust.Shared.Containers;
namespace Content.Shared.IdentityManagement;
public abstract class SharedIdentitySystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
private static string SlotName = "identity";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IdentityComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<IdentityBlockerComponent, SeeIdentityAttemptEvent>(OnSeeIdentity);
SubscribeLocalEvent<IdentityBlockerComponent, InventoryRelayedEvent<SeeIdentityAttemptEvent>>((e, c, ev) => OnSeeIdentity(e, c, ev.Args));
SubscribeLocalEvent<IdentityBlockerComponent, ItemMaskToggledEvent>(OnMaskToggled);
}
private void OnSeeIdentity(EntityUid uid, IdentityBlockerComponent component, SeeIdentityAttemptEvent args)
{
if (component.Enabled)
{
args.TotalCoverage |= component.Coverage;
if(args.TotalCoverage == IdentityBlockerCoverage.FULL)
args.Cancel();
}
}
protected virtual void OnComponentInit(EntityUid uid, IdentityComponent component, ComponentInit args)
{
component.IdentityEntitySlot = _container.EnsureContainer<ContainerSlot>(uid, SlotName);
}
private void OnMaskToggled(Entity<IdentityBlockerComponent> ent, ref ItemMaskToggledEvent args)
{
ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
}
/// <summary>
/// Queues an identity update to the start of the next tick.
/// </summary>
public virtual void QueueIdentityUpdate(EntityUid uid) { }
}
/// <summary>
/// Gets called whenever an entity changes their identity.
/// </summary>
[ByRefEvent]
public record struct IdentityChangedEvent(EntityUid CharacterEntity, EntityUid IdentityEntity);

View File

@@ -4,7 +4,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Radio.Components;
/// <summary>
/// Prevents all radio in range from sending messages
/// Prevents all non whitelisted radios from sending messages
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedJammerSystem))]

View File

@@ -46,6 +46,12 @@ public sealed partial class RadioJammerComponent : Component
[DataField(required: true), ViewVariables(VVAccess.ReadOnly)]
public RadioJamSetting[] Settings;
/// <summary>
/// Frequencies that are NOT jammed by this jammer.
/// </summary>
[DataField]
public HashSet<int> FrequenciesExcluded = [];
/// <summary>
/// Index of the currently selected setting.
/// </summary>

View File

@@ -1,17 +1,16 @@
using Content.Shared.Random;
using Content.Shared.EntityTable.EntitySelectors;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.RatKing;
namespace Content.Shared.RatKing.Components;
/// <summary>
/// This is used for entities that can be
/// rummaged through by the rat king to get loot.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedRatKingSystem))]
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class RatKingRummageableComponent : Component
public sealed partial class RummageableComponent : Component
{
/// <summary>
/// Whether or not this entity has been rummaged through already.
@@ -28,11 +27,10 @@ public sealed partial class RatKingRummageableComponent : Component
public float RummageDuration = 3f;
/// <summary>
/// A weighted random entity prototype containing the different loot that rummaging can provide.
/// The entity table to select loot from.
/// </summary>
[DataField("rummageLoot", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomEntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public string RummageLoot = "RatKingLoot";
[DataField(required: true)]
public EntityTableSelector Table = default!;
/// <summary>
/// Sound played on rummage completion.

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameStates;
namespace Content.Shared.RatKing.Components;
/// <summary>
/// This is used for entities that can rummage through entities
/// with the <see cref="RummageableComponent"/>
/// </summary>
///
[RegisterComponent, NetworkedComponent]
public sealed partial class RummagerComponent : Component;

View File

@@ -1,26 +1,15 @@
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.DoAfter;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Content.Shared.Actions.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
namespace Content.Shared.RatKing;
public abstract class SharedRatKingSystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] protected readonly IRobustRandom Random = default!;
[Dependency] private readonly SharedActionsSystem _action = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
/// <inheritdoc/>
public override void Initialize()
@@ -28,11 +17,7 @@ public abstract class SharedRatKingSystem : EntitySystem
SubscribeLocalEvent<RatKingComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<RatKingComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<RatKingComponent, RatKingOrderActionEvent>(OnOrderAction);
SubscribeLocalEvent<RatKingServantComponent, ComponentShutdown>(OnServantShutdown);
SubscribeLocalEvent<RatKingRummageableComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerb);
SubscribeLocalEvent<RatKingRummageableComponent, RatKingRummageDoAfterEvent>(OnDoAfterComplete);
}
private void OnStartup(EntityUid uid, RatKingComponent component, ComponentStartup args)
@@ -105,43 +90,6 @@ public abstract class SharedRatKingSystem : EntitySystem
_action.StartUseDelay(component.ActionOrderLooseEntity);
}
private void OnGetVerb(EntityUid uid, RatKingRummageableComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!HasComp<RatKingComponent>(args.User) || component.Looted)
return;
args.Verbs.Add(new AlternativeVerb
{
Text = Loc.GetString("rat-king-rummage-text"),
Priority = 0,
Act = () =>
{
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.RummageDuration,
new RatKingRummageDoAfterEvent(), uid, uid)
{
BlockDuplicate = true,
BreakOnDamage = true,
BreakOnMove = true,
DistanceThreshold = 2f
});
}
});
}
private void OnDoAfterComplete(EntityUid uid, RatKingRummageableComponent component, RatKingRummageDoAfterEvent args)
{
if (args.Cancelled || component.Looted)
return;
component.Looted = true;
Dirty(uid, component);
_audio.PlayPredicted(component.Sound, uid, args.User);
var spawn = PrototypeManager.Index<WeightedRandomEntityPrototype>(component.RummageLoot).Pick(Random);
if (_net.IsServer)
Spawn(spawn, Transform(uid).Coordinates);
}
public void UpdateAllServants(EntityUid uid, RatKingComponent component)
{
foreach (var servant in component.Servants)
@@ -160,9 +108,3 @@ public abstract class SharedRatKingSystem : EntitySystem
}
}
[Serializable, NetSerializable]
public sealed partial class RatKingRummageDoAfterEvent : SimpleDoAfterEvent
{
}

View File

@@ -0,0 +1,82 @@
using Content.Shared.DoAfter;
using Content.Shared.EntityTable;
using Content.Shared.RatKing.Components;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared.RatKing.Systems;
public sealed class RummagerSystem : EntitySystem
{
[Dependency] private readonly EntityTableSystem _entityTable = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RummageableComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerb);
SubscribeLocalEvent<RummageableComponent, RummageDoAfterEvent>(OnDoAfterComplete);
}
private void OnGetVerb(Entity<RummageableComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!HasComp<RummagerComponent>(args.User) || ent.Comp.Looted)
return;
var user = args.User;
args.Verbs.Add(new AlternativeVerb
{
Text = Loc.GetString("rat-king-rummage-text"),
Priority = 0,
Act = () =>
{
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager,
user,
ent.Comp.RummageDuration,
new RummageDoAfterEvent(),
ent,
ent)
{
BlockDuplicate = true,
BreakOnDamage = true,
BreakOnMove = true,
DistanceThreshold = 2f
});
}
});
}
private void OnDoAfterComplete(Entity<RummageableComponent> ent, ref RummageDoAfterEvent args)
{
if (args.Cancelled || ent.Comp.Looted)
return;
ent.Comp.Looted = true;
Dirty(ent, ent.Comp);
_audio.PlayPredicted(ent.Comp.Sound, ent, args.User);
if (_net.IsClient)
return;
var spawns = _entityTable.GetSpawns(ent.Comp.Table);
var coordinates = Transform(ent).Coordinates;
foreach (var spawn in spawns)
{
Spawn(spawn, coordinates);
}
}
}
/// <summary>
/// DoAfter event for rummaging through a container with RummageableComponent.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class RummageDoAfterEvent : SimpleDoAfterEvent;

View File

@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.StationRecords;
public abstract class SharedStationRecordsSystem : EntitySystem
@@ -40,4 +42,60 @@ public abstract class SharedStationRecordsSystem : EntitySystem
}
return result;
}
/// <summary>
/// Try to get a record from this station's record entries,
/// from the provided station record key. Will always return
/// null if the key does not match the station.
/// </summary>
/// <param name="key">Station and key to try and index from the record set.</param>
/// <param name="entry">The resulting entry.</param>
/// <param name="records">Station record component.</param>
/// <typeparam name="T">Type to get from the record set.</typeparam>
/// <returns>True if the record was obtained, false otherwise. Always false on client.</returns>
public bool TryGetRecord<T>(StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
{
entry = default;
if (!Resolve(key.OriginStation, ref records))
return false;
return records.Records.TryGetRecordEntry(key.Id, out entry);
}
/// <summary>
/// Gets all records of a specific type from a station.
/// </summary>
/// <param name="station">The station to get the records from.</param>
/// <param name="records">Station records component.</param>
/// <typeparam name="T">Type of record to fetch</typeparam>
/// <returns>Enumerable of pairs with a station record key, and the entry in question of type T. Always empty on client.</returns>
public IEnumerable<(uint, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
return Array.Empty<(uint, T)>();
return records.Records.GetRecordsOfType<T>();
}
/// <summary>
/// Returns an id if a record with the same name exists.
/// </summary>
/// <remarks>
/// Linear search so O(n) time complexity.
/// </remarks>
/// <returns>Returns a station record id. Always null on client.</returns>
public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records, false))
return null;
foreach (var (id, record) in GetRecordsOfType<GeneralStationRecord>(station, records))
{
if (record.Name == name)
return id;
}
return null;
}
}

View File

@@ -1,9 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.StationRecords;
using Robust.Shared.Utility;
namespace Content.Server.StationRecords;
namespace Content.Shared.StationRecords;
/// <summary>
/// Set of station records for a single station. StationRecordsComponent stores these.

View File

@@ -1,8 +1,6 @@
using Content.Server.StationRecords.Systems;
namespace Content.Shared.StationRecords;
namespace Content.Server.StationRecords;
[Access(typeof(StationRecordsSystem))]
[Access(typeof(SharedStationRecordsSystem))]
[RegisterComponent]
public sealed partial class StationRecordsComponent : Component
{

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Content.Shared.Ghost;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
@@ -6,7 +6,6 @@ using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Teleportation.Components;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
@@ -19,8 +18,10 @@ using Robust.Shared.Utility;
namespace Content.Shared.Teleportation.Systems;
/// <summary>
/// This handles teleporting entities through portals, and creating new linked portals.
/// This handles teleporting entities from a portal to a linked portal, or to a random nearby destination.
/// Uses <see cref="LinkedEntitySystem"/> to get linked portals.
/// </summary>
/// <seealso cref="PortalComponent"/>
public abstract class SharedPortalSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
@@ -39,12 +40,13 @@ public abstract class SharedPortalSystem : EntitySystem
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<PortalComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
SubscribeLocalEvent<PortalComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<PortalComponent, EndCollideEvent>(OnEndCollide);
SubscribeLocalEvent<PortalComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
}
private void OnGetVerbs(EntityUid uid, PortalComponent component, GetVerbsEvent<AlternativeVerb> args)
private void OnGetVerbs(Entity<PortalComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
// Traversal altverb for ghosts to use that bypasses normal functionality
if (!args.CanAccess || !HasComp<GhostComponent>(args.User))
@@ -52,8 +54,9 @@ public abstract class SharedPortalSystem : EntitySystem
// Don't use the verb with unlinked or with multi-output portals
// (this is only intended to be useful for ghosts to see where a linked portal leads)
var disabled = !TryComp<LinkedEntityComponent>(uid, out var link) || link.LinkedEntities.Count != 1;
var disabled = !TryComp<LinkedEntityComponent>(ent, out var link) || link.LinkedEntities.Count != 1;
var subject = args.User;
args.Verbs.Add(new AlternativeVerb
{
Priority = 11,
@@ -62,8 +65,13 @@ public abstract class SharedPortalSystem : EntitySystem
if (link == null || disabled)
return;
var ent = link.LinkedEntities.First();
TeleportEntity(uid, args.User, Transform(ent).Coordinates, ent, false);
// check prediction
if (_netMan.IsClient && !CanPredictTeleport((ent, link)))
return;
var destination = link.LinkedEntities.First();
TeleportEntity(ent, subject, Transform(destination).Coordinates, destination, false);
},
Disabled = disabled,
Text = Loc.GetString("portal-component-ghost-traverse"),
@@ -74,14 +82,7 @@ public abstract class SharedPortalSystem : EntitySystem
});
}
private bool ShouldCollide(string ourId, string otherId, Fixture our, Fixture other)
{
// most non-hard fixtures shouldn't pass through portals, but projectiles are non-hard as well
// and they should still pass through
return ourId == PortalFixture && (other.Hard || otherId == ProjectileFixture);
}
private void OnCollide(EntityUid uid, PortalComponent component, ref StartCollideEvent args)
private void OnCollide(Entity<PortalComponent> ent, ref StartCollideEvent args)
{
if (!ShouldCollide(args.OurFixtureId, args.OtherFixtureId, args.OurFixture, args.OtherFixture))
return;
@@ -92,7 +93,7 @@ public abstract class SharedPortalSystem : EntitySystem
if (Transform(subject).Anchored)
return;
// break pulls before portal enter so we dont break shit
// break pulls before portal enter so we don't break shit
if (TryComp<PullableComponent>(subject, out var pullable) && pullable.BeingPulled)
{
_pulling.TryStopPull(subject, pullable);
@@ -110,33 +111,27 @@ public abstract class SharedPortalSystem : EntitySystem
return;
}
if (TryComp<LinkedEntityComponent>(uid, out var link))
if (TryComp<LinkedEntityComponent>(ent, out var link))
{
if (link.LinkedEntities.Count == 0)
return;
// client can't predict outside of simple portal-to-portal interactions due to randomness involved
// --also can't predict if the target doesn't exist on the client / is outside of PVS
if (_netMan.IsClient)
{
var first = link.LinkedEntities.First();
var exists = Exists(first);
if (link.LinkedEntities.Count != 1 || !exists || (exists && Transform(first).MapID == MapId.Nullspace))
return;
}
// check prediction
if (_netMan.IsClient && !CanPredictTeleport((ent, link)))
return;
// pick a target and teleport there
var target = _random.Pick(link.LinkedEntities);
if (HasComp<PortalComponent>(target))
{
// if target is a portal, signal that they shouldn't be immediately portaled back
// if target is a portal, signal that they shouldn't be immediately teleported back
var timeout = EnsureComp<PortalTimeoutComponent>(subject);
timeout.EnteredPortal = uid;
timeout.EnteredPortal = ent;
Dirty(subject, timeout);
}
TeleportEntity(uid, subject, Transform(target).Coordinates, target);
TeleportEntity(ent, subject, Transform(target).Coordinates, target);
return;
}
@@ -144,49 +139,86 @@ public abstract class SharedPortalSystem : EntitySystem
return;
// no linked entity--teleport randomly
if (component.RandomTeleport)
TeleportRandomly(uid, subject, component);
if (ent.Comp.RandomTeleport)
TeleportRandomly(ent, subject);
}
private void OnEndCollide(EntityUid uid, PortalComponent component, ref EndCollideEvent args)
private void OnEndCollide(Entity<PortalComponent> ent, ref EndCollideEvent args)
{
if (!ShouldCollide(args.OurFixtureId, args.OtherFixtureId, args.OurFixture, args.OtherFixture))
return;
var subject = args.OtherEntity;
// if they came from (not us), remove the timeout
if (TryComp<PortalTimeoutComponent>(subject, out var timeout) && timeout.EnteredPortal != uid)
// if they came from a different portal, remove the timeout
if (TryComp<PortalTimeoutComponent>(subject, out var timeout) && timeout.EnteredPortal != ent)
{
RemCompDeferred<PortalTimeoutComponent>(subject);
}
}
private void TeleportEntity(EntityUid portal, EntityUid subject, EntityCoordinates target, EntityUid? targetEntity = null, bool playSound = true,
PortalComponent? portalComponent = null)
/// <summary>
/// Checks if the colliding fixtures are the ones we want.
/// </summary>
/// <returns>
/// False if our fixture is not a portal fixture.
/// False if other fixture is not hard, but makes an exception for projectiles.
/// </returns>
private bool ShouldCollide(string ourId, string otherId, Fixture our, Fixture other)
{
if (!Resolve(portal, ref portalComponent))
return;
return ourId == PortalFixture && (other.Hard || otherId == ProjectileFixture);
}
var ourCoords = Transform(portal).Coordinates;
/// <summary>
/// Checks if the client is able to predict the teleport.
/// Client can't predict outside 1-to-1 portal-to-portal interactions due to randomness involved.
/// </summary>
/// <returns>
/// False if the linked entity count isn't 1.
/// False if the linked entity doesn't exist on the client / is outside PVS.
/// </returns>
private bool CanPredictTeleport(Entity<LinkedEntityComponent> portal)
{
var first = portal.Comp.LinkedEntities.First();
var exists = Exists(first);
if (!exists ||
portal.Comp.LinkedEntities.Count != 1 || // 0 and >1 use RNG
exists && Transform(first).MapID == MapId.Nullspace) // The linked entity is most likely outside PVS
return false;
return true;
}
/// <summary>
/// Handles teleporting a subject from the portal entity to a coordinate.
/// Also deletes invalid portals.
/// </summary>
/// <param name="ent"> The portal being collided with. </param>
/// <param name="subject"> The entity getting teleported. </param>
/// <param name="target"> The location to teleport to. </param>
/// <param name="targetEntity"> The portal on the other side of the teleport. </param>
private void TeleportEntity(Entity<PortalComponent> ent, EntityUid subject, EntityCoordinates target, EntityUid? targetEntity = null, bool playSound = true)
{
var ourCoords = Transform(ent).Coordinates;
var onSameMap = _transform.GetMapId(ourCoords) == _transform.GetMapId(target);
var distanceInvalid = portalComponent.MaxTeleportRadius != null
var distanceInvalid = ent.Comp.MaxTeleportRadius != null
&& ourCoords.TryDistance(EntityManager, target, out var distance)
&& distance > portalComponent.MaxTeleportRadius;
&& distance > ent.Comp.MaxTeleportRadius;
if (!onSameMap && !portalComponent.CanTeleportToOtherMaps || distanceInvalid)
// Early out if this is an invalid configuration
if (!onSameMap && !ent.Comp.CanTeleportToOtherMaps || distanceInvalid)
{
if (!_netMan.IsServer)
if (_netMan.IsClient)
return;
// Early out if this is an invalid configuration
_popup.PopupCoordinates(Loc.GetString("portal-component-invalid-configuration-fizzle"),
ourCoords, Filter.Pvs(ourCoords, entityMan: EntityManager), true);
_popup.PopupCoordinates(Loc.GetString("portal-component-invalid-configuration-fizzle"),
target, Filter.Pvs(target, entityMan: EntityManager), true);
QueueDel(portal);
QueueDel(ent);
if (targetEntity != null)
QueueDel(targetEntity.Value);
@@ -194,8 +226,8 @@ public abstract class SharedPortalSystem : EntitySystem
return;
}
var arrivalSound = CompOrNull<PortalComponent>(targetEntity)?.ArrivalSound ?? portalComponent.ArrivalSound;
var departureSound = portalComponent.DepartureSound;
var arrivalSound = CompOrNull<PortalComponent>(targetEntity)?.ArrivalSound ?? ent.Comp.ArrivalSound;
var departureSound = ent.Comp.DepartureSound;
// Some special cased stuff: projectiles should stop ignoring shooter when they enter a portal, to avoid
// stacking 500 bullets in between 2 portals and instakilling people--you'll just hit yourself instead
@@ -205,36 +237,41 @@ public abstract class SharedPortalSystem : EntitySystem
projectile.IgnoreShooter = false;
}
LogTeleport(portal, subject, Transform(subject).Coordinates, target);
LogTeleport(ent, subject, Transform(subject).Coordinates, target);
_transform.SetCoordinates(subject, target);
if (!playSound)
return;
_audio.PlayPredicted(departureSound, portal, subject);
_audio.PlayPredicted(departureSound, ent, subject);
_audio.PlayPredicted(arrivalSound, subject, subject);
}
private void TeleportRandomly(EntityUid portal, EntityUid subject, PortalComponent? component = null)
/// <summary>
/// Finds a random coordinate within the portal's radius and teleports the subject there.
/// Attempts to not put the subject inside a static entity (e.g. wall).
/// </summary>
/// <param name="ent"> The portal being collided with. </param>
/// <param name="subject"> The entity getting teleported. </param>
private void TeleportRandomly(Entity<PortalComponent> ent, EntityUid subject)
{
if (!Resolve(portal, ref component))
return;
var xform = Transform(portal);
var xform = Transform(ent);
var coords = xform.Coordinates;
var newCoords = coords.Offset(_random.NextVector2(component.MaxRandomRadius));
var newCoords = coords.Offset(_random.NextVector2(ent.Comp.MaxRandomRadius));
for (var i = 0; i < MaxRandomTeleportAttempts; i++)
{
var randVector = _random.NextVector2(component.MaxRandomRadius);
var randVector = _random.NextVector2(ent.Comp.MaxRandomRadius);
newCoords = coords.Offset(randVector);
if (!_lookup.AnyEntitiesIntersecting(_transform.ToMapCoordinates(newCoords), LookupFlags.Static))
{
// newCoords is not a wall
break;
}
// after "MaxRandomTeleportAttempts" attempts, end up in the walls
}
TeleportEntity(portal, subject, newCoords);
TeleportEntity(ent, subject, newCoords);
}
protected virtual void LogTeleport(EntityUid portal, EntityUid subject, EntityCoordinates source,

View File

@@ -5,7 +5,6 @@ using Content.Shared.CCVar;
using Content.Shared.Construction.Components;
using Content.Shared.Database;
using Content.Shared.Friction;
using Content.Shared.Gravity;
using Content.Shared.Projectiles;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
@@ -30,7 +29,6 @@ public sealed class ThrowingSystem : EntitySystem
private float _airDamping;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ThrownItemSystem _thrownSystem = default!;

View File

@@ -1,3 +1,4 @@
using Content.Shared.Explosion.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Effects;
@@ -7,8 +8,6 @@ namespace Content.Shared.Trigger.Components.Effects;
/// TargetUser will only work of the user has ExplosiveComponent as well.
/// The User will be logged in the admin logs.
/// </summary>
/// <summary>
/// TODO: Allow this to work without an ExplosiveComponent on the user via QueueExplosion.
/// </summary>
/// <seealso cref="ExplosionOnTriggerComponent"/>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExplodeOnTriggerComponent : BaseXOnTriggerComponent;

View File

@@ -0,0 +1,46 @@
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.EntitySystems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Trigger.Components.Effects;
// TODO some sort of struct like DamageSpecifier but for explosions.
/// <summary>
/// Will explode the entity using this component's explosion specifications.
/// If TargetUser is true, they'll explode instead.
/// The User will be logged in the admin logs.
/// </summary>
/// <seealso cref="ExplodeOnTriggerComponent"/>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExplosionOnTriggerComponent : BaseXOnTriggerComponent
{
/// <inheritdoc cref="ExplosiveComponent.ExplosionType"/>
[DataField, AutoNetworkedField]
public ProtoId<ExplosionPrototype> ExplosionType = SharedExplosionSystem.DefaultExplosionPrototypeId;
/// <inheritdoc cref="ExplosiveComponent.MaxIntensity"/>
[DataField, AutoNetworkedField]
public float MaxTileIntensity = 4;
/// <inheritdoc cref="ExplosiveComponent.IntensitySlope"/>
[DataField, AutoNetworkedField]
public float IntensitySlope = 1;
/// <inheritdoc cref="ExplosiveComponent.TotalIntensity"/>
[DataField, AutoNetworkedField]
public float TotalIntensity = 10;
/// <inheritdoc cref="ExplosiveComponent.TileBreakScale"/>
[DataField, AutoNetworkedField]
public float TileBreakScale = 1f;
/// <inheritdoc cref="ExplosiveComponent.MaxTileBreak"/>
[DataField, AutoNetworkedField]
public int MaxTileBreak = int.MaxValue;
/// <inheritdoc cref="ExplosiveComponent.CanCreateVacuum"/>
[DataField, AutoNetworkedField]
public bool CanCreateVacuum = true;
}

View File

@@ -0,0 +1,25 @@
using Content.Shared.Weather;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Trigger.Components.Effects;
/// <summary>
/// Changes the current weather when triggered.
/// If TargetUser is true then it will change the weather at the user's map instead of the entitys map.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class WeatherOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// Weather type. Null to clear the weather.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<WeatherPrototype>? Weather;
/// <summary>
/// How long the weather should last. Null for forever.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? Duration;
}

View File

@@ -0,0 +1,11 @@
using Content.Shared.GameTicking;
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Triggers;
/// <summary>
/// A trigger which occurs on <see cref="PlayerSpawnCompleteEvent"/>.
/// </summary>
/// <remarks>This does not work with <see cref="TraitSystem"/>, as it would add this component while the event is getting raised.</remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class TriggerOnPlayerSpawnCompleteComponent : BaseTriggerOnXComponent;

View File

@@ -13,7 +13,7 @@ public sealed class DnaScrambleOnTriggerSystem : EntitySystem
{
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly SharedIdentitySystem _identity = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly SharedForensicsSystem _forensics = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly INetManager _net = default!;

View File

@@ -11,10 +11,11 @@ public sealed class ExplodeOnTriggerSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<ExplodeOnTriggerComponent, TriggerEvent>(OnTrigger);
SubscribeLocalEvent<ExplodeOnTriggerComponent, TriggerEvent>(OnExplodeTrigger);
SubscribeLocalEvent<ExplosionOnTriggerComponent, TriggerEvent>(OnQueueExplosionTrigger);
}
private void OnTrigger(Entity<ExplodeOnTriggerComponent> ent, ref TriggerEvent args)
private void OnExplodeTrigger(Entity<ExplodeOnTriggerComponent> ent, ref TriggerEvent args)
{
if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key))
return;
@@ -27,4 +28,27 @@ public sealed class ExplodeOnTriggerSystem : EntitySystem
_explosion.TriggerExplosive(target.Value, user: args.User);
args.Handled = true;
}
private void OnQueueExplosionTrigger(Entity<ExplosionOnTriggerComponent> ent, ref TriggerEvent args)
{
var (uid, comp) = ent;
if (args.Key != null && !comp.KeysIn.Contains(args.Key))
return;
var target = comp.TargetUser ? args.User : uid;
if (target == null)
return;
_explosion.QueueExplosion(target.Value,
comp.ExplosionType,
comp.TotalIntensity,
comp.IntensitySlope,
comp.MaxTileIntensity,
comp.TileBreakScale,
comp.MaxTileBreak,
comp.CanCreateVacuum,
args.User);
args.Handled = true;
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Trigger.Components.Effects;
using Content.Shared.GameTicking;
using Content.Shared.Trigger.Components.Effects;
using Content.Shared.Trigger.Components.Triggers;
using Robust.Shared.Prototypes;
@@ -9,6 +10,7 @@ public sealed partial class TriggerSystem
private void InitializeSpawn()
{
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnInit);
SubscribeLocalEvent<TriggerOnPlayerSpawnCompleteComponent, PlayerSpawnCompleteEvent>(OnPlayerSpawn);
SubscribeLocalEvent<SpawnOnTriggerComponent, TriggerEvent>(HandleSpawnOnTrigger);
SubscribeLocalEvent<SpawnEntityTableOnTriggerComponent, TriggerEvent>(HandleSpawnTableOnTrigger);
@@ -20,6 +22,11 @@ public sealed partial class TriggerSystem
Trigger(ent.Owner, null, ent.Comp.KeyOut);
}
private void OnPlayerSpawn(Entity<TriggerOnPlayerSpawnCompleteComponent> ent, ref PlayerSpawnCompleteEvent args)
{
Trigger(ent.Owner, null, ent.Comp.KeyOut);
}
private void HandleSpawnOnTrigger(Entity<SpawnOnTriggerComponent> ent, ref TriggerEvent args)
{
if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key))

View File

@@ -0,0 +1,44 @@
using Content.Shared.Trigger.Components.Effects;
using Content.Shared.Weather;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Trigger.Systems;
public sealed class WeatherTriggerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedWeatherSystem _weather = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WeatherOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(Entity<WeatherOnTriggerComponent> ent, ref TriggerEvent args)
{
if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key))
return;
var target = ent.Comp.TargetUser ? args.User : ent.Owner;
if (target == null)
return;
var xform = Transform(target.Value);
if (ent.Comp.Weather == null) //Clear weather if nothing is set
{
_weather.SetWeather(xform.MapID, null, null);
return;
}
var endTime = ent.Comp.Duration == null ? null : ent.Comp.Duration + _timing.CurTime;
if (_prototypeManager.Resolve(ent.Comp.Weather, out var weatherPrototype))
_weather.SetWeather(xform.MapID, weatherPrototype, endTime);
}
}

View File

@@ -1,48 +1,4 @@
Entries:
- author: qwerltaz
changes:
- message: Snipping the panic wire in an air alarm now forces panic mode until the
wire is mended.
type: Tweak
id: 8485
time: '2025-05-14T20:39:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/36439
- author: IProduceWidgets
changes:
- message: more wizard name variety
type: Add
id: 8486
time: '2025-05-14T21:06:56.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/36437
- author: archee1
changes:
- message: Bike horns of all varieties have had their textures updated slightly.
type: Tweak
id: 8487
time: '2025-05-14T22:05:45.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/37413
- author: slarticodefast
changes:
- message: Fixed paradox clones not copying the mute, snoring and frontal lisp traits.
type: Fix
id: 8488
time: '2025-05-15T01:45:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/37467
- author: kosticia
changes:
- message: Hamsters, mice, butterflies, mothroaches, bees, bats, snails, rats, ticks
and cockroaches now don't leave organs on gibbing.
type: Fix
id: 8489
time: '2025-05-15T03:45:50.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/37080
- author: EmoGarbage404
changes:
- message: Goliath tentacles no longer miss if you stand still.
type: Fix
id: 8490
time: '2025-05-15T03:55:05.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/37168
- author: K-Dynamic
changes:
- message: Industrial advanced welders may be found in welding supplies lockers.
@@ -3954,3 +3910,48 @@
id: 8995
time: '2025-09-22T06:40:14.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/40491
- author: hoshizora-sayo
changes:
- message: Fixed fire helmets alone giving you full temperature protection
type: Fix
id: 8996
time: '2025-09-23T17:02:50.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/40481
- author: aada
changes:
- message: Circuit tiles and faux tiles have been moved to the cutter machine.
type: Tweak
id: 8997
time: '2025-09-24T00:12:45.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/37982
- author: Kittygyat
changes:
- message: Added 4 diagnostic huds to the engi-vend
type: Add
id: 8998
time: '2025-09-24T04:19:38.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/40461
- author: RedBookcase
changes:
- message: Rechargers can now charge power cells again.
type: Tweak
id: 8999
time: '2025-09-24T20:33:45.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/38138
- author: Nox38, BurgerMoth
changes:
- message: Added descriptions to .20 ammo boxes, magazines, and cartridges.
type: Add
- message: Changed the descriptions of the Lecter, Estoc DMR, and M90GL.
type: Tweak
id: 9000
time: '2025-09-24T21:48:34.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/36496
- author: Keer-Sar
changes:
- message: Added two new markings for lizard snouts, "Lizard Visage (Round)" & "Lizard
Visage (Sharp)."
type: Add
id: 9001
time: '2025-09-24T23:13:14.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35294

View File

@@ -1,8 +1,8 @@
comp-kitchen-spike-begin-hook-self = You begin dragging yourself onto { THE($hook) }!
comp-kitchen-spike-begin-hook-self = You begin dragging yourself onto { THE($hook) }!
comp-kitchen-spike-begin-hook-self-other = { CAPITALIZE(THE($victim)) } begins dragging { REFLEXIVE($victim) } onto { THE($hook) }!
comp-kitchen-spike-begin-hook-other-self = You begin dragging { CAPITALIZE(THE($victim)) } onto { THE($hook) }!
comp-kitchen-spike-begin-hook-other = { CAPITALIZE(THE($user)) } begins dragging { CAPITALIZE(THE($victim)) } onto { THE($hook) }!a
comp-kitchen-spike-begin-hook-other = { CAPITALIZE(THE($user)) } begins dragging { CAPITALIZE(THE($victim)) } onto { THE($hook) }!
comp-kitchen-spike-hook-self = You threw yourself on { THE($hook) }!
comp-kitchen-spike-hook-self-other = { CAPITALIZE(THE($victim)) } threw { REFLEXIVE($victim) } on { THE($hook) }!

View File

@@ -4,7 +4,6 @@ lathe-category-clothing = Clothing
lathe-category-lights = Lights
lathe-category-machines = Machines
lathe-category-parts = Parts
lathe-category-tiles = Tiles
lathe-category-tools = Tools
lathe-category-weapons = Weapons
@@ -24,13 +23,16 @@ lathe-category-service = Service
lathe-category-supply = Supply
# Cutter
lathe-category-concrete = Concrete
lathe-category-dark = Dark
lathe-category-maints = Maints
lathe-category-steel = Steel
lathe-category-white = White
lathe-category-wood = Wood
lathe-category-tiles = Tiles
lathe-category-circuit-tile = Circuit
lathe-category-concrete-tile = Concrete
lathe-category-dark-tile = Dark
lathe-category-faux-tile = Faux
lathe-category-maints-tile = Maints
lathe-category-marble = Marble
lathe-category-steel-tile = Steel
lathe-category-white-tile = White
lathe-category-wood-tile = Wood
# Science
lathe-category-mechs = Mechs

View File

@@ -117,3 +117,9 @@ marking-LizardChestFin = Lizard Fin
marking-LizardSnoutSplotch = Lizard Snout (Splotch)
marking-LizardSnoutSplotch-snout_splotch_primary = Muzzle
marking-LizardSnoutSplotch-snout_splotch_secondary = Snoot
marking-LizardSnoutVisageSharp = Lizard Visage (Sharp)
marking-LizardSnoutVisageSharp-visage_sharp = Lizard Visage (Sharp)
marking-LizardSnoutVisageRound = Lizard Visage (Round)
marking-LizardSnoutVisageRound-visage_round = Lizard Visage (Round)

View File

@@ -2,6 +2,7 @@
id: EngiVendInventory
startingInventory:
ClothingEyesGlassesMeson: 4
ClothingEyesHudDiagnostic: 4
ClothingHeadHatWelding: 6
CrowbarYellow: 8
Multitool: 4

View File

@@ -273,8 +273,8 @@
quickEquip: true
- type: IngestionBlocker
- type: TemperatureProtection
heatingCoefficient: 0.01
coolingCoefficient: 0.2
heatingCoefficient: 0.5
coolingCoefficient: 0.8
- type: FireProtection
reduction: 0.15 # not fully sealed so less protection
- type: IdentityBlocker
@@ -301,8 +301,8 @@
quickEquip: true
- type: IngestionBlocker
- type: TemperatureProtection
heatingCoefficient: 0.01
coolingCoefficient: 0.2
heatingCoefficient: 0.3
coolingCoefficient: 0.8
- type: FireProtection
reduction: 0.2
- type: PressureProtection

View File

@@ -206,7 +206,7 @@
delay: 3
transferAmount: 1
solution: puddle
utensil: None
utensil: Spoon
- type: ExaminableSolution
solution: puddle
locVolume: "examinable-solution-on-examine-volume-puddle"

View File

@@ -213,6 +213,24 @@
- sprite: Mobs/Customization/reptilian_parts.rsi
state: snout_splotch_secondary
- type: marking
id: LizardSnoutVisageRound
bodyPart: Snout
markingCategory: Snout
speciesRestriction: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: visage_round
- type: marking
id: LizardSnoutVisageSharp
bodyPart: Snout
markingCategory: Snout
speciesRestriction: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: visage_sharp
- type: marking
id: LizardChestTiger
bodyPart: Chest

View File

@@ -102,6 +102,7 @@
- CannotSuicide
- FootstepSound
- type: NoSlip
- type: Rummager
- type: RatKing
hungerPerArmyUse: 25
hungerPerDomainUse: 50
@@ -308,12 +309,16 @@
sprite: Mobs/Effects/onfire.rsi
normalState: Mouse_burning
- type: weightedRandomEntity
- type: entityTable
id: RatKingLoot
weights:
RandomSpawner100: 66 #garbage
FoodCheese: 28 #food
IngotGold1: 5 #loot
table: !type:GroupSelector
children:
- id: RandomSpawner100
weight: 66
- id: FoodCheese
weight: 28
- id: IngotGold1
weight: 5
- type: entity
parent: BaseAction

View File

@@ -1,4 +1,4 @@
- type: entity
- type: entity
abstract: true
save: false
parent: [BaseMobSpeciesOrganic]
@@ -172,9 +172,7 @@
- type: entity
parent: [BaseSpeciesDummy]
id: MobVulpkaninDummy
name: Vulpkanin Dummy
categories: [ HideSpawnMenu ]
description: A dummy Vulpkanin meant to be used in character setup.
components:
- type: HumanoidAppearance
species: Vulpkanin

View File

@@ -9,7 +9,6 @@
- type: FlavorProfile
flavors:
- sweet
- type: Food
- type: Sprite
sprite: Objects/Consumable/Food/Baked/cake.rsi
- type: SolutionContainerManager

View File

@@ -6,7 +6,6 @@
abstract: true
description: Goes great with robust coffee.
components:
- type: Food
- type: Tag
tags:
- Donut

View File

@@ -10,7 +10,7 @@
tags:
- Egg
- Meat
- type: Food
- type: Edible
trash:
- Eggshells
- type: Sprite

View File

@@ -1,4 +1,4 @@
# When adding new food also add to random spawner located in Resources\Prototypes\Entities\Markers\Spawners\Random\Food_Drinks\food_snack.yml
# When adding new food also add to random spawner located in Resources\Prototypes\Entities\Markers\Spawners\Random\Food_Drinks\food_snack.yml
# Base
- type: entity
@@ -63,7 +63,7 @@
- type: entity
parent: FoodFrozenBase
id: FoodFrozenCornuto
name: strawberry ice-cream sandwich
name: cornuto
description: A Neapolitan vanilla and chocolate ice-cream cone. It menaces with a sprinkling of caramelized nuts.
components:
- type: Item

View File

@@ -15,10 +15,16 @@
tags:
- Bot # for funny bot moments
blacklist:
components:
components: # TODO: This blacklist should be cut down by a lot once Chameleon Projector code is less buggy. See #40510
- ChameleonDisguise # no becoming kleiner
- Door # no faking doors
- MindContainer # no
- Pda # PDAs currently make you invisible /!\
- SubFloorHide # no hiding under the floor
tags:
- Catwalk # Catwalks make you invisible
- Wall # Walls make you invisible
- Window # Windows make you invisible
disguiseProto: ChameleonDisguise
- type: StaticPrice
price: 5000

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