CatStation/Content.Client/Corvax/ExportSprites/EntityScreenshotGenerator.cs
2026-10-05 20:58:26 +07:00

381 lines
12 KiB
C#

using System.Linq;
using System.Threading.Tasks;
using Content.Client.Gameplay;
using Content.Shared.CCVar;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Corvax.GuideGenerator;
using Content.Shared.Prototypes;
using Robust.Client;
using Robust.Client.GameObjects;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Utility;
namespace Content.Client.Corvax.ExportSprites;
public sealed partial class EntityScreenshotGenerator
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IBaseClient _baseClient = default!;
[Dependency] private IEntityManager _entityManager = default!;
[Dependency] private IEntitySystemManager _entitySystemManager = default!;
[Dependency] private EntityScreenshotRenderService _renderService = default!;
[Dependency] private IGameController _gameController = default!;
[Dependency] private IClientGameTiming _gameTiming = default!;
[Dependency] private ILogManager _logManager = default!;
[Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private IResourceManager _resourceManager = default!;
[Dependency] private ISerializationManager _serialization = default!;
[Dependency] private IStateManager _stateManager = default!;
private ISawmill _sawmill = default!;
private bool _started;
private bool _startupRequested;
private bool _pendingStart;
private const uint WarmupFrames = 3;
public void Initialize()
{
IoCManager.InjectDependencies(this);
_sawmill = _logManager.GetSawmill("entity-screenshot-generator");
_renderService.Initialize();
}
public bool PostInit()
{
if (!_cfg.GetCVar(CCVars.EntityScreenshotGeneratorEnabled))
return false;
if (_baseClient.RunLevel == ClientRunLevel.SinglePlayerGame)
{
_pendingStart = true;
return true;
}
if (_startupRequested)
return true;
_startupRequested = true;
_baseClient.StartSinglePlayer();
_stateManager.RequestStateChange<GameplayState>();
_pendingStart = true;
return true;
}
public void Update()
{
if (!_pendingStart || _started)
return;
if (_baseClient.RunLevel != ClientRunLevel.SinglePlayerGame)
return;
TryStart();
}
public bool TryStart()
{
if (_started || !_cfg.GetCVar(CCVars.EntityScreenshotGeneratorEnabled))
return _started;
try
{
_entitySystemManager.GetEntitySystem<SharedMapSystem>();
}
catch
{
return false;
}
try
{
_ = RunAsync();
}
catch (Exception)
{
return false;
}
_started = true;
_pendingStart = false;
return true;
}
private async Task RunAsync()
{
var outputDir = new ResPath(_cfg.GetCVar(CCVars.EntityScreenshotOutputPath));
var wasPaused = _gameTiming.Paused;
try
{
_gameTiming.Paused = true;
var failures = new List<string>();
var exported = 0;
var mapSystem = _entitySystemManager.GetEntitySystem<SharedMapSystem>();
var allowedIds = EntityProjectHelper.GetProjectEntityIds();
var prototypes = _prototypeManager.EnumeratePrototypes<EntityPrototype>()
.Where(proto =>
!proto.Abstract &&
HasExportableSprite(proto) &&
EntityProjectHelper.MatchesAllowedIds(proto.ID, allowedIds))
.OrderBy(proto => proto.ID)
.ToList();
var previewMap = mapSystem.CreateMap(out var mapId);
var previewGrid = mapSystem.CreateGridEntity(mapId);
if (!_resourceManager.UserData.IsDir(outputDir))
_resourceManager.UserData.CreateDir(outputDir);
foreach (var proto in prototypes)
{
EntityUid entity = default;
try
{
if (proto.HasComp<SpriteComponent>(_entityManager.ComponentFactory))
{
entity = _entityManager.SpawnEntity(proto.ID, new EntityCoordinates(previewGrid.Owner, default));
await WaitForEntityAppearanceAsync(entity);
ApplyPrototypeAppearance(entity, proto);
await WaitForEntityAppearanceAsync(entity, 1);
}
else
{
entity = SpawnIconEntity(proto);
}
await _renderService.Export(entity, Direction.South, outputDir / $"{proto.ID}.png");
exported++;
}
catch (Exception e)
{
failures.Add($"{proto.ID}: {e.Message}");
_sawmill.Error($"Failed to export {proto.ID}: {e}");
}
finally
{
if (_entityManager.EntityExists(entity))
_entityManager.DeleteEntity(entity);
}
}
if (failures.Count > 0)
WriteFailures(outputDir, failures);
if (_entityManager.EntityExists(previewGrid))
_entityManager.DeleteEntity(previewGrid);
if (_entityManager.EntityExists(previewMap))
_entityManager.DeleteEntity(previewMap);
_gameController.Shutdown($"Entity screenshot generation complete. Exported {exported}/{prototypes.Count}");
}
catch (Exception e)
{
_sawmill.Error($"Entity screenshot generation crashed: {e}");
WriteFailures(outputDir, new[] { e.ToString() });
_gameController.Shutdown("Entity screenshot generation failed");
}
finally
{
_gameTiming.Paused = wasPaused;
}
}
private void WriteFailures(ResPath outputDir, IEnumerable<string> failures)
{
if (!_resourceManager.UserData.IsDir(outputDir))
_resourceManager.UserData.CreateDir(outputDir);
using var writer = _resourceManager.UserData.OpenWriteText(outputDir / "failures.txt");
foreach (var failure in failures)
{
writer.WriteLine(failure);
}
writer.Flush();
}
private async Task WaitForEntityAppearanceAsync(EntityUid entity)
{
await WaitForEntityAppearanceAsync(entity, WarmupFrames);
}
private async Task WaitForEntityAppearanceAsync(EntityUid entity, uint frames)
{
if (!_entityManager.TryGetComponent(entity, out MetaDataComponent? metadata))
return;
if (!metadata.EntityInitialized)
_entityManager.InitializeAndStartEntity((entity, metadata), doMapInit: true);
var targetFrame = _gameTiming.CurFrame + frames;
while (_entityManager.EntityExists(entity) && _gameTiming.CurFrame < targetFrame)
{
await Task.Delay(1);
}
}
private void ApplyPrototypeAppearance(EntityUid entity, EntityPrototype prototype)
{
if (!_entityManager.TryGetComponent(entity, out AppearanceComponent? appearance))
return;
if (!prototype.TryGetComponent<SolutionContainerManagerComponent>(out var manager, _entityManager.ComponentFactory) ||
manager.Solutions == null ||
manager.Solutions.Count == 0)
{
return;
}
var solutionEntry = manager.Solutions.FirstOrDefault(entry => entry.Value.Volume > 0);
if (string.IsNullOrEmpty(solutionEntry.Key))
solutionEntry = manager.Solutions.First();
var solution = solutionEntry.Value;
var appearanceSystem = _entitySystemManager.GetEntitySystem<SharedAppearanceSystem>();
appearanceSystem.SetData(entity, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearance);
appearanceSystem.SetData(entity, SolutionContainerVisuals.Color, solution.GetColor(_prototypeManager), appearance);
appearanceSystem.SetData(entity, SolutionContainerVisuals.SolutionName, solutionEntry.Key, appearance);
if (solution.GetPrimaryReagentId() is { } reagent)
appearanceSystem.SetData(entity, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearance);
}
private bool HasExportableSprite(EntityPrototype prototype)
{
if (prototype.HasComp<SpriteComponent>(_entityManager.ComponentFactory))
return true;
return TryGetPrototypeIcon(prototype, out _);
}
private EntityUid SpawnIconEntity(EntityPrototype prototype)
{
if (!TryGetPrototypeIcon(prototype, out var icon) || icon == null)
throw new InvalidOperationException($"Prototype {prototype.ID} has no exportable icon.");
var entity = _entityManager.SpawnEntity(null, MapCoordinates.Nullspace);
var sprite = _entityManager.EnsureComponent<SpriteComponent>(entity);
var spriteSystem = _entitySystemManager.GetEntitySystem<SpriteSystem>();
spriteSystem.AddBlankLayer((entity, sprite), 0);
if (icon is SpriteSpecifier.EntityPrototype entityIcon)
spriteSystem.LayerSetTexture((entity, sprite), 0, spriteSystem.Frame0(new SpriteSpecifier.EntityPrototype(entityIcon.EntityPrototypeId)));
else
spriteSystem.LayerSetSprite((entity, sprite), 0, icon);
sprite.LayerSetShader(0, "unshaded");
spriteSystem.LayerSetVisible((entity, sprite), 0, true);
return entity;
}
private bool TryGetPrototypeIcon(EntityPrototype prototype, out SpriteSpecifier? icon)
{
icon = null;
if (prototype.TryGetComponent(out IconComponent? iconComp, _entityManager.ComponentFactory))
{
icon = iconComp.Icon;
return true;
}
return false;
}
private bool TryExtractSpriteSpecifier(Type? expectedType, DataNode? node, out SpriteSpecifier? icon)
{
icon = null;
if (node == null)
return false;
if (expectedType != null &&
typeof(SpriteSpecifier).IsAssignableFrom(expectedType) &&
TryParseSpriteSpecifier(node, out icon))
{
return true;
}
if (node is MappingDataNode mapping)
{
foreach (var (key, child) in mapping.Children)
{
Type? childType = null;
if (expectedType != null &&
_serialization.TryGetVariableType(expectedType, key, out var resolvedType))
{
childType = resolvedType;
}
if (TryExtractSpriteSpecifier(childType, child, out icon))
return true;
}
return false;
}
if (node is SequenceDataNode sequence)
{
var elementType = GetSequenceElementType(expectedType);
foreach (var child in sequence.Sequence)
{
if (TryExtractSpriteSpecifier(elementType, child, out icon))
return true;
}
}
return false;
}
private static Type? GetSequenceElementType(Type? type)
{
if (type == null)
return null;
if (type.IsArray)
return type.GetElementType();
var genericArguments = type.GenericTypeArguments;
if (genericArguments.Length == 1)
return genericArguments[0];
return null;
}
private bool TryParseSpriteSpecifier(DataNode node, out SpriteSpecifier? icon)
{
icon = null;
try
{
icon = _serialization.Read<SpriteSpecifier>(node, notNullableOverride: true);
if (icon == SpriteSpecifier.Invalid)
{
icon = null;
return false;
}
return true;
}
catch
{
icon = null;
return false;
}
}
}