Merge remote-tracking branch 'space-wizards/master'

# Conflicts:
#	Content.Client/Construction/UI/ConstructionMenu.xaml
#	Content.Client/Construction/UI/ConstructionMenu.xaml.cs
#	Content.Client/Construction/UI/ConstructionMenuPresenter.cs
#	Content.Server/Warps/WarpPointSystem.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.cs
#	Content.Shared/Construction/Steps/ArbitraryInsertConstructionGraphStep.cs
#	Content.Shared/Construction/Steps/MaterialConstructionGraphStep.cs
#	Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
#	Content.Shared/Inventory/InventorySystem.Relay.cs
#	Content.Shared/Weapons/Reflect/ReflectSystem.cs
#	Resources/Locale/en-US/_strings/plants/mutations.ftl
#	Resources/Locale/en-US/_strings/stack/stacks.ftl
#	Resources/Locale/en-US/_strings/teleportation/teleportation-menu-gui.ftl
#	Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/winterdrobe.yml
#	Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml
#	Resources/Prototypes/Entities/Objects/Power/powercells.yml
#	Resources/Prototypes/Entities/Objects/Specific/Cargo/cargo_pallet.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
#	Resources/Prototypes/Entities/Stations/base.yml
#	Resources/Prototypes/Entities/Structures/Machines/nuke.yml
#	Resources/Prototypes/Reagents/toxins.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/clothing/clown_banana.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/machines/computer.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/mechs/hamtr_construction.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/mechs/honker_construction.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/mechs/ripley_construction.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/mechs/vim_construction.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/airlock.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/airlock_clockwork.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/blast_door.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/conveyor.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/firelock.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/glassbox.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/secretdoor.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/shutter.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/shuttle.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/structures/windoor.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/tools/logic_gate.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/APC.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/air_alarms.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/disposal_machines.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/intercom.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/lighting.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/solarpanel.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/station_maps.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/wallmount_generator.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/wallmount_substation.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/weapons/bladed_hats.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/weapons/bola.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/weapons/improvised_arrow.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/weapons/modular_grenade.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/weapons/modular_mine.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/weapons/shiv.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/weapons/spear.yml
#	Resources/Prototypes/Recipes/Construction/structures.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/bots/cleanbot.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/bots/firebot.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/bots/honkbot.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/bots/medibot.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/bots/mimebot.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/bots/supplybot.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/firebomb.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/improvised_shotgun.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/improvised_shotgun_shell.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/makeshiftstunprod.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/pipebomb.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/pneumatic_cannon.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/improvised/potato.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/storage/cratefreezer.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/storage/tallbox.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/strawhat.yml
#	Resources/Prototypes/Recipes/Crafting/Graphs/toys.yml
#	Resources/Prototypes/Recipes/Crafting/improvised.yml
#	Resources/Prototypes/Recipes/Reactions/medicine.yml
#	Resources/Prototypes/Roles/Jobs/CentComm/official.yml
#	Resources/Textures/Clothing/Neck/mantles/cemantle.rsi/equipped-NECK.png
#	Resources/Textures/Clothing/Neck/mantles/cemantle.rsi/icon.png
#	Resources/Textures/Clothing/Neck/mantles/cemantle.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/mime.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/mime.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/mime.rsi/inhand-left.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/mime.rsi/inhand-right.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/mime.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/mime.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/mime.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/mime.rsi/inhand-left.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/mime.rsi/inhand-right.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/mime.rsi/meta.json
This commit is contained in:
Vigers Ray 2025-05-04 23:13:12 +03:00
commit 95b86d7a4b
776 changed files with 89359 additions and 26409 deletions

7
.github/CODEOWNERS vendored
View file

@ -29,13 +29,6 @@
/Pow3r/ @PJB3005
/Content.Server/Power/Pow3r/ @PJB3005
# notafet
/Content.*/Atmos/ @Partmedia
/Content.*/Botany/ @Partmedia
# Jezi
/Content.*/Medical @Jezithyr
/Content.*/Body @Jezithyr
# Sloth
/Content.*/Audio @metalgearsloth

