6
2026-01-24 12:49:55 +03:00

207 lines
8.2 KiB
C#

using System.Linq;
using Content.Server.Power.Components;
using Content.Server._NF.Solar.Components;
using Content.Shared.GameTicking;
using Content.Shared.Physics;
using JetBrains.Annotations;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server._NF.Solar.EntitySystems;
/// <summary>
/// Responsible for maintaining the solar-panel sun angle and updating <see cref='NFSolarPanelComponent'/> coverage.
/// Keeps track of per-grid solar panel angle and velocity using <see cref='SolarPoweredGridComponent'/>.
/// Largely based on upstream's PowerSolarSystem (with many thanks to 20kdc, DrSmugleaf and others)
/// </summary>
[UsedImplicitly]
internal sealed class NFPowerSolarSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; // Frontier
/// <summary>
/// Maximum panel angular velocity range - used to stop people rotating panels fast enough that the lag prevention becomes noticable
/// </summary>
public const float MaxPanelVelocityDegrees = 1f;
/// <summary>
/// The current sun angle.
/// </summary>
public Angle TowardsSun = Angle.Zero;
/// <summary>
/// The current sun angular velocity. (This is changed in Initialize)
/// </summary>
public Angle SunAngularVelocity = Angle.Zero;
/// <summary>
/// The distance before the sun is considered to have been 'visible anyway'.
/// This value, like the occlusion semantics, is borrowed from all the other SS13 stations with solars.
/// </summary>
public float SunOcclusionCheckDistance = 20;
/// <summary>
/// Queue of panels to update each cycle.
/// </summary>
private readonly Queue<Entity<NFSolarPanelComponent>> _updateQueue = new();
public override void Initialize()
{
SubscribeLocalEvent<NFSolarPanelComponent, MapInitEvent>(OnPanelMapInit);
SubscribeLocalEvent<SolarPoweredGridComponent, MapInitEvent>(OnSolarPoweredGridMapInit);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
RandomizeSun();
}
public void Reset(RoundRestartCleanupEvent ev)
{
RandomizeSun();
}
private void RandomizeSun()
{
// Initialize the sun to something random
TowardsSun = MathHelper.TwoPi * _robustRandom.NextDouble();
SunAngularVelocity = Angle.FromDegrees(0.125 + (_robustRandom.NextDouble() - 0.5) * 0.1); // 0.075/s - 0.175/s (4800s - ~2000s per orbit)
if (_robustRandom.Prob(0.5f))
SunAngularVelocity = -SunAngularVelocity; // retrograde rotation(?)
}
private void OnPanelMapInit(EntityUid uid, NFSolarPanelComponent component, MapInitEvent args)
{
UpdateSupply(uid, component);
}
private void OnSolarPoweredGridMapInit(EntityUid uid, SolarPoweredGridComponent component, MapInitEvent args)
{
if (component.TrackOnInit)
{
component.TargetPanelRotation = TowardsSun;
component.TargetPanelVelocity = SunAngularVelocity;
}
}
public override void Update(float frameTime)
{
TowardsSun += SunAngularVelocity * frameTime;
TowardsSun = TowardsSun.Reduced();
if (_updateQueue.Count > 0)
{
UpdateSolarGridRotations(false, frameTime); // Frontier
var panel = _updateQueue.Dequeue();
if (panel.Comp.Running)
UpdatePanelCoverage(panel);
}
else
{
UpdateSolarGridRotations(true, frameTime); // Frontier
var query = EntityQueryEnumerator<NFSolarPanelComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var panel, out var xform))
{
if (xform.GridUid == null)
continue;
var poweredGridComp = EnsureComp<SolarPoweredGridComponent>(xform.GridUid.Value);
poweredGridComp.TotalPanelPower += panel.MaxSupply * panel.Coverage;
poweredGridComp.LastUpdatedTick = _gameTiming.CurTick.Value;
_transformSystem.SetWorldRotation(xform, poweredGridComp.TargetPanelRotation);
_updateQueue.Enqueue((uid, panel));
}
// Cull grid set
var gridQuery = EntityQueryEnumerator<SolarPoweredGridComponent>();
while (gridQuery.MoveNext(out var uid, out var gridPower))
{
if (!gridPower.DoNotCull &&
gridPower.LastUpdatedTick != _gameTiming.CurTick.Value)
{
RemCompDeferred<SolarPoweredGridComponent>(uid);
}
}
}
}
// Adjusts all grid rotations at their current tracking velocity and optionally resets their total power.
private void UpdateSolarGridRotations(bool resetPower, float dt)
{
var gridQuery = EntityQueryEnumerator<SolarPoweredGridComponent>();
while (gridQuery.MoveNext(out _, out var grid))
{
if (resetPower)
grid.TotalPanelPower = 0;
grid.TargetPanelRotation += grid.TargetPanelVelocity * dt;
grid.TargetPanelRotation = grid.TargetPanelRotation.Reduced();
}
}
// Currently verbatim from PowerSolarSystem.UpdatePanelCoverage
private void UpdatePanelCoverage(Entity<NFSolarPanelComponent> panel)
{
var entity = panel.Owner;
var xform = EntityManager.GetComponent<TransformComponent>(entity);
// So apparently, and yes, I *did* only find this out later,
// this is just a really fancy way of saying "Lambert's law of cosines".
// ...I still think this explaination makes more sense.
// In the 'sunRelative' coordinate system:
// the sun is considered to be an infinite distance directly up.
// this is the rotation of the panel relative to that.
// directly upwards (theta = 0) = coverage 1
// left/right 90 degrees (abs(theta) = (pi / 2)) = coverage 0
// directly downwards (abs(theta) = pi) = coverage -1
// as TowardsSun + = CCW,
// panelRelativeToSun should - = CW
var panelRelativeToSun = _transformSystem.GetWorldRotation(xform) - TowardsSun;
// essentially, given cos = X & sin = Y & Y is 'downwards',
// then for the first 90 degrees of rotation in either direction,
// this plots the lower-right quadrant of a circle.
// now basically assume a line going from the negated X/Y to there,
// and that's the hypothetical solar panel.
//
// since, again, the sun is considered to be an infinite distance upwards,
// this essentially means Cos(panelRelativeToSun) is half of the cross-section,
// and since the full cross-section has a max of 2, effectively-halving it is fine.
//
// as for when it goes negative, it only does that when (abs(theta) > pi)
// and that's expected behavior.
float coverage = (float)Math.Max(0, Math.Cos(panelRelativeToSun));
if (coverage > 0)
{
// Determine if the solar panel is occluded, and zero out coverage if so.
var ray = new CollisionRay(_transformSystem.GetWorldPosition(xform), TowardsSun.ToWorldVec(), (int)CollisionGroup.Opaque);
var rayCastResults = _physicsSystem.IntersectRayWithPredicate(
xform.MapID,
ray,
SunOcclusionCheckDistance,
e => !xform.Anchored || e == entity);
if (rayCastResults.Any())
coverage = 0;
}
// Total coverage calculated; apply it to the panel.
panel.Comp.Coverage = coverage;
UpdateSupply(panel, panel);
}
public void UpdateSupply(
EntityUid uid,
NFSolarPanelComponent? solar = null,
PowerSupplierComponent? supplier = null)
{
if (!Resolve(uid, ref solar, ref supplier, false))
return;
supplier.MaxSupply = (int)(solar.MaxSupply * solar.Coverage);
}
}