Merge remote-tracking branch 'upstream/master' into ed-27-09-2024-upstream

# Conflicts:
#	Content.Server/Explosion/EntitySystems/TriggerSystem.cs
This commit is contained in:
Ed
2024-09-27 10:22:05 +03:00
373 changed files with 47135 additions and 26244 deletions

View File

@@ -41,21 +41,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)
@@ -68,8 +57,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

@@ -51,6 +51,29 @@ namespace Content.Client.Actions
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
while (worldActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
while (instantActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
while (entityActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
}
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not InstantActionComponentState state)
@@ -95,6 +118,8 @@ namespace Content.Client.Actions
component.Icon = state.Icon;
component.IconOn = state.IconOn;
component.IconColor = state.IconColor;
component.OriginalIconColor = state.OriginalIconColor;
component.DisabledIconColor = state.DisabledIconColor;
component.Keywords.Clear();
component.Keywords.UnionWith(state.Keywords);
component.Enabled = state.Enabled;
@@ -125,6 +150,8 @@ namespace Content.Client.Actions
if (!ResolveActionData(actionId, ref action))
return;
action.IconColor = action.Charges < 1 ? action.DisabledIconColor : action.OriginalIconColor;
base.UpdateAction(actionId, action);
if (_playerManager.LocalEntity != action.AttachedEntity)
return;

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

@@ -214,9 +214,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

@@ -15,7 +15,6 @@ internal sealed class BuckleSystem : SharedBuckleSystem
{
base.Initialize();
SubscribeLocalEvent<BuckleComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<BuckleComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
}
@@ -57,21 +56,6 @@ internal sealed class BuckleSystem : SharedBuckleSystem
}
}
private void OnHandleState(Entity<BuckleComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not BuckleState state)
return;
ent.Comp.DontCollide = state.DontCollide;
ent.Comp.BuckleTime = state.BuckleTime;
var strapUid = EnsureEntity<BuckleComponent>(state.BuckledTo, ent);
SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null));
var (uid, component) = ent;
}
private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args)
{
if (!TryComp<RotationVisualsComponent>(uid, out var rotVisuals))

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,7 @@ using Content.Shared.Ensnaring;
using Content.Shared.Ensnaring.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Ensnaring.Visualizers;
namespace Content.Client.Ensnaring;
public sealed class EnsnareableSystem : SharedEnsnareableSystem
{
@@ -12,13 +12,14 @@ public sealed class EnsnareableSystem : SharedEnsnareableSystem
{
base.Initialize();
SubscribeLocalEvent<EnsnareableComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<EnsnareableComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnComponentInit(EntityUid uid, EnsnareableComponent component, ComponentInit args)
protected override void OnEnsnareInit(Entity<EnsnareableComponent> ent, ref ComponentInit args)
{
if(!TryComp<SpriteComponent>(uid, out var sprite))
base.OnEnsnareInit(ent, ref args);
if(!TryComp<SpriteComponent>(ent.Owner, out var sprite))
return;
// TODO remove this, this should just be in yaml.

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

@@ -1,9 +0,0 @@
using Content.Shared.GPS;
namespace Content.Client.GPS.Components
{
[RegisterComponent]
public sealed partial class HandheldGPSComponent : SharedHandheldGPSComponent
{
}
}

View File

@@ -1,4 +1,4 @@
using Content.Client.GPS.Components;
using Content.Shared.GPS.Components;
using Content.Client.GPS.UI;
using Content.Client.Items;

View File

@@ -1,4 +1,4 @@
using Content.Client.GPS.Components;
using Content.Shared.GPS.Components;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Robust.Client.GameObjects;
@@ -30,6 +30,13 @@ public sealed class HandheldGpsStatusControl : Control
{
base.FrameUpdate(args);
// don't display the label if the gps component is being removed
if (_parent.Comp.LifeStage > ComponentLifeStage.Running)
{
_label.Visible = false;
return;
}
_updateDif += args.DeltaSeconds;
if (_updateDif < _parent.Comp.UpdateRate)
return;
@@ -44,9 +51,9 @@ public sealed class HandheldGpsStatusControl : Control
var posText = "Error";
if (_entMan.TryGetComponent(_parent, out TransformComponent? transComp))
{
var pos = _transform.GetMapCoordinates(_parent.Owner, xform: transComp);
var x = (int) pos.X;
var y = (int) pos.Y;
var pos = _transform.GetMapCoordinates(_parent.Owner, xform: transComp);
var x = (int)pos.X;
var y = (int)pos.Y;
posText = $"({x}, {y})";
}
_label.SetMarkup(Loc.GetString("handheld-gps-coordinates-title", ("coordinates", posText)));

View File

@@ -21,6 +21,7 @@
Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
<RichTextLabel Name="NameLabel" SetWidth="150" />
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />

View File

@@ -73,6 +73,8 @@ namespace Content.Client.HealthAnalyzer.UI
// Patient Information
SpriteView.SetEntity(target.Value);
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
NoDataTex.Visible = !SpriteView.Visible;
var name = new FormattedMessage();
name.PushColor(Color.White);

View File

@@ -136,7 +136,7 @@ namespace Content.Client.Inventory
StyleClasses = { StyleBase.ButtonOpenRight }
};
button.OnPressed += (_) => SendMessage(new StrippingEnsnareButtonPressed());
button.OnPressed += (_) => SendPredictedMessage(new StrippingEnsnareButtonPressed());
_strippingMenu.SnareContainer.AddChild(button);
}
@@ -177,7 +177,7 @@ namespace Content.Client.Inventory
// So for now: only stripping & examining
if (ev.Function == EngineKeyFunctions.Use)
{
SendMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton));
SendPredictedMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton));
return;
}

View File

@@ -14,7 +14,7 @@
Stretch="KeepAspectCovered" />
<BoxContainer Name="MainContainer" VerticalExpand="True" HorizontalExpand="True" Orientation="Horizontal"
Margin="10 10 10 10" SeparationOverride="2">
<SplitContainer State="Auto" HorizontalExpand="True">
<SplitContainer State="Auto" ResizeMode="NotResizable" HorizontalExpand="True">
<!-- LHS Controls -->
<BoxContainer Name="LeftSide" Orientation="Vertical" SeparationOverride="4" HorizontalExpand="True">
<Control Name="DefaultState" VerticalExpand="True">

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

