Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
69 changes: 69 additions & 0 deletions Content.Client/_Lua/Bank/UI/LuaATMMenu.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'bank-atm-menu-title'}"
MinSize="300 125"
Resizable="False">
<BoxContainer Orientation="Vertical">
<controls:StripeBack>
<PanelContainer Name="ATMNamePanel">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat/>
</PanelContainer.PanelOverride>
<Label Name="ATMName" Align="Center" Text="{Loc 'bank-atm-menu-name-nt'}"/>
</PanelContainer>
</controls:StripeBack>
<BoxContainer Margin="8 0 3 3" Orientation="Vertical">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc 'bank-atm-menu-balance-label'}" FontColorOverride="Silver"/>
<Label Name="BalanceLabel" Text="{Loc 'bank-atm-menu-no-bank'}" Margin="3 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc 'bank-atm-yupi-code'}" FontColorOverride="Silver"/>
<Label Name="YUPICode" Text="{Loc 'bank-atm-yupi-code-default'}" Margin="3 0" StyleClasses="monospace"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'bank-atm-menu-operation-history-label'}" FontColorOverride="Silver"/>
<BoxContainer Name="OperationHistoryContainer" Orientation="Vertical">
<Label Text="{Loc 'bank-atm-menu-operation-default'}"/>
</BoxContainer>
Comment on lines +13 to +29
</BoxContainer>
<BoxContainer Name="DepositContainer" Orientation="Horizontal" HorizontalExpand="True" VerticalAlignment="Bottom">
<Label Text="{Loc 'bank-atm-menu-deposit-label'}" FontColorOverride="Silver"/>
<Label Name="DepositLabel" Text="{Loc 'bank-atm-menu-no-deposit'}" HorizontalExpand="True"/>
<Button Name="DepositButton"
Text="{Loc 'bank-atm-menu-deposit-button'}"
SetSize="96 30"
Margin="6 1 3 1"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Bottom">
<LineEdit Name="WithdrawEdit" SetHeight="28" HorizontalExpand="True"/>
<Button Name="WithdrawButton"
Text="{Loc 'bank-atm-menu-withdraw-button'}"
SetSize="96 30"
Margin="3 1"/>
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="0 2 0 0" VerticalAlignment="Bottom">
<PanelContainer StyleClasses="LowDivider"/>
<BoxContainer Orientation="Horizontal"
Margin="10 2 5 0"
VerticalAlignment="Bottom">
<Label Text="{Loc 'bank-atm-hint'}"
StyleClasses="WindowFooterText"
HorizontalAlignment="Left"
Margin="0 0 5 0" />
<Label Text="{Loc 'bank-atm-version'}"
StyleClasses="WindowFooterText"
HorizontalAlignment="Right"
HorizontalExpand="True"
Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
HorizontalAlignment="Right"
SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
143 changes: 143 additions & 0 deletions Content.Client/_Lua/Bank/UI/LuaATMMenu.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// LuaCorp - This file is licensed under AGPLv3
// Copyright (c) 2026 LuaCorp Contributors
// See AGPLv3.txt for details.

using Robust.Client.Graphics;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface.Controls;
using Content.Client.UserInterface.Controls;
using Content.Shared._Lua.Bank.Events;
using Content.Shared._Lua.Bank.UI;
using Content.Shared._Lua.Bank;
using Content.Shared._NF.Bank;

namespace Content.Client._Lua.Bank.UI;