View file

@ -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()

View file

@ -7,6 +7,7 @@ using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Server.Warps;
using Content.Shared.Warps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;

View file

@ -203,6 +203,7 @@ namespace Content.Client.Actions
return;
OnActionAdded?.Invoke(actionId);
ActionsUpdated?.Invoke();
}
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
@ -211,6 +212,7 @@ namespace Content.Client.Actions
return;
OnActionRemoved?.Invoke(actionId);
ActionsUpdated?.Invoke();
}
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()

View file

@ -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);
}

View 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>();
}
}
}

View file

@ -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>();
}
}
}

View file

@ -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)

View file

@ -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;

View 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();
}
}
}

View file

@ -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),

View file

@ -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();
}
}
}

View file

@ -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);

View file

@ -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="{ Loc 'construction-menu-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"/>

View file

@ -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 Id, 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 Id, 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(Loc.GetString($"recipe-{Id}-name"));
TargetDesc.SetMessage(Loc.GetString($"recipe-{Id}-description"));
TargetTexture.Texture = iconTexture;
TargetName.SetMessage(name);
TargetDesc.SetMessage(description);
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;
}
}

View file

@ -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,66 +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 (_playerManager.LocalSession == null
|| _playerManager.LocalEntity == null
|| _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value))
continue;
var recipeName = recipe.Name.ToLowerInvariant();
var localizedRecipeName = Loc.GetString($"recipe-{recipe.ID}-name").ToLowerInvariant();
if (!string.IsNullOrEmpty(search))
{
var searchLower = search.Trim().ToLowerInvariant();
if (!recipeName.Contains(searchLower) && !localizedRecipeName.Contains(searchLower))
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();
@ -231,60 +192,120 @@ 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)
{
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 };
}
@ -306,12 +327,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);
@ -329,19 +350,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.ID,
_spriteSystem.Frame0(prototype.Icon),
prototype.Name!,
prototype.Description!,
proto,
prototype.Type != ConstructionType.Item,
!_favoritedRecipes.Contains(prototype));
@ -354,16 +387,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)
@ -374,23 +408,12 @@ namespace Content.Client.Construction.UI
}
}
private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
{
return new(itemList)
{
Metadata = recipe,
Text = Loc.GetString($"recipe-{recipe.ID}-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)
@ -407,10 +430,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();
}
@ -434,38 +458,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);
@ -497,6 +522,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;
@ -538,7 +566,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();
@ -546,7 +575,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
}
}

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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:

View file

@ -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;
}

View file

@ -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;

View 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);
}
}