@@ -2,6 +2,7 @@ using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Utility;
using Content.Shared.Paper;
using static Content.Shared.Paper.PaperComponent;
namespace Content.Client.Paper.UI;
@@ -23,6 +24,10 @@ public sealed class PaperBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<PaperWindow>();
_window.OnSaved += InputOnTextEntered;
if (EntMan.TryGetComponent<PaperComponent>(Owner, out var paper))
{
_window.MaxInputLength = paper.ContentSize;
}
if (EntMan.TryGetComponent<PaperVisualsComponent>(Owner, out var visuals))
{
_window.InitVisuals(Owner, visuals);

View File

@@ -15,10 +15,13 @@
<Control Name="TextAlignmentPadding" VerticalAlignment="Top"/>
<RichTextLabel Name="BlankPaperIndicator" StyleClasses="LabelSecondaryColor" VerticalAlignment="Top" HorizontalAlignment="Center"/>
<RichTextLabel StyleClasses="PaperWrittenText" Name="WrittenTextLabel" VerticalAlignment="Top"/>
<PanelContainer Name="InputContainer" StyleClasses="TransparentBorderedWindowPanel" MinHeight="100"
VerticalAlignment="Stretch" VerticalExpand="True" HorizontalExpand="True">
<TextEdit Name="Input" StyleClasses="PaperLineEdit" Access="Public" />
</PanelContainer>
<BoxContainer Name="InputContainer" Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Stretch">
<PanelContainer StyleClasses="TransparentBorderedWindowPanel" MinHeight="100"
VerticalAlignment="Stretch" VerticalExpand="True" HorizontalExpand="True">
<TextEdit Name="Input" StyleClasses="PaperLineEdit" Access="Public" />
</PanelContainer>
<Label Name="FillStatus" StyleClasses="LabelSecondaryColor"/>
</BoxContainer>
</BoxContainer>
<paper:StampCollection Name="StampDisplay" VerticalAlignment="Bottom" Margin="6"/>

View File

@@ -48,6 +48,20 @@ namespace Content.Client.Paper.UI
public event Action<string>? OnSaved;
private int _MaxInputLength = -1;
public int MaxInputLength
{
get
{
return _MaxInputLength;
}
set
{
_MaxInputLength = value;
UpdateFillState();
}
}
public PaperWindow()
{
IoCManager.InjectDependencies(this);
@@ -63,11 +77,21 @@ namespace Content.Client.Paper.UI
{
if (args.Function == EngineKeyFunctions.MultilineTextSubmit)
{
RunOnSaved();
args.Handle();
// SaveButton is disabled when we hit the max input limit. Just check
// that flag instead of trying to calculate the input length again
if (!SaveButton.Disabled)
{
RunOnSaved();
args.Handle();
}
}
};
Input.OnTextChanged += args =>
{
UpdateFillState();
};
SaveButton.OnPressed += _ =>
{
RunOnSaved();
@@ -126,6 +150,7 @@ namespace Content.Client.Paper.UI
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
FillStatus.ModulateSelfOverride = visuals.FontAccentColor;
var contentImage = visuals.ContentImagePath != null ? _resCache.GetResource<TextureResource>(visuals.ContentImagePath) : null;
if (contentImage != null)
@@ -296,5 +321,25 @@ namespace Content.Client.Paper.UI
{
OnSaved?.Invoke(Rope.Collapse(Input.TextRope));
}
private void UpdateFillState()
{
if (MaxInputLength != -1)
{
var inputLength = Input.TextLength;
FillStatus.Text = Loc.GetString("paper-ui-fill-level",
("currentLength", inputLength),
("maxLength", MaxInputLength));
// Disable the save button if we've gone over the limit
SaveButton.Disabled = inputLength > MaxInputLength;
}
else
{
FillStatus.Text = "";
SaveButton.Disabled = false;
}
}
}
}

View File

@@ -0,0 +1,19 @@
<Control xmlns="https://spacestation14.io" HorizontalExpand="True">
<BoxContainer Name="MainContainer"
Orientation="Horizontal"
HorizontalExpand="True">
<PanelContainer Name="ColorPanel"
VerticalExpand="True"
SetWidth="7"
Margin="0 1 0 0" />
<Button Name="MainButton"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="ButtonSquare"
Margin="-1 0 0 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Name="BeaconNameLabel" />
</BoxContainer>
</Button>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,50 @@
using Content.Shared.Pinpointer;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapBeaconControl : Control, IComparable<StationMapBeaconControl>
{
[Dependency] private readonly IEntityManager _entMan = default!;
public readonly EntityCoordinates BeaconPosition;
public Action<EntityCoordinates>? OnPressed;
public string? Label => BeaconNameLabel.Text;
private StyleBoxFlat _styleBox;
public Color Color => _styleBox.BackgroundColor;
public StationMapBeaconControl(EntityUid mapUid, SharedNavMapSystem.NavMapBeacon beacon)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
BeaconPosition = new EntityCoordinates(mapUid, beacon.Position);
_styleBox = new StyleBoxFlat { BackgroundColor = beacon.Color };
ColorPanel.PanelOverride = _styleBox;
BeaconNameLabel.Text = beacon.Text;
MainButton.OnPressed += args => OnPressed?.Invoke(BeaconPosition);
}
public int CompareTo(StationMapBeaconControl? other)
{
if (other == null)
return 1;
// Group by color
var colorCompare = Color.ToArgb().CompareTo(other.Color.ToArgb());
if (colorCompare != 0)
{
return colorCompare;
}
// If same color, sort by text
return string.Compare(Label, other.Label);
}
}

View File

@@ -24,9 +24,16 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<StationMapWindow>();
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
string stationName = string.Empty;
if(EntMan.TryGetComponent<MetaDataComponent>(gridUid, out var gridMetaData))
{
stationName = gridMetaData.EntityName;
}
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.ShowLocation)
_window.Set(gridUid, Owner);
_window.Set(stationName, gridUid, Owner);
else
_window.Set(gridUid, null);
_window.Set(stationName, gridUid, null);
}
}

View File

@@ -3,11 +3,28 @@
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
Title="{Loc 'station-map-window-title'}"
Resizable="False"
SetSize="668 713"
MinSize="668 713">
SetSize="868 748"
MinSize="868 748">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 8 0 10" VerticalAlignment="Top">
<!-- Station name -->
<controls:StripeBack>
<PanelContainer>
<Label Name="StationName" Text="Unknown station" StyleClasses="LabelBig" Align="Center"/>
</PanelContainer>
</controls:StripeBack>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalAlignment="Top">
<ui:NavMapControl Name="NavMapScreen"/>
<BoxContainer Orientation="Vertical" SetWidth="200">
<!-- Search bar -->
<LineEdit Name="FilterBar" PlaceHolder="{Loc 'station-map-filter-placeholder'}" Margin="0 0 10 10" HorizontalExpand="True"/>
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<!-- Beacon Buttons (filled by code) -->
<BoxContainer Name="BeaconButtons" Orientation="Vertical" HorizontalExpand="True" />
</ScrollContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->

View File

@@ -3,24 +3,75 @@ using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Content.Shared.Pinpointer;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly List<StationMapBeaconControl> _buttons = new();
public StationMapWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
FilterBar.OnTextChanged += (bar) => OnFilterChanged(bar.Text);
}
public void Set(EntityUid? mapUid, EntityUid? trackedEntity)
public void Set(string stationName, EntityUid? mapUid, EntityUid? trackedEntity)
{
NavMapScreen.MapUid = mapUid;
if (trackedEntity != null)
NavMapScreen.TrackedCoordinates.Add(new EntityCoordinates(trackedEntity.Value, Vector2.Zero), (true, Color.Cyan));
if (!string.IsNullOrEmpty(stationName))
{
StationName.Text = stationName;
}
NavMapScreen.ForceNavMapUpdate();
UpdateBeaconList(mapUid);
}
}
public void OnFilterChanged(string newFilter)
{
foreach (var button in _buttons)
{
button.Visible = string.IsNullOrEmpty(newFilter) || (
!string.IsNullOrEmpty(button.Label) &&
button.Label.Contains(newFilter, StringComparison.OrdinalIgnoreCase)
);
};
}
public void UpdateBeaconList(EntityUid? mapUid)
{
BeaconButtons.Children.Clear();
_buttons.Clear();
if (!mapUid.HasValue)
return;
if (!_entMan.TryGetComponent<NavMapComponent>(mapUid, out var navMap))
return;
foreach (var beacon in navMap.Beacons.Values)
{
var button = new StationMapBeaconControl(mapUid.Value, beacon);
button.OnPressed += NavMapScreen.CenterToCoordinates;
_buttons.Add(button);
}
_buttons.Sort();
foreach (var button in _buttons)
BeaconButtons.AddChild(button);
}
}

View File

@@ -148,7 +148,12 @@ namespace Content.Client.Popups
}
public override void PopupCursor(string? message, PopupType type = PopupType.Small)
=> PopupCursorInternal(message, type, true);
{
if (!_timing.IsFirstTimePredicted)
return;
PopupCursorInternal(message, type, true);
}
public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
{

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

@@ -87,6 +87,9 @@ public sealed partial class DialogWindow : FancyWindow
Prompts.AddChild(box);
}
// Grab keyboard focus for the first dialog entry
_promptLines[0].Item2.GrabKeyboardFocus();
OkButton.OnPressed += _ => Confirm();
CancelButton.OnPressed += _ =>

View File

@@ -23,29 +23,17 @@ namespace Content.Client.VendingMachines
{
base.Open();
var vendingMachineSys = EntMan.System<VendingMachineSystem>();
_cachedInventory = vendingMachineSys.GetAllInventory(Owner);
_menu = this.CreateWindow<VendingMachineMenu>();
_menu.OpenCenteredLeft();
_menu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_menu.OnItemSelected += OnItemSelected;
_menu.Populate(_cachedInventory);
_menu.OpenCenteredLeft();
Refresh();
}
protected override void UpdateState(BoundUserInterfaceState state)
public void Refresh()
{
base.UpdateState(state);
if (state is not VendingMachineInterfaceState newState)
return;
_cachedInventory = newState.Inventory;
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
_menu?.Populate(_cachedInventory);
}

View File