[GenerateTypedNameReferences]
public sealed partial class LuaATMMenu : FancyWindow
{
public event Action<int>? WithdrawRequest;
public event Action? DepositRequest;

private static Color CorruptedColor => new(139, 0, 0, 10);
private static Color NotCorruptedColor => new(0, 0, 139, 10);

private bool _enabled;

private int? _amount;

public LuaATMMenu()
{
RobustXamlLoader.Load(this);

DepositButton.OnPressed += OnDepositPressed;
WithdrawButton.OnPressed += OnWithdrawPressed;
WithdrawEdit.OnTextChanged += OnAmountChanged;
}

public void UpdateState(LuaATMMenuInterfaceState state)
{
UpdateCorruption(state.Corrupted);
UpdateDeposit(state.WithdrawOnly, state.Deposit);
}

public void UpdatePersonalState(LuaATMPersonalInfoMessage personalState)
{
UpdateStatus(personalState.Enabled);
UpdateBalance(personalState.Balance);
UpdateYUPICode(personalState.YUPICode);
UpdateHistory(personalState.History);
}

private void UpdateCorruption(bool corrupted)
{
var atmNameString = corrupted ? "bank-atm-menu-name-bm" : "bank-atm-menu-name-nt";
ATMName.Text = Loc.GetString(atmNameString);

if (ATMNamePanel.PanelOverride is StyleBoxFlat styleBox)
{
styleBox.BackgroundColor = corrupted ? CorruptedColor : NotCorruptedColor;
}
}

private void UpdateDeposit(bool withdrawOnly, int amount)
{
DepositButton.Disabled = amount <= 0;
DepositLabel.Text = amount >= 0
? BankSystemExtensions.ToSpesoString(amount)
: Loc.GetString("bank-atm-menu-cash-error");
DepositContainer.Visible = !withdrawOnly;
Comment on lines +64 to +70
}

private void UpdateStatus(bool enabled)
{
_enabled = enabled;
WithdrawButton.Disabled = !_enabled;
}

private void UpdateBalance(int balance)
{
BalanceLabel.Text = BankSystemExtensions.ToSpesoString(balance);
}

private void UpdateYUPICode(string yupiCode)
{
YUPICode.Text = string.IsNullOrWhiteSpace(yupiCode)
? Loc.GetString("bank-atm-yupi-code-default")
: yupiCode.ToUpperInvariant();
}

private void UpdateHistory(List<BankAccountOperation> history)
{
OperationHistoryContainer.RemoveAllChildren();

if (history.Count == 0)
{
var noOperationLabel = new Label
{
Text = Loc.GetString("bank-atm-menu-operation-default")
};

OperationHistoryContainer.AddChild(noOperationLabel);
return;
}

foreach (var operation in history)
{
var typeString = Loc.GetString($"bank-atm-menu-operation-{operation.Type.ToString().ToLower()}");
var valueString = BankSystemExtensions.ToSpesoString(operation.Value);
var timeString = operation.Time.ToString(@"hh\:mm\:ss");
var operationString = Loc.GetString("bank-atm-menu-operation", ("type", typeString), ("value", valueString), ("time", timeString));

var operationLabel = new Label
{
Text = operationString
};

OperationHistoryContainer.AddChild(operationLabel);
}
Comment on lines +106 to +119
}

private void OnWithdrawPressed(BaseButton.ButtonEventArgs args)
{
if (_amount == null)
{
return;
}

WithdrawRequest?.Invoke(_amount.Value);
}

private void OnDepositPressed(BaseButton.ButtonEventArgs args)
{
DepositRequest?.Invoke();
}

private void OnAmountChanged(LineEdit.LineEditEventArgs args)
{
var parsable = int.TryParse(args.Text, out var amount);
_amount = parsable ? amount : null;
WithdrawButton.Disabled = !_enabled || !parsable;
}
Comment on lines +137 to +142
}
55 changes: 55 additions & 0 deletions Content.Client/_Lua/Bank/UI/LuaATMMenuBoundUserInterface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// LuaCorp - This file is licensed under AGPLv3
// Copyright (c) 2026 LuaCorp Contributors
// See AGPLv3.txt for details.

using Robust.Client.UserInterface;
using Content.Shared._NF.Bank.Events;
using Content.Shared._Lua.Bank.Events;
using Content.Shared._Lua.Bank.UI;

namespace Content.Client._Lua.Bank.UI;

public sealed class LuaATMMenuBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private LuaATMMenu? _menu;

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

_menu = this.CreateWindow<LuaATMMenu>();

_menu.WithdrawRequest += OnWithdraw;
_menu.DepositRequest += OnDeposit;
}

private void OnWithdraw(int amount)
{
SendMessage(new BankWithdrawMessage(amount));
}

private void OnDeposit()
{
SendMessage(new BankDepositMessage());
}

