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); } }