using Content.Server.Audio; using Content.Server.Fluids.EntitySystems; using Content.Server.Materials; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.FixedPoint; using Content.Shared.Popups; using Content.Shared.Power.Generator; using Robust.Server.GameObjects; namespace Content.Server.Power.Generator; /// /// /// /// public sealed class GeneratorSystem : SharedGeneratorSystem { [Dependency] private readonly AppearanceSystem _appearance = default!; [Dependency] private readonly AmbientSoundSystem _ambientSound = default!; [Dependency] private readonly MaterialStorageSystem _materialStorage = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PuddleSystem _puddle = default!; private EntityQuery _upgradeQuery; public override void Initialize() { _upgradeQuery = GetEntityQuery(); UpdatesBefore.Add(typeof(PowerNetSystem)); SubscribeLocalEvent(OnTargetPowerSet); SubscribeLocalEvent(OnEjectFuel); SubscribeLocalEvent(OnAnchorStateChanged); SubscribeLocalEvent(SolidGetFuel); SubscribeLocalEvent(SolidUseFuel); SubscribeLocalEvent(SolidEmpty); SubscribeLocalEvent(ChemicalGetFuel); SubscribeLocalEvent(ChemicalUseFuel); SubscribeLocalEvent(ChemicalGetClogged); SubscribeLocalEvent(ChemicalEmpty); } private void OnAnchorStateChanged(EntityUid uid, FuelGeneratorComponent component, ref AnchorStateChangedEvent args) { // Turn off generator if unanchored while running. if (!component.On) return; SetFuelGeneratorOn(uid, false, component); } private void OnEjectFuel(EntityUid uid, FuelGeneratorComponent component, PortableGeneratorEjectFuelMessage args) { EmptyGenerator(uid); } private void SolidEmpty(EntityUid uid, SolidFuelGeneratorAdapterComponent component, GeneratorEmpty args) { _materialStorage.EjectAllMaterial(uid); } private void ChemicalEmpty(EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, GeneratorEmpty args) { if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution)) return; var spillSolution = _solutionContainer.SplitSolution(uid, solution, solution.Volume); _puddle.TrySpillAt(uid, spillSolution, out _); } private void ChemicalGetClogged(EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, ref GeneratorGetCloggedEvent args) { if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution)) return; foreach (var reagentQuantity in solution) { if (reagentQuantity.Reagent.Prototype != component.Reagent) { args.Clogged = true; return; } } } private void ChemicalUseFuel(EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, GeneratorUseFuel args) { if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution)) return; var availableReagent = solution.GetTotalPrototypeQuantity(component.Reagent).Value; var toRemove = RemoveFractionalFuel( ref component.FractionalReagent, args.FuelUsed, component.Multiplier * FixedPoint2.Epsilon.Float(), availableReagent); solution.RemoveReagent(component.Reagent, FixedPoint2.FromCents(toRemove)); } private void ChemicalGetFuel( EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, ref GeneratorGetFuelEvent args) { if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution)) return; var availableReagent = solution.GetTotalPrototypeQuantity(component.Reagent).Float(); var reagent = component.FractionalReagent * FixedPoint2.Epsilon.Float() + availableReagent; args.Fuel = reagent * component.Multiplier; } private void SolidUseFuel(EntityUid uid, SolidFuelGeneratorAdapterComponent component, GeneratorUseFuel args) { var availableMaterial = _materialStorage.GetMaterialAmount(uid, component.FuelMaterial); var toRemove = RemoveFractionalFuel( ref component.FractionalMaterial, args.FuelUsed, component.Multiplier, availableMaterial); _materialStorage.TryChangeMaterialAmount(uid, component.FuelMaterial, -toRemove); } private int RemoveFractionalFuel(ref float fractional, float fuelUsed, float multiplier, int availableQuantity) { fractional -= fuelUsed / multiplier; if (fractional >= 0) return 0; // worst (unrealistic) case: -5.5 -> -6.0 -> 6 var toRemove = -(int) MathF.Floor(fractional); toRemove = Math.Min(availableQuantity, toRemove); fractional = Math.Max(0, fractional + toRemove); return toRemove; } private void SolidGetFuel( EntityUid uid, SolidFuelGeneratorAdapterComponent component, ref GeneratorGetFuelEvent args) { var material = component.FractionalMaterial + _materialStorage.GetMaterialAmount(uid, component.FuelMaterial); args.Fuel = material * component.Multiplier; } private void OnTargetPowerSet(EntityUid uid, FuelGeneratorComponent component, PortableGeneratorSetTargetPowerMessage args) { component.TargetPower = Math.Clamp( args.TargetPower, component.MinTargetPower / 1000, component.MaxTargetPower / 1000) * 1000; } public void SetFuelGeneratorOn(EntityUid uid, bool on, FuelGeneratorComponent? generator = null) { if (!Resolve(uid, ref generator)) return; if (on && !Transform(uid).Anchored) { // Generator must be anchored to start. return; } generator.On = on; UpdateState(uid, generator); } public override void Update(float frameTime) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var gen, out var supplier)) { if (!gen.On) continue; var fuel = GetFuel(uid); if (fuel <= 0) { SetFuelGeneratorOn(uid, false, gen); continue; } if (GetIsClogged(uid)) { _popup.PopupEntity(Loc.GetString("generator-clogged", ("generator", uid)), uid, PopupType.SmallCaution); SetFuelGeneratorOn(uid, false, gen); continue; } supplier.Enabled = true; var upgradeMultiplier = _upgradeQuery.CompOrNull(uid)?.ActualScalar ?? 1f; supplier.MaxSupply = gen.TargetPower * upgradeMultiplier; var eff = 1 / CalcFuelEfficiency(gen.TargetPower, gen.OptimalPower, gen); var consumption = gen.OptimalBurnRate * frameTime * eff; RaiseLocalEvent(uid, new GeneratorUseFuel(consumption)); } } public float GetFuel(EntityUid generator) { GeneratorGetFuelEvent getFuelEvent = default; RaiseLocalEvent(generator, ref getFuelEvent); return getFuelEvent.Fuel; } public bool GetIsClogged(EntityUid generator) { GeneratorGetCloggedEvent getCloggedEvent = default; RaiseLocalEvent(generator, ref getCloggedEvent); return getCloggedEvent.Clogged; } public void EmptyGenerator(EntityUid generator) { RaiseLocalEvent(generator, GeneratorEmpty.Instance); } private void UpdateState(EntityUid generator, FuelGeneratorComponent component) { _appearance.SetData(generator, GeneratorVisuals.Running, component.On); _ambientSound.SetAmbience(generator, component.On); if (!component.On) Comp(generator).Enabled = false; } } /// /// Raised by to calculate the amount of remaining fuel in the generator. /// [ByRefEvent] public record struct GeneratorGetFuelEvent(float Fuel); /// /// Raised by to check if a generator is "clogged". /// For example there's bad chemicals in the fuel tank that prevent starting it. /// [ByRefEvent] public record struct GeneratorGetCloggedEvent(bool Clogged); /// /// Raised by to draw fuel from its adapters. /// /// /// Implementations are expected to round fuel consumption up if the used fuel value is too small (e.g. reagent units). /// public record struct GeneratorUseFuel(float FuelUsed); /// /// Raised by to empty a generator of its fuel contents. /// public sealed class GeneratorEmpty { public static readonly GeneratorEmpty Instance = new(); }