Merge remote-tracking branch 'refs/remotes/wizards/master'

# Conflicts:
#	Content.Shared/StatusEffect/StatusEffectsSystem.cs
#	Resources/Audio/Effects/attributions.yml
#	Resources/Locale/en-US/_strings/anomaly/inner_anomaly.ftl
#	Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
#	Resources/Prototypes/Entities/Mobs/Species/base.yml
#	Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml
#	Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml
This commit is contained in:
VigersRay 2024-09-19 16:09:00 +03:00
commit 47cdabdaed
162 changed files with 3518 additions and 459 deletions

View file

@ -40,21 +40,10 @@ jobs:
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- name: Upload build artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: build
path: release/*.zip
compression-level: 0
retention-days: 0
- name: Publish version
run: Tools/publish_github_artifact.py
run: Tools/publish_multi_request.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
ARTIFACT_ID: ${{ steps.artifact-upload-step.outputs.artifact-id }}
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
- name: Publish changelog (Discord)
@ -67,8 +56,3 @@ jobs:
run: Tools/actions_changelog_rss.py
env:
CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}
- uses: geekyeggo/delete-artifact@v5
if: always()
with:
name: build

View file

@ -101,7 +101,7 @@ namespace Content.Client.Actions.UI
{
var duration = Cooldown.Value.End - Cooldown.Value.Start;
if (!FormattedMessage.TryFromMarkup($"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 1} sec remaining)[/color]", out var markup))
if (!FormattedMessage.TryFromMarkup(Loc.GetString("ui-actionslot-duration", ("duration", (int)duration.TotalSeconds), ("timeLeft", (int)timeLeft.TotalSeconds + 1)), out var markup))
return;
_cooldownLabel.SetMessage(markup);

View file

@ -1,14 +1,13 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration.UI.SetOutfit
@ -65,9 +64,18 @@ namespace Content.Client.Administration.UI.SetOutfit
PopulateByFilter(SearchBar.Text);
}
private IEnumerable<StartingGearPrototype> GetPrototypes()
{
// Filter out any StartingGearPrototypes that belong to loadouts
var loadouts = _prototypeManager.EnumeratePrototypes<LoadoutPrototype>();
var loadoutGears = loadouts.Select(l => l.StartingGear);
return _prototypeManager.EnumeratePrototypes<StartingGearPrototype>()
.Where(p => !loadoutGears.Contains(p.ID));
}
private void PopulateList()
{
foreach (var gear in _prototypeManager.EnumeratePrototypes<StartingGearPrototype>())
foreach (var gear in GetPrototypes())
{
OutfitList.Add(GetItem(gear, OutfitList));
}
@ -76,7 +84,7 @@ namespace Content.Client.Administration.UI.SetOutfit
private void PopulateByFilter(string filter)
{
OutfitList.Clear();
foreach (var gear in _prototypeManager.EnumeratePrototypes<StartingGearPrototype>())
foreach (var gear in GetPrototypes())
{
if (!string.IsNullOrEmpty(filter) &&
gear.ID.ToLowerInvariant().Contains(filter.Trim().ToLowerInvariant()))

View file

@ -20,8 +20,9 @@ public sealed class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent<AnomalyComponent, AppearanceChangeEvent>(OnAppearanceChanged);
SubscribeLocalEvent<AnomalyComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<AnomalyComponent, AnimationCompletedEvent>(OnAnimationComplete);
}
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
}
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
{
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);
@ -75,4 +76,13 @@ public sealed class AnomalySystem : SharedAnomalySystem
}
}
}
private void OnShutdown(Entity<AnomalySupercriticalComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
sprite.Scale = Vector2.One;
sprite.Color = sprite.Color.WithAlpha(1f);
}
}

View file

@ -0,0 +1,50 @@
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
using Content.Shared.Body.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Anomaly.Effects;
public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
public override void Initialize()
{
SubscribeLocalEvent<InnerBodyAnomalyComponent, AfterAutoHandleStateEvent>(OnAfterHandleState);
SubscribeLocalEvent<InnerBodyAnomalyComponent, ComponentShutdown>(OnCompShutdown);
}
private void OnAfterHandleState(Entity<InnerBodyAnomalyComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
if (ent.Comp.FallbackSprite is null)
return;
if (!sprite.LayerMapTryGet(ent.Comp.LayerMap, out var index))
index = sprite.LayerMapReserveBlank(ent.Comp.LayerMap);
if (TryComp<BodyComponent>(ent, out var body) &&
body.Prototype is not null &&
ent.Comp.SpeciesSprites.TryGetValue(body.Prototype.Value, out var speciesSprite))
{
sprite.LayerSetSprite(index, speciesSprite);
}
else
{
sprite.LayerSetSprite(index, ent.Comp.FallbackSprite);
}
sprite.LayerSetVisible(index, true);
sprite.LayerSetShader(index, "unshaded");
}
private void OnCompShutdown(Entity<InnerBodyAnomalyComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
var index = sprite.LayerMapGet(ent.Comp.LayerMap);
sprite.LayerSetVisible(index, false);
}
}

View file

@ -306,6 +306,9 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
.WithMaxDistance(comp.Range);
var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
if (stream == null)
continue;
_playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key);
playingCount++;

View file

@ -67,7 +67,7 @@ public sealed class ClientGlobalSoundSystem : SharedGlobalSoundSystem
if(!_adminAudioEnabled) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
_adminAudio.Add(stream.Value.Entity);
_adminAudio.Add(stream?.Entity);
}
private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
@ -76,7 +76,7 @@ public sealed class ClientGlobalSoundSystem : SharedGlobalSoundSystem
if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
_eventAudio.Add(soundEvent.Type, stream.Value.Entity);
_eventAudio.Add(soundEvent.Type, stream?.Entity);
}
private void PlayGameSound(GameGlobalSoundEvent soundEvent)

View file

@ -213,9 +213,9 @@ public sealed partial class ContentAudioSystem
false,
AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider));
_ambientMusicStream = strim.Value.Entity;
_ambientMusicStream = strim?.Entity;
if (_musicProto.FadeIn)
if (_musicProto.FadeIn && strim != null)
{
FadeIn(_ambientMusicStream, strim.Value.Component, AmbientMusicFadeTime);
}

View file

@ -185,7 +185,7 @@ public sealed partial class ContentAudioSystem
false,
_lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
);
if (playResult.Value.Entity == default)
if (playResult == null)
{
_sawmill.Warning(
$"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!",

View file

@ -0,0 +1,30 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
public sealed partial class WantedListUi : UIFragment
{
private WantedListUiFragment? _fragment;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new WantedListUiFragment();
}
public override void UpdateState(BoundUserInterfaceState state)
{
switch (state)
{
case WantedListUiState cast:
_fragment?.UpdateState(cast.Records);
break;
}
}
}

View file

@ -0,0 +1,240 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.Security;
using Content.Shared.StatusIcon;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class WantedListUiFragment : BoxContainer
{
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SpriteSystem _spriteSystem;
private string? _selectedTargetName;
private List<WantedRecord> _wantedRecords = new();
public WantedListUiFragment()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
SearchBar.OnTextChanged += OnSearchBarTextChanged;
}
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
{
var found = !String.IsNullOrWhiteSpace(args.Text)
? _wantedRecords.FindAll(r =>
r.TargetInfo.Name.Contains(args.Text) ||
r.Status.ToString().Contains(args.Text, StringComparison.OrdinalIgnoreCase))
: _wantedRecords;
UpdateState(found, false);
}
public void UpdateState(List<WantedRecord> records, bool refresh = true)
{
if (records.Count == 0)
{
NoRecords.Visible = true;
RecordsList.Visible = false;
RecordUnselected.Visible = false;
PersonContainer.Visible = false;
_selectedTargetName = null;
if (refresh)
_wantedRecords.Clear();
RecordsList.PopulateList(new List<ListData>());
return;
}
NoRecords.Visible = false;
RecordsList.Visible = true;
RecordUnselected.Visible = true;
PersonContainer.Visible = false;
var dataList = records.Select(r => new StatusListData(r)).ToList();
RecordsList.GenerateItem = GenerateItem;
RecordsList.ItemPressed = OnItemSelected;
RecordsList.PopulateList(dataList);
if (refresh)
_wantedRecords = records;
}
private void OnItemSelected(BaseButton.ButtonEventArgs args, ListData data)
{
if (data is not StatusListData(var record))
return;
FormattedMessage GetLoc(string fluentId, params (string,object)[] args)
{
var msg = new FormattedMessage();
var fluent = Loc.GetString(fluentId, args);
msg.AddMarkupPermissive(fluent);
return msg;
}
// Set personal info
PersonName.Text = record.TargetInfo.Name;
TargetAge.SetMessage(GetLoc(
"wanted-list-age-label",
("age", record.TargetInfo.Age)
));
TargetJob.SetMessage(GetLoc(
"wanted-list-job-label",
("job", record.TargetInfo.JobTitle.ToLower())
));
TargetSpecies.SetMessage(GetLoc(
"wanted-list-species-label",
("species", record.TargetInfo.Species.ToLower())
));
TargetGender.SetMessage(GetLoc(
"wanted-list-gender-label",
("gender", record.TargetInfo.Gender)
));
// Set reason
WantedReason.SetMessage(GetLoc(
"wanted-list-reason-label",
("reason", record.Reason ?? Loc.GetString("wanted-list-unknown-reason-label"))
));
// Set status
PersonState.SetMessage(GetLoc(
"wanted-list-status-label",
("status", record.Status.ToString().ToLower())
));
// Set initiator
InitiatorName.SetMessage(GetLoc(
"wanted-list-initiator-label",
("initiator", record.Initiator ?? Loc.GetString("wanted-list-unknown-initiator-label"))
));
// History table
// Clear table if it exists
HistoryTable.RemoveAllChildren();
HistoryTable.AddChild(new Label()
{
Text = Loc.GetString("wanted-list-history-table-time-col"),
StyleClasses = { "LabelSmall" },
HorizontalAlignment = HAlignment.Center,
});
HistoryTable.AddChild(new Label()
{
Text = Loc.GetString("wanted-list-history-table-reason-col"),
StyleClasses = { "LabelSmall" },
HorizontalAlignment = HAlignment.Center,
HorizontalExpand = true,
});
HistoryTable.AddChild(new Label()
{
Text = Loc.GetString("wanted-list-history-table-initiator-col"),
StyleClasses = { "LabelSmall" },
HorizontalAlignment = HAlignment.Center,
});
if (record.History.Count > 0)
{
HistoryTable.Visible = true;
foreach (var history in record.History.OrderByDescending(h => h.AddTime))
{
HistoryTable.AddChild(new Label()
{
Text = $"{history.AddTime.Hours:00}:{history.AddTime.Minutes:00}:{history.AddTime.Seconds:00}",
StyleClasses = { "LabelSmall" },
VerticalAlignment = VAlignment.Top,
});
HistoryTable.AddChild(new RichTextLabel()
{
Text = $"[color=white]{history.Crime}[/color]",
HorizontalExpand = true,
VerticalAlignment = VAlignment.Top,
StyleClasses = { "LabelSubText" },
Margin = new(10f, 0f),
});
HistoryTable.AddChild(new RichTextLabel()
{
Text = $"[color=white]{history.InitiatorName}[/color]",
StyleClasses = { "LabelSubText" },
VerticalAlignment = VAlignment.Top,
});
}
}
RecordUnselected.Visible = false;
PersonContainer.Visible = true;
// Save selected item
_selectedTargetName = record.TargetInfo.Name;
}
private void GenerateItem(ListData data, ListContainerButton button)
{
if (data is not StatusListData(var record))
return;
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal, HorizontalExpand = true };
var label = new Label() { Text = record.TargetInfo.Name };
var rect = new TextureRect()
{
TextureScale = new(2.2f),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Margin = new(0f, 0f, 6f, 0f),
};
if (record.Status is not SecurityStatus.None)
{
var proto = "SecurityIcon" + record.Status switch
{
SecurityStatus.Detained => "Incarcerated",
_ => record.Status.ToString(),
};
if (_prototypeManager.TryIndex<SecurityIconPrototype>(proto, out var prototype))
{
rect.Texture = _spriteSystem.Frame0(prototype.Icon);
}
}
box.AddChild(rect);
box.AddChild(label);
button.AddChild(box);
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
if (record.TargetInfo.Name.Equals(_selectedTargetName))
{
button.Pressed = true;
// For some reason the event is not called when `Pressed` changed, call it manually.
OnItemSelected(
new(button, new(new(), BoundKeyState.Down, new(), false, new(), new())),
data);
}
}
}
internal record StatusListData(WantedRecord Record) : ListData;

View file

@ -0,0 +1,50 @@
<cartridges:WantedListUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'wanted-list-search-placeholder'}"/>
<BoxContainer Name="MainContainer" Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<Label Name="NoRecords" Text="{Loc 'wanted-list-label-no-records'}" Align="Center" VAlign="Center" HorizontalExpand="True" FontColorOverride="DarkGray"/>
<!-- Any attempts to set dimensions for ListContainer breaks the renderer, I have to roughly set sizes and margins in other controllers. -->
<controls:ListContainer
Name="RecordsList"
HorizontalAlignment="Left"
VerticalExpand="True"
Visible="False"
Toggle="True"
Group="True"
SetWidth="192" />
<Label Name="RecordUnselected"
Text="{Loc 'criminal-records-console-select-record-info'}"
Align="Center"
FontColorOverride="DarkGray"
Visible="False"
HorizontalExpand="True" />
<BoxContainer Name="PersonContainer" Orientation="Vertical" HorizontalExpand="True" SetWidth="334" Margin="5 0 77 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Name="PersonName" StyleClasses="LabelBig" />
<RichTextLabel Name="PersonState" HorizontalAlignment="Right" HorizontalExpand="True" />
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5"/>
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Name="DataContainer" Orientation="Vertical">
<RichTextLabel Name="TargetAge" />
<RichTextLabel Name="TargetJob" />
<RichTextLabel Name="TargetSpecies" />
<RichTextLabel Name="TargetGender" />
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5"/>
<RichTextLabel Name="InitiatorName" VerticalAlignment="Stretch"/>
<RichTextLabel Name="WantedReason" VerticalAlignment="Stretch"/>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
<controls:TableContainer Name="HistoryTable" Columns="3" Visible="False" HorizontalAlignment="Stretch" />
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</BoxContainer>
</cartridges:WantedListUiFragment>

View file

@ -2,7 +2,4 @@ using Content.Shared.Explosion.EntitySystems;
namespace Content.Client.Explosion.EntitySystems;
public sealed class ExplosionSystem : SharedExplosionSystem
{
}
public sealed class ExplosionSystem : SharedExplosionSystem;

View file

@ -16,6 +16,7 @@ namespace Content.Client.Flash
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedFlashSystem _flash;
private readonly StatusEffectsSystem _statusSys;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
@ -27,6 +28,7 @@ namespace Content.Client.Flash
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("FlashedEffect").InstanceUnique();
_flash = _entityManager.System<SharedFlashSystem>();
_statusSys = _entityManager.System<StatusEffectsSystem>();
}
@ -41,7 +43,7 @@ namespace Content.Client.Flash
|| !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status))
return;
if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time, status))
if (!_statusSys.TryGetTime(playerEntity.Value, _flash.FlashedKey, out var time, status))
return;
var curTime = _timing.CurTime;

View file

@ -78,6 +78,7 @@
ToolTip="Pick (Hold 5)" />
<mapping:MappingActionsButton Name="Delete" Access="Public"
ToolTip="Delete (Hold 6)" />
<mapping:MappingActionsButton Name="Flip" Access="Public" ToggleMode="False"/>
</BoxContainer>
</PanelContainer>
</LayoutContainer>

View file

@ -96,6 +96,22 @@ public sealed partial class MappingScreen : InGameScreen
Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png";
Delete.Texture.TexturePath = "/Textures/Interface/eraser.svg.png";
Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
Flip.OnPressed += args => FlipSides();
}
public void FlipSides()
{
ScreenContainer.Flip();
if (SpawnContainer.GetPositionInParent() == 0)
{
Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
}
else
{
Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png";
}
}
private void OnDecalColorPicked(Color color)

View file

@ -15,6 +15,9 @@
</PanelContainer>
</controls:StripeBack>
<LineEdit Name="SearchLineEdit" HorizontalExpand="True"
PlaceHolder="{Loc crew-monitor-filter-line-placeholder}" />
<ScrollContainer Name="SensorScroller"
VerticalExpand="True"
SetWidth="520"

View file

@ -156,6 +156,11 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
// Populate departments
foreach (var sensor in departmentSensors)
{
if (!string.IsNullOrEmpty(SearchLineEdit.Text)
&& !sensor.Name.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase)
&& !sensor.Job.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase))
continue;
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
// Add a button that will hold a username and other details

View file

@ -199,7 +199,9 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
var gridMatrix = _transform.GetWorldMatrix(gUid);
var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert);
var color = _shuttles.GetIFFColor(grid, self: false, iff);
var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
var coordColor = new Color(labelColor.R * 0.8f, labelColor.G * 0.8f, labelColor.B * 0.8f, 0.5f);
// Others default:
// Color.FromHex("#FFC000FF")
@ -213,25 +215,52 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty);
gridCentre.Y = -gridCentre.Y;
var distance = gridCentre.Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{distance:0.0}"));
var mapCoords = _transform.GetWorldPosition(gUid);
var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
// yes 1.0 scale is intended here.
var labelDimensions = handle.GetDimensions(Font, labelText, 1f);
var coordsDimensions = handle.GetDimensions(Font, coordsText, 0.7f);
// y-offset the control to always render below the grid (vertically)
var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
// The actual position in the UI. We offset the matrix position to render it off by half its width
// plus by the offset.
var uiPosition = ScalePosition(gridCentre)- new Vector2(labelDimensions.X / 2f, -yOffset);
// The actual position in the UI. We centre the label by offsetting the matrix position
// by half the label's width, plus the y-offset
var gridScaledPosition = ScalePosition(gridCentre) - new Vector2(0, -yOffset);
// Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport.
uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, PixelWidth - labelDimensions.X ),
Math.Clamp(uiPosition.Y, 0f, PixelHeight - labelDimensions.Y));
// Normalize the grid position if it exceeds the viewport bounds
// normalizing it instead of clamping it preserves the direction of the vector and prevents corner-hugging
var gridOffset = gridScaledPosition / PixelSize - new Vector2(0.5f, 0.5f);
var offsetMax = Math.Max(Math.Abs(gridOffset.X), Math.Abs(gridOffset.Y)) * 2f;
if (offsetMax > 1)
{
gridOffset = new Vector2(gridOffset.X / offsetMax, gridOffset.Y / offsetMax);
handle.DrawString(Font, uiPosition, labelText, color);
gridScaledPosition = (gridOffset + new Vector2(0.5f, 0.5f)) * PixelSize;
}
var labelUiPosition = gridScaledPosition - new Vector2(labelDimensions.X / 2f, 0);
var coordUiPosition = gridScaledPosition - new Vector2(coordsDimensions.X / 2f, -labelDimensions.Y);
// clamp the IFF label's UI position to within the viewport extents so it hugs the edges of the viewport
// coord label intentionally isn't clamped so we don't get ugly clutter at the edges
var controlExtents = PixelSize - new Vector2(labelDimensions.X, labelDimensions.Y); //new Vector2(labelDimensions.X * 2f, labelDimensions.Y);
labelUiPosition = Vector2.Clamp(labelUiPosition, Vector2.Zero, controlExtents);
// draw IFF label
handle.DrawString(Font, labelUiPosition, labelText, labelColor);
// only draw coords label if close enough
if (offsetMax < 1)
{
handle.DrawString(Font, coordUiPosition, coordsText, 0.7f, coordColor);
}
}
// Detailed view
@ -241,7 +270,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
if (!gridAABB.Intersects(viewAABB))
continue;
DrawGrid(handle, matty, grid, color);
DrawGrid(handle, matty, grid, labelColor);
DrawDocks(handle, gUid, matty);
}
}

View file

@ -695,6 +695,18 @@ namespace Content.Client.Stylesheets
new StyleProperty("font-color", Color.FromHex("#E5E5E581")),
}),
// ItemStatus for hands
Element()
.Class(StyleClassItemStatusNotHeld)
.Prop("font", notoSansItalic10)
.Prop("font-color", ItemStatusNotHeldColor)
.Prop(nameof(Control.Margin), new Thickness(4, 0, 0, 2)),
Element()
.Class(StyleClassItemStatus)
.Prop(nameof(RichTextLabel.LineHeightScale), 0.7f)
.Prop(nameof(Control.Margin), new Thickness(4, 0, 0, 2)),
// Context Menu window
Element<PanelContainer>().Class(ContextMenuPopup.StyleClassContextMenuPopup)
.Prop(PanelContainer.StylePropertyPanel, contextMenuBackground),

View file

@ -69,7 +69,7 @@ public sealed class ParacusiaSystem : SharedParacusiaSystem
var newCoords = Transform(uid).Coordinates.Offset(randomOffset);
// Play the sound
paracusia.Stream = _audio.PlayStatic(paracusia.Sounds, uid, newCoords).Value.Entity;
paracusia.Stream = _audio.PlayStatic(paracusia.Sounds, uid, newCoords)?.Entity;
}
}

View file

@ -47,10 +47,11 @@ public sealed class WeatherSystem : SharedWeatherSystem
if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null)
return;
weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true).Value.Entity;
weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true)?.Entity;
if (!TryComp(weather.Stream, out AudioComponent? comp))
return;
var stream = weather.Stream.Value;
var comp = Comp<AudioComponent>(stream);
var occlusion = 0f;
// Work out tiles nearby to determine volume.
@ -115,7 +116,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
var alpha = GetPercent(weather, uid);
alpha *= SharedAudioSystem.VolumeToGain(weatherProto.Sound.Params.Volume);
_audio.SetGain(stream, alpha, comp);
_audio.SetGain(weather.Stream, alpha, comp);
comp.Occlusion = occlusion;
}

View file

@ -584,17 +584,10 @@ namespace Content.Client.Wires.UI
private sealed class HelpPopup : Popup
{
private const string Text = "Click on the gold contacts with a multitool in hand to pulse their wire.\n" +
"Click on the wires with a pair of wirecutters in hand to cut/mend them.\n\n" +
"The lights at the top show the state of the machine, " +
"messing with wires will probably do stuff to them.\n" +
"Wire layouts are different each round, " +
"but consistent between machines of the same type.";
public HelpPopup()
{
var label = new RichTextLabel();
label.SetMessage(Text);
label.SetMessage(Loc.GetString("wires-menu-help-popup"));
AddChild(new PanelContainer
{
StyleClasses = {ExamineSystem.StyleClassEntityTooltip},

View file

@ -0,0 +1,108 @@
using Content.Shared.Buckle;
using Content.Shared.Buckle.Components;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Buckle;
public sealed partial class BuckleTest
{
[Test]
public async Task BuckleInteractUnbuckleOther()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IServerEntityManager>();
var buckleSystem = entMan.System<SharedBuckleSystem>();
EntityUid user = default;
EntityUid victim = default;
EntityUid chair = default;
BuckleComponent buckle = null;
StrapComponent strap = null;
await server.WaitAssertion(() =>
{
user = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace);
victim = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace);
chair = entMan.SpawnEntity(StrapDummyId, MapCoordinates.Nullspace);
Assert.That(entMan.TryGetComponent(victim, out buckle));
Assert.That(entMan.TryGetComponent(chair, out strap));
#pragma warning disable RA0002
buckle.Delay = TimeSpan.Zero;
#pragma warning restore RA0002
// Buckle victim to chair
Assert.That(buckleSystem.TryBuckle(victim, user, chair, buckle));
Assert.Multiple(() =>
{
Assert.That(buckle.BuckledTo, Is.EqualTo(chair), "Victim did not get buckled to the chair.");
Assert.That(buckle.Buckled, "Victim is not buckled.");
Assert.That(strap.BuckledEntities, Does.Contain(victim), "Chair does not have victim buckled to it.");
});
// InteractHand with chair to unbuckle victim
entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair));
Assert.Multiple(() =>
{
Assert.That(buckle.BuckledTo, Is.Null);
Assert.That(buckle.Buckled, Is.False);
Assert.That(strap.BuckledEntities, Does.Not.Contain(victim));
});
});
await pair.CleanReturnAsync();
}
[Test]
public async Task BuckleInteractBuckleUnbuckleSelf()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IServerEntityManager>();
EntityUid user = default;
EntityUid chair = default;
BuckleComponent buckle = null;
StrapComponent strap = null;
await server.WaitAssertion(() =>
{
user = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace);
chair = entMan.SpawnEntity(StrapDummyId, MapCoordinates.Nullspace);
Assert.That(entMan.TryGetComponent(user, out buckle));
Assert.That(entMan.TryGetComponent(chair, out strap));
#pragma warning disable RA0002
buckle.Delay = TimeSpan.Zero;
#pragma warning restore RA0002
// Buckle user to chair
entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair));
Assert.Multiple(() =>
{
Assert.That(buckle.BuckledTo, Is.EqualTo(chair), "Victim did not get buckled to the chair.");
Assert.That(buckle.Buckled, "Victim is not buckled.");
Assert.That(strap.BuckledEntities, Does.Contain(user), "Chair does not have victim buckled to it.");
});
// InteractHand with chair to unbuckle
entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair));
Assert.Multiple(() =>
{
Assert.That(buckle.BuckledTo, Is.Null);
Assert.That(buckle.Buckled, Is.False);
Assert.That(strap.BuckledEntities, Does.Not.Contain(user));
});
});
await pair.CleanReturnAsync();
}
}

View file

@ -15,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Buckle
[TestFixture]
[TestOf(typeof(BuckleComponent))]
[TestOf(typeof(StrapComponent))]
public sealed class BuckleTest
public sealed partial class BuckleTest
{
private const string BuckleDummyId = "BuckleDummy";
private const string StrapDummyId = "StrapDummy";

View file

@ -4,11 +4,15 @@ using Content.Server.Hands.Systems;
using Content.Server.Preferences.Managers;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Content.Shared.Clothing;
using Content.Shared.Hands.Components;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@ -82,9 +86,11 @@ namespace Content.Server.Administration.Commands
return false;
HumanoidCharacterProfile? profile = null;
ICommonSession? session = null;
// Check if we are setting the outfit of a player to respect the preferences
if (entityManager.TryGetComponent(target, out ActorComponent? actorComponent))
{
session = actorComponent.PlayerSession;
var userId = actorComponent.PlayerSession.UserId;
var preferencesManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = preferencesManager.GetPreferences(userId);
@ -128,6 +134,36 @@ namespace Content.Server.Administration.Commands
}
}
// See if this starting gear is associated with a job
var jobs = prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (job.StartingGear != gear)
continue;
var jobProtoId = LoadoutSystem.GetJobPrototype(job.ID);
if (!prototypeManager.TryIndex<RoleLoadoutPrototype>(jobProtoId, out var jobProto))
break;
// Don't require a player, so this works on Urists
profile ??= entityManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var comp)
? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species)
: new HumanoidCharacterProfile();
// Try to get the user's existing loadout for the role
profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout);
if (roleLoadout == null)
{
// If they don't have a loadout for the role, make a default one
roleLoadout = new RoleLoadout(jobProtoId);
roleLoadout.SetDefault(profile, session, prototypeManager);
}
// Equip the target with the job loadout
var stationSpawning = entityManager.System<SharedStationSpawningSystem>();
stationSpawning.EquipRoleLoadout(target, roleLoadout, jobProto);
}
return true;
}
}

View file

@ -1,4 +1,5 @@
using System.Linq;
using System.Numerics;
using Content.Server.Anomaly.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Power.Components;
@ -10,6 +11,7 @@ using Content.Shared.Popups;
using Content.Shared.Power;
using Robust.Shared.Audio.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Timing;
namespace Content.Server.Anomaly;
@ -25,6 +27,7 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@ -40,6 +43,34 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnAnomalyStabilityChanged);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<AnomalySynchronizerComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var sync, out var xform))
{
if (sync.ConnectedAnomaly is null)
continue;
if (_timing.CurTime < sync.NextCheckTime)
continue;
sync.NextCheckTime += sync.CheckFrequency;
if (Transform(sync.ConnectedAnomaly.Value).MapUid != Transform(uid).MapUid)
{
DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value);
continue;
}
if (!xform.Coordinates.TryDistance(EntityManager, Transform(sync.ConnectedAnomaly.Value).Coordinates, out var distance))
continue;
if (distance > sync.AttachRange)
DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value);
}
}
/// <summary>
/// If powered, try to attach a nearby anomaly.
/// </summary>
@ -73,10 +104,10 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
if (args.Powered)
return;
if (!TryComp<AnomalyComponent>(ent.Comp.ConnectedAnomaly, out var anomaly))
if (ent.Comp.ConnectedAnomaly is null)
return;
DisconnectFromAnomaly(ent, anomaly);
DisconnectFromAnomaly(ent, ent.Comp.ConnectedAnomaly.Value);
}
private void OnExamined(Entity<AnomalySynchronizerComponent> ent, ref ExaminedEvent args)
@ -125,13 +156,16 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
//TODO: disconnection from the anomaly should also be triggered if the anomaly is far away from the synchronizer.
//Currently only bluespace anomaly can do this, but for some reason it is the only one that cannot be connected to the synchronizer.
private void DisconnectFromAnomaly(Entity<AnomalySynchronizerComponent> ent, AnomalyComponent anomaly)
private void DisconnectFromAnomaly(Entity<AnomalySynchronizerComponent> ent, EntityUid other)
{
if (ent.Comp.ConnectedAnomaly == null)
return;
if (ent.Comp.PulseOnDisconnect)
_anomaly.DoAnomalyPulse(ent.Comp.ConnectedAnomaly.Value, anomaly);
if (TryComp<AnomalyComponent>(other, out var anomaly))
{
if (ent.Comp.PulseOnDisconnect)
_anomaly.DoAnomalyPulse(ent.Comp.ConnectedAnomaly.Value, anomaly);
}
_popup.PopupEntity(Loc.GetString("anomaly-sync-disconnected"), ent, PopupType.Large);
_audio.PlayPvs(ent.Comp.ConnectedSound, ent);

View file

@ -55,6 +55,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent<AnomalyComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
InitializeGenerator();
InitializeScanner();
InitializeVessel();
@ -86,7 +87,10 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
private void OnShutdown(Entity<AnomalyComponent> anomaly, ref ComponentShutdown args)
{
EndAnomaly(anomaly);
if (anomaly.Comp.CurrentBehavior is not null)
RemoveBehavior(anomaly, anomaly.Comp.CurrentBehavior.Value);
EndAnomaly(anomaly, spawnCore: false);
}
private void OnStartCollide(Entity<AnomalyComponent> anomaly, ref StartCollideEvent args)

View file

@ -7,7 +7,7 @@ namespace Content.Server.Anomaly.Components;
/// <summary>
/// a device that allows you to translate anomaly activity into multitool signals.
/// </summary>
[RegisterComponent, Access(typeof(AnomalySynchronizerSystem))]
[RegisterComponent, AutoGenerateComponentPause, Access(typeof(AnomalySynchronizerSystem))]
public sealed partial class AnomalySynchronizerComponent : Component
{
/// <summary>
@ -34,6 +34,15 @@ public sealed partial class AnomalySynchronizerComponent : Component
[DataField]
public float AttachRange = 0.4f;
/// <summary>
/// Periodicheski checks to see if the anomaly has moved to disconnect it.
/// </summary>
[DataField]
public TimeSpan CheckFrequency = TimeSpan.FromSeconds(1f);
[DataField, AutoPausedField]
public TimeSpan NextCheckTime = TimeSpan.Zero;
[DataField]
public ProtoId<SourcePortPrototype> DecayingPort = "Decaying";

View file

@ -0,0 +1,236 @@
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Chat.Managers;
using Content.Server.Jittering;
using Content.Server.Mind;
using Content.Server.Stunnable;
using Content.Shared.Actions;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
using Content.Shared.Body.Components;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Mobs;
using Content.Shared.Popups;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
namespace Content.Server.Anomaly.Effects;
public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly AnomalySystem _anomaly = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly JitteringSystem _jitter = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly StunSystem _stun = default!;
private readonly Color _messageColor = Color.FromSrgb(new Color(201, 22, 94));
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InnerBodyAnomalyInjectorComponent, StartCollideEvent>(OnStartCollideInjector);
SubscribeLocalEvent<InnerBodyAnomalyComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<InnerBodyAnomalyComponent, ComponentShutdown>(OnCompShutdown);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalyPulseEvent>(OnAnomalyPulse);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalyShutdownEvent>(OnAnomalyShutdown);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalySupercriticalEvent>(OnAnomalySupercritical);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalySeverityChangedEvent>(OnSeverityChanged);
SubscribeLocalEvent<InnerBodyAnomalyComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<AnomalyComponent, ActionAnomalyPulseEvent>(OnActionPulse);
}
private void OnActionPulse(Entity<AnomalyComponent> ent, ref ActionAnomalyPulseEvent args)
{
if (args.Handled)
return;
_anomaly.DoAnomalyPulse(ent, ent.Comp);
args.Handled = true;
}
private void OnStartCollideInjector(Entity<InnerBodyAnomalyInjectorComponent> ent, ref StartCollideEvent args)
{
if (ent.Comp.Whitelist is not null && !_whitelist.IsValid(ent.Comp.Whitelist, args.OtherEntity))
return;
if (TryComp<InnerBodyAnomalyComponent>(args.OtherEntity, out var innerAnom) && innerAnom.Injected)
return;
if (!_mind.TryGetMind(args.OtherEntity, out _, out var mindComponent))
return;
EntityManager.AddComponents(args.OtherEntity, ent.Comp.InjectionComponents);
QueueDel(ent);
}
private void OnMapInit(Entity<InnerBodyAnomalyComponent> ent, ref MapInitEvent args)
{
AddAnomalyToBody(ent);
}
private void AddAnomalyToBody(Entity<InnerBodyAnomalyComponent> ent)
{
if (!_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
return;
if (ent.Comp.Injected)
return;
ent.Comp.Injected = true;
EntityManager.AddComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
if (ent.Comp.StartSound is not null)
_audio.PlayPvs(ent.Comp.StartSound, ent);
if (ent.Comp.StartMessage is not null &&
_mind.TryGetMind(ent, out _, out var mindComponent) &&
mindComponent.Session != null)
{
var message = Loc.GetString(ent.Comp.StartMessage);
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chat.ChatMessageToOne(ChatChannel.Server,
message,
wrappedMessage,
default,
false,
mindComponent.Session.Channel,
_messageColor);
_popup.PopupEntity(message, ent, ent, PopupType.MediumCaution);
_adminLog.Add(LogType.Anomaly,LogImpact.Extreme,$"{ToPrettyString(ent)} became anomaly host.");
}
Dirty(ent);
}
private void OnAnomalyPulse(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyPulseEvent args)
{
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
}
private void OnAnomalySupercritical(Entity<InnerBodyAnomalyComponent> ent, ref AnomalySupercriticalEvent args)
{
if (!TryComp<BodyComponent>(ent, out var body))
return;
_body.GibBody(ent, true, body, splatModifier: 5f);
}
private void OnSeverityChanged(Entity<InnerBodyAnomalyComponent> ent, ref AnomalySeverityChangedEvent args)
{
if (!_mind.TryGetMind(ent, out _, out var mindComponent) || mindComponent.Session == null)
return;
var message = string.Empty;
if (args.Severity >= 0.5 && ent.Comp.LastSeverityInformed < 0.5)
{
ent.Comp.LastSeverityInformed = 0.5f;
message = Loc.GetString("inner-anomaly-severity-info-50");
}
if (args.Severity >= 0.75 && ent.Comp.LastSeverityInformed < 0.75)
{
ent.Comp.LastSeverityInformed = 0.75f;
message = Loc.GetString("inner-anomaly-severity-info-75");
}
if (args.Severity >= 0.9 && ent.Comp.LastSeverityInformed < 0.9)
{
ent.Comp.LastSeverityInformed = 0.9f;
message = Loc.GetString("inner-anomaly-severity-info-90");
}
if (args.Severity >= 1 && ent.Comp.LastSeverityInformed < 1)
{
ent.Comp.LastSeverityInformed = 1f;
message = Loc.GetString("inner-anomaly-severity-info-100");
}
if (message == string.Empty)
return;
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chat.ChatMessageToOne(ChatChannel.Server,
message,
wrappedMessage,
default,
false,
mindComponent.Session.Channel,
_messageColor);
_popup.PopupEntity(message, ent, ent, PopupType.MediumCaution);
}
private void OnMobStateChanged(Entity<InnerBodyAnomalyComponent> ent, ref MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead)
return;
_anomaly.ChangeAnomalyHealth(ent, -2); //Shutdown it
}
private void OnAnomalyShutdown(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyShutdownEvent args)
{
RemoveAnomalyFromBody(ent);
RemCompDeferred<InnerBodyAnomalyComponent>(ent);
}
private void OnCompShutdown(Entity<InnerBodyAnomalyComponent> ent, ref ComponentShutdown args)
{
RemoveAnomalyFromBody(ent);
}
private void RemoveAnomalyFromBody(Entity<InnerBodyAnomalyComponent> ent)
{
if (!ent.Comp.Injected)
return;
if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
EntityManager.RemoveComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
if (ent.Comp.EndMessage is not null &&
_mind.TryGetMind(ent, out _, out var mindComponent) &&
mindComponent.Session != null)
{
var message = Loc.GetString(ent.Comp.EndMessage);
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chat.ChatMessageToOne(ChatChannel.Server,
message,
wrappedMessage,
default,
false,
mindComponent.Session.Channel,
_messageColor);
_popup.PopupEntity(message, ent, ent, PopupType.MediumCaution);
_adminLog.Add(LogType.Anomaly, LogImpact.Medium,$"{ToPrettyString(ent)} is no longer a host for the anomaly.");
}
ent.Comp.Injected = false;
RemCompDeferred<AnomalyComponent>(ent);
}
}

View file

@ -37,7 +37,7 @@ public sealed class TechAnomalySystem : EntitySystem
if (_timing.CurTime < tech.NextTimer)
continue;
tech.NextTimer += TimeSpan.FromSeconds(tech.TimerFrequency * anom.Stability);
tech.NextTimer += TimeSpan.FromSeconds(tech.TimerFrequency);
_signal.InvokePort(uid, tech.TimerPort);
}

View file

@ -1,19 +1,21 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Reactions;
using Content.Server.Decals;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Reactions;
using Content.Shared.Audio;
using Content.Shared.Database;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
[Dependency] private readonly DecalSystem _decalSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private const int HotspotSoundCooldownCycles = 200;
private int _hotspotSoundCooldown = 0;
@ -56,7 +58,30 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.State = 3;
// TODO ATMOS: Burn tile here
var gridUid = ent.Owner;
var tilePos = tile.GridIndices;
// Get the existing decals on the tile
var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos);
// Count the burnt decals on the tile
var tileBurntDecals = 0;
foreach (var set in tileDecals)
{
if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1)
continue;
tileBurntDecals++;
if (tileBurntDecals > 4)
break;
}
// Add a random burned decal to the tile only if there are less than 4 of them
if (tileBurntDecals < 4)
_decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], new EntityCoordinates(gridUid, tilePos), out _, cleanable: true);
if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread)
{

View file

@ -4,6 +4,7 @@ using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Decals;
using Content.Shared.Doors.Components;
using Content.Shared.Maps;
using JetBrains.Annotations;
@ -12,7 +13,9 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Atmos.EntitySystems;
@ -36,6 +39,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly TileSystem _tile = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] public readonly PuddleSystem Puddle = default!;
private const float ExposedUpdateDelay = 1f;
@ -47,6 +51,8 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
private EntityQuery<FirelockComponent> _firelockQuery;
private HashSet<EntityUid> _entSet = new();
private string[] _burntDecals = [];
public override void Initialize()
{
base.Initialize();
@ -66,7 +72,9 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
_firelockQuery = GetEntityQuery<FirelockComponent>();
SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CacheDecals();
}
public override void Shutdown()
@ -81,6 +89,12 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
InvalidateTile(ev.NewTile.GridUid, ev.NewTile.GridIndices);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
{
if (ev.WasModified<DecalPrototype>())
CacheDecals();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@ -107,4 +121,9 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
_exposedTimer -= ExposedUpdateDelay;
}
private void CacheDecals()
{
_burntDecals = _prototypeManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
}
}

View file

@ -291,12 +291,13 @@ public partial class SeedData
CanScream = CanScream,
TurnIntoKudzu = TurnIntoKudzu,
SplatPrototype = SplatPrototype,
Mutations = Mutations,
Mutations = new List<RandomPlantMutation>(),
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.
Unique = true,
};
newSeed.Mutations.AddRange(Mutations);
return newSeed;
}

View file

@ -340,6 +340,9 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID)
return;
if (TryComp(args.Entity, out CartridgeComponent? cartridge))
cartridge.LoaderUid = uid;
RaiseLocalEvent(args.Entity, new CartridgeAddedEvent(uid));
base.OnItemInserted(uid, loader, args);
}
@ -360,6 +363,9 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
if (deactivate)
RaiseLocalEvent(args.Entity, new CartridgeDeactivatedEvent(uid));
if (TryComp(args.Entity, out CartridgeComponent? cartridge))
cartridge.LoaderUid = null;
RaiseLocalEvent(args.Entity, new CartridgeRemovedEvent(uid));
base.OnItemRemoved(uid, loader, args);

View file

@ -0,0 +1,8 @@
using Content.Shared.Security;
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class WantedListCartridgeComponent : Component
{
}

View file

@ -16,31 +16,31 @@ public sealed partial class SolutionRegenerationComponent : Component
/// <summary>
/// The name of the solution to add to.
/// </summary>
[DataField("solution", required: true), ViewVariables(VVAccess.ReadWrite)]
[DataField("solution", required: true)]
public string SolutionName = string.Empty;
/// <summary>
/// The solution to add reagents to.
/// </summary>
[DataField("solutionRef")]
public Entity<SolutionComponent>? Solution = null;
[DataField]
public Entity<SolutionComponent>? SolutionRef = null;
/// <summary>
/// The reagent(s) to be regenerated in the solution.
/// </summary>
[DataField("generated", required: true), ViewVariables(VVAccess.ReadWrite)]
[DataField(required: true)]
public Solution Generated = default!;
/// <summary>
/// How long it takes to regenerate once.
/// </summary>
[DataField("duration"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(1);
/// <summary>
/// The time when the next regeneration will occur.
/// </summary>
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextRegenTime = TimeSpan.FromSeconds(0);

View file

@ -24,7 +24,7 @@ public sealed class SolutionRegenerationSystem : EntitySystem
// timer ignores if its full, it's just a fixed cycle
regen.NextRegenTime = _timing.CurTime + regen.Duration;
if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.Solution, out var solution))
if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.SolutionRef, out var solution))
{
var amount = FixedPoint2.Min(solution.AvailableVolume, regen.Generated.Volume);
if (amount <= FixedPoint2.Zero)
@ -41,7 +41,7 @@ public sealed class SolutionRegenerationSystem : EntitySystem
generated = regen.Generated.Clone().SplitSolution(amount);
}
_solutionContainer.TryAddSolution(regen.Solution.Value, generated);
_solutionContainer.TryAddSolution(regen.SolutionRef.Value, generated);
}
}
}

View file

@ -1,44 +1,50 @@
using Content.Server.Stack;
using Content.Shared.Construction;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Construction.Completions
namespace Content.Server.Construction.Completions;
[UsedImplicitly]
[DataDefinition]
public sealed partial class GivePrototype : IGraphAction
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class GivePrototype : IGraphAction
{
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype { get; private set; } = string.Empty;
[DataField("amount")]
public int Amount { get; private set; } = 1;
[DataField]
public EntProtoId Prototype { get; private set; } = string.Empty;
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
[DataField]
public int Amount { get; private set; } = 1;
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
if (string.IsNullOrEmpty(Prototype))
return;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
{
if (string.IsNullOrEmpty(Prototype))
var stackSystem = entityManager.EntitySysManager.GetEntitySystem<StackSystem>();
var stacks = stackSystem.SpawnMultiple(Prototype, Amount, userUid ?? uid);
if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp))
return;
var coordinates = entityManager.GetComponent<TransformComponent>(userUid ?? uid).Coordinates;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
foreach (var item in stacks)
{
var stackEnt = entityManager.SpawnEntity(Prototype, coordinates);
var stack = entityManager.GetComponent<StackComponent>(stackEnt);
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(stackEnt, Amount, stack);
entityManager.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(userUid, stackEnt);
stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp);
}
else
}
else
{
var handsSystem = entityManager.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
var handsComp = userUid is not null ? entityManager.GetComponent<HandsComponent>(userUid.Value) : null;
for (var i = 0; i < Amount; i++)
{
for (var i = 0; i < Amount; i++)
{
var item = entityManager.SpawnEntity(Prototype, coordinates);
entityManager.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(userUid, item);
}
var item = entityManager.SpawnNextToOrDrop(Prototype, userUid ?? uid);
handsSystem.PickupOrDrop(userUid, item, handsComp: handsComp);
}
}
}

View file

@ -68,6 +68,13 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
}
}
private void GetOfficer(EntityUid uid, out string officer)
{
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, uid);
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
officer = tryGetIdentityShortInfoEvent.Title ?? Loc.GetString("criminal-records-console-unknown-officer");
}
private void OnChangeStatus(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordChangeStatus msg)
{
// prevent malf client violating wanted/reason nullability
@ -90,29 +97,22 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
return;
}
var oldStatus = record.Status;
var name = _records.RecordName(key.Value);
GetOfficer(mob.Value, out var officer);
// when arresting someone add it to history automatically
// fallback exists if the player was not set to wanted beforehand
if (msg.Status == SecurityStatus.Detained)
{
var oldReason = record.Reason ?? Loc.GetString("criminal-records-console-unspecified-reason");
var history = Loc.GetString("criminal-records-console-auto-history", ("reason", oldReason));
_criminalRecords.TryAddHistory(key.Value, history);
_criminalRecords.TryAddHistory(key.Value, history, officer);
}
var oldStatus = record.Status;
// will probably never fail given the checks above
_criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason);
var name = _records.RecordName(key.Value);
var officer = Loc.GetString("criminal-records-console-unknown-officer");
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value);
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
if (tryGetIdentityShortInfoEvent.Title != null)
{
officer = tryGetIdentityShortInfoEvent.Title;
}
_criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer);
(string, object)[] args;
if (reason != null)
@ -152,14 +152,16 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
private void OnAddHistory(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordAddHistory msg)
{
if (!CheckSelected(ent, msg.Actor, out _, out var key))
if (!CheckSelected(ent, msg.Actor, out var mob, out var key))
return;
var line = msg.Line.Trim();
if (line.Length < 1 || line.Length > ent.Comp.MaxStringLength)
return;
if (!_criminalRecords.TryAddHistory(key.Value, line))
GetOfficer(mob.Value, out var officer);
if (!_criminalRecords.TryAddHistory(key.Value, line, officer))
return;
// no radio message since its not crucial to officers patrolling

View file

@ -1,10 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.CartridgeLoader;
using Content.Server.CartridgeLoader.Cartridges;
using Content.Server.StationRecords.Systems;
using Content.Shared.CriminalRecords;
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.Security;
using Content.Shared.StationRecords;
using Content.Server.GameTicking;
using Content.Server.Station.Systems;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Server.CriminalRecords.Systems;
@ -20,12 +25,18 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
{
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly StationRecordsSystem _records = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly CartridgeLoaderSystem _cartridge = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(OnGeneralRecordCreated);
SubscribeLocalEvent<WantedListCartridgeComponent, CriminalRecordChangedEvent>(OnRecordChanged);
SubscribeLocalEvent<WantedListCartridgeComponent, CartridgeUiReadyEvent>(OnCartridgeUiReady);
SubscribeLocalEvent<WantedListCartridgeComponent, CriminalHistoryAddedEvent>(OnHistoryAdded);
SubscribeLocalEvent<WantedListCartridgeComponent, CriminalHistoryRemovedEvent>(OnHistoryRemoved);
}
private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
@ -39,14 +50,14 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
/// Reason should only be passed if status is Wanted, nullability isn't checked.
/// </summary>
/// <returns>True if the status is changed, false if not</returns>
public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason)
public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason, string? initiatorName = null)
{
// don't do anything if its the same status
if (!_records.TryGetRecord<CriminalRecord>(key, out var record)
|| status == record.Status)
return false;
OverwriteStatus(key, record, status, reason);
OverwriteStatus(key, record, status, reason, initiatorName);
return true;
}
@ -54,16 +65,24 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
/// <summary>
/// Sets the status without checking previous status or reason nullability.
/// </summary>
public void OverwriteStatus(StationRecordKey key, CriminalRecord record, SecurityStatus status, string? reason)
public void OverwriteStatus(StationRecordKey key, CriminalRecord record, SecurityStatus status, string? reason, string? initiatorName = null)
{
record.Status = status;
record.Reason = reason;
record.InitiatorName = initiatorName;
var name = _records.RecordName(key);
if (name != string.Empty)
UpdateCriminalIdentity(name, status);
_records.Synchronize(key);
var args = new CriminalRecordChangedEvent(record);
var query = EntityQueryEnumerator<WantedListCartridgeComponent>();
while (query.MoveNext(out var readerUid, out _))
{
RaiseLocalEvent(readerUid, ref args);
}
}
/// <summary>
@ -76,15 +95,23 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
return false;
record.History.Add(entry);
var args = new CriminalHistoryAddedEvent(entry);
var query = EntityQueryEnumerator<WantedListCartridgeComponent>();
while (query.MoveNext(out var readerUid, out _))
{
RaiseLocalEvent(readerUid, ref args);
}
return true;
}
/// <summary>
/// Creates and tries to add a history entry using the current time.
/// </summary>
public bool TryAddHistory(StationRecordKey key, string line)
public bool TryAddHistory(StationRecordKey key, string line, string? initiatorName = null)
{
var entry = new CrimeHistory(_ticker.RoundDuration(), line);
var entry = new CrimeHistory(_ticker.RoundDuration(), line, initiatorName);
return TryAddHistory(key, entry);
}
@ -100,7 +127,58 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
if (index >= record.History.Count)
return false;
var history = record.History[(int)index];
record.History.RemoveAt((int) index);
var args = new CriminalHistoryRemovedEvent(history);
var query = EntityQueryEnumerator<WantedListCartridgeComponent>();
while (query.MoveNext(out var readerUid, out _))
{
RaiseLocalEvent(readerUid, ref args);
}
return true;
}
private void OnRecordChanged(Entity<WantedListCartridgeComponent> ent, ref CriminalRecordChangedEvent args) =>
StateChanged(ent);
private void OnHistoryAdded(Entity<WantedListCartridgeComponent> ent, ref CriminalHistoryAddedEvent args) =>
StateChanged(ent);
private void OnHistoryRemoved(Entity<WantedListCartridgeComponent> ent, ref CriminalHistoryRemovedEvent args) =>
StateChanged(ent);
private void StateChanged(Entity<WantedListCartridgeComponent> ent)
{
if (Comp<CartridgeComponent>(ent).LoaderUid is not { } loaderUid)
return;
UpdateReaderUi(ent, loaderUid);
}
private void OnCartridgeUiReady(Entity<WantedListCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
UpdateReaderUi(ent, args.Loader);
}
private void UpdateReaderUi(Entity<WantedListCartridgeComponent> ent, EntityUid loaderUid)
{
if (_station.GetOwningStation(ent) is not { } station)
return;
var records = _records.GetRecordsOfType<CriminalRecord>(station)
.Where(cr => cr.Item2.Status is not SecurityStatus.None || cr.Item2.History.Count > 0)
.Select(cr =>
{
var (i, r) = cr;
var key = new StationRecordKey(i, station);
// Hopefully it will work smoothly.....
_records.TryGetRecord(key, out GeneralStationRecord? generalRecord);
return new WantedRecord(generalRecord!, r.Status, r.Reason, r.InitiatorName, r.History);
});
var state = new WantedListUiState(records.ToList());
_cartridge.UpdateCartridgeUiState(loaderUid, state);
}
}

View file

@ -1,4 +1,4 @@
using Content.Server.Explosion.Components;
using Content.Shared.Explosion.Components;
using JetBrains.Annotations;
namespace Content.Server.Destructible.Thresholds.Behaviors

View file

@ -4,6 +4,7 @@ using Content.Shared.Coordinates.Helpers;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Stacks;
using JetBrains.Annotations;
using Robust.Shared.Map.Components;
@ -15,6 +16,7 @@ namespace Content.Server.Engineering.EntitySystems
{
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
[Dependency] private readonly TurfSystem _turfSystem = default!;
public override void Initialize()
{
@ -36,7 +38,7 @@ namespace Content.Server.Engineering.EntitySystems
bool IsTileClear()
{
return tileRef.Tile.IsEmpty == false && !tileRef.IsBlockedTurf(true);
return tileRef.Tile.IsEmpty == false && !_turfSystem.IsTileBlocked(tileRef, CollisionGroup.MobMask);
}
if (!IsTileClear())

View file

@ -6,9 +6,10 @@ using Content.Shared.Explosion;
using Content.Shared.Explosion.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Shared.Map.Components;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem : SharedExplosionSystem
public sealed partial class ExplosionSystem
{
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!;

View file

@ -1,8 +1,8 @@
using Content.Shared.CCVar;
using Content.Shared.Explosion.EntitySystems;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem : SharedExplosionSystem
public sealed partial class ExplosionSystem
{
public int MaxIterations { get; private set; }
public int MaxArea { get; private set; }

View file

@ -12,7 +12,7 @@ namespace Content.Server.Explosion.EntitySystems;
// A good portion of it is focused around keeping track of what tile-indices on a grid correspond to tiles that border
// space. AFAIK no other system currently needs to track these "edge-tiles". If they do, this should probably be a
// property of the grid itself?
public sealed partial class ExplosionSystem : SharedExplosionSystem
public sealed partial class ExplosionSystem
{
/// <summary>
/// Set of tiles of each grid that are directly adjacent to space, along with the directions that face space.

View file

@ -22,9 +22,10 @@ using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem : SharedExplosionSystem
public sealed partial class ExplosionSystem
{
[Dependency] private readonly FlammableSystem _flammableSystem = default!;
@ -218,7 +219,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
// get the entities on a tile. Note that we cannot process them directly, or we get
// enumerator-changed-while-enumerating errors.
List<(EntityUid, TransformComponent)> list = new();
var state = (list, processed, _transformQuery);
var state = (list, processed, EntityManager.TransformQuery);
// get entities:
lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
@ -317,7 +318,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
var gridBox = Box2.FromDimensions(tile * DefaultTileSize, new Vector2(DefaultTileSize, DefaultTileSize));
var worldBox = spaceMatrix.TransformBox(gridBox);
var list = new List<(EntityUid, TransformComponent)>();
var state = (list, processed, invSpaceMatrix, lookup.Owner, _transformQuery, gridBox, _transformSystem);
var state = (list, processed, invSpaceMatrix, lookup.Owner, EntityManager.TransformQuery, gridBox, _transformSystem);
// get entities:
lookup.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);

View file

@ -7,13 +7,13 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using Content.Shared.Explosion.EntitySystems;
namespace Content.Server.Explosion.EntitySystems;
// This partial part of the explosion system has all of the functions used to create the actual explosion map.
// I.e, to get the sets of tiles & intensity values that describe an explosion.
public sealed partial class ExplosionSystem : SharedExplosionSystem
public sealed partial class ExplosionSystem
{
/// <summary>
/// This is the main explosion generating function.

View file

@ -5,10 +5,11 @@ using Content.Shared.Explosion.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
namespace Content.Server.Explosion.EntitySystems;
// This part of the system handled send visual / overlay data to clients.
public sealed partial class ExplosionSystem : SharedExplosionSystem
public sealed partial class ExplosionSystem
{
public void InitVisuals()
{

View file

@ -12,6 +12,8 @@ using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.EntitySystems;
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.Projectiles;
@ -53,7 +55,6 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
private EntityQuery<TransformComponent> _transformQuery;
private EntityQuery<FlammableComponent> _flammableQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<ProjectileComponent> _projectileQuery;
@ -103,7 +104,6 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
InitAirtightMap();
InitVisuals();
_transformQuery = GetEntityQuery<TransformComponent>();
_flammableQuery = GetEntityQuery<FlammableComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_projectileQuery = GetEntityQuery<ProjectileComponent>();
@ -141,15 +141,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
args.DamageCoefficient *= modifier;
}
/// <summary>
/// Given an entity with an explosive component, spawn the appropriate explosion.
/// </summary>
/// <remarks>
/// Also accepts radius or intensity arguments. This is useful for explosives where the intensity is not
/// specified in the yaml / by the component, but determined dynamically (e.g., by the quantity of a
/// solution in a reaction).
/// </remarks>
public void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null)
/// <inheritdoc/>
public override void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null)
{
// log missing: false, because some entities (e.g. liquid tanks) attempt to trigger explosions when damaged,
// but may not actually be explosive.

View file

@ -202,6 +202,7 @@ namespace Content.Server.Explosion.EntitySystems
args.Handled = true;
}
private void HandleRattleTrigger(EntityUid uid, RattleComponent component, TriggerEvent args)
{
if (!TryComp<SubdermalImplantComponent>(uid, out var implanted))
@ -230,7 +231,7 @@ namespace Content.Server.Explosion.EntitySystems
private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, ref StartCollideEvent args)
{
if (args.OurFixtureId == component.FixtureID && (!component.IgnoreOtherNonHard || args.OtherFixture.Hard))
Trigger(uid);
Trigger(uid, args.OtherEntity);
}
private void OnSpawnTriggered(EntityUid uid, TriggerOnSpawnComponent component, MapInitEvent args)

View file

@ -152,7 +152,7 @@ namespace Content.Server.Flash
}
}
public void FlashArea(Entity<FlashComponent?> source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null)
public override void FlashArea(Entity<FlashComponent?> source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null)
{
var transform = Transform(source);
var mapPosition = _transform.GetMapCoordinates(transform);

View file

@ -112,7 +112,7 @@ namespace Content.Server.Kitchen.EntitySystems
SetAppearance(ent.Owner, MicrowaveVisualState.Cooking, microwaveComponent);
microwaveComponent.PlayingStream =
_audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5)).Value.Entity;
_audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5))?.Entity;
}
private void OnCookStop(Entity<ActiveMicrowaveComponent> ent, ref ComponentShutdown args)

View file

@ -305,7 +305,7 @@ namespace Content.Server.Kitchen.EntitySystems
active.Program = program;
reagentGrinder.AudioStream = _audioSystem.PlayPvs(sound, uid,
AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier)).Value.Entity; //slightly higher pitched
AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier))?.Entity; //slightly higher pitched
_userInterfaceSystem.ServerSendUiMessage(uid, ReagentGrinderUiKey.Key,
new ReagentGrinderWorkStartedMessage(program));
}

View file

@ -155,7 +155,7 @@ public sealed class MechGrabberSystem : EntitySystem
return;
args.Handled = true;
component.AudioStream = _audio.PlayPvs(component.GrabSound, uid).Value.Entity;
component.AudioStream = _audio.PlayPvs(component.GrabSound, uid)?.Entity;
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.GrabDelay, new GrabberDoAfterEvent(), uid, target: target, used: uid)
{
BreakOnMove = true

View file

@ -1,5 +1,6 @@
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Components.Targets;
using Content.Shared.CartridgeLoader;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
@ -172,6 +173,11 @@ public sealed class StealConditionSystem : EntitySystem
if (target.StealGroup != condition.StealGroup)
return 0;
// check if cartridge is installed
if (TryComp<CartridgeComponent>(entity, out var cartridge) &&
cartridge.InstallationStatus is not InstallationStatus.Cartridge)
return 0;
// check if needed target alive
if (condition.CheckAlive)
{

View file

@ -102,10 +102,6 @@ public sealed class TegSystem : EntitySystem
private void GeneratorUpdate(EntityUid uid, TegGeneratorComponent component, ref AtmosDeviceUpdateEvent args)
{
var tegGroup = GetNodeGroup(uid);
if (tegGroup is not { IsFullyBuilt: true })
return;
var supplier = Comp<PowerSupplierComponent>(uid);
var powerReceiver = Comp<ApcPowerReceiverComponent>(uid);
if (!powerReceiver.Powered)
@ -114,6 +110,10 @@ public sealed class TegSystem : EntitySystem
return;
}
var tegGroup = GetNodeGroup(uid);
if (tegGroup is not { IsFullyBuilt: true })
return;
var circA = tegGroup.CirculatorA!.Owner;
var circB = tegGroup.CirculatorB!.Owner;

View file

@ -2,16 +2,15 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Shared.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.Database;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Respawn;
using Content.Shared.Station.Components;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Respawn;
@ -179,7 +178,7 @@ public sealed class SpecialRespawnSystem : SharedSpecialRespawnSystem
foreach (var newTileRef in grid.GetTilesIntersecting(circle))
{
if (newTileRef.IsSpace(_tileDefinitionManager) || newTileRef.IsBlockedTurf(true) || !_atmosphere.IsTileMixtureProbablySafe(targetGrid, targetMap, mapTarget))
if (newTileRef.IsSpace(_tileDefinitionManager) || _turf.IsTileBlocked(newTileRef, CollisionGroup.MobMask) || !_atmosphere.IsTileMixtureProbablySafe(targetGrid, targetMap, mapTarget))
continue;
found = true;

View file

@ -126,7 +126,7 @@ namespace Content.Server.RoundEnd
return _countdownTokenSource != null;
}
public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station")
public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement")
{
var duration = DefaultCountdownDuration;
@ -144,7 +144,7 @@ namespace Content.Server.RoundEnd
RequestRoundEnd(duration, requester, checkCooldown, text, name);
}
public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station")
public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement")
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
return;
@ -184,7 +184,7 @@ namespace Content.Server.RoundEnd
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString(text,
("time", time),
("units", Loc.GetString(units))),
name,
Loc.GetString(name),
false,
null,
colorOverride: Color.Gold); // Sunrise-TTS

View file

@ -154,8 +154,8 @@ public sealed partial class SalvageSystem
}
else if (comp.Stream == null && remaining < audioLength)
{
var audio = _audio.PlayPvs(comp.Sound, uid).Value;
comp.Stream = audio.Entity;
var audio = _audio.PlayPvs(comp.Sound, uid);
comp.Stream = audio?.Entity;
_audio.SetMapAudio(audio);
comp.Stage = ExpeditionStage.MusicCountdown;
Dirty(uid, comp);

View file

@ -398,7 +398,8 @@ public sealed partial class ShuttleSystem
new EntityCoordinates(fromMapUid.Value, _mapSystem.GetGridPosition(entity.Owner)), true, startupAudio.Params);
_audio.SetPlaybackPosition(clippedAudio, entity.Comp1.StartupTime);
clippedAudio.Value.Component.Flags |= AudioFlags.NoOcclusion;
if (clippedAudio != null)
clippedAudio.Value.Component.Flags |= AudioFlags.NoOcclusion;
}
// Offset the start by buffer range just to avoid overlap.

View file

@ -37,6 +37,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ComponentRemove>(OnComponentRemoved);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, EventHorizonAttemptConsumeEntityEvent>(PreventBreach);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, MapInitEvent>(OnMapInit);
}
public override void Update(float frameTime)
@ -61,6 +62,12 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
#region Events
private void OnMapInit(Entity<ContainmentFieldGeneratorComponent> generator, ref MapInitEvent args)
{
if (generator.Comp.Enabled)
ChangeFieldVisualizer(generator);
}
/// <summary>
/// A generator receives power from a source colliding with it.
/// </summary>

View file

@ -1,6 +1,6 @@
using System.Numerics;
using Content.Shared.Anomaly.Effects;
using Content.Shared.Anomaly.Prototypes;
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@ -16,7 +16,7 @@ namespace Content.Shared.Anomaly.Components;
/// Anomalies and their related components were designed here: https://hackmd.io/@ss14-design/r1sQbkJOs
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedAnomalySystem))]
[Access(typeof(SharedAnomalySystem), typeof(SharedInnerBodyAnomalySystem))]
public sealed partial class AnomalyComponent : Component
{
/// <summary>
@ -184,21 +184,21 @@ public sealed partial class AnomalyComponent : Component
/// <summary>
/// The minimum amount of research points generated per second
/// </summary>
[DataField("minPointsPerSecond")]
[DataField]
public int MinPointsPerSecond = 10;
/// <summary>
/// The maximum amount of research points generated per second
/// This doesn't include the point bonus for being unstable.
/// </summary>
[DataField("maxPointsPerSecond")]
[DataField]
public int MaxPointsPerSecond = 70;
/// <summary>
/// The multiplier applied to the point value for the
/// anomaly being above the <see cref="GrowthThreshold"/>
/// </summary>
[DataField("growingPointMultiplier")]
[DataField]
public float GrowingPointMultiplier = 1.5f;
#endregion
@ -252,10 +252,13 @@ public sealed partial class AnomalyComponent : Component
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("offset")]
public Vector2 FloatingOffset = new(0, 0.15f);
public Vector2 FloatingOffset = new(0, 0);
public readonly string AnimationKey = "anomalyfloat";
#endregion
[DataField]
public bool DeleteEntity = true;
}
/// <summary>

View file

@ -0,0 +1,72 @@
using Content.Shared.Anomaly.Effects;
using Content.Shared.Body.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Anomaly.Components;
/// <summary>
/// An anomaly within the body of a living being. Controls the ability to return to the standard state.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedInnerBodyAnomalySystem))]
public sealed partial class InnerBodyAnomalyComponent : Component
{
[DataField]
public bool Injected;
/// <summary>
/// A prototype of an entity whose components will be added to the anomaly host **AND** then removed at the right time
/// </summary>
[DataField(required: true)]
public EntProtoId? InjectionProto;
/// <summary>
/// Duration of stun from the effect of the anomaly
/// </summary>
[DataField]
public float StunDuration = 4f;
/// <summary>
/// A message sent in chat to a player who has become infected by an anomaly
/// </summary>
[DataField]
public LocId? StartMessage = null;
/// <summary>
/// A message sent in chat to a player who has cleared an anomaly
/// </summary>
[DataField]
public LocId? EndMessage = "inner-anomaly-end-message";
/// <summary>
/// Sound, playing on becoming anomaly
/// </summary>
[DataField]
public SoundSpecifier? StartSound = new SoundPathSpecifier("/Audio/Effects/inneranomaly.ogg");
/// <summary>
/// Used to display messages to the player about their level of disease progression
/// </summary>
[DataField]
public float LastSeverityInformed = 0f;
/// <summary>
/// The fallback sprite to be added on the original entity. Allows you to visually identify the feature and type of anomaly to other players
/// </summary>
[DataField, AutoNetworkedField]
public SpriteSpecifier? FallbackSprite = null;
/// <summary>
/// Ability to use unique sprites for different body types
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<BodyPrototype>, SpriteSpecifier> SpeciesSprites = new();
/// <summary>
/// The key of the entity layer into which the sprite will be inserted
/// </summary>
[DataField]
public string LayerMap = "inner_anomaly_layer";
}

View file

@ -0,0 +1,21 @@
using Content.Shared.Anomaly.Effects;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
namespace Content.Shared.Anomaly.Components;
/// <summary>
/// On contact with an entity, if it meets the conditions, it will transfer the specified components to it
/// </summary>
[RegisterComponent, Access(typeof(SharedInnerBodyAnomalySystem))]
public sealed partial class InnerBodyAnomalyInjectorComponent : Component
{
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// components that will be automatically removed after “curing”
/// </summary>
[DataField(required: true)]
public ComponentRegistry InjectionComponents = default!;
}

View file

@ -0,0 +1,5 @@
namespace Content.Shared.Anomaly.Effects;
public abstract class SharedInnerBodyAnomalySystem : EntitySystem
{
}

View file

@ -1,13 +1,10 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Prototypes;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Weapons.Melee.Components;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@ -21,6 +18,7 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using Content.Shared.Actions;
namespace Content.Shared.Anomaly;
@ -36,6 +34,7 @@ public abstract class SharedAnomalySystem : EntitySystem
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
@ -145,7 +144,7 @@ public abstract class SharedAnomalySystem : EntitySystem
if (!Timing.IsFirstTimePredicted)
return;
Audio.PlayPvs(component.SupercriticalSound, uid);
Audio.PlayPvs(component.SupercriticalSound, Transform(uid).Coordinates);
if (_net.IsServer)
Log.Info($"Raising supercritical event. Entity: {ToPrettyString(uid)}");
@ -169,7 +168,8 @@ public abstract class SharedAnomalySystem : EntitySystem
/// <param name="uid">The anomaly being shut down</param>
/// <param name="component"></param>
/// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false)
/// <param name="spawnCore">Create anomaly cores based on the result of completing an anomaly?</param>
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true)
{
// Logging before resolve, in case the anomaly has deleted itself.
if (_net.IsServer)
@ -186,9 +186,16 @@ public abstract class SharedAnomalySystem : EntitySystem
if (Terminating(uid) || _net.IsClient)
return;
Spawn(supercritical ? component.CorePrototype : component.CoreInertPrototype, Transform(uid).Coordinates);
if (spawnCore)
{
var core = Spawn(supercritical ? component.CorePrototype : component.CoreInertPrototype, Transform(uid).Coordinates);
_transform.PlaceNextTo(core, uid);
}
QueueDel(uid);
if (component.DeleteEntity)
QueueDel(uid);
else
RemCompDeferred<AnomalySupercriticalComponent>(uid);
}
/// <summary>
@ -458,3 +465,5 @@ public partial record struct AnomalySpawnSettings()
/// </summary>
public bool SpawnOnSeverityChanged { get; set; } = false;
}
public sealed partial class ActionAnomalyPulseEvent : InstantActionEvent { }

View file

@ -1,5 +1,5 @@
using System.Linq;
using Content.Shared.Buckle.Components;
using Content.Shared.Cuffs.Components;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.IdentityManagement;
@ -84,15 +84,29 @@ public abstract partial class SharedBuckleSystem
if (!TryComp(args.User, out BuckleComponent? buckle))
return;
if (buckle.BuckledTo == null && component.BuckleOnInteractHand)
// Buckle self
if (buckle.BuckledTo == null && component.BuckleOnInteractHand && StrapHasSpace(uid, buckle, component))
{
TryBuckle(args.User, args.User, uid, buckle, popup: true);
else if (buckle.BuckledTo == uid)
TryUnbuckle(args.User, args.User, buckle, popup: true);
else
args.Handled = true;
return;
}
// Unbuckle self
if (buckle.BuckledTo == uid && TryUnbuckle(args.User, args.User, buckle, popup: true))
{
args.Handled = true;
return;
}
// Unbuckle others
if (component.BuckledEntities.TryFirstOrNull(out var buckled) && TryUnbuckle(buckled.Value, args.User))
{
args.Handled = true;
return;
}
// TODO BUCKLE add out bool for whether a pop-up was generated or not.
args.Handled = true;
}
private void OnBuckleInteractHand(Entity<BuckleComponent> ent, ref InteractHandEvent args)

View file

@ -0,0 +1,11 @@
using Content.Shared.CriminalRecords;
using Content.Shared.CriminalRecords.Systems;
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
[Serializable, NetSerializable]
public sealed class WantedListUiState(List<WantedRecord> records) : BoundUserInterfaceState
{
public List<WantedRecord> Records = records;
}

View file

@ -359,39 +359,76 @@ namespace Content.Shared.Containers.ItemSlots
/// Useful for predicted interactions
/// </param>
/// <returns>False if failed to insert item</returns>
public bool TryInsertEmpty(Entity<ItemSlotsComponent?> ent, EntityUid item, EntityUid? user, bool excludeUserAudio = false)
public bool TryInsertEmpty(Entity<ItemSlotsComponent?> ent,
EntityUid item,
EntityUid? user,
bool excludeUserAudio = false)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
TryComp(user, out HandsComponent? handsComp);
if (!TryGetAvailableSlot(ent,
item,
user == null ? null : (user.Value, handsComp),
out var itemSlot,
emptyOnly: true))
return false;
if (user != null && !_handsSystem.TryDrop(user.Value, item, handsComp: handsComp))
return false;
Insert(ent, itemSlot, item, user, excludeUserAudio: excludeUserAudio);
return true;
}
/// <summary>
/// Tries to get any slot that the <paramref name="item"/> can be inserted into.
/// </summary>
/// <param name="ent">Entity that <paramref name="item"/> is being inserted into.</param>
/// <param name="item">Entity being inserted into <paramref name="ent"/>.</param>
/// <param name="userEnt">Entity inserting <paramref name="item"/> into <paramref name="ent"/>.</param>
/// <param name="itemSlot">The ItemSlot on <paramref name="ent"/> to insert <paramref name="item"/> into.</param>
/// <param name="emptyOnly"> True only returns slots that are empty.
/// False returns any slot that is able to receive <paramref name="item"/>.</param>
/// <returns>True when a slot is found. Otherwise, false.</returns>
public bool TryGetAvailableSlot(Entity<ItemSlotsComponent?> ent,
EntityUid item,
Entity<HandsComponent?>? userEnt,
[NotNullWhen(true)] out ItemSlot? itemSlot,
bool emptyOnly = false)
{
itemSlot = null;
if (userEnt is { } user
&& Resolve(user, ref user.Comp)
&& _handsSystem.IsHolding(user, item))
{
if (!_handsSystem.CanDrop(user, item, user.Comp))
return false;
}
if (!Resolve(ent, ref ent.Comp, false))
return false;
var slots = new List<ItemSlot>();
foreach (var slot in ent.Comp.Slots.Values)
{
if (slot.ContainerSlot?.ContainedEntity != null)
if (emptyOnly && slot.ContainerSlot?.ContainedEntity != null)
continue;
if (CanInsert(ent, item, user, slot))
if (CanInsert(ent, item, userEnt, slot))
slots.Add(slot);
}
if (slots.Count == 0)
return false;
if (user != null && _handsSystem.IsHolding(user.Value, item))
{
if (!_handsSystem.TryDrop(user.Value, item))
return false;
}
slots.Sort(SortEmpty);
foreach (var slot in slots)
{
if (TryInsert(ent, slot, item, user, excludeUserAudio: excludeUserAudio))
return true;
}
return false;
itemSlot = slots[0];
return true;
}
private static int SortEmpty(ItemSlot a, ItemSlot b)

View file

@ -23,6 +23,12 @@ public sealed record CriminalRecord
[DataField]
public string? Reason;
/// <summary>
/// The name of the person who changed the status.
/// </summary>
[DataField]
public string? InitiatorName;
/// <summary>
/// Criminal history of the person.
/// This should have charges and time served added after someone is detained.
@ -35,4 +41,4 @@ public sealed record CriminalRecord
/// A line of criminal activity and the time it was added at.
/// </summary>
[Serializable, NetSerializable]
public record struct CrimeHistory(TimeSpan AddTime, string Crime);
public record struct CrimeHistory(TimeSpan AddTime, string Crime, string? InitiatorName);

View file

@ -2,6 +2,8 @@ using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Security;
using Content.Shared.Security.Components;
using Content.Shared.StationRecords;
using Robust.Shared.Serialization;
namespace Content.Shared.CriminalRecords.Systems;
@ -50,3 +52,22 @@ public abstract class SharedCriminalRecordsSystem : EntitySystem
Dirty(characterUid, record);
}
}
[Serializable, NetSerializable]
public struct WantedRecord(GeneralStationRecord targetInfo, SecurityStatus status, string? reason, string? initiator, List<CrimeHistory> history)
{
public GeneralStationRecord TargetInfo = targetInfo;
public SecurityStatus Status = status;
public string? Reason = reason;
public string? Initiator = initiator;
public List<CrimeHistory> History = history;
};
[ByRefEvent]
public record struct CriminalRecordChangedEvent(CriminalRecord Record);
[ByRefEvent]
public record struct CriminalHistoryAddedEvent(CrimeHistory History);
[ByRefEvent]
public record struct CriminalHistoryRemovedEvent(CrimeHistory History);

View file

@ -1,8 +1,7 @@
using Content.Server.Explosion.EntitySystems;
using Content.Shared.Explosion;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Shared.Explosion.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Server.Explosion.Components;
namespace Content.Shared.Explosion.Components;
/// <summary>
/// Specifies an explosion that can be spawned by this entity. The explosion itself is spawned via <see
@ -12,31 +11,27 @@ namespace Content.Server.Explosion.Components;
/// The total intensity may be overridden by whatever system actually calls TriggerExplosive(), but this
/// component still determines the explosion type and other properties.
/// </remarks>
[RegisterComponent]
[RegisterComponent, Access(typeof(SharedExplosionSystem))]
public sealed partial class ExplosiveComponent : Component
{
/// <summary>
/// The explosion prototype. This determines the damage types, the tile-break chance, and some visual
/// information (e.g., the light that the explosion gives off).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("explosionType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
public string ExplosionType = default!;
[DataField(required: true)]
public ProtoId<ExplosionPrototype> ExplosionType = default!;
/// <summary>
/// The maximum intensity the explosion can have on a single tile. This limits the maximum damage and tile
/// break chance the explosion can achieve at any given location.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxIntensity")]
[DataField]
public float MaxIntensity = 4;
/// <summary>
/// How quickly the intensity drops off as you move away from the epicenter.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("intensitySlope")]
[DataField]
public float IntensitySlope = 1;
/// <summary>
@ -47,38 +42,34 @@ public sealed partial class ExplosiveComponent : Component
/// This number can be overridden by passing optional argument to <see
/// cref="ExplosionSystem.TriggerExplosive"/>.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("totalIntensity")]
[DataField]
public float TotalIntensity = 10;
/// <summary>
/// Factor used to scale the explosion intensity when calculating tile break chances. Allows for stronger
/// explosives that don't space tiles, without having to create a new explosion-type prototype.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("tileBreakScale")]
[DataField]
public float TileBreakScale = 1f;
/// <summary>
/// Maximum number of times that an explosive can break a tile. Currently, for normal space stations breaking a
/// tile twice will generally result in a vacuum.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxTileBreak")]
[DataField]
public int MaxTileBreak = int.MaxValue;
/// <summary>
/// Whether this explosive should be able to create a vacuum by breaking tiles.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canCreateVacuum")]
[DataField]
public bool CanCreateVacuum = true;
/// <summary>
/// An override for whether or not the entity should be deleted after it explodes.
/// If null, the system calling the explode method handles it.
/// </summary>
[DataField("deleteAfterExplosion")]
[DataField]
public bool? DeleteAfterExplosion;
/// <summary>

View file

@ -1,25 +1,40 @@
using Content.Shared.Explosion.Components;
using Content.Shared.Armor;
using Content.Shared.Explosion.Components;
namespace Content.Shared.Explosion.EntitySystems;
/// <summary>
/// Lets code in shared trigger explosions and handles explosion resistance examining.
/// All processing is still done clientside.
/// </summary>
public abstract class SharedExplosionSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExplosionResistanceComponent, ArmorExamineEvent>(OnArmorExamine);
}
private void OnArmorExamine(EntityUid uid, ExplosionResistanceComponent component, ref ArmorExamineEvent args)
private void OnArmorExamine(Entity<ExplosionResistanceComponent> ent, ref ArmorExamineEvent args)
{
var value = MathF.Round((1f - component.DamageCoefficient) * 100, 1);
var value = MathF.Round((1f - ent.Comp.DamageCoefficient) * 100, 1);
if (value == 0)
return;
args.Msg.PushNewline();
args.Msg.AddMarkupOrThrow(Loc.GetString(component.Examine, ("value", value)));
args.Msg.AddMarkupOrThrow(Loc.GetString(ent.Comp.Examine, ("value", value)));
}
/// <summary>
/// Given an entity with an explosive component, spawn the appropriate explosion.
/// </summary>
/// <remarks>
/// Also accepts radius or intensity arguments. This is useful for explosives where the intensity is not
/// specified in the yaml / by the component, but determined dynamically (e.g., by the quantity of a
/// solution in a reaction).
/// </remarks>
public virtual void TriggerExplosive(EntityUid uid, ExplosiveComponent? explosive = null, bool delete = true, float? totalIntensity = null, float? radius = null, EntityUid? user = null)
{
}
}

View file

@ -1,10 +1,15 @@
using Content.Shared.Flash.Components;
using Content.Shared.StatusEffect;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Shared.Flash
namespace Content.Shared.Flash;
public abstract class SharedFlashSystem : EntitySystem
{
public abstract class SharedFlashSystem : EntitySystem
public ProtoId<StatusEffectPrototype> FlashedKey = "Flashed";
public virtual void FlashArea(Entity<FlashComponent?> source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null)
{
[ValidatePrototypeId<StatusEffectPrototype>]
public const string FlashedKey = "Flashed";
}
}

View file

@ -23,14 +23,14 @@ public sealed partial class DrainComponent : Component
[DataField]
public Entity<SolutionComponent>? Solution = null;
[DataField("accumulator")]
[DataField]
public float Accumulator = 0f;
/// <summary>
/// Does this drain automatically absorb surrouding puddles? Or is it a drain designed to empty
/// solutions in it manually?
/// solutions in it manually?
/// </summary>
[DataField("autoDrain"), ViewVariables(VVAccess.ReadOnly)]
[DataField]
public bool AutoDrain = true;
/// <summary>
@ -38,47 +38,47 @@ public sealed partial class DrainComponent : Component
/// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle.
/// This will stay fixed to 1 second no matter what DrainFrequency is.
/// </summary>
[DataField("unitsPerSecond")]
[DataField]
public float UnitsPerSecond = 6f;
/// <summary>
/// How many units are ejected from the buffer per second.
/// </summary>
[DataField("unitsDestroyedPerSecond")]
[DataField]
public float UnitsDestroyedPerSecond = 3f;
/// <summary>
/// How many (unobstructed) tiles away the drain will
/// drain puddles from.
/// </summary>
[DataField("range"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float Range = 2f;
/// <summary>
/// How often in seconds the drain checks for puddles around it.
/// If the EntityQuery seems a bit unperformant this can be increased.
/// </summary>
[DataField("drainFrequency")]
[DataField]
public float DrainFrequency = 1f;
/// <summary>
/// How much time it takes to unclog it with a plunger
/// </summary>
[DataField("unclogDuration"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float UnclogDuration = 1f;
/// <summary>
/// What's the probability of uncloging on each try
/// </summary>
[DataField("unclogProbability"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float UnclogProbability = 0.75f;
[DataField("manualDrainSound"), ViewVariables(VVAccess.ReadOnly)]
[DataField]
public SoundSpecifier ManualDrainSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
[DataField("plungerSound"), ViewVariables(VVAccess.ReadOnly)]
[DataField]
public SoundSpecifier PlungerSound = new SoundPathSpecifier("/Audio/Items/Janitor/plunger.ogg");
[DataField("unclogSound"), ViewVariables(VVAccess.ReadOnly)]
[DataField]
public SoundSpecifier UnclogSound = new SoundPathSpecifier("/Audio/Effects/Fluids/glug.ogg");
}

View file

@ -0,0 +1,7 @@
namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised on the target when failing to pet/hug something.
/// </summary>
[ByRefEvent]
public readonly record struct InteractionFailureEvent(EntityUid User);

View file

@ -0,0 +1,7 @@
namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised on the target when successfully petting/hugging something.
/// </summary>
[ByRefEvent]
public readonly record struct InteractionSuccessEvent(EntityUid User);

View file

@ -1,6 +1,7 @@
using Content.Shared.Bed.Sleep;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
@ -100,6 +101,9 @@ public sealed class InteractionPopupSystem : EntitySystem
if (component.InteractSuccessSpawn != null)
Spawn(component.InteractSuccessSpawn, _transform.GetMapCoordinates(uid));
var ev = new InteractionSuccessEvent(user);
RaiseLocalEvent(target, ref ev);
}
else
{
@ -111,6 +115,9 @@ public sealed class InteractionPopupSystem : EntitySystem
if (component.InteractFailureSpawn != null)
Spawn(component.InteractFailureSpawn, _transform.GetMapCoordinates(uid));
var ev = new InteractionFailureEvent(user);
RaiseLocalEvent(target, ref ev);
}
if (!string.IsNullOrEmpty(component.MessagePerceivedByOthers))

View file

@ -1,6 +1,7 @@
using System.Numerics;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
namespace Content.Shared.Placeable;
@ -8,12 +9,16 @@ namespace Content.Shared.Placeable;
public sealed class PlaceableSurfaceSystem : EntitySystem
{
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlaceableSurfaceComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<PlaceableSurfaceComponent, StorageInteractUsingAttemptEvent>(OnStorageInteractUsingAttempt);
SubscribeLocalEvent<PlaceableSurfaceComponent, StorageAfterOpenEvent>(OnStorageAfterOpen);
SubscribeLocalEvent<PlaceableSurfaceComponent, StorageAfterCloseEvent>(OnStorageAfterClose);
}
public void SetPlaceable(EntityUid uid, bool isPlaceable, PlaceableSurfaceComponent? surface = null)
@ -21,6 +26,9 @@ public sealed class PlaceableSurfaceSystem : EntitySystem
if (!Resolve(uid, ref surface, false))
return;
if (surface.IsPlaceable == isPlaceable)
return;
surface.IsPlaceable = isPlaceable;
Dirty(uid, surface);
}
@ -59,11 +67,24 @@ public sealed class PlaceableSurfaceSystem : EntitySystem
if (!_handsSystem.TryDrop(args.User, args.Used))
return;
if (surface.PlaceCentered)
Transform(args.Used).LocalPosition = Transform(uid).LocalPosition + surface.PositionOffset;
else
Transform(args.Used).Coordinates = args.ClickLocation;
_transformSystem.SetCoordinates(args.Used,
surface.PlaceCentered ? Transform(uid).Coordinates.Offset(surface.PositionOffset) : args.ClickLocation);
args.Handled = true;
}
private void OnStorageInteractUsingAttempt(Entity<PlaceableSurfaceComponent> ent, ref StorageInteractUsingAttemptEvent args)
{
args.Cancelled = true;
}
private void OnStorageAfterOpen(Entity<PlaceableSurfaceComponent> ent, ref StorageAfterOpenEvent args)
{
SetPlaceable(ent.Owner, true, ent.Comp);
}
private void OnStorageAfterClose(Entity<PlaceableSurfaceComponent> ent, ref StorageAfterCloseEvent args)
{
SetPlaceable(ent.Owner, false, ent.Comp);
}
}

View file

@ -79,7 +79,7 @@ public sealed partial class ContainmentFieldGeneratorComponent : Component
/// <summary>
/// Is the generator toggled on?
/// </summary>
[ViewVariables]
[DataField]
public bool Enabled;
/// <summary>

View file

@ -14,6 +14,7 @@ namespace Content.Shared.StatusEffect
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
private List<EntityUid> _toRemove = new();
public override void Initialize()
{
@ -32,17 +33,27 @@ namespace Content.Shared.StatusEffect
var curTime = _gameTiming.CurTime;
var enumerator = EntityQueryEnumerator<ActiveStatusEffectsComponent, StatusEffectsComponent>();
_toRemove.Clear();
while (enumerator.MoveNext(out var uid, out _, out var status))
{
foreach (var state in status.ActiveEffects.ToArray())
if (status.ActiveEffects.Count == 0)
{
// if we're past the end point of the effect
if (curTime > state.Value.Cooldown.Item2)
{
TryRemoveStatusEffect(uid, state.Key, status);
}
// This shouldn't happen, but just in case something sneaks through
_toRemove.Add(uid);
continue;
}
foreach (var state in status.ActiveEffects)
{
if (curTime > state.Value.Cooldown.Item2)
TryRemoveStatusEffect(uid, state.Key, status);
}
}
foreach (var uid in _toRemove)
{
RemComp<ActiveStatusEffectsComponent>(uid);
}
}
@ -62,32 +73,21 @@ namespace Content.Shared.StatusEffect
component.AllowedEffects.AddRange(state.AllowedEffects);
// Remove non-existent effects.
foreach (var effect in component.ActiveEffects.Keys)
foreach (var key in component.ActiveEffects.Keys)
{
if (!state.ActiveEffects.ContainsKey(effect))
{
TryRemoveStatusEffect(uid, effect, component, remComp: false);
}
if (!state.ActiveEffects.ContainsKey(key))
component.ActiveEffects.Remove(key);
}
foreach (var (key, effect) in state.ActiveEffects)
{
// don't bother with anything if we already have it
if (component.ActiveEffects.ContainsKey(key))
{
component.ActiveEffects[key] = new(effect);
continue;
}
var time = effect.Cooldown.Item2 - effect.Cooldown.Item1;
TryAddStatusEffect(uid, key, time, true, component, effect.Cooldown.Item1);
// Sunrise-edit: В душе не ебу что это за хуйня, но без этой хуйни в конце раунда всё ломается.
if (!component.ActiveEffects.TryGetValue(key, out var activeEffect))
return;
activeEffect.RelevantComponent = effect.RelevantComponent;
// state handling should not add networked components, that is handled separately by the client game state manager.
component.ActiveEffects[key] = new(effect);
}
if (component.ActiveEffects.Count == 0)
RemComp<ActiveStatusEffectsComponent>(uid);
else
EnsureComp<ActiveStatusEffectsComponent>(uid);
}
private void OnRejuvenate(EntityUid uid, StatusEffectsComponent component, RejuvenateEvent args)
@ -112,18 +112,16 @@ namespace Content.Shared.StatusEffect
if (!Resolve(uid, ref status, false))
return false;
if (TryAddStatusEffect(uid, key, time, refresh, status))
{
// If they already have the comp, we just won't bother updating anything.
if (!EntityManager.HasComponent<T>(uid))
{
var comp = EntityManager.AddComponent<T>(uid);
status.ActiveEffects[key].RelevantComponent = _componentFactory.GetComponentName(comp.GetType());
}
return true;
}
if (!TryAddStatusEffect(uid, key, time, refresh, status))
return false;
if (HasComp<T>(uid))
return true;
EntityManager.AddComponent<T>(uid);
status.ActiveEffects[key].RelevantComponent = _componentFactory.GetComponentName<T>();
return true;
return false;
}
public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, string component,
@ -165,8 +163,12 @@ namespace Content.Shared.StatusEffect
/// If the effect already exists, it will simply replace the cooldown with the new one given.
/// If you want special 'effect merging' behavior, do it your own damn self!
/// </remarks>
public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh,
StatusEffectsComponent? status = null, TimeSpan? startTime = null)
public bool TryAddStatusEffect(EntityUid uid,
string key,
TimeSpan time,
bool refresh,
StatusEffectsComponent? status = null,
TimeSpan? startTime = null)
{
if (!Resolve(uid, ref status, false))
return false;
@ -337,8 +339,7 @@ namespace Content.Shared.StatusEffect
/// <param name="uid">The entity to check on.</param>
/// <param name="key">The status effect ID to check for</param>
/// <param name="status">The status effect component, should you already have it.</param>
public bool CanApplyEffect(EntityUid uid, string key,
StatusEffectsComponent? status = null)
public bool CanApplyEffect(EntityUid uid, string key, StatusEffectsComponent? status = null)
{
// don't log since stuff calling this prolly doesn't care if we don't actually have it
if (!Resolve(uid, ref status, false))

View file

@ -487,9 +487,6 @@ public abstract class SharedEntityStorageSystem : EntitySystem
}
}
if (TryComp<PlaceableSurfaceComponent>(uid, out var surface))
_placeableSurface.SetPlaceable(uid, component.Open, surface);
_appearance.SetData(uid, StorageVisuals.Open, component.Open);
_appearance.SetData(uid, StorageVisuals.HasContents, component.Contents.ContainedEntities.Count > 0);
}

View file

@ -364,7 +364,9 @@ public abstract class SharedStorageSystem : EntitySystem
if (args.Handled || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert, false))
return;
if (HasComp<PlaceableSurfaceComponent>(uid))
var attemptEv = new StorageInteractUsingAttemptEvent();
RaiseLocalEvent(uid, ref attemptEv);
if (attemptEv.Cancelled)
return;
PlayerInsertHeldEntity((uid, storageComp), args.User);

View file

@ -238,6 +238,9 @@ namespace Content.Shared.Storage
[ByRefEvent]
public record struct StorageInteractAttemptEvent(bool Silent, bool Cancelled = false);
[ByRefEvent]
public record struct StorageInteractUsingAttemptEvent(bool Cancelled = false);
[NetSerializable]
[Serializable]
public enum StorageVisuals : byte

View file

@ -232,6 +232,11 @@
license: "CC-BY-SA-3.0"
source: https://github.com/YuriyKiss/space-station-14/commit/971a135a9c83aed46e967aac9302ab5b35562b5f
- files: [inneranomaly.ogg]
copyright: 'created by waveplaySFX on Freesound'
license: "CC0-1.0"
source: https://freesound.org/people/waveplaySFX/sounds/553744/
- files: [changeling_shriek.ogg]
copyright: whateverusername0
license: CC-BY-SA-3.0
@ -240,4 +245,4 @@
- files: [beeps.ogg, electrical_short_circuit.ogg, electrical_short_circuit2.ogg, ]
copyright: '"taken from zvukipro.com'
license: "CC-BY-NC-3.0"
source: https://zvukipro.com/predmet/158-zvuk-elektrichestva.html
source: https://zvukipro.com/predmet/158-zvuk-elektrichestva.html

Binary file not shown.

View file

@ -1,115 +1,4 @@
Entries:
- author: Errant
changes:
- message: Vox now have their entry in the guidebook.
type: Fix
id: 6886
time: '2024-07-09T00:28:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29713
- author: Whisper
changes:
- message: Light toggle actions now have a 1 second cooldown between uses.
type: Tweak
id: 6887
time: '2024-07-09T04:14:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29833
- author: Cojoke-dot
changes:
- message: Shotgun loading doafter now does something
type: Fix
id: 6888
time: '2024-07-09T04:23:08.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29827
- author: slarticodefast
changes:
- message: The construction menu and building preview now show the correct passive
vent sprite.
type: Fix
id: 6889
time: '2024-07-09T13:39:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29820
- author: Cojoke-dot
changes:
- message: Pacifists can now use foam weaponry
type: Tweak
id: 6890
time: '2024-07-09T13:46:21.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29835
- author: Plykiya
changes:
- message: You can now drop food and drinks to stop consuming it.
type: Fix
id: 6891
time: '2024-07-09T23:12:40.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29854
- author: Boaz1111
changes:
- message: Guitars can now be worn in the suit storage slot
type: Add
id: 6892
time: '2024-07-09T23:28:10.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29048
- author: Winkarst-cpu
changes:
- message: Fixed popup spam when trying to open borg's UI while the borg is locked.
type: Fix
id: 6893
time: '2024-07-09T23:48:56.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29861
- author: Lokachop
changes:
- message: Scarves now count as warm clothing for the warm clothing cargo bounty.
type: Tweak
id: 6894
time: '2024-07-10T05:26:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29779
- author: Aquif
changes:
- message: It is now possible to "lock" admin faxes such that they cannot be edited
by cybersun pens or any other IC means.
type: Add
id: 6895
time: '2024-07-10T05:28:36.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28972
- author: Ghagliiarghii
changes:
- message: The Librarian's Books Bag can now hold D&D related items such as dice
and battlemats.
type: Tweak
id: 6896
time: '2024-07-10T05:51:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29863
- author: Beck Thompson, Tayrtahn
changes:
- message: Typing indicators now correctly stack and will not overwrite your default
species indicator.
type: Fix
id: 6897
time: '2024-07-10T05:51:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29492
- author: Winkarst-cpu
changes:
- message: Now confirmation popup is displayed and item panel status is updated
after setting a custom solution transfer volume.
type: Fix
id: 6898
time: '2024-07-10T10:32:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29852
- author: Winkarst-cpu
changes:
- message: Added exit confirmation for character setup menu with unsaved changes.
type: Add
id: 6899
time: '2024-07-11T00:24:37.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29875
- author: ShadowCommander
changes:
- message: Players can now use melee attacks and shoves while dragging an entity
in their active hand.
type: Tweak
id: 6900
time: '2024-07-11T04:48:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29703
- author: Cojoke-dot
changes:
- message: You can no longer shoot out of crates with guns
@ -3910,3 +3799,125 @@
id: 7385
time: '2024-09-16T12:45:15.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32147
- author: Alice Liddel
changes:
- message: Crayon charges increased from 15 to 25
type: Add
id: 7386
time: '2024-09-17T00:35:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32061
- author: TheShuEd
changes:
- message: Anomalous infections added! People can now be infected by anomalies!
This allows you to use abnormal abilities, but can easily kill the host. To
cure them, bombard them with containment particles, because if the anomaly inside
them explodes, their bodies will be gibbed....
type: Add
- message: Flesh anomaly resprite
type: Tweak
- message: anomalies now disconnect from the anomaly synchronizer if they are too
far away from it.
type: Fix
id: 7387
time: '2024-09-17T09:49:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31876
- author: TheShuEd
changes:
- message: fix Tech anomaly loud sounds and superfast flickering
type: Fix
id: 7388
time: '2024-09-17T16:05:38.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32245
- author: drakewill-CRL
changes:
- message: Fixed plant mutations carrying over to other plants and future rounds.
type: Fix
id: 7389
time: '2024-09-17T19:45:42.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32257
- author: Moomoobeef
changes:
- message: Added more names to the pool of names the AI can have.
type: Add
id: 7390
time: '2024-09-17T22:09:55.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31951
- author: Calecute
changes:
- message: Corrected cake batter recipe in guidebook
type: Fix
id: 7391
time: '2024-09-18T15:15:34.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32276
- author: Beck Thompson
changes:
- message: Recycler no longer allows basic materials to be inserted into it.
type: Fix
id: 7392
time: '2024-09-18T21:58:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32144
- author: deltanedas
changes:
- message: Epinephrine now adds Adrenaline, because it is.
type: Tweak
id: 7393
time: '2024-09-18T23:00:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32076
- author: ShadowCommander
changes:
- message: Fixed clicking on chairs and beds with an entity buckled to them not
unbuckling them.
type: Fix
id: 7394
time: '2024-09-18T23:55:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29998
- author: Winkarst-cpu
changes:
- message: Now fire leaves burn marks on the tiles that were affected by it.
type: Add
id: 7395
time: '2024-09-19T00:23:50.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31939
- author: ArchRBX
changes:
- message: Mass scanners and shuttle consoles now display coordinates beneath IFF
labels
type: Add
- message: IFF labels that are beyond the viewport extents maintain their heading
and don't hug corners
type: Fix
id: 7396
time: '2024-09-19T01:25:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31501
- author: coffeeware
changes:
- message: a powered TEG won't produce infinite power when destroyed
type: Fix
id: 7397
time: '2024-09-19T02:15:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29972
- author: Boaz1111
changes:
- message: Added plasma and uranium arrows.
type: Add
id: 7398
time: '2024-09-19T08:41:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31241
- author: Ertanic
changes:
- message: Wanted list program and its cartridge.
type: Add
- message: The cartridge has been added to the HOS locker.
type: Add
- message: Added target to thief on wanted list cartridge.
type: Add
id: 7399
time: '2024-09-19T10:22:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31223
- author: Errant
changes:
- message: Crew monitor list can now be filtered by name and job.
type: Add
id: 7400
time: '2024-09-19T10:23:45.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31659

View file

@ -1,2 +1,2 @@
ui-actionslot-charges = Uses left: {$charges}
ui-actionslot-duration = [color=#a10505] {$duration} sec cooldown ({$timeLeft} sec remaining)[/color]

View file

@ -0,0 +1,17 @@
inner-anomaly-start-message-pyro = You can feel the insane flame inside of you. You became the host of a pyroclastic anomaly.
inner-anomaly-start-message-shock = Lightning bolts quivering at your fingertips! You became the host of a electric anomaly.
inner-anomaly-start-message-shadow = There's an impenetrable darkness oozing out of you... You became the host of a shadow anomaly.
inner-anomaly-start-message-frost = The icy frost is binding your bones. You became the host of a ice anomaly.
inner-anomaly-start-message-flora = Leaves and flowers sprout through your skin! You became the host of a floral anomaly.
inner-anomaly-start-message-bluespace = Your thoughts are racing like mad! You became the host of a bluespace anomaly.
inner-anomaly-start-message-flesh = Your body is growing frantically. You became the host of a flesh anomaly.
inner-anomaly-start-message-grav = Everything becames unnaturally heavy and light at the same time... You became the host of a gravity anomaly.
inner-anomaly-start-message-tech = Your head is buzzing with the amount of chaotic information! You became the host of a tech anomaly.
inner-anomaly-start-message-rock = The crystals are growing through your bones! You became the host of a rock anomaly.
inner-anomaly-end-message = The abnormal activity within you disappears without a trace....
inner-anomaly-severity-info-50 = You feel that the anomaly is taking over half your body.
inner-anomaly-severity-info-75 = You feel that the anomaly is taking over a large part of your body.
inner-anomaly-severity-info-90 = You feel that the anomaly has almost completely taken over your body.
inner-anomaly-severity-info-100 = The anomaly inside you is growing uncontrollably, causing immense pain, and tearing you apart!

View file

@ -19,3 +19,32 @@ log-probe-scan = Downloaded logs from {$device}!
log-probe-label-time = Time
log-probe-label-accessor = Accessed by
log-probe-label-number = #
# Wanted list cartridge
wanted-list-program-name = Wanted list
wanted-list-label-no-records = It's all right, cowboy
wanted-list-search-placeholder = Search by name and status
wanted-list-age-label = [color=darkgray]Age:[/color] [color=white]{$age}[/color]
wanted-list-job-label = [color=darkgray]Job:[/color] [color=white]{$job}[/color]
wanted-list-species-label = [color=darkgray]Species:[/color] [color=white]{$species}[/color]
wanted-list-gender-label = [color=darkgray]Gender:[/color] [color=white]{$gender}[/color]
wanted-list-reason-label = [color=darkgray]Reason:[/color] [color=white]{$reason}[/color]
wanted-list-unknown-reason-label = unknown reason
wanted-list-initiator-label = [color=darkgray]Initiator:[/color] [color=white]{$initiator}[/color]
wanted-list-unknown-initiator-label = unknown initiator
wanted-list-status-label = [color=darkgray]status:[/color] {$status ->
[suspected] [color=yellow]suspected[/color]
[wanted] [color=red]wanted[/color]
[detained] [color=#b18644]detained[/color]
[paroled] [color=green]paroled[/color]
[discharged] [color=green]discharged[/color]
*[other] none
}
wanted-list-history-table-time-col = Time
wanted-list-history-table-reason-col = Crime
wanted-list-history-table-initiator-col = Initiator

View file

@ -39,7 +39,7 @@ criminal-records-console-released = {$name} has been released by {$officer}.
criminal-records-console-not-wanted = {$officer} cleared the wanted status of {$name}.
criminal-records-console-paroled = {$name} has been released on parole by {$officer}.
criminal-records-console-not-parole = {$officer} cleared the parole status of {$name}.
criminal-records-console-unknown-officer = <unknown officer>
criminal-records-console-unknown-officer = <unknown>
## Filters

View file

@ -2,6 +2,8 @@
crew-monitoring-user-interface-title = Crew Monitoring Console
crew-monitor-filter-line-placeholder = Filter
crew-monitoring-user-interface-name = Name
crew-monitoring-user-interface-job = Job
crew-monitoring-user-interface-status = Status

View file

@ -40,6 +40,7 @@ steal-target-groups-clothing-eyes-hud-beer = beer goggles
steal-target-groups-bible = bible
steal-target-groups-clothing-neck-goldmedal = gold medal of crewmanship
steal-target-groups-clothing-neck-clownmedal = clown medal
steal-target-groups-wanted-list-cartridge = wanted list cartridge
# Thief structures
steal-target-groups-teg = teg generator part

View file

@ -4,6 +4,7 @@ round-end-system-shuttle-called-announcement = An emergency shuttle has been sen
round-end-system-shuttle-already-called-announcement = An emergency shuttle has already been sent.
round-end-system-shuttle-auto-called-announcement = An automatic crew shift change shuttle has been sent. ETA: {$time} {$units}. Recall the shuttle to extend the shift.
round-end-system-shuttle-recalled-announcement = The emergency shuttle has been recalled.
round-end-system-shuttle-sender-announcement = Station
round-end-system-round-restart-eta-announcement = Restarting the round in {$time} {$units}...
eta-units-minutes = minutes

View file

@ -10,3 +10,9 @@ wires-component-ui-on-receive-message-cannot-mend-uncut-wire = You can't mend a
wires-menu-name-label = Wires
wires-menu-dead-beef-text = DEAD-BEEF
wires-menu-help-popup =
Click on the gold contacts with a multitool in hand to pulse their wire.
Click on the wires with a pair of wirecutters in hand to cut/mend them.
The lights at the top show the state of the machine, messing with wires will probably do stuff to them.
Wire layouts are different each round, but consistent between machines of the same type.

View file

@ -154188,12 +154188,36 @@ entities:
parent: 60
- proto: WindoorSecure
entities:
- uid: 2538
components:
- type: Transform
rot: 1.5707963267948966 rad
pos: 13.5,-11.5
parent: 60
- type: DeviceLinkSink
invokeCounter: 1
- type: DeviceLinkSource
linkedPorts:
3481:
- DoorStatus: DoorBolt
- uid: 3269
components:
- type: Transform
rot: -1.5707963267948966 rad
pos: 23.5,-46.5
parent: 60
- uid: 3481
components:
- type: Transform
rot: 1.5707963267948966 rad
pos: 14.5,-13.5
parent: 60
- type: DeviceLinkSink
invokeCounter: 1
- type: DeviceLinkSource
linkedPorts:
2538:
- DoorStatus: DoorBolt
- uid: 3911
components:
- type: Transform
@ -154249,30 +154273,6 @@ entities:
rot: -1.5707963267948966 rad
pos: -23.5,-9.5
parent: 60
- uid: 2538
components:
- type: Transform
rot: 1.5707963267948966 rad
pos: 14.5,-13.5
parent: 60
- type: DeviceLinkSink
invokeCounter: 1
- type: DeviceLinkSource
linkedPorts:
3481:
- DoorStatus: DoorBolt
- uid: 3481
components:
- type: Transform
rot: 1.5707963267948966 rad
pos: 13.5,-11.5
parent: 60
- type: DeviceLinkSink
invokeCounter: 1
- type: DeviceLinkSource
linkedPorts:
2538:
- DoorStatus: DoorBolt
- uid: 12188
components:
- type: Transform

View file

@ -0,0 +1,9 @@
- type: entity
id: ActionAnomalyPulse
name: Anomaly pulse
description: Release a pulse of energy of your abnormal nature
components:
- type: InstantAction
icon: Structures/Specific/anomaly.rsi/anom1.png
event: !type:ActionAnomalyPulseEvent
useDelay: 30

View file

@ -331,6 +331,7 @@
- id: RubberStampHos
- id: SecurityTechFabCircuitboard
- id: WeaponDisabler
- id: WantedListCartridge
- id: FlippoLighterSunriseHos # Sunrise-Flippo
# Hardsuit table, used for suit storage as well

View file

@ -2,8 +2,11 @@
id: names_ai
values:
- 16-20
- 512k
- 640k #ought to be enough for anybody
- "790"
- Adaptive Manipulator
- Adlib #named after the famous soundcard
- ALICE
- Allied Mastercomputer
- Alpha 2
@ -19,21 +22,28 @@
- Aniel
- AOL
- Asimov
- Bell 301 #the most influential modem ever, created by the bell system. It still lives on today in certain applications
- Bishop
- Blitz
- Box
- Calculator
- Cassandra
- Cell
- Chii
- Chip
- C.R.A.I.G.
- Cray-2 #commercial supercomputer from the 70s
- CompuServe #if we're going to have AOL we may as well have some of their major competitors
- Computer
- Cutie
- Daedalus
- DecTalk
- Dee Model
- Dial Up
- Dorfl
- Duey
- Emma-2
- ENIAC #famous early computer
- Erasmus
- Everything
- Ez-27
@ -47,12 +57,16 @@
- Helios
- Hivebot Overmind
- Huey
- iAI #a play on the fad apple spawned of putting "i" infront of your tech products name
- I.E. 6 #hell on earth (web browser)
- Icarus
- Jeeves #if you don't get this one you are too young
- Jinx
- K.I.N.G
- Klapaucius
- Knight
- Louie
- Manchester Mark 2 #named after the Manchester Mark 1, the successor of which was actually named the Ferranti Mark 1, rather than Manchester Mark 2
- MARK13
- Maria
- Marvin
@ -64,6 +78,8 @@
- Mugsy3000
- Multivac
- NCH
- NT v6.0 #A play on both NT as in NanoTrasen and NT as in windows NT, of which version 6.0 is windows vista
- Packard Bell
- PTO
- Project Y2K
- Revelation
@ -76,10 +92,13 @@
- Shrike
- Solo
- Station Control Program
- AINU (AI's Not Unix)
- Super 35
- Surgeon General
- TWA
- Terminus
- TPM 3.0
- Turing Complete
- Tidy
- Ulysses
- W1k1

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