⚗️ Import/Export: Import, event rework

This commit is contained in:
Liza 2022-12-22 01:01:09 +01:00
parent bb721bc37f
commit 4c22334761
10 changed files with 350 additions and 136 deletions

View File

@ -1,5 +1,6 @@
using Dalamud.Configuration;
using Dalamud.Logging;
using Pal.Client.Scheduled;
using System;
using System.Collections.Generic;
using System.Linq;
@ -25,6 +26,8 @@ namespace Pal.Client
public Dictionary<string, Guid> AccountIds { private get; set; } = new();
public Dictionary<string, AccountInfo> Accounts { get; set; } = new();
public List<ImportHistoryEntry> ImportHistory { get; set; } = new();
public bool ShowTraps { get; set; } = true;
public Vector4 TrapColor { get; set; } = new Vector4(1, 0, 0, 0.4f);
public bool OnlyVisibleTrapsAfterPomander { get; set; } = true;
@ -36,10 +39,12 @@ namespace Pal.Client
public bool ShowSilverCoffers { get; set; } = false;
public Vector4 SilverCofferColor { get; set; } = new Vector4(1, 1, 1, 0.4f);
public bool FillSilverCoffers { get; set; } = true;
#endregion
public delegate void OnSaved();
public event OnSaved? Saved;
/// <summary>
/// Needs to be manually set.
/// </summary>
public string BetaKey { get; set; } = "";
#endregion
#pragma warning disable CS0612 // Type or member is obsolete
public void Migrate()
@ -75,7 +80,7 @@ namespace Pal.Client
public void Save()
{
Service.PluginInterface.SavePluginConfig(this);
Saved?.Invoke();
Service.Plugin.EarlyEventQueue.Enqueue(new QueuedConfigUpdate());
}
public enum EMode
@ -105,5 +110,17 @@ namespace Pal.Client
/// </summary>
public List<string> CachedRoles { get; set; } = new List<string>();
}
public class ImportHistoryEntry
{
public Guid Id { get; set; }
public string? RemoteUrl { get; set; }
public DateTime ExportedAt { get; set; }
/// <summary>
/// Set when the file is imported locally.
/// </summary>
public DateTime ImportedAt { get; set; }
}
}
}

View File

@ -26,7 +26,11 @@ namespace Pal.Client
private void ApplyFilters()
{
if (Service.Configuration.Mode == Configuration.EMode.Offline)
Markers = new ConcurrentBag<Marker>(Markers.Where(x => x.Seen));
Markers = new ConcurrentBag<Marker>(Markers.Where(x => x.Seen || (x.WasImported && x.Imports.Count > 0)));
else
// ensure old import markers are removed if they are no longer part of a "current" import
// this MAY remove markers the server sent you (and that you haven't seen), but this should be fixed the next time you enter the zone
Markers = new ConcurrentBag<Marker>(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0));
}
public static LocalState? Load(uint territoryType)

View File

@ -41,6 +41,15 @@ namespace Pal.Client
[JsonIgnore]
public bool RemoteSeenRequested { get; set; } = false;
/// <summary>
/// To keep track of which markers were imported through a downloaded file, we save the associated import-id.
///
/// Importing another file for the same remote server will remove the old import-id, and add the new import-id here.
/// </summary>
public List<Guid> Imports { get; set; } = new List<Guid>();
public bool WasImported { get; set; }
[JsonIgnore]
public Element? SplatoonElement { get; set; }

View File