@@ -8,6 +8,7 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
@@ -15,6 +16,15 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState);
}
private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
{
if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
{
bui.Refresh();
}
}
private void OnAnimationCompleted(EntityUid uid, VendingMachineComponent component, AnimationCompletedEvent args)

View File

@@ -1,9 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.Client.Popups;
using Content.Shared.CCVar;
using Content.Shared.Examine;
using Content.Shared.Tag;
using Content.Shared.Verbs;
@@ -13,6 +13,8 @@ using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Utility;
@@ -28,11 +30,11 @@ namespace Content.Client.Verbs
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary>
/// When a user right clicks somewhere, how large is the box we use to get entities for the context menu?
/// </summary>
public const float EntityMenuLookupSize = 0.25f;
private float _lookupSize;
/// <summary>
/// These flags determine what entities the user can see on the context menu.
@@ -41,114 +43,127 @@ namespace Content.Client.Verbs
public Action<VerbsResponseEvent>? OnVerbsResponse;
private List<EntityUid> _entities = new();
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<VerbsResponseEvent>(HandleVerbResponse);
Subs.CVar(_cfg, CCVars.GameEntityMenuLookup, OnLookupChanged, true);
}
private void OnLookupChanged(float val)
{
_lookupSize = val;
}
/// <summary>
/// Get all of the entities in an area for displaying on the context menu.
/// Get all of the entities in an area for displaying on the context menu.
/// </summary>
public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true)] out List<EntityUid>? result)
/// <returns>True if any entities were found.</returns>
public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true)] out List<EntityUid>? entities)
{
result = null;
entities = null;
if (_stateManager.CurrentState is not GameplayStateBase gameScreenBase)
if (_stateManager.CurrentState is not GameplayStateBase)
return false;
var player = _playerManager.LocalEntity;
if (player == null)
if (_playerManager.LocalEntity is not { } player)
return false;
// If FOV drawing is disabled, we will modify the visibility option to ignore visiblity checks.
var visibility = _eyeManager.CurrentEye.DrawFov
? Visibility
: Visibility | MenuVisibility.NoFov;
var visibility = _eyeManager.CurrentEye.DrawFov ? Visibility : Visibility | MenuVisibility.NoFov;
var ev = new MenuVisibilityEvent()
var ev = new MenuVisibilityEvent
{
TargetPos = targetPos,
Visibility = visibility,
};
RaiseLocalEvent(player.Value, ref ev);
RaiseLocalEvent(player, ref ev);
visibility = ev.Visibility;
// Get entities
_entities.Clear();
var entitiesUnderMouse = _tree.QueryAabb(targetPos.MapId, Box2.CenteredAround(targetPos.Position, new Vector2(EntityMenuLookupSize, EntityMenuLookupSize)));
// Do we have to do FoV checks?
if ((visibility & MenuVisibility.NoFov) == 0)
// Initially, we include all entities returned by a sprite area lookup
var box = Box2.CenteredAround(targetPos.Position, new Vector2(_lookupSize, _lookupSize));
var queryResult = _tree.QueryAabb(targetPos.MapId, box);
entities = new List<EntityUid>(queryResult.Count);
foreach (var ent in queryResult)
{
bool Predicate(EntityUid e) => e == player;
TryComp(player.Value, out ExaminerComponent? examiner);
foreach (var ent in entitiesUnderMouse)
{
if (_examine.CanExamine(player.Value, targetPos, Predicate, ent.Uid, examiner))
_entities.Add(ent.Uid);
}
}
else
{
foreach (var ent in entitiesUnderMouse)
{
_entities.Add(ent.Uid);
}
entities.Add(ent.Uid);
}
if (_entities.Count == 0)
return false;
if (visibility == MenuVisibility.All)
// If we're in a container list all other entities in it.
// E.g., allow players in lockers to examine / interact with other entities in the same locker
if (_containers.TryGetContainingContainer((player, null), out var container))
{
result = new (_entities);
return true;
}
// remove any entities in containers
if ((visibility & MenuVisibility.InContainer) == 0)
{
for (var i = _entities.Count - 1; i >= 0; i--)
// Only include the container contents when clicking near it.
if (entities.Contains(container.Owner)
|| _containers.TryGetOuterContainer(container.Owner, Transform(container.Owner), out var outer)
&& entities.Contains(outer.Owner))
{
var entity = _entities[i];
// The container itself might be in some other container, so it might not have been added by the
// sprite tree lookup.
if (!entities.Contains(container.Owner))
entities.Add(container.Owner);
if (ContainerSystem.IsInSameOrTransparentContainer(player.Value, entity))
continue;
_entities.RemoveSwap(i);
}
}
// remove any invisible entities
if ((visibility & MenuVisibility.Invisible) == 0)
{
var spriteQuery = GetEntityQuery<SpriteComponent>();
for (var i = _entities.Count - 1; i >= 0; i--)
{
var entity = _entities[i];
if (!spriteQuery.TryGetComponent(entity, out var spriteComponent) ||
!spriteComponent.Visible ||
_tagSystem.HasTag(entity, "HideContextMenu"))
// TODO Context Menu
// This might miss entities in some situations. E.g., one of the contained entities entity in it, that
// itself has another entity attached to it, then we should be able to "see" that entity.
// E.g., if a security guard is on a segway and gets thrown in a locker, this wouldn't let you see the guard.
foreach (var ent in container.ContainedEntities)
{
_entities.RemoveSwap(i);
if (!entities.Contains(ent))
entities.Add(ent);
}
}
}
if (_entities.Count == 0)
return false;
if ((visibility & MenuVisibility.InContainer) != 0)
{
// This is inefficient, but I'm lazy and CBF implementing my own recursive container method. Note that
// this might actually fail to add the contained children of some entities in the menu. E.g., an entity
// with a large sprite aabb, but small broadphase might appear in the menu, but have its children added
// by this.
var flags = LookupFlags.All & ~LookupFlags.Sensors;
foreach (var e in _lookup.GetEntitiesInRange(targetPos, _lookupSize, flags: flags))
{
if (!entities.Contains(e))
entities.Add(e);
}
}
result = new(_entities);
return true;
// Do we have to do FoV checks?
if ((visibility & MenuVisibility.NoFov) == 0)
{
TryComp(player, out ExaminerComponent? examiner);
for (var i = entities.Count - 1; i >= 0; i--)
{
if (!_examine.CanExamine(player, targetPos, e => e == player, entities[i], examiner))
entities.RemoveSwap(i);
}
}
if ((visibility & MenuVisibility.Invisible) != 0)
return entities.Count != 0;
for (var i = entities.Count - 1; i >= 0; i--)
{
if (_tagSystem.HasTag(entities[i], "HideContextMenu"))
entities.RemoveSwap(i);
}
// Unless we added entities in containers, every entity should already have a visible sprite due to
// the fact that we used the sprite tree query.
if (container == null && (visibility & MenuVisibility.InContainer) == 0)
return entities.Count != 0;
var spriteQuery = GetEntityQuery<SpriteComponent>();
for (var i = entities.Count - 1; i >= 0; i--)
{
if (!spriteQuery.TryGetComponent(entities[i], out var spriteComponent) || !spriteComponent.Visible)
entities.RemoveSwap(i);
}
return entities.Count != 0;
}
/// <summary>

View File

@@ -22,6 +22,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<VoiceMaskNameChangeWindow>();
_window.ReloadVerbs(_protomanager);
_window.AddVerbs();
_window.OnNameChange += OnNameSelected;
_window.OnVerbChange += verb => SendMessage(new VoiceMaskChangeVerbMessage(verb));

View File

