Merge remote-tracking branch 'upstream/master' into ed-27-09-2024-upstream
# Conflicts: # Content.Server/Explosion/EntitySystems/TriggerSystem.cs
This commit is contained in:
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
50
Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs
Normal file
50
Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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++;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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))
|
||||
|
||||
30
Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs
Normal file
30
Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -2,7 +2,4 @@ using Content.Shared.Explosion.EntitySystems;
|
||||
|
||||
namespace Content.Client.Explosion.EntitySystems;
|
||||
|
||||
public sealed class ExplosionSystem : SharedExplosionSystem
|
||||
{
|
||||
|
||||
}
|
||||
public sealed class ExplosionSystem : SharedExplosionSystem;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using Content.Shared.GPS;
|
||||
|
||||
namespace Content.Client.GPS.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class HandheldGPSComponent : SharedHandheldGPSComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Client.GPS.Components;
|
||||
using Content.Shared.GPS.Components;
|
||||
using Content.Client.GPS.UI;
|
||||
using Content.Client.Items;
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml
Normal file
19
Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml
Normal 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>
|
||||
50
Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs
Normal file
50
Content.Client/Pinpointer/UI/StationMapBeaconControl.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 += _ =>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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'}" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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;
|
||||
|
||||
34
Content.Client/Voting/VotingSystem.cs
Normal file
34
Content.Client/Voting/VotingSystem.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
108
Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs
Normal file
108
Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
236
Content.Server/Anomaly/Effects/InnerBodyAnomalySystem.cs
Normal file
236
Content.Server/Anomaly/Effects/InnerBodyAnomalySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Security;
|
||||
|
||||
namespace Content.Server.CartridgeLoader.Cartridges;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class WantedListCartridgeComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Shared.Explosion.Components;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Destructible.Thresholds.Behaviors
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace Content.Server.Entry
|
||||
"GuideHelp",
|
||||
"Clickable",
|
||||
"Icon",
|
||||
"HandheldGPS",
|
||||
"CableVisualizer",
|
||||
"SolutionItemStatus",
|
||||
"UIFragment",
|
||||
|
||||
@@ -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!;
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user