View 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);
}
}

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
@ -29,48 +30,47 @@ namespace Content.Client.PDA.Ringer
{
var input = RingerNoteInputs[i];
var index = i;
var foo = () => // Prevents unauthorized characters from being entered into the LineEdit
{
input.Text = input.Text.ToUpper();
if (!IsNote(input.Text))
{
input.Text = PreviousNoteInputs[index];
}
else
PreviousNoteInputs[index] = input.Text;
input.RemoveStyleClass("Caution");
};
input.OnFocusExit += _ => foo();
input.OnTextEntered += _ =>
{
foo();
input.CursorPosition = input.Text.Length; // Resets caret position to the end of the typed input
};
input.OnTextChanged += args =>
{
// Convert to uppercase
var upperText = args.Text.ToUpper();
if (input.Text.Length <= 0)
return;
// Filter to only valid notes
var newText = upperText;
if (!IsNote(newText))
input.Text = args.Text.ToUpper();
var isValid = IsNote(input.Text);
if (!isValid)
{
newText = PreviousNoteInputs[index];
input.Text = PreviousNoteInputs[index];
input.AddStyleClass("Caution");
}
else
{
PreviousNoteInputs[index] = newText;
PreviousNoteInputs[index] = input.Text;
input.RemoveStyleClass("Caution");
}
// Only update if there's a change
if (newText != input.Text)
input.Text = newText;
input.CursorPosition = input.Text.Length;
};
input.OnFocusExit += _ =>
{
if (!IsNote(input.Text))
{
input.Text = PreviousNoteInputs[index];
input.RemoveStyleClass("Caution");
}
};
input.OnTextEntered += _ =>
{
if (!IsNote(input.Text))
{
input.Text = PreviousNoteInputs[index];
input.RemoveStyleClass("Caution");
}
input.CursorPosition = input.Text.Length;
};
}
}
@ -86,6 +86,9 @@ namespace Content.Client.PDA.Ringer
/// </summary>
public static bool IsNote(string input)
{
if (input.Any(char.IsDigit))
return false;
input = input.Replace("#", "sharp");
return Enum.TryParse(input, true, out Note _);

View file

@ -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()

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -68,6 +68,10 @@ namespace Content.Client.Stylesheets
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";
public const string StyleClassSliderBlue = "Blue";
@ -194,6 +198,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
{
@ -428,9 +436,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");
@ -960,6 +1019,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[]
{
@ -1158,6 +1228,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),
@ -1257,6 +1343,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(

View file

@ -0,0 +1,11 @@
using Content.Shared.Teleportation.Systems;
namespace Content.Client.Teleportation;
/// <summary>
/// <inheritdoc cref="SharedTeleportLocationsSystem"/>
/// </summary>
public sealed partial class TeleportLocationsSystem : SharedTeleportLocationsSystem
{
}

View file

@ -0,0 +1,36 @@
using Content.Shared.Teleportation;
using Content.Shared.Teleportation.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Teleportation.Ui;
[UsedImplicitly]
public sealed class TeleportLocationsBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private TeleportMenu? _menu;
public TeleportLocationsBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<TeleportMenu>();
if (!EntMan.TryGetComponent<TeleportLocationsComponent>(Owner, out var teleComp))
return;
_menu.Title = Loc.GetString(teleComp.Name);
_menu.Warps = teleComp.AvailableWarps;
_menu.AddTeleportButtons();
_menu.TeleportClicked += (netEnt, pointName) =>
{
SendPredictedMessage(new TeleportLocationDestinationMessage(netEnt, pointName));
};
}
}

View file

@ -0,0 +1,10 @@
<DefaultWindow xmlns="https://spacestation14.io" Title = "{Loc 'teleportation-scroll-window-title'}" MinSize="450 450" SetSize="450 450">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.4">
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True" Margin="0 4" />
<ScrollContainer Name="TeleportScroll" VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
<BoxContainer Name="TeleportButtonContainer" Orientation="Vertical" VerticalExpand="True" SeparationOverride="5">
<!-- Teleport buttons get added here by code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</DefaultWindow>

View file

@ -0,0 +1,74 @@
using System.Numerics;
using Content.Shared.Teleportation.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Teleportation.Ui;
[GenerateTypedNameReferences]
public sealed partial class TeleportMenu : DefaultWindow
{
public string SearchText = "";
public HashSet<TeleportPoint> Warps = new();
public event Action<NetEntity, string>? TeleportClicked;
public TeleportMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
SearchBar.OnTextChanged += OnSearchTextChanged;
}
public void AddTeleportButtons()
{
foreach (var points in Warps)
{
var name = points.Location;
var teleportPoint = points.TelePoint;
var currentButtonRef = new Button
{
Text = name,
TextAlign = Label.AlignMode.Right,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
SizeFlagsStretchRatio = 1,
MinSize = new Vector2(340, 20),
ClipText = true,
};
currentButtonRef.OnPressed += _ => TeleportClicked?.Invoke(teleportPoint, name);
currentButtonRef.Visible = ButtonIsVisible(currentButtonRef);
TeleportButtonContainer.AddChild(currentButtonRef);
}
}
private bool ButtonIsVisible(Button button)
{
return string.IsNullOrEmpty(SearchText) || button.Text == null ||
button.Text.Contains(SearchText, StringComparison.OrdinalIgnoreCase);
}
private void UpdateVisibleButtons()
{
foreach (var child in TeleportButtonContainer.Children)
{
if (child is Button button)
button.Visible = ButtonIsVisible(button);
}
}
public void OnSearchTextChanged(LineEdit.LineEditEventArgs args)
{
SearchText = args.Text;
UpdateVisibleButtons();
// Very funny it's called TeleportScroll
TeleportScroll.SetScrollValue(Vector2.Zero);
}
}

