using Content.Server.Cargo.Components; using Content.Server.Salvage.Expeditions; using Content.Server.Salvage.Expeditions.Structure; using Content.Shared.CCVar; using Content.Shared.Examine; using Content.Shared.Salvage; using Robust.Shared.CPUJob.JobQueues; using Robust.Shared.CPUJob.JobQueues.Queues; using System.Linq; using System.Threading; using Content.Shared.Salvage.Expeditions; using Robust.Shared.GameStates; namespace Content.Server.Salvage; public sealed partial class SalvageSystem { /* * Handles setup / teardown of salvage expeditions. */ private const int MissionLimit = 3; private readonly JobQueue _salvageQueue = new(); private readonly List<(SpawnSalvageMissionJob Job, CancellationTokenSource CancelToken)> _salvageJobs = new(); private const double SalvageJobTime = 0.002; private float _cooldown; private float _failedCooldown; private void InitializeExpeditions() { SubscribeLocalEvent(OnSalvageConsoleInit); SubscribeLocalEvent(OnSalvageConsoleParent); SubscribeLocalEvent(OnSalvageClaimMessage); SubscribeLocalEvent(OnDataUnpaused); SubscribeLocalEvent(OnExpeditionShutdown); SubscribeLocalEvent(OnExpeditionUnpaused); SubscribeLocalEvent(OnExpeditionGetState); SubscribeLocalEvent(OnStructureExamine); _cooldown = _configurationManager.GetCVar(CCVars.SalvageExpeditionCooldown); _failedCooldown = _configurationManager.GetCVar(CCVars.SalvageExpeditionFailedCooldown); _configurationManager.OnValueChanged(CCVars.SalvageExpeditionCooldown, SetCooldownChange); _configurationManager.OnValueChanged(CCVars.SalvageExpeditionFailedCooldown, SetFailedCooldownChange); } private void OnExpeditionGetState(EntityUid uid, SalvageExpeditionComponent component, ref ComponentGetState args) { args.State = new SalvageExpeditionComponentState() { Stage = component.Stage }; } private void ShutdownExpeditions() { _configurationManager.UnsubValueChanged(CCVars.SalvageExpeditionCooldown, SetCooldownChange); _configurationManager.UnsubValueChanged(CCVars.SalvageExpeditionFailedCooldown, SetFailedCooldownChange); } private void SetCooldownChange(float obj) { // Update the active cooldowns if we change it. var diff = obj - _cooldown; var query = AllEntityQuery(); while (query.MoveNext(out var comp)) { comp.NextOffer += TimeSpan.FromSeconds(diff); } _cooldown = obj; } private void SetFailedCooldownChange(float obj) { var diff = obj - _failedCooldown; var query = AllEntityQuery(); while (query.MoveNext(out var comp)) { comp.NextOffer += TimeSpan.FromSeconds(diff); } _failedCooldown = obj; } private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent component, ComponentShutdown args) { component.Stream?.Stop(); foreach (var (job, cancelToken) in _salvageJobs.ToArray()) { if (job.Station == component.Station) { cancelToken.Cancel(); _salvageJobs.Remove((job, cancelToken)); } } if (Deleted(component.Station)) return; // Finish mission if (TryComp(component.Station, out var data)) { FinishExpedition(data, uid, component, null); } } private void OnDataUnpaused(EntityUid uid, SalvageExpeditionDataComponent component, ref EntityUnpausedEvent args) { component.NextOffer += args.PausedTime; } private void OnExpeditionUnpaused(EntityUid uid, SalvageExpeditionComponent component, ref EntityUnpausedEvent args) { component.EndTime += args.PausedTime; } private void UpdateExpeditions() { var currentTime = _timing.CurTime; _salvageQueue.Process(); foreach (var (job, cancelToken) in _salvageJobs.ToArray()) { switch (job.Status) { case JobStatus.Finished: _salvageJobs.Remove((job, cancelToken)); break; } } foreach (var comp in EntityQuery()) { // Update offers if (comp.NextOffer > currentTime || comp.Claimed) continue; comp.Cooldown = false; comp.NextOffer += TimeSpan.FromSeconds(_cooldown); GenerateMissions(comp); UpdateConsoles(comp); } } private void FinishExpedition(SalvageExpeditionDataComponent component, EntityUid uid, SalvageExpeditionComponent expedition, EntityUid? shuttle) { // Finish mission cleanup. switch (expedition.MissionParams.MissionType) { // Handles the mining taxation. case SalvageMissionType.Mining: expedition.Completed = true; if (shuttle != null && TryComp(uid, out var mining)) { var xformQuery = GetEntityQuery(); var entities = new List(); MiningTax(entities, shuttle.Value, mining, xformQuery); var tax = GetMiningTax(expedition.MissionParams.Difficulty); _random.Shuffle(entities); // TODO: urgh this pr is already taking so long I'll do this later for (var i = 0; i < Math.Ceiling(entities.Count * tax); i++) { // QueueDel(entities[i]); } } break; } // Handle payout after expedition has finished if (expedition.Completed) { Log.Debug($"Completed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}"); component.NextOffer = _timing.CurTime + TimeSpan.FromSeconds(_cooldown); Announce(uid, Loc.GetString("salvage-expedition-mission-completed")); GiveRewards(expedition); } else { Log.Debug($"Failed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}"); component.NextOffer = _timing.CurTime + TimeSpan.FromSeconds(_failedCooldown); Announce(uid, Loc.GetString("salvage-expedition-mission-failed")); } component.ActiveMission = 0; component.Cooldown = true; UpdateConsoles(component); } /// /// Deducts ore tax for mining. /// private void MiningTax(List entities, EntityUid entity, SalvageMiningExpeditionComponent mining, EntityQuery xformQuery) { if (!mining.ExemptEntities.Contains(entity)) { entities.Add(entity); } var xform = xformQuery.GetComponent(entity); var children = xform.ChildEnumerator; while (children.MoveNext(out var child)) { MiningTax(entities, child.Value, mining, xformQuery); } } private void GenerateMissions(SalvageExpeditionDataComponent component) { component.Missions.Clear(); var configs = Enum.GetValues().ToList(); // Temporarily removed coz it SUCKS configs.Remove(SalvageMissionType.Mining); // this doesn't support having more missions than types of ratings // but the previous system didn't do that either. var allDifficulties = Enum.GetValues(); _random.Shuffle(allDifficulties); var difficulties = allDifficulties.Take(MissionLimit).ToList(); difficulties.Sort(); if (configs.Count == 0) return; for (var i = 0; i < MissionLimit; i++) { _random.Shuffle(configs); var rating = difficulties[i]; foreach (var config in configs) { var mission = new SalvageMissionParams { Index = component.NextIndex, MissionType = config, Seed = _random.Next(), Difficulty = rating, }; component.Missions[component.NextIndex++] = mission; break; } } } private SalvageExpeditionConsoleState GetState(SalvageExpeditionDataComponent component) { var missions = component.Missions.Values.ToList(); return new SalvageExpeditionConsoleState(component.NextOffer, component.Claimed, component.Cooldown, component.ActiveMission, missions); } private void SpawnMission(SalvageMissionParams missionParams, EntityUid station) { var cancelToken = new CancellationTokenSource(); var job = new SpawnSalvageMissionJob( SalvageJobTime, EntityManager, _timing, _mapManager, _prototypeManager, _anchorable, _biome, _dungeon, this, station, missionParams, cancelToken.Token); _salvageJobs.Add((job, cancelToken)); _salvageQueue.EnqueueJob(job); } private void OnStructureExamine(EntityUid uid, SalvageStructureComponent component, ExaminedEvent args) { args.PushMarkup(Loc.GetString("salvage-expedition-structure-examine")); } private void GiveRewards(SalvageExpeditionComponent comp) { // send it to cargo, no rewards otherwise. if (!TryComp(comp.Station, out var cargoDb)) return; foreach (var reward in comp.Rewards) { var sender = Loc.GetString("cargo-gift-default-sender"); var desc = Loc.GetString("salvage-expedition-reward-description"); var dest = Loc.GetString("cargo-gift-default-dest"); _cargo.AddAndApproveOrder(comp.Station, reward, 0, 1, sender, desc, dest, cargoDb); } } }