305 lines
12 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|