View file

@ -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);
}
}

View 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();
}
}

View 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;
}
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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
{
@ -163,7 +164,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);

View file

@ -219,6 +219,7 @@ public sealed class CargoTest
- type: stack
id: StackProto
name: stack-steel
spawn: A
- type: entity

View file

@ -183,7 +183,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);

View file

@ -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;

View file

@ -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();

View file

@ -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);

View file

@ -1,6 +1,5 @@
using Content.Shared.Interaction;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Actions;
@ -20,8 +19,10 @@ namespace Content.Server.Actions;
[RegisterComponent]
public sealed partial class ActionOnInteractComponent : Component
{
[DataField(required:true)]
[DataField(required: true)]
public List<EntProtoId>? Actions;
[DataField] public List<EntityUid>? ActionEntities;
[DataField] public bool RequiresCharge;
}

View file

@ -1,5 +1,7 @@
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Interaction;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@ -15,6 +17,7 @@ public sealed class ActionOnInteractSystem : EntitySystem
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
public override void Initialize()
{
@ -54,6 +57,9 @@ public sealed class ActionOnInteractSystem : EntitySystem
if (options.Count == 0)
return;
if (!TryUseCharge((uid, component)))
return;
var (actId, act) = _random.Pick(options);
_actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false);
args.Handled = true;
@ -85,6 +91,9 @@ public sealed class ActionOnInteractSystem : EntitySystem
if (entOptions.Count > 0)
{
if (!TryUseCharge((uid, component)))
return;
var (entActId, entAct) = _random.Pick(entOptions);
if (entAct.Event != null)
{
@ -108,6 +117,9 @@ public sealed class ActionOnInteractSystem : EntitySystem
if (entWorldOptions.Count > 0)
{
if (!TryUseCharge((uid, component)))
return;
var (entActId, entAct) = _random.Pick(entWorldOptions);
if (entAct.Event != null)
{
@ -132,6 +144,9 @@ public sealed class ActionOnInteractSystem : EntitySystem
if (options.Count == 0)
return;
if (!TryUseCharge((uid, component)))
return;
var (actId, act) = _random.Pick(options);
if (act.Event != null)
{
@ -163,4 +178,17 @@ public sealed class ActionOnInteractSystem : EntitySystem
return valid;
}
private bool TryUseCharge(Entity<ActionOnInteractComponent> ent)
{
if (!ent.Comp.RequiresCharge)
return true;
Entity<LimitedChargesComponent?> charges = ent.Owner;
if (_charges.IsEmpty(charges))
return false;
_charges.TryUseCharge(charges);
return true;
}
}

View file

@ -4,6 +4,7 @@ using Content.Server.Warps;
using Content.Shared.Administration;
using Content.Shared.Follower;
using Content.Shared.Ghost;
using Content.Shared.Warps;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.Map;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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),
};
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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))

View file

@ -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)
{

View file

@ -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();

View file

@ -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;

View file

@ -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,
});
}
}
}

View file

@ -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;

View file

@ -1,9 +0,0 @@
using Content.Shared.Atmos;
namespace Content.Server.Atmos
{
public interface IGasMixtureHolder
{
public GasMixture Air { get; set; }
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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";
}
}

View file

@ -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;
}
}

View file

@ -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))

View file

@ -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));
}
}
}
}

View file

@ -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)

View file