protected override void UpdateState(BoundUserInterfaceState state)
{
if (_menu == null || state is not LuaATMMenuInterfaceState bankState)
{
return;
}

_menu.UpdateState(bankState);
}

protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_menu == null || message is not LuaATMPersonalInfoMessage personalMessage)
{
return;
}

_menu.UpdatePersonalState(personalMessage);
}
}
45 changes: 38 additions & 7 deletions Content.IntegrationTests/Tests/EntityTest.cs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Spawners;

namespace Content.IntegrationTests.Tests
{
[TestFixture]
[TestOf(typeof(EntityUid))]
public sealed class EntityTest
{
private static readonly ProtoId<EntityCategoryPrototype> SpawnerCategory = "Spawner";
private static readonly HashSet<ProtoId<EntityCategoryPrototype>> IgnoredCategories = ["Spawner", "Debug"];

[Test]
[Ignore("Test broken upstream, restore when working.")] // Frontier
Expand Down Expand Up @@ -262,10 +263,10 @@ public async Task SpawnAndDeleteEntityCountTest()

var protoIds = server.ProtoMan
.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract
&& !pair.IsTestPrototype(p)
&& !excluded.Any(p.Components.ContainsKey)
&& p.Categories.All(x => x.ID != SpawnerCategory))
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => !excluded.Any(p.Components.ContainsKey))
.Where(p => p.Categories.All(x => !IgnoredCategories.Contains(x.ID)))
.Select(p => p.ID)
.ToList();

Expand All @@ -283,7 +284,7 @@ await server.WaitPost(() =>

// We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e)); // Lua
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e));

await Assert.MultipleAsync(async () =>
{
Expand All @@ -300,6 +301,8 @@ await Assert.MultipleAsync(async () =>
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
{
await CleanupTransientEntities(pair, serverEntities);

Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deleting itself\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deleting itself\n" +
Expand All @@ -319,12 +322,13 @@ await Assert.MultipleAsync(async () =>

await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
await CleanupTransientEntities(pair, serverEntities);

// Check that the number of entities has gone back to the original value.
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
}
Expand All @@ -333,6 +337,33 @@ await Assert.MultipleAsync(async () =>
await pair.CleanReturnAsync();
}

/// <summary>
/// Deletes any entities with <see cref="TimedDespawnComponent"/> that were not present in the baseline snapshot.
/// Some entities spawn transient side-effects on deletion (e.g. explosion visuals). These side-effect entities
/// use TimedDespawn and would persist across test iterations, corrupting baseline entity counts and causing
/// cascading assertion failures.
/// </summary>
private static async Task CleanupTransientEntities(Pair.TestPair pair, HashSet<EntityUid> baselineEntities)
{
var server = pair.Server;
await server.WaitPost(() =>
{
var toRemove = new List<EntityUid>();
var query = server.EntMan.AllEntityQueryEnumerator<TimedDespawnComponent>();
while (query.MoveNext(out var uid, out _))
{
if (!baselineEntities.Contains(uid))
toRemove.Add(uid);
}

foreach (var uid in toRemove)
{
server.EntMan.DeleteEntity(uid);
}
});
await pair.RunTicksSync(3);
}

private static string BuildDiffString(IEnumerable<EntityUid> oldEnts, IEnumerable<EntityUid> newEnts, IEntityManager entMan)
{
var sb = new StringBuilder();
Expand Down
24 changes: 24 additions & 0 deletions Content.Server/_Lua/Bank/Systems/BankSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// LuaCorp - This file is licensed under AGPLv3
// Copyright (c) 2026 LuaCorp Contributors
// See AGPLv3.txt for details.

using Robust.Shared.Timing;
using Content.Shared._Lua.Bank;
using Content.Shared._NF.Bank.Components;

namespace Content.Server._NF.Bank;

public sealed partial class BankSystem
{
[Dependency] private readonly IGameTiming _timing = default!;

private void AddOperationRecord(BankAccountComponent comp, BankAccountOperationType type, int value)
{
comp.OperationHistory.Add(new(type, value, _timing.CurTime));

if (comp.OperationHistory.Count > 50)
{
comp.OperationHistory.RemoveRange(0, 30);
}
}
}
Loading
Loading