Tourist/Tourist/Util.cs

207 lines
7.4 KiB
C#
Executable File

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Dalamud.Game.Chat.SeStringHandling.Payloads;
using Dalamud.Plugin;
using FFXIVWeather.Lumina;
using Lumina.Excel.GeneratedSheets;
namespace Tourist {
public static class Util {
private static Dictionary<uint, (DateTimeOffset start, DateTimeOffset end)> Availability { get; } = new();
public static void OpenMapLocation(this DalamudPluginInterface @interface, Adventure adventure) {
var loc = adventure.Level?.Value;
var map = loc?.Map?.Value;
var terr = map?.TerritoryType?.Value;
if (terr == null) {
return;
}
var x = ToMapCoordinate(loc!.X, map!.SizeFactor);
var y = ToMapCoordinate(loc.Z, map.SizeFactor);
var mapLink = new MapLinkPayload(
@interface.Data,
terr.RowId,
map.RowId,
ConvertMapCoordinateToRawPosition(x, map.SizeFactor),
ConvertMapCoordinateToRawPosition(y, map.SizeFactor)
);
@interface.Framework.Gui.OpenMapWithMapLink(mapLink);
}
private static int ConvertMapCoordinateToRawPosition(float pos, float scale) {
var c = scale / 100.0f;
var scaledPos = ((pos - 1.0f) * c / 41.0f * 2048.0f - 1024.0f) / c;
scaledPos *= 1000.0f;
return (int) scaledPos;
}
private static float ToMapCoordinate(float val, float scale) {
var c = scale / 100f;
val *= c;
return 41f / c * ((val + 1024f) / 2048f) + 1;
}
public static DateTimeOffset EorzeaTime(DateTimeOffset? at = null) {
at ??= DateTimeOffset.UtcNow;
return DateTimeOffset.FromUnixTimeMilliseconds(at.Value.ToUnixTimeMilliseconds() * 144 / 7);
}
private static DateTimeOffset EarthTime(DateTimeOffset eorzea) {
return DateTimeOffset.FromUnixTimeMilliseconds(eorzea.ToUnixTimeMilliseconds() * 7 / 144);
}
public static (DateTimeOffset start, DateTimeOffset end)? NextAvailable(this Adventure adventure, FFXIVWeatherLuminaService service) {
if (adventure.MinTime == 0 && adventure.MaxTime == 0) {
return null;
}
var actualNow = DateTimeOffset.UtcNow;
if (!Availability.ContainsKey(adventure.RowId) && adventure.Available(service)) {
var ends = adventure.AvailabilityEnds(service, DateTimeOffset.Now) ?? default;
Availability[adventure.RowId] = (DateTimeOffset.Now, ends);
}
// check the cache and use that if it's not expired
if (Availability.TryGetValue(adventure.RowId, out var cached) && cached.end >= actualNow) {
return cached;
}
var eorzea = EorzeaTime(actualNow);
// start at a clean hour
eorzea = new DateTimeOffset(eorzea.Year, eorzea.Month, eorzea.Day, eorzea.Hour, 0, 0, 0, eorzea.Offset);
var minHour = adventure.MinTime / 100;
var maxHour = adventure.MaxTime / 100 + 1;
var numHours = (24 + maxHour - minHour) % 24;
for (var i = 0; i < 10_000; i++) {
// find the next available time
var add = (minHour + 24 - eorzea.Hour) % 24;
eorzea = eorzea.AddHours(add);
// check the weather for each hour in the available range
for (var h = 0; h < numHours; h++) {
var earth = EarthTime(eorzea);
// check the weather
var offset = Math.Ceiling((earth - actualNow).TotalSeconds);
if (adventure.WeatherAvailable(service, offset)) {
// determine when the availability will end
var ends = adventure.AvailabilityEnds(service, earth) ?? default;
// cache the result of the calculation
Availability[adventure.RowId] = (earth, ends);
return (earth, ends);
}
eorzea = eorzea.AddHours(1);
}
}
return null;
}
private static DateTimeOffset? AvailabilityEnds(this Adventure adventure, FFXIVWeatherLuminaService service, DateTimeOffset starting) {
if (adventure.MinTime == 0 && adventure.MaxTime == 0) {
return null;
}
var now = starting;
var eorzea = EorzeaTime(now);
eorzea = new DateTimeOffset(eorzea.Year, eorzea.Month, eorzea.Day, eorzea.Hour, 0, 0, 0, eorzea.Offset);
now = EarthTime(eorzea);
var maxHour = adventure.MaxTime / 100 + 1;
while (eorzea.Hour != maxHour) {
if (!adventure.Available(service, eorzea)) {
return now;
}
eorzea = eorzea.AddHours(1);
now = EarthTime(eorzea);
}
return now;
}
public static bool Available(this Adventure adventure, FFXIVWeatherLuminaService service, DateTimeOffset? eorzea = null) {
var time = adventure.TimeAvailable(eorzea);
var offset = eorzea == null
? 0
: Math.Ceiling((EarthTime(eorzea.Value) - DateTimeOffset.UtcNow).TotalSeconds);
var weather = adventure.WeatherAvailable(service, offset);
return time && weather;
}
private static bool TimeAvailable(this Adventure adventure, DateTimeOffset? eorzea = null) {
if (adventure.MinTime == 0 && adventure.MaxTime == 0) {
return true;
}
eorzea ??= EorzeaTime();
var minHour = adventure.MinTime / 100;
var minMins = adventure.MinTime % 100;
var min = new TimeSpan(minHour, minMins, 0);
var maxHour = adventure.MaxTime / 100;
var maxMins = adventure.MaxTime % 100;
var max = new TimeSpan(maxHour, maxMins, 59);
var now = eorzea.Value.TimeOfDay;
if (min <= max) {
// start and stop times are in the same day
if (now >= min && now <= max) {
return true;
}
} else {
// start and stop times are in different days
if (now >= min || now <= max) {
return true;
}
}
return false;
}
private static bool WeatherAvailable(this Adventure adventure, FFXIVWeatherLuminaService service, double offset = 0d) {
if (!Weathers.All.TryGetValue(adventure.RowId, out var weathers)) {
return true;
}
var (weather, _) = service.GetCurrentWeather(adventure.Level.Value.Territory.Value, offset);
return weathers.Contains(weather.RowId);
}
public static string ToHumanReadable(this TimeSpan span) {
var hours = span.TotalDays > 0
? (int) Math.Truncate(span.TotalSeconds / 60 / 60)
: span.Hours;
var readable = new StringBuilder();
if (hours != 0) {
readable.Append($"{hours:00}:");
}
readable.Append($"{span.Minutes:00}:");
readable.Append($"{span.Seconds:00}");
return readable.ToString();
}
}
}