fish-station/Content.Client/_Sunrise/CartridgeLoader/Cartridges/PhotoCartridgeClientSystem.cs

237 lines
8.4 KiB
C#

using System.IO;
using Content.Shared.CartridgeLoader;
using System.Linq;
using System.Numerics;
using Content.Client.Viewport;
using Content.Shared.Mobs.Components;
using Robust.Client.Graphics;
using Robust.Client.State;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
public sealed class PhotoCartridgeClientSystem : EntitySystem
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
private TimeSpan _nextCaptureTime = TimeSpan.Zero;
public bool CameraReady => _timing.CurTime >= _nextCaptureTime;
public float CaptureDistance { get; set; } = 2.0f;
public float MinCaptureDistance { get; set; } = 1.0f;
private ISawmill _sawmill = default!;
private const int TargetPhotoWidth = 256;
private const int TargetPhotoHeight = 256;
public override void Initialize()
{
_sawmill = _logManager.GetSawmill("photo.cartridge.client");
}
public override void Shutdown()
{
}
public Vector2 GetCameraPosition(EntityUid source, float distance)
{
if (!_entityManager.TryGetComponent<TransformComponent>(source, out var xform))
return Vector2.Zero;
var sourcePos = _transformSystem.GetWorldPosition(source);
var lookRotation = _transformSystem.GetWorldRotation(source);
var ignoreEnt = source;
if (xform.ParentUid.IsValid() && !_entityManager.HasComponent<MapComponent>(xform.ParentUid) && !_entityManager.HasComponent<MapGridComponent>(xform.ParentUid))
{
lookRotation = _transformSystem.GetWorldRotation(xform.ParentUid);
ignoreEnt = xform.ParentUid;
}
var gridRot = Angle.Zero;
if (xform.GridUid != null)
gridRot = _transformSystem.GetWorldRotation(xform.GridUid.Value);
var relativeRot = lookRotation - gridRot;
var directionVec = gridRot.RotateVec(relativeRot.GetCardinalDir().ToVec());
var ray = new CollisionRay(sourcePos, directionVec, (int) Content.Shared.Physics.CollisionGroup.Opaque);
var results = _physics.IntersectRayWithPredicate(xform.MapID, ray, distance, uid =>
{
return uid == source ||
uid == ignoreEnt ||
_entityManager.HasComponent<MobStateComponent>(uid);
}, false).ToList();
if (results.Count > 0)
{
return sourcePos + directionVec * Math.Max(0.1f, results[0].Distance - 0.2f);
}
return sourcePos + directionVec * distance;
}
public void CaptureAndSendPhoto(EntityUid source)
{
if (TryComp<CartridgeComponent>(source, out var cart) && cart.LoaderUid.HasValue)
source = cart.LoaderUid.Value;
if (_timing.CurTime < _nextCaptureTime)
return;
_nextCaptureTime = _timing.CurTime + TimeSpan.FromSeconds(1.5);
Timer.Spawn(500, () => CaptureInternal(source));
}
public Vector2 GetClampedCapturePosition(EntityUid source, float distance)
{
var targetPos = GetCameraPosition(source, distance);
if (_stateManager.CurrentState is not IMainViewportState viewportState)
return targetPos;
var targetScreen = _eyeManager.WorldToScreen(targetPos);
var sourcePos = _transformSystem.GetWorldPosition(source);
var sourceScreen = _eyeManager.WorldToScreen(sourcePos);
var offsetScreen = _eyeManager.WorldToScreen(sourcePos + new Vector2(1, 0));
var pixelsPerMeter = (offsetScreen - sourceScreen).Length();
var size = 3.0f * pixelsPerMeter;
var control = (Control)viewportState.Viewport;
var vpRect = control.GlobalPixelRect;
size = Math.Min(size, Math.Min(vpRect.Width, vpRect.Height));
var halfSize = size / 2f;
var clampedX = Math.Clamp(targetScreen.X, vpRect.Left + halfSize, vpRect.Right - halfSize);
var clampedY = Math.Clamp(targetScreen.Y, vpRect.Top + halfSize, vpRect.Bottom - halfSize);
return _eyeManager.ScreenToMap(new Vector2(clampedX, clampedY)).Position;
}
private void CaptureInternal(EntityUid source)
{
if (!_netManager.IsConnected)
{
_sawmill.Warning("Cannot capture photo: client not connected to server");
return;
}
if (_playerManager.LocalEntity is not { })
return;
if (_stateManager.CurrentState is not IMainViewportState viewportState)
return;
var viewport = viewportState.Viewport.Viewport;
var sourcePos = _transformSystem.GetWorldPosition(source);
var targetPos = GetClampedCapturePosition(source, CaptureDistance);
var center = viewport.WorldToRenderTargetPixels(targetPos);
var p1 = viewport.WorldToRenderTargetPixels(sourcePos);
var p2 = viewport.WorldToRenderTargetPixels(sourcePos + new Vector2(1, 0));
var pixelSize = 3.0f * (p2 - p1).Length();
viewport.Screenshot(img => {
var size = (int)pixelSize;
size = Math.Min(size, Math.Min(img.Width, img.Height));
var left = (int)(center.X - size / 2f);
var top = (int)(center.Y - size / 2f);
left = Math.Clamp(left, 0, img.Width - size);
top = Math.Clamp(top, 0, img.Height - size);
if (size <= 0)
{
img.Dispose();
return;
}
var finalRect = new Rectangle(left, top, size, size);
TakeScreenshot(source, img, finalRect);
});
}
private void TakeScreenshot<T>(EntityUid source, Image<T> screenshot, Rectangle cropRect) where T : unmanaged, IPixel<T>
{
try
{
ProcessCapturedImage(source, screenshot, cropRect);
}
catch (Exception ex)
{
_sawmill.Error($"Error processing screenshot: {ex.Message}");
}
finally
{
screenshot.Dispose();
}
}
private void ProcessCapturedImage<T>(EntityUid source, Image<T> image, Rectangle cropRect) where T : unmanaged, IPixel<T>
{
if (cropRect.Width <= 0 || cropRect.Height <= 0)
return;
var rescaled = new Image<T>(TargetPhotoWidth, TargetPhotoHeight);
var rescaledSpan = rescaled.GetPixelSpan();
var sourceSpan = image.GetPixelSpan();
float scaleX = (float)cropRect.Width / TargetPhotoWidth;
float scaleY = (float)cropRect.Height / TargetPhotoHeight;
for (int y = 0; y < TargetPhotoHeight; y++)
{
for (int x = 0; x < TargetPhotoWidth; x++)
{
int srcX = cropRect.X + (int)(x * scaleX);
int srcY = cropRect.Y + (int)(y * scaleY);
if (srcX >= 0 && srcX < image.Width && srcY >= 0 && srcY < image.Height)
{
rescaledSpan[y * TargetPhotoWidth + x] = sourceSpan[srcY * image.Width + srcX];
}
}
}
var width = rescaled.Width;
var height = rescaled.Height;
byte[] imageData;
using (var memoryStream = new MemoryStream())
{
rescaled.SaveAsPng(memoryStream);
imageData = memoryStream.ToArray();
}
rescaled.Dispose();
SendPhotoToServer(source, imageData, width, height);
}
private void SendPhotoToServer(EntityUid source, byte[] imageData, int width, int height)
{
_netTexturesManager.SendPhotoToServer(_entityManager.GetNetEntity(source), imageData, width, height);
}
}