Screenie/ScreenshotMetadata.cs

305 lines
12 KiB
C#

using System.Numerics;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Housing;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Graphics.Environment;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
using Screenie.Util;
using Map = Lumina.Excel.GeneratedSheets.Map;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
namespace Screenie;
[Serializable]
public class SavedMetadata {
public required string Blake3Hash;
public required string Path;
public required ScreenshotMetadata Metadata;
}
[Serializable]
public class ScreenshotMetadata {
public required Character? ActiveCharacter;
public required string? Location;
public required string? LocationSub;
public required string? Area;
public required string? AreaSub;
public required uint TerritoryType;
public required string? World;
public required uint WorldId;
public required DateTime CapturedAtLocal;
public required DateTime CapturedAtUtc;
public required string EorzeaTime;
public required string? Weather;
public required uint? Ward;
public required uint? Plot;
public required Character[] VisibleCharacters;
public required PenumbraMod[] ModsInUse;
internal static ScreenshotMetadata Capture(Plugin plugin) {
EorzeaTime eorzea;
uint? ward;
uint? plot;
short offsetX;
short offsetY;
float scale;
Weather? weather;
Map? map;
PlaceName? area;
PlaceName? areaSub;
unsafe {
var framework = Framework.Instance();
eorzea = new EorzeaTime((ulong) framework->ClientTime.EorzeaTime);
var housing = HousingManager.Instance();
try {
ward = (uint) housing->GetCurrentWard() + 1;
} catch {
ward = null;
}
try {
plot = (uint) housing->GetCurrentPlot() + 1;
} catch {
plot = null;
}
var env = EnvManager.Instance();
var weatherId = env->ActiveWeather;
weather = plugin.DataManager.GetExcelSheet<Weather>()?.GetRow(weatherId);
var agentMap = AgentMap.Instance();
offsetX = agentMap->CurrentOffsetX;
offsetY = agentMap->CurrentOffsetY;
scale = agentMap->CurrentMapSizeFactorFloat;
var mapId = agentMap->CurrentMapId;
map = plugin.DataManager.GetExcelSheet<Map>()?.GetRow(mapId);
var territoryInfo = TerritoryInfo.Instance();
area = plugin.DataManager.GetExcelSheet<PlaceName>()?.GetRow(territoryInfo->AreaPlaceNameID);
areaSub = plugin.DataManager.GetExcelSheet<PlaceName>()?.GetRow(territoryInfo->SubAreaPlaceNameID);
}
var territory = plugin.DataManager.GetExcelSheet<TerritoryType>()?.GetRow(plugin.ClientState.TerritoryType);
Character? active = null;
World? world = null;
if (plugin.ClientState.LocalPlayer is { } player) {
world = plugin.DataManager.GetExcelSheet<World>()?.GetRow(player.CurrentWorld.Id);
plugin.GameGui.WorldToScreen(player.Position, out var screenPos);
active = new Character(player, screenPos, scale, offsetX, offsetY);
}
var timeUtc = DateTime.UtcNow;
// var relevantModObjects = new List<Dalamud.Game.ClientState.Objects.Types.GameObject>();
var relevantModObjects = new List<ushort>();
var visible = plugin.ObjectTable
.Select((obj, idx) => (obj, idx))
.Where(tuple => {
var (obj, idx) = tuple;
// pull a sneaky and populate the relevantModObjects list here
if (
obj.ObjectKind is
ObjectKind.Player
or ObjectKind.Companion
or ObjectKind.BattleNpc
or ObjectKind.EventNpc
or ObjectKind.MountType
or ObjectKind.Retainer
) {
if (idx > ushort.MaxValue) {
Plugin.Log.Warning($"cannot pass object with idx {idx} to Penumbra: too large");
} else {
unsafe {
var gobj = (GameObject*) obj.Address;
var draw = gobj->DrawObject;
if (draw != null && draw->IsVisible) {
var visible = plugin.GameGui.WorldToScreen(obj.Position, out _, out _);
if (visible) {
relevantModObjects.Add((ushort) idx);
}
}
}
}
}
return obj is PlayerCharacter;
})
.Select(tuple => tuple.obj)
.Cast<PlayerCharacter>()
.Where(chara => {
unsafe {
var obj = (GameObject*) chara.Address;
var draw = obj->DrawObject;
return draw != null && draw->IsVisible;
}
})
.Select(chara => {
var visible = plugin.GameGui.WorldToScreen(chara.Position, out var screenPos, out _);
return (chara, screenPos, visible);
})
.Where(tuple => tuple.visible)
.Select(tuple => new Character(tuple.chara, tuple.screenPos, scale, offsetX, offsetY))
.ToArray();
var modDir = plugin.Penumbra.GetModDirectory();
var paths = plugin.Penumbra.GetGameObjectResourcePaths([.. relevantModObjects]);
var mods = plugin.Penumbra.GetMods();
var modsInUse = new HashSet<PenumbraMod>();
if (modDir != null && paths != null && mods != null) {
var baseUri = new Uri($"{Path.TrimEndingDirectorySeparator(modDir)}/");
var enumerable = paths
.Where(p => p != null)
.SelectMany(p => p!.Values);
foreach (var dict in paths) {
if (dict == null) {
continue;
}
foreach (var path in dict.Keys) {
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var pathUri)) {
continue;
}
if (!pathUri.IsAbsoluteUri || !baseUri.IsBaseOf(pathUri)) {
continue;
}
var modPathUntrimmed = Uri.UnescapeDataString(pathUri.Segments[baseUri.Segments.Length..][0]);
var modPath = Path.TrimEndingDirectorySeparator(modPathUntrimmed);
var found = mods.FirstOrDefault(mod => string.Equals(mod.Directory, modPath, StringComparison.OrdinalIgnoreCase));
if (found != default) {
modsInUse.Add(new PenumbraMod {
Name = found.Name,
Path = found.Directory,
});
}
}
}
}
return new ScreenshotMetadata {
ActiveCharacter = active,
Location = map?.PlaceName.Value?.Name.ToDalamudString().TextValue.WhitespaceToNull()
?? territory?.PlaceName.Value?.Name.ToDalamudString().TextValue.WhitespaceToNull(),
LocationSub = map?.PlaceNameSub.Value?.Name.ToDalamudString().TextValue.WhitespaceToNull(),
Area = area?.Name.ToDalamudString().TextValue.WhitespaceToNull(),
AreaSub = areaSub?.Name.ToDalamudString().TextValue.WhitespaceToNull(),
TerritoryType = plugin.ClientState.TerritoryType,
World = world?.Name.ToDalamudString().TextValue.WhitespaceToNull(),
WorldId = world?.RowId ?? 0,
CapturedAtLocal = timeUtc.ToLocalTime(),
CapturedAtUtc = timeUtc,
EorzeaTime = $"{eorzea.Hour:00}:{eorzea.Minute:00}",
Weather = weather?.Name.ToDalamudString().TextValue.WhitespaceToNull(),
Ward = ward,
Plot = plot,
VisibleCharacters = visible,
ModsInUse = [.. modsInUse.OrderBy(mod => mod.Name)],
};
}
}
[Serializable]
public class Character {
public string Name;
public string? HomeWorld;
public uint HomeWorldId;
public Vector3 RawPosition;
public Vector3 MapPosition;
public Vector2 ImagePosition;
public uint Level;
public string? Job;
public uint JobId;
public Character(PlayerCharacter player, Vector2 screenPos, float scale, short offsetX, short offsetY) {
this.Name = player.Name.TextValue;
this.HomeWorld = player.HomeWorld.GameData?.Name.ToDalamudString().TextValue.WhitespaceToNull();
this.HomeWorldId = player.HomeWorld.Id;
this.RawPosition = player.Position;
this.MapPosition = new Vector3(
ConvertRawPositionToMapCoordinate(player.Position.X, scale, offsetX),
ConvertRawPositionToMapCoordinate(player.Position.Z, scale, offsetY),
player.Position.Y // TODO: how does map Z coord work
);
this.ImagePosition = screenPos;
this.Level = player.Level;
this.Job = player.ClassJob.GameData?.Name.ToDalamudString().TextValue.WhitespaceToNull();
this.JobId = player.ClassJob.Id;
}
[JsonConstructor]
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private Character() {
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private static float ConvertRawPositionToMapCoordinate(float pos, float scale, short offset) {
var num2 = (pos + offset) * scale;
return (float) (41.0 / scale * ((num2 + 1024.0) / 2048.0) + 1.0);
}
}
public struct EorzeaTime {
public readonly ulong Year;
public readonly ulong Month;
public readonly ulong Day;
public readonly ulong Hour;
public readonly ulong Minute;
public readonly ulong Second;
private const double EorzeaTimeConst = (double) 3600 / 175;
private const double YearConst = 33177600;
private const double MonthConst = 2764800;
private const double DayConst = 86400;
private const double HourConst = 3600;
private const double MinuteConst = 60;
private const double SecondConst = 1;
public EorzeaTime(DateTime time) : this((ulong) Math.Floor(((DateTimeOffset) time).ToUnixTimeSeconds() * EorzeaTimeConst)) {
}
public EorzeaTime(ulong eorzea) {
this.Year = (ulong) Math.Floor(eorzea / YearConst) + 1;
this.Month = (ulong) Math.Floor(eorzea / MonthConst % 12) + 1;
this.Day = (ulong) Math.Floor(eorzea / DayConst % 32) + 1;
this.Hour = (ulong) Math.Floor(eorzea / HourConst % 24);
this.Minute = (ulong) Math.Floor(eorzea / MinuteConst % 60);
this.Second = (ulong) Math.Floor(eorzea / SecondConst % 60);
}
}
[Serializable]
public class PenumbraMod {
public required string Path { get; init; }
public required string Name { get; init; }
protected bool Equals(PenumbraMod other) {
return this.Path == other.Path && this.Name == other.Name;
}
public override bool Equals(object? obj) {
if (ReferenceEquals(null, obj)) {
return false;
}
if (ReferenceEquals(this, obj)) {
return true;
}
return obj.GetType() == this.GetType() && this.Equals((PenumbraMod) obj);
}
public override int GetHashCode() {
unchecked {
return (this.Path.GetHashCode() * 397) ^ this.Name.GetHashCode();
}
}
}