893 lines
36 KiB
C#
893 lines
36 KiB
C#
using Content.Server.Access.Systems;
|
|
using Content.Server.Popups;
|
|
using Content.Server.Radio.EntitySystems;
|
|
using Content.Server._NF.Bank;
|
|
using Content.Server._NF.Shipyard.Components;
|
|
using Content.Server._NF.ShuttleRecords;
|
|
using Content.Shared._NF.Bank.Components;
|
|
using Content.Shared._NF.Shipyard;
|
|
using Content.Shared._NF.Shipyard.Events;
|
|
using Content.Shared._NF.Shipyard.BUI;
|
|
using Content.Shared._NF.Shipyard.Prototypes;
|
|
using Content.Shared._NF.Shipyard.Components;
|
|
using Content.Shared.Access.Systems;
|
|
using Content.Shared.Access.Components;
|
|
using Content.Shared.Ghost;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Prototypes;
|
|
using Content.Shared.Radio;
|
|
using System.Linq;
|
|
using Content.Server.Administration.Logs;
|
|
using Content.Shared.Mobs.Components;
|
|
using Content.Shared.Mobs.Systems;
|
|
using Content.Server.Maps;
|
|
using Content.Shared.StationRecords;
|
|
using Content.Server.Chat.Systems;
|
|
using Content.Server.Mind;
|
|
using Content.Server.Preferences.Managers;
|
|
using Content.Server.StationRecords;
|
|
using Content.Server.StationRecords.Systems;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Preferences;
|
|
using Content.Server.Shuttles.Components;
|
|
using Content.Server._NF.Station.Components;
|
|
using System.Text.RegularExpressions;
|
|
using Content.Shared.UserInterface;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Content.Shared.Access;
|
|
using Content.Shared._NF.Bank.BUI;
|
|
using Content.Shared._NF.ShuttleRecords;
|
|
using Content.Server.StationEvents.Components;
|
|
using Content.Shared.Forensics.Components;
|
|
using Robust.Server.Player;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Timing;
|
|
using Content.Server._Horizon.Shipyard;
|
|
|
|
namespace Content.Server._NF.Shipyard.Systems;
|
|
|
|
public sealed partial class ShipyardSystem : SharedShipyardSystem
|
|
{
|
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
[Dependency] private readonly IGameTiming _timing = default!;
|
|
[Dependency] private readonly IPlayerManager _player = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly IServerPreferencesManager _prefManager = default!;
|
|
[Dependency] private readonly AccessSystem _accessSystem = default!;
|
|
[Dependency] private readonly AccessReaderSystem _access = default!;
|
|
[Dependency] private readonly PopupSystem _popup = default!;
|
|
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
|
[Dependency] private readonly RadioSystem _radio = default!;
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly BankSystem _bank = default!;
|
|
[Dependency] private readonly IdCardSystem _idSystem = default!;
|
|
[Dependency] private readonly StationRecordsSystem _records = default!;
|
|
[Dependency] private readonly ChatSystem _chat = default!;
|
|
[Dependency] private readonly MindSystem _mind = default!;
|
|
[Dependency] private readonly ShuttleRecordsSystem _shuttleRecordsSystem = default!;
|
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
|
|
|
// Horizon
|
|
[Dependency] private readonly ShipOwnershipSystem? _shipOwnership = default!;
|
|
|
|
private static readonly Regex DeedRegex = new(@"\s*\([^()]*\)");
|
|
|
|
public void InitializeConsole()
|
|
{
|
|
|
|
}
|
|
|
|
private void OnPurchaseMessage(EntityUid shipyardConsoleUid, ShipyardConsoleComponent component, ShipyardConsolePurchaseMessage args)
|
|
{
|
|
if (args.Actor is not { Valid: true } player)
|
|
return;
|
|
|
|
if (component.TargetIdSlot.ContainerSlot?.ContainedEntity is not { Valid: true } targetId)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-idcard"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
TryComp<IdCardComponent>(targetId, out var idCard);
|
|
TryComp<ShipyardVoucherComponent>(targetId, out var voucher);
|
|
if (idCard is null && voucher is null)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-idcard"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
if (HasComp<ShuttleDeedComponent>(targetId))
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-already-deeded"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
if (TryComp<AccessReaderComponent>(shipyardConsoleUid, out var accessReaderComponent) && !_access.IsAllowed(player, shipyardConsoleUid, accessReaderComponent))
|
|
{
|
|
ConsolePopup(player, Loc.GetString("comms-console-permission-denied"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
if (!_prototypeManager.TryIndex<VesselPrototype>(args.Vessel, out var vessel))
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-invalid-vessel", ("vessel", args.Vessel)));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
if (!GetAvailableShuttles(shipyardConsoleUid, targetId: targetId).available.Contains(vessel.ID))
|
|
{
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(player):player} tried to purchase a vessel that was never available.");
|
|
return;
|
|
}
|
|
|
|
var name = vessel.Name;
|
|
if (vessel.Price <= 0)
|
|
return;
|
|
|
|
if (_station.GetOwningStation(shipyardConsoleUid) is not { Valid: true } station)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-invalid-station"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
if (!TryComp<BankAccountComponent>(player, out var bank))
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-bank"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
// Keep track of whether or not a voucher was used.
|
|
// TODO: voucher purchase should be done in a separate function.
|
|
bool voucherUsed = false;
|
|
if (voucher is not null)
|
|
{
|
|
if (voucher!.RedemptionsLeft <= 0)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-voucher-redemptions"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
if (voucher!.DestroyOnEmpty)
|
|
{
|
|
QueueDel(targetId);
|
|
}
|
|
return;
|
|
}
|
|
else if (voucher!.ConsoleType != (ShipyardConsoleUiKey)args.UiKey)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-invalid-voucher-type"));
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
voucher.RedemptionsLeft--;
|
|
voucherUsed = true;
|
|
}
|
|
else
|
|
{
|
|
// Horizon start
|
|
var price = vessel.Price;
|
|
|
|
foreach (var item in vessel.CostModifiers)
|
|
item.Modify(player, shipyardConsoleUid, ref price, _entityManager);
|
|
|
|
// Horizon end
|
|
if (!_bank.TryBankWithdraw(player, price)) // Horizon - modify price
|
|
{
|
|
ConsolePopup(player, Loc.GetString("cargo-console-insufficient-funds", ("cost", price))); // Horizon - modify price
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!TryPurchaseShuttle(station, vessel.ShuttlePath, out var shuttleUidOut))
|
|
{
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
|
|
var shuttleUid = shuttleUidOut.Value;
|
|
if (!TryComp<ShuttleComponent>(shuttleUid, out var shuttle))
|
|
{
|
|
PlayDenySound(player, shipyardConsoleUid, component);
|
|
return;
|
|
}
|
|
EntityUid? shuttleStation = null;
|
|
// setting up any stations if we have a matching game map prototype to allow late joins directly onto the vessel
|
|
if (_prototypeManager.TryIndex<GameMapPrototype>(vessel.ID, out var stationProto))
|
|
{
|
|
List<EntityUid> gridUids = new()
|
|
{
|
|
shuttleUid
|
|
};
|
|
shuttleStation = _station.InitializeNewStation(stationProto.Stations[vessel.ID], gridUids);
|
|
name = Name(shuttleStation.Value);
|
|
|
|
var vesselInfo = EnsureComp<ExtraShuttleInformationComponent>(shuttleStation.Value);
|
|
vesselInfo.Vessel = vessel.ID;
|
|
}
|
|
|
|
if (TryComp<AccessComponent>(targetId, out var newCap))
|
|
{
|
|
var newAccess = newCap.Tags.ToList();
|
|
newAccess.AddRange(component.NewAccessLevels);
|
|
_accessSystem.TrySetTags(targetId, newAccess, newCap);
|
|
}
|
|
|
|
var deedID = EnsureComp<ShuttleDeedComponent>(targetId);
|
|
|
|
var shuttleOwner = Name(player).Trim();
|
|
AssignShuttleDeedProperties((targetId, deedID), shuttleUid, name, shuttleOwner, voucherUsed);
|
|
|
|
var deedShuttle = EnsureComp<ShuttleDeedComponent>(shuttleUid);
|
|
AssignShuttleDeedProperties((shuttleUid, deedShuttle), shuttleUid, name, shuttleOwner, voucherUsed);
|
|
|
|
if (!voucherUsed && component.NewJobTitle != null && !HasComp<PreventShipyardTitleOverwriteComponent>(args.Actor))
|
|
{
|
|
_idSystem.TryChangeJobTitle(targetId, Loc.GetString(component.NewJobTitle), idCard, player);
|
|
}
|
|
|
|
// The following block of code is entirely to do with trying to sanely handle moving records from station to station.
|
|
// it is ass.
|
|
// This probably shouldnt be messed with further until station records themselves become more robust
|
|
// and not entirely dependent upon linking ID card entity to station records key lookups
|
|
// its just bad
|
|
|
|
var stationList = EntityQueryEnumerator<StationRecordsComponent>();
|
|
|
|
if (TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
|
&& shuttleStation != null
|
|
&& keyStorage.Key != null)
|
|
{
|
|
bool recSuccess = false;
|
|
while (stationList.MoveNext(out var stationUid, out var stationRecComp))
|
|
{
|
|
if (!_records.TryGetRecord<GeneralStationRecord>(keyStorage.Key.Value, out var record))
|
|
continue;
|
|
|
|
//_records.RemoveRecord(keyStorage.Key.Value);
|
|
_records.AddRecordEntry(shuttleStation.Value, record);
|
|
recSuccess = true;
|
|
break;
|
|
}
|
|
|
|
if (!recSuccess
|
|
&& _mind.TryGetMind(player, out var mindUid, out var mindComp)
|
|
&& mindComp.UserId != null
|
|
&& _prefManager.GetPreferences(mindComp.UserId.Value).SelectedCharacter is HumanoidCharacterProfile profile)
|
|
{
|
|
TryComp<FingerprintComponent>(player, out var fingerprintComponent);
|
|
TryComp<DnaComponent>(player, out var dnaComponent);
|
|
TryComp<StationRecordsComponent>(shuttleStation, out var stationRec);
|
|
_records.CreateGeneralRecord(shuttleStation.Value, targetId, profile.Name, profile.Age, profile.Species, profile.Gender, $"Captain", fingerprintComponent!.Fingerprint, dnaComponent!.DNA, profile, stationRec!);
|
|
}
|
|
}
|
|
_records.Synchronize(shuttleStation!.Value);
|
|
_records.Synchronize(station);
|
|
|
|
EntityManager.AddComponents(shuttleUid, vessel.AddComponents);
|
|
|
|
// Ensure cleanup on ship sale
|
|
EnsureComp<LinkedLifecycleGridParentComponent>(shuttleUid);
|
|
|
|
// Horizon start
|
|
// Register ship ownership for auto-deletion when owner is offline too long
|
|
// We need to get the player's session from their entity
|
|
if (_shipOwnership != null && TryComp<ActorComponent>(player, out var actorComp) && actorComp.PlayerSession != null)
|
|
{
|
|
_shipOwnership.RegisterShipOwnership(shuttleUid, actorComp.PlayerSession);
|
|
}
|
|
// Horizon end
|
|
|
|
var sellValue = 0;
|
|
if (!voucherUsed)
|
|
{
|
|
// Get the price of the ship
|
|
if (TryComp<ShuttleDeedComponent>(targetId, out var deed))
|
|
sellValue = (int)_pricing.AppraiseGrid((EntityUid)(deed?.ShuttleUid!), LacksPreserveOnSaleComp);
|
|
|
|
// Adjust for taxes
|
|
sellValue = CalculateShipResaleValue((shipyardConsoleUid, component), sellValue);
|
|
}
|
|
|
|
SendPurchaseMessage(shipyardConsoleUid, player, name, component.ShipyardChannel, secret: false);
|
|
if (component.SecretShipyardChannel is { } secretChannel)
|
|
SendPurchaseMessage(shipyardConsoleUid, player, name, secretChannel, secret: true);
|
|
|
|
PlayConfirmSound(player, shipyardConsoleUid, component);
|
|
if (voucherUsed)
|
|
_adminLogger.Add(LogType.ShipYardUsage, LogImpact.Low, $"{ToPrettyString(player):actor} used {ToPrettyString(targetId)} to purchase shuttle {ToPrettyString(shuttleUid)} with a voucher via {ToPrettyString(shipyardConsoleUid)}");
|
|
else
|
|
_adminLogger.Add(LogType.ShipYardUsage, LogImpact.Low, $"{ToPrettyString(player):actor} used {ToPrettyString(targetId)} to purchase shuttle {ToPrettyString(shuttleUid)} for {vessel.Price} credits via {ToPrettyString(shipyardConsoleUid)}");
|
|
|
|
// Adding the record to the shuttle records system makes them eligible to be copied.
|
|
// Can be set on the component of the shipyard.
|
|
if (component.CanTransferDeed)
|
|
{
|
|
_shuttleRecordsSystem.AddRecord(
|
|
new ShuttleRecord(
|
|
name: deedShuttle.ShuttleName ?? "",
|
|
suffix: deedShuttle.ShuttleNameSuffix ?? "",
|
|
ownerName: shuttleOwner,
|
|
entityUid: EntityManager.GetNetEntity(shuttleUid),
|
|
purchasedWithVoucher: voucherUsed,
|
|
purchasePrice: (uint)vessel.Price,
|
|
vesselPrototypeId: vessel.ID
|
|
)
|
|
);
|
|
}
|
|
|
|
RefreshState(shipyardConsoleUid, bank.Balance, true, name, sellValue, targetId, (ShipyardConsoleUiKey)args.UiKey, voucherUsed);
|
|
}
|
|
|
|
private void TryParseShuttleName(ShuttleDeedComponent deed, string name)
|
|
{
|
|
// The logic behind this is: if a name part fits the requirements, it is the required part. Otherwise it's the name.
|
|
// This may cause problems but ONLY when renaming a ship. It will still display properly regardless of this.
|
|
var nameParts = name.Split(' ');
|
|
|
|
var hasSuffix = nameParts.Length > 1 && nameParts.Last().Length < ShuttleDeedComponent.MaxSuffixLength && nameParts.Last().Contains('-');
|
|
deed.ShuttleNameSuffix = hasSuffix ? nameParts.Last() : null;
|
|
deed.ShuttleName = String.Join(" ", nameParts.SkipLast(hasSuffix ? 1 : 0));
|
|
}
|
|
|
|
public void OnSellMessage(EntityUid uid, ShipyardConsoleComponent component, ShipyardConsoleSellMessage args)
|
|
{
|
|
|
|
if (args.Actor is not { Valid: true } player)
|
|
return;
|
|
|
|
if (component.TargetIdSlot.ContainerSlot?.ContainedEntity is not { Valid: true } targetId)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-idcard"));
|
|
PlayDenySound(player, uid, component);
|
|
return;
|
|
}
|
|
|
|
TryComp<IdCardComponent>(targetId, out var idCard);
|
|
TryComp<ShipyardVoucherComponent>(targetId, out var voucher);
|
|
if (idCard is null && voucher is null)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-idcard"));
|
|
PlayDenySound(player, uid, component);
|
|
return;
|
|
}
|
|
|
|
if (!TryComp<ShuttleDeedComponent>(targetId, out var deed) || deed.ShuttleUid is not { Valid: true } shuttleUid)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-deed"));
|
|
PlayDenySound(player, uid, component);
|
|
return;
|
|
}
|
|
|
|
bool voucherUsed = deed.PurchasedWithVoucher;
|
|
|
|
if (!TryComp<BankAccountComponent>(player, out var bank))
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-no-bank"));
|
|
PlayDenySound(player, uid, component);
|
|
return;
|
|
}
|
|
|
|
if (_station.GetOwningStation(uid) is not { Valid: true } stationUid)
|
|
{
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-invalid-station"));
|
|
PlayDenySound(player, uid, component);
|
|
return;
|
|
}
|
|
|
|
if (_station.GetOwningStation(shuttleUid) is { Valid: true } shuttleStation
|
|
&& TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|
|
&& keyStorage.Key != null
|
|
&& keyStorage.Key.Value.OriginStation == shuttleStation
|
|
&& _records.TryGetRecord<GeneralStationRecord>(keyStorage.Key.Value, out var record))
|
|
{
|
|
//_records.RemoveRecord(keyStorage.Key.Value);
|
|
_records.AddRecordEntry(stationUid, record);
|
|
_records.Synchronize(stationUid);
|
|
}
|
|
|
|
var shuttleName = ToPrettyString(shuttleUid); // Grab the name before it gets 1984'd
|
|
var shuttleNetEntity = _entityManager.GetNetEntity(shuttleUid); // same with the netEntity for shuttle records
|
|
|
|
// Check for shipyard blacklisting components
|
|
var disableSaleQuery = GetEntityQuery<ShipyardSellConditionComponent>();
|
|
var xformQuery = GetEntityQuery<TransformComponent>();
|
|
var disableSaleMsg = FindDisableShipyardSaleObjects(shuttleUid, (ShipyardConsoleUiKey)args.UiKey, disableSaleQuery, xformQuery);
|
|
if (disableSaleMsg != null)
|
|
{
|
|
ConsolePopup(player, Loc.GetString(disableSaleMsg));
|
|
PlayDenySound(player, uid, component);
|
|
return;
|
|
}
|
|
|
|
var saleResult = TrySellShuttle(stationUid, shuttleUid, uid, out var bill);
|
|
if (saleResult.Error != ShipyardSaleError.Success)
|
|
{
|
|
switch (saleResult.Error)
|
|
{
|
|
case ShipyardSaleError.Undocked:
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-sale-not-docked"));
|
|
break;
|
|
case ShipyardSaleError.OrganicsAboard:
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-sale-organic-aboard", ("name", saleResult.OrganicName ?? "Somebody")));
|
|
break;
|
|
case ShipyardSaleError.InvalidShip:
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-sale-invalid-ship"));
|
|
break;
|
|
default:
|
|
ConsolePopup(player, Loc.GetString("shipyard-console-sale-unknown-reason", ("reason", saleResult.Error.ToString())));
|
|
break;
|
|
}
|
|
PlayDenySound(player, uid, component);
|
|
return;
|
|
}
|
|
|
|
// Update shuttle records
|
|
_shuttleRecordsSystem.TrySetSaleTime(shuttleNetEntity);
|
|
|
|
RemComp<ShuttleDeedComponent>(targetId);
|
|
|
|
if (!voucherUsed)
|
|
{
|
|
if (!component.IgnoreBaseSaleRate)
|
|
bill = (int)(bill * _baseSaleRate);
|
|
|
|
int originalBill = bill;
|
|
foreach (var (account, taxCoeff) in component.TaxAccounts)
|
|
{
|
|
var tax = CalculateSalesTax(originalBill, taxCoeff);
|
|
_bank.TrySectorDeposit(account, tax, LedgerEntryType.BlackMarketShipyardTax);
|
|
bill -= tax;
|
|
}
|
|
bill = int.Max(0, bill);
|
|
|
|
_bank.TryBankDeposit(player, bill);
|
|
PlayConfirmSound(player, uid, component);
|
|
}
|
|
|
|
var name = GetFullName(deed);
|
|
SendSellMessage(uid, deed.ShuttleOwner!, name, component.ShipyardChannel, player, secret: false);
|
|
if (component.SecretShipyardChannel is { } secretChannel)
|
|
SendSellMessage(uid, deed.ShuttleOwner!, name, secretChannel, player, secret: true);
|
|
|
|
EntityUid? refreshId = targetId;
|
|
|
|
if (voucherUsed)
|
|
_adminLogger.Add(LogType.ShipYardUsage, LogImpact.Low, $"{ToPrettyString(player):actor} used {ToPrettyString(targetId)} to sell {shuttleName} (purchased with voucher) via {ToPrettyString(uid)}");
|
|
else
|
|
_adminLogger.Add(LogType.ShipYardUsage, LogImpact.Low, $"{ToPrettyString(player):actor} used {ToPrettyString(targetId)} to sell {shuttleName} for {bill} credits via {ToPrettyString(uid)}");
|
|
|
|
// No uses on the voucher left, destroy it.
|
|
if (voucher != null
|
|
&& voucher!.RedemptionsLeft <= 0
|
|
&& voucher!.DestroyOnEmpty)
|
|
{
|
|
QueueDel(targetId);
|
|
refreshId = null;
|
|
}
|
|
|
|
RefreshState(uid, bank.Balance, true, null, 0, refreshId, (ShipyardConsoleUiKey)args.UiKey, voucherUsed);
|
|
}
|
|
|
|
private void OnConsoleUIOpened(EntityUid uid, ShipyardConsoleComponent component, BoundUIOpenedEvent args)
|
|
{
|
|
if (!component.Initialized)
|
|
return;
|
|
|
|
// kind of cursed. We need to update the UI when an Id is entered, but the UI needs to know the player characters bank account.
|
|
if (!TryComp<ActivatableUIComponent>(uid, out var uiComp) || uiComp.Key == null)
|
|
return;
|
|
|
|
if (args.Actor is not { Valid: true } player)
|
|
return;
|
|
|
|
// mayhaps re-enable this later for HoS/SA
|
|
// var station = _station.GetOwningStation(uid);
|
|
|
|
if (!TryComp<BankAccountComponent>(player, out var bank))
|
|
return;
|
|
|
|
var targetId = component.TargetIdSlot.ContainerSlot?.ContainedEntity;
|
|
|
|
if (TryComp<ShuttleDeedComponent>(targetId, out var deed))
|
|
{
|
|
if (Deleted(deed!.ShuttleUid))
|
|
{
|
|
RemComp<ShuttleDeedComponent>(targetId!.Value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var voucherUsed = HasComp<ShipyardVoucherComponent>(targetId);
|
|
|
|
int sellValue = 0;
|
|
if (deed?.ShuttleUid != null)
|
|
{
|
|
sellValue = (int)_pricing.AppraiseGrid((EntityUid)(deed?.ShuttleUid!), LacksPreserveOnSaleComp);
|
|
sellValue = CalculateShipResaleValue((uid, component), sellValue);
|
|
}
|
|
|
|
var fullName = deed != null ? GetFullName(deed) : null;
|
|
RefreshState(uid, bank.Balance, true, fullName, sellValue, targetId, (ShipyardConsoleUiKey)args.UiKey, voucherUsed);
|
|
}
|
|
|
|
private void ConsolePopup(EntityUid uid, string text)
|
|
{
|
|
_popup.PopupEntity(text, uid);
|
|
}
|
|
|
|
private void SendPurchaseMessage(EntityUid uid, EntityUid player, string name, string shipyardChannel, bool secret)
|
|
{
|
|
var channel = _prototypeManager.Index<RadioChannelPrototype>(shipyardChannel);
|
|
|
|
if (secret)
|
|
{
|
|
_radio.SendRadioMessage(uid, Loc.GetString("shipyard-console-docking-secret"), channel, uid);
|
|
_chat.TrySendInGameICMessage(uid, Loc.GetString("shipyard-console-docking-secret"), InGameICChatType.Speak, true);
|
|
}
|
|
else
|
|
{
|
|
_radio.SendRadioMessage(uid, Loc.GetString("shipyard-console-docking", ("owner", player), ("vessel", name)), channel, uid);
|
|
_chat.TrySendInGameICMessage(uid, Loc.GetString("shipyard-console-docking", ("owner", player!), ("vessel", name)), InGameICChatType.Speak, true);
|
|
}
|
|
}
|
|
|
|
private void SendSellMessage(EntityUid uid, string? player, string name, string shipyardChannel, EntityUid seller, bool secret)
|
|
{
|
|
var channel = _prototypeManager.Index<RadioChannelPrototype>(shipyardChannel);
|
|
|
|
if (secret)
|
|
{
|
|
_radio.SendRadioMessage(uid, Loc.GetString("shipyard-console-leaving-secret"), channel, uid);
|
|
_chat.TrySendInGameICMessage(uid, Loc.GetString("shipyard-console-leaving-secret"), InGameICChatType.Speak, true);
|
|
}
|
|
else
|
|
{
|
|
_radio.SendRadioMessage(uid, Loc.GetString("shipyard-console-leaving", ("owner", player!), ("vessel", name!), ("player", seller)), channel, uid);
|
|
_chat.TrySendInGameICMessage(uid, Loc.GetString("shipyard-console-leaving", ("owner", player!), ("vessel", name!), ("player", seller)), InGameICChatType.Speak, true);
|
|
}
|
|
}
|
|
|
|
private void PlayDenySound(EntityUid playerUid, EntityUid consoleUid, ShipyardConsoleComponent component)
|
|
{
|
|
if (_timing.CurTime >= component.NextDenySoundTime)
|
|
{
|
|
component.NextDenySoundTime = _timing.CurTime + component.DenySoundDelay;
|
|
_audio.PlayPvs(_audio.ResolveSound(component.ErrorSound), consoleUid);
|
|
}
|
|
}
|
|
|
|
private void PlayConfirmSound(EntityUid playerUid, EntityUid consoleUid, ShipyardConsoleComponent component)
|
|
{
|
|
_audio.PlayEntity(component.ConfirmSound, playerUid, consoleUid);
|
|
}
|
|
|
|
private void OnItemSlotChanged(EntityUid uid, ShipyardConsoleComponent component, ContainerModifiedMessage args)
|
|
{
|
|
if (!component.Initialized)
|
|
return;
|
|
|
|
if (args.Container.ID != component.TargetIdSlot.ID)
|
|
return;
|
|
|
|
// kind of cursed. We need to update the UI when an Id is entered, but the UI needs to know the player characters bank account.
|
|
if (!TryComp<ActivatableUIComponent>(uid, out var uiComp) || uiComp.Key == null)
|
|
return;
|
|
|
|
var uiUsers = _ui.GetActors(uid, uiComp.Key);
|
|
|
|
foreach (var user in uiUsers)
|
|
{
|
|
if (user is not { Valid: true } player)
|
|
continue;
|
|
|
|
if (!TryComp<BankAccountComponent>(player, out var bank))
|
|
continue;
|
|
|
|
var targetId = component.TargetIdSlot.ContainerSlot?.ContainedEntity;
|
|
|
|
if (TryComp<ShuttleDeedComponent>(targetId, out var deed))
|
|
{
|
|
if (Deleted(deed!.ShuttleUid))
|
|
{
|
|
RemComp<ShuttleDeedComponent>(targetId!.Value);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var voucherUsed = HasComp<ShipyardVoucherComponent>(targetId);
|
|
|
|
int sellValue = 0;
|
|
if (deed?.ShuttleUid != null)
|
|
{
|
|
sellValue = (int)_pricing.AppraiseGrid(deed.ShuttleUid.Value, LacksPreserveOnSaleComp);
|
|
sellValue = CalculateShipResaleValue((uid, component), sellValue);
|
|
}
|
|
|
|
var fullName = deed != null ? GetFullName(deed) : null;
|
|
RefreshState(uid,
|
|
bank.Balance,
|
|
true,
|
|
fullName,
|
|
sellValue,
|
|
targetId,
|
|
(ShipyardConsoleUiKey)uiComp.Key,
|
|
voucherUsed);
|
|
|
|
}
|
|
|
|
// Horizon start
|
|
component.CurIdCard = GetNetEntity(component.TargetIdSlot.ContainerSlot?.ContainedEntity);
|
|
Dirty(uid, component);
|
|
// Horizon end
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks for a living, sapient being aboard a particular entity.
|
|
/// </summary>
|
|
/// <param name="uid">The entity to search (e.g. a shuttle, a station)</param>
|
|
/// <param name="mobQuery">A query to get the MobState from an entity</param>
|
|
/// <param name="xformQuery">A query to get the transform component of an entity</param>
|
|
/// <returns>The name of the sapient being if one was found, null otherwise.</returns>
|
|
public string? FoundOrganics(EntityUid uid, EntityQuery<MobStateComponent> mobQuery, EntityQuery<TransformComponent> xformQuery)
|
|
{
|
|
var xform = xformQuery.GetComponent(uid);
|
|
var childEnumerator = xform.ChildEnumerator;
|
|
|
|
while (childEnumerator.MoveNext(out var child))
|
|
{
|
|
// Ghosts don't stop a ship sale.
|
|
if (HasComp<GhostComponent>(child))
|
|
continue;
|
|
|
|
// Check if we have a player entity that's either still around or alive and may come back
|
|
if (_mind.TryGetMind(child, out _, out var mindComp)
|
|
&& (mindComp.UserId != null && _player.ValidSessionId(mindComp.UserId.Value)
|
|
|| !_mind.IsCharacterDeadPhysically(mindComp)))
|
|
{
|
|
return Name(child);
|
|
}
|
|
else
|
|
{
|
|
var charName = FoundOrganics(child, mobQuery, xformQuery);
|
|
if (charName != null)
|
|
return charName;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks for any entities marked as preventing sale on a shuttle
|
|
/// </summary>
|
|
/// <param name="shuttle">The entity to search (e.g. a shuttle, a station)</param>
|
|
/// <param name="key">The UI key of the current shipyard console. Used to see if the shipyard should ignore this check</param>
|
|
/// <param name="disableSaleQuery">A query to get any marked objects from an entity</param>
|
|
/// <param name="xformQuery">A query to get the transform component of an entity</param>
|
|
/// <returns>The reason that a shuttle should be blocked from sale, null otherwise.</returns>
|
|
public string? FindDisableShipyardSaleObjects(EntityUid shuttle, ShipyardConsoleUiKey key, EntityQuery<ShipyardSellConditionComponent> disableSaleQuery, EntityQuery<TransformComponent> xformQuery)
|
|
{
|
|
var xform = xformQuery.GetComponent(shuttle);
|
|
var childEnumerator = xform.ChildEnumerator;
|
|
|
|
while (childEnumerator.MoveNext(out var child))
|
|
{
|
|
if (disableSaleQuery.TryGetComponent(child, out var disableSale)
|
|
&& disableSale.BlockSale is true
|
|
&& !disableSale.AllowedShipyardTypes.Contains(key))
|
|
{
|
|
return disableSale.Reason ?? "shipyard-console-fallback-prevent-sale";
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private struct IDShipAccesses
|
|
{
|
|
public IReadOnlyCollection<ProtoId<AccessLevelPrototype>> Tags;
|
|
public IReadOnlyCollection<ProtoId<AccessGroupPrototype>> Groups;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all shuttle prototype IDs the given shipyard console can offer.
|
|
/// </summary>
|
|
public (List<string> available, List<string> unavailable) GetAvailableShuttles(EntityUid uid, ShipyardConsoleUiKey? key = null,
|
|
ShipyardListingComponent? listing = null, EntityUid? targetId = null)
|
|
{
|
|
var available = new List<string>();
|
|
var unavailable = new List<string>();
|
|
|
|
if (key == null && TryComp<UserInterfaceComponent>(uid, out var ui))
|
|
{
|
|
// Try to find a ui key that is an instance of the shipyard console ui key
|
|
foreach (var (k, v) in ui.Actors)
|
|
{
|
|
if (k is ShipyardConsoleUiKey shipyardKey)
|
|
{
|
|
key = shipyardKey;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// No listing provided, try to get the current one from the console being used as a default.
|
|
if (listing is null)
|
|
TryComp(uid, out listing);
|
|
|
|
// Construct access set from input type (voucher or ID card)
|
|
IDShipAccesses accesses;
|
|
bool initialHasAccess = true;
|
|
if (TryComp<ShipyardVoucherComponent>(targetId, out var voucher))
|
|
{
|
|
if (voucher.ConsoleType == key)
|
|
{
|
|
accesses.Tags = voucher.Access;
|
|
accesses.Groups = voucher.AccessGroups;
|
|
}
|
|
else
|
|
{
|
|
accesses.Tags = new HashSet<ProtoId<AccessLevelPrototype>>();
|
|
accesses.Groups = new HashSet<ProtoId<AccessGroupPrototype>>();
|
|
initialHasAccess = false;
|
|
}
|
|
}
|
|
else if (TryComp<AccessComponent>(targetId, out var accessComponent))
|
|
{
|
|
accesses.Tags = accessComponent.Tags;
|
|
accesses.Groups = accessComponent.Groups;
|
|
}
|
|
else
|
|
{
|
|
accesses.Tags = new HashSet<ProtoId<AccessLevelPrototype>>();
|
|
accesses.Groups = new HashSet<ProtoId<AccessGroupPrototype>>();
|
|
}
|
|
|
|
foreach (var vessel in _prototypeManager.EnumeratePrototypes<VesselPrototype>())
|
|
{
|
|
bool hasAccess = initialHasAccess;
|
|
// If the vessel needs access to be bought, check the user's access.
|
|
if (!string.IsNullOrEmpty(vessel.Access))
|
|
{
|
|
hasAccess = false;
|
|
// Check tags
|
|
if (accesses.Tags.Contains(vessel.Access))
|
|
hasAccess = true;
|
|
|
|
// Check each group if we haven't found access already.
|
|
if (!hasAccess)
|
|
{
|
|
foreach (var groupId in accesses.Groups)
|
|
{
|
|
var groupProto = _prototypeManager.Index(groupId);
|
|
if (groupProto?.Tags.Contains(vessel.Access) ?? false)
|
|
{
|
|
hasAccess = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check that the listing contains the shuttle or that the shuttle is in the group that the console is looking for
|
|
if (listing?.Shuttles.Contains(vessel.ID) ?? false ||
|
|
key != null && key != ShipyardConsoleUiKey.Custom &&
|
|
vessel.Group == key)
|
|
{
|
|
if (hasAccess)
|
|
available.Add(vessel.ID);
|
|
else
|
|
unavailable.Add(vessel.ID);
|
|
}
|
|
}
|
|
|
|
return (available, unavailable);
|
|
}
|
|
|
|
private void RefreshState(EntityUid uid, int balance, bool access, string? shipDeed, int shipSellValue, EntityUid? targetId, ShipyardConsoleUiKey uiKey, bool freeListings)
|
|
{
|
|
var newState = new ShipyardConsoleInterfaceState(
|
|
balance,
|
|
access,
|
|
shipDeed,
|
|
shipSellValue,
|
|
targetId.HasValue,
|
|
((byte)uiKey),
|
|
GetAvailableShuttles(uid, uiKey, targetId: targetId),
|
|
uiKey.ToString(),
|
|
freeListings,
|
|
CalculateSellRate(uid));
|
|
|
|
_ui.SetUiState(uid, uiKey, newState);
|
|
}
|
|
|
|
#region Deed Assignment
|
|
void AssignShuttleDeedProperties(Entity<ShuttleDeedComponent> deed, EntityUid? shuttleUid, string? shuttleName, string? shuttleOwner, bool purchasedWithVoucher)
|
|
{
|
|
deed.Comp.ShuttleUid = shuttleUid;
|
|
TryParseShuttleName(deed.Comp, shuttleName!);
|
|
deed.Comp.ShuttleOwner = shuttleOwner;
|
|
deed.Comp.PurchasedWithVoucher = purchasedWithVoucher;
|
|
Dirty(deed);
|
|
}
|
|
|
|
private void OnInitDeedSpawner(EntityUid uid, StationDeedSpawnerComponent component, MapInitEvent args)
|
|
{
|
|
if (!HasComp<IdCardComponent>(uid)) // Test if the deed on an ID
|
|
return;
|
|
|
|
var xform = Transform(uid); // Get the grid the card is on
|
|
if (xform.GridUid == null)
|
|
return;
|
|
|
|
if (!TryComp<ShuttleDeedComponent>(xform.GridUid.Value, out var shuttleDeed) || !TryComp<ShuttleComponent>(xform.GridUid.Value, out var shuttle) || !HasComp<TransformComponent>(xform.GridUid.Value) || shuttle == null || ShipyardMap == null)
|
|
return;
|
|
|
|
var output = DeedRegex.Replace($"{shuttleDeed.ShuttleOwner}", ""); // Removes content inside parentheses along with parentheses and a preceding space
|
|
_idSystem.TryChangeFullName(uid, output); // Update the card with owner name
|
|
|
|
var deedID = EnsureComp<ShuttleDeedComponent>(uid);
|
|
AssignShuttleDeedProperties((uid, deedID), shuttleDeed.ShuttleUid, shuttleDeed.ShuttleName, shuttleDeed.ShuttleOwner, shuttleDeed.PurchasedWithVoucher);
|
|
}
|
|
#endregion
|
|
|
|
#region Ship Pricing
|
|
// Calculates the sell rate of a given shipyard console
|
|
private float CalculateSellRate(Entity<ShipyardConsoleComponent?> console)
|
|
{
|
|
if (!Resolve(console, ref console.Comp))
|
|
return 0.0f;
|
|
|
|
var taxRate = 0.0f;
|
|
foreach (var taxAccount in console.Comp.TaxAccounts)
|
|
{
|
|
taxRate += taxAccount.Value;
|
|
}
|
|
taxRate = 1.0f - taxRate; // Return the value minus the taxes
|
|
|
|
if (console.Comp.IgnoreBaseSaleRate)
|
|
return taxRate;
|
|
else
|
|
return _baseSaleRate * taxRate;
|
|
}
|
|
|
|
private int CalculateShipResaleValue(Entity<ShipyardConsoleComponent?> console, int baseAppraisal)
|
|
{
|
|
if (!Resolve(console, ref console.Comp))
|
|
return 0;
|
|
|
|
int resaleValue = baseAppraisal;
|
|
if (!console.Comp.IgnoreBaseSaleRate)
|
|
resaleValue = (int)(_baseSaleRate * resaleValue);
|
|
|
|
resaleValue -= CalculateTotalSalesTax(console.Comp, resaleValue);
|
|
return resaleValue;
|
|
}
|
|
|
|
// Calculates total sales tax over all accounts.
|
|
private int CalculateTotalSalesTax(ShipyardConsoleComponent component, int sellValue)
|
|
{
|
|
int salesTax = 0;
|
|
foreach (var (account, taxCoeff) in component.TaxAccounts)
|
|
salesTax += CalculateSalesTax(sellValue, taxCoeff);
|
|
return salesTax;
|
|
}
|
|
|
|
// Calculates sales tax for a particular account.
|
|
private int CalculateSalesTax(int sellValue, float taxRate)
|
|
{
|
|
if (float.IsFinite(taxRate) && taxRate > 0f)
|
|
return (int)(sellValue * taxRate);
|
|
return 0;
|
|
}
|
|
#endregion Ship Pricing
|
|
}
|