Merge remote-tracking branch 'upstream/stable' into ed-12-05-2025-upstream
# Conflicts: # .github/CODEOWNERS # Content.Client/Construction/UI/ConstructionMenuPresenter.cs # Content.Shared/Construction/Prototypes/ConstructionPrototype.cs # Content.Shared/Damage/Systems/SharedStaminaSystem.cs # Content.Shared/Lock/LockSystem.cs # Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml # Resources/Prototypes/Entities/Objects/Specific/chemistry.yml # Resources/Prototypes/Procedural/vgroid.yml
This commit is contained in:
1
.github/workflows/publish-testing.yml
vendored
1
.github/workflows/publish-testing.yml
vendored
@@ -2,6 +2,7 @@
|
||||
|
||||
concurrency:
|
||||
group: publish-testing
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
3
.github/workflows/publish.yml
vendored
3
.github/workflows/publish.yml
vendored
@@ -2,6 +2,7 @@ name: Publish
|
||||
|
||||
concurrency:
|
||||
group: publish
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -48,12 +49,14 @@ jobs:
|
||||
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
|
||||
|
||||
- name: Publish changelog (Discord)
|
||||
continue-on-error: true
|
||||
run: Tools/actions_changelogs_since_last_run.py
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
|
||||
|
||||
- name: Publish changelog (RSS)
|
||||
continue-on-error: true
|
||||
run: Tools/actions_changelog_rss.py
|
||||
env:
|
||||
CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}
|
||||
|
||||
@@ -5,6 +5,7 @@ import subprocess
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
@@ -104,7 +105,21 @@ def reset_solution():
|
||||
with SOLUTION_PATH.open("w") as f:
|
||||
f.write(content)
|
||||
|
||||
def check_for_zip_download():
|
||||
# Check if .git exists,
|
||||
cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
if not os.path.isdir(os.path.join(cur_dir, ".git")):
|
||||
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
|
||||
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
|
||||
"Such as information to download the engine or even the ability to even be able to create contributions. \n"
|
||||
"Please read and follow https://docs.spacestation14.com/en/general-development/setup/setting-up-a-development-environment.html \n"
|
||||
"If you just want a Sandbox Server, you are following the wrong guide! You can download a premade server following the instructions here:"
|
||||
"https://docs.spacestation14.com/en/general-development/setup/server-hosting-tutorial.html \n"
|
||||
"Closing automatically in 30 seconds.")
|
||||
time.sleep(30)
|
||||
exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_for_zip_download()
|
||||
install_hooks()
|
||||
update_submodules()
|
||||
|
||||
@@ -202,6 +202,7 @@ namespace Content.Client.Actions
|
||||
return;
|
||||
|
||||
OnActionAdded?.Invoke(actionId);
|
||||
ActionsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
|
||||
@@ -210,6 +211,7 @@ namespace Content.Client.Actions
|
||||
return;
|
||||
|
||||
OnActionRemoved?.Invoke(actionId);
|
||||
ActionsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using Content.Shared.Alert;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -15,6 +16,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
|
||||
public event EventHandler? ClearAlerts;
|
||||
public event EventHandler<IReadOnlyDictionary<AlertKey, AlertState>>? SyncAlerts;
|
||||
@@ -27,6 +29,12 @@ public sealed class ClientAlertsSystem : AlertsSystem
|
||||
SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
protected override void HandledAlert()
|
||||
{
|
||||
_ui.ClickSound();
|
||||
}
|
||||
|
||||
protected override void LoadPrototypes()
|
||||
{
|
||||
base.LoadPrototypes();
|
||||
@@ -52,8 +60,24 @@ public sealed class ClientAlertsSystem : AlertsSystem
|
||||
if (args.Current is not AlertComponentState cast)
|
||||
return;
|
||||
|
||||
// Save all client-sided alerts to later put back in
|
||||
var clientAlerts = new Dictionary<AlertKey, AlertState>();
|
||||
foreach (var alert in alerts.Comp.Alerts)
|
||||
{
|
||||
if (alert.Key.AlertType != null && TryGet(alert.Key.AlertType.Value, out var alertProto))
|
||||
{
|
||||
if (alertProto.ClientHandled)
|
||||
clientAlerts[alert.Key] = alert.Value;
|
||||
}
|
||||
}
|
||||
|
||||
alerts.Comp.Alerts = new(cast.Alerts);
|
||||
|
||||
foreach (var alert in clientAlerts)
|
||||
{
|
||||
alerts.Comp.Alerts[alert.Key] = alert.Value;
|
||||
}
|
||||
|
||||
UpdateHud(alerts);
|
||||
}
|
||||
|
||||
|
||||
29
Content.Client/Atmos/EntitySystems/GasTankSystem.cs
Normal file
29
Content.Client/Atmos/EntitySystems/GasTankSystem.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed class GasTankSystem : SharedGasTankSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GasTankComponent, AfterAutoHandleStateEvent>(OnGasTankState);
|
||||
}
|
||||
|
||||
private void OnGasTankState(Entity<GasTankComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
|
||||
{
|
||||
bui.Update<GasTankBoundUserInterfaceState>();
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateUserInterface(Entity<GasTankComponent> ent)
|
||||
{
|
||||
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
|
||||
{
|
||||
bui.Update<GasTankBoundUserInterfaceState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Content.Client.Atmos.UI;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary.Systems;
|
||||
using Content.Shared.NodeContainer;
|
||||
|
||||
namespace Content.Client.Atmos.Piping.Unary.Systems;
|
||||
|
||||
public sealed class GasCanisterSystem : SharedGasCanisterSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GasCanisterComponent, AfterAutoHandleStateEvent>(OnGasState);
|
||||
}
|
||||
|
||||
private void OnGasState(Entity<GasCanisterComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(ent.Owner, GasCanisterUiKey.Key, out var bui))
|
||||
{
|
||||
bui.Update<GasCanisterBoundUserInterfaceState>();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null)
|
||||
{
|
||||
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(uid, GasCanisterUiKey.Key, out var bui))
|
||||
{
|
||||
bui.Update<GasCanisterBoundUserInterfaceState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
@@ -32,22 +35,22 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
private void OnTankEjectPressed()
|
||||
{
|
||||
SendMessage(new GasCanisterHoldingTankEjectMessage());
|
||||
SendPredictedMessage(new GasCanisterHoldingTankEjectMessage());
|
||||
}
|
||||
|
||||
private void OnReleasePressureSet(float value)
|
||||
{
|
||||
SendMessage(new GasCanisterChangeReleasePressureMessage(value));
|
||||
SendPredictedMessage(new GasCanisterChangeReleasePressureMessage(value));
|
||||
}
|
||||
|
||||
private void OnReleaseValveOpenPressed()
|
||||
{
|
||||
SendMessage(new GasCanisterChangeReleaseValveMessage(true));
|
||||
SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(true));
|
||||
}
|
||||
|
||||
private void OnReleaseValveClosePressed()
|
||||
{
|
||||
SendMessage(new GasCanisterChangeReleaseValveMessage(false));
|
||||
SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,17 +60,21 @@ namespace Content.Client.Atmos.UI
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast)
|
||||
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast || !EntMan.TryGetComponent(Owner, out GasCanisterComponent? component))
|
||||
return;
|
||||
|
||||
_window.SetCanisterLabel(cast.CanisterLabel);
|
||||
var canisterLabel = Identity.Name(Owner, EntMan);
|
||||
var tankLabel = component.GasTankSlot.Item != null ? Identity.Name(component.GasTankSlot.Item.Value, EntMan) : null;
|
||||
|
||||
_window.SetCanisterLabel(canisterLabel);
|
||||
_window.SetCanisterPressure(cast.CanisterPressure);
|
||||
_window.SetPortStatus(cast.PortStatus);
|
||||
_window.SetTankLabel(cast.TankLabel);
|
||||
|
||||
_window.SetTankLabel(tankLabel);
|
||||
_window.SetTankPressure(cast.TankPressure);
|
||||
_window.SetReleasePressureRange(cast.ReleasePressureMin, cast.ReleasePressureMax);
|
||||
_window.SetReleasePressure(cast.ReleasePressure);
|
||||
_window.SetReleaseValve(cast.ReleaseValve);
|
||||
_window.SetReleasePressureRange(component.MinReleasePressure, component.MaxReleasePressure);
|
||||
_window.SetReleasePressure(component.ReleasePressure);
|
||||
_window.SetReleaseValve(component.ReleaseValve);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -13,9 +13,6 @@ namespace Content.Client.Atmos.UI;
|
||||
[UsedImplicitly]
|
||||
public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
[ViewVariables]
|
||||
private const float MaxPressure = Atmospherics.MaxOutputPressure;
|
||||
|
||||
[ViewVariables]
|
||||
private GasPressurePumpWindow? _window;
|
||||
|
||||
|
||||
24
Content.Client/Body/Systems/InternalsSystem.cs
Normal file
24
Content.Client/Body/Systems/InternalsSystem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
|
||||
namespace Content.Client.Body.Systems;
|
||||
|
||||
public sealed class InternalsSystem : SharedInternalsSystem
|
||||
{
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<InternalsComponent, AfterAutoHandleStateEvent>(OnInternalsAfterState);
|
||||
}
|
||||
|
||||
private void OnInternalsAfterState(Entity<InternalsComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (ent.Comp.GasTankEntity != null && _ui.TryGetOpenUi(ent.Comp.GasTankEntity.Value, SharedGasTankUiKey.Key, out var bui))
|
||||
{
|
||||
bui.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,10 @@
|
||||
<!-- Products get added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<Control MinHeight="5" Name="OrdersSpacer"/>
|
||||
<PanelContainer VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="1">
|
||||
SizeFlagsStretchRatio="1"
|
||||
Name="Orders">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
@@ -208,6 +208,7 @@ namespace Content.Client.Cargo.UI
|
||||
|
||||
var product = _protoManager.Index<EntityPrototype>(order.ProductId);
|
||||
var productName = product.Name;
|
||||
var account = _protoManager.Index(order.Account);
|
||||
|
||||
var row = new CargoOrderRow
|
||||
{
|
||||
@@ -219,7 +220,9 @@ namespace Content.Client.Cargo.UI
|
||||
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
|
||||
("productName", productName),
|
||||
("orderAmount", order.OrderQuantity),
|
||||
("orderRequester", order.Requester))
|
||||
("orderRequester", order.Requester),
|
||||
("accountColor", account.Color),
|
||||
("account", Loc.GetString(account.Code)))
|
||||
},
|
||||
Description =
|
||||
{
|
||||
@@ -282,6 +285,9 @@ namespace Content.Client.Cargo.UI
|
||||
AccountActionButton.Disabled = TransferSpinBox.Value <= 0 ||
|
||||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
|
||||
_timing.CurTime < orderConsole.NextAccountActionTime;
|
||||
|
||||
OrdersSpacer.Visible = !orderConsole.SlipPrinter;
|
||||
Orders.Visible = !orderConsole.SlipPrinter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<Label Name="ProductName"
|
||||
<RichTextLabel Name="ProductName"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
StyleClasses="LabelSubText" />
|
||||
<Label Name="Description"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace Content.Client.Cargo.UI
|
||||
{
|
||||
var product = protoManager.Index<EntityPrototype>(order.ProductId);
|
||||
var productName = product.Name;
|
||||
var account = protoManager.Index(order.Account);
|
||||
|
||||
var row = new CargoOrderRow
|
||||
{
|
||||
@@ -47,7 +48,9 @@ namespace Content.Client.Cargo.UI
|
||||
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
|
||||
("productName", productName),
|
||||
("orderAmount", order.OrderQuantity - order.NumDispatched),
|
||||
("orderRequester", order.Requester))
|
||||
("orderRequester", order.Requester),
|
||||
("accountColor", account.Color),
|
||||
("account", Loc.GetString(account.Code)))
|
||||
},
|
||||
Description = {Text = Loc.GetString("cargo-console-menu-order-reason-description",
|
||||
("reason", order.Reason))}
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
|
||||
private readonly TimeSpan _typingTimeout = TimeSpan.FromSeconds(2);
|
||||
private TimeSpan _lastTextChange;
|
||||
private bool _isClientTyping;
|
||||
private bool _isClientChatFocused;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -31,7 +32,8 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
|
||||
return;
|
||||
|
||||
// client typed something - show typing indicator
|
||||
ClientUpdateTyping(true);
|
||||
_isClientTyping = true;
|
||||
ClientUpdateTyping();
|
||||
_lastTextChange = _time.CurTime;
|
||||
}
|
||||
|
||||
@@ -42,7 +44,19 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
|
||||
return;
|
||||
|
||||
// client submitted text - hide typing indicator
|
||||
ClientUpdateTyping(false);
|
||||
_isClientTyping = false;
|
||||
ClientUpdateTyping();
|
||||
}
|
||||
|
||||
public void ClientChangedChatFocus(bool isFocused)
|
||||
{
|
||||
// don't update it if player don't want to show typing
|
||||
if (!_cfg.GetCVar(CCVars.ChatShowTypingIndicator))
|
||||
return;
|
||||
|
||||
// client submitted text - hide typing indicator
|
||||
_isClientChatFocused = isFocused;
|
||||
ClientUpdateTyping();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -55,23 +69,25 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
|
||||
var dif = _time.CurTime - _lastTextChange;
|
||||
if (dif > _typingTimeout)
|
||||
{
|
||||
// client didn't typed anything for a long time - hide indicator
|
||||
ClientUpdateTyping(false);
|
||||
// client didn't typed anything for a long time - change indicator
|
||||
_isClientTyping = false;
|
||||
ClientUpdateTyping();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClientUpdateTyping(bool isClientTyping)
|
||||
private void ClientUpdateTyping()
|
||||
{
|
||||
if (_isClientTyping == isClientTyping)
|
||||
return;
|
||||
|
||||
// check if player controls any entity.
|
||||
// check if player controls any pawn
|
||||
if (_playerManager.LocalEntity == null)
|
||||
return;
|
||||
|
||||
_isClientTyping = isClientTyping;
|
||||
RaisePredictiveEvent(new TypingChangedEvent(isClientTyping));
|
||||
var state = TypingIndicatorState.None;
|
||||
if (_isClientChatFocused)
|
||||
state = _isClientTyping ? TypingIndicatorState.Typing : TypingIndicatorState.Idle;
|
||||
|
||||
// send a networked event to server
|
||||
RaisePredictiveEvent(new TypingChangedEvent(state));
|
||||
}
|
||||
|
||||
private void OnShowTypingChanged(bool showTyping)
|
||||
@@ -79,7 +95,8 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
|
||||
// hide typing indicator immediately if player don't want to show it anymore
|
||||
if (!showTyping)
|
||||
{
|
||||
ClientUpdateTyping(false);
|
||||
_isClientTyping = false;
|
||||
ClientUpdateTyping();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
|
||||
return;
|
||||
}
|
||||
|
||||
AppearanceSystem.TryGetData<bool>(uid, TypingIndicatorVisuals.IsTyping, out var isTyping, args.Component);
|
||||
var layerExists = args.Sprite.LayerMapTryGet(TypingIndicatorLayers.Base, out var layer);
|
||||
if (!layerExists)
|
||||
layer = args.Sprite.LayerMapReserveBlank(TypingIndicatorLayers.Base);
|
||||
@@ -44,6 +43,17 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
|
||||
args.Sprite.LayerSetState(layer, proto.TypingState);
|
||||
args.Sprite.LayerSetShader(layer, proto.Shader);
|
||||
args.Sprite.LayerSetOffset(layer, proto.Offset);
|
||||
args.Sprite.LayerSetVisible(layer, isTyping);
|
||||
|
||||
AppearanceSystem.TryGetData<TypingIndicatorState>(uid, TypingIndicatorVisuals.State, out var state);
|
||||
args.Sprite.LayerSetVisible(layer, state != TypingIndicatorState.None);
|
||||
switch (state)
|
||||
{
|
||||
case TypingIndicatorState.Idle:
|
||||
args.Sprite.LayerSetState(layer, proto.IdleState);
|
||||
break;
|
||||
case TypingIndicatorState.Typing:
|
||||
args.Sprite.LayerSetState(layer, proto.TypingState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,10 @@ namespace Content.Client.Chemistry.UI
|
||||
("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
|
||||
("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
|
||||
("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
|
||||
("15", ChemMasterReagentAmount.U15, StyleBase.ButtonOpenBoth),
|
||||
("20", ChemMasterReagentAmount.U20, StyleBase.ButtonOpenBoth),
|
||||
("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
|
||||
("30", ChemMasterReagentAmount.U30, StyleBase.ButtonOpenBoth),
|
||||
("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
|
||||
("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
|
||||
(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class HyposprayStatusControl : Control
|
||||
PrevMaxVolume = solution.MaxVolume;
|
||||
PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
|
||||
|
||||
var modeStringLocalized = Loc.GetString(_parent.Comp.OnlyAffectsMobs switch
|
||||
var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
|
||||
{
|
||||
false => "hypospray-all-mode-text",
|
||||
true => "hypospray-mobs-only-mode-text",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Client.Guidebook.Components;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using JetBrains.Annotations;
|
||||
@@ -31,8 +32,7 @@ namespace Content.Client.Chemistry.UI
|
||||
|
||||
// Setup window layout/elements
|
||||
_window = this.CreateWindow<ReagentDispenserWindow>();
|
||||
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
_window.HelpGuidebookIds = EntMan.GetComponent<GuideHelpComponent>(Owner).Guides;
|
||||
_window.SetInfoFromEntity(EntMan, Owner);
|
||||
|
||||
// Setup static button actions.
|
||||
_window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName));
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Construction
|
||||
{
|
||||
@@ -45,7 +47,14 @@ namespace Content.Client.Construction
|
||||
public override void StartHijack(PlacementManager manager)
|
||||
{
|
||||
base.StartHijack(manager);
|
||||
manager.CurrentTextures = _prototype?.Layers.Select(sprite => sprite.DirFrame0()).ToList();
|
||||
|
||||
if (_prototype is null || !_constructionSystem.TryGetRecipePrototype(_prototype.ID, out var targetProtoId))
|
||||
return;
|
||||
|
||||
if (!IoCManager.Resolve<IPrototypeManager>().TryIndex(targetProtoId, out EntityPrototype? proto))
|
||||
return;
|
||||
|
||||
manager.CurrentTextures = SpriteComponent.GetPrototypeTextures(proto, IoCManager.Resolve<IResourceCache>()).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Client.Popups;
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Construction.Steps;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Wall;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -15,6 +14,7 @@ using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Construction
|
||||
{
|
||||
@@ -25,7 +25,6 @@ namespace Content.Client.Construction
|
||||
public sealed class ConstructionSystem : SharedConstructionSystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
@@ -33,6 +32,8 @@ namespace Content.Client.Construction
|
||||
private readonly Dictionary<int, EntityUid> _ghosts = new();
|
||||
private readonly Dictionary<string, ConstructionGuide> _guideCache = new();
|
||||
|
||||
private readonly Dictionary<string, string> _recipesMetadataCache = [];
|
||||
|
||||
public bool CraftingEnabled { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -40,6 +41,8 @@ namespace Content.Client.Construction
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
WarmupRecipesCache();
|
||||
|
||||
UpdatesOutsidePrediction = true;
|
||||
SubscribeLocalEvent<LocalPlayerAttachedEvent>(HandlePlayerAttached);
|
||||
SubscribeNetworkEvent<AckStructureConstructionMessage>(HandleAckStructure);
|
||||
@@ -63,6 +66,77 @@ namespace Content.Client.Construction
|
||||
ClearGhost(component.GhostId);
|
||||
}
|
||||
|
||||
public bool TryGetRecipePrototype(string constructionProtoId, [NotNullWhen(true)] out string? targetProtoId)
|
||||
{
|
||||
if (_recipesMetadataCache.TryGetValue(constructionProtoId, out targetProtoId))
|
||||
return true;
|
||||
|
||||
targetProtoId = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void WarmupRecipesCache()
|
||||
{
|
||||
foreach (var constructionProto in PrototypeManager.EnumeratePrototypes<ConstructionPrototype>())
|
||||
{
|
||||
if (!PrototypeManager.TryIndex(constructionProto.Graph, out var graphProto))
|
||||
continue;
|
||||
|
||||
if (constructionProto.TargetNode is not { } targetNodeId)
|
||||
continue;
|
||||
|
||||
if (!graphProto.Nodes.TryGetValue(targetNodeId, out var targetNode))
|
||||
continue;
|
||||
|
||||
// Recursion is for wimps.
|
||||
var stack = new Stack<ConstructionGraphNode>();
|
||||
stack.Push(targetNode);
|
||||
|
||||
do
|
||||
{
|
||||
var node = stack.Pop();
|
||||
|
||||
// I never realized if this uid affects anything...
|
||||
// EntityUid? userUid = args.SenderSession.State.ControlledEntity.HasValue
|
||||
// ? GetEntity(args.SenderSession.State.ControlledEntity.Value)
|
||||
// : null;
|
||||
|
||||
// We try to get the id of the target prototype, if it fails, we try going through the edges.
|
||||
if (node.Entity.GetId(null, null, new(EntityManager)) is not { } entityId)
|
||||
{
|
||||
// If the stack is not empty, there is a high probability that the loop will go to infinity.
|
||||
if (stack.Count == 0)
|
||||
{
|
||||
foreach (var edge in node.Edges)
|
||||
{
|
||||
if (graphProto.Nodes.TryGetValue(edge.Target, out var graphNode))
|
||||
stack.Push(graphNode);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we got the id of the prototype, we exit the “recursion” by clearing the stack.
|
||||
stack.Clear();
|
||||
|
||||
if (!PrototypeManager.TryIndex(constructionProto.ID, out ConstructionPrototype? recipe))
|
||||
continue;
|
||||
|
||||
if (!PrototypeManager.TryIndex(entityId, out var proto))
|
||||
continue;
|
||||
|
||||
var name = recipe.SetName.HasValue ? Loc.GetString(recipe.SetName) : proto.Name;
|
||||
var desc = recipe.SetDescription.HasValue ? Loc.GetString(recipe.SetDescription) : proto.Description;
|
||||
|
||||
recipe.Name = name;
|
||||
recipe.Description = desc;
|
||||
|
||||
_recipesMetadataCache.Add(constructionProto.ID, entityId);
|
||||
} while (stack.Count > 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConstructionGuideReceived(ResponseConstructionGuide ev)
|
||||
{
|
||||
_guideCache[ev.ConstructionId] = ev.Guide;
|
||||
@@ -88,7 +162,7 @@ namespace Content.Client.Construction
|
||||
|
||||
private void HandleConstructionGhostExamined(EntityUid uid, ConstructionGhostComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (component.Prototype == null)
|
||||
if (component.Prototype?.Name is null)
|
||||
return;
|
||||
|
||||
using (args.PushGroup(nameof(ConstructionGhostComponent)))
|
||||
@@ -97,7 +171,7 @@ namespace Content.Client.Construction
|
||||
"construction-ghost-examine-message",
|
||||
("name", component.Prototype.Name)));
|
||||
|
||||
if (!_prototypeManager.TryIndex(component.Prototype.Graph, out ConstructionGraphPrototype? graph))
|
||||
if (!PrototypeManager.TryIndex(component.Prototype.Graph, out var graph))
|
||||
return;
|
||||
|
||||
var startNode = graph.Nodes[component.Prototype.StartNode];
|
||||
@@ -198,6 +272,9 @@ namespace Content.Client.Construction
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetRecipePrototype(prototype.ID, out var targetProtoId) || !PrototypeManager.TryIndex(targetProtoId, out EntityPrototype? targetProto))
|
||||
return false;
|
||||
|
||||
if (GhostPresent(loc))
|
||||
return false;
|
||||
|
||||
@@ -214,16 +291,43 @@ namespace Content.Client.Construction
|
||||
comp.GhostId = ghost.GetHashCode();
|
||||
EntityManager.GetComponent<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle();
|
||||
_ghosts.Add(comp.GhostId, ghost.Value);
|
||||
|
||||
var sprite = EntityManager.GetComponent<SpriteComponent>(ghost.Value);
|
||||
sprite.Color = new Color(48, 255, 48, 128);
|
||||
|
||||
for (int i = 0; i < prototype.Layers.Count; i++)
|
||||
if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory))
|
||||
{
|
||||
sprite.AddBlankLayer(i); // There is no way to actually check if this already exists, so we blindly insert a new one
|
||||
sprite.LayerSetSprite(i, prototype.Layers[i]);
|
||||
sprite.LayerSetShader(i, "unshaded");
|
||||
sprite.LayerSetVisible(i, true);
|
||||
sprite.AddBlankLayer(0);
|
||||
sprite.LayerSetSprite(0, icon.Icon);
|
||||
sprite.LayerSetShader(0, "unshaded");
|
||||
sprite.LayerSetVisible(0, true);
|
||||
}
|
||||
else if (targetProto.Components.TryGetValue("Sprite", out _))
|
||||
{
|
||||
var dummy = EntityManager.SpawnEntity(targetProtoId, MapCoordinates.Nullspace);
|
||||
var targetSprite = EntityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
EntityManager.System<AppearanceSystem>().OnChangeData(dummy, targetSprite);
|
||||
|
||||
for (var i = 0; i < targetSprite.AllLayers.Count(); i++)
|
||||
{
|
||||
if (!targetSprite[i].Visible || !targetSprite[i].RsiState.IsValid)
|
||||
continue;
|
||||
|
||||
var rsi = targetSprite[i].Rsi ?? targetSprite.BaseRSI;
|
||||
if (rsi is null || !rsi.TryGetState(targetSprite[i].RsiState, out var state) ||
|
||||
state.StateId.Name is null)
|
||||
continue;
|
||||
|
||||
sprite.AddBlankLayer(i);
|
||||
sprite.LayerSetSprite(i, new SpriteSpecifier.Rsi(rsi.Path, state.StateId.Name));
|
||||
sprite.LayerSetShader(i, "unshaded");
|
||||
sprite.LayerSetVisible(i, true);
|
||||
}
|
||||
|
||||
EntityManager.DeleteEntity(dummy);
|
||||
}
|
||||
else
|
||||
return false;
|
||||
|
||||
if (prototype.CanBuildInImpassable)
|
||||
EnsureComp<WallMountComponent>(ghost.Value).Arc = new(Math.Tau);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io">
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" MinWidth="243" Margin="0 0 5 0">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
|
||||
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True"/>
|
||||
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'construction-menu-search'}" HorizontalExpand="True"/>
|
||||
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
|
||||
</BoxContainer>
|
||||
<ItemList Name="Recipes" Access="Public" SelectMode="Single" VerticalExpand="True"/>
|
||||
<controls:ListContainer Name="Recipes" Access="Public" Group="True" Toggle="True" VerticalExpand="True" />
|
||||
<ScrollContainer Name="RecipesGridScrollContainer" VerticalExpand="True" Access="Public" Visible="False">
|
||||
<GridContainer Name="RecipesGrid" Columns="5" Access="Public"/>
|
||||
</ScrollContainer>
|
||||
@@ -18,7 +19,7 @@
|
||||
<Control>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 5">
|
||||
<BoxContainer Orientation="Horizontal" Align="Center">
|
||||
<TextureRect Name="TargetTexture" HorizontalAlignment="Right" Stretch="Keep" Margin="0 0 10 0"/>
|
||||
<EntityPrototypeView Name="TargetTexture" HorizontalAlignment="Right" Stretch="Fill" Margin="0 0 10 0"/>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<RichTextLabel Name="TargetName"/>
|
||||
<RichTextLabel Name="TargetDesc"/>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Construction.UI
|
||||
{
|
||||
@@ -28,7 +25,7 @@ namespace Content.Client.Construction.UI
|
||||
bool GridViewButtonPressed { get; set; }
|
||||
bool BuildButtonPressed { get; set; }
|
||||
|
||||
ItemList Recipes { get; }
|
||||
ListContainer Recipes { get; }
|
||||
ItemList RecipeStepList { get; }
|
||||
|
||||
|
||||
@@ -36,14 +33,14 @@ namespace Content.Client.Construction.UI
|
||||
GridContainer RecipesGrid { get; }
|
||||
|
||||
event EventHandler<(string search, string catagory)> PopulateRecipes;
|
||||
event EventHandler<ItemList.Item?> RecipeSelected;
|
||||
event EventHandler<ConstructionMenu.ConstructionMenuListData?> RecipeSelected;
|
||||
event EventHandler RecipeFavorited;
|
||||
event EventHandler<bool> BuildButtonToggled;
|
||||
event EventHandler<bool> EraseButtonToggled;
|
||||
event EventHandler ClearAllGhosts;
|
||||
|
||||
void ClearRecipeInfo();
|
||||
void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem, bool isFavorite);
|
||||
void SetRecipeInfo(string name, string description, EntityPrototype? targetPrototype, bool isItem, bool isFavorite);
|
||||
void ResetPlacement();
|
||||
|
||||
#region Window Control
|
||||
@@ -94,8 +91,36 @@ namespace Content.Client.Construction.UI
|
||||
Title = Loc.GetString("construction-menu-title");
|
||||
|
||||
BuildButton.Text = Loc.GetString("construction-menu-place-ghost");
|
||||
Recipes.OnItemSelected += obj => RecipeSelected?.Invoke(this, obj.ItemList[obj.ItemIndex]);
|
||||
Recipes.OnItemDeselected += _ => RecipeSelected?.Invoke(this, null);
|
||||
Recipes.ItemPressed += (_, data) => RecipeSelected?.Invoke(this, data as ConstructionMenuListData);
|
||||
Recipes.NoItemSelected += () => RecipeSelected?.Invoke(this, null);
|
||||
Recipes.GenerateItem += (data, button) =>
|
||||
{
|
||||
if (data is not ConstructionMenuListData (var prototype, var targetPrototype))
|
||||
return;
|
||||
|
||||
var entProtoView = new EntityPrototypeView()
|
||||
{
|
||||
SetSize = new(32f),
|
||||
Stretch = SpriteView.StretchMode.Fill,
|
||||
Scale = new(2),
|
||||
Margin = new(0, 2),
|
||||
};
|
||||
entProtoView.SetPrototype(targetPrototype);
|
||||
|
||||
var label = new Label()
|
||||
{
|
||||
Text = prototype.Name,
|
||||
Margin = new(5, 0),
|
||||
};
|
||||
|
||||
var box = new BoxContainer();
|
||||
box.AddChild(entProtoView);
|
||||
box.AddChild(label);
|
||||
|
||||
button.AddChild(box);
|
||||
button.ToolTip = prototype.Description;
|
||||
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
|
||||
};
|
||||
|
||||
SearchBar.OnTextChanged += _ =>
|
||||
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
|
||||
@@ -121,7 +146,7 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
public event EventHandler? ClearAllGhosts;
|
||||
public event EventHandler<(string search, string catagory)>? PopulateRecipes;
|
||||
public event EventHandler<ItemList.Item?>? RecipeSelected;
|
||||
public event EventHandler<ConstructionMenuListData?>? RecipeSelected;
|
||||
public event EventHandler? RecipeFavorited;
|
||||
public event EventHandler<bool>? BuildButtonToggled;
|
||||
public event EventHandler<bool>? EraseButtonToggled;
|
||||
@@ -133,13 +158,17 @@ namespace Content.Client.Construction.UI
|
||||
}
|
||||
|
||||
public void SetRecipeInfo(
|
||||
string name, string description, Texture iconTexture, bool isItem, bool isFavorite)
|
||||
string name,
|
||||
string description,
|
||||
EntityPrototype? targetPrototype,
|
||||
bool isItem,
|
||||
bool isFavorite)
|
||||
{
|
||||
BuildButton.Disabled = false;
|
||||
BuildButton.Text = Loc.GetString(isItem ? "construction-menu-place-ghost" : "construction-menu-craft");
|
||||
TargetName.SetMessage(name);
|
||||
TargetDesc.SetMessage(description);
|
||||
TargetTexture.Texture = iconTexture;
|
||||
TargetTexture.SetPrototype(targetPrototype?.ID);
|
||||
FavoriteButton.Visible = true;
|
||||
FavoriteButton.Text = Loc.GetString(
|
||||
isFavorite ? "construction-add-favorite-button" : "construction-remove-from-favorite-button");
|
||||
@@ -150,9 +179,11 @@ namespace Content.Client.Construction.UI
|
||||
BuildButton.Disabled = true;
|
||||
TargetName.SetMessage(string.Empty);
|
||||
TargetDesc.SetMessage(string.Empty);
|
||||
TargetTexture.Texture = null;
|
||||
TargetTexture.SetPrototype(null);
|
||||
FavoriteButton.Visible = false;
|
||||
RecipeStepList.Clear();
|
||||
}
|
||||
|
||||
public sealed record ConstructionMenuListData(ConstructionPrototype Prototype, EntityPrototype TargetPrototype) : ListData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@ using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.Construction.UI
|
||||
{
|
||||
@@ -30,18 +28,20 @@ namespace Content.Client.Construction.UI
|
||||
[Dependency] private readonly IPlacementManager _placementManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
private readonly IConstructionMenuView _constructionView;
|
||||
private readonly EntityWhitelistSystem _whitelistSystem;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
private ConstructionSystem? _constructionSystem;
|
||||
private ConstructionPrototype? _selected;
|
||||
private List<ConstructionPrototype> _favoritedRecipes = [];
|
||||
private Dictionary<string, TextureButton> _recipeButtons = new();
|
||||
private Dictionary<string, ContainerButton> _recipeButtons = new();
|
||||
private string _selectedCategory = string.Empty;
|
||||
private string _favoriteCatName = "construction-category-favorites";
|
||||
private string _forAllCategoryName = "construction-category-all";
|
||||
|
||||
private const string FavoriteCatName = "construction-category-favorites";
|
||||
private const string ForAllCategoryName = "construction-category-all";
|
||||
|
||||
private bool CraftingAvailable
|
||||
{
|
||||
get => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Visible;
|
||||
@@ -98,15 +98,18 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
_placementManager.PlacementChanged += OnPlacementChanged;
|
||||
|
||||
_constructionView.OnClose += () => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false;
|
||||
_constructionView.OnClose +=
|
||||
() => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false;
|
||||
_constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts();
|
||||
_constructionView.PopulateRecipes += OnViewPopulateRecipes;
|
||||
_constructionView.RecipeSelected += OnViewRecipeSelected;
|
||||
_constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b);
|
||||
_constructionView.EraseButtonToggled += (_, b) =>
|
||||
{
|
||||
if (_constructionSystem is null) return;
|
||||
if (b) _placementManager.Clear();
|
||||
if (_constructionSystem is null)
|
||||
return;
|
||||
if (b)
|
||||
_placementManager.Clear();
|
||||
_placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_constructionSystem, null));
|
||||
_constructionView.EraseButtonPressed = b;
|
||||
};
|
||||
@@ -117,7 +120,7 @@ namespace Content.Client.Construction.UI
|
||||
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
|
||||
}
|
||||
|
||||
public void OnHudCraftingButtonToggled(ButtonToggledEventArgs args)
|
||||
public void OnHudCraftingButtonToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
WindowOpen = args.Pressed;
|
||||
}
|
||||
@@ -139,7 +142,7 @@ namespace Content.Client.Construction.UI
|
||||
_constructionView.ResetPlacement();
|
||||
}
|
||||
|
||||
private void OnViewRecipeSelected(object? sender, ItemList.Item? item)
|
||||
private void OnViewRecipeSelected(object? sender, ConstructionMenu.ConstructionMenuListData? item)
|
||||
{
|
||||
if (item is null)
|
||||
{
|
||||
@@ -148,12 +151,15 @@ namespace Content.Client.Construction.UI
|
||||
return;
|
||||
}
|
||||
|
||||
_selected = (ConstructionPrototype) item.Metadata!;
|
||||
if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
|
||||
_selected = item.Prototype;
|
||||
|
||||
if (_placementManager is { IsActive: true, Eraser: false })
|
||||
UpdateGhostPlacement();
|
||||
|
||||
PopulateInfo(_selected);
|
||||
}
|
||||
|
||||
private void OnGridViewRecipeSelected(object? sender, ConstructionPrototype? recipe)
|
||||
private void OnGridViewRecipeSelected(object? _, ConstructionPrototype? recipe)
|
||||
{
|
||||
if (recipe is null)
|
||||
{
|
||||
@@ -163,65 +169,21 @@ namespace Content.Client.Construction.UI
|
||||
}
|
||||
|
||||
_selected = recipe;
|
||||
if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
|
||||
|
||||
if (_placementManager is { IsActive: true, Eraser: false })
|
||||
UpdateGhostPlacement();
|
||||
|
||||
PopulateInfo(_selected);
|
||||
}
|
||||
|
||||
private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
|
||||
{
|
||||
var (search, category) = args;
|
||||
if (_constructionSystem is null)
|
||||
return;
|
||||
|
||||
var recipes = new List<ConstructionPrototype>();
|
||||
|
||||
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
|
||||
|
||||
if (isEmptyCategory)
|
||||
_selectedCategory = string.Empty;
|
||||
else
|
||||
_selectedCategory = category;
|
||||
|
||||
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
|
||||
{
|
||||
if (recipe.Hide)
|
||||
continue;
|
||||
|
||||
if (!recipe.CrystallPunkAllowed) //CrystallEdge clearing recipes
|
||||
continue;
|
||||
|
||||
if (_playerManager.LocalSession == null
|
||||
|| _playerManager.LocalEntity == null
|
||||
|| _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value))
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
{
|
||||
if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant()))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isEmptyCategory)
|
||||
{
|
||||
if (category == _favoriteCatName)
|
||||
{
|
||||
if (!_favoritedRecipes.Contains(recipe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (recipe.Category != category)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
recipes.Add(recipe);
|
||||
}
|
||||
|
||||
recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture));
|
||||
var actualRecipes = GetAndSortRecipes(args);
|
||||
|
||||
var recipesList = _constructionView.Recipes;
|
||||
recipesList.Clear();
|
||||
|
||||
var recipesGrid = _constructionView.RecipesGrid;
|
||||
recipesGrid.RemoveAllChildren();
|
||||
|
||||
@@ -230,60 +192,123 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
if (_constructionView.GridViewButtonPressed)
|
||||
{
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
var itemButton = new TextureButton
|
||||
{
|
||||
TextureNormal = _spriteSystem.Frame0(recipe.Icon),
|
||||
VerticalAlignment = Control.VAlignment.Center,
|
||||
Name = recipe.Name,
|
||||
ToolTip = recipe.Name,
|
||||
Scale = new Vector2(1.35f),
|
||||
ToggleMode = true,
|
||||
};
|
||||
var itemButtonPanelContainer = new PanelContainer
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
|
||||
Children = { itemButton },
|
||||
};
|
||||
|
||||
itemButton.OnToggled += buttonToggledEventArgs =>
|
||||
{
|
||||
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
|
||||
|
||||
if (buttonToggledEventArgs.Pressed &&
|
||||
_selected != null &&
|
||||
_recipeButtons.TryGetValue(_selected.Name, out var oldButton))
|
||||
{
|
||||
oldButton.Pressed = false;
|
||||
SelectGridButton(oldButton, false);
|
||||
}
|
||||
|
||||
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe : null);
|
||||
};
|
||||
|
||||
recipesGrid.AddChild(itemButtonPanelContainer);
|
||||
_recipeButtons[recipe.Name] = itemButton;
|
||||
var isCurrentButtonSelected = _selected == recipe;
|
||||
itemButton.Pressed = isCurrentButtonSelected;
|
||||
SelectGridButton(itemButton, isCurrentButtonSelected);
|
||||
}
|
||||
recipesList.PopulateList([]);
|
||||
PopulateGrid(recipesGrid, actualRecipes);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
recipesList.Add(GetItem(recipe, recipesList));
|
||||
}
|
||||
recipesList.PopulateList(actualRecipes);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectGridButton(TextureButton button, bool select)
|
||||
private void PopulateGrid(GridContainer recipesGrid,
|
||||
IEnumerable<ConstructionMenu.ConstructionMenuListData> actualRecipes)
|
||||
{
|
||||
foreach (var recipe in actualRecipes)
|
||||
{
|
||||
if (!recipe.Prototype.CrystallPunkAllowed) //CrystallEdge clearing recipes
|
||||
continue;
|
||||
|
||||
var protoView = new EntityPrototypeView()
|
||||
{
|
||||
Scale = new Vector2(1.2f),
|
||||
};
|
||||
protoView.SetPrototype(recipe.TargetPrototype);
|
||||
|
||||
var itemButton = new ContainerButton()
|
||||
{
|
||||
VerticalAlignment = Control.VAlignment.Center,
|
||||
Name = recipe.TargetPrototype.Name,
|
||||
ToolTip = recipe.TargetPrototype.Name,
|
||||
ToggleMode = true,
|
||||
Children = { protoView },
|
||||
};
|
||||
|
||||
var itemButtonPanelContainer = new PanelContainer
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
|
||||
Children = { itemButton },
|
||||
};
|
||||
|
||||
itemButton.OnToggled += buttonToggledEventArgs =>
|
||||
{
|
||||
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
|
||||
|
||||
if (buttonToggledEventArgs.Pressed &&
|
||||
_selected != null &&
|
||||
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton))
|
||||
{
|
||||
oldButton.Pressed = false;
|
||||
SelectGridButton(oldButton, false);
|
||||
}
|
||||
|
||||
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe.Prototype : null);
|
||||
};
|
||||
|
||||
recipesGrid.AddChild(itemButtonPanelContainer);
|
||||
_recipeButtons[recipe.Prototype.Name!] = itemButton;
|
||||
var isCurrentButtonSelected = _selected == recipe.Prototype;
|
||||
itemButton.Pressed = isCurrentButtonSelected;
|
||||
SelectGridButton(itemButton, isCurrentButtonSelected);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ConstructionMenu.ConstructionMenuListData> GetAndSortRecipes((string, string) args)
|
||||
{
|
||||
var recipes = new List<ConstructionMenu.ConstructionMenuListData>();
|
||||
|
||||
var (search, category) = args;
|
||||
var isEmptyCategory = string.IsNullOrEmpty(category) || category == ForAllCategoryName;
|
||||
_selectedCategory = isEmptyCategory ? string.Empty : category;
|
||||
|
||||
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
|
||||
{
|
||||
if (recipe.Hide)
|
||||
continue;
|
||||
|
||||
if (_playerManager.LocalSession == null
|
||||
|| _playerManager.LocalEntity == null
|
||||
|| _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value))
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrEmpty(search) && (recipe.Name is { } name &&
|
||||
!name.Contains(search.Trim(),
|
||||
StringComparison.InvariantCultureIgnoreCase)))
|
||||
continue;
|
||||
|
||||
if (!isEmptyCategory)
|
||||
{
|
||||
if ((category != FavoriteCatName || !_favoritedRecipes.Contains(recipe)) &&
|
||||
recipe.Category != category)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
|
||||
{
|
||||
Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
|
||||
recipe.ID,
|
||||
nameof(ConstructionPrototype));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_prototypeManager.TryIndex(targetProtoId, out EntityPrototype? proto))
|
||||
continue;
|
||||
|
||||
recipes.Add(new(recipe, proto));
|
||||
}
|
||||
|
||||
recipes.Sort(
|
||||
(a, b) => string.Compare(a.Prototype.Name, b.Prototype.Name, StringComparison.InvariantCulture));
|
||||
|
||||
return recipes;
|
||||
}
|
||||
|
||||
private void SelectGridButton(BaseButton button, bool select)
|
||||
{
|
||||
if (button.Parent is not PanelContainer buttonPanel)
|
||||
return;
|
||||
|
||||
button.Modulate = select ? Color.Green : Color.White;
|
||||
button.Modulate = select ? Color.Green : Color.Transparent;
|
||||
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
|
||||
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
|
||||
}
|
||||
@@ -305,12 +330,12 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
// hard-coded to show all recipes
|
||||
var idx = 0;
|
||||
categoriesArray[idx++] = _forAllCategoryName;
|
||||
categoriesArray[idx++] = ForAllCategoryName;
|
||||
|
||||
// hard-coded to show favorites if it need
|
||||
if (isFavorites)
|
||||
{
|
||||
categoriesArray[idx++] = _favoriteCatName;
|
||||
categoriesArray[idx++] = FavoriteCatName;
|
||||
}
|
||||
|
||||
var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString);
|
||||
@@ -328,18 +353,31 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i])
|
||||
_constructionView.OptionCategories.SelectId(i);
|
||||
|
||||
}
|
||||
|
||||
_constructionView.Categories = categoriesArray;
|
||||
}
|
||||
|
||||
private void PopulateInfo(ConstructionPrototype prototype)
|
||||
private void PopulateInfo(ConstructionPrototype? prototype)
|
||||
{
|
||||
if (_constructionSystem is null)
|
||||
return;
|
||||
|
||||
_constructionView.ClearRecipeInfo();
|
||||
|
||||
if (prototype is null)
|
||||
return;
|
||||
|
||||
if (!_constructionSystem.TryGetRecipePrototype(prototype.ID, out var targetProtoId))
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.TryIndex(targetProtoId, out EntityPrototype? proto))
|
||||
return;
|
||||
|
||||
_constructionView.SetRecipeInfo(
|
||||
prototype.Name, prototype.Description, _spriteSystem.Frame0(prototype.Icon),
|
||||
prototype.Name!,
|
||||
prototype.Description!,
|
||||
proto,
|
||||
prototype.Type != ConstructionType.Item,
|
||||
!_favoritedRecipes.Contains(prototype));
|
||||
|
||||
@@ -352,16 +390,17 @@ namespace Content.Client.Construction.UI
|
||||
if (_constructionSystem?.GetGuide(prototype) is not { } guide)
|
||||
return;
|
||||
|
||||
|
||||
foreach (var entry in guide.Entries)
|
||||
{
|
||||
var text = entry.Arguments != null
|
||||
? Loc.GetString(entry.Localization, entry.Arguments) : Loc.GetString(entry.Localization);
|
||||
? Loc.GetString(entry.Localization, entry.Arguments)
|
||||
: Loc.GetString(entry.Localization);
|
||||
|
||||
if (entry.EntryNumber is { } number)
|
||||
{
|
||||
text = Loc.GetString("construction-presenter-step-wrapper",
|
||||
("step-number", number), ("text", text));
|
||||
("step-number", number),
|
||||
("text", text));
|
||||
}
|
||||
|
||||
// The padding needs to be applied regardless of text length... (See PadLeft documentation)
|
||||
@@ -372,23 +411,12 @@ namespace Content.Client.Construction.UI
|
||||
}
|
||||
}
|
||||
|
||||
private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
|
||||
{
|
||||
return new(itemList)
|
||||
{
|
||||
Metadata = recipe,
|
||||
Text = recipe.Name,
|
||||
Icon = _spriteSystem.Frame0(recipe.Icon),
|
||||
TooltipEnabled = true,
|
||||
TooltipText = recipe.Description,
|
||||
};
|
||||
}
|
||||
|
||||
private void BuildButtonToggled(bool pressed)
|
||||
{
|
||||
if (pressed)
|
||||
{
|
||||
if (_selected == null) return;
|
||||
if (_selected == null)
|
||||
return;
|
||||
|
||||
// not bound to a construction system
|
||||
if (_constructionSystem is null)
|
||||
@@ -405,10 +433,11 @@ namespace Content.Client.Construction.UI
|
||||
}
|
||||
|
||||
_placementManager.BeginPlacing(new PlacementInformation
|
||||
{
|
||||
IsTile = false,
|
||||
PlacementOption = _selected.PlacementMode
|
||||
}, new ConstructionPlacementHijack(_constructionSystem, _selected));
|
||||
{
|
||||
IsTile = false,
|
||||
PlacementOption = _selected.PlacementMode
|
||||
},
|
||||
new ConstructionPlacementHijack(_constructionSystem, _selected));
|
||||
|
||||
UpdateGhostPlacement();
|
||||
}
|
||||
@@ -432,38 +461,39 @@ namespace Content.Client.Construction.UI
|
||||
var constructSystem = _systemManager.GetEntitySystem<ConstructionSystem>();
|
||||
|
||||
_placementManager.BeginPlacing(new PlacementInformation()
|
||||
{
|
||||
IsTile = false,
|
||||
PlacementOption = _selected.PlacementMode,
|
||||
}, new ConstructionPlacementHijack(constructSystem, _selected));
|
||||
{
|
||||
IsTile = false,
|
||||
PlacementOption = _selected.PlacementMode,
|
||||
},
|
||||
new ConstructionPlacementHijack(constructSystem, _selected));
|
||||
|
||||
_constructionView.BuildButtonPressed = true;
|
||||
}
|
||||
|
||||
private void OnSystemLoaded(object? sender, SystemChangedArgs args)
|
||||
{
|
||||
if (args.System is ConstructionSystem system) SystemBindingChanged(system);
|
||||
if (args.System is ConstructionSystem system)
|
||||
SystemBindingChanged(system);
|
||||
}
|
||||
|
||||
private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
|
||||
{
|
||||
if (args.System is ConstructionSystem) SystemBindingChanged(null);
|
||||
if (args.System is ConstructionSystem)
|
||||
SystemBindingChanged(null);
|
||||
}
|
||||
|
||||
private void OnViewFavoriteRecipe()
|
||||
{
|
||||
if (_selected is not ConstructionPrototype recipe)
|
||||
if (_selected is null)
|
||||
return;
|
||||
|
||||
if (!_favoritedRecipes.Remove(_selected))
|
||||
_favoritedRecipes.Add(_selected);
|
||||
|
||||
if (_selectedCategory == _favoriteCatName)
|
||||
if (_selectedCategory == FavoriteCatName)
|
||||
{
|
||||
if (_favoritedRecipes.Count > 0)
|
||||
OnViewPopulateRecipes(_constructionView, (string.Empty, _favoriteCatName));
|
||||
else
|
||||
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
|
||||
OnViewPopulateRecipes(_constructionView,
|
||||
_favoritedRecipes.Count > 0 ? (string.Empty, FavoriteCatName) : (string.Empty, string.Empty));
|
||||
}
|
||||
|
||||
PopulateInfo(_selected);
|
||||
@@ -495,6 +525,9 @@ namespace Content.Client.Construction.UI
|
||||
private void BindToSystem(ConstructionSystem system)
|
||||
{
|
||||
_constructionSystem = system;
|
||||
|
||||
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
|
||||
|
||||
system.ToggleCraftingWindow += SystemOnToggleMenu;
|
||||
system.FlipConstructionPrototype += SystemFlipConstructionPrototype;
|
||||
system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged;
|
||||
@@ -536,7 +569,8 @@ namespace Content.Client.Construction.UI
|
||||
if (IsAtFront)
|
||||
{
|
||||
WindowOpen = false;
|
||||
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(false); // This does not call CraftingButtonToggled
|
||||
_uiManager.GetActiveUIWidget<GameTopMenuBar>()
|
||||
.CraftingButton.SetClickPressed(false); // This does not call CraftingButtonToggled
|
||||
}
|
||||
else
|
||||
_constructionView.MoveToFront();
|
||||
@@ -544,7 +578,8 @@ namespace Content.Client.Construction.UI
|
||||
else
|
||||
{
|
||||
WindowOpen = true;
|
||||
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(true); // This does not call CraftingButtonToggled
|
||||
_uiManager.GetActiveUIWidget<GameTopMenuBar>()
|
||||
.CraftingButton.SetClickPressed(true); // This does not call CraftingButtonToggled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
Content.Client/Damage/Systems/StaminaSystem.cs
Normal file
7
Content.Client/Damage/Systems/StaminaSystem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Content.Shared.Damage.Systems;
|
||||
|
||||
namespace Content.Client.Damage.Systems;
|
||||
|
||||
public sealed partial class StaminaSystem : SharedStaminaSystem
|
||||
{
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Content.Shared.Disposal;
|
||||
using Content.Shared.Disposal.Components;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Disposal.Mailing;
|
||||
|
||||
@@ -70,10 +71,10 @@ public sealed class MailingUnitBoundUserInterface : BoundUserInterface
|
||||
//UnitTag.Text = state.Tag;
|
||||
MailingUnitWindow.Target.Text = entity.Comp.Target;
|
||||
|
||||
MailingUnitWindow.TargetListContainer.Clear();
|
||||
foreach (var target in entity.Comp.TargetList)
|
||||
{
|
||||
MailingUnitWindow.TargetListContainer.AddItem(target);
|
||||
}
|
||||
var entries = entity.Comp.TargetList.Select(target => new ItemList.Item(MailingUnitWindow.TargetListContainer) {
|
||||
Text = target,
|
||||
Selected = target == entity.Comp.Target
|
||||
}).ToList();
|
||||
MailingUnitWindow.TargetListContainer.SetItems(entries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ using Content.Client.Replay;
|
||||
using Content.Client.Screenshot;
|
||||
using Content.Client.Singularity;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Client.Voting;
|
||||
using Content.Shared._CP14.Sponsor;
|
||||
@@ -82,6 +83,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
|
||||
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
@@ -240,6 +242,15 @@ namespace Content.Client.Entry
|
||||
{
|
||||
_debugMonitorManager.FrameUpdate();
|
||||
}
|
||||
|
||||
if (level == ModUpdateLevel.PreEngine)
|
||||
{
|
||||
if (_baseClient.RunLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame)
|
||||
{
|
||||
var updateSystem = _entitySystemManager.GetEntitySystem<BuiPreTickUpdateSystem>();
|
||||
updateSystem.RunUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ namespace Content.Client.Examine
|
||||
public const string StyleClassEntityTooltip = "entity-tooltip";
|
||||
|
||||
private EntityUid _examinedEntity;
|
||||
private EntityUid _lastExaminedEntity;
|
||||
private Popup? _examineTooltipOpen;
|
||||
private ScreenCoordinates _popupPos;
|
||||
private CancellationTokenSource? _requestCancelTokenSource;
|
||||
@@ -416,15 +415,14 @@ namespace Content.Client.Examine
|
||||
if (!IsClientSide(entity))
|
||||
{
|
||||
// Ask server for extra examine info.
|
||||
if (entity != _lastExaminedEntity)
|
||||
unchecked
|
||||
{
|
||||
_idCounter += 1;
|
||||
if (_idCounter == int.MaxValue)
|
||||
_idCounter = 0;
|
||||
}
|
||||
RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(GetNetEntity(entity), _idCounter, true));
|
||||
}
|
||||
|
||||
RaiseLocalEvent(entity, new ClientExaminedEvent(entity, playerEnt.Value));
|
||||
_lastExaminedEntity = entity;
|
||||
}
|
||||
|
||||
private void CloseTooltip()
|
||||
|
||||
@@ -93,7 +93,7 @@ public sealed partial class TriggerSystem
|
||||
break;
|
||||
case ProximityTriggerVisuals.Active:
|
||||
if (_player.HasRunningAnimation(uid, player, AnimKey)) return;
|
||||
_player.Play(uid, player, _flasherAnimation, AnimKey);
|
||||
_player.Play((uid, player), _flasherAnimation, AnimKey);
|
||||
break;
|
||||
case ProximityTriggerVisuals.Off:
|
||||
default:
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class EyeLerpingSystem : EntitySystem
|
||||
SubscribeLocalEvent<LerpingEyeComponent, LocalPlayerDetachedEvent>(OnDetached);
|
||||
|
||||
UpdatesAfter.Add(typeof(TransformSystem));
|
||||
UpdatesAfter.Add(typeof(PhysicsSystem));
|
||||
UpdatesAfter.Add(typeof(Robust.Client.Physics.PhysicsSystem));
|
||||
UpdatesBefore.Add(typeof(SharedEyeSystem));
|
||||
UpdatesOutsidePrediction = true;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
if (!HasComp<HolopadUserComponent>(uid))
|
||||
return;
|
||||
|
||||
var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.IsTyping);
|
||||
var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.State);
|
||||
RaiseNetworkEvent(netEv);
|
||||
}
|
||||
|
||||
|
||||
@@ -167,11 +167,14 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
|
||||
private void UpdateRendererMaster(InstrumentComponent instrument)
|
||||
{
|
||||
if (instrument.Renderer == null || instrument.Master == null)
|
||||
if (instrument.Renderer == null)
|
||||
return;
|
||||
|
||||
if (!TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
|
||||
if (instrument.Master == null || !TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
|
||||
{
|
||||
instrument.Renderer.Master = null;
|
||||
return;
|
||||
}
|
||||
|
||||
instrument.Renderer.Master = masterInstrument.Renderer;
|
||||
}
|
||||
@@ -196,15 +199,16 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
return;
|
||||
}
|
||||
|
||||
instrument.Renderer?.SystemReset();
|
||||
instrument.Renderer?.ClearAllEvents();
|
||||
if (instrument.Renderer is { } renderer)
|
||||
{
|
||||
renderer.Master = null;
|
||||
renderer.SystemReset();
|
||||
renderer.ClearAllEvents();
|
||||
|
||||
var renderer = instrument.Renderer;
|
||||
|
||||
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
|
||||
// Don't use timers bound to the entity in case it is getting deleted.
|
||||
if (renderer != null)
|
||||
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
|
||||
// Don't use timers bound to the entity in case it is getting deleted.
|
||||
Timer.Spawn(2000, () => { renderer.Dispose(); });
|
||||
}
|
||||
|
||||
instrument.Renderer = null;
|
||||
instrument.MidiEventBuffer.Clear();
|
||||
|
||||
@@ -100,7 +100,27 @@
|
||||
<controls:HSpacer Spacing="10" />
|
||||
<widgets:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3" MinHeight="50" />
|
||||
</BoxContainer>
|
||||
<TextureButton Name="CollapseButton"
|
||||
TexturePath="filled_right_arrow.svg.192dpi"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0 2 2 0"
|
||||
ModulateSelfOverride="#DEDEDE"
|
||||
Scale="0.5 0.5"/>
|
||||
</PanelContainer>
|
||||
</SplitContainer>
|
||||
<PanelContainer Name="ExpandPanel"
|
||||
StyleClasses="AngleRect"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0 2 2 0"
|
||||
Visible="False">
|
||||
<TextureButton Name="ExpandButton"
|
||||
TexturePath="filled_left_arrow.svg.192dpi"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
ModulateSelfOverride="#DEDEDE"
|
||||
Scale="0.5 0.5"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</lobbyUi:LobbyGui>
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
|
||||
OptionsButton.OnPressed += _ => UserInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
|
||||
|
||||
CollapseButton.OnPressed += _ => TogglePanel(false);
|
||||
ExpandButton.OnPressed += _ => TogglePanel(true);
|
||||
}
|
||||
|
||||
public void SwitchState(LobbyGuiState state)
|
||||
@@ -53,6 +56,12 @@ namespace Content.Client.Lobby.UI
|
||||
}
|
||||
}
|
||||
|
||||
private void TogglePanel(bool value)
|
||||
{
|
||||
RightSide.Visible = value;
|
||||
ExpandPanel.Visible = !value;
|
||||
}
|
||||
|
||||
public enum LobbyGuiState : byte
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -3,11 +3,10 @@ using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Medical.Cryogenics;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client.GameObjects;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
public sealed class CryoPodSystem: SharedCryoPodSystem
|
||||
public sealed class CryoPodSystem : SharedCryoPodSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
@@ -63,11 +62,9 @@ public sealed class CryoPodSystem: SharedCryoPodSystem
|
||||
{
|
||||
args.Sprite.LayerSetState(CryoPodVisualLayers.Base, "pod-open");
|
||||
args.Sprite.LayerSetVisible(CryoPodVisualLayers.Cover, false);
|
||||
args.Sprite.DrawDepth = (int) DrawDepth.Objects;
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Sprite.DrawDepth = (int) DrawDepth.Mobs;
|
||||
args.Sprite.LayerSetState(CryoPodVisualLayers.Base, isOn ? "pod-on" : "pod-off");
|
||||
args.Sprite.LayerSetState(CryoPodVisualLayers.Cover, isOn ? "cover-on" : "cover-off");
|
||||
args.Sprite.LayerSetVisible(CryoPodVisualLayers.Cover, true);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Physics.Controllers;
|
||||
using Content.Client.PhysicsSystem.Controllers;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.NPC;
|
||||
using Content.Shared.NPC.Events;
|
||||
|
||||
47
Content.Client/Overlays/BlackAndWhiteOverlay.cs
Normal file
47
Content.Client/Overlays/BlackAndWhiteOverlay.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class BlackAndWhiteOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
private readonly ShaderInstance _greyscaleShader;
|
||||
|
||||
public BlackAndWhiteOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_greyscaleShader = _prototypeManager.Index<ShaderPrototype>("GreyscaleFullscreen").InstanceUnique();
|
||||
ZIndex = 10; // draw this over the DamageOverlay, RainbowOverlay etc.
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
|
||||
return false;
|
||||
|
||||
if (args.Viewport.Eye != eyeComp.Eye)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (ScreenTexture == null)
|
||||
return;
|
||||
|
||||
var handle = args.WorldHandle;
|
||||
_greyscaleShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
handle.UseShader(_greyscaleShader);
|
||||
handle.DrawRect(args.WorldBounds, Color.White);
|
||||
handle.UseShader(null);
|
||||
}
|
||||
}
|
||||
34
Content.Client/Overlays/BlackAndWhiteOverlaySystem.cs
Normal file
34
Content.Client/Overlays/BlackAndWhiteOverlaySystem.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class BlackAndWhiteOverlaySystem : EquipmentHudSystem<BlackAndWhiteOverlayComponent>
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
|
||||
private BlackAndWhiteOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_overlay = new();
|
||||
}
|
||||
|
||||
protected override void UpdateInternal(RefreshEquipmentHudEvent<BlackAndWhiteOverlayComponent> component)
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
protected override void DeactivateInternal()
|
||||
{
|
||||
base.DeactivateInternal();
|
||||
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Paper.UI;
|
||||
|
||||
@@ -55,6 +56,24 @@ public sealed partial class PaperVisualsComponent : Component
|
||||
[DataField("headerMargin")]
|
||||
public Box2 HeaderMargin = default;
|
||||
|
||||
/// <summary>
|
||||
/// A path to an image which will be used as a footer on the paper
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ResPath? FooterImagePath;
|
||||
|
||||
/// <summary>
|
||||
/// Modulate the footer image by this color
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color FooterImageModulate = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Any additional margin to add around the footer
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Box2 FooterMargin = default;
|
||||
|
||||
/// <summary>
|
||||
/// Path to an image to use as the background to the "content" of the paper
|
||||
/// The header and actual written text will use this as a background. The
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</PanelContainer>
|
||||
<Label Name="FillStatus" StyleClasses="LabelSecondaryColor"/>
|
||||
</BoxContainer>
|
||||
<TextureButton Name="FooterImage" HorizontalAlignment="Center" VerticalAlignment="Top" MouseFilter="Ignore"/>
|
||||
</BoxContainer>
|
||||
<paper:StampCollection Name="StampDisplay" VerticalAlignment="Bottom" Margin="6"/>
|
||||
|
||||
|
||||
@@ -149,6 +149,16 @@ namespace Content.Client.Paper.UI
|
||||
HeaderImage.Margin = new Thickness(visuals.HeaderMargin.Left, visuals.HeaderMargin.Top,
|
||||
visuals.HeaderMargin.Right, visuals.HeaderMargin.Bottom);
|
||||
|
||||
// Then the footer
|
||||
if (visuals.FooterImagePath is {} path)
|
||||
{
|
||||
FooterImage.TexturePath = path.ToString();
|
||||
FooterImage.MinSize = FooterImage.TextureNormal?.Size ?? Vector2.Zero;
|
||||
}
|
||||
|
||||
FooterImage.ModulateSelfOverride = visuals.FooterImageModulate;
|
||||
FooterImage.Margin = new Thickness(visuals.FooterMargin.Left, visuals.FooterMargin.Top,
|
||||
visuals.FooterMargin.Right, visuals.FooterMargin.Bottom);
|
||||
|
||||
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
|
||||
WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
|
||||
|
||||
@@ -3,15 +3,13 @@ using Content.Shared.CCVar;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Physics.Controllers;
|
||||
namespace Content.Client.PhysicsSystem.Controllers;
|
||||
|
||||
public sealed class MoverController : SharedMoverController
|
||||
{
|
||||
@@ -101,39 +99,13 @@ public sealed class MoverController : SharedMoverController
|
||||
|
||||
private void HandleClientsideMovement(EntityUid player, float frameTime)
|
||||
{
|
||||
if (!MoverQuery.TryGetComponent(player, out var mover) ||
|
||||
!XformQuery.TryGetComponent(player, out var xform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var physicsUid = player;
|
||||
PhysicsComponent? body;
|
||||
var xformMover = xform;
|
||||
|
||||
if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
|
||||
{
|
||||
if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
|
||||
!XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
physicsUid = xform.ParentUid;
|
||||
}
|
||||
else if (!PhysicsQuery.TryGetComponent(player, out body))
|
||||
if (!MoverQuery.TryGetComponent(player, out var mover))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-side should just be handled on its own so we'll just do this shizznit
|
||||
HandleMobMovement(
|
||||
player,
|
||||
mover,
|
||||
physicsUid,
|
||||
body,
|
||||
xformMover,
|
||||
frameTime);
|
||||
HandleMobMovement((player, mover), frameTime);
|
||||
}
|
||||
|
||||
protected override bool CanSound()
|
||||
|
||||
85
Content.Client/Power/Battery/BatteryBoundUserInterface.cs
Normal file
85
Content.Client/Power/Battery/BatteryBoundUserInterface.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Shared.Power;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Power.Battery;
|
||||
|
||||
/// <summary>
|
||||
/// BUI for <see cref="BatteryUiKey.Key"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="BoundUserInterfaceState"/>
|
||||
/// <seealso cref="BatteryMenu"/>
|
||||
[UsedImplicitly]
|
||||
public sealed class BatteryBoundUserInterface : BoundUserInterface, IBuiPreTickUpdate
|
||||
{
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = null!;
|
||||
|
||||
[ViewVariables]
|
||||
private BatteryMenu? _menu;
|
||||
|
||||
private BuiPredictionState? _pred;
|
||||
private InputCoalescer<float> _chargeRateCoalescer;
|
||||
private InputCoalescer<float> _dischargeRateCoalescer;
|
||||
|
||||
public BatteryBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_pred = new BuiPredictionState(this, _gameTiming);
|
||||
|
||||
_menu = this.CreateWindow<BatteryMenu>();
|
||||
_menu.SetEntity(Owner);
|
||||
|
||||
_menu.OnInBreaker += val => _pred!.SendMessage(new BatterySetInputBreakerMessage(val));
|
||||
_menu.OnOutBreaker += val => _pred!.SendMessage(new BatterySetOutputBreakerMessage(val));
|
||||
|
||||
_menu.OnChargeRate += val => _chargeRateCoalescer.Set(val);
|
||||
_menu.OnDischargeRate += val => _dischargeRateCoalescer.Set(val);
|
||||
}
|
||||
|
||||
void IBuiPreTickUpdate.PreTickUpdate()
|
||||
{
|
||||
if (_chargeRateCoalescer.CheckIsModified(out var chargeRateValue))
|
||||
_pred!.SendMessage(new BatterySetChargeRateMessage(chargeRateValue));
|
||||
|
||||
if (_dischargeRateCoalescer.CheckIsModified(out var dischargeRateValue))
|
||||
_pred!.SendMessage(new BatterySetDischargeRateMessage(dischargeRateValue));
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
if (state is not BatteryBuiState batteryState)
|
||||
return;
|
||||
|
||||
foreach (var replayMsg in _pred!.MessagesToReplay())
|
||||
{
|
||||
switch (replayMsg)
|
||||
{
|
||||
case BatterySetInputBreakerMessage setInputBreaker:
|
||||
batteryState.CanCharge = setInputBreaker.On;
|
||||
break;
|
||||
|
||||
case BatterySetOutputBreakerMessage setOutputBreaker:
|
||||
batteryState.CanDischarge = setOutputBreaker.On;
|
||||
break;
|
||||
|
||||
case BatterySetChargeRateMessage setChargeRate:
|
||||
batteryState.MaxChargeRate = setChargeRate.Rate;
|
||||
break;
|
||||
|
||||
case BatterySetDischargeRateMessage setDischargeRate:
|
||||
batteryState.MaxSupply = setDischargeRate.Rate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_menu?.Update(batteryState);
|
||||
}
|
||||
}
|
||||
146
Content.Client/Power/Battery/BatteryMenu.xaml
Normal file
146
Content.Client/Power/Battery/BatteryMenu.xaml
Normal file
@@ -0,0 +1,146 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
SetSize="650 330"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Top row: main content -->
|
||||
<BoxContainer Name="MainContent" Orientation="Horizontal" VerticalExpand="True" Margin="4">
|
||||
<!-- Left pane: I/O, passthrough, sprite view -->
|
||||
<BoxContainer Name="IOPane" Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Top row: input -->
|
||||
<BoxContainer Name="InputRow" Orientation="Horizontal">
|
||||
<!-- Input power line -->
|
||||
<PanelContainer Name="InPowerLine" SetHeight="2" VerticalAlignment="Top" SetWidth="32"
|
||||
Margin="2 16" />
|
||||
|
||||
<!-- Box with breaker, label, values -->
|
||||
<PanelContainer HorizontalExpand="True" StyleClasses="Inset">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-in'}" HorizontalExpand="True" VerticalAlignment="Top"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<controls:OnOffButton Name="InBreaker" />
|
||||
</BoxContainer>
|
||||
<Label Name="InValue" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Middle row: Entity view & passthrough -->
|
||||
<BoxContainer Name="MiddleRow" Orientation="Horizontal" VerticalExpand="True">
|
||||
<SpriteView Name="EntityView" SetSize="64 64" Scale="2 2" OverrideDirection="South" Margin="15" />
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Center" HorizontalExpand="True"
|
||||
HorizontalAlignment="Right">
|
||||
<Label HorizontalAlignment="Right" Text="{Loc 'battery-menu-passthrough'}" StyleClasses="StatusFieldTitle" />
|
||||
<Label HorizontalAlignment="Right" Name="PassthroughValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Bottom row: output -->
|
||||
<BoxContainer Name="OutputRow" Orientation="Horizontal">
|
||||
<!-- Output power line -->
|
||||
<PanelContainer Name="OutPowerLine" SetHeight="2" VerticalAlignment="Bottom" SetWidth="32"
|
||||
Margin="2 16" />
|
||||
|
||||
<!-- Box with breaker, label, values -->
|
||||
<PanelContainer HorizontalExpand="True" StyleClasses="Inset">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-out'}" HorizontalExpand="True" VerticalAlignment="Top"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<controls:OnOffButton Name="OutBreaker" />
|
||||
</BoxContainer>
|
||||
<Label Name="OutValue" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Separator connecting panes with some wires -->
|
||||
<BoxContainer Orientation="Vertical" SetWidth="22" Margin="2 16">
|
||||
<PanelContainer Name="InSecondPowerLine" SetHeight="2" />
|
||||
<PanelContainer Name="PassthroughPowerLine" SetWidth="2" HorizontalAlignment="Center" VerticalExpand="True" />
|
||||
<PanelContainer Name="OutSecondPowerLine" SetHeight="2" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Middle pane: charge/discharge -->
|
||||
<BoxContainer Name="ChargeDischarge" Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Charge -->
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="Inset" Margin="0 0 0 8">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'battery-menu-charge-header'}" StyleClasses="LabelKeyText" />
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
|
||||
<Slider Name="ChargeRateSlider" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-max'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="ChargeMaxValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-current'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="ChargeCurrentValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<!-- Discharge -->
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="Inset">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'battery-menu-discharge-header'}" StyleClasses="LabelKeyText" />
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
|
||||
<Slider Name="DischargeRateSlider" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-max'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="DischargeMaxValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'battery-menu-current'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
|
||||
<Label Name="DischargeCurrentValue" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Separator connecting panes with some wires -->
|
||||
<BoxContainer Orientation="Vertical" SetWidth="22" Margin="2 16">
|
||||
<PanelContainer Name="ChargePowerLine" SetHeight="2" VerticalAlignment="Top" VerticalExpand="True" />
|
||||
<PanelContainer Name="DischargePowerLine" SetHeight="2" VerticalAlignment="Bottom" VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Right pane: storage -->
|
||||
<PanelContainer Name="Storage" StyleClasses="Inset" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'battery-menu-storage-header'}" StyleClasses="LabelKeyText" />
|
||||
<GridContainer Columns="2">
|
||||
<Label Text="{Loc 'battery-menu-stored'}" StyleClasses="StatusFieldTitle" />
|
||||
<Label Name="StoredPercentageValue" HorizontalAlignment="Right" HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'battery-menu-energy'}" StyleClasses="StatusFieldTitle" />
|
||||
<Label Name="StoredEnergyValue" HorizontalAlignment="Right" />
|
||||
<Label Name="EtaLabel" StyleClasses="StatusFieldTitle" />
|
||||
<Label Name="EtaValue" HorizontalAlignment="Right" />
|
||||
</GridContainer>
|
||||
|
||||
<!-- Charge meter -->
|
||||
<GridContainer Name="ChargeMeter" Columns="3" VerticalExpand="True" Margin="0 24 0 0">
|
||||
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Name="Footer" Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'battery-menu-footer-left'}" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'battery-menu-footer-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
280
Content.Client/Power/Battery/BatteryMenu.xaml.cs
Normal file
280
Content.Client/Power/Battery/BatteryMenu.xaml.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Rounding;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Power.Battery;
|
||||
|
||||
/// <summary>
|
||||
/// Interface control for batteries.
|
||||
/// </summary>
|
||||
/// <seealso cref="BatteryBoundUserInterface"/>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BatteryMenu : FancyWindow
|
||||
{
|
||||
// Cutoff for the ETA time to switch from "~" to ">" and cap out.
|
||||
private const float MaxEtaValueMinutes = 60;
|
||||
// Cutoff where ETA times likely don't make sense and it's better to just say "N/A".
|
||||
private const float NotApplicableEtaHighCutoffMinutes = 1000;
|
||||
private const float NotApplicableEtaLowCutoffMinutes = 0.01f;
|
||||
// Fudge factor to ignore small charge/discharge values, that are likely caused by floating point rounding errors.
|
||||
private const float PrecisionRoundFactor = 100_000;
|
||||
|
||||
// Colors used for the storage cell bar graphic.
|
||||
private static readonly Color[] StorageColors =
|
||||
[
|
||||
StyleNano.DangerousRedFore,
|
||||
Color.FromHex("#C49438"),
|
||||
Color.FromHex("#B3BF28"),
|
||||
StyleNano.GoodGreenFore,
|
||||
];
|
||||
|
||||
// StorageColors but dimmed for "off" bars.
|
||||
private static readonly Color[] DimStorageColors =
|
||||
[
|
||||
DimStorageColor(StorageColors[0]),
|
||||
DimStorageColor(StorageColors[1]),
|
||||
DimStorageColor(StorageColors[2]),
|
||||
DimStorageColor(StorageColors[3]),
|
||||
];
|
||||
|
||||
// Parameters for the sine wave pulsing animations for active power lines in the UI.
|
||||
private static readonly Color ActivePowerLineHighColor = Color.FromHex("#CCC");
|
||||
private static readonly Color ActivePowerLineLowColor = Color.FromHex("#888");
|
||||
private const float PowerPulseFactor = 4;
|
||||
|
||||
// Dependencies
|
||||
[Dependency] private readonly IEntityManager _entityManager = null!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = null!;
|
||||
|
||||
// Active and inactive style boxes for power lines.
|
||||
// We modify _activePowerLineStyleBox's properties programmatically to implement the pulsing animation.
|
||||
private readonly StyleBoxFlat _activePowerLineStyleBox = new();
|
||||
private readonly StyleBoxFlat _inactivePowerLineStyleBox = new() { BackgroundColor = Color.FromHex("#555") };
|
||||
|
||||
// Style boxes for the storage cell bar graphic.
|
||||
// We modify the properties of these to change the bars' colors.
|
||||
private StyleBoxFlat[] _chargeMeterBoxes;
|
||||
|
||||
// State for the powerline pulsing animation.
|
||||
private float _powerPulseValue;
|
||||
|
||||
// State for the storage cell bar graphic and its blinking effect.
|
||||
private float _blinkPulseValue;
|
||||
private bool _blinkPulse;
|
||||
private int _storageLevel;
|
||||
private bool _hasStorageDelta;
|
||||
|
||||
// The entity that this UI is for.
|
||||
private EntityUid _entity;
|
||||
|
||||
// Used to avoid sending input events when updating slider values.
|
||||
private bool _suppressSliderEvents;
|
||||
|
||||
// Events for the BUI to subscribe to.
|
||||
public event Action<bool>? OnInBreaker;
|
||||
public event Action<bool>? OnOutBreaker;
|
||||
|
||||
public event Action<float>? OnChargeRate;
|
||||
public event Action<float>? OnDischargeRate;
|
||||
|
||||
public BatteryMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
InitChargeMeter();
|
||||
|
||||
InBreaker.StateChanged += val => OnInBreaker?.Invoke(val);
|
||||
OutBreaker.StateChanged += val => OnOutBreaker?.Invoke(val);
|
||||
|
||||
ChargeRateSlider.OnValueChanged += _ =>
|
||||
{
|
||||
if (!_suppressSliderEvents)
|
||||
OnChargeRate?.Invoke(ChargeRateSlider.Value);
|
||||
};
|
||||
DischargeRateSlider.OnValueChanged += _ =>
|
||||
{
|
||||
if (!_suppressSliderEvents)
|
||||
OnDischargeRate?.Invoke(DischargeRateSlider.Value);
|
||||
};
|
||||
}
|
||||
|
||||
public void SetEntity(EntityUid entity)
|
||||
{
|
||||
_entity = entity;
|
||||
|
||||
this.SetInfoFromEntity(_entityManager, _entity);
|
||||
|
||||
EntityView.SetEntity(entity);
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(_chargeMeterBoxes))]
|
||||
public void InitChargeMeter()
|
||||
{
|
||||
_chargeMeterBoxes = new StyleBoxFlat[StorageColors.Length];
|
||||
|
||||
for (var i = StorageColors.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var styleBox = new StyleBoxFlat();
|
||||
_chargeMeterBoxes[i] = styleBox;
|
||||
|
||||
for (var j = 0; j < ChargeMeter.Columns; j++)
|
||||
{
|
||||
var control = new PanelContainer
|
||||
{
|
||||
Margin = new Thickness(2),
|
||||
PanelOverride = styleBox,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
};
|
||||
ChargeMeter.AddChild(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(BatteryBuiState msg)
|
||||
{
|
||||
var inValue = msg.CurrentReceiving;
|
||||
var outValue = msg.CurrentSupply;
|
||||
|
||||
var storageDelta = inValue - outValue;
|
||||
// Mask rounding errors in power code.
|
||||
if (Math.Abs(storageDelta) < msg.Capacity / PrecisionRoundFactor)
|
||||
storageDelta = 0;
|
||||
|
||||
// Update power lines based on a ton of parameters.
|
||||
SetPowerLineState(InPowerLine, msg.SupplyingNetworkHasPower);
|
||||
SetPowerLineState(OutPowerLine, msg.LoadingNetworkHasPower);
|
||||
SetPowerLineState(InSecondPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge);
|
||||
SetPowerLineState(ChargePowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && storageDelta > 0);
|
||||
SetPowerLineState(PassthroughPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && msg.CanDischarge);
|
||||
SetPowerLineState(OutSecondPowerLine,
|
||||
msg.CanDischarge && (msg.Charge > 0 || msg.SupplyingNetworkHasPower && msg.CanCharge));
|
||||
SetPowerLineState(DischargePowerLine, storageDelta < 0);
|
||||
|
||||
// Update breakers.
|
||||
InBreaker.IsOn = msg.CanCharge;
|
||||
OutBreaker.IsOn = msg.CanDischarge;
|
||||
|
||||
// Update various power values.
|
||||
InValue.Text = FormatPower(inValue);
|
||||
OutValue.Text = FormatPower(outValue);
|
||||
PassthroughValue.Text = FormatPower(Math.Min(msg.CurrentReceiving, msg.CurrentSupply));
|
||||
ChargeMaxValue.Text = FormatPower(msg.MaxChargeRate);
|
||||
DischargeMaxValue.Text = FormatPower(msg.MaxSupply);
|
||||
ChargeCurrentValue.Text = FormatPower(Math.Max(0, storageDelta));
|
||||
DischargeCurrentValue.Text = FormatPower(Math.Max(0, -storageDelta));
|
||||
|
||||
// Update charge/discharge rate sliders.
|
||||
_suppressSliderEvents = true;
|
||||
ChargeRateSlider.MaxValue = msg.MaxMaxChargeRate;
|
||||
ChargeRateSlider.MinValue = msg.MinMaxChargeRate;
|
||||
ChargeRateSlider.Value = msg.MaxChargeRate;
|
||||
|
||||
DischargeRateSlider.MaxValue = msg.MaxMaxSupply;
|
||||
DischargeRateSlider.MinValue = msg.MinMaxSupply;
|
||||
DischargeRateSlider.Value = msg.MaxSupply;
|
||||
_suppressSliderEvents = false;
|
||||
|
||||
// Update ETA display.
|
||||
var storageEtaDiff = storageDelta > 0 ? (msg.Capacity - msg.Charge) * (1 / msg.Efficiency) : -msg.Charge;
|
||||
var etaTimeSeconds = storageEtaDiff / storageDelta;
|
||||
var etaTimeMinutes = etaTimeSeconds / 60.0;
|
||||
|
||||
EtaLabel.Text = _loc.GetString(
|
||||
storageDelta > 0 ? "battery-menu-eta-full" : "battery-menu-eta-empty");
|
||||
if (!double.IsFinite(etaTimeMinutes)
|
||||
|| Math.Abs(etaTimeMinutes) > NotApplicableEtaHighCutoffMinutes
|
||||
|| Math.Abs(etaTimeMinutes) < NotApplicableEtaLowCutoffMinutes)
|
||||
{
|
||||
EtaValue.Text = _loc.GetString("battery-menu-eta-value-na");
|
||||
}
|
||||
else
|
||||
{
|
||||
EtaValue.Text = _loc.GetString(
|
||||
etaTimeMinutes > MaxEtaValueMinutes ? "battery-menu-eta-value-max" : "battery-menu-eta-value",
|
||||
("minutes", Math.Min(Math.Ceiling(etaTimeMinutes), MaxEtaValueMinutes)));
|
||||
}
|
||||
|
||||
// Update storage display.
|
||||
StoredPercentageValue.Text = _loc.GetString(
|
||||
"battery-menu-stored-percent-value",
|
||||
("value", msg.Charge / msg.Capacity));
|
||||
StoredEnergyValue.Text = _loc.GetString(
|
||||
"battery-menu-stored-energy-value",
|
||||
("value", msg.Charge));
|
||||
|
||||
// Update charge meter.
|
||||
_storageLevel = ContentHelpers.RoundToNearestLevels(msg.Charge, msg.Capacity, _chargeMeterBoxes.Length);
|
||||
_hasStorageDelta = Math.Abs(storageDelta) > 0;
|
||||
}
|
||||
|
||||
private static Color DimStorageColor(Color color)
|
||||
{
|
||||
var hsv = Color.ToHsv(color);
|
||||
hsv.Z /= 5;
|
||||
return Color.FromHsv(hsv);
|
||||
}
|
||||
|
||||
private void SetPowerLineState(PanelContainer control, bool value)
|
||||
{
|
||||
control.PanelOverride = value ? _activePowerLineStyleBox : _inactivePowerLineStyleBox;
|
||||
}
|
||||
|
||||
private string FormatPower(float value)
|
||||
{
|
||||
return _loc.GetString("battery-menu-power-value", ("value", value));
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
// Pulse power lines.
|
||||
_powerPulseValue += args.DeltaSeconds * PowerPulseFactor;
|
||||
|
||||
var color = Color.InterpolateBetween(
|
||||
ActivePowerLineLowColor,
|
||||
ActivePowerLineHighColor,
|
||||
MathF.Sin(_powerPulseValue) / 2 + 1);
|
||||
_activePowerLineStyleBox.BackgroundColor = color;
|
||||
|
||||
// Update storage indicator and blink it.
|
||||
for (var i = 0; i < _chargeMeterBoxes.Length; i++)
|
||||
{
|
||||
var box = _chargeMeterBoxes[i];
|
||||
if (_storageLevel > i)
|
||||
{
|
||||
// On
|
||||
box.BackgroundColor = StorageColors[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
box.BackgroundColor = DimStorageColors[i];
|
||||
}
|
||||
}
|
||||
|
||||
_blinkPulseValue += args.DeltaSeconds;
|
||||
if (_blinkPulseValue > 1)
|
||||
{
|
||||
_blinkPulseValue -= 1;
|
||||
_blinkPulse ^= true;
|
||||
}
|
||||
|
||||
// If there is a storage delta (charging or discharging), we want to blink the highest bar.
|
||||
if (_hasStorageDelta)
|
||||
{
|
||||
// If there is no highest bar (UI completely at 0), then blink bar 0.
|
||||
var toBlink = Math.Max(0, _storageLevel - 1);
|
||||
_chargeMeterBoxes[toBlink].BackgroundColor =
|
||||
_blinkPulse ? StorageColors[toBlink] : DimStorageColors[toBlink];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,10 +66,60 @@ namespace Content.Client.Stack
|
||||
if (!_appearanceSystem.TryGetData<bool>(uid, StackVisuals.Hide, out var hidden, args.Component))
|
||||
hidden = false;
|
||||
|
||||
if (comp.LayerFunction != StackLayerFunction.None)
|
||||
ApplyLayerFunction((uid, comp), ref actual, ref maxCount);
|
||||
|
||||
if (comp.IsComposite)
|
||||
_counterSystem.ProcessCompositeSprite(uid, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
|
||||
else
|
||||
_counterSystem.ProcessOpaqueSprite(uid, comp.BaseLayer, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the actual and maxCount to change how stack amounts are displayed.
|
||||
/// </summary>
|
||||
/// <param name="ent">The entity considered.</param>
|
||||
/// <param name="actual">The actual number of items in the stack. Altered depending on the function to run.</param>
|
||||
/// <param name="maxCount">The maximum number of items in the stack. Altered depending on the function to run.</param>
|
||||
/// <returns>Whether or not a function was applied.</returns>
|
||||
private bool ApplyLayerFunction(Entity<StackComponent> ent, ref int actual, ref int maxCount)
|
||||
{
|
||||
switch (ent.Comp.LayerFunction)
|
||||
{
|
||||
case StackLayerFunction.Threshold:
|
||||
if (TryComp<StackLayerThresholdComponent>(ent, out var threshold))
|
||||
{
|
||||
ApplyThreshold(threshold, ref actual, ref maxCount);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// No function applied.
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects which layer a stack applies based on a list of thresholds.
|
||||
/// Each threshold passed results in the next layer being selected.
|
||||
/// </summary>
|
||||
/// <param name="comp">The threshold parameters to apply.</param>
|
||||
/// <param name="actual">The number of items in the stack. Will be set to the index of the layer to use.</param>
|
||||
/// <param name="maxCount">The maximum possible number of items in the stack. Will be set to the number of selectable layers.</param>
|
||||
private static void ApplyThreshold(StackLayerThresholdComponent comp, ref int actual, ref int maxCount)
|
||||
{
|
||||
// We must stop before we run out of thresholds or layers, whichever's smaller.
|
||||
maxCount = Math.Min(comp.Thresholds.Count + 1, maxCount);
|
||||
var newActual = 0;
|
||||
foreach (var threshold in comp.Thresholds)
|
||||
{
|
||||
//If our value exceeds threshold, the next layer should be displayed.
|
||||
//Note: we must ensure actual <= MaxCount.
|
||||
if (actual >= threshold && newActual < maxCount)
|
||||
newActual++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
actual = newActual;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Content.Client.Station;
|
||||
/// <summary>
|
||||
/// This handles letting the client know stations are a thing. Only really used by an admin menu.
|
||||
/// </summary>
|
||||
public sealed class StationSystem : EntitySystem
|
||||
public sealed partial class StationSystem : SharedStationSystem
|
||||
{
|
||||
private readonly List<(string Name, NetEntity Entity)> _stations = new();
|
||||
|
||||
@@ -15,11 +15,14 @@ public sealed class StationSystem : EntitySystem
|
||||
/// <remarks>
|
||||
/// I'd have this just invoke an entity query, but we're on the client and the client barely knows about stations.
|
||||
/// </remarks>
|
||||
// TODO: Stations have a global PVS override now, this can probably be changed into a query.
|
||||
public IReadOnlyList<(string Name, NetEntity Entity)> Stations => _stations;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<StationsUpdatedEvent>(StationsUpdated);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ namespace Content.Client.Stylesheets
|
||||
public const string StyleClassChatChannelSelectorButton = "chatSelectorOptionButton";
|
||||
public const string StyleClassChatFilterOptionButton = "chatFilterOptionButton";
|
||||
public const string StyleClassStorageButton = "storageButton";
|
||||
public const string StyleClassInset = "Inset";
|
||||
|
||||
public const string StyleClassConsoleHeading = "ConsoleHeading";
|
||||
public const string StyleClassConsoleSubHeading = "ConsoleSubHeading";
|
||||
public const string StyleClassConsoleText = "ConsoleText";
|
||||
|
||||
public const string StyleClassSliderRed = "Red";
|
||||
public const string StyleClassSliderGreen = "Green";
|
||||
@@ -179,6 +184,10 @@ namespace Content.Client.Stylesheets
|
||||
var notoSansBold18 = resCache.NotoStack(variation: "Bold", size: 18);
|
||||
var notoSansBold20 = resCache.NotoStack(variation: "Bold", size: 20);
|
||||
var notoSansMono = resCache.GetFont("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf", size: 12);
|
||||
var robotoMonoBold11 = resCache.GetFont("/Fonts/RobotoMono/RobotoMono-Bold.ttf", size: 11);
|
||||
var robotoMonoBold12 = resCache.GetFont("/Fonts/RobotoMono/RobotoMono-Bold.ttf", size: 12);
|
||||
var robotoMonoBold14 = resCache.GetFont("/Fonts/RobotoMono/RobotoMono-Bold.ttf", size: 14);
|
||||
|
||||
var windowHeaderTex = resCache.GetTexture("/Textures/Interface/Nano/window_header.png");
|
||||
var windowHeader = new StyleBoxTexture
|
||||
{
|
||||
@@ -413,9 +422,60 @@ namespace Content.Client.Stylesheets
|
||||
};
|
||||
progressBarForeground.SetContentMarginOverride(StyleBox.Margin.Vertical, 14.5f);
|
||||
|
||||
// Monotone (unfilled)
|
||||
var monotoneButton = new StyleBoxTexture
|
||||
{
|
||||
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button.svg.96dpi.png"),
|
||||
};
|
||||
monotoneButton.SetPatchMargin(StyleBox.Margin.All, 11);
|
||||
monotoneButton.SetPadding(StyleBox.Margin.All, 1);
|
||||
monotoneButton.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
|
||||
monotoneButton.SetContentMarginOverride(StyleBox.Margin.Horizontal, 14);
|
||||
|
||||
var monotoneButtonOpenLeft = new StyleBoxTexture(monotoneButton)
|
||||
{
|
||||
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button_open_left.svg.96dpi.png"),
|
||||
};
|
||||
|
||||
var monotoneButtonOpenRight = new StyleBoxTexture(monotoneButton)
|
||||
{
|
||||
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button_open_right.svg.96dpi.png"),
|
||||
};
|
||||
|
||||
var monotoneButtonOpenBoth = new StyleBoxTexture(monotoneButton)
|
||||
{
|
||||
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button_open_both.svg.96dpi.png"),
|
||||
};
|
||||
|
||||
// Monotone (filled)
|
||||
var monotoneFilledButton = new StyleBoxTexture(monotoneButton)
|
||||
{
|
||||
Texture = buttonTex,
|
||||
};
|
||||
|
||||
var monotoneFilledButtonOpenLeft = new StyleBoxTexture(monotoneButton)
|
||||
{
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
|
||||
};
|
||||
monotoneFilledButtonOpenLeft.SetPatchMargin(StyleBox.Margin.Left, 0);
|
||||
|
||||
var monotoneFilledButtonOpenRight = new StyleBoxTexture(monotoneButton)
|
||||
{
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
|
||||
};
|
||||
monotoneFilledButtonOpenRight.SetPatchMargin(StyleBox.Margin.Right, 0);
|
||||
|
||||
var monotoneFilledButtonOpenBoth = new StyleBoxTexture(monotoneButton)
|
||||
{
|
||||
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
|
||||
};
|
||||
monotoneFilledButtonOpenBoth.SetPatchMargin(StyleBox.Margin.Horizontal, 0);
|
||||
|
||||
// CheckBox
|
||||
var checkBoxTextureChecked = resCache.GetTexture("/Textures/Interface/Nano/checkbox_checked.svg.96dpi.png");
|
||||
var checkBoxTextureUnchecked = resCache.GetTexture("/Textures/Interface/Nano/checkbox_unchecked.svg.96dpi.png");
|
||||
var monotoneCheckBoxTextureChecked = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_checkbox_checked.svg.96dpi.png");
|
||||
var monotoneCheckBoxTextureUnchecked = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_checkbox_unchecked.svg.96dpi.png");
|
||||
|
||||
// Tooltip box
|
||||
var tooltipTexture = resCache.GetTexture("/Textures/Interface/Nano/tooltip.png");
|
||||
@@ -945,6 +1005,17 @@ namespace Content.Client.Stylesheets
|
||||
new StyleProperty(BoxContainer.StylePropertySeparation, 10),
|
||||
}),
|
||||
|
||||
// MonotoneCheckBox
|
||||
new StyleRule(new SelectorElement(typeof(TextureRect), new [] { MonotoneCheckBox.StyleClassMonotoneCheckBox }, null, null), new[]
|
||||
{
|
||||
new StyleProperty(TextureRect.StylePropertyTexture, monotoneCheckBoxTextureUnchecked),
|
||||
}),
|
||||
|
||||
new StyleRule(new SelectorElement(typeof(TextureRect), new [] { MonotoneCheckBox.StyleClassMonotoneCheckBox, CheckBox.StyleClassCheckBoxChecked }, null, null), new[]
|
||||
{
|
||||
new StyleProperty(TextureRect.StylePropertyTexture, monotoneCheckBoxTextureChecked),
|
||||
}),
|
||||
|
||||
// Tooltip
|
||||
new StyleRule(new SelectorElement(typeof(Tooltip), null, null, null), new[]
|
||||
{
|
||||
@@ -1143,6 +1214,22 @@ namespace Content.Client.Stylesheets
|
||||
new StyleProperty(Label.StylePropertyFontColor, Color.DarkGray),
|
||||
}),
|
||||
|
||||
// Console text
|
||||
new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassConsoleText}, null, null), new[]
|
||||
{
|
||||
new StyleProperty(Label.StylePropertyFont, robotoMonoBold11)
|
||||
}),
|
||||
|
||||
new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassConsoleSubHeading}, null, null), new[]
|
||||
{
|
||||
new StyleProperty(Label.StylePropertyFont, robotoMonoBold12)
|
||||
}),
|
||||
|
||||
new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassConsoleHeading}, null, null), new[]
|
||||
{
|
||||
new StyleProperty(Label.StylePropertyFont, robotoMonoBold14)
|
||||
}),
|
||||
|
||||
// Big Button
|
||||
new StyleRule(new SelectorChild(
|
||||
new SelectorElement(typeof(Button), new[] {StyleClassButtonBig}, null, null),
|
||||
@@ -1242,6 +1329,64 @@ namespace Content.Client.Stylesheets
|
||||
new StyleProperty(Label.StylePropertyFont, notoSansDisplayBold14),
|
||||
}),
|
||||
|
||||
// MonotoneButton (unfilled)
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), null, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneButton),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenLeft }, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneButtonOpenLeft),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenRight }, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneButtonOpenRight),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenBoth }, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneButtonOpenBoth),
|
||||
}),
|
||||
|
||||
// MonotoneButton (filled)
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), null, null, new[] { Button.StylePseudoClassPressed }),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButton),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenLeft }, null, new[] { Button.StylePseudoClassPressed }),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButtonOpenLeft),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenRight }, null, new[] { Button.StylePseudoClassPressed }),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButtonOpenRight),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenBoth }, null, new[] { Button.StylePseudoClassPressed }),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButtonOpenBoth),
|
||||
}),
|
||||
|
||||
// NanoHeading
|
||||
|
||||
new StyleRule(
|
||||
@@ -1675,7 +1820,11 @@ namespace Content.Client.Stylesheets
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Bwoink/un_pinned.png"))
|
||||
})
|
||||
}),
|
||||
|
||||
Element<PanelContainer>()
|
||||
.Class(StyleClassInset)
|
||||
.Prop(PanelContainer.StylePropertyPanel, insetBack),
|
||||
}).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
75
Content.Client/UserInterface/BuiPreTickUpdateSystem.cs
Normal file
75
Content.Client/UserInterface/BuiPreTickUpdateSystem.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for <see cref="BoundUserInterface"/>s that need some updating logic
|
||||
/// ran in the <see cref="ModUpdateLevel.PreEngine"/> stage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is called on all open <see cref="BoundUserInterface"/>s that implement this interface.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// One intended use case is coalescing input events (e.g. via <see cref="InputCoalescer{T}"/>) to send them to the
|
||||
/// server only once per tick.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="BuiPreTickUpdateSystem"/>
|
||||
public interface IBuiPreTickUpdate
|
||||
{
|
||||
void PreTickUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="BuiPreTickUpdateSystem"/>.
|
||||
/// </summary>
|
||||
public sealed class BuiPreTickUpdateSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = null!;
|
||||
|
||||
private EntityQuery<UserInterfaceUserComponent> _userQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_userQuery = GetEntityQuery<UserInterfaceUserComponent>();
|
||||
}
|
||||
|
||||
public void RunUpdates()
|
||||
{
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var localSession = _playerManager.LocalSession;
|
||||
if (localSession?.AttachedEntity is not { } localEntity)
|
||||
return;
|
||||
|
||||
if (!_userQuery.TryGetComponent(localEntity, out var userUIComp))
|
||||
return;
|
||||
|
||||
foreach (var (entity, uis) in userUIComp.OpenInterfaces)
|
||||
{
|
||||
foreach (var key in uis)
|
||||
{
|
||||
if (!_uiSystem.TryGetOpenUi(entity, key, out var ui))
|
||||
{
|
||||
DebugTools.Assert("Unable to find UI that was in the open UIs list??");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ui is IBuiPreTickUpdate tickUpdate)
|
||||
{
|
||||
tickUpdate.PreTickUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Content.Client/UserInterface/BuiPredictionState.cs
Normal file
80
Content.Client/UserInterface/BuiPredictionState.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// A local buffer for <see cref="BoundUserInterface"/>s to manually implement prediction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// In many current (and future) cases, it is not practically possible to implement prediction for UIs
|
||||
/// by implementing the logic in shared. At the same time, we want to implement prediction for the best user experience
|
||||
/// (and it is sometimes the easiest way to make even a middling user experience).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// You can queue predicted messages into this class with <see cref="SendMessage"/>,
|
||||
/// and then call <see cref="MessagesToReplay"/> later from <see cref="BoundUserInterface.UpdateState"/>
|
||||
/// to get all messages that are still "ahead" of the latest server state.
|
||||
/// These messages can then manually be "applied" to the latest state received from the server.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note that this system only works if the server is guaranteed to send some kind of update in response to UI messages,
|
||||
/// or at a regular schedule. If it does not, there is no opportunity to error correct the prediction.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class BuiPredictionState
|
||||
{
|
||||
private readonly BoundUserInterface _parent;
|
||||
private readonly IClientGameTiming _gameTiming;
|
||||
|
||||
private readonly Queue<MessageData> _queuedMessages = new();
|
||||
|
||||
public BuiPredictionState(BoundUserInterface parent, IClientGameTiming gameTiming)
|
||||
{
|
||||
_parent = parent;
|
||||
_gameTiming = gameTiming;
|
||||
}
|
||||
|
||||
public void SendMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
if (_gameTiming.IsFirstTimePredicted)
|
||||
{
|
||||
var messageData = new MessageData
|
||||
{
|
||||
TickSent = _gameTiming.CurTick,
|
||||
Message = message,
|
||||
};
|
||||
|
||||
_queuedMessages.Enqueue(messageData);
|
||||
}
|
||||
|
||||
_parent.SendPredictedMessage(message);
|
||||
}
|
||||
|
||||
public IEnumerable<BoundUserInterfaceMessage> MessagesToReplay()
|
||||
{
|
||||
var curTick = _gameTiming.LastRealTick;
|
||||
while (_queuedMessages.TryPeek(out var data) && data.TickSent <= curTick)
|
||||
{
|
||||
_queuedMessages.Dequeue();
|
||||
}
|
||||
|
||||
if (_queuedMessages.Count == 0)
|
||||
return [];
|
||||
|
||||
return _queuedMessages.Select(c => c.Message);
|
||||
}
|
||||
|
||||
private struct MessageData
|
||||
{
|
||||
public GameTick TickSent;
|
||||
public required BoundUserInterfaceMessage Message;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Message} @ {TickSent}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,4 +81,54 @@ namespace Content.Client.UserInterface.Controls
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions for working with <see cref="FancyWindow"/>.
|
||||
/// </summary>
|
||||
public static class FancyWindowExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets information for a window (title and guidebooks) based on an entity.
|
||||
/// </summary>
|
||||
/// <param name="window">The window to modify.</param>
|
||||
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
|
||||
/// <param name="entity">The entity that this window represents.</param>
|
||||
/// <seealso cref="SetTitleFromEntity"/>
|
||||
/// <seealso cref="SetGuidebookFromEntity"/>
|
||||
public static void SetInfoFromEntity(this FancyWindow window, IEntityManager entityManager, EntityUid entity)
|
||||
{
|
||||
window.SetTitleFromEntity(entityManager, entity);
|
||||
window.SetGuidebookFromEntity(entityManager, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a window's title to the name of an entity.
|
||||
/// </summary>
|
||||
/// <param name="window">The window to modify.</param>
|
||||
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
|
||||
/// <param name="entity">The entity that this window represents.</param>
|
||||
/// <seealso cref="SetInfoFromEntity"/>
|
||||
public static void SetTitleFromEntity(
|
||||
this FancyWindow window,
|
||||
IEntityManager entityManager,
|
||||
EntityUid entity)
|
||||
{
|
||||
window.Title = entityManager.GetComponent<MetaDataComponent>(entity).EntityName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a window's guidebook IDs to those of an entity.
|
||||
/// </summary>
|
||||
/// <param name="window">The window to modify.</param>
|
||||
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
|
||||
/// <param name="entity">The entity that this window represents.</param>
|
||||
/// <seealso cref="SetInfoFromEntity"/>
|
||||
public static void SetGuidebookFromEntity(
|
||||
this FancyWindow window,
|
||||
IEntityManager entityManager,
|
||||
EntityUid entity)
|
||||
{
|
||||
window.HelpGuidebookIds = entityManager.GetComponentOrNull<GuideHelpComponent>(entity)?.Guides;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,12 +264,6 @@ public class ListContainer : Control
|
||||
_updateChildren = false;
|
||||
|
||||
var toRemove = new Dictionary<ListData, ListContainerButton>(_buttons);
|
||||
foreach (var child in Children.ToArray())
|
||||
{
|
||||
if (child == _vScrollBar)
|
||||
continue;
|
||||
RemoveChild(child);
|
||||
}
|
||||
|
||||
if (_data.Count > 0)
|
||||
{
|
||||
@@ -292,8 +286,9 @@ public class ListContainer : Control
|
||||
|
||||
if (Toggle && data == _selected)
|
||||
button.Pressed = true;
|
||||
AddChild(button);
|
||||
}
|
||||
AddChild(button);
|
||||
button.SetPositionInParent(i - _topIndex);
|
||||
button.Measure(finalSize);
|
||||
}
|
||||
}
|
||||
|
||||
79
Content.Client/UserInterface/Controls/MonotoneButton.cs
Normal file
79
Content.Client/UserInterface/Controls/MonotoneButton.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Robust.Client.UserInterface.Controls.Label;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A button intended for use with a monotone color palette
|
||||
/// </summary>
|
||||
public sealed class MonotoneButton : ContainerButton
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the color of the label text when the button is pressed.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Color AltTextColor { set; get; } = new Color(0.2f, 0.2f, 0.2f);
|
||||
|
||||
/// <summary>
|
||||
/// The label that holds the button text.
|
||||
/// </summary>
|
||||
public Label Label { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The text displayed by the button.
|
||||
/// </summary>
|
||||
[PublicAPI, ViewVariables]
|
||||
public string? Text { get => Label.Text; set => Label.Text = value; }
|
||||
|
||||
/// <summary>
|
||||
/// How to align the text inside the button.
|
||||
/// </summary>
|
||||
[PublicAPI, ViewVariables]
|
||||
public AlignMode TextAlign { get => Label.Align; set => Label.Align = value; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the button will allow shrinking and clip text
|
||||
/// to prevent the text from going outside the bounds of the button.
|
||||
/// If false, the minimum size will always fit the contained text.
|
||||
/// </summary>
|
||||
[PublicAPI, ViewVariables]
|
||||
public bool ClipText
|
||||
{
|
||||
get => Label.ClipText;
|
||||
set => Label.ClipText = value;
|
||||
}
|
||||
|
||||
public MonotoneButton()
|
||||
{
|
||||
Label = new Label
|
||||
{
|
||||
StyleClasses = { StyleClassButton }
|
||||
};
|
||||
|
||||
AddChild(Label);
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
// Recolor the label
|
||||
if (Label != null)
|
||||
Label.ModulateSelfOverride = DrawMode == DrawModeEnum.Pressed ? AltTextColor : null;
|
||||
|
||||
// Modulate the button if disabled
|
||||
Modulate = Disabled ? Color.Gray : Color.White;
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
base.StylePropertiesChanged();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
UpdateAppearance();
|
||||
}
|
||||
}
|
||||
24
Content.Client/UserInterface/Controls/MonotoneCheckBox.cs
Normal file
24
Content.Client/UserInterface/Controls/MonotoneCheckBox.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A check box intended for use with a monotone color palette
|
||||
/// </summary>
|
||||
public sealed class MonotoneCheckBox : CheckBox
|
||||
{
|
||||
public const string StyleClassMonotoneCheckBox = "monotoneCheckBox";
|
||||
|
||||
public MonotoneCheckBox()
|
||||
{
|
||||
TextureRect.AddStyleClass(StyleClassMonotoneCheckBox);
|
||||
}
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
|
||||
// Appearance modulations
|
||||
Modulate = Disabled ? Color.Gray : Color.White;
|
||||
}
|
||||
}
|
||||
6
Content.Client/UserInterface/Controls/OnOffButton.xaml
Normal file
6
Content.Client/UserInterface/Controls/OnOffButton.xaml
Normal file
@@ -0,0 +1,6 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="OffButton" StyleClasses="OpenRight" Text="{Loc 'ui-button-off'}" />
|
||||
<Button Name="OnButton" StyleClasses="OpenLeft" Text="{Loc 'ui-button-on'}" />
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
48
Content.Client/UserInterface/Controls/OnOffButton.xaml.cs
Normal file
48
Content.Client/UserInterface/Controls/OnOffButton.xaml.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A simple control that displays a toggleable on/off button.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class OnOffButton : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the control is currently in the "on" state.
|
||||
/// </summary>
|
||||
public bool IsOn
|
||||
{
|
||||
get => OnButton.Pressed;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
OnButton.Pressed = true;
|
||||
else
|
||||
OffButton.Pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user changes the state of the control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not get raised if state is changed with <see cref="set_IsOn"/>.
|
||||
/// </remarks>
|
||||
public event Action<bool>? StateChanged;
|
||||
|
||||
public OnOffButton()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var group = new ButtonGroup(isNoneSetAllowed: false);
|
||||
OffButton.Group = group;
|
||||
OnButton.Group = group;
|
||||
|
||||
OffButton.OnPressed += _ => StateChanged?.Invoke(false);
|
||||
OnButton.OnPressed += _ => StateChanged?.Invoke(true);
|
||||
}
|
||||
}
|
||||
40
Content.Client/UserInterface/InputCoalescer.cs
Normal file
40
Content.Client/UserInterface/InputCoalescer.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// A simple utility class to "coalesce" multiple input events into a single one, fired later.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public struct InputCoalescer<T>
|
||||
{
|
||||
public bool IsModified;
|
||||
public T LastValue;
|
||||
|
||||
/// <summary>
|
||||
/// Replace the value in the <see cref="InputCoalescer{T}"/>. This sets <see cref="IsModified"/> to true.
|
||||
/// </summary>
|
||||
public void Set(T value)
|
||||
{
|
||||
LastValue = value;
|
||||
IsModified = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the <see cref="InputCoalescer{T}"/> has been modified.
|
||||
/// If it was, return the value and clear <see cref="IsModified"/>.
|
||||
/// </summary>
|
||||
/// <returns>True if the value was modified since the last check.</returns>
|
||||
public bool CheckIsModified([MaybeNullWhen(false)] out T value)
|
||||
{
|
||||
if (IsModified)
|
||||
{
|
||||
value = LastValue;
|
||||
IsModified = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return IsModified;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
|
||||
{
|
||||
public sealed class AlertControl : BaseButton
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public AlertPrototype Alert { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -33,8 +35,7 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
|
||||
private (TimeSpan Start, TimeSpan End)? _cooldown;
|
||||
|
||||
private short? _severity;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly IEntityManager _entityManager;
|
||||
|
||||
private readonly SpriteView _icon;
|
||||
private readonly CooldownGraphic _cooldownGraphic;
|
||||
|
||||
@@ -47,8 +48,10 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
|
||||
/// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
|
||||
public AlertControl(AlertPrototype alert, short? severity)
|
||||
{
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
_entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
// Alerts will handle this.
|
||||
MuteSounds = true;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
Alert = alert;
|
||||
_severity = severity;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
@@ -17,7 +18,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
|
||||
|
||||
public void SetOutputPressure(float value)
|
||||
{
|
||||
SendMessage(new GasTankSetPressureMessage
|
||||
SendPredictedMessage(new GasTankSetPressureMessage
|
||||
{
|
||||
Pressure = value
|
||||
});
|
||||
@@ -25,13 +26,14 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
|
||||
|
||||
public void ToggleInternals()
|
||||
{
|
||||
SendMessage(new GasTankToggleInternalsMessage());
|
||||
SendPredictedMessage(new GasTankToggleInternalsMessage());
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = this.CreateWindow<GasTankWindow>();
|
||||
_window.Entity = Owner;
|
||||
_window.SetTitle(EntMan.GetComponent<MetaDataComponent>(Owner).EntityName);
|
||||
_window.OnOutputPressure += SetOutputPressure;
|
||||
_window.OnToggleInternals += ToggleInternals;
|
||||
@@ -41,6 +43,12 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out GasTankComponent? component))
|
||||
{
|
||||
var canConnect = EntMan.System<SharedGasTankSystem>().CanConnectToInternals((Owner, component));
|
||||
_window?.Update(canConnect, component.IsConnected, component.OutputPressure);
|
||||
}
|
||||
|
||||
if (state is GasTankBoundUserInterfaceState cast)
|
||||
_window?.UpdateState(cast);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ using Content.Client.Message;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Timing;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Timing;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Atmos.GasTank;
|
||||
@@ -15,6 +18,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank;
|
||||
public sealed class GasTankWindow
|
||||
: BaseWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IResourceCache _cache = default!;
|
||||
|
||||
private readonly RichTextLabel _lblPressure;
|
||||
@@ -23,6 +27,8 @@ public sealed class GasTankWindow
|
||||
private readonly Button _btnInternals;
|
||||
private readonly Label _topLabel;
|
||||
|
||||
public EntityUid Entity;
|
||||
|
||||
public event Action<float>? OnOutputPressure;
|
||||
public event Action? OnToggleInternals;
|
||||
|
||||
@@ -194,12 +200,30 @@ public sealed class GasTankWindow
|
||||
public void UpdateState(GasTankBoundUserInterfaceState state)
|
||||
{
|
||||
_lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}")));
|
||||
_btnInternals.Disabled = !state.CanConnectInternals;
|
||||
}
|
||||
|
||||
public void Update(bool canConnectInternals, bool internalsConnected, float outputPressure)
|
||||
{
|
||||
_btnInternals.Disabled = !canConnectInternals;
|
||||
_lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text",
|
||||
("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
|
||||
if (state.OutputPressure.HasValue)
|
||||
("status", Loc.GetString(internalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
|
||||
_spbPressure.Value = outputPressure;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
// Easier than managing state on any ent changes. Previously this was just ticked on server's GasTankSystem.
|
||||
if (_entManager.TryGetComponent(Entity, out GasTankComponent? tank))
|
||||
{
|
||||
_spbPressure.Value = state.OutputPressure.Value;
|
||||
var canConnectInternals = _entManager.System<SharedGasTankSystem>().CanConnectToInternals((Entity, tank));
|
||||
_btnInternals.Disabled = !canConnectInternals;
|
||||
}
|
||||
|
||||
if (!_btnInternals.Disabled)
|
||||
{
|
||||
_btnInternals.Disabled = _entManager.System<UseDelaySystem>().IsDelayed(Entity, id: SharedGasTankSystem.GasTankDelay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -918,6 +918,11 @@ public sealed class ChatUIController : UIController
|
||||
_typingIndicator?.ClientChangedChatText();
|
||||
}
|
||||
|
||||
public void NotifyChatFocus(bool isFocused)
|
||||
{
|
||||
_typingIndicator?.ClientChangedChatFocus(isFocused);
|
||||
}
|
||||
|
||||
public void Repopulate()
|
||||
{
|
||||
foreach (var chat in _chats)
|
||||
|
||||
@@ -34,6 +34,8 @@ public partial class ChatBox : UIWidget
|
||||
ChatInput.Input.OnTextEntered += OnTextEntered;
|
||||
ChatInput.Input.OnKeyBindDown += OnInputKeyBindDown;
|
||||
ChatInput.Input.OnTextChanged += OnTextChanged;
|
||||
ChatInput.Input.OnFocusEnter += OnFocusEnter;
|
||||
ChatInput.Input.OnFocusExit += OnFocusExit;
|
||||
ChatInput.ChannelSelector.OnChannelSelect += OnChannelSelect;
|
||||
ChatInput.FilterButton.Popup.OnChannelFilter += OnChannelFilter;
|
||||
|
||||
@@ -174,6 +176,18 @@ public partial class ChatBox : UIWidget
|
||||
_controller.NotifyChatTextChange();
|
||||
}
|
||||
|
||||
private void OnFocusEnter(LineEditEventArgs args)
|
||||
{
|
||||
// Warn typing indicator about focus
|
||||
_controller.NotifyChatFocus(true);
|
||||
}
|
||||
|
||||
private void OnFocusExit(LineEditEventArgs args)
|
||||
{
|
||||
// Warn typing indicator about focus
|
||||
_controller.NotifyChatFocus(false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Client.UserInterface;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.VendingMachines.UI
|
||||
{
|
||||
@@ -162,7 +163,9 @@ namespace Content.Client.VendingMachines.UI
|
||||
continue;
|
||||
|
||||
var dummy = _dummies[proto];
|
||||
var amount = cachedInventory.First(o => o.ID == proto).Amount;
|
||||
if (!cachedInventory.TryFirstOrDefault(o => o.ID == proto, out var entry))
|
||||
continue;
|
||||
var amount = entry.Amount;
|
||||
// Could be better? Problem is all inventory entries get squashed.
|
||||
var text = GetItemText(dummy, amount);
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using Content.Client.Xenoarchaeology.Ui;
|
||||
using Content.Shared.Xenoarchaeology.Equipment;
|
||||
using Content.Shared.Xenoarchaeology.Equipment.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Xenoarchaeology.Equipment;
|
||||
|
||||
/// <inheritdoc cref="SharedNodeScannerSystem"/>
|
||||
public sealed class NodeScannerSystem : SharedNodeScannerSystem
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<NodeScannerComponent, AfterAutoHandleStateEvent>(OnAnalysisConsoleAfterAutoHandleState);
|
||||
}
|
||||
|
||||
protected override void TryOpenUi(Entity<NodeScannerComponent> device, EntityUid actor)
|
||||
{
|
||||
_ui.TryOpenUi(device.Owner, NodeScannerUiKey.Key, actor, true);
|
||||
}
|
||||
|
||||
private void OnAnalysisConsoleAfterAutoHandleState(Entity<NodeScannerComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (_ui.TryGetOpenUi<NodeScannerBoundUserInterface>(ent.Owner, NodeScannerUiKey.Key, out var bui))
|
||||
bui.Update(ent);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Shared.Xenoarchaeology.Equipment.Components;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Xenoarchaeology.Ui;
|
||||
@@ -18,15 +17,6 @@ public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) :
|
||||
|
||||
_scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
|
||||
_scannerDisplay.SetOwner(Owner);
|
||||
_scannerDisplay.OnClose += Close;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update UI state based on corresponding component.
|
||||
/// </summary>
|
||||
public void Update(Entity<NodeScannerComponent> ent)
|
||||
{
|
||||
_scannerDisplay?.Update(ent);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Content.Shared.Xenoarchaeology.Artifact;
|
||||
using Content.Shared.Xenoarchaeology.Artifact.Components;
|
||||
using Content.Shared.Xenoarchaeology.Equipment.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Xenoarchaeology.Ui;
|
||||
|
||||
@@ -10,12 +14,21 @@ namespace Content.Client.Xenoarchaeology.Ui;
|
||||
public sealed partial class NodeScannerDisplay : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _ent = default!;
|
||||
[Dependency] private readonly IGameTiming _timing= default!;
|
||||
|
||||
private readonly SharedXenoArtifactSystem _artifact;
|
||||
private TimeSpan? _nextUpdate;
|
||||
private EntityUid _owner;
|
||||
private TimeSpan _updateFromAttachedFrequency;
|
||||
private readonly HashSet<string> _triggeredNodeNames = new();
|
||||
|
||||
public NodeScannerDisplay()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_artifact = _ent.System<SharedXenoArtifactSystem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -30,30 +43,80 @@ public sealed partial class NodeScannerDisplay : FancyWindow
|
||||
return;
|
||||
}
|
||||
|
||||
Update((scannerEntityUid, scannerComponent));
|
||||
_updateFromAttachedFrequency = scannerComponent.DisplayDataUpdateInterval;
|
||||
_owner = scannerEntityUid;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if(_nextUpdate != null && _timing.CurTime < _nextUpdate)
|
||||
return;
|
||||
|
||||
_nextUpdate = _timing.CurTime + _updateFromAttachedFrequency;
|
||||
|
||||
if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
|
||||
{
|
||||
Update(false, ArtifactState.None);
|
||||
return;
|
||||
}
|
||||
|
||||
var attachedArtifactEnt = connectedScanner.AttachedTo;
|
||||
if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
|
||||
return;
|
||||
|
||||
_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
|
||||
|
||||
_triggeredNodeNames.Clear();
|
||||
ArtifactState artifactState;
|
||||
if (unlockingComponent == null)
|
||||
{
|
||||
var timeToUnlockAvailable = artifactComponent.NextUnlockTime - _timing.CurTime;
|
||||
artifactState = timeToUnlockAvailable > TimeSpan.Zero
|
||||
? ArtifactState.Cooldown
|
||||
: ArtifactState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
var triggeredIndexes = unlockingComponent.TriggeredNodeIndexes;
|
||||
|
||||
foreach (var triggeredIndex in triggeredIndexes)
|
||||
{
|
||||
var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
|
||||
var triggeredNodeName = (_ent.GetComponentOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
|
||||
_triggeredNodeNames.Add(triggeredNodeName);
|
||||
}
|
||||
|
||||
artifactState = ArtifactState.Unlocking;
|
||||
}
|
||||
|
||||
Update(true, artifactState, _triggeredNodeNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates labels with scanned artifact data and list of triggered nodes from component.
|
||||
/// </summary>
|
||||
public void Update(Entity<NodeScannerComponent> ent)
|
||||
private void Update(bool isConnected, ArtifactState artifactState, HashSet<string>? triggeredNodeNames = null)
|
||||
{
|
||||
ArtifactStateLabel.Text = GetState(ent);
|
||||
var scannedAt = ent.Comp.ScannedAt;
|
||||
NodeScannerState.Text = scannedAt > TimeSpan.Zero
|
||||
? Loc.GetString("node-scanner-artifact-scanned-time", ("time", scannedAt.Value.ToString(@"hh\:mm\:ss")))
|
||||
: Loc.GetString("node-scanner-artifact-scanned-time-none");
|
||||
ArtifactStateLabel.Text = GetStateText(artifactState);
|
||||
NodeScannerState.Text = isConnected
|
||||
? Loc.GetString("node-scanner-artifact-connected")
|
||||
: Loc.GetString("node-scanner-artifact-non-connected");
|
||||
|
||||
ActiveNodesList.Children.Clear();
|
||||
|
||||
var triggeredNodesSnapshot = ent.Comp.TriggeredNodesSnapshot;
|
||||
if (triggeredNodesSnapshot.Count > 0)
|
||||
if (triggeredNodeNames == null)
|
||||
return;
|
||||
|
||||
if (triggeredNodeNames.Count > 0)
|
||||
{
|
||||
// show list of triggered nodes instead of 'no data' placeholder
|
||||
NoActiveNodeDataLabel.Visible = false;
|
||||
ActiveNodesList.Visible = true;
|
||||
|
||||
foreach (var nodeId in triggeredNodesSnapshot)
|
||||
foreach (var nodeId in triggeredNodeNames)
|
||||
{
|
||||
var nodeLabel = new Button
|
||||
{
|
||||
@@ -73,9 +136,9 @@ public sealed partial class NodeScannerDisplay : FancyWindow
|
||||
}
|
||||
}
|
||||
|
||||
private string GetState(Entity<NodeScannerComponent> ent)
|
||||
private string GetStateText(ArtifactState state)
|
||||
{
|
||||
return ent.Comp.ArtifactState switch
|
||||
return state switch
|
||||
{
|
||||
ArtifactState.None => "\u2800", // placeholder for line to not be squeezed
|
||||
ArtifactState.Ready => Loc.GetString("node-scanner-artifact-state-ready"),
|
||||
|
||||
@@ -219,6 +219,7 @@ public sealed class CargoTest
|
||||
|
||||
- type: stack
|
||||
id: StackProto
|
||||
name: stack-steel
|
||||
spawn: A
|
||||
|
||||
- type: entity
|
||||
|
||||
@@ -181,7 +181,7 @@ public sealed class NukeOpsTest
|
||||
}
|
||||
|
||||
Assert.That(!entMan.EntityExists(nukieStationEnt)); // its not supposed to be a station!
|
||||
Assert.That(server.MapMan.MapExists(gridsRule.Map));
|
||||
Assert.That(mapSys.MapExists(gridsRule.Map));
|
||||
var nukieMap = mapSys.GetMap(gridsRule.Map!.Value);
|
||||
|
||||
var targetStation = entMan.GetComponent<StationDataComponent>(ruleComp.TargetStation!.Value);
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Power.Nodes;
|
||||
using Content.Shared.Coordinates;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -182,9 +182,9 @@ namespace Content.IntegrationTests.Tests
|
||||
var server = pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
|
||||
EntityUid packageRight;
|
||||
EntityUid packageWrong;
|
||||
@@ -255,7 +255,7 @@ namespace Content.IntegrationTests.Tests
|
||||
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent), Has.Count.GreaterThan(0),
|
||||
"Machine available inventory count is not greater than zero after restock.");
|
||||
|
||||
mapManager.DeleteMap(testMap.MapId);
|
||||
mapSystem.DeleteMap(testMap.MapId);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
@@ -269,9 +269,9 @@ namespace Content.IntegrationTests.Tests
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
|
||||
var damageableSystem = entitySystemManager.GetEntitySystem<DamageableSystem>();
|
||||
|
||||
@@ -319,7 +319,7 @@ namespace Content.IntegrationTests.Tests
|
||||
Assert.That(totalRamen, Is.EqualTo(2),
|
||||
"Did not find enough ramen after destroying restock box.");
|
||||
|
||||
mapManager.DeleteMap(testMap.MapId);
|
||||
mapSystem.DeleteMap(testMap.MapId);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
|
||||
@@ -197,7 +197,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
if (!PrivilegedIdIsAuthorized(uid, component))
|
||||
return;
|
||||
|
||||
if (!_interactionSystem.InRangeUnobstructed(uid, component.TargetAccessReaderId))
|
||||
if (!_interactionSystem.InRangeUnobstructed(player, component.TargetAccessReaderId))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("access-overrider-out-of-range"), player, player);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
@@ -5,6 +5,8 @@ using Content.Server.Chat.Managers;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Content.Shared.NodeContainer.NodeGroups;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Ame.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
@@ -84,10 +84,6 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
|
||||
|
||||
private void SpawnTiles(Entity<TileSpawnAnomalyComponent> anomaly, TileSpawnSettingsEntry entry, float stability, float severity, float powerMod)
|
||||
{
|
||||
var xform = Transform(anomaly);
|
||||
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
|
||||
return;
|
||||
|
||||
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings, powerMod);
|
||||
if (tiles == null)
|
||||
return;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Server.Atmos.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Used in internals as breath tool.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentProtoName("BreathMask")]
|
||||
public sealed partial class BreathToolComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool is functional only in allowed slots
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SlotFlags AllowedSlots = SlotFlags.MASK | SlotFlags.HEAD;
|
||||
public bool IsFunctional;
|
||||
|
||||
public EntityUid? ConnectedInternalsEntity;
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Atmos.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class GasTankComponent : Component, IGasMixtureHolder
|
||||
{
|
||||
public const float MaxExplosionRange = 26f;
|
||||
private const float DefaultLowPressure = 0f;
|
||||
private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
|
||||
|
||||
public int Integrity = 3;
|
||||
public bool IsLowPressure => (Air?.Pressure ?? 0F) <= TankLowPressure;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")]
|
||||
public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("connectSound")]
|
||||
public SoundSpecifier? ConnectSound =
|
||||
new SoundPathSpecifier("/Audio/Effects/internals.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(5f),
|
||||
};
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")]
|
||||
public SoundSpecifier? DisconnectSound;
|
||||
|
||||
// Cancel toggles sounds if we re-toggle again.
|
||||
|
||||
public EntityUid? ConnectStream;
|
||||
public EntityUid? DisconnectStream;
|
||||
|
||||
[DataField("air"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public GasMixture Air { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Pressure at which tank should be considered 'low' such as for internals.
|
||||
/// </summary>
|
||||
[DataField("tankLowPressure"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TankLowPressure = DefaultLowPressure;
|
||||
|
||||
/// <summary>
|
||||
/// Distributed pressure.
|
||||
/// </summary>
|
||||
[DataField("outputPressure"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float OutputPressure = DefaultOutputPressure;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed output pressure.
|
||||
/// </summary>
|
||||
[DataField("maxOutputPressure"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxOutputPressure = 3 * DefaultOutputPressure;
|
||||
|
||||
/// <summary>
|
||||
/// Tank is connected to internals.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool IsConnected => User != null;
|
||||
|
||||
[ViewVariables]
|
||||
public EntityUid? User;
|
||||
|
||||
/// <summary>
|
||||
/// True if this entity was recently moved out of a container. This might have been a hand -> inventory
|
||||
/// transfer, or it might have been the user dropping the tank. This indicates the tank needs to be checked.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool CheckUser;
|
||||
|
||||
/// <summary>
|
||||
/// Pressure at which tanks start leaking.
|
||||
/// </summary>
|
||||
[DataField("tankLeakPressure"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere;
|
||||
|
||||
/// <summary>
|
||||
/// Pressure at which tank spills all contents into atmosphere.
|
||||
/// </summary>
|
||||
[DataField("tankRupturePressure"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere;
|
||||
|
||||
/// <summary>
|
||||
/// Base 3x3 explosion.
|
||||
/// </summary>
|
||||
[DataField("tankFragmentPressure"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere;
|
||||
|
||||
/// <summary>
|
||||
/// Increases explosion for each scale kPa above threshold.
|
||||
/// </summary>
|
||||
[DataField("tankFragmentScale"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere;
|
||||
|
||||
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ToggleAction = "ActionToggleInternals";
|
||||
|
||||
[DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Valve to release gas from tank
|
||||
/// </summary>
|
||||
[DataField("isValveOpen"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool IsValveOpen = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gas release rate in L/s
|
||||
/// </summary>
|
||||
[DataField("valveOutputRate"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float ValveOutputRate = 100f;
|
||||
|
||||
[DataField("valveSound"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public SoundSpecifier ValveSound =
|
||||
new SoundCollectionSpecifier("valveSqueak")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-5f),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
|
||||
namespace Content.Server.Atmos.Consoles;
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Body.Components;
|
||||
|
||||
namespace Content.Server.Atmos.EntitySystems;
|
||||
|
||||
public sealed partial class AtmosphereSystem
|
||||
{
|
||||
private void InitializeBreathTool()
|
||||
{
|
||||
SubscribeLocalEvent<BreathToolComponent, ComponentShutdown>(OnBreathToolShutdown);
|
||||
}
|
||||
|
||||
private void OnBreathToolShutdown(Entity<BreathToolComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
DisconnectInternals(entity);
|
||||
}
|
||||
|
||||
public void DisconnectInternals(Entity<BreathToolComponent> entity)
|
||||
{
|
||||
var old = entity.Comp.ConnectedInternalsEntity;
|
||||
entity.Comp.ConnectedInternalsEntity = null;
|
||||
|
||||
if (TryComp<InternalsComponent>(old, out var internalsComponent))
|
||||
{
|
||||
_internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner);
|
||||
}
|
||||
|
||||
entity.Comp.IsFunctional = false;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public sealed partial class AtmosphereSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var mixtures = new GasMixture[8];
|
||||
var mixtures = new GasMixture[9];
|
||||
for (var i = 0; i < mixtures.Length; i++)
|
||||
mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
|
||||
|
||||
@@ -68,6 +68,10 @@ public sealed partial class AtmosphereSystem
|
||||
// 7: Nitrogen (101kpa) for vox rooms
|
||||
mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
|
||||
|
||||
// 8: Air (GM)
|
||||
mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner);
|
||||
mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner);
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))
|
||||
|
||||
@@ -44,8 +44,6 @@ public sealed partial class AtmosphereSystem
|
||||
|
||||
private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent component, ComponentInit args)
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
EnsureComp<GasTileOverlayComponent>(uid);
|
||||
foreach (var tile in component.Tiles.Values)
|
||||
{
|
||||
|
||||
@@ -28,7 +28,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLog = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly InternalsSystem _internals = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containers = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly GasTileOverlaySystem _gasTileOverlaySystem = default!;
|
||||
@@ -56,7 +55,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
|
||||
|
||||
UpdatesAfter.Add(typeof(NodeGroupSystem));
|
||||
|
||||
InitializeBreathTool();
|
||||
InitializeGases();
|
||||
InitializeCommands();
|
||||
InitializeCVars();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.NodeContainer;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Configuration;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -23,14 +15,11 @@ using Content.Shared.CCVar;
|
||||
namespace Content.Server.Atmos.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class GasTankSystem : EntitySystem
|
||||
public sealed class GasTankSystem : SharedGasTankSystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly ExplosionSystem _explosions = default!;
|
||||
[Dependency] private readonly InternalsSystem _internals = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSys = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containers = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwing = default!;
|
||||
@@ -44,17 +33,9 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GasTankComponent, ComponentShutdown>(OnGasShutdown);
|
||||
SubscribeLocalEvent<GasTankComponent, BeforeActivatableUIOpenEvent>(BeforeUiOpen);
|
||||
SubscribeLocalEvent<GasTankComponent, GetItemActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<GasTankComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<GasTankComponent, ToggleActionEvent>(OnActionToggle);
|
||||
SubscribeLocalEvent<GasTankComponent, EntParentChangedMessage>(OnParentChange);
|
||||
SubscribeLocalEvent<GasTankComponent, GasTankSetPressureMessage>(OnGasTankSetPressure);
|
||||
SubscribeLocalEvent<GasTankComponent, GasTankToggleInternalsMessage>(OnGasTankToggleInternals);
|
||||
SubscribeLocalEvent<GasTankComponent, GasAnalyzerScanEvent>(OnAnalyzed);
|
||||
SubscribeLocalEvent<GasTankComponent, PriceCalculationEvent>(OnGasTankPrice);
|
||||
SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
|
||||
Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true);
|
||||
}
|
||||
|
||||
@@ -63,44 +44,16 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
_maxExplosionRange = value;
|
||||
}
|
||||
|
||||
private void OnGasShutdown(Entity<GasTankComponent> gasTank, ref ComponentShutdown args)
|
||||
{
|
||||
DisconnectFromInternals(gasTank);
|
||||
}
|
||||
|
||||
private void OnGasTankToggleInternals(Entity<GasTankComponent> ent, ref GasTankToggleInternalsMessage args)
|
||||
{
|
||||
ToggleInternals(ent);
|
||||
}
|
||||
|
||||
private void OnGasTankSetPressure(Entity<GasTankComponent> ent, ref GasTankSetPressureMessage args)
|
||||
{
|
||||
var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure);
|
||||
|
||||
ent.Comp.OutputPressure = pressure;
|
||||
|
||||
UpdateUserInterface(ent, true);
|
||||
}
|
||||
|
||||
public void UpdateUserInterface(Entity<GasTankComponent> ent, bool initialUpdate = false)
|
||||
public override void UpdateUserInterface(Entity<GasTankComponent> ent)
|
||||
{
|
||||
var (owner, component) = ent;
|
||||
_ui.SetUiState(owner, SharedGasTankUiKey.Key,
|
||||
new GasTankBoundUserInterfaceState
|
||||
{
|
||||
TankPressure = component.Air?.Pressure ?? 0,
|
||||
OutputPressure = initialUpdate ? component.OutputPressure : null,
|
||||
InternalsConnected = component.IsConnected,
|
||||
CanConnectInternals = CanConnectToInternals(ent)
|
||||
});
|
||||
}
|
||||
|
||||
private void BeforeUiOpen(Entity<GasTankComponent> ent, ref BeforeActivatableUIOpenEvent args)
|
||||
{
|
||||
// Only initial update includes output pressure information, to avoid overwriting client-input as the updates come in.
|
||||
UpdateUserInterface(ent, true);
|
||||
}
|
||||
|
||||
private void OnParentChange(EntityUid uid, GasTankComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
// When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor.
|
||||
@@ -109,30 +62,6 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
component.CheckUser = true;
|
||||
}
|
||||
|
||||
private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
|
||||
{
|
||||
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args)
|
||||
{
|
||||
using var _ = args.PushGroup(nameof(GasTankComponent));
|
||||
if (args.IsInDetailsRange)
|
||||
args.PushMarkup(Loc.GetString("comp-gas-tank-examine", ("pressure", Math.Round(component.Air?.Pressure ?? 0))));
|
||||
if (component.IsConnected)
|
||||
args.PushMarkup(Loc.GetString("comp-gas-tank-connected"));
|
||||
args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve"));
|
||||
}
|
||||
|
||||
private void OnActionToggle(Entity<GasTankComponent> gasTank, ref ToggleActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
ToggleInternals(gasTank);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
@@ -167,8 +96,10 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
{
|
||||
_atmosphereSystem.React(comp.Air, comp);
|
||||
}
|
||||
|
||||
CheckStatus(gasTank);
|
||||
if (_ui.IsUiOpen(uid, SharedGasTankUiKey.Key))
|
||||
|
||||
if ((comp.IsConnected || comp.IsValveOpen) && _ui.IsUiOpen(uid, SharedGasTankUiKey.Key))
|
||||
{
|
||||
UpdateUserInterface(gasTank);
|
||||
}
|
||||
@@ -190,18 +121,6 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
_audioSys.PlayPvs(gasTank.Comp.RuptureSound, gasTank);
|
||||
}
|
||||
|
||||
private void ToggleInternals(Entity<GasTankComponent> ent)
|
||||
{
|
||||
if (ent.Comp.IsConnected)
|
||||
{
|
||||
DisconnectFromInternals(ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConnectToInternals(ent);
|
||||
}
|
||||
}
|
||||
|
||||
public GasMixture? RemoveAir(Entity<GasTankComponent> gasTank, float amount)
|
||||
{
|
||||
var gas = gasTank.Comp.Air?.Remove(amount);
|
||||
@@ -227,95 +146,6 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
return air;
|
||||
}
|
||||
|
||||
public bool CanConnectToInternals(Entity<GasTankComponent> ent)
|
||||
{
|
||||
TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User);
|
||||
return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen;
|
||||
}
|
||||
|
||||
public void ConnectToInternals(Entity<GasTankComponent> ent)
|
||||
{
|
||||
var (owner, component) = ent;
|
||||
if (component.IsConnected || !CanConnectToInternals(ent))
|
||||
return;
|
||||
|
||||
TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, ent.Comp.User);
|
||||
if (internalsUid == null || internalsComp == null)
|
||||
return;
|
||||
|
||||
if (_internals.TryConnectTank((internalsUid.Value, internalsComp), owner))
|
||||
component.User = internalsUid.Value;
|
||||
|
||||
_actions.SetToggled(component.ToggleActionEntity, component.IsConnected);
|
||||
|
||||
// Couldn't toggle!
|
||||
if (!component.IsConnected)
|
||||
return;
|
||||
|
||||
component.ConnectStream = _audioSys.Stop(component.ConnectStream);
|
||||
component.ConnectStream = _audioSys.PlayPvs(component.ConnectSound, owner)?.Entity;
|
||||
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
public void DisconnectFromInternals(Entity<GasTankComponent> ent)
|
||||
{
|
||||
var (owner, component) = ent;
|
||||
|
||||
if (component.User == null)
|
||||
return;
|
||||
|
||||
TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, component.User);
|
||||
component.User = null;
|
||||
|
||||
_actions.SetToggled(component.ToggleActionEntity, false);
|
||||
|
||||
if (internalsUid != null && internalsComp != null)
|
||||
_internals.DisconnectTank((internalsUid.Value, internalsComp));
|
||||
component.DisconnectStream = _audioSys.Stop(component.DisconnectStream);
|
||||
component.DisconnectStream = _audioSys.PlayPvs(component.DisconnectSound, owner)?.Entity;
|
||||
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the internals component of either the gas tank's user,
|
||||
/// or the gas tank's... containing container
|
||||
/// </summary>
|
||||
/// <param name="user">The user of the gas tank</param>
|
||||
/// <returns>True if internals comp isn't null, false if it is null</returns>
|
||||
private bool TryGetInternalsComp(Entity<GasTankComponent> ent, out EntityUid? internalsUid, out InternalsComponent? internalsComp, EntityUid? user = null)
|
||||
{
|
||||
internalsUid = default;
|
||||
internalsComp = default;
|
||||
|
||||
// If the gas tank doesn't exist for whatever reason, don't even bother
|
||||
if (TerminatingOrDeleted(ent.Owner))
|
||||
return false;
|
||||
|
||||
user ??= ent.Comp.User;
|
||||
// Check if the gas tank's user actually has the component that allows them to use a gas tank and mask
|
||||
if (TryComp<InternalsComponent>(user, out var userInternalsComp) && userInternalsComp != null)
|
||||
{
|
||||
internalsUid = user;
|
||||
internalsComp = userInternalsComp;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Yeah I have no clue what this actually does, I appreciate the lack of comments on the original function
|
||||
if (_containers.TryGetContainingContainer((ent.Owner, Transform(ent.Owner)), out var container) && container != null)
|
||||
{
|
||||
if (TryComp<InternalsComponent>(container.Owner, out var containerInternalsComp) && containerInternalsComp != null)
|
||||
{
|
||||
internalsUid = container.Owner;
|
||||
internalsComp = containerInternalsComp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void AssumeAir(Entity<GasTankComponent> ent, GasMixture giver)
|
||||
{
|
||||
_atmosphereSystem.Merge(ent.Comp.Air, giver);
|
||||
@@ -404,21 +234,5 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
{
|
||||
args.Price += _atmosphereSystem.GetPrice(component.Air);
|
||||
}
|
||||
|
||||
private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
|
||||
return;
|
||||
args.Verbs.Add(new AlternativeVerb()
|
||||
{
|
||||
Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"),
|
||||
Act = () =>
|
||||
{
|
||||
component.IsValveOpen = !component.IsValveOpen;
|
||||
_audioSys.PlayPvs(component.ValveSound, uid);
|
||||
},
|
||||
Disabled = component.IsConnected,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Server.Atmos
|
||||
{
|
||||
public interface IGasMixtureHolder
|
||||
{
|
||||
public GasMixture Air { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.EntitySystems;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Server.Popups;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("port")]
|
||||
public string PortName { get; set; } = "port";
|
||||
|
||||
/// <summary>
|
||||
/// Container name for the gas tank holder.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("container")]
|
||||
public string ContainerName { get; set; } = "tank_slot";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public ItemSlot GasTankSlot = new();
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("gasMixture")]
|
||||
public GasMixture Air { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Last recorded pressure, for appearance-updating purposes.
|
||||
/// </summary>
|
||||
public float LastPressure { get; set; } = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum release pressure possible for the release valve.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("minReleasePressure")]
|
||||
public float MinReleasePressure { get; set; } = Atmospherics.OneAtmosphere / 10;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum release pressure possible for the release valve.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("maxReleasePressure")]
|
||||
public float MaxReleasePressure { get; set; } = Atmospherics.OneAtmosphere * 10;
|
||||
|
||||
/// <summary>
|
||||
/// Valve release pressure.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("releasePressure")]
|
||||
public float ReleasePressure { get; set; } = Atmospherics.OneAtmosphere;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the release valve is open on the canister.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("releaseValve")]
|
||||
public bool ReleaseValve { get; set; } = false;
|
||||
|
||||
[DataField("accessDeniedSound")]
|
||||
public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
|
||||
|
||||
#region GuidebookData
|
||||
|
||||
[GuidebookData]
|
||||
public float Volume => Air.Volume;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,32 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.Atmos.Piping.Unary.Components;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Atmos.Piping.Unary.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Lock;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.NodeContainer;
|
||||
using GasCanisterComponent = Content.Shared.Atmos.Piping.Unary.Components.GasCanisterComponent;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.EntitySystems;
|
||||
|
||||
public sealed class GasCanisterSystem : EntitySystem
|
||||
public sealed class GasCanisterSystem : SharedGasCanisterSystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmos = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
|
||||
[Dependency] private readonly ItemSlotsSystem _slots = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GasCanisterComponent, ComponentStartup>(OnCanisterStartup);
|
||||
SubscribeLocalEvent<GasCanisterComponent, AtmosDeviceUpdateEvent>(OnCanisterUpdated);
|
||||
SubscribeLocalEvent<GasCanisterComponent, ActivateInWorldEvent>(OnCanisterActivate, after: new[] { typeof(LockSystem) });
|
||||
SubscribeLocalEvent<GasCanisterComponent, InteractHandEvent>(OnCanisterInteractHand);
|
||||
SubscribeLocalEvent<GasCanisterComponent, ItemSlotInsertAttemptEvent>(OnCanisterInsertAttempt);
|
||||
SubscribeLocalEvent<GasCanisterComponent, EntInsertedIntoContainerMessage>(OnCanisterContainerInserted);
|
||||
SubscribeLocalEvent<GasCanisterComponent, EntRemovedFromContainerMessage>(OnCanisterContainerRemoved);
|
||||
SubscribeLocalEvent<GasCanisterComponent, PriceCalculationEvent>(CalculateCanisterPrice);
|
||||
SubscribeLocalEvent<GasCanisterComponent, GasAnalyzerScanEvent>(OnAnalyzed);
|
||||
// Bound UI subscriptions
|
||||
SubscribeLocalEvent<GasCanisterComponent, GasCanisterHoldingTankEjectMessage>(OnHoldingTankEjectMessage);
|
||||
SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleasePressureMessage>(OnCanisterChangeReleasePressure);
|
||||
SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleaseValveMessage>(OnCanisterChangeReleaseValve);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,24 +42,16 @@ public sealed class GasCanisterSystem : EntitySystem
|
||||
if (environment is not null)
|
||||
_atmos.Merge(environment, canister.Air);
|
||||
|
||||
_adminLogger.Add(LogType.CanisterPurged, LogImpact.Medium, $"Canister {ToPrettyString(uid):canister} purged its contents of {canister.Air:gas} into the environment.");
|
||||
AdminLogger.Add(LogType.CanisterPurged, LogImpact.Medium, $"Canister {ToPrettyString(uid):canister} purged its contents of {canister.Air:gas} into the environment.");
|
||||
canister.Air.Clear();
|
||||
}
|
||||
|
||||
private void OnCanisterStartup(EntityUid uid, GasCanisterComponent comp, ComponentStartup args)
|
||||
{
|
||||
// Ensure container
|
||||
_slots.AddItemSlot(uid, comp.ContainerName, comp.GasTankSlot);
|
||||
}
|
||||
|
||||
private void DirtyUI(EntityUid uid,
|
||||
GasCanisterComponent? canister = null, NodeContainerComponent? nodeContainer = null)
|
||||
protected override void DirtyUI(EntityUid uid, GasCanisterComponent? canister = null, NodeContainerComponent? nodeContainer = null)
|
||||
{
|
||||
if (!Resolve(uid, ref canister, ref nodeContainer))
|
||||
return;
|
||||
|
||||
var portStatus = false;
|
||||
string? tankLabel = null;
|
||||
var tankPressure = 0f;
|
||||
|
||||
if (_nodeContainer.TryGetNode(nodeContainer, canister.PortName, out PipeNode? portNode) && portNode.NodeGroup?.Nodes.Count > 1)
|
||||
@@ -92,62 +61,11 @@ public sealed class GasCanisterSystem : EntitySystem
|
||||
{
|
||||
var tank = canister.GasTankSlot.Item.Value;
|
||||
var tankComponent = Comp<GasTankComponent>(tank);
|
||||
tankLabel = Name(tank);
|
||||
tankPressure = tankComponent.Air.Pressure;
|
||||
}
|
||||
|
||||
_ui.SetUiState(uid, GasCanisterUiKey.Key,
|
||||
new GasCanisterBoundUserInterfaceState(Name(uid),
|
||||
canister.Air.Pressure, portStatus, tankLabel, tankPressure, canister.ReleasePressure,
|
||||
canister.ReleaseValve, canister.MinReleasePressure, canister.MaxReleasePressure));
|
||||
}
|
||||
|
||||
private void OnHoldingTankEjectMessage(EntityUid uid, GasCanisterComponent canister, GasCanisterHoldingTankEjectMessage args)
|
||||
{
|
||||
if (canister.GasTankSlot.Item == null)
|
||||
return;
|
||||
|
||||
var item = canister.GasTankSlot.Item;
|
||||
_slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor);
|
||||
|
||||
if (canister.ReleaseValve)
|
||||
{
|
||||
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
|
||||
{
|
||||
var pressure = Math.Clamp(args.Pressure, canister.MinReleasePressure, canister.MaxReleasePressure);
|
||||
|
||||
_adminLogger.Add(LogType.CanisterPressure, LogImpact.Medium, $"{ToPrettyString(args.Actor):player} set the release pressure on {ToPrettyString(uid):canister} to {args.Pressure}");
|
||||
|
||||
canister.ReleasePressure = pressure;
|
||||
DirtyUI(uid, canister);
|
||||
}
|
||||
|
||||
private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
|
||||
{
|
||||
// filling a jetpack with plasma is less important than filling a room with it
|
||||
var hasItem = canister.GasTankSlot.HasItem;
|
||||
var impact = hasItem ? LogImpact.Medium : LogImpact.High;
|
||||
|
||||
_adminLogger.Add(
|
||||
LogType.CanisterValve,
|
||||
impact,
|
||||
$"{ToPrettyString(args.Actor):player} {(args.Valve ? "opened" : "closed")} the valve on {ToPrettyString(uid):canister} to {(hasItem ? "inserted tank" : "environment")} while it contained [{GetContainedGasesString((uid, canister))}]");
|
||||
|
||||
canister.ReleaseValve = args.Valve;
|
||||
DirtyUI(uid, canister);
|
||||
}
|
||||
|
||||
private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
|
||||
{
|
||||
return string.Join(", ", canister.Comp.Air);
|
||||
UI.SetUiState(uid, GasCanisterUiKey.Key,
|
||||
new GasCanisterBoundUserInterfaceState(canister.Air.Pressure, portStatus, tankPressure));
|
||||
}
|
||||
|
||||
private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args)
|
||||
@@ -207,76 +125,6 @@ public sealed class GasCanisterSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanisterActivate(EntityUid uid, GasCanisterComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (!args.Complex)
|
||||
return;
|
||||
|
||||
if (!TryComp<ActorComponent>(args.User, out var actor))
|
||||
return;
|
||||
|
||||
if (CheckLocked(uid, component, args.User))
|
||||
return;
|
||||
|
||||
// Needs to be here so the locked check still happens if the canister
|
||||
// is locked and you don't have permissions
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
_ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCanisterInteractHand(EntityUid uid, GasCanisterComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(args.User, out var actor))
|
||||
return;
|
||||
|
||||
if (CheckLocked(uid, component, args.User))
|
||||
return;
|
||||
|
||||
_ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent component, ref ItemSlotInsertAttemptEvent args)
|
||||
{
|
||||
if (args.Slot.ID != component.ContainerName || args.User == null)
|
||||
return;
|
||||
|
||||
if (!TryComp<GasTankComponent>(args.Item, out var gasTank) || gasTank.IsValveOpen)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Preventing inserting a tank since if its locked you cant remove it.
|
||||
if (!CheckLocked(uid, component, args.User.Value))
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnCanisterContainerInserted(EntityUid uid, GasCanisterComponent component, EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (args.Container.ID != component.ContainerName)
|
||||
return;
|
||||
|
||||
DirtyUI(uid, component);
|
||||
|
||||
_appearance.SetData(uid, GasCanisterVisuals.TankInserted, true);
|
||||
}
|
||||
|
||||
private void OnCanisterContainerRemoved(EntityUid uid, GasCanisterComponent component, EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (args.Container.ID != component.ContainerName)
|
||||
return;
|
||||
|
||||
DirtyUI(uid, component);
|
||||
|
||||
_appearance.SetData(uid, GasCanisterVisuals.TankInserted, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mix air from a gas container into a pipe net.
|
||||
/// Useful for anything that uses connector ports.
|
||||
@@ -317,23 +165,4 @@ public sealed class GasCanisterSystem : EntitySystem
|
||||
args.GasMixtures.Add((Name(tank), tankComponent.Air));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the canister is locked, playing its sound and popup if so.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// True if locked, false otherwise.
|
||||
/// </returns>
|
||||
private bool CheckLocked(EntityUid uid, GasCanisterComponent comp, EntityUid user)
|
||||
{
|
||||
if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("gas-canister-popup-denied"), uid, user);
|
||||
_audio.PlayPvs(comp.AccessDeniedSound, uid);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
{
|
||||
@@ -41,7 +42,6 @@ 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!;
|
||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||
@@ -60,7 +60,7 @@ 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, GetVerbsEvent<Verb>>(OnGetVerbs);
|
||||
SubscribeLocalEvent<GasVentPumpComponent, VentScrewedDoAfterEvent>(OnVentScrewed);
|
||||
}
|
||||
|
||||
@@ -379,23 +379,43 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
{
|
||||
UpdateState(uid, component);
|
||||
}
|
||||
private void OnInteractUsing(EntityUid uid, GasVentPumpComponent component, InteractUsingEvent args)
|
||||
|
||||
private void OnGetVerbs(Entity<GasVentPumpComponent> ent, ref GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (args.Handled
|
||||
|| component.UnderPressureLockout == false
|
||||
|| !_toolSystem.HasQuality(args.Used, "Screwing")
|
||||
|| !Transform(uid).Anchored
|
||||
)
|
||||
{
|
||||
if (ent.Comp.UnderPressureLockout == false || !Transform(ent).Anchored)
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
var user = args.User;
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.ManualLockoutDisableDoAfter, new VentScrewedDoAfterEvent(), uid, uid, args.Used));
|
||||
var v = new Verb
|
||||
{
|
||||
Priority = 1,
|
||||
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/unlock.svg.192dpi.png")),
|
||||
Text = Loc.GetString("gas-vent-pump-release-lockout"),
|
||||
Impact = LogImpact.Low,
|
||||
DoContactInteraction = true,
|
||||
Act = () =>
|
||||
{
|
||||
var doAfter = new DoAfterArgs(EntityManager, user, ent.Comp.ManualLockoutDisableDoAfter, new VentScrewedDoAfterEvent(), ent, ent)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
NeedHand = true,
|
||||
BreakOnMove = true,
|
||||
BreakOnWeightlessMove = true,
|
||||
};
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(doAfter);
|
||||
},
|
||||
};
|
||||
|
||||
args.Verbs.Add(v);
|
||||
}
|
||||
|
||||
private void OnVentScrewed(EntityUid uid, GasVentPumpComponent component, VentScrewedDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Handled)
|
||||
return;
|
||||
|
||||
component.ManualLockoutReenabledAt = _timing.CurTime + component.ManualLockoutDisabledDuration;
|
||||
component.IsPressureLockoutManuallyDisabled = true;
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Body.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class InternalsComponent : Component
|
||||
{
|
||||
[ViewVariables]
|
||||
public EntityUid? GasTankEntity;
|
||||
|
||||
[ViewVariables]
|
||||
public HashSet<EntityUid> BreathTools { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Toggle Internals delay when the target is not you.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(3);
|
||||
|
||||
[DataField]
|
||||
public ProtoId<AlertPrototype> InternalsAlert = "Internals";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,21 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Internals;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Body.Systems;
|
||||
|
||||
public sealed class InternalsSystem : EntitySystem
|
||||
public sealed class InternalsSystem : SharedInternalsSystem
|
||||
{
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmos = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly GasTankSystem _gasTank = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly RespiratorSystem _respirator = default!;
|
||||
|
||||
private EntityQuery<InternalsComponent> _internalsQuery;
|
||||
@@ -34,12 +27,6 @@ public sealed class InternalsSystem : EntitySystem
|
||||
_internalsQuery = GetEntityQuery<InternalsComponent>();
|
||||
|
||||
SubscribeLocalEvent<InternalsComponent, InhaleLocationEvent>(OnInhaleLocation);
|
||||
SubscribeLocalEvent<InternalsComponent, ComponentStartup>(OnInternalsStartup);
|
||||
SubscribeLocalEvent<InternalsComponent, ComponentShutdown>(OnInternalsShutdown);
|
||||
SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
|
||||
SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<InternalsComponent, ToggleInternalsAlertEvent>(OnToggleInternalsAlert);
|
||||
|
||||
SubscribeLocalEvent<InternalsComponent, StartingGearEquippedEvent>(OnStartingGear);
|
||||
}
|
||||
|
||||
@@ -66,120 +53,6 @@ public sealed class InternalsSystem : EntitySystem
|
||||
ToggleInternals(uid, uid, force: false, component);
|
||||
}
|
||||
|
||||
private void OnGetInteractionVerbs(
|
||||
Entity<InternalsComponent> ent,
|
||||
ref GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract || args.Hands is null)
|
||||
return;
|
||||
|
||||
if (!AreInternalsWorking(ent) && ent.Comp.BreathTools.Count == 0)
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
ToggleInternals(ent, user, force: false, ent);
|
||||
},
|
||||
Message = Loc.GetString("action-description-internals-toggle"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
|
||||
Text = Loc.GetString("action-name-internals-toggle"),
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
public void ToggleInternals(
|
||||
EntityUid uid,
|
||||
EntityUid user,
|
||||
bool force,
|
||||
InternalsComponent? internals = null)
|
||||
{
|
||||
if (!Resolve(uid, ref internals, logMissing: false))
|
||||
return;
|
||||
|
||||
// Toggle off if they're on
|
||||
if (AreInternalsWorking(internals))
|
||||
{
|
||||
if (force)
|
||||
{
|
||||
DisconnectTank((uid, internals));
|
||||
return;
|
||||
}
|
||||
|
||||
StartToggleInternalsDoAfter(user, (uid, internals));
|
||||
return;
|
||||
}
|
||||
|
||||
// If they're not on then check if we have a mask to use
|
||||
if (internals.BreathTools.Count == 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("internals-no-breath-tool"), uid, user);
|
||||
return;
|
||||
}
|
||||
|
||||
var tank = FindBestGasTank(uid);
|
||||
|
||||
if (tank is null)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("internals-no-tank"), uid, user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force)
|
||||
{
|
||||
StartToggleInternalsDoAfter(user, (uid, internals));
|
||||
return;
|
||||
}
|
||||
|
||||
_gasTank.ConnectToInternals(tank.Value);
|
||||
}
|
||||
|
||||
private void StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsComponent> targetEnt)
|
||||
{
|
||||
// Is the target not you? If yes, use a do-after to give them time to respond.
|
||||
var isUser = user == targetEnt.Owner;
|
||||
var delay = !isUser ? targetEnt.Comp.Delay : TimeSpan.Zero;
|
||||
|
||||
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
MovementThreshold = 0.1f,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Handled)
|
||||
return;
|
||||
|
||||
ToggleInternals(ent, args.User, force: true, ent);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnToggleInternalsAlert(Entity<InternalsComponent> ent, ref ToggleInternalsAlertEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
ToggleInternals(ent, ent, false, internals: ent.Comp);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
|
||||
}
|
||||
|
||||
private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
|
||||
}
|
||||
|
||||
private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocationEvent args)
|
||||
{
|
||||
if (AreInternalsWorking(ent))
|
||||
@@ -190,110 +63,4 @@ public sealed class InternalsSystem : EntitySystem
|
||||
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
|
||||
}
|
||||
}
|
||||
public void DisconnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
|
||||
{
|
||||
ent.Comp.BreathTools.Remove(toolEntity);
|
||||
|
||||
if (TryComp(toolEntity, out BreathToolComponent? breathTool))
|
||||
_atmos.DisconnectInternals((toolEntity, breathTool));
|
||||
|
||||
if (ent.Comp.BreathTools.Count == 0)
|
||||
DisconnectTank(ent);
|
||||
|
||||
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
|
||||
}
|
||||
|
||||
public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
|
||||
{
|
||||
if (!ent.Comp.BreathTools.Add(toolEntity))
|
||||
return;
|
||||
|
||||
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
|
||||
}
|
||||
|
||||
public void DisconnectTank(Entity<InternalsComponent> ent)
|
||||
{
|
||||
if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
|
||||
_gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
|
||||
|
||||
ent.Comp.GasTankEntity = null;
|
||||
_alerts.ShowAlert(ent.Owner, ent.Comp.InternalsAlert, GetSeverity(ent.Comp));
|
||||
}
|
||||
|
||||
public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
|
||||
{
|
||||
if (ent.Comp.BreathTools.Count == 0)
|
||||
return false;
|
||||
|
||||
if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
|
||||
_gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
|
||||
|
||||
ent.Comp.GasTankEntity = tankEntity;
|
||||
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
|
||||
{
|
||||
return Resolve(uid, ref component, logMissing: false)
|
||||
&& AreInternalsWorking(component);
|
||||
}
|
||||
|
||||
public bool AreInternalsWorking(InternalsComponent component)
|
||||
{
|
||||
return TryComp(component.BreathTools.FirstOrNull(), out BreathToolComponent? breathTool)
|
||||
&& breathTool.IsFunctional
|
||||
&& HasComp<GasTankComponent>(component.GasTankEntity);
|
||||
}
|
||||
|
||||
private short GetSeverity(InternalsComponent component)
|
||||
{
|
||||
if (component.BreathTools.Count == 0 || !AreInternalsWorking(component))
|
||||
return 2;
|
||||
|
||||
// If pressure in the tank is below low pressure threshold, flash warning on internals UI
|
||||
if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank)
|
||||
&& gasTank.IsLowPressure)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public Entity<GasTankComponent>? FindBestGasTank(
|
||||
Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
|
||||
{
|
||||
// TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
|
||||
// Prioritise
|
||||
// 1. back equipped tanks
|
||||
// 2. exo-slot tanks
|
||||
// 3. in-hand tanks
|
||||
// 4. pocket/belt tanks
|
||||
|
||||
if (!Resolve(user, ref user.Comp2, ref user.Comp3))
|
||||
return null;
|
||||
|
||||
if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&
|
||||
TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
|
||||
_gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
|
||||
{
|
||||
return (backEntity.Value, backGasTank);
|
||||
}
|
||||
|
||||
if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
|
||||
TryComp<GasTankComponent>(entity, out var gasTank) &&
|
||||
_gasTank.CanConnectToInternals((entity.Value, gasTank)))
|
||||
{
|
||||
return (entity.Value, gasTank);
|
||||
}
|
||||
|
||||
foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
|
||||
{
|
||||
if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
|
||||
return (item, gasTank);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
@@ -6,6 +5,8 @@ using Content.Shared.Atmos;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using BreathToolComponent = Content.Shared.Atmos.Components.BreathToolComponent;
|
||||
using InternalsComponent = Content.Shared.Body.Components.InternalsComponent;
|
||||
|
||||
namespace Content.Server.Body.Systems;
|
||||
|
||||
@@ -23,7 +24,6 @@ public sealed class LungSystem : EntitySystem
|
||||
SubscribeLocalEvent<LungComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<BreathToolComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<BreathToolComponent, GotUnequippedEvent>(OnGotUnequipped);
|
||||
SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
|
||||
@@ -38,8 +38,6 @@ public sealed class LungSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
ent.Comp.IsFunctional = true;
|
||||
|
||||
if (TryComp(args.Equipee, out InternalsComponent? internals))
|
||||
{
|
||||
ent.Comp.ConnectedInternalsEntity = args.Equipee;
|
||||
@@ -56,24 +54,6 @@ public sealed class LungSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
if (args.Mask.Comp.IsToggled)
|
||||
{
|
||||
_atmos.DisconnectInternals(ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
ent.Comp.IsFunctional = true;
|
||||
|
||||
if (TryComp(args.Wearer, out InternalsComponent? internals))
|
||||
{
|
||||
ent.Comp.ConnectedInternalsEntity = args.Wearer;
|
||||
_internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GasToReagent(EntityUid uid, LungComponent lung)
|
||||
{
|
||||
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
|
||||
namespace Content.Server.Botany.Systems;
|
||||
@@ -37,4 +38,23 @@ public sealed partial class BotanySystem
|
||||
solutionContainer.AddReagent(chem, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnProduceExamined(EntityUid uid, ProduceComponent comp, ExaminedEvent args)
|
||||
{
|
||||
if (comp.Seed == null)
|
||||
return;
|
||||
|
||||
using (args.PushGroup(nameof(ProduceComponent)))
|
||||
{
|
||||
foreach (var m in comp.Seed.Mutations)
|
||||
{
|
||||
// Don't show mutations that have no effect on produce (sentience)
|
||||
if (!m.AppliesToProduce)
|
||||
continue;
|
||||
|
||||
if (m.Description != null)
|
||||
args.PushMarkup(Loc.GetString(m.Description));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public sealed partial class BotanySystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SeedComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<ProduceComponent, ExaminedEvent>(OnProduceExamined);
|
||||
}
|
||||
|
||||
public bool TryGetSeed(SeedComponent comp, [NotNullWhen(true)] out SeedData? seed)
|
||||
|
||||
@@ -116,6 +116,20 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
? "plant-holder-component-plant-old-adjective"
|
||||
: "plant-holder-component-plant-unhealthy-adjective"))));
|
||||
}
|
||||
|
||||
// For future reference, mutations should only appear on examine if they apply to a plant, not to produce.
|
||||
|
||||
if (component.Seed.Ligneous)
|
||||
args.PushMarkup(Loc.GetString("mutation-plant-ligneous"));
|
||||
|
||||
if (component.Seed.TurnIntoKudzu)
|
||||
args.PushMarkup(Loc.GetString("mutation-plant-kudzu"));
|
||||
|
||||
if (component.Seed.CanScream)
|
||||
args.PushMarkup(Loc.GetString("mutation-plant-scream"));
|
||||
|
||||
if (component.Seed.Viable == false)
|
||||
args.PushMarkup(Loc.GetString("mutation-plant-unviable"));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Cargo.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class CargoShuttleConsoleComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("soundDeny")]
|
||||
public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_two.ogg");
|
||||
}
|
||||
@@ -93,7 +93,11 @@ public sealed partial class CargoSystem
|
||||
if (TryComp<AccessReaderComponent>(uid, out var accessReaderComponent) &&
|
||||
!_accessReaderSystem.IsAllowed(mob, uid, accessReaderComponent))
|
||||
{
|
||||
_audio.PlayPvs(component.DenySound, uid);
|
||||
if (Timing.CurTime >= component.NextDenySoundTime)
|
||||
{
|
||||
component.NextDenySoundTime = Timing.CurTime + component.DenySoundDelay;
|
||||
_audio.PlayPvs(component.DenySound, uid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ using Content.Shared.Interaction;
|
||||
using Content.Shared.Labels.Components;
|
||||
using Content.Shared.Paper;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Cargo.Systems
|
||||
@@ -23,6 +25,7 @@ namespace Content.Server.Cargo.Systems
|
||||
{
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private void InitializeConsole()
|
||||
{
|
||||
@@ -35,11 +38,8 @@ namespace Content.Server.Cargo.Systems
|
||||
SubscribeLocalEvent<CargoOrderConsoleComponent, GotEmaggedEvent>(OnEmagged);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
|
||||
private void OnInteractUsingCash(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
|
||||
{
|
||||
if (!HasComp<CashComponent>(args.Used))
|
||||
return;
|
||||
|
||||
var price = _pricing.GetPrice(args.Used);
|
||||
|
||||
if (price == 0)
|
||||
@@ -56,6 +56,55 @@ namespace Content.Server.Cargo.Systems
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnInteractUsingSlip(Entity<CargoOrderConsoleComponent> ent, ref InteractUsingEvent args, CargoSlipComponent slip)
|
||||
{
|
||||
if (slip.OrderQuantity <= 0)
|
||||
return;
|
||||
|
||||
var stationUid = _station.GetOwningStation(ent);
|
||||
|
||||
if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
|
||||
return;
|
||||
|
||||
if (!_protoMan.TryIndex(slip.Product, out var product))
|
||||
{
|
||||
Log.Error($"Tried to add invalid cargo product {slip.Product} as order!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ent.Comp.AllowedGroups.Contains(product.Group))
|
||||
return;
|
||||
|
||||
var orderId = GenerateOrderId(orderDatabase);
|
||||
var data = new CargoOrderData(orderId, product.Product, product.Name, product.Cost, slip.OrderQuantity, slip.Requester, slip.Reason, slip.Account);
|
||||
|
||||
if (!TryAddOrder(stationUid.Value, ent.Comp.Account, data, orderDatabase))
|
||||
{
|
||||
PlayDenySound(ent, ent.Comp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log order addition
|
||||
_audio.PlayPvs(ent.Comp.ScanSound, ent);
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(args.User):user} inserted order slip [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.ProductId}, requester:{data.Requester}, reason:{data.Reason}]");
|
||||
QueueDel(args.Used);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
|
||||
{
|
||||
if (HasComp<CashComponent>(args.Used))
|
||||
{
|
||||
OnInteractUsingCash(uid, component, ref args);
|
||||
}
|
||||
else if (TryComp<CargoSlipComponent>(args.Used, out var slip) && !component.SlipPrinter)
|
||||
{
|
||||
OnInteractUsingSlip((uid, component), ref args, slip);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, CargoOrderConsoleComponent orderConsole, ComponentInit args)
|
||||
{
|
||||
var station = _station.GetOwningStation(uid);
|
||||
@@ -94,6 +143,9 @@ namespace Content.Server.Cargo.Systems
|
||||
if (args.Actor is not { Valid: true } player)
|
||||
return;
|
||||
|
||||
if (component.SlipPrinter)
|
||||
return;
|
||||
|
||||
if (!_accessReaderSystem.IsAllowed(player, uid))
|
||||
{
|
||||
ConsolePopup(args.Actor, Loc.GetString("cargo-console-order-not-allowed"));
|
||||
@@ -115,7 +167,7 @@ namespace Content.Server.Cargo.Systems
|
||||
|
||||
// Find our order again. It might have been dispatched or approved already
|
||||
var order = orderDatabase.Orders[component.Account].Find(order => args.OrderId == order.OrderId && !order.Approved);
|
||||
if (order == null)
|
||||
if (order == null || !_protoMan.TryIndex(order.Account, out var account))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -128,7 +180,7 @@ namespace Content.Server.Cargo.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
var amount = GetOutstandingOrderCount(orderDatabase, component.Account);
|
||||
var amount = GetOutstandingOrderCount(orderDatabase, order.Account);
|
||||
var capacity = orderDatabase.Capacity;
|
||||
|
||||
// Too many orders, avoid them getting spammed in the UI.
|
||||
@@ -150,7 +202,7 @@ namespace Content.Server.Cargo.Systems
|
||||
}
|
||||
|
||||
var cost = order.Price * order.OrderQuantity;
|
||||
var accountBalance = GetBalanceFromAccount((station.Value, bank), component.Account);
|
||||
var accountBalance = GetBalanceFromAccount((station.Value, bank), order.Account);
|
||||
|
||||
// Not enough balance
|
||||
if (cost > accountBalance)
|
||||
@@ -166,7 +218,7 @@ namespace Content.Server.Cargo.Systems
|
||||
|
||||
if (!ev.Handled)
|
||||
{
|
||||
ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), component.Account, order, orderDatabase);
|
||||
ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), order.Account, order, orderDatabase);
|
||||
|
||||
if (ev.FulfillmentEntity == null)
|
||||
{
|
||||
@@ -190,8 +242,8 @@ namespace Content.Server.Cargo.Systems
|
||||
("orderAmount", order.OrderQuantity),
|
||||
("approver", order.Approver ?? string.Empty),
|
||||
("cost", cost));
|
||||
_radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
|
||||
if (CargoOrderConsoleComponent.BaseAnnouncementChannel != component.AnnouncementChannel)
|
||||
_radio.SendRadioMessage(uid, message, account.RadioChannel, uid, escapeMarkup: false);
|
||||
if (CargoOrderConsoleComponent.BaseAnnouncementChannel != account.RadioChannel)
|
||||
_radio.SendRadioMessage(uid, message, CargoOrderConsoleComponent.BaseAnnouncementChannel, uid, escapeMarkup: false);
|
||||
}
|
||||
|
||||
@@ -200,10 +252,10 @@ namespace Content.Server.Cargo.Systems
|
||||
// Log order approval
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] on account {component.Account} with balance at {accountBalance}");
|
||||
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] on account {order.Account} with balance at {accountBalance}");
|
||||
|
||||
orderDatabase.Orders[component.Account].Remove(order);
|
||||
UpdateBankAccount((station.Value, bank), -cost, component.Account);
|
||||
UpdateBankAccount((station.Value, bank), -cost, order.Account);
|
||||
UpdateOrders(station.Value);
|
||||
}
|
||||
|
||||
@@ -259,12 +311,48 @@ namespace Content.Server.Cargo.Systems
|
||||
{
|
||||
var station = _station.GetOwningStation(uid);
|
||||
|
||||
if (component.SlipPrinter)
|
||||
return;
|
||||
|
||||
if (!TryGetOrderDatabase(station, out var orderDatabase))
|
||||
return;
|
||||
|
||||
RemoveOrder(station.Value, component.Account, args.OrderId, orderDatabase);
|
||||
}
|
||||
|
||||
private void OnAddOrderMessageSlipPrinter(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args, CargoProductPrototype product)
|
||||
{
|
||||
if (!_protoMan.TryIndex(component.Account, out var account))
|
||||
return;
|
||||
|
||||
if (Timing.CurTime < component.NextPrintTime)
|
||||
return;
|
||||
|
||||
var label = Spawn(account.AcquisitionSlip, Transform(uid).Coordinates);
|
||||
component.NextPrintTime = Timing.CurTime + component.PrintDelay;
|
||||
_audio.PlayPvs(component.PrintSound, uid);
|
||||
|
||||
var paper = EnsureComp<PaperComponent>(label);
|
||||
var msg = new FormattedMessage();
|
||||
|
||||
msg.AddMarkupPermissive(Loc.GetString("cargo-acquisition-slip-body",
|
||||
("product", product.Name),
|
||||
("description", product.Description),
|
||||
("unit", product.Cost),
|
||||
("amount", args.Amount),
|
||||
("cost", product.Cost * args.Amount),
|
||||
("orderer", args.Requester),
|
||||
("reason", args.Reason)));
|
||||
_paperSystem.SetContent((label, paper), msg.ToMarkup());
|
||||
|
||||
var slip = EnsureComp<CargoSlipComponent>(label);
|
||||
slip.Product = product.ID;
|
||||
slip.Requester = args.Requester;
|
||||
slip.Reason = args.Reason;
|
||||
slip.OrderQuantity = args.Amount;
|
||||
slip.Account = component.Account;
|
||||
}
|
||||
|
||||
private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args)
|
||||
{
|
||||
if (args.Actor is not { Valid: true } player)
|
||||
@@ -287,7 +375,13 @@ namespace Content.Server.Cargo.Systems
|
||||
if (!component.AllowedGroups.Contains(product.Group))
|
||||
return;
|
||||
|
||||
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase));
|
||||
if (component.SlipPrinter)
|
||||
{
|
||||
OnAddOrderMessageSlipPrinter(uid, component, args, product);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase), component.Account);
|
||||
|
||||
if (!TryAddOrder(stationUid.Value, component.Account, data, orderDatabase))
|
||||
{
|
||||
@@ -339,12 +433,16 @@ namespace Content.Server.Cargo.Systems
|
||||
|
||||
private void PlayDenySound(EntityUid uid, CargoOrderConsoleComponent component)
|
||||
{
|
||||
_audio.PlayPvs(_audio.ResolveSound(component.ErrorSound), uid);
|
||||
if (_timing.CurTime >= component.NextDenySoundTime)
|
||||
{
|
||||
component.NextDenySoundTime = _timing.CurTime + component.DenySoundDelay;
|
||||
_audio.PlayPvs(_audio.ResolveSound(component.ErrorSound), uid);
|
||||
}
|
||||
}
|
||||
|
||||
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id)
|
||||
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id, ProtoId<CargoAccountPrototype> account)
|
||||
{
|
||||
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
|
||||
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason, account);
|
||||
}
|
||||
|
||||
public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component, ProtoId<CargoAccountPrototype> account)
|
||||
@@ -378,16 +476,6 @@ namespace Content.Server.Cargo.Systems
|
||||
|
||||
UpdateOrderState(uid, station);
|
||||
}
|
||||
|
||||
var consoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
|
||||
while (consoleQuery.MoveNext(out var uid, out var _))
|
||||
{
|
||||
var station = _station.GetOwningStation(uid);
|
||||
if (station != dbUid)
|
||||
continue;
|
||||
|
||||
UpdateShuttleState(uid, station);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddAndApproveOrder(
|
||||
@@ -407,7 +495,7 @@ namespace Content.Server.Cargo.Systems
|
||||
DebugTools.Assert(_protoMan.HasIndex<EntityPrototype>(spawnId));
|
||||
// Make an order
|
||||
var id = GenerateOrderId(component);
|
||||
var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description);
|
||||
var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description, account);
|
||||
|
||||
// Approve it now
|
||||
order.SetApproverData(dest, sender);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user