Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Content.Client/_DV/Shipyard/ShipyardConsoleSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Content.Shared._DV.Shipyard;

namespace Content.Client._DV.Shipyard;

public sealed class ShipyardConsoleSystem : SharedShipyardConsoleSystem;
70 changes: 70 additions & 0 deletions Content.Client/_DV/Shipyard/UI/ShipyardBoundUserInterface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Content.Shared.Access.Systems;
using Content.Shared._DV.Shipyard;
using Content.Shared.Whitelist;
using Robust.Client.Player;
using Robust.Shared.Prototypes;

namespace Content.Client._DV.Shipyard.UI;

public sealed class ShipyardConsoleBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IPlayerManager _player = default!;

private readonly AccessReaderSystem _access;
private readonly EntityWhitelistSystem _whitelist;

[ViewVariables]
private ShipyardConsoleMenu? _menu;

public ShipyardConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_access = EntMan.System<AccessReaderSystem>();
_whitelist = EntMan.System<EntityWhitelistSystem>();
}

protected override void Open()
{
base.Open();

if (_menu == null)
{
_menu = new ShipyardConsoleMenu(Owner, _proto, EntMan, _player, _access, _whitelist);
_menu.OnClose += Close;
_menu.OnPurchased += Purchase;
}

_menu.OpenCentered();
}

protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);

if (state is not ShipyardConsoleState cast)
return;

_menu?.UpdateState(cast);
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (!disposing)
return;

if (_menu == null)
return;

_menu.OnClose -= Close;
_menu.OnPurchased -= Purchase;
_menu.Close();
_menu = null;
}

private void Purchase(string id)
{
SendMessage(new ShipyardConsolePurchaseMessage(id));
}
}
28 changes: 28 additions & 0 deletions Content.Client/_DV/Shipyard/UI/ShipyardConsoleMenu.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="500 360"
MinSize="460 280"
Title="{Loc 'shipyard-console-menu-title'}">
<BoxContainer Orientation="Vertical" Margin="5 0 5 0">
<Label Name="BankAccountLabel" />
<BoxContainer Orientation="Horizontal">
<OptionButton Name="Categories"
Prefix="{Loc 'cargo-console-menu-categories-label'}"
HorizontalExpand="True" />
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="6">
<BoxContainer Name="Vessels"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Vessels get added here by code -->
</BoxContainer>
</ScrollContainer>
<TextureButton VerticalExpand="True" />
</BoxContainer>
</controls:FancyWindow>
121 changes: 121 additions & 0 deletions Content.Client/_DV/Shipyard/UI/ShipyardConsoleMenu.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared._DV.Shipyard;
using Content.Shared._DV.Shipyard.Prototypes;
using Content.Shared.Whitelist;
using Robust.Client.AutoGenerated;
using Robust.Client.Player;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;

namespace Content.Client._DV.Shipyard.UI;

[GenerateTypedNameReferences]
public sealed partial class ShipyardConsoleMenu : FancyWindow
{
private readonly AccessReaderSystem _access;
private readonly IPlayerManager _player;

public event Action<string>? OnPurchased;

private readonly List<VesselPrototype> _vessels = [];
private readonly List<VesselCategoryPrototype> _categories = [];

public Entity<ShipyardConsoleComponent> Console;

// The currently selected category
private ProtoId<VesselCategoryPrototype>? _category;

public ShipyardConsoleMenu(EntityUid console, IPrototypeManager proto, IEntityManager entMan, IPlayerManager player, AccessReaderSystem access, EntityWhitelistSystem whitelist)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

Console = (console, entMan.GetComponent<ShipyardConsoleComponent>(console));
_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));

// inserting here and not adding at the start so it doesn't get affected by sort
PopulateCategories(Console);

SearchBar.OnTextChanged += _ => PopulateProducts(Console);
Categories.OnItemSelected += args =>
{
_category = _categories[args.Id];
Categories.SelectId(args.Id);
PopulateProducts(Console);
};

PopulateProducts(Console);
}

/// <summary>
/// Populates the list of products that will actually be shown, using the current filters.
/// </summary>
private void PopulateProducts(Entity<ShipyardConsoleComponent> entity)
{
Vessels.RemoveAllChildren();

var access = _player.LocalSession?.AttachedEntity is { } player
&& _access.IsAllowed(player, Console);

var search = SearchBar.Text.Trim().ToLowerInvariant();
foreach (var vessel in _vessels)
{
if (search.Length != 0 && !vessel.Name.Contains(search, StringComparison.InvariantCultureIgnoreCase))
continue;

if (_category != null && !vessel.Categories.Contains(_category.Value.Id))
continue;

var vesselEntry = new VesselRow(vessel, access, isFree: !entity.Comp.UseStationFunds);
vesselEntry.OnPurchasePressed += () => OnPurchased?.Invoke(vessel.ID);
Vessels.AddChild(vesselEntry);
}
}