@ -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
{

View file

@ -389,7 +389,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
var removedSolution = SolutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
// Add back non-whitelisted reagents to the target solution
applicableTargetSolution.AddSolution(temporarilyRemovedSolution, null);
SolutionContainers.TryAddSolution(targetSolution, temporarilyRemovedSolution);
if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
{

View file

@ -0,0 +1,30 @@
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Shared.Map;
namespace Content.Server.Chemistry.TileReactions
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class SpillIfPuddlePresentTileReaction : ITileReaction
{
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager,
List<ReagentData>? data)
{
var spillSystem = entityManager.System<PuddleSystem>();
if (!spillSystem.TryGetPuddle(tile, out _))
return FixedPoint2.Zero;
return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out _, sound: false, tileReact: false)
? reactVolume
: FixedPoint2.Zero;
}
}
}

View file

@ -0,0 +1,28 @@
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Shared.Map;
namespace Content.Server.Chemistry.TileReactions
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class SpillTileReaction : ITileReaction
{
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
FixedPoint2 reactVolume,
IEntityManager entityManager,
List<ReagentData>? data)
{
var spillSystem = entityManager.System<PuddleSystem>();
return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out _, sound: false, tileReact: false)
? reactVolume
: FixedPoint2.Zero;
}
}
}

View file

@ -4,7 +4,6 @@ using Content.Shared.Construction;
using Content.Shared.DoAfter;
using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;

View file