@@ -31,8 +31,6 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
OnVerbChange?.Invoke((string?) args.Button.GetItemMetadata(args.Id));
SpeechVerbSelector.SelectId(args.Id);
};
AddVerbs();
}
public void ReloadVerbs(IPrototypeManager proto)
@@ -44,7 +42,7 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
_verbs.Sort((a, b) => a.Item1.CompareTo(b.Item1));
}
private void AddVerbs()
public void AddVerbs()
{
SpeechVerbSelector.Clear();

View File

@@ -1,7 +1,7 @@
<ui:VoteCallMenu xmlns="https://spacestation14.io"
<ui:VoteCallMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Voting.UI"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MouseFilter="Stop" MinSize="350 150">
MouseFilter="Stop" MinSize="350 200">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical">
<BoxContainer Margin="8 0" Orientation="Horizontal">
@@ -13,16 +13,18 @@
<controls:HighDivider />
<BoxContainer Orientation="Vertical" Margin="8 2 8 0" VerticalExpand="True" VerticalAlignment="Top">
<BoxContainer Orientation="Horizontal">
<OptionButton Name="VoteTypeButton" HorizontalExpand="True" />
<Control HorizontalExpand="True">
<OptionButton Name="VoteSecondButton" Visible="False" />
</Control>
<BoxContainer Orientation="Vertical">
<OptionButton Margin="2 1" Name="VoteTypeButton" HorizontalExpand="False" />
<BoxContainer Name="VoteOptionsButtonContainer" HorizontalExpand="False" Orientation="Vertical">
</BoxContainer>
<Button Margin="64 4" Name="FollowButton" Text="{Loc 'ui-vote-follow-button'}" Visible="False" />
<Label Margin="2 2" Name="VoteNotTrustedLabel" Text="{Loc 'ui-vote-trusted-users-notice'}" Visible="False" />
<Label Margin="2 2" Name="VoteWarningLabel" Text="{Loc 'ui-vote-abuse-warning'}" Visible="False" HorizontalAlignment="Center"/>
</BoxContainer>
<Label Name="VoteTypeTimeoutLabel" Visible="False" />
<Label Margin="8 2" Name="VoteTypeTimeoutLabel" Visible="False" />
</BoxContainer>
<Button Margin="8 2" Name="CreateButton" Text="{Loc 'ui-vote-create-button'}" />
<Button Margin="8 32 8 2" Name="CreateButton" Text="{Loc 'ui-vote-create-button'}" />
<PanelContainer StyleClasses="LowDivider" />
<Label Margin="12 0 0 0" StyleClasses="LabelSubText" Text="{Loc 'ui-vote-fluff'}" />

View File

@@ -1,7 +1,9 @@
using System;
using System.Linq;
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Voting;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
@@ -9,10 +11,8 @@ using Robust.Client.Console;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
@@ -25,32 +25,54 @@ namespace Content.Client.Voting.UI
[Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntityNetworkManager _entNetManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public static readonly (string name, StandardVoteType type, (string name, string id)[]? secondaries)[]
AvailableVoteTypes =
{
("ui-vote-type-restart", StandardVoteType.Restart, null),
("ui-vote-type-gamemode", StandardVoteType.Preset, null),
("ui-vote-type-map", StandardVoteType.Map, null)
};
private VotingSystem _votingSystem;
public StandardVoteType Type;
public Dictionary<StandardVoteType, CreateVoteOption> AvailableVoteOptions = new Dictionary<StandardVoteType, CreateVoteOption>()
{
{ StandardVoteType.Restart, new CreateVoteOption("ui-vote-type-restart", new(), false, null) },
{ StandardVoteType.Preset, new CreateVoteOption("ui-vote-type-gamemode", new(), false, null) },
{ StandardVoteType.Map, new CreateVoteOption("ui-vote-type-map", new(), false, null) },
{ StandardVoteType.Votekick, new CreateVoteOption("ui-vote-type-votekick", new(), true, 0) }
};
public Dictionary<string, string> VotekickReasons = new Dictionary<string, string>()
{
{ VotekickReasonType.Raiding.ToString(), Loc.GetString("ui-vote-votekick-type-raiding") },
{ VotekickReasonType.Cheating.ToString(), Loc.GetString("ui-vote-votekick-type-cheating") },
{ VotekickReasonType.Spam.ToString(), Loc.GetString("ui-vote-votekick-type-spamming") }
};
public Dictionary<NetUserId, (NetEntity, string)> PlayerList = new();
public OptionButton? _followDropdown = null;
public bool IsAllowedVotekick = false;
public VoteCallMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_votingSystem = _entityManager.System<VotingSystem>();
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
CloseButton.OnPressed += _ => Close();
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime) / 60));
for (var i = 0; i < AvailableVoteTypes.Length; i++)
foreach (StandardVoteType voteType in Enum.GetValues<StandardVoteType>())
{
var (text, _, _) = AvailableVoteTypes[i];
VoteTypeButton.AddItem(Loc.GetString(text), i);
var option = AvailableVoteOptions[voteType];
VoteTypeButton.AddItem(Loc.GetString(option.Name), (int)voteType);
}
VoteTypeButton.OnItemSelected += VoteTypeSelected;
VoteSecondButton.OnItemSelected += VoteSecondSelected;
CreateButton.OnPressed += CreatePressed;
FollowButton.OnPressed += FollowSelected;
}
protected override void Opened()
@@ -60,6 +82,8 @@ namespace Content.Client.Voting.UI
_netManager.ClientSendMessage(new MsgVoteMenu());
_voteManager.CanCallVoteChanged += CanCallVoteChanged;
_votingSystem.VotePlayerListResponse += UpdateVotePlayerList;
_votingSystem.RequestVotePlayerList();
}
public override void Close()
@@ -67,6 +91,7 @@ namespace Content.Client.Voting.UI
base.Close();
_voteManager.CanCallVoteChanged -= CanCallVoteChanged;
_votingSystem.VotePlayerListResponse -= UpdateVotePlayerList;
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -82,21 +107,50 @@ namespace Content.Client.Voting.UI
Close();
}
private void UpdateVotePlayerList(VotePlayerListResponseEvent msg)
{
Dictionary<string, string> optionsList = new();
Dictionary<NetUserId, (NetEntity, string)> playerList = new();
foreach ((NetUserId, NetEntity, string) player in msg.Players)
{
optionsList.Add(player.Item1.ToString(), player.Item3);
playerList.Add(player.Item1, (player.Item2, player.Item3));
}
if (optionsList.Count == 0)
optionsList.Add(" ", " ");
PlayerList = playerList;
IsAllowedVotekick = !msg.Denied;
var updatedDropdownOption = AvailableVoteOptions[StandardVoteType.Votekick];
updatedDropdownOption.Dropdowns = new List<Dictionary<string, string>>() { optionsList, VotekickReasons };
AvailableVoteOptions[StandardVoteType.Votekick] = updatedDropdownOption;
}
private void CreatePressed(BaseButton.ButtonEventArgs obj)
{
var typeId = VoteTypeButton.SelectedId;
var (_, typeKey, secondaries) = AvailableVoteTypes[typeId];
var voteType = AvailableVoteOptions[(StandardVoteType)typeId];
if (secondaries != null)
var commandArgs = "";
if (voteType.Dropdowns == null || voteType.Dropdowns.Count == 0)
{
var secondaryId = VoteSecondButton.SelectedId;
var (_, secondKey) = secondaries[secondaryId];
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {typeKey} {secondKey}");
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {((StandardVoteType)typeId).ToString()}");
}
else
{
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {typeKey}");
int i = 0;
foreach(var dropdowns in VoteOptionsButtonContainer.Children)
{
if (dropdowns is OptionButton optionButton && AvailableVoteOptions[(StandardVoteType)typeId].Dropdowns != null)
{
commandArgs += AvailableVoteOptions[(StandardVoteType)typeId].Dropdowns[i].ElementAt(optionButton.SelectedId).Key + " ";
i++;
}
}
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {((StandardVoteType)typeId).ToString()} {commandArgs}");
}
Close();
@@ -104,9 +158,16 @@ namespace Content.Client.Voting.UI
private void UpdateVoteTimeout()
{
var (_, typeKey, _) = AvailableVoteTypes[VoteTypeButton.SelectedId];
var typeKey = (StandardVoteType)VoteTypeButton.SelectedId;
var isAvailable = _voteManager.CanCallStandardVote(typeKey, out var timeout);
CreateButton.Disabled = !isAvailable;
if (typeKey == StandardVoteType.Votekick && !IsAllowedVotekick)
{
CreateButton.Disabled = true;
}
else
{
CreateButton.Disabled = !isAvailable;
}
VoteTypeTimeoutLabel.Visible = !isAvailable;
if (!isAvailable)
@@ -123,29 +184,73 @@ namespace Content.Client.Voting.UI
}
}
private static void VoteSecondSelected(OptionButton.ItemSelectedEventArgs obj)
private static void ButtonSelected(OptionButton.ItemSelectedEventArgs obj)
{
obj.Button.SelectId(obj.Id);
}
private void FollowSelected(Button.ButtonEventArgs obj)
{
if (_followDropdown == null)
return;
if (_followDropdown.SelectedId >= PlayerList.Count)
return;
var netEntity = PlayerList.ElementAt(_followDropdown.SelectedId).Value.Item1;
var msg = new GhostWarpToTargetRequestEvent(netEntity);
_entNetManager.SendSystemNetworkMessage(msg);
}
private void VoteTypeSelected(OptionButton.ItemSelectedEventArgs obj)
{
VoteTypeButton.SelectId(obj.Id);
var (_, _, options) = AvailableVoteTypes[obj.Id];
if (options == null)
VoteNotTrustedLabel.Visible = false;
if ((StandardVoteType)obj.Id == StandardVoteType.Votekick)
{
VoteSecondButton.Visible = false;
}
else
{
VoteSecondButton.Visible = true;
VoteSecondButton.Clear();
for (var i = 0; i < options.Length; i++)
if (!IsAllowedVotekick)
{
var (text, _) = options[i];
VoteSecondButton.AddItem(Loc.GetString(text), i);
VoteNotTrustedLabel.Visible = true;
var updatedDropdownOption = AvailableVoteOptions[StandardVoteType.Votekick];
updatedDropdownOption.Dropdowns = new List<Dictionary<string, string>>();
AvailableVoteOptions[StandardVoteType.Votekick] = updatedDropdownOption;
}
else
{
_votingSystem.RequestVotePlayerList();
}
}
VoteWarningLabel.Visible = AvailableVoteOptions[(StandardVoteType)obj.Id].EnableVoteWarning;
FollowButton.Visible = false;
var voteList = AvailableVoteOptions[(StandardVoteType)obj.Id].Dropdowns;
VoteOptionsButtonContainer.RemoveAllChildren();
if (voteList != null)
{
int i = 0;
foreach (var voteDropdown in voteList)
{
var optionButton = new OptionButton();
int j = 0;
foreach (var (key, value) in voteDropdown)
{
optionButton.AddItem(Loc.GetString(value), j);
j++;
}
VoteOptionsButtonContainer.AddChild(optionButton);
optionButton.Visible = true;
optionButton.OnItemSelected += ButtonSelected;
optionButton.Margin = new Thickness(2, 1);
if (AvailableVoteOptions[(StandardVoteType)obj.Id].FollowDropdownId != null && AvailableVoteOptions[(StandardVoteType)obj.Id].FollowDropdownId == i)
{
_followDropdown = optionButton;
FollowButton.Visible = true;
}
i++;
}
}
}
@@ -168,4 +273,20 @@ namespace Content.Client.Voting.UI
new VoteCallMenu().OpenCentered();
}
}
public record struct CreateVoteOption
{
public string Name;
public List<Dictionary<string, string>> Dropdowns;
public bool EnableVoteWarning;
public int? FollowDropdownId; // If set, this will enable the Follow button and use the dropdown matching the ID as input.
public CreateVoteOption(string name, List<Dictionary<string, string>> dropdowns, bool enableVoteWarning, int? followDropdownId)
{
Name = name;
Dropdowns = dropdowns;
EnableVoteWarning = enableVoteWarning;
FollowDropdownId = followDropdownId;
}
}
}

