207 lines
7.4 KiB
C#
Executable File
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();
|
|
}
|
|
}
|
|
}
|