/// <summary>
/// Populates the list categories that will actually be shown, using the current filters.
/// </summary>
private void PopulateCategories(Entity<ShipyardConsoleComponent> entity)
{
Categories.Clear();
// 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(_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(Console);
}
}
16 changes: 16 additions & 0 deletions Content.Client/_DV/Shipyard/UI/VesselRow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<PanelContainer xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True">
<Button Name="Purchase" Text="{Loc 'shipyard-console-purchase'}" StyleClasses="LabelSubText" />
<Label Name="VesselName" HorizontalExpand="True" />
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252A" />
</PanelContainer.PanelOverride>

<Label Name="Price" MinSize="52 32" Align="Right" />
</PanelContainer>
</BoxContainer>
</PanelContainer>
32 changes: 32 additions & 0 deletions Content.Client/_DV/Shipyard/UI/VesselRow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Content.Shared._DV.Shipyard.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;

namespace Content.Client._DV.Shipyard.UI;

[GenerateTypedNameReferences]
public sealed partial class VesselRow : PanelContainer
{
public event Action? OnPurchasePressed;

public VesselRow(VesselPrototype vessel, bool access = false, bool isFree = false)
{
RobustXamlLoader.Load(this);

VesselName.Text = vessel.Name;

var tooltip = new Tooltip();
tooltip.SetMessage(FormattedMessage.FromMarkupOrThrow(vessel.Description));
Purchase.TooltipSupplier = _ => tooltip;
Purchase.Disabled = !access;
Purchase.OnPressed += _ => OnPurchasePressed?.Invoke();

if (isFree)
Purchase.Text = Loc.GetString("shipyard-console-price-free");
else
Purchase.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
}
}
94 changes: 94 additions & 0 deletions Content.IntegrationTests/Tests/DeltaV/ShipyardTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Content.Server.Cargo.Systems;
using Content.Server._DV.Shipyard;
using Content.Server.Shuttles.Components;
using Content.Shared._DV.Shipyard.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;

namespace Content.IntegrationTests.Tests._DV;

[TestFixture]
[TestOf(typeof(ShipyardSystem))]
public sealed class ShipyardTest
{
[Test]
public async Task NoShipyardArbitrage()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;

var entities = server.ResolveDependency<IEntityManager>();
var proto = server.ResolveDependency<IPrototypeManager>();
var shipyard = entities.System<ShipyardSystem>();
var pricing = entities.System<PricingSystem>();

await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
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);
Assert.That(value, Is.AtMost(vessel.Price), $"Found arbitrage on shuttle {vessel.ID}! Price is {vessel.Price} but value is {value}!");
entities.DeleteEntity(shuttle);
}
});
});

await pair.CleanReturnAsync();
}

[Test]
public async Task AllShuttlesValid()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;

var entities = server.ResolveDependency<IEntityManager>();
var proto = server.ResolveDependency<IPrototypeManager>();
var shipyard = entities.System<ShipyardSystem>();

await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
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);
Assert.That(console, Is.True, $"Shuttle {vessel.ID} had no shuttle console!");

var dock = FindComponent<DockingComponent>(entities, shuttle.Value);
Assert.That(dock, Is.True, $"Shuttle {vessel.ID} had no shuttle dock!");

entities.DeleteEntity(shuttle);
}
});
});

await pair.CleanReturnAsync();
}

private bool FindComponent<T>(IEntityManager entities, EntityUid shuttle) where T: Component
{
var query = entities.EntityQueryEnumerator<T, TransformComponent>();
while (query.MoveNext(out _, out var xform))
{
if (xform.ParentUid != shuttle)
continue;

return true;
}

return false;
}
}
17 changes: 17 additions & 0 deletions Content.Server/_DV/Shipyard/MapDeleterShuttleComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Content.Server._DV.Shipyard;

/// <summary>
/// When added to a shuttle, once it FTLs the previous map is deleted.
/// After that the component is removed to prevent insane abuse.
/// </summary>
/// <remarks>
/// Could be upstreamed at some point, loneop shuttle could use it.
/// </remarks>
[RegisterComponent, Access(typeof(MapDeleterShuttleSystem))]
public sealed partial class MapDeleterShuttleComponent : Component
{
/// <summary>
/// Only set by the system to prevent someone in VV deleting maps by mistake or otherwise.
/// </summary>
public bool Enabled;
}
Loading
Loading