View File

@@ -1,10 +1,11 @@
<Control xmlns="https://spacestation14.io" MinWidth="300" MaxWidth="500">
<Control xmlns="https://spacestation14.io" MinWidth="300" MaxWidth="500">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Margin="4" Orientation="Vertical">
<Label Name="VoteCaller" />
<RichTextLabel Name="VoteTitle" />
<GridContainer Columns="3" Name="VoteOptionsContainer" />
<Button Margin="4 4" Name="FollowVoteTarget" Text="{Loc 'ui-vote-follow-button-popup'}" Visible="False"></Button>
<GridContainer Columns="3" Name="VoteOptionsContainer"/>
<BoxContainer Orientation="Horizontal">
<ProgressBar Margin="4" HorizontalExpand="True" Name="TimeLeftBar" MinValue="0" MaxValue="1" />
<Label Name="TimeLeftText" />

View File

@@ -1,12 +1,10 @@
using System;
using System;
using Content.Client.Stylesheets;
using Content.Shared.Ghost;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -17,9 +15,11 @@ namespace Content.Client.Voting.UI
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly IEntityNetworkManager _net = default!;
private readonly VoteManager.ActiveVote _vote;
private readonly Button[] _voteButtons;
private readonly NetEntity? _targetEntity;
public VotePopup(VoteManager.ActiveVote vote)
{
@@ -29,6 +29,13 @@ namespace Content.Client.Voting.UI
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
if (_vote.TargetEntity != null && _vote.TargetEntity != 0)
{
_targetEntity = new NetEntity(_vote.TargetEntity.Value);
FollowVoteTarget.Visible = true;
FollowVoteTarget.OnPressed += _ => AttemptFollowVoteEntity();
}
Modulate = Color.White.WithAlpha(0.75f);
_voteButtons = new Button[vote.Entries.Length];
var group = new ButtonGroup();
@@ -55,13 +62,29 @@ namespace Content.Client.Voting.UI
for (var i = 0; i < _voteButtons.Length; i++)
{
var entry = _vote.Entries[i];
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
if (_vote.DisplayVotes)
{
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
}
else
{
_voteButtons[i].Text = Loc.GetString("ui-vote-button-no-votes", ("text", entry.Text));
}
if (_vote.OurVote == i)
_voteButtons[i].Pressed = true;
}
}
private void AttemptFollowVoteEntity()
{
if (_targetEntity != null)
{
var msg = new GhostWarpToTargetRequestEvent(_targetEntity.Value);
_net.SendSystemNetworkMessage(msg);
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
// Logger.Debug($"{_gameTiming.ServerTime}, {_vote.StartTime}, {_vote.EndTime}");

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Voting;
@@ -184,6 +184,8 @@ namespace Content.Client.Voting
existingVote.Title = message.VoteTitle;
existingVote.StartTime = _gameTiming.RealServerToLocal(message.StartTime);
existingVote.EndTime = _gameTiming.RealServerToLocal(message.EndTime);
existingVote.DisplayVotes = message.DisplayVotes;
existingVote.TargetEntity = message.TargetEntity;
// Logger.Debug($"{existingVote.StartTime}, {existingVote.EndTime}, {_gameTiming.RealTime}");
@@ -245,7 +247,8 @@ namespace Content.Client.Voting
public string Initiator = "";
public int? OurVote;
public int Id;
public bool DisplayVotes;
public int? TargetEntity; // NetEntity
public ActiveVote(int voteId)
{
Id = voteId;

View File

@@ -0,0 +1,34 @@
using Content.Client.Ghost;
using Content.Shared.Voting;
namespace Content.Client.Voting;
public sealed class VotingSystem : EntitySystem
{
public event Action<VotePlayerListResponseEvent>? VotePlayerListResponse; //Provides a list of players elligble for vote actions
[Dependency] private readonly GhostSystem _ghostSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<VotePlayerListResponseEvent>(OnVotePlayerListResponseEvent);
}
private void OnVotePlayerListResponseEvent(VotePlayerListResponseEvent msg)
{
if (!_ghostSystem.IsGhost)
{
return;
}
VotePlayerListResponse?.Invoke(msg);
}
public void RequestVotePlayerList()
{
RaiseNetworkEvent(new VotePlayerListRequestEvent());
}
}

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

@@ -39,7 +39,7 @@ public sealed class ComputerConstruction : InteractionTest
await StartDeconstruction(ComputerId);
// Initial interaction turns id computer into generic computer
await InteractUsing(Screw);
await InteractUsing(Pry);
AssertPrototype(ComputerFrame);
// Perform deconstruction steps
@@ -69,7 +69,7 @@ public sealed class ComputerConstruction : InteractionTest
await SpawnTarget(ComputerId);
// Initial interaction turns id computer into generic computer
await InteractUsing(Screw);
await InteractUsing(Pry);
AssertPrototype(ComputerFrame);
// Perform partial deconstruction steps

View File

@@ -32,7 +32,7 @@ namespace Content.Server.Access.Systems
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
{
if (!TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target) || args.Target == null)
if (args.Target == null || !args.CanReach || !TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
return;
if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))

View File

@@ -1,5 +1,7 @@
using System.Linq;
using System.Text;
using Content.Server.Administration.BanList;
using Content.Server.EUI;
using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Server.Player;
@@ -10,6 +12,12 @@ namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
public sealed class RoleBanListCommand : IConsoleCommand
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly IPlayerLocator _locator = default!;
public string Command => "rolebanlist";
public string Description => Loc.GetString("cmd-rolebanlist-desc");
public string Help => Loc.GetString("cmd-rolebanlist-help");
@@ -29,66 +37,37 @@ public sealed class RoleBanListCommand : IConsoleCommand
return;
}
var dbMan = IoCManager.Resolve<IServerDbManager>();
var data = await _locator.LookupIdByNameOrIdAsync(args[0]);
var target = args[0];
var locator = IoCManager.Resolve<IPlayerLocator>();
var located = await locator.LookupIdByNameOrIdAsync(target);
if (located == null)
if (data == null)
{
shell.WriteError("Unable to find a player with that name or id.");
return;
}
var targetUid = located.UserId;
var targetHWid = located.LastHWId;
var targetAddress = located.LastAddress;
var bans = await dbMan.GetServerRoleBansAsync(targetAddress, targetUid, targetHWid, includeUnbanned);
if (bans.Count == 0)
if (shell.Player is not { } player)
{
shell.WriteLine("That user has no bans in their record.");
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastHWId, includeUnbanned);
if (bans.Count == 0)
{
shell.WriteLine("That user has no bans in their record.");
return;
}
foreach (var ban in bans)
{
var msg = $"ID: {ban.Id}: Role: {ban.Role} Reason: {ban.Reason}";
shell.WriteLine(msg);
}
return;
}
var bansString = new StringBuilder("Bans in record:\n");
var ui = new BanListEui();
_eui.OpenEui(ui, player);
await ui.ChangeBanListPlayer(data.UserId);
var first = true;
foreach (var ban in bans)
{
if (!first)
bansString.Append("\n\n");
else
first = false;
bansString
.Append("Ban ID: ")
.Append(ban.Id)
.Append('\n')
.Append("Role: ")
.Append(ban.Role)
.Append('\n')
.Append("Banned on ")
.Append(ban.BanTime);
if (ban.ExpirationTime != null)
{
bansString
.Append(" until ")
.Append(ban.ExpirationTime.Value);
}
bansString
.Append('\n');
bansString
.Append("Reason: ")
.Append(ban.Reason);
}
shell.WriteLine(bansString.ToString());
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)

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

