using System.Diagnostics.CodeAnalysis;
using System.Linq;
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.Cargo.Prototypes;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.Paper;
using Content.Shared.Radio;
using Content.Shared.Salvage.JobBoard;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Salvage.JobBoard;
public sealed class SalvageJobBoardSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly CargoSystem _cargo = default!;
[Dependency] private readonly LabelSystem _label = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
///
/// Radio channel that unlock messages are broadcast on.
///
private static readonly ProtoId UnlockChannel = "Supply";
///
public override void Initialize()
{
SubscribeLocalEvent(OnSold);
SubscribeLocalEvent(OnBUIOpened);
Subs.BuiEvents(SalvageJobBoardUiKey.Key,
subs =>
{
subs.Event(OnPrintLabelMessage);
});
}
private void OnSold(ref EntitySoldEvent args)
{
if (!TryComp(args.Station, out var salvageJobsData))
return;
foreach (var sold in args.Sold)
{
if (!FulfillsSalvageJob(sold, (args.Station, salvageJobsData), out var jobId))
continue;
TryCompleteSalvageJob((args.Station, salvageJobsData), jobId.Value);
}
}
///
/// Gets the jobs that the station can currently access.
///
public List> GetAvailableJobs(Entity ent)
{
var outJobs = new List>();
var availableGroups = new HashSet>();
var completedCount = ent.Comp.CompletedJobs.Count;
foreach (var (thresholds, rank) in ent.Comp.RankThresholds)
{
if (completedCount < thresholds)
continue;
if (rank.BountyGroup == null)
continue;
availableGroups.Add(rank.BountyGroup.Value);
}
foreach (var bounty in _prototypeManager.EnumeratePrototypes())
{
if (ent.Comp.CompletedJobs.Contains(bounty))
continue;
if (availableGroups.Contains(bounty.Group))
outJobs.Add(bounty);
}
return outJobs;
}
///
/// Gets the "progression" of a rank, expressed as on the range [0, 1]
///
public float GetRankProgression(Entity ent)
{
// Need to have at least two of these.
if (ent.Comp.RankThresholds.Count <= 1)
return 1;
var completedCount = ent.Comp.CompletedJobs.Count;
for (var i = ent.Comp.RankThresholds.Count - 1; i >= 0; i--)
{
var low = ent.Comp.RankThresholds.Keys.ElementAt(i);
if (completedCount < low)
continue;
// don't worry abooouuuuut it (it'll be O K !)
var high = i != ent.Comp.RankThresholds.Count - 1
? ent.Comp.RankThresholds.Keys.ElementAt(i + 1)
: _prototypeManager.EnumeratePrototypes()
.Count(p => ent.Comp.RankThresholds.Values
.Select(r => r.BountyGroup)
.Contains(p.Group));
return (completedCount - low) / (float)(high - low);
}
return 1f;
}
///
/// Checks if the current station is the max rank
///
public bool IsMaxRank(Entity ent)
{
return GetAvailableJobs(ent).Count == 0;
}
///
/// Gets the current rank of the station
///
public SalvageRankDatum GetRank(Entity ent)
{
if (IsMaxRank(ent))
return ent.Comp.MaxRank;
var completedCount = ent.Comp.CompletedJobs.Count;
foreach (var (threshold, rank) in ent.Comp.RankThresholds.Reverse())
{
if (completedCount < threshold)
continue;
return rank;
}
// base case
return ent.Comp.RankThresholds[0];
}
///
///
///
///
///
///
public bool TryCompleteSalvageJob(Entity ent, ProtoId job)
{
if (!GetAvailableJobs(ent).Contains(job))
return false;
var jobProto = _prototypeManager.Index(job);
var oldRank = GetRank(ent);
ent.Comp.CompletedJobs.Add(job);
var newRank = GetRank(ent);
// Add reward
if (TryComp(ent, out var stationBankAccount))
{
_cargo.UpdateBankAccount(
(ent.Owner, stationBankAccount),
jobProto.Reward,
_cargo.CreateAccountDistribution((ent, stationBankAccount)));
}
// We ranked up!
if (oldRank != newRank)
{
// We need to find a computer to send the message from.
var computerQuery = EntityQueryEnumerator();
while (computerQuery.MoveNext(out var uid, out _))
{
var message = Loc.GetString("job-board-radio-announce", ("rank", FormattedMessage.RemoveMarkupPermissive(Loc.GetString(newRank.Title))));
_radio.SendRadioMessage(uid, message, UnlockChannel, uid, false);
break;
}
if (newRank.UnlockedMarket is { } market &&
TryComp(ent, out var stationCargoOrder))
{
stationCargoOrder.Markets.Add(market);
}
}
var enumerator = EntityQueryEnumerator();
while (enumerator.MoveNext(out var consoleUid, out var console))
{
UpdateUi((consoleUid, console), ent);
}
return true;
}
///
/// Checks if a given entity fulfills a bounty for the station.
///
public bool FulfillsSalvageJob(EntityUid uid, Entity? station, [NotNullWhen(true)] out ProtoId? job)
{
job = null;
if (!_label.TryGetLabel(uid, out var labelEnt))
return false;
if (labelEnt.Value.Comp.JobId is not { } jobId)
return false;
job = jobId;
if (station is null)
{
if (_station.GetOwningStation(uid) is not { } stationUid ||
!TryComp(stationUid, out var stationComp))
return false;
station = (stationUid, stationComp);
}
if (!GetAvailableJobs((station.Value, station.Value.Comp)).Contains(job.Value))
return false;
if (!_cargo.IsBountyComplete(uid, job))
return false;
return true;
}
private void OnBUIOpened(Entity ent, ref BoundUIOpenedEvent args)
{
if (args.UiKey is not SalvageJobBoardUiKey.Key)
return;
if (_station.GetOwningStation(ent.Owner) is not { } station ||
!TryComp(station, out var jobData))
return;
UpdateUi(ent, (station, jobData));
}
private void OnPrintLabelMessage(Entity ent, ref JobBoardPrintLabelMessage args)
{
if (_timing.CurTime < ent.Comp.NextPrintTime)
return;
if (_station.GetOwningStation(ent) is not { } station ||
!TryComp(station, out var jobsData))
return;
if (!_prototypeManager.TryIndex(args.JobId, out var job))
return;
if (!GetAvailableJobs((station, jobsData)).Contains(args.JobId))
return;
_audio.PlayPvs(ent.Comp.PrintSound, ent);
var label = SpawnAtPosition(ent.Comp.LabelEntity, Transform(ent).Coordinates);
EnsureComp(label).JobId = job.ID;
var target = new List();
foreach (var entry in job.Entries)
{
target.Add(Loc.GetString("bounty-console-manifest-entry",
("amount", entry.Amount),
("item", Loc.GetString(entry.Name))));
}
_paper.SetContent(label, Loc.GetString("job-board-label-text", ("target", string.Join(',', target)), ("reward", job.Reward)));
ent.Comp.NextPrintTime = _timing.CurTime + ent.Comp.PrintDelay;
}
private void UpdateUi(Entity ent, Entity stationEnt)
{
var state = new SalvageJobBoardConsoleState(
GetRank(stationEnt).Title,
GetRankProgression(stationEnt),
GetAvailableJobs(stationEnt));
_ui.SetUiState(ent.Owner, SalvageJobBoardUiKey.Key, state);
}
}