@ -20,23 +20,6 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem
SubscribeLocalEvent<DeviceLinkSourceComponent, NewLinkEvent>(OnNewLink);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<DeviceLinkSinkComponent>();
while (query.MoveNext(out var component))
{
if (component.InvokeLimit < 1)
{
component.InvokeCounter = 0;
continue;
}
if(component.InvokeCounter > 0)
component.InvokeCounter--;
}
}
#region Sending & Receiving
public override void InvokePort(EntityUid uid, string port, NetworkPayload? data = null, DeviceLinkSourceComponent? sourceComponent = null)
{
@ -67,16 +50,17 @@ public sealed class DeviceLinkSystem : SharedDeviceLinkSystem
if (!Resolve(sink, ref sink.Comp))
return;
if (sink.Comp.InvokeCounter > sink.Comp.InvokeLimit)
var invokeCounter = GetEffectiveInvokeCounter(sink.Comp);
if (invokeCounter > sink.Comp.InvokeLimit)
{
sink.Comp.InvokeCounter = 0;
SetInvokeCounter(sink.Comp, 0);
var args = new DeviceLinkOverloadedEvent();
RaiseLocalEvent(sink, ref args);
RemoveAllFromSink(sink, sink.Comp);
return;
}
sink.Comp.InvokeCounter++;
SetInvokeCounter(sink.Comp, invokeCounter + 1);
//Just skip using device networking if the source or the sink doesn't support it
if (!HasComp<DeviceNetworkComponent>(source) || !TryComp<DeviceNetworkComponent>(sink, out var sinkNetwork))

View file

@ -5,6 +5,7 @@ using Content.Server.Power.Nodes;
using Content.Server.Power.NodeGroups;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.NodeContainer;
using Content.Shared.Popups;
using Content.Shared.Power.Generator;
using Content.Shared.Timing;

View file

@ -1,5 +1,6 @@
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.NodeContainer;
namespace Content.Server.DeviceNetwork.Components
{

View file

@ -5,6 +5,7 @@ using JetBrains.Annotations;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Nodes;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.NodeContainer;
namespace Content.Server.DeviceNetwork.Systems
{

View file

@ -1,6 +1,7 @@
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.NodeContainer;
using Robust.Shared.Map.Components;
namespace Content.Server.Electrocution

View file

@ -17,6 +17,8 @@ using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Jittering;
using Content.Shared.Maps;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
using Content.Shared.Popups;
using Content.Shared.Speech.EntitySystems;
using Content.Shared.StatusEffect;

View file

@ -0,0 +1,29 @@
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Explosion.EntitySystems;
using Content.Shared.RepulseAttract;
using Content.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
public sealed class RepulseAttractOnTriggerSystem : SharedRepulseAttractOnTriggerSystem
{
[Dependency] private readonly RepulseAttractSystem _repulse = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedRepulseAttractOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(Entity<SharedRepulseAttractOnTriggerComponent> ent, ref TriggerEvent args)
{
if (_delay.IsDelayed(ent.Owner))
return;
var position = _transform.GetMapCoordinates(ent);
_repulse.TryRepulseAttract(position, args.User, ent.Comp.Speed, ent.Comp.Range, ent.Comp.Whitelist, ent.Comp.CollisionMask);
}
}

View file

@ -390,6 +390,9 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
if (!TryComp<StepTriggerComponent>(entity, out var comp))
return;
// Ensure we actually have the component
EnsureComp<TileFrictionModifierComponent>(entity);
// This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
// the sprite threshold for a puddle larger than 5 pixels.
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
@ -413,7 +416,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
if (solution.Volume <= smallPuddleThreshold)
{
_stepTrigger.SetActive(entity, false, comp);
_tile.SetModifier(entity, TileFrictionController.DefaultFriction);
_tile.SetModifier(entity, 1f);
return;
}
@ -465,7 +468,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Lower tile friction based on how slippery it is, lets items slide across a puddle of lube
slipComp.SlipData.SlipFriction = (float)(puddleFriction/solution.Volume);
_tile.SetModifier(entity, TileFrictionController.DefaultFriction * slipComp.SlipData.SlipFriction);
_tile.SetModifier(entity, slipComp.SlipData.SlipFriction);
Dirty(entity, slipComp);
}
@ -483,7 +486,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
{
var comp = EnsureComp<SpeedModifierContactsComponent>(uid);
var speed = 1 - maxViscosity;
_speedModContacts.ChangeModifiers(uid, speed, comp);
_speedModContacts.ChangeSpeedModifiers(uid, speed, comp);
}
else
{

View file

@ -19,6 +19,7 @@ using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Random;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
using Content.Shared.Hands.Components;
namespace Content.Server.Forensics
{
@ -32,7 +33,7 @@ namespace Content.Server.Forensics
public override void Initialize()
{
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<HandsComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
// The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
@ -60,7 +61,7 @@ namespace Content.Server.Forensics
}
}
private void OnInteract(EntityUid uid, FingerprintComponent component, ContactInteractionEvent args)
private void OnInteract(EntityUid uid, HandsComponent component, ContactInteractionEvent args)
{
ApplyEvidence(uid, args.Other);
}

View file

@ -31,6 +31,7 @@ using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Content.Shared.Warps;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;

View file

@ -72,7 +72,7 @@ public sealed class IdentitySystem : SharedIdentitySystem
/// <summary>
/// Queues an identity update to the start of the next tick.
/// </summary>
public void QueueIdentityUpdate(EntityUid uid)
public override void QueueIdentityUpdate(EntityUid uid)
{
_queuedIdentityUpdates.Add(uid);
}

View file

@ -21,13 +21,6 @@ public sealed class MagicSystem : SharedMagicSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpeakSpellEvent>(OnSpellSpoken);
}
private void OnSpellSpoken(ref SpeakSpellEvent args)
{
_chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), InGameICChatType.Speak, false);
}
public override void OnVoidApplause(VoidApplauseSpellEvent ev)

View file

@ -1,5 +1,6 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.Collections;

View file

@ -181,11 +181,24 @@ public sealed class HTNSystem : EntitySystem
_planQueue.Process();
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
// Move ahead "count" entries in the query.
// This is to ensure that if we didn't process all the npcs the first time,
// we get to the remaining ones instead of iterating over the beginning again.
for (var i = 0; i < count; i++)
{
query.MoveNext(out _, out _);
}
// the amount of updates we've processed during this iteration.
var updates = 0;
while (query.MoveNext(out var uid, out _, out var comp))
{
// If we're over our max count or it's not MapInit then ignore the NPC.
if (count >= maxUpdates)
break;
if (updates >= maxUpdates)
{
// Intentional return. We don't want to go to the end logic and reset count.
return;
}
if (!comp.Enabled)
continue;
@ -275,7 +288,12 @@ public sealed class HTNSystem : EntitySystem
Update(uid, comp, frameTime);
count++;
updates++;
}
// only reset our counter back to 0 if we finish iterating.
// otherwise it lets us know where we left off.
count = 0;
}
private void AppendDebugText(HTNTask task, StringBuilder text, List<int> planBtr, List<int> btr, ref int level)

View file

@ -133,7 +133,6 @@ namespace Content.Server.NPC.Systems
if (!Enabled)
return;
_count = 0;
// Add your system here.
_htn.UpdateNPC(ref _count, _maxUpdates, frameTime);
}

View file

@ -232,6 +232,9 @@ public sealed class NPCUtilitySystem : EntitySystem
{
if (_container.TryGetContainingContainer(targetUid, out var container))
{
if (container.Owner == owner)
return 0f;
if (TryComp<EntityStorageComponent>(container.Owner, out var storageComponent))
{
if (storageComponent is { Open: false } && _weldable.IsWelded(container.Owner))

View file

@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Examine;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
using JetBrains.Annotations;
namespace Content.Server.NodeContainer.EntitySystems

View file

@ -5,6 +5,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Administration;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;

View file

@ -1,17 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.NodeContainer.Nodes;
namespace Content.Server.NodeContainer
{
/// <summary>
/// Creates and maintains a set of <see cref="Node"/>s.
/// </summary>
[RegisterComponent]
public sealed partial class NodeContainerComponent : Component
{
//HACK: THIS BEING readOnly IS A FILTHY HACK AND I HATE IT --moony
[DataField("nodes", readOnly: true)] public Dictionary<string, Node> Nodes { get; private set; } = new();
[DataField("examinable")] public bool Examinable = false;
}
}

View file

@ -1,40 +1,12 @@
using System.Linq;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.NodeContainer.NodeGroups
{
/// <summary>
/// Maintains a collection of <see cref="Node"/>s, and performs operations requiring a list of
/// all connected <see cref="Node"/>s.
/// </summary>
public interface INodeGroup
{
bool Remaking { get; }
/// <summary>
/// The list of nodes currently in this group.
/// </summary>
IReadOnlyList<Node> Nodes { get; }
void Create(NodeGroupID groupId);
void Initialize(Node sourceNode, IEntityManager entMan);
void RemoveNode(Node node);
void LoadNodes(List<Node> groupNodes);
// In theory, the SS13 curse ensures this method will never be called.
void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups);
/// <summary>
/// Return any additional data to display for the node-visualizer debug overlay.
/// </summary>
string? GetDebugData();
}
[NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)]
[Virtual]
public class BaseNodeGroup : INodeGroup

View file

@ -1,3 +1,4 @@
using Content.Shared.NodeContainer.NodeGroups;
using JetBrains.Annotations;
namespace Content.Server.NodeContainer.NodeGroups

View file

@ -1,5 +1,5 @@
using System.Reflection;
using Content.Server.Power.Generation.Teg;
using Content.Shared.NodeContainer.NodeGroups;
using Robust.Shared.Reflection;
namespace Content.Server.NodeContainer.NodeGroups
@ -51,22 +51,4 @@ namespace Content.Server.NodeContainer.NodeGroups
return instance;
}
}
public enum NodeGroupID : byte
{
Default,
HVPower,
MVPower,
Apc,
AMEngine,
Pipe,
WireNet,
/// <summary>
/// Group used by the TEG.
/// </summary>
/// <seealso cref="TegSystem"/>
/// <seealso cref="TegNodeGroup"/>
Teg,
}
}

View file

@ -3,6 +3,8 @@ using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
using Robust.Shared.Utility;
namespace Content.Server.NodeContainer.NodeGroups

View file

@ -1,3 +1,4 @@
using Content.Shared.NodeContainer;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;

View file

@ -1,3 +1,5 @@
using Content.Shared.NodeContainer;
namespace Content.Server.NodeContainer.Nodes
{
/// <summary>

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