@@ -6,6 +6,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
namespace Content.Server.Atmos.Piping.Unary.Components
{
// The world if people documented their shit.
[AutoGenerateComponentPause]
[RegisterComponent]
public sealed partial class GasVentPumpComponent : Component
{
@@ -15,31 +16,25 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[ViewVariables]
public bool IsDirty { get; set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("inlet")]
[DataField]
public string Inlet { get; set; } = "pipe";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("outlet")]
[DataField]
public string Outlet { get; set; } = "pipe";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("pumpDirection")]
[DataField]
public VentPumpDirection PumpDirection { get; set; } = VentPumpDirection.Releasing;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("pressureChecks")]
[DataField]
public VentPressureBound PressureChecks { get; set; } = VentPressureBound.ExternalBound;
[ViewVariables(VVAccess.ReadOnly)]
[DataField("underPressureLockout")]
[DataField]
public bool UnderPressureLockout { get; set; } = false;
/// <summary>
/// In releasing mode, do not pump when environment pressure is below this limit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("underPressureLockoutThreshold")]
[DataField]
public float UnderPressureLockoutThreshold = 80; // this must be tuned in conjunction with atmos.mmos_spacing_speed
/// <summary>
@@ -55,12 +50,30 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// repressurizing of the development map take about 30 minutes using an oxygen tank (high pressure)
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("underPressureLockoutLeaking")]
[DataField]
public float UnderPressureLockoutLeaking = 0.0001f;
/// <summary>
/// Is the vent pressure lockout currently manually disabled?
/// </summary>
[DataField]
public bool IsPressureLockoutManuallyDisabled = false;
/// <summary>
/// The time when the manual pressure lockout will be reenabled.
/// </summary>
[DataField]
[AutoPausedField]
public TimeSpan ManualLockoutReenabledAt;
/// <summary>
/// How long the lockout should remain manually disabled after being interacted with.
/// </summary>
[DataField]
public TimeSpan ManualLockoutDisabledDuration = TimeSpan.FromSeconds(30); // Enough time to fill a 5x5 room
/// <summary>
/// How long the doAfter should take when attempting to manually disable the pressure lockout.
/// </summary>
public float ManualLockoutDisableDoAfter = 2.0f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("externalPressureBound")]
[DataField]
public float ExternalPressureBound
{
get => _externalPressureBound;
@@ -72,8 +85,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
private float _externalPressureBound = Atmospherics.OneAtmosphere;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("internalPressureBound")]
[DataField]
public float InternalPressureBound
{
get => _internalPressureBound;
@@ -88,8 +100,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// <summary>
/// Max pressure of the target gas (NOT relative to source).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxPressure")]
[DataField]
public float MaxPressure = Atmospherics.MaxOutputPressure;
/// <summary>
@@ -100,8 +111,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// is too high, and the vent is connected to a large pipe-net, then someone can nearly instantly flood a
/// room with gas.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("targetPressureChange")]
[DataField]
public float TargetPressureChange = Atmospherics.OneAtmosphere;
/// <summary>
@@ -111,29 +121,26 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// Vents cannot suck a pipe completely empty, instead pressurizing a section to a max of
/// pipe pressure * PumpPower (in kPa). So a 51 kPa pipe is required for 101 kPA sections at PumpPower 2.0
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("PumpPower")]
[DataField]
public float PumpPower = 2.0f;
#region Machine Linking
/// <summary>
/// Whether or not machine linking is enabled for this component.
/// </summary>
[DataField("canLink")]
[DataField]
public bool CanLink = false;
[DataField("pressurizePort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string PressurizePort = "Pressurize";
[DataField("depressurizePort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string DepressurizePort = "Depressurize";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("pressurizePressure")]
[DataField]
public float PressurizePressure = Atmospherics.OneAtmosphere;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("depressurizePressure")]
[DataField]
public float DepressurizePressure = 0;
// When true, ignore under-pressure lockout. Used to re-fill rooms in air alarm "Fill" mode.

View File

@@ -7,21 +7,22 @@ using Content.Server.DeviceLinking.Systems;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Unary;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Audio;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Power;
using Content.Shared.Tools.Systems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
@@ -35,7 +36,9 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly WeldableSystem _weldable = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
@@ -51,6 +54,8 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
SubscribeLocalEvent<GasVentPumpComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<GasVentPumpComponent, GasAnalyzerScanEvent>(OnAnalyzed);
SubscribeLocalEvent<GasVentPumpComponent, WeldableChangedEvent>(OnWeldChanged);
SubscribeLocalEvent<GasVentPumpComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<GasVentPumpComponent, VentScrewedDoAfterEvent>(OnVentScrewed);
}
private void OnGasVentPumpUpdated(EntityUid uid, GasVentPumpComponent vent, ref AtmosDeviceUpdateEvent args)
@@ -80,11 +85,16 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
return;
}
// If the lockout has expired, disable it.
if (vent.IsPressureLockoutManuallyDisabled && _timing.CurTime >= vent.ManualLockoutReenabledAt)
{
vent.IsPressureLockoutManuallyDisabled = false;
}
var timeDelta = args.dt;
var pressureDelta = timeDelta * vent.TargetPressureChange;
var lockout = (environment.Pressure < vent.UnderPressureLockoutThreshold);
var lockout = (environment.Pressure < vent.UnderPressureLockoutThreshold) && !vent.IsPressureLockoutManuallyDisabled;
if (vent.UnderPressureLockout != lockout) // update visuals only if this changes
{
vent.UnderPressureLockout = lockout;
@@ -115,7 +125,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
var transferMoles = pressureDelta * environment.Volume / (pipe.Air.Temperature * Atmospherics.R);
// Only run if the device is under lockout and not being overriden
if (vent.UnderPressureLockout & !vent.PressureLockoutOverride)
if (vent.UnderPressureLockout & !vent.PressureLockoutOverride & !vent.IsPressureLockoutManuallyDisabled)
{
// Leak only a small amount of gas as a proportion of supply pipe pressure.
var pipeDelta = pipe.Air.Pressure - environment.Pressure;
@@ -273,7 +283,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
else if (vent.PumpDirection == VentPumpDirection.Releasing)
{
if (vent.UnderPressureLockout & !vent.PressureLockoutOverride)
if (vent.UnderPressureLockout & !vent.PressureLockoutOverride & !vent.IsPressureLockoutManuallyDisabled)
_appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Lockout, appearance);
else
_appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Out, appearance);
@@ -290,7 +300,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
if (args.IsInDetailsRange)
{
if (pumpComponent.PumpDirection == VentPumpDirection.Releasing & pumpComponent.UnderPressureLockout & !pumpComponent.PressureLockoutOverride)
if (pumpComponent.PumpDirection == VentPumpDirection.Releasing & pumpComponent.UnderPressureLockout & !pumpComponent.PressureLockoutOverride & !pumpComponent.IsPressureLockoutManuallyDisabled)
{
args.PushMarkup(Loc.GetString("gas-vent-pump-uvlo"));
}
@@ -325,5 +335,25 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
UpdateState(uid, component);
}
private void OnInteractUsing(EntityUid uid, GasVentPumpComponent component, InteractUsingEvent args)
{
if (args.Handled
|| component.UnderPressureLockout == false
|| !_toolSystem.HasQuality(args.Used, "Screwing")
|| !Transform(uid).Anchored
)
{
return;
}
args.Handled = true;
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.ManualLockoutDisableDoAfter, new VentScrewedDoAfterEvent(), uid, uid, args.Used));
}
private void OnVentScrewed(EntityUid uid, GasVentPumpComponent component, VentScrewedDoAfterEvent args)
{
component.ManualLockoutReenabledAt = _timing.CurTime + component.ManualLockoutDisabledDuration;
component.IsPressureLockoutManuallyDisabled = true;
}
}
}