@ -15,6 +15,7 @@ using Grpc.Core;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Pal.Client.Net;
using Pal.Client.Scheduled;
using Pal.Client.Windows;
using Pal.Common;
using System;
@ -35,23 +36,23 @@ namespace Pal.Client
private const string SPLATOON_TRAP_HOARD = "PalacePal.TrapHoard";
private const string SPLATOON_REGULAR_COFFERS = "PalacePal.RegularCoffers";
private readonly ConcurrentQueue<Sync> _pendingSyncResponses = new();
private readonly static Dictionary<Marker.EType, MarkerConfig> _markerConfig = new Dictionary<Marker.EType, MarkerConfig>
{
{ Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } },
{ Marker.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } },
{ Marker.EType.SilverCoffer, new MarkerConfig { Radius = 1f } },
};
private bool _configUpdated = false;
private LocalizedChatMessages _localizedChatMessages = new();
internal ConcurrentDictionary<ushort, LocalState> FloorMarkers { get; } = new();
internal ConcurrentBag<Marker> EphemeralMarkers { get; set; } = new();
internal ushort LastTerritory { get; private set; }
internal ushort LastTerritory { get; set; }
public SyncState TerritorySyncState { get; set; }
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;
public string? DebugMessage { get; set; }
internal Queue<IQueueOnFrameworkThread> EarlyEventQueue { get; } = new();
internal Queue<IQueueOnFrameworkThread> LateEventQueue { get; } = new();
public string Name => "Palace Pal";
@ -101,7 +102,6 @@ namespace Pal.Client
pluginInterface.UiBuilder.Draw += Service.WindowSystem.Draw;
pluginInterface.UiBuilder.OpenConfigUi += OnOpenConfigUi;
Service.Framework.Update += OnFrameworkUpdate;
Service.Configuration.Saved += OnConfigSaved;
Service.Chat.ChatMessage += OnChatMessage;
Service.CommandManager.AddHandler("/pal", new CommandInfo(OnCommand)
{
@ -182,7 +182,6 @@ namespace Pal.Client
Service.PluginInterface.UiBuilder.Draw -= Service.WindowSystem.Draw;
Service.PluginInterface.UiBuilder.OpenConfigUi -= OnOpenConfigUi;
Service.Framework.Update -= OnFrameworkUpdate;
Service.Configuration.Saved -= OnConfigSaved;
Service.Chat.ChatMessage -= OnChatMessage;
Service.WindowSystem.RemoveAllWindows();
@ -208,11 +207,6 @@ namespace Pal.Client
}
#endregion
private void OnConfigSaved()
{
_configUpdated = true;
}
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, ref bool isHandled)
{
if (Service.Configuration.FirstUse)
@ -259,27 +253,18 @@ namespace Pal.Client
try
{
bool recreateLayout = false;
if (_configUpdated)
{
if (Service.Configuration.Mode == Configuration.EMode.Offline)
{
LocalState.UpdateAll();
FloorMarkers.Clear();
EphemeralMarkers.Clear();
LastTerritory = 0;
}
_configUpdated = false;
recreateLayout = true;
}
bool saveMarkers = false;
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
queued?.Run(this, ref recreateLayout, ref saveMarkers);
if (LastTerritory != Service.ClientState.TerritoryType)
{
LastTerritory = Service.ClientState.TerritoryType;
TerritorySyncState = SyncState.NotAttempted;
if (IsInDeepDungeon())
FloorMarkers[LastTerritory] = LocalState.Load(LastTerritory) ?? new LocalState(LastTerritory);
GetFloorMarkers(LastTerritory);
EphemeralMarkers.Clear();
PomanderOfSight = PomanderState.Inactive;
PomanderOfIntuition = PomanderState.Inactive;
@ -296,15 +281,10 @@ namespace Pal.Client
Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory));
}
if (_pendingSyncResponses.Count > 0)
{
HandleSyncResponses();
recreateLayout = true;
saveMarkers = true;
}
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
queued?.Run(this, ref recreateLayout, ref saveMarkers);
if (!FloorMarkers.TryGetValue(LastTerritory, out var currentFloor))
FloorMarkers[LastTerritory] = currentFloor = new LocalState(LastTerritory);
var currentFloor = GetFloorMarkers(LastTerritory);
IList<Marker> visibleMarkers = GetRelevantGameObjects();
HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout);
@ -316,6 +296,11 @@ namespace Pal.Client
}
}
internal LocalState GetFloorMarkers(ushort territoryType)
{
return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt));
}
private void HandlePersistentMarkers(LocalState currentFloor, IList<Marker> visibleMarkers, bool saveMarkers, bool recreateLayout)
{
var config = Service.Configuration;
@ -403,7 +388,7 @@ namespace Pal.Client
List<Element> elements = new List<Element>();
foreach (var marker in currentFloorMarkers)
{
if (marker.Seen || config.Mode == Configuration.EMode.Online)
if (marker.Seen || config.Mode == Configuration.EMode.Online || (marker.WasImported && marker.Imports.Count > 0))
{
if (marker.Type == Marker.EType.Trap && config.ShowTraps)
{
@ -503,7 +488,7 @@ namespace Pal.Client
try
{
var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId);
_pendingSyncResponses.Enqueue(new Sync
LateEventQueue.Enqueue(new QueuedSyncResponse
{
Type = SyncType.Download,
TerritoryType = territoryId,
@ -522,7 +507,7 @@ namespace Pal.Client
try
{
var (success, uploadedMarkers) = await Service.RemoteApi.UploadMarker(territoryId, markersToUpload);
_pendingSyncResponses.Enqueue(new Sync
LateEventQueue.Enqueue(new QueuedSyncResponse
{
Type = SyncType.Upload,
TerritoryType = territoryId,
@ -541,7 +526,7 @@ namespace Pal.Client
try
{
var success = await Service.RemoteApi.MarkAsSeen(territoryId, markersToUpdate);
_pendingSyncResponses.Enqueue(new Sync
LateEventQueue.Enqueue(new QueuedSyncResponse
{
Type = SyncType.MarkSeen,
TerritoryType = territoryId,
@ -587,70 +572,6 @@ namespace Pal.Client
}
}
private void HandleSyncResponses()
{
while (_pendingSyncResponses.TryDequeue(out Sync? sync) && sync != null)
{
try
{
var territoryId = sync.TerritoryType;
var remoteMarkers = sync.Markers;
if (Service.Configuration.Mode == Configuration.EMode.Online && sync.Success && FloorMarkers.TryGetValue(territoryId, out var currentFloor) && remoteMarkers.Count > 0)
{
switch (sync.Type)
{
case SyncType.Download:
case SyncType.Upload:
foreach (var remoteMarker in remoteMarkers)
{
// Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved.
Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker);
if (localMarker != null)
{
localMarker.NetworkId = remoteMarker.NetworkId;
continue;
}
if (sync.Type == SyncType.Download)
currentFloor.Markers.Add(remoteMarker);
}
break;
case SyncType.MarkSeen:
var accountId = Service.RemoteApi.AccountId;
if (accountId == null)
break;
foreach (var remoteMarker in remoteMarkers)
{
Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker);
if (localMarker != null)
localMarker.RemoteSeenOn.Add(accountId.Value);
}
break;
}
}
// don't modify state for outdated floors
if (LastTerritory != territoryId)
continue;
if (sync.Type == SyncType.Download)
{
if (sync.Success)
TerritorySyncState = SyncState.Complete;
else
TerritorySyncState = SyncState.Failed;
}
}
catch (Exception e)
{
DebugMessage = $"{DateTime.Now}\n{e}";
if (sync.Type == SyncType.Download)
TerritorySyncState = SyncState.Failed;
}
}
}
private IList<Marker> GetRelevantGameObjects()
{
List<Marker> result = new();
@ -729,30 +650,6 @@ namespace Pal.Client
return Service.DataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text?.ToString() ?? "Unknown";
}
internal class Sync
{
public SyncType Type { get; set; }
public ushort TerritoryType { get; set; }
public bool Success { get; set; }
public List<Marker> Markers { get; set; } = new();
}
public enum SyncState
{
NotAttempted,
NotNeeded,
Started,
Complete,
Failed,
}
public enum SyncType
{
Upload,
Download,
MarkSeen,
}
public enum PomanderState
{
Inactive,

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pal.Client.Scheduled
{
internal interface IQueueOnFrameworkThread
{
void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers);
}
}

View File

@ -0,0 +1,19 @@
namespace Pal.Client.Scheduled
{
internal class QueuedConfigUpdate : IQueueOnFrameworkThread
{
public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers)
{
if (Service.Configuration.Mode == Configuration.EMode.Offline)
{
LocalState.UpdateAll();
plugin.FloorMarkers.Clear();
plugin.EphemeralMarkers.Clear();
plugin.LastTerritory = 0;
recreateLayout = true;
saveMarkers = true;
}
}
}
}

View File

@ -0,0 +1,121 @@
using Account;
using Dalamud.Logging;
using Pal.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
namespace Pal.Client.Scheduled
{
internal class QueuedImport : IQueueOnFrameworkThread
{
private readonly ExportRoot _export;
private Guid _exportId;
private int importedTraps;
private int importedHoardCoffers;
public QueuedImport(string sourcePath)
{
using (var input = File.OpenRead(sourcePath))
_export = ExportRoot.Parser.ParseFrom(input);
}
public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers)
{
try
{
if (!Validate())
return;
var config = Service.Configuration;
var oldExportIds = string.IsNullOrEmpty(_export.ServerUrl) ? config.ImportHistory.Where(x => x.RemoteUrl == _export.ServerUrl).Select(x => x.Id).Where(x => x != Guid.Empty).ToList() : new List<Guid>();
foreach (var remoteFloor in _export.Floors)
{
ushort territoryType = (ushort)remoteFloor.TerritoryType;
var localState = plugin.GetFloorMarkers(territoryType);
CleanupFloor(localState, oldExportIds);
ImportFloor(remoteFloor, localState);
localState.Save();
}
config.ImportHistory.RemoveAll(hist => oldExportIds.Contains(hist.Id) || hist.Id == _exportId);
config.ImportHistory.Add(new Configuration.ImportHistoryEntry
{
Id = _exportId,
RemoteUrl = _export.ServerUrl,
ExportedAt = _export.CreatedAt.ToDateTime(),
ImportedAt = DateTime.UtcNow,
});
config.Save();
recreateLayout = true;
saveMarkers = true;
Service.Chat.Print($"Imported {importedTraps} new trap locations and {importedHoardCoffers} new hoard coffer locations.");
}
catch (Exception e)
{
PluginLog.Error(e, "Import failed");
Service.Chat.PrintError($"Import failed: {e}");
}
}
private bool Validate()
{
if (_export.ExportVersion != ExportConfig.ExportVersion)
{
Service.Chat.PrintError("Import failed: Incompatible version.");
return false;
}
if (!Guid.TryParse(_export.ExportId, out _exportId) || _exportId == Guid.Empty)
{
Service.Chat.PrintError("Import failed: No id present.");
return false;
}
if (string.IsNullOrEmpty(_export.ServerUrl))
{
// If we allow for backups as import/export, this should be removed
Service.Chat.PrintError("Import failed: Unknown server.");
return false;
}
return true;
}
private void CleanupFloor(LocalState localState, List<Guid> oldExportIds)
{
// When saving a floor state, any markers not seen, not remote seen, and not having an import id are removed;
// so it is possible to remove "wrong" markers by not having them be in the current import.
foreach (var marker in localState.Markers)
marker.Imports.RemoveAll(id => oldExportIds.Contains(id));
}
private void ImportFloor(ExportFloor remoteFloor, LocalState localState)
{
var remoteMarkers = remoteFloor.Objects.Select(m => new Marker((Marker.EType)m.Type, new Vector3(m.X, m.Y, m.Z)) { WasImported = true });
foreach (var remoteMarker in remoteMarkers)
{
Marker? localMarker = localState.Markers.SingleOrDefault(x => x == remoteMarker);
if (localMarker == null)
{
localState.Markers.Add(remoteMarker);
localMarker = remoteMarker;
if (localMarker.Type == Marker.EType.Trap)
importedTraps++;
else if (localMarker.Type == Marker.EType.Hoard)
importedHoardCoffers++;
}
remoteMarker.Imports.Add(_exportId);
}
}
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Pal.Client.Plugin;
namespace Pal.Client.Scheduled
{
internal class QueuedSyncResponse : IQueueOnFrameworkThread
{
public SyncType Type { get; set; }
public ushort TerritoryType { get; set; }
public bool Success { get; set; }
public List<Marker> Markers { get; set; } = new();
public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers)
{
recreateLayout = true;
saveMarkers = true;
try
{
var remoteMarkers = Markers;
var currentFloor = plugin.GetFloorMarkers(TerritoryType);
if (Service.Configuration.Mode == Configuration.EMode.Online && Success && remoteMarkers.Count > 0)
{
switch (Type)
{
case SyncType.Download:
case SyncType.Upload:
foreach (var remoteMarker in remoteMarkers)
{
// Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved.
Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker);
if (localMarker != null)
{
localMarker.NetworkId = remoteMarker.NetworkId;
continue;
}
if (Type == SyncType.Download)
currentFloor.Markers.Add(remoteMarker);
}
break;
case SyncType.MarkSeen:
var accountId = Service.RemoteApi.AccountId;
if (accountId == null)
break;
foreach (var remoteMarker in remoteMarkers)
{
Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker);
if (localMarker != null)
localMarker.RemoteSeenOn.Add(accountId.Value);
}
break;
}
}
// don't modify state for outdated floors
if (plugin.LastTerritory != TerritoryType)
return;
if (Type == SyncType.Download)
{
if (Success)
plugin.TerritorySyncState = SyncState.Complete;
else
plugin.TerritorySyncState = SyncState.Failed;
}
}
catch (Exception e)
{
plugin.DebugMessage = $"{DateTime.Now}\n{e}";
if (Type == SyncType.Download)
plugin.TerritorySyncState = SyncState.Failed;
}
}
}
public enum SyncState
{
NotAttempted,
NotNeeded,
Started,
Complete,
Failed,
}
public enum SyncType
{
Upload,
Download,
MarkSeen,
}
}

View File

@ -4,11 +4,14 @@ using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using ECommons;
using ECommons.Reflection;
using ECommons.SplatoonAPI;
using Google.Protobuf;
using ImGuiNET;
using Pal.Client.Net;
using Pal.Client.Scheduled;
using Pal.Common;
using System;
using System.Collections;
using System.Collections.Generic;
@ -88,7 +91,7 @@ namespace Pal.Client.Windows
{
DrawTrapCofferTab(ref save, ref saveAndClose);
DrawCommunityTab(ref saveAndClose);
//DrawImportTab();
DrawImportTab();
DrawExportTab();
DrawDebugTab();
@ -196,15 +199,26 @@ namespace Pal.Client.Windows
private void DrawImportTab()
{
if (Service.Configuration.BetaKey != "import")
return;
if (ImGui.BeginTabItem("Import"))
{
ImGui.TextWrapped("Using an export is useful if you're unable to connect to the server, or don't wish to share your findings.");
ImGui.TextWrapped("Exports are (currently) generated manually, and they only include traps and hoard coffers encountered by 5 or more people. This may lead to higher floors having very sporadic coverage, but commonly run floors (e.g. PotD 51-60, HoH 21-30) are closer to complete.");
ImGui.TextWrapped("If you aren't offline, importing a file won't have any noticeable effect.");
ImGui.Separator();
ImGui.TextWrapped("Exports are available from https://github.com/carvelli/PalacePal/releases/ (as *.pal files).");
if (ImGui.Button("Visit GitHub"))
GenericHelpers.ShellStart("https://github.com/carvelli/PalacePal/releases/");
ImGui.Separator();
ImGui.Text("File to Import:");
ImGui.SameLine();
ImGui.InputTextWithHint("", "Path to *.pal file", ref _openImportPath, 260);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Search))
{
_importDialog.OpenFileDialog("Palace Pal - Import", "Palace Pal (*.pal) {*.pal}", (success, paths) =>
_importDialog.OpenFileDialog("Palace Pal - Import", "Palace Pal (*.pal) {.pal}", (success, paths) =>
{
if (success && paths.Count == 1)
{
@ -213,6 +227,11 @@ namespace Pal.Client.Windows
}, selectionCountMax: 1, startPath: _openImportDialogStartPath, isModal: false);
_openImportDialogStartPath = null; // only use this once, FileDialogManager will save path between calls
}
ImGui.BeginDisabled(string.IsNullOrEmpty(_openImportPath) || !File.Exists(_openImportPath));
if (ImGui.Button("Start Import"))
DoImport(_openImportPath);
ImGui.EndDisabled();
ImGui.EndTabItem();
}
}
@ -232,7 +251,7 @@ namespace Pal.Client.Windows
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Search))
{
_importDialog.SaveFileDialog("Palace Pal - Export", "Palace Pal (*.pal) {*.pal}", todaysFileName, "pal", (success, path) =>
_importDialog.SaveFileDialog("Palace Pal - Export", "Palace Pal (*.pal) {.pal}", todaysFileName, "pal", (success, path) =>
{
if (success && !string.IsNullOrEmpty(path))
{
@ -244,7 +263,7 @@ namespace Pal.Client.Windows
ImGui.BeginDisabled(string.IsNullOrEmpty(_saveExportPath) || File.Exists(_saveExportPath));
if (ImGui.Button("Start Export"))
this.DoExport(_saveExportPath);
DoExport(_saveExportPath);
ImGui.EndDisabled();
ImGui.EndTabItem();
@ -382,7 +401,12 @@ namespace Pal.Client.Windows
});
}
internal void DoExport(string destination)
internal void DoImport(string sourcePath)
{
Service.Plugin.EarlyEventQueue.Enqueue(new QueuedImport(sourcePath));
}
internal void DoExport(string destinationPath)
{
Task.Run(async () =>
{
@ -391,10 +415,10 @@ namespace Pal.Client.Windows
(bool success, ExportRoot export) = await Service.RemoteApi.DoExport();
if (success)
{
using var output = File.Create(destination);
using var output = File.Create(destinationPath);
export.WriteTo(output);
Service.Chat.Print($"Export saved as {destination}.");
Service.Chat.Print($"Export saved as {destinationPath}.");
}
else
{

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pal.Common
{
public static class ExportConfig
{
public static int ExportVersion { get; } = 1;
}
}