CentComm Shipyard (#5172)

* Added CentComm shipyard console. Brought some things in Shipyard console up to modern standards.

* More shipyard stuff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* PR Feedback

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Vanessa
2026-01-16 19:37:33 +01:00
committed by GitHub
co-authored by pre-commit-ci[bot]
parent fce57f5120
commit 58e500d254
14 changed files with 229 additions and 75 deletions
@@ -19,10 +19,12 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
public event Action<string>? OnPurchased;
private readonly List<VesselPrototype> _vessels = [];
private readonly List<string> _categories = [];
private readonly List<VesselCategoryPrototype> _categories = [];
public Entity<ShipyardConsoleComponent> Console;
private string? _category;
// The currently selected category
private ProtoId<VesselCategoryPrototype>? _category;
public ShipyardConsoleMenu(EntityUid console, IPrototypeManager proto, IEntityManager entMan, IPlayerManager player, AccessReaderSystem access, EntityWhitelistSystem whitelist)
{
@@ -33,46 +35,44 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
_access = access;
_player = player;
_categories.Clear();
foreach (var vesselCategoryProto in Console.Comp.Categories)
{
if (proto.Resolve(vesselCategoryProto, out var vesselCategory))
_categories.Add(vesselCategory);
}
// don't include ships that aren't allowed by whitelist, server won't accept them anyway
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
if (whitelist.IsWhitelistPassOrNull(vessel.Whitelist, console))
_vessels.Add(vessel);
}
_vessels.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase));
// only list categories in said ships
foreach (var vessel in _vessels)
{
foreach (var category in vessel.Categories)
{
if (!_categories.Contains(category))
_categories.Add(category);
}
}
_categories.Sort();
// inserting here and not adding at the start so it doesn't get affected by sort
_categories.Insert(0, Loc.GetString("cargo-console-menu-populate-categories-all-text"));
PopulateCategories();
PopulateCategories(Console);
SearchBar.OnTextChanged += _ => PopulateProducts();
SearchBar.OnTextChanged += _ => PopulateProducts(Console);
Categories.OnItemSelected += args =>
{
_category = args.Id == 0 ? null : _categories[args.Id];
_category = _categories[args.Id];
Categories.SelectId(args.Id);
PopulateProducts();
PopulateProducts(Console);
};
PopulateProducts(Console);
}
/// <summary>
/// Populates the list of products that will actually be shown, using the current filters.
/// </summary>
private void PopulateProducts()
private void PopulateProducts(Entity<ShipyardConsoleComponent> entity)
{
Vessels.RemoveAllChildren();
var access = _player.LocalSession?.AttachedEntity is {} player
var access = _player.LocalSession?.AttachedEntity is { } player
&& _access.IsAllowed(player, Console);
var search = SearchBar.Text.Trim().ToLowerInvariant();
@@ -80,10 +80,11 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
{
if (search.Length != 0 && !vessel.Name.Contains(search, StringComparison.InvariantCultureIgnoreCase))
continue;
if (_category != null && !vessel.Categories.Contains(_category))
if (_category != null && !vessel.Categories.Contains(_category.Value.Id))
continue;
var vesselEntry = new VesselRow(vessel, access);
var vesselEntry = new VesselRow(vessel, access, isFree: !entity.Comp.UseStationFunds);
vesselEntry.OnPurchasePressed += () => OnPurchased?.Invoke(vessel.ID);
Vessels.AddChild(vesselEntry);
}
@@ -92,18 +93,29 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
/// <summary>
/// Populates the list categories that will actually be shown, using the current filters.
/// </summary>
private void PopulateCategories()
private void PopulateCategories(Entity<ShipyardConsoleComponent> entity)
{
Categories.Clear();
foreach (var category in _categories)
// Guh, there's gotta be an easier way to select a category by default...
var selectedId = 0; // Default to the first category. May get overridden later.
for (var i = 0; i < _categories.Count; i++)
{
Categories.AddItem(category);
Categories.AddItem(_categories[i].LocalizedName, i);
if (entity.Comp.DefaultCategory.HasValue && entity.Comp.DefaultCategory.Value.Id.Equals(_categories[i].ID, StringComparison.CurrentCultureIgnoreCase))
selectedId = i;
}
if (selectedId >= 0 && selectedId < _categories.Count)
{
_category = _categories[selectedId];
Categories.SelectId(selectedId);
}
}
public void UpdateState(ShipyardConsoleState state)
{
BankAccountLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", state.Balance.ToString()));
PopulateProducts();
PopulateProducts(Console);
}
}
@@ -12,7 +12,7 @@ public sealed partial class VesselRow : PanelContainer
{
public event Action? OnPurchasePressed;
public VesselRow(VesselPrototype vessel, bool access)
public VesselRow(VesselPrototype vessel, bool access = false, bool isFree = false)
{
RobustXamlLoader.Load(this);
@@ -24,6 +24,9 @@ public sealed partial class VesselRow : PanelContainer
Purchase.Disabled = !access;
Purchase.OnPressed += _ => OnPurchasePressed?.Invoke();
Price.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
if (isFree)
Purchase.Text = Loc.GetString("shipyard-console-price-free");
else
Purchase.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
}
}
@@ -28,7 +28,9 @@ public sealed class ShipyardTest
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
var shuttle = shipyard.TryCreateShuttle(vessel.Path);
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);
Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");
var value = pricing.AppraiseGrid(shuttle.Value);
@@ -57,7 +59,9 @@ public sealed class ShipyardTest
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
var shuttle = shipyard.TryCreateShuttle(vessel.Path);
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);
Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");
var console = FindComponent<ShuttleConsoleComponent>(entities, shuttle.Value);
@@ -1,8 +1,8 @@
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Radio.EntitySystems;
using Content.Server.Station.Systems;
using Content.Shared.Cargo.Components;
using Content.Shared.Popups;
using Content.Shared.Shipyard;
using Content.Shared.Shipyard.Prototypes;
using Content.Shared.Whitelist;
@@ -39,29 +39,43 @@ public sealed class ShipyardConsoleSystem : SharedShipyardConsoleSystem
if (_whitelist.IsWhitelistFail(vessel.Whitelist, ent))
return;
if (GetBankAccount(ent) is not {} bank)
return;
if (bank.Comp.Accounts[bank.Comp.PrimaryAccount] < vessel.Price)
var purchasingGrid = Transform(ent).GridUid;
Entity<StationBankAccountComponent>? bankAccount = null;
if (ent.Comp.UseStationFunds)
{
var popup = Loc.GetString("cargo-console-insufficient-funds", ("cost", vessel.Price));
Popup.PopupEntity(popup, ent, user);
Audio.PlayPvs(ent.Comp.DenySound, ent);
return;
bankAccount = GetBankAccount(ent);
if (!bankAccount.HasValue)
{
var popup = Loc.GetString("shipyard-console-error-bank");
Popup.PopupEntity(popup, ent, user, PopupType.SmallCaution);
Audio.PlayPvs(ent.Comp.DenySound, ent);
return;
}
if (bankAccount.Value.Comp.Accounts[bankAccount.Value.Comp.PrimaryAccount] < vessel.Price)
{
var popup = Loc.GetString("cargo-console-insufficient-funds", ("cost", vessel.Price));
Popup.PopupEntity(popup, ent, user, PopupType.SmallCaution);
Audio.PlayPvs(ent.Comp.DenySound, ent);
return;
}
purchasingGrid = bankAccount.Value.Owner;
}
if (_shipyard.TrySendShuttle(bank.Owner, vessel.Path) is not {} shuttle)
if (purchasingGrid is not { } grid
|| !_shipyard.TrySendShuttle(grid, vessel.Path, out var shuttle))
{
var popup = Loc.GetString("shipyard-console-error");
Popup.PopupEntity(popup, ent, user);
Popup.PopupEntity(popup, ent, user, PopupType.SmallCaution);
Audio.PlayPvs(ent.Comp.DenySound, ent);
return;
}
_meta.SetEntityName(shuttle, $"{vessel.Name} {_random.Next(1000):000}");
_cargo.UpdateBankAccount((bank, bank), -vessel.Price, _cargo.CreateAccountDistribution(bank));
if (bankAccount.HasValue)
_cargo.UpdateBankAccount(bankAccount.Value.Owner, -vessel.Price, _cargo.CreateAccountDistribution(bankAccount.Value));
_meta.SetEntityName(shuttle.Value, $"{vessel.Name} {_random.Next(1000):000}");
var message = Loc.GetString("shipyard-console-docking", ("vessel", vessel.Name));
_radio.SendRadioMessage(ent, message, ent.Comp.Channel, ent);
Audio.PlayPvs(ent.Comp.ConfirmSound, ent);
@@ -76,7 +90,7 @@ public sealed class ShipyardConsoleSystem : SharedShipyardConsoleSystem
if (!_ui.IsUiOpen(uid, ShipyardConsoleUiKey.Key))
return;
if (GetBankAccount(uid) is {} bank)
if (GetBankAccount(uid) is { } bank)
UpdateUI(uid, args.Balance[bank.Comp.PrimaryAccount]);
}
}
@@ -88,7 +102,7 @@ public sealed class ShipyardConsoleSystem : SharedShipyardConsoleSystem
private void UpdateUI(EntityUid uid)
{
if (GetBankAccount(uid) is {} bank)
if (GetBankAccount(uid) is { } bank)
UpdateUI(uid, bank.Comp.Accounts[bank.Comp.PrimaryAccount]);
}
@@ -103,7 +117,7 @@ public sealed class ShipyardConsoleSystem : SharedShipyardConsoleSystem
private Entity<StationBankAccountComponent>? GetBankAccount(EntityUid console)
{
if (_station.GetOwningStation(console) is not {} station)
if (_station.GetOwningStation(console) is not { } station)
return null;
if (!TryComp<StationBankAccountComponent>(station, out var bank))
+18 -23
View File
@@ -1,13 +1,12 @@
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared._DV.CCVars;
using Content.Shared.Station.Components;
using Content.Shared.Tag;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Content.Server.Shipyard;
@@ -21,7 +20,6 @@ public sealed class ShipyardSystem : EntitySystem
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly ShuttleSystem _shuttle = default!;
[Dependency] private readonly StationSystem _station = default!;
public ProtoId<TagPrototype> DockTag = "DockShipyard";
@@ -37,50 +35,47 @@ public sealed class ShipyardSystem : EntitySystem
/// <summary>
/// Creates a ship from its yaml path in the shipyard.
/// </summary>
public Entity<ShuttleComponent>? TryCreateShuttle(ResPath path)
public bool TryCreateShuttle(ResPath path, [NotNullWhen(true)] out Entity<ShuttleComponent>? shuttle)
{
shuttle = null;
if (!Enabled)
return null;
return false;
var map = _map.CreateMap(out var mapId);
if (!_mapLoader.TryLoadGrid(mapId, path, out var grid))
{
Log.Error($"Failed to load shuttle {path}");
Del(map);
return null;
return false;
}
if (!TryComp<ShuttleComponent>(grid, out var comp))
{
Log.Error($"Shuttle {path}'s grid was missing ShuttleComponent");
Del(map);
return null;
return false;
}
_map.SetPaused(map, false);
_mapDeleterShuttle.Enable(grid.Value);
return (grid.Value, comp);
shuttle = (grid.Value, comp);
return true;
}
/// <summary>
/// Adds a ship to the shipyard and attempts to ftl-dock it to the given station.
/// Adds a ship to the shipyard and attempts to ftl-dock it to the given grid.
/// </summary>
public Entity<ShuttleComponent>? TrySendShuttle(Entity<StationDataComponent?> station, ResPath path)
public bool TrySendShuttle(Entity<ShuttleComponent?> shuttleDestination, ResPath path, [NotNullWhen(true)] out Entity<ShuttleComponent>? shuttle)
{
if (!Resolve(station, ref station.Comp))
return null;
shuttle = null;
if (!Resolve(shuttleDestination, ref shuttleDestination.Comp))
return false;
if (_station.GetLargestGrid(station) is not {} grid)
{
Log.Error($"Station {ToPrettyString(station):station} had no largest grid to FTL to");
return null;
}
if (!TryCreateShuttle(path, out shuttle))
return false;
if (TryCreateShuttle(path) is not {} shuttle)
return null;
Log.Info($"Shuttle {path} was spawned for {ToPrettyString(station):station}, FTLing to {grid}");
_shuttle.FTLToDock(shuttle, shuttle.Comp, grid, priorityTag: DockTag);
return shuttle;
Log.Info($"Shuttle {path} was spawned for {ToPrettyString(shuttleDestination):station}");
_shuttle.FTLToDock(shuttle.Value, shuttle.Value.Comp, shuttleDestination, priorityTag: DockTag);
return true;
}
}
@@ -9,6 +9,21 @@ namespace Content.Shared.Shipyard.Prototypes;
[Prototype("vesselCategory")]
public sealed class VesselCategoryPrototype : IPrototype
{
/// <summary>
/// The unique ID for the vessel category.
/// </summary>
[ViewVariables, IdDataField]
public string ID { get; } = default!;
/// <summary>
/// The LocId containing the localization ID of the category name.
/// </summary>
[DataField(required: true)]
private LocId Name { get; set; }
/// <summary>
/// Gets the localized string from the LocID set by <see cref="Name"/>
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
}
@@ -1,4 +1,5 @@
using Content.Shared.Radio;
using Content.Shared.Shipyard.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -8,7 +9,7 @@ namespace Content.Shared.Shipyard;
/// <summary>
/// Component for the shipyard console.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedShipyardConsoleSystem))]
[RegisterComponent, NetworkedComponent]
public sealed partial class ShipyardConsoleComponent : Component
{
/// <summary>
@@ -28,4 +29,22 @@ public sealed partial class ShipyardConsoleComponent : Component
/// </summary>
[DataField]
public ProtoId<RadioChannelPrototype> Channel = "Command";
/// <summary>
/// If false, shuttles won't use the station's bank and will be available at no cost.
/// </summary>
[DataField]
public bool UseStationFunds = true;
/// <summary>
/// The list of selectable categories. Note that this does not filter ships, just allows some categories to be hidden from crew (e.g. CentComm, ERT). Does not need to include All.
/// </summary>
[DataField(required: true)]
public List<ProtoId<VesselCategoryPrototype>> Categories = new();
/// <summary>
/// If not null, will attempt to set the category when opening the shipyard console.
/// </summary>
[DataField]
public ProtoId<VesselCategoryPrototype>? DefaultCategory = null;
}
@@ -1,5 +1,22 @@
shipyard-console-menu-title = Shipyard Console
shipyard-console-error = Temporary embargo is in place, try later?
shipyard-console-docking = {$vessel} is en route to the station, eta 60 seconds.
shipyard-console-error = Temporary embargo is in place. Try again later?
shipyard-console-error-bank = Cannot access station funds from this location.
shipyard-console-docking = {$vessel} is en route to the station. ETA approximately 60 seconds.
shipyard-console-purchase = Purchase
shipyard-console-price-free = Free
# Vessel Categories
vessel-category-civilian = Civilian
vessel-category-military = Military
vessel-category-experimental = Experimental
vessel-category-small = Small
vessel-category-medium = Medium
vessel-category-large = Large
vessel-category-medical = Medical
vessel-category-mining = Mining
vessel-category-research = Research
vessel-category-shipping = Shipping
vessel-category-transport = Transport
vessel-category-centcomm = CentComm
vessel-category-ert = ERT
@@ -38,7 +38,7 @@ cargo-console-station-not-found = No available station
cargo-console-invalid-product = Invalid product ID
cargo-console-too-many = Too many approved orders
cargo-console-snip-snip = Order trimmed to capacity
cargo-console-insufficient-funds = Insufficient funds (require {$cost})
cargo-console-insufficient-funds = Insufficient funds (requires ${$cost})
cargo-console-unfulfilled = No room to fulfill order
cargo-console-trade-station = Sent to {$destination}
cargo-console-unlock-approved-order-broadcast = [bold]{$productName} x{$orderAmount}[/bold], which cost [bold]{$cost}[/bold], was approved by [bold]{$approver}[/bold]
@@ -1,35 +1,55 @@
# class
- type: vesselCategory
id: Civilian
name: vessel-category-civilian
- type: vesselCategory
id: Military
name: vessel-category-military
- type: vesselCategory
id: Experimental
name: vessel-category-experimental
# size
- type: vesselCategory
id: Small
name: vessel-category-small
- type: vesselCategory
id: Medium
name: vessel-category-medium
- type: vesselCategory
id: Large
name: vessel-category-large
# purpose
- type: vesselCategory
id: Medical
name: vessel-category-medical
- type: vesselCategory
id: Mining
name: vessel-category-mining
- type: vesselCategory
id: Research
name: vessel-category-research
- type: vesselCategory
id: Shipping
name: vessel-category-shipping
- type: vesselCategory
id: Transport
name: vessel-category-transport
# admin
- type: vesselCategory
id: CentComm
name: vessel-category-centcomm
- type: vesselCategory
id: ERT
name: vessel-category-ert
@@ -0,0 +1,3 @@
# Ships put in here still need to have a price associated with them due to the arbitrage tests.
# The CentComm console won't charge for them so just put like a million spesos or something
# in the case that the station does see it in their console (could be a random bug, idk!)
@@ -0,0 +1,12 @@
# Ships put in here still need to have a price associated with them due to the arbitrage tests.
# The CentComm console won't charge for them so just put like a million spesos or something
# in the case that the station does see it in their console (could be a random bug, idk!)
- type: vessel
id: Ares
name: NT-Ares
description: A small ship, outfitted with some combat supplies for those extra dangerous ERT situations. Combat supplies include a Hristov, 4 hyperzine injectors, a handheld crew monitor, and an experimenal RCD.
price: 1000000
path: /Maps/Shuttles/AdminSpawn/ERT-Small-Deathsquad.yml
categories:
- ERT
@@ -8,3 +8,4 @@
- Military
- Small
- Transport
- ERT
@@ -2,9 +2,21 @@
parent: BaseComputer
id: ComputerShipyard
name: shipyard console
description: Used to purchase and sell shuttles
description: Used to purchase shuttles for the station.
components:
- type: ShipyardConsole
categories:
- Transport
- Shipping
- Medical
- Mining
- Civilian
- Research
- Military
- Experimental
- Small
- Medium
- Large
- type: AccessReader
access: [[ Captain ]]
- type: ActivatableUI
@@ -30,6 +42,33 @@
- map: ["computerLayerKeys"]
state: tech_key
- type: entity
parent: ComputerShipyard
id: ComputerShipyardAdmin
name: central command shipyard console
description: Used to purchase shuttles for CentComm personnel and agents to use. The costs are heavily subsidized and small amounts of funds are laundered from all of NanoTrasen's stations, making ships purchased here essentially free.
components:
- type: ShipyardConsole
channel: CentCom
useStationFunds: false
categories:
- CentComm
- ERT
- Transport
- Shipping
- Medical
- Mining
- Civilian
- Research
- Military
- Experimental
- Small
- Medium
- Large
defaultCategory: ERT # Probably switch to CentComm once we have ships in that category
- type: AccessReader
access: [[ CentralCommand ]]
- type: entity
abstract: true
parent: BaseComputer