View File

@@ -239,6 +239,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
Loc.GetString(
"earlyleave-cryo-announcement",
("character", name),
("entity", ent.Owner),
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))
), Loc.GetString("earlyleave-cryo-sender"),
playDefaultSound: false

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,9 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.GPS;
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class AstroNavCartridgeComponent : Component
{
}

View File

@@ -0,0 +1,32 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.GPS.Components;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class AstroNavCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AstroNavCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded);
SubscribeLocalEvent<AstroNavCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
}
private void OnCartridgeAdded(Entity<AstroNavCartridgeComponent> ent, ref CartridgeAddedEvent args)
{
EnsureComp<HandheldGPSComponent>(args.Loader);
}
private void OnCartridgeRemoved(Entity<AstroNavCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
// only remove when the program itself is removed
if (!_cartridgeLoaderSystem.HasProgram<AstroNavCartridgeComponent>(args.Loader))
{
RemComp<HandheldGPSComponent>(args.Loader);
}
}
}

View File

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

View File

@@ -4,7 +4,6 @@ using System.Text;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.Examine;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Server.Speech.Components;
@@ -18,13 +17,10 @@ using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Players;
using Content.Shared.Radio;
using Content.Shared.Speech;
using Content.Shared.Whitelist;
using Robust.Server.Player;
using Robust.Shared.Audio;
@@ -122,6 +118,7 @@ public sealed partial class ChatSystem : SharedChatSystem
_configurationManager.SetCVar(CCVars.OocEnabled, false);
break;
case GameRunLevel.PostRound:
case GameRunLevel.PreRoundLobby:
if (!_configurationManager.GetCVar(CCVars.OocEnableDuringRound))
_configurationManager.SetCVar(CCVars.OocEnabled, true);
break;
@@ -439,9 +436,9 @@ public sealed partial class ChatSystem : SharedChatSystem
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.Name;
name = nameEv.VoiceName;
// Check for a speech verb override
if (nameEv.SpeechVerb != null && _prototypeManager.TryIndex<SpeechVerbPrototype>(nameEv.SpeechVerb, out var proto))
if (nameEv.SpeechVerb != null && _prototypeManager.TryIndex(nameEv.SpeechVerb, out var proto))
speech = proto;
}
@@ -513,7 +510,7 @@ public sealed partial class ChatSystem : SharedChatSystem
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.Name;
name = nameEv.VoiceName;
}
name = FormattedMessage.EscapeText(name);
@@ -910,20 +907,6 @@ public record ExpandICChatRecipientsEvent(EntityUid Source, float VoiceRange, Di
{
}
public sealed class TransformSpeakerNameEvent : EntityEventArgs
{
public EntityUid Sender;
public string Name;
public string? SpeechVerb;
public TransformSpeakerNameEvent(EntityUid sender, string name, string? speechVerb = null)
{
Sender = sender;
Name = name;
SpeechVerb = speechVerb;
}
}
/// <summary>
/// Raised broadcast in order to transform speech.transmit
/// </summary>

View File

@@ -14,31 +14,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

@@ -52,7 +52,7 @@ namespace Content.Server.Database
.ThenInclude(h => h.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSingleQuery()
.AsSplitQuery()
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
if (prefs is null)

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

@@ -320,9 +320,10 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
return;
}
if (component.Engaged && !TryFlush(uid, component))
if (component.Engaged)
{
QueueAutomaticEngage(uid, component);
// Run ManualEngage to recalculate a new flush time
ManualEngage(uid, component);
}
}

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

@@ -1,189 +0,0 @@
using System.Linq;
using Content.Server.Body.Systems;
using Content.Shared.Alert;
using Content.Shared.Body.Part;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Ensnaring;
using Content.Shared.Ensnaring.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.StepTrigger.Systems;
using Content.Shared.Throwing;
namespace Content.Server.Ensnaring;
public sealed partial class EnsnareableSystem
{
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
public void InitializeEnsnaring()
{
SubscribeLocalEvent<EnsnaringComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<EnsnaringComponent, StepTriggerAttemptEvent>(AttemptStepTrigger);
SubscribeLocalEvent<EnsnaringComponent, StepTriggeredOffEvent>(OnStepTrigger);
SubscribeLocalEvent<EnsnaringComponent, ThrowDoHitEvent>(OnThrowHit);
SubscribeLocalEvent<EnsnaringComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
SubscribeLocalEvent<EnsnareableComponent, RemoveEnsnareAlertEvent>(OnRemoveEnsnareAlert);
}
private void OnAttemptPacifiedThrow(Entity<EnsnaringComponent> ent, ref AttemptPacifiedThrowEvent args)
{
args.Cancel("pacified-cannot-throw-snare");
}
private void OnRemoveEnsnareAlert(Entity<EnsnareableComponent> ent, ref RemoveEnsnareAlertEvent args)
{
if (args.Handled)
return;
foreach (var ensnare in ent.Comp.Container.ContainedEntities)
{
if (!TryComp<EnsnaringComponent>(ensnare, out var ensnaringComponent))
return;
TryFree(ent, ent, ensnare, ensnaringComponent);
args.Handled = true;
// Only one snare at a time.
break;
}
}
private void OnComponentRemove(EntityUid uid, EnsnaringComponent component, ComponentRemove args)
{
if (!TryComp<EnsnareableComponent>(component.Ensnared, out var ensnared))
return;
if (ensnared.IsEnsnared)
ForceFree(uid, component);
}
private void AttemptStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggerAttemptEvent args)
{
args.Continue = true;
}
private void OnStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggeredOffEvent args)
{
TryEnsnare(args.Tripper, uid, component);
}
private void OnThrowHit(EntityUid uid, EnsnaringComponent component, ThrowDoHitEvent args)
{
if (!component.CanThrowTrigger)
return;
TryEnsnare(args.Target, uid, component);
}
/// <summary>
/// Used where you want to try to ensnare an entity with the <see cref="EnsnareableComponent"/>
/// </summary>
/// <param name="target">The entity that will be ensnared</param>
/// <paramref name="ensnare"> The entity that is used to ensnare</param>
/// <param name="component">The ensnaring component</param>
public void TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent component)
{
//Don't do anything if they don't have the ensnareable component.
if (!TryComp<EnsnareableComponent>(target, out var ensnareable))
return;
var legs = _body.GetBodyChildrenOfType(target, BodyPartType.Leg).Count();
var ensnaredLegs = (2 * ensnareable.Container.ContainedEntities.Count);
var freeLegs = legs - ensnaredLegs;
if (freeLegs <= 0)
return;
// Apply stamina damage to target if they weren't ensnared before.
if (ensnareable.IsEnsnared != true)
{
if (TryComp<StaminaComponent>(target, out var stamina))
{
_stamina.TakeStaminaDamage(target, component.StaminaDamage, with: ensnare);
}
}
component.Ensnared = target;
_container.Insert(ensnare, ensnareable.Container);
ensnareable.IsEnsnared = true;
Dirty(target, ensnareable);
UpdateAlert(target, ensnareable);
var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed);
RaiseLocalEvent(target, ev);
}
/// <summary>
/// Used where you want to try to free an entity with the <see cref="EnsnareableComponent"/>
/// </summary>
/// <param name="target">The entity that will be freed</param>
/// <param name="user">The entity that is freeing the target</param>
/// <param name="ensnare">The entity used to ensnare</param>
/// <param name="component">The ensnaring component</param>
public void TryFree(EntityUid target, EntityUid user, EntityUid ensnare, EnsnaringComponent component)
{
// Don't do anything if they don't have the ensnareable component.
if (!HasComp<EnsnareableComponent>(target))
return;
var freeTime = user == target ? component.BreakoutTime : component.FreeTime;
var breakOnMove = !component.CanMoveBreakout;
var doAfterEventArgs = new DoAfterArgs(EntityManager, user, freeTime, new EnsnareableDoAfterEvent(), target, target: target, used: ensnare)
{
BreakOnMove = breakOnMove,
BreakOnDamage = false,
NeedHand = true,
BreakOnDropItem = false,
};
if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
return;
if (user == target)
_popup.PopupEntity(Loc.GetString("ensnare-component-try-free", ("ensnare", ensnare)), target, target);
else
_popup.PopupEntity(Loc.GetString("ensnare-component-try-free-other", ("ensnare", ensnare), ("user", Identity.Entity(target, EntityManager))), user, user);
}
/// <summary>
/// Used to force free someone for things like if the <see cref="EnsnaringComponent"/> is removed
/// </summary>
public void ForceFree(EntityUid ensnare, EnsnaringComponent component)
{
if (component.Ensnared == null)
return;
if (!TryComp<EnsnareableComponent>(component.Ensnared, out var ensnareable))
return;
var target = component.Ensnared.Value;
_container.Remove(ensnare, ensnareable.Container, force: true);
ensnareable.IsEnsnared = ensnareable.Container.ContainedEntities.Count > 0;
Dirty(component.Ensnared.Value, ensnareable);
component.Ensnared = null;
UpdateAlert(target, ensnareable);
var ev = new EnsnareRemoveEvent(component.WalkSpeed, component.SprintSpeed);
RaiseLocalEvent(ensnare, ev);
}
/// <summary>
/// Update the Ensnared alert for an entity.
/// </summary>
/// <param name="target">The entity that has been affected by a snare</param>
public void UpdateAlert(EntityUid target, EnsnareableComponent component)
{
if (!component.IsEnsnared)
_alerts.ClearAlert(target, component.EnsnaredAlert);
else
_alerts.ShowAlert(target, component.EnsnaredAlert);
}
}

View File

@@ -1,61 +1,5 @@
using Content.Server.Popups;
using Content.Shared.DoAfter;
using Content.Shared.Ensnaring;
using Content.Shared.Ensnaring.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Popups;
using Robust.Server.Containers;
using Robust.Shared.Containers;
namespace Content.Server.Ensnaring;
public sealed partial class EnsnareableSystem : SharedEnsnareableSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly PopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
InitializeEnsnaring();
SubscribeLocalEvent<EnsnareableComponent, ComponentInit>(OnEnsnareableInit);
SubscribeLocalEvent<EnsnareableComponent, EnsnareableDoAfterEvent>(OnDoAfter);
}
private void OnEnsnareableInit(EntityUid uid, EnsnareableComponent component, ComponentInit args)
{
component.Container = _container.EnsureContainer<Container>(uid, "ensnare");
}
private void OnDoAfter(EntityUid uid, EnsnareableComponent component, DoAfterEvent args)
{
if (args.Args.Target == null)
return;
if (args.Handled || !TryComp<EnsnaringComponent>(args.Args.Used, out var ensnaring))
return;
if (args.Cancelled || !_container.Remove(args.Args.Used.Value, component.Container))
{
_popup.PopupEntity(Loc.GetString("ensnare-component-try-free-fail", ("ensnare", args.Args.Used)), uid, uid, PopupType.MediumCaution);
return;
}
component.IsEnsnared = component.Container.ContainedEntities.Count > 0;
Dirty(uid, component);
ensnaring.Ensnared = null;
_hands.PickupOrDrop(args.Args.User, args.Args.Used.Value);
_popup.PopupEntity(Loc.GetString("ensnare-component-try-free-complete", ("ensnare", args.Args.Used)), uid, uid, PopupType.Medium);
UpdateAlert(args.Args.Target.Value, component);
var ev = new EnsnareRemoveEvent(ensnaring.WalkSpeed, ensnaring.SprintSpeed);
RaiseLocalEvent(uid, ev);
args.Handled = true;
}
}
public sealed class EnsnareableSystem : SharedEnsnareableSystem;

View File

@@ -14,7 +14,6 @@ namespace Content.Server.Entry
"GuideHelp",
"Clickable",
"Icon",
"HandheldGPS",
"CableVisualizer",
"SolutionItemStatus",
"UIFragment",

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, args.OtherEntity); // CP14 Other Entity user
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

@@ -11,6 +11,7 @@ using Content.Shared.Storage;
using Content.Shared.Verbs;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Kitchen;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@@ -72,12 +73,17 @@ public sealed class SharpSystem : EntitySystem
if (!sharp.Butchering.Add(target))
return false;
// if the user isn't the entity with the sharp component,
// they will need to be holding something with their hands, so we set needHand to true
// so that the doafter can be interrupted if they drop the item in their hands
var needHand = user != knife;
var doAfter =
new DoAfterArgs(EntityManager, user, sharp.ButcherDelayModifier * butcher.ButcherDelay, new SharpDoAfterEvent(), knife, target: target, used: knife)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
NeedHand = needHand,
};
_doAfterSystem.TryStartDoAfter(doAfter);
return true;
@@ -136,13 +142,20 @@ public sealed class SharpSystem : EntitySystem
private void OnGetInteractionVerbs(EntityUid uid, ButcherableComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (component.Type != ButcheringType.Knife || args.Hands == null || !args.CanAccess || !args.CanInteract)
if (component.Type != ButcheringType.Knife || !args.CanAccess || !args.CanInteract)
return;
bool disabled = false;
// if the user has no hands, don't show them the verb if they have no SharpComponent either
if (!TryComp<SharpComponent>(args.User, out var userSharpComp) && args.Hands == null)
return;
var disabled = false;
string? message = null;
if (!HasComp<SharpComponent>(args.Using))
// if the user has hands
// and the item they're holding doesn't have the SharpComponent
// disable the verb
if (!TryComp<SharpComponent>(args.Using, out var usingSharpComp) && args.Hands != null)
{
disabled = true;
message = Loc.GetString("butcherable-need-knife",
@@ -150,9 +163,9 @@ public sealed class SharpSystem : EntitySystem
}
else if (_containerSystem.IsEntityInContainer(uid))
{
disabled = true;
message = Loc.GetString("butcherable-not-in-container",
("target", uid));
disabled = true;
}
else if (TryComp<MobStateComponent>(uid, out var state) && !_mobStateSystem.IsDead(uid, state))
{
@@ -160,12 +173,20 @@ public sealed class SharpSystem : EntitySystem
message = Loc.GetString("butcherable-mob-isnt-dead");
}
// set the object doing the butchering to the item in the user's hands or to the user themselves
// if either has the SharpComponent
EntityUid sharpObject = default;
if (usingSharpComp != null)
sharpObject = args.Using!.Value;
else if (userSharpComp != null)
sharpObject = args.User;
InteractionVerb verb = new()
{
Act = () =>
{
if (!disabled)
TryStartButcherDoafter(args.Using!.Value, args.Target, args.User);
TryStartButcherDoafter(sharpObject, args.Target, args.User);
},
Message = message,
Disabled = disabled,

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