Db: Move Markers into database

This commit is contained in:
Liza 2023-02-18 21:12:36 +01:00
parent f63e70b0c4
commit 94f3fa2ede
36 changed files with 1128 additions and 606 deletions

View File

@ -3,6 +3,7 @@ using System.Linq;
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Floors;
using Pal.Client.Rendering; using Pal.Client.Rendering;
namespace Pal.Client.Commands namespace Pal.Client.Commands
@ -32,30 +33,33 @@ namespace Pal.Client.Commands
break; break;
case "tnear": case "tnear":
DebugNearest(m => m.Type == Marker.EType.Trap); DebugNearest(m => m.Type == MemoryLocation.EType.Trap);
break; break;
case "hnear": case "hnear":
DebugNearest(m => m.Type == Marker.EType.Hoard); DebugNearest(m => m.Type == MemoryLocation.EType.Hoard);
break; break;
} }
} }
private void DebugNearest(Predicate<Marker> predicate) private void DebugNearest(Predicate<PersistentLocation> predicate)
{ {
if (!_territoryState.IsInDeepDungeon()) if (!_territoryState.IsInDeepDungeon())
return; return;
var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType);
if (state == null)
return;
var playerPosition = _clientState.LocalPlayer?.Position; var playerPosition = _clientState.LocalPlayer?.Position;
if (playerPosition == null) if (playerPosition == null)
return; return;
_chat.Message($"{playerPosition}"); _chat.Message($"{playerPosition}");
var nearbyMarkers = state.Markers var nearbyMarkers = state.Locations
.Where(m => predicate(m)) .Where(m => predicate(m))
.Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible)
.Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) .Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() })
.OrderBy(m => m.distance) .OrderBy(m => m.distance)
.Take(5) .Take(5)
.ToList(); .ToList();

View File

@ -114,6 +114,9 @@ namespace Pal.Client.Configuration.Legacy
.Cast<ImportHistory>() .Cast<ImportHistory>()
.Distinct() .Distinct()
.ToList(), .ToList(),
Imported = o.WasImported,
SinceVersion = o.SinceVersion ?? "0.0",
}; };
clientLocation.RemoteEncounters = o.RemoteSeenOn clientLocation.RemoteEncounters = o.RemoteSeenOn

View File

@ -30,6 +30,17 @@ namespace Pal.Client.Database
/// </summary> /// </summary>
public List<ImportHistory> ImportedBy { get; set; } = new(); public List<ImportHistory> ImportedBy { get; set; } = new();
/// <summary>
/// Whether this location was originally imported.
/// </summary>
public bool Imported { get; set; }
/// <summary>
/// To make rollbacks of local data easier, keep track of the plugin version which was used to create this location initially.
/// </summary>
public string SinceVersion { get; set; } = "0.0";
public enum EType public enum EType
{ {
Trap = 1, Trap = 1,

View File

@ -0,0 +1,148 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Pal.Client.Database;
#nullable disable
namespace Pal.Client.Database.Migrations
{
[DbContext(typeof(PalClientContext))]
[Migration("20230218112804_AddImportedAndSinceVersionToClientLocation")]
partial class AddImportedAndSinceVersionToClientLocation
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
modelBuilder.Entity("ClientLocationImportHistory", b =>
{
b.Property<Guid>("ImportedById")
.HasColumnType("TEXT");
b.Property<int>("ImportedLocationsLocalId")
.HasColumnType("INTEGER");
b.HasKey("ImportedById", "ImportedLocationsLocalId");
b.HasIndex("ImportedLocationsLocalId");
b.ToTable("LocationImports", (string)null);
});
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
{
b.Property<int>("LocalId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Imported")
.HasColumnType("INTEGER");
b.Property<bool>("Seen")
.HasColumnType("INTEGER");
b.Property<string>("SinceVersion")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ushort>("TerritoryType")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<float>("X")
.HasColumnType("REAL");
b.Property<float>("Y")
.HasColumnType("REAL");
b.Property<float>("Z")
.HasColumnType("REAL");
b.HasKey("LocalId");
b.ToTable("Locations");
});
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("ExportedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedAt")
.HasColumnType("TEXT");
b.Property<string>("RemoteUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Imports");
});
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccountId")
.IsRequired()
.HasMaxLength(13)
.HasColumnType("TEXT");
b.Property<int>("ClientLocationId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ClientLocationId");
b.ToTable("RemoteEncounters");
});
modelBuilder.Entity("ClientLocationImportHistory", b =>
{
b.HasOne("Pal.Client.Database.ImportHistory", null)
.WithMany()
.HasForeignKey("ImportedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Pal.Client.Database.ClientLocation", null)
.WithMany()
.HasForeignKey("ImportedLocationsLocalId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
{
b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation")
.WithMany("RemoteEncounters")
.HasForeignKey("ClientLocationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ClientLocation");
});
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
{
b.Navigation("RemoteEncounters");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Pal.Client.Database.Migrations
{
/// <inheritdoc />
public partial class AddImportedAndSinceVersionToClientLocation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Imported",
table: "Locations",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "SinceVersion",
table: "Locations",
type: "TEXT",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Imported",
table: "Locations");
migrationBuilder.DropColumn(
name: "SinceVersion",
table: "Locations");
}
}
}

View File

@ -38,9 +38,16 @@ namespace Pal.Client.Database.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool>("Imported")
.HasColumnType("INTEGER");
b.Property<bool>("Seen") b.Property<bool>("Seen")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("SinceVersion")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ushort>("TerritoryType") b.Property<ushort>("TerritoryType")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -58,7 +65,7 @@ namespace Pal.Client.Database.Migrations
b.HasKey("LocalId"); b.HasKey("LocalId");
b.ToTable("Locations", (string)null); b.ToTable("Locations");
}); });
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
@ -78,7 +85,7 @@ namespace Pal.Client.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Imports", (string)null); b.ToTable("Imports");
}); });
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
@ -99,7 +106,7 @@ namespace Pal.Client.Database.Migrations
b.HasIndex("ClientLocationId"); b.HasIndex("ClientLocationId");
b.ToTable("RemoteEncounters", (string)null); b.ToTable("RemoteEncounters");
}); });
modelBuilder.Entity("ClientLocationImportHistory", b => modelBuilder.Entity("ClientLocationImportHistory", b =>
@ -120,13 +127,18 @@ namespace Pal.Client.Database.Migrations
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
{ {
b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation")
.WithMany() .WithMany("RemoteEncounters")
.HasForeignKey("ClientLocationId") .HasForeignKey("ClientLocationId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ClientLocation"); b.Navigation("ClientLocation");
}); });
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
{
b.Navigation("RemoteEncounters");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -1,15 +0,0 @@
using System.Collections.Concurrent;
namespace Pal.Client.DependencyInjection
{
internal sealed class FloorService
{
public ConcurrentDictionary<ushort, LocalState> FloorMarkers { get; } = new();
public ConcurrentBag<Marker> EphemeralMarkers { get; set; } = new();
public LocalState GetFloorMarkers(ushort territoryType)
{
return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt));
}
}
}

View File

@ -14,9 +14,11 @@ using ImGuiNET;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Floors;
using Pal.Client.Net; using Pal.Client.Net;
using Pal.Client.Rendering; using Pal.Client.Rendering;
using Pal.Client.Scheduled; using Pal.Client.Scheduled;
using Pal.Common;
namespace Pal.Client.DependencyInjection namespace Pal.Client.DependencyInjection
{ {
@ -84,44 +86,45 @@ namespace Pal.Client.DependencyInjection
try try
{ {
bool recreateLayout = false; bool recreateLayout = false;
bool saveMarkers = false;
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
HandleQueued(queued, ref recreateLayout, ref saveMarkers); HandleQueued(queued, ref recreateLayout);
if (_territoryState.LastTerritory != _clientState.TerritoryType) if (_territoryState.LastTerritory != _clientState.TerritoryType)
{ {
_territoryState.LastTerritory = _clientState.TerritoryType; _territoryState.LastTerritory = _clientState.TerritoryType;
_territoryState.TerritorySyncState = SyncState.NotAttempted; _territoryState.TerritorySyncState = ESyncState.NotAttempted;
NextUpdateObjects.Clear(); NextUpdateObjects.Clear();
if (_territoryState.IsInDeepDungeon()) _floorService.ChangeTerritory(_territoryState.LastTerritory);
_floorService.GetFloorMarkers(_territoryState.LastTerritory);
_floorService.EphemeralMarkers.Clear();
_territoryState.PomanderOfSight = PomanderState.Inactive; _territoryState.PomanderOfSight = PomanderState.Inactive;
_territoryState.PomanderOfIntuition = PomanderState.Inactive; _territoryState.PomanderOfIntuition = PomanderState.Inactive;
recreateLayout = true; recreateLayout = true;
_debugState.Reset(); _debugState.Reset();
} }
if (!_territoryState.IsInDeepDungeon()) if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory))
return; return;
if (_configuration.Mode == EMode.Online && _territoryState.TerritorySyncState == SyncState.NotAttempted) if (_configuration.Mode == EMode.Online &&
_territoryState.TerritorySyncState == ESyncState.NotAttempted)
{ {
_territoryState.TerritorySyncState = SyncState.Started; _territoryState.TerritorySyncState = ESyncState.Started;
Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory)); Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory));
} }
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
HandleQueued(queued, ref recreateLayout, ref saveMarkers); HandleQueued(queued, ref recreateLayout);
var currentFloor = _floorService.GetFloorMarkers(_territoryState.LastTerritory); (IReadOnlyList<PersistentLocation> visiblePersistentMarkers,
IReadOnlyList<EphemeralLocation> visibleEphemeralMarkers) =
GetRelevantGameObjects();
IList<Marker> visibleMarkers = GetRelevantGameObjects(); ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory;
HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout);
recreateLayout);
HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout))
RecreateEphemeralLayout();
} }
catch (Exception e) catch (Exception e)
{ {
@ -131,142 +134,116 @@ namespace Pal.Client.DependencyInjection
#region Render Markers #region Render Markers
private void HandlePersistentMarkers(LocalState currentFloor, IList<Marker> visibleMarkers, bool saveMarkers, private void HandlePersistentLocations(ETerritoryType territoryType,
IReadOnlyList<PersistentLocation> visiblePersistentMarkers,
bool recreateLayout) bool recreateLayout)
{ {
var currentFloorMarkers = currentFloor.Markers; bool recreatePersistentLocations = _floorService.MergePersistentLocations(
territoryType,
bool updateSeenMarkers = false; visiblePersistentMarkers,
var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); recreateLayout,
foreach (var visibleMarker in visibleMarkers) out List<PersistentLocation> locationsToSync);
recreatePersistentLocations |= CheckLocationsForPomanders(visiblePersistentMarkers);
if (locationsToSync.Count > 0)
{ {
Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); Task.Run(async () =>
if (knownMarker != null) await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, locationsToSync));
{
if (!knownMarker.Seen)
{
knownMarker.Seen = true;
saveMarkers = true;
} }
// This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, UploadLocations();
// markers discovered afterwards are automatically marked seen.
if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } &&
!knownMarker.RemoteSeenOn.Contains(partialAccountId))
updateSeenMarkers = true;
continue; if (recreatePersistentLocations)
RecreatePersistentLayout(visiblePersistentMarkers);
} }
currentFloorMarkers.Add(visibleMarker); private bool CheckLocationsForPomanders(IReadOnlyList<PersistentLocation> visibleLocations)
recreateLayout = true; {
saveMarkers = true; MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
} if (memoryTerritory is { Locations.Count: > 0 } &&
if (!recreateLayout && currentFloorMarkers.Count > 0 &&
(_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander))
{ {
try try
{ {
foreach (var marker in currentFloorMarkers) foreach (var location in memoryTerritory.Locations)
{ {
uint desiredColor = DetermineColor(marker, visibleMarkers); uint desiredColor = DetermineColor(location, visibleLocations);
if (marker.RenderElement == null || !marker.RenderElement.IsValid) if (location.RenderElement == null || !location.RenderElement.IsValid)
{ return true;
recreateLayout = true;
break;
}
if (marker.RenderElement.Color != desiredColor) if (location.RenderElement.Color != desiredColor)
marker.RenderElement.Color = desiredColor; location.RenderElement.Color = desiredColor;
} }
} }
catch (Exception e) catch (Exception e)
{ {
_debugState.SetFromException(e); _debugState.SetFromException(e);
recreateLayout = true; return true;
} }
} }
if (updateSeenMarkers && partialAccountId != null) return false;
{
var markersToUpdate = currentFloorMarkers.Where(x =>
x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } &&
!x.RemoteSeenOn.Contains(partialAccountId)).ToList();
foreach (var marker in markersToUpdate)
marker.RemoteSeenRequested = true;
Task.Run(async () => await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, markersToUpdate));
} }
if (saveMarkers) private void UploadLocations()
{ {
currentFloor.Save(); MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory == null)
return;
if (_territoryState.TerritorySyncState == SyncState.Complete) List<PersistentLocation> locationsToUpload = memoryTerritory.Locations
.Where(loc => loc.NetworkId == null && loc.UploadRequested == false)
.ToList();
if (locationsToUpload.Count > 0)
{ {
var markersToUpload = currentFloorMarkers foreach (var location in locationsToUpload)
.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); location.UploadRequested = true;
if (markersToUpload.Count > 0)
{
foreach (var marker in markersToUpload)
marker.UploadRequested = true;
Task.Run(async () => Task.Run(async () =>
await UploadMarkersForTerritory(_territoryState.LastTerritory, markersToUpload)); await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload));
}
} }
} }
if (recreateLayout) private void RecreatePersistentLayout(IReadOnlyList<PersistentLocation> visibleMarkers)
{ {
_renderAdapter.ResetLayer(ELayer.TrapHoard); _renderAdapter.ResetLayer(ELayer.TrapHoard);
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory == null)
return;
List<IRenderElement> elements = new(); List<IRenderElement> elements = new();
foreach (var marker in currentFloorMarkers) foreach (var location in memoryTerritory.Locations)
{ {
if (marker.Seen || _configuration.Mode == EMode.Online || if (location.Type == MemoryLocation.EType.Trap)
marker is { WasImported: true, Imports.Count: > 0 })
{ {
if (marker.Type == Marker.EType.Trap) CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers),
{
CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers),
_configuration.DeepDungeons.Traps); _configuration.DeepDungeons.Traps);
} }
else if (marker.Type == Marker.EType.Hoard) else if (location.Type == MemoryLocation.EType.Hoard)
{ {
CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers),
_configuration.DeepDungeons.HoardCoffers); _configuration.DeepDungeons.HoardCoffers);
} }
} }
}
if (elements.Count == 0) if (elements.Count == 0)
return; return;
_renderAdapter.SetLayer(ELayer.TrapHoard, elements); _renderAdapter.SetLayer(ELayer.TrapHoard, elements);
} }
}
private void HandleEphemeralMarkers(IList<Marker> visibleMarkers, bool recreateLayout) private void RecreateEphemeralLayout()
{
recreateLayout |=
_floorService.EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker));
recreateLayout |=
visibleMarkers.Any(visibleMarker => _floorService.EphemeralMarkers.All(x => x != visibleMarker));
if (recreateLayout)
{ {
_renderAdapter.ResetLayer(ELayer.RegularCoffers); _renderAdapter.ResetLayer(ELayer.RegularCoffers);
_floorService.EphemeralMarkers.Clear();
List<IRenderElement> elements = new(); List<IRenderElement> elements = new();
foreach (var marker in visibleMarkers) foreach (var location in _floorService.EphemeralLocations)
{ {
_floorService.EphemeralMarkers.Add(marker); if (location.Type == MemoryLocation.EType.SilverCoffer &&
_configuration.DeepDungeons.SilverCoffers.Show)
if (marker.Type == Marker.EType.SilverCoffer && _configuration.DeepDungeons.SilverCoffers.Show)
{ {
CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), CreateRenderElement(location, elements, DetermineColor(location),
_configuration.DeepDungeons.SilverCoffers); _configuration.DeepDungeons.SilverCoffers);
} }
} }
@ -276,38 +253,42 @@ namespace Pal.Client.DependencyInjection
_renderAdapter.SetLayer(ELayer.RegularCoffers, elements); _renderAdapter.SetLayer(ELayer.RegularCoffers, elements);
} }
}
private uint DetermineColor(Marker marker, IList<Marker> visibleMarkers) private uint DetermineColor(PersistentLocation location, IReadOnlyList<PersistentLocation> visibleLocations)
{ {
switch (marker.Type) switch (location.Type)
{ {
case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive || case MemoryLocation.EType.Trap
when _territoryState.PomanderOfSight == PomanderState.Inactive ||
!_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
visibleMarkers.Any(x => x == marker): visibleLocations.Any(x => x == location):
return _configuration.DeepDungeons.Traps.Color; return _configuration.DeepDungeons.Traps.Color;
case Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive || case MemoryLocation.EType.Hoard
when _territoryState.PomanderOfIntuition == PomanderState.Inactive ||
!_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander ||
visibleMarkers.Any(x => x == marker): visibleLocations.Any(x => x == location):
return _configuration.DeepDungeons.HoardCoffers.Color; return _configuration.DeepDungeons.HoardCoffers.Color;
case Marker.EType.SilverCoffer:
return _configuration.DeepDungeons.SilverCoffers.Color;
case Marker.EType.Trap:
case Marker.EType.Hoard:
return RenderData.ColorInvisible;
default: default:
return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); return RenderData.ColorInvisible;
} }
} }
private void CreateRenderElement(Marker marker, List<IRenderElement> elements, uint color, private uint DetermineColor(EphemeralLocation location)
{
if (location.Type == MemoryLocation.EType.SilverCoffer)
return _configuration.DeepDungeons.SilverCoffers.Color;
return RenderData.ColorInvisible;
}
private void CreateRenderElement(MemoryLocation location, List<IRenderElement> elements, uint color,
MarkerConfiguration config) MarkerConfiguration config)
{ {
if (!config.Show) if (!config.Show)
return; return;
var element = _renderAdapter.CreateElement(marker.Type, marker.Position, color, config.Fill); var element = _renderAdapter.CreateElement(location.Type, location.Position, color, config.Fill);
marker.RenderElement = element; location.RenderElement = element;
elements.Add(element); elements.Add(element);
} }
@ -325,7 +306,7 @@ namespace Pal.Client.DependencyInjection
Type = SyncType.Download, Type = SyncType.Download,
TerritoryType = territoryId, TerritoryType = territoryId,
Success = success, Success = success,
Markers = downloadedMarkers Locations = downloadedMarkers
}); });
} }
catch (Exception e) catch (Exception e)
@ -334,17 +315,17 @@ namespace Pal.Client.DependencyInjection
} }
} }
private async Task UploadMarkersForTerritory(ushort territoryId, List<Marker> markersToUpload) private async Task UploadLocationsForTerritory(ushort territoryId, List<PersistentLocation> locationsToUpload)
{ {
try try
{ {
var (success, uploadedMarkers) = await _remoteApi.UploadMarker(territoryId, markersToUpload); var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
LateEventQueue.Enqueue(new QueuedSyncResponse LateEventQueue.Enqueue(new QueuedSyncResponse
{ {
Type = SyncType.Upload, Type = SyncType.Upload,
TerritoryType = territoryId, TerritoryType = territoryId,
Success = success, Success = success,
Markers = uploadedMarkers Locations = uploadedLocations
}); });
} }
catch (Exception e) catch (Exception e)
@ -353,17 +334,18 @@ namespace Pal.Client.DependencyInjection
} }
} }
private async Task SyncSeenMarkersForTerritory(ushort territoryId, List<Marker> markersToUpdate) private async Task SyncSeenMarkersForTerritory(ushort territoryId,
IReadOnlyList<PersistentLocation> locationsToUpdate)
{ {
try try
{ {
var success = await _remoteApi.MarkAsSeen(territoryId, markersToUpdate); var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate);
LateEventQueue.Enqueue(new QueuedSyncResponse LateEventQueue.Enqueue(new QueuedSyncResponse
{ {
Type = SyncType.MarkSeen, Type = SyncType.MarkSeen,
TerritoryType = territoryId, TerritoryType = territoryId,
Success = success, Success = success,
Markers = markersToUpdate, Locations = locationsToUpdate,
}); });
} }
catch (Exception e) catch (Exception e)
@ -374,9 +356,10 @@ namespace Pal.Client.DependencyInjection
#endregion #endregion
private IList<Marker> GetRelevantGameObjects() private (IReadOnlyList<PersistentLocation>, IReadOnlyList<EphemeralLocation>) GetRelevantGameObjects()
{ {
List<Marker> result = new(); List<PersistentLocation> persistentLocations = new();
List<EphemeralLocation> ephemeralLocations = new();
for (int i = 246; i < _objectTable.Length; i++) for (int i = 246; i < _objectTable.Length; i++)
{ {
GameObject? obj = _objectTable[i]; GameObject? obj = _objectTable[i];
@ -391,16 +374,31 @@ namespace Pal.Client.DependencyInjection
case 2007185: case 2007185:
case 2007186: case 2007186:
case 2009504: case 2009504:
result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); persistentLocations.Add(new PersistentLocation
{
Type = MemoryLocation.EType.Trap,
Position = obj.Position,
Seen = true
});
break; break;
case 2007542: case 2007542:
case 2007543: case 2007543:
result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true }); persistentLocations.Add(new PersistentLocation
{
Type = MemoryLocation.EType.Hoard,
Position = obj.Position,
Seen = true
});
break; break;
case 2007357: case 2007357:
result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true }); ephemeralLocations.Add(new EphemeralLocation
{
Type = MemoryLocation.EType.SilverCoffer,
Position = obj.Position,
Seen = true
});
break; break;
} }
} }
@ -409,18 +407,25 @@ namespace Pal.Client.DependencyInjection
{ {
var obj = _objectTable.FirstOrDefault(x => x.Address == address); var obj = _objectTable.FirstOrDefault(x => x.Address == address);
if (obj != null && obj.Position.Length() > 0.1) if (obj != null && obj.Position.Length() > 0.1)
result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); {
persistentLocations.Add(new PersistentLocation
{
Type = MemoryLocation.EType.Trap,
Position = obj.Position,
Seen = true,
});
}
} }
return result; return (persistentLocations, ephemeralLocations);
} }
private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout)
{ {
Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType()); Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType());
var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType); var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType);
handler.RunIfCompatible(queued, ref recreateLayout, ref saveMarkers); handler.RunIfCompatible(queued, ref recreateLayout);
} }
} }
} }

View File

@ -3,21 +3,28 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Account;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database; using Pal.Client.Database;
using Pal.Client.Floors;
using Pal.Client.Floors.Tasks;
using Pal.Common;
namespace Pal.Client.DependencyInjection namespace Pal.Client.DependencyInjection
{ {
internal sealed class ImportService internal sealed class ImportService
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly FloorService _floorService;
public ImportService(IServiceProvider serviceProvider) public ImportService(IServiceProvider serviceProvider, FloorService floorService)
{ {
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_floorService = floorService;
} }
/*
public void Add(ImportHistory history) public void Add(ImportHistory history)
{ {
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
@ -26,6 +33,7 @@ namespace Pal.Client.DependencyInjection
dbContext.Imports.Add(history); dbContext.Imports.Add(history);
dbContext.SaveChanges(); dbContext.SaveChanges();
} }
*/
public async Task<ImportHistory?> FindLast(CancellationToken token = default) public async Task<ImportHistory?> FindLast(CancellationToken token = default)
{ {
@ -35,6 +43,7 @@ namespace Pal.Client.DependencyInjection
return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefaultAsync(cancellationToken: token); return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefaultAsync(cancellationToken: token);
} }
/*
public List<ImportHistory> FindForServer(string server) public List<ImportHistory> FindForServer(string server)
{ {
if (string.IsNullOrEmpty(server)) if (string.IsNullOrEmpty(server))
@ -44,18 +53,58 @@ namespace Pal.Client.DependencyInjection
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList(); return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList();
} }*/
public void RemoveAllByIds(List<Guid> ids) public (int traps, int hoard) Import(ExportRoot import)
{ {
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.RemoveRange(dbContext.Imports.Where(x => ids.Contains(x.Id))); dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList());
ImportHistory importHistory = new ImportHistory
{
Id = Guid.Parse(import.ExportId),
RemoteUrl = import.ServerUrl,
ExportedAt = import.CreatedAt.ToDateTime(),
ImportedAt = DateTime.UtcNow,
};
dbContext.Imports.Add(importHistory);
int traps = 0;
int hoard = 0;
foreach (var floor in import.Floors)
{
ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType;
List<PersistentLocation> existingLocations = dbContext.Locations
.Where(loc => loc.TerritoryType == floor.TerritoryType)
.ToList()
.Select(LoadTerritory.ToMemoryLocation)
.ToList();
foreach (var newLocation in floor.Objects)
{
throw new NotImplementedException();
}
}
// TODO filter here, update territories
dbContext.SaveChanges(); dbContext.SaveChanges();
_floorService.ResetAll();
return (traps, hoard);
} }
public void RemoveById(Guid id) public void RemoveById(Guid id)
=> RemoveAllByIds(new List<Guid> { id }); {
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id));
// TODO filter here, update territories
dbContext.SaveChanges();
_floorService.ResetAll();
}
} }
} }

View File

@ -17,7 +17,7 @@ namespace Pal.Client.DependencyInjection
} }
public ushort LastTerritory { get; set; } public ushort LastTerritory { get; set; }
public SyncState TerritorySyncState { get; set; } public ESyncState TerritorySyncState { get; set; }
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;

View File

@ -24,6 +24,7 @@ using Pal.Client.Database;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.DependencyInjection.Logging; using Pal.Client.DependencyInjection.Logging;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Floors;
using Pal.Client.Net; using Pal.Client.Net;
using Pal.Client.Properties; using Pal.Client.Properties;
using Pal.Client.Rendering; using Pal.Client.Rendering;
@ -63,7 +64,8 @@ namespace Pal.Client
CommandManager commandManager, CommandManager commandManager,
DataManager dataManager) DataManager dataManager)
{ {
_logger.LogInformation("Building service container"); _logger.LogInformation("Building service container for {Assembly}",
typeof(DependencyInjectionContext).Assembly.FullName);
// set up legacy services // set up legacy services
#pragma warning disable CS0612 #pragma warning disable CS0612

View File

@ -0,0 +1,24 @@
using System;
namespace Pal.Client.Floors
{
/// <summary>
/// This is a currently-visible marker.
/// </summary>
internal sealed class EphemeralLocation : MemoryLocation
{
public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj);
public override int GetHashCode() => base.GetHashCode();
public static bool operator ==(EphemeralLocation? a, object? b)
{
return Equals(a, b);
}
public static bool operator !=(EphemeralLocation? a, object? b)
{
return !Equals(a, b);
}
}
}

View File

@ -0,0 +1,147 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Configuration;
using Pal.Client.Extensions;
using Pal.Client.Floors.Tasks;
using Pal.Client.Net;
using Pal.Common;
namespace Pal.Client.Floors
{
internal sealed class FloorService
{
private readonly IPalacePalConfiguration _configuration;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IReadOnlyDictionary<ETerritoryType, MemoryTerritory> _territories;
private ConcurrentBag<EphemeralLocation> _ephemeralLocations = new();
public FloorService(IPalacePalConfiguration configuration, IServiceScopeFactory serviceScopeFactory)
{
_configuration = configuration;
_serviceScopeFactory = serviceScopeFactory;
_territories = Enum.GetValues<ETerritoryType>().ToDictionary(o => o, o => new MemoryTerritory(o));
}
public IReadOnlyCollection<EphemeralLocation> EphemeralLocations => _ephemeralLocations;
public void ChangeTerritory(ushort territoryType)
{
_ephemeralLocations = new ConcurrentBag<EphemeralLocation>();
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
ChangeTerritory((ETerritoryType)territoryType);
}
private void ChangeTerritory(ETerritoryType newTerritory)
{
var territory = _territories[newTerritory];
if (!territory.IsReady && !territory.IsLoading)
{
territory.IsLoading = true;
new LoadTerritory(_serviceScopeFactory, territory).Start();
}
}
public MemoryTerritory? GetTerritoryIfReady(ushort territoryType)
{
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
return GetTerritoryIfReady((ETerritoryType)territoryType);
return null;
}
public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType)
{
var territory = _territories[territoryType];
if (!territory.IsReady)
return null;
return territory;
}
public bool IsReady(ushort territoryId) => GetTerritoryIfReady(territoryId) != null;
public bool MergePersistentLocations(
ETerritoryType territoryType,
IReadOnlyList<PersistentLocation> visibleLocations,
bool recreateLayout,
out List<PersistentLocation> locationsToSync)
{
MemoryTerritory? territory = GetTerritoryIfReady(territoryType);
locationsToSync = new();
if (territory == null)
return false;
var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
var persistentLocations = territory.Locations.ToList();
List<PersistentLocation> markAsSeen = new();
List<PersistentLocation> newLocations = new();
foreach (var visibleLocation in visibleLocations)
{
PersistentLocation? existingLocation = persistentLocations.SingleOrDefault(x => x == visibleLocation);
if (existingLocation != null)
{
if (existingLocation is { Seen: false, LocalId: { } })
{
existingLocation.Seen = true;
markAsSeen.Add(existingLocation);
}
// This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states,
// markers discovered afterwards are automatically marked seen.
if (partialAccountId != null &&
existingLocation is { LocalId: { }, NetworkId: { }, RemoteSeenRequested: false } &&
!existingLocation.RemoteSeenOn.Contains(partialAccountId))
{
existingLocation.RemoteSeenRequested = true;
locationsToSync.Add(existingLocation);
}
continue;
}
territory.Locations.Add(visibleLocation);
newLocations.Add(visibleLocation);
recreateLayout = true;
}
if (markAsSeen.Count > 0)
new MarkAsSeen(_serviceScopeFactory, territory, markAsSeen).Start();
if (newLocations.Count > 0)
new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start();
return recreateLayout;
}
/// <returns>Whether the locations have changed</returns>
public bool MergeEphemeralLocations(IReadOnlyList<EphemeralLocation> visibleLocations, bool recreate)
{
recreate |= _ephemeralLocations.Any(loc => visibleLocations.All(x => x != loc));
recreate |= visibleLocations.Any(loc => _ephemeralLocations.All(x => x != loc));
if (!recreate)
return false;
_ephemeralLocations.Clear();
foreach (var visibleLocation in visibleLocations)
_ephemeralLocations.Add(visibleLocation);
return true;
}
public void ResetAll()
{
foreach (var memoryTerritory in _territories.Values)
{
lock (memoryTerritory.LockObj)
memoryTerritory.Reset();
}
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Pal.Client.Rendering;
using Pal.Common;
using Palace;
namespace Pal.Client.Floors
{
/// <summary>
/// Base class for <see cref="MemoryLocation"/> and <see cref="EphemeralLocation"/>.
/// </summary>
internal abstract class MemoryLocation
{
public required EType Type { get; init; }
public required Vector3 Position { get; init; }
public bool Seen { get; set; }
public IRenderElement? RenderElement { get; set; }
public enum EType
{
Unknown,
Hoard,
Trap,
SilverCoffer,
}
public override bool Equals(object? obj)
{
return obj is MemoryLocation otherLocation &&
Type == otherLocation.Type &&
PalaceMath.IsNearlySamePosition(Position, otherLocation.Position);
}
public override int GetHashCode()
{
return HashCode.Combine(Type, PalaceMath.GetHashCode(Position));
}
}
internal static class ETypeExtensions
{
public static MemoryLocation.EType ToMemoryType(this ObjectType objectType)
{
return objectType switch
{
ObjectType.Trap => MemoryLocation.EType.Trap,
ObjectType.Hoard => MemoryLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null)
};
}
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Pal.Client.Configuration;
using Pal.Common;
namespace Pal.Client.Floors
{
/// <summary>
/// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60.
/// </summary>
internal sealed class MemoryTerritory
{
public MemoryTerritory(ETerritoryType territoryType)
{
TerritoryType = territoryType;
}
public ETerritoryType TerritoryType { get; }
public bool IsReady { get; set; }
public bool IsLoading { get; set; }
public ConcurrentBag<PersistentLocation> Locations { get; } = new();
public object LockObj { get; } = new();
public void Initialize(IEnumerable<PersistentLocation> locations)
{
Locations.Clear();
foreach (var location in locations)
Locations.Add(location);
IsReady = true;
IsLoading = false;
}
public IEnumerable<PersistentLocation> GetRemovableLocations(EMode mode)
{
// TODO there was better logic here;
return Locations.Where(x => !x.Seen);
}
public void Reset()
{
Locations.Clear();
IsReady = false;
IsLoading = false;
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using Pal.Client.Database;
namespace Pal.Client.Floors
{
/// <summary>
/// A <see cref="ClientLocation"/> loaded in memory, with certain extra attributes as needed.
/// </summary>
internal sealed class PersistentLocation : MemoryLocation
{
/// <see cref="ClientLocation.LocalId"/>
public int? LocalId { get; set; }
/// <summary>
/// Network id for the server you're currently connected to.
/// </summary>
public Guid? NetworkId { get; set; }
/// <summary>
/// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests.
/// </summary>
public bool UploadRequested { get; set; }
/// <see cref="ClientLocation.RemoteEncounters"/>
///
public List<string> RemoteSeenOn { get; set; } = new();
/// <summary>
/// Whether this marker was requested to be seen, to avoid duplicate requests.
/// </summary>
public bool RemoteSeenRequested { get; set; }
public override bool Equals(object? obj) => obj is PersistentLocation && base.Equals(obj);
public override int GetHashCode() => base.GetHashCode();
public static bool operator ==(PersistentLocation? a, object? b)
{
return Equals(a, b);
}
public static bool operator !=(PersistentLocation? a, object? b)
{
return !Equals(a, b);
}
}
}

View File

@ -0,0 +1,29 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal abstract class DbTask
{
private readonly IServiceScopeFactory _serviceScopeFactory;
protected DbTask(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public void Start()
{
Task.Run(() =>
{
using var scope = _serviceScopeFactory.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
Run(dbContext);
});
}
protected abstract void Run(PalClientContext dbContext);
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal sealed class LoadTerritory : DbTask
{
private readonly MemoryTerritory _territory;
public LoadTerritory(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory)
: base(serviceScopeFactory)
{
_territory = territory;
}
protected override void Run(PalClientContext dbContext)
{
lock (_territory.LockObj)
{
if (_territory.IsReady)
return;
List<ClientLocation> locations = dbContext.Locations
.Where(o => o.TerritoryType == (ushort)_territory.TerritoryType)
.Include(o => o.ImportedBy)
.Include(o => o.RemoteEncounters)
.ToList();
_territory.Initialize(locations.Select(ToMemoryLocation));
}
}
public static PersistentLocation ToMemoryLocation(ClientLocation location)
{
return new PersistentLocation
{
LocalId = location.LocalId,
Type = ToMemoryLocationType(location.Type),
Position = new Vector3(location.X, location.Y, location.Z),
Seen = location.Seen,
RemoteSeenOn = location.RemoteEncounters.Select(o => o.AccountId).ToList(),
};
}
private static MemoryLocation.EType ToMemoryLocationType(ClientLocation.EType type)
{
return type switch
{
ClientLocation.EType.Trap => MemoryLocation.EType.Trap,
ClientLocation.EType.Hoard => MemoryLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
}
}

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal sealed class MarkAsSeen : DbTask
{
private readonly MemoryTerritory _territory;
private readonly IReadOnlyList<PersistentLocation> _locations;
public MarkAsSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
IReadOnlyList<PersistentLocation> locations)
: base(serviceScopeFactory)
{
_territory = territory;
_locations = locations;
}
protected override void Run(PalClientContext dbContext)
{
lock (_territory.LockObj)
{
dbContext.Locations
.Where(loc => _locations.Any(l => l.LocalId == loc.LocalId))
.ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true));
dbContext.SaveChanges();
}
}
}
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal sealed class MarkRemoteSeen : DbTask
{
private readonly MemoryTerritory _territory;
private readonly IReadOnlyList<PersistentLocation> _locations;
private readonly string _accountId;
public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory,
MemoryTerritory territory,
IReadOnlyList<PersistentLocation> locations,
string accountId)
: base(serviceScopeFactory)
{
_territory = territory;
_locations = locations;
_accountId = accountId;
}
protected override void Run(PalClientContext dbContext)
{
lock (_territory.LockObj)
{
List<ClientLocation> locationsToUpdate = dbContext.Locations
.Where(loc => _locations.Any(l =>
l.LocalId == loc.LocalId && loc.RemoteEncounters.All(r => r.AccountId != _accountId)))
.ToList();
foreach (var clientLocation in locationsToUpdate)
clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId));
dbContext.SaveChanges();
}
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
using Pal.Common;
namespace Pal.Client.Floors.Tasks
{
internal sealed class SaveNewLocations : DbTask
{
private readonly MemoryTerritory _territory;
private readonly List<PersistentLocation> _newLocations;
public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
List<PersistentLocation> newLocations)
: base(serviceScopeFactory)
{
_territory = territory;
_newLocations = newLocations;
}
protected override void Run(PalClientContext dbContext)
{
Run(_territory, dbContext, _newLocations);
}
public static void Run(MemoryTerritory territory, PalClientContext dbContext,
List<PersistentLocation> locations)
{
lock (territory.LockObj)
{
Dictionary<PersistentLocation, ClientLocation> mapping =
locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType));
dbContext.Locations.AddRange(mapping.Values);
dbContext.SaveChanges();
foreach ((PersistentLocation persistentLocation, ClientLocation clientLocation) in mapping)
{
persistentLocation.LocalId = clientLocation.LocalId;
}
}
}
private static ClientLocation ToDatabaseLocation(PersistentLocation location, ETerritoryType territoryType)
{
return new ClientLocation
{
TerritoryType = (ushort)territoryType,
Type = ToDatabaseType(location.Type),
X = location.Position.X,
Y = location.Position.Y,
Z = location.Position.Z,
Seen = location.Seen,
SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2),
};
}
private static ClientLocation.EType ToDatabaseType(MemoryLocation.EType type)
{
return type switch
{
MemoryLocation.EType.Trap => ClientLocation.EType.Trap,
MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
}
}

View File

@ -1,159 +0,0 @@
using Pal.Common;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Pal.Client.Configuration;
using Pal.Client.Extensions;
namespace Pal.Client
{
/// <summary>
/// JSON for a single floor set (e.g. 51-60).
/// </summary>
internal sealed class LocalState
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
private const int CurrentVersion = 4;
internal static string PluginConfigDirectory { get; set; } = null!;
internal static EMode Mode { get; set; }
public uint TerritoryType { get; set; }
public ConcurrentBag<Marker> Markers { get; set; } = new();
public LocalState(uint territoryType)
{
TerritoryType = territoryType;
}
private void ApplyFilters()
{
if (Mode == EMode.Offline)
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)
{
string path = GetSaveLocation(territoryType);
if (!File.Exists(path))
return null;
string content = File.ReadAllText(path);
if (content.Length == 0)
return null;
LocalState localState;
int version = 1;
if (content[0] == '[')
{
// v1 only had a list of markers, not a JSON object as root
localState = new LocalState(territoryType)
{
Markers = new ConcurrentBag<Marker>(JsonSerializer.Deserialize<HashSet<Marker>>(content, JsonSerializerOptions) ?? new()),
};
}
else
{
var save = JsonSerializer.Deserialize<SaveFile>(content, JsonSerializerOptions);
if (save == null)
return null;
localState = new LocalState(territoryType)
{
Markers = new ConcurrentBag<Marker>(save.Markers.Where(o => o.Type == Marker.EType.Trap || o.Type == Marker.EType.Hoard)),
};
version = save.Version;
}
localState.ApplyFilters();
if (version <= 3)
{
foreach (var marker in localState.Markers)
marker.RemoteSeenOn = marker.RemoteSeenOn.Select(x => x.ToPartialId()).ToList();
}
if (version < CurrentVersion)
localState.Save();
return localState;
}
public void Save()
{
string path = GetSaveLocation(TerritoryType);
ApplyFilters();
SaveImpl(path);
}
public void Backup(string suffix)
{
string path = $"{GetSaveLocation(TerritoryType)}.{suffix}";
if (!File.Exists(path))
{
SaveImpl(path);
}
}
private void SaveImpl(string path)
{
foreach (var marker in Markers)
{
if (string.IsNullOrEmpty(marker.SinceVersion))
marker.SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2);
}
if (Markers.Count == 0)
File.Delete(path);
else
{
File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile
{
Version = CurrentVersion,
Markers = new HashSet<Marker>(Markers)
}, JsonSerializerOptions));
}
}
public string GetSaveLocation() => GetSaveLocation(TerritoryType);
private static string GetSaveLocation(uint territoryType) => Path.Join(PluginConfigDirectory, $"{territoryType}.json");
public static void ForEach(Action<LocalState> action)
{
foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
{
LocalState? localState = Load((ushort)territory);
if (localState != null)
action(localState);
}
}
public static void UpdateAll()
{
ForEach(s => s.Save());
}
public void UndoImport(List<Guid> importIds)
{
// 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 Markers)
marker.Imports.RemoveAll(importIds.Contains);
}
public sealed class SaveFile
{
public int Version { get; set; }
public HashSet<Marker> Markers { get; set; } = new();
}
}
}

View File

@ -1,110 +0,0 @@
using ECommons.SplatoonAPI;
using Pal.Client.Rendering;
using Pal.Common;
using Palace;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text.Json.Serialization;
namespace Pal.Client
{
internal sealed class Marker
{
public EType Type { get; set; } = EType.Unknown;
public Vector3 Position { get; set; }
/// <summary>
/// Whether we have encountered the trap/coffer at this location in-game.
/// </summary>
public bool Seen { get; set; }
/// <summary>
/// Network id for the server you're currently connected to.
/// </summary>
[JsonIgnore]
public Guid? NetworkId { get; set; }
/// <summary>
/// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests.
/// </summary>
[JsonIgnore]
public bool UploadRequested { get; set; }
/// <summary>
/// Which account ids this marker was seen. This is a list merely to support different remote endpoints
/// (where each server would assign you a different id).
/// </summary>
public List<string> RemoteSeenOn { get; set; } = new();
/// <summary>
/// Whether this marker was requested to be seen, to avoid duplicate requests.
/// </summary>
[JsonIgnore]
public bool RemoteSeenRequested { get; set; }
/// <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();
public bool WasImported { get; set; }
/// <summary>
/// To make rollbacks of local data easier, keep track of the version which was used to write the marker initially.
/// </summary>
public string? SinceVersion { get; set; }
[JsonIgnore]
public IRenderElement? RenderElement { get; set; }
public Marker(EType type, Vector3 position, Guid? networkId = null)
{
Type = type;
Position = position;
NetworkId = networkId;
}
public override int GetHashCode()
{
return HashCode.Combine(Type, PalaceMath.GetHashCode(Position));
}
public override bool Equals(object? obj)
{
return obj is Marker otherMarker && Type == otherMarker.Type && PalaceMath.IsNearlySamePosition(Position, otherMarker.Position);
}
public static bool operator ==(Marker? a, object? b)
{
return Equals(a, b);
}
public static bool operator !=(Marker? a, object? b)
{
return !Equals(a, b);
}
public bool IsPermanent() => Type == EType.Trap || Type == EType.Hoard;
public enum EType
{
Unknown = ObjectType.Unknown,
#region Permanent Markers
Trap = ObjectType.Trap,
Hoard = ObjectType.Hoard,
[Obsolete]
Debug = 3,
#endregion
# region Markers that only show up if they're currently visible
SilverCoffer = 100,
#endregion
}
}
}

View File

@ -5,24 +5,25 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Pal.Client.Floors;
namespace Pal.Client.Net namespace Pal.Client.Net
{ {
internal partial class RemoteApi internal partial class RemoteApi
{ {
public async Task<(bool, List<Marker>)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) public async Task<(bool, List<PersistentLocation>)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default)
{ {
if (!await Connect(cancellationToken)) if (!await Connect(cancellationToken))
return (false, new()); return (false, new());
var palaceClient = new PalaceService.PalaceServiceClient(_channel); var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken);
return (downloadReply.Success, downloadReply.Objects.Select(CreateMarkerFromNetworkObject).ToList()); return (downloadReply.Success, downloadReply.Objects.Select(CreateLocationFromNetworkObject).ToList());
} }
public async Task<(bool, List<Marker>)> UploadMarker(ushort territoryType, IList<Marker> markers, CancellationToken cancellationToken = default) public async Task<(bool, List<PersistentLocation>)> UploadLocations(ushort territoryType, IReadOnlyList<PersistentLocation> locations, CancellationToken cancellationToken = default)
{ {
if (markers.Count == 0) if (locations.Count == 0)
return (true, new()); return (true, new());
if (!await Connect(cancellationToken)) if (!await Connect(cancellationToken))
@ -33,7 +34,7 @@ namespace Pal.Client.Net
{ {
TerritoryType = territoryType, TerritoryType = territoryType,
}; };
uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
{ {
Type = (ObjectType)m.Type, Type = (ObjectType)m.Type,
X = m.Position.X, X = m.Position.X,
@ -41,12 +42,12 @@ namespace Pal.Client.Net
Z = m.Position.Z Z = m.Position.Z
})); }));
var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken);
return (uploadReply.Success, uploadReply.Objects.Select(CreateMarkerFromNetworkObject).ToList()); return (uploadReply.Success, uploadReply.Objects.Select(CreateLocationFromNetworkObject).ToList());
} }
public async Task<bool> MarkAsSeen(ushort territoryType, IList<Marker> markers, CancellationToken cancellationToken = default) public async Task<bool> MarkAsSeen(ushort territoryType, IReadOnlyList<PersistentLocation> locations, CancellationToken cancellationToken = default)
{ {
if (markers.Count == 0) if (locations.Count == 0)
return true; return true;
if (!await Connect(cancellationToken)) if (!await Connect(cancellationToken))
@ -54,15 +55,22 @@ namespace Pal.Client.Net
var palaceClient = new PalaceService.PalaceServiceClient(_channel); var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType };
foreach (var marker in markers) foreach (var marker in locations)
seenRequest.NetworkIds.Add(marker.NetworkId.ToString()); seenRequest.NetworkIds.Add(marker.NetworkId.ToString());
var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
return seenReply.Success; return seenReply.Success;
} }
private Marker CreateMarkerFromNetworkObject(PalaceObject obj) => private PersistentLocation CreateLocationFromNetworkObject(PalaceObject obj)
new Marker((Marker.EType)obj.Type, new Vector3(obj.X, obj.Y, obj.Z), Guid.Parse(obj.NetworkId)); {
return new PersistentLocation
{
Type = obj.Type.ToMemoryType(),
Position = new Vector3(obj.X, obj.Y, obj.Z),
NetworkId = Guid.Parse(obj.NetworkId),
};
}
public async Task<(bool, List<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default) public async Task<(bool, List<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default)
{ {

View File

@ -13,7 +13,7 @@ namespace Pal.Client.Net
#if DEBUG #if DEBUG
public const string RemoteUrl = "http://localhost:5145"; public const string RemoteUrl = "http://localhost:5145";
#else #else
public const string RemoteUrl = "https://pal.liza.sh"; //public const string RemoteUrl = "https://pal.liza.sh";
#endif #endif
private readonly string _userAgent = private readonly string _userAgent =
$"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}";

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Floors;
namespace Pal.Client.Rendering namespace Pal.Client.Rendering
{ {
@ -12,7 +13,7 @@ namespace Pal.Client.Rendering
void ResetLayer(ELayer layer); void ResetLayer(ELayer layer);
IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false); IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false);
void DrawDebugItems(uint trapColor, uint hoardColor); void DrawDebugItems(uint trapColor, uint hoardColor);
} }

View File

@ -1,20 +1,23 @@
using System.Collections.Generic; using System.Collections.Generic;
using Pal.Client.Floors;
namespace Pal.Client.Rendering namespace Pal.Client.Rendering
{ {
internal sealed class MarkerConfig internal sealed class MarkerConfig
{ {
private static readonly MarkerConfig EmptyConfig = new(); private static readonly MarkerConfig EmptyConfig = new();
private static readonly Dictionary<Marker.EType, MarkerConfig> MarkerConfigs = new()
private static readonly Dictionary<MemoryLocation.EType, MarkerConfig> MarkerConfigs = new()
{ {
{ Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } }, { MemoryLocation.EType.Trap, new MarkerConfig { Radius = 1.7f } },
{ Marker.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } }, { MemoryLocation.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } },
{ Marker.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, { MemoryLocation.EType.SilverCoffer, new MarkerConfig { Radius = 1f } },
}; };
public float OffsetY { get; private init; } public float OffsetY { get; private init; }
public float Radius { get; private init; } = 0.25f; public float Radius { get; private init; } = 0.25f;
public static MarkerConfig ForType(Marker.EType type) => MarkerConfigs.GetValueOrDefault(type, EmptyConfig); public static MarkerConfig ForType(MemoryLocation.EType type) =>
MarkerConfigs.GetValueOrDefault(type, EmptyConfig);
} }
} }

View File

@ -4,6 +4,7 @@ using System.Numerics;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Floors;
namespace Pal.Client.Rendering namespace Pal.Client.Rendering
{ {
@ -56,7 +57,7 @@ namespace Pal.Client.Rendering
public void ResetLayer(ELayer layer) public void ResetLayer(ELayer layer)
=> _implementation.ResetLayer(layer); => _implementation.ResetLayer(layer);
public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
=> _implementation.CreateElement(type, pos, color, fill); => _implementation.CreateElement(type, pos, color, fill);
public ERenderer GetConfigValue() public ERenderer GetConfigValue()

View File

@ -9,6 +9,7 @@ using Dalamud.Game.ClientState;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Floors;
namespace Pal.Client.Rendering namespace Pal.Client.Rendering
{ {
@ -53,7 +54,7 @@ namespace Pal.Client.Rendering
l.Dispose(); l.Dispose();
} }
public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
{ {
var config = MarkerConfig.ForType(type); var config = MarkerConfig.ForType(type);
return new SimpleElement return new SimpleElement
@ -73,9 +74,13 @@ namespace Pal.Client.Rendering
TerritoryType = _clientState.TerritoryType, TerritoryType = _clientState.TerritoryType,
Elements = new List<SimpleElement> Elements = new List<SimpleElement>
{ {
(SimpleElement)CreateElement(Marker.EType.Trap, _clientState.LocalPlayer?.Position ?? default, (SimpleElement)CreateElement(
MemoryLocation.EType.Trap,
_clientState.LocalPlayer?.Position ?? default,
trapColor), trapColor),
(SimpleElement)CreateElement(Marker.EType.Hoard, _clientState.LocalPlayer?.Position ?? default, (SimpleElement)CreateElement(
MemoryLocation.EType.Hoard,
_clientState.LocalPlayer?.Position ?? default,
hoardColor) hoardColor)
}, },
ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout
@ -120,15 +125,15 @@ namespace Pal.Client.Rendering
switch (e.Type) switch (e.Type)
{ {
case Marker.EType.Hoard: case MemoryLocation.EType.Hoard:
// ignore distance if this is a found hoard coffer // ignore distance if this is a found hoard coffer
if (_territoryState.PomanderOfIntuition == PomanderState.Active && if (_territoryState.PomanderOfIntuition == PomanderState.Active &&
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)
break; break;
goto case Marker.EType.Trap; goto case MemoryLocation.EType.Trap;
case Marker.EType.Trap: case MemoryLocation.EType.Trap:
var playerPos = _clientState.LocalPlayer?.Position; var playerPos = _clientState.LocalPlayer?.Position;
if (playerPos == null) if (playerPos == null)
return; return;
@ -189,7 +194,7 @@ namespace Pal.Client.Rendering
public sealed class SimpleElement : IRenderElement public sealed class SimpleElement : IRenderElement
{ {
public bool IsValid { get; set; } = true; public bool IsValid { get; set; } = true;
public required Marker.EType Type { get; init; } public required MemoryLocation.EType Type { get; init; }
public required Vector3 Position { get; init; } public required Vector3 Position { get; init; }
public required uint Color { get; set; } public required uint Color { get; set; }
public required float Radius { get; init; } public required float Radius { get; init; }

View File

@ -13,6 +13,7 @@ using Dalamud.Game.ClientState;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Floors;
namespace Pal.Client.Rendering namespace Pal.Client.Rendering
{ {
@ -57,7 +58,8 @@ namespace Pal.Client.Rendering
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer, elements.Count); _logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer,
elements.Count);
_debugState.SetFromException(e); _debugState.SetFromException(e);
} }
}); });
@ -78,7 +80,7 @@ namespace Pal.Client.Rendering
private string ToLayerName(ELayer layer) private string ToLayerName(ELayer layer)
=> $"PalacePal.{layer}"; => $"PalacePal.{layer}";
public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
{ {
MarkerConfig config = MarkerConfig.ForType(type); MarkerConfig config = MarkerConfig.ForType(type);
Element element = new Element(ElementType.CircleAtFixedCoordinates) Element element = new Element(ElementType.CircleAtFixedCoordinates)
@ -109,8 +111,8 @@ namespace Pal.Client.Rendering
var elements = new List<IRenderElement> var elements = new List<IRenderElement>
{ {
CreateElement(Marker.EType.Trap, pos.Value, trapColor), CreateElement(MemoryLocation.EType.Trap, pos.Value, trapColor),
CreateElement(Marker.EType.Hoard, pos.Value, hoardColor), CreateElement(MemoryLocation.EType.Hoard, pos.Value, hoardColor),
}; };
if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test),

View File

@ -8,7 +8,7 @@ namespace Pal.Client.Scheduled
{ {
internal interface IHandler internal interface IHandler
{ {
void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers); void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout);
} }
internal abstract class Handler<T> : IHandler internal abstract class Handler<T> : IHandler
@ -21,14 +21,14 @@ namespace Pal.Client.Scheduled
_logger = logger; _logger = logger;
} }
protected abstract void Run(T queued, ref bool recreateLayout, ref bool saveMarkers); protected abstract void Run(T queued, ref bool recreateLayout);
public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout)
{ {
if (queued is T t) if (queued is T t)
{ {
_logger.LogInformation("Handling {QueuedType}", queued.GetType()); _logger.LogInformation("Handling {QueuedType}", queued.GetType());
Run(t, ref recreateLayout, ref saveMarkers); Run(t, ref recreateLayout);
} }
else else
{ {

View File

@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Floors;
using Pal.Client.Rendering; using Pal.Client.Rendering;
namespace Pal.Client.Scheduled namespace Pal.Client.Scheduled
@ -9,38 +10,19 @@ namespace Pal.Client.Scheduled
{ {
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedConfigUpdate> internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>
{ {
private readonly IPalacePalConfiguration _configuration;
private readonly FloorService _floorService;
private readonly TerritoryState _territoryState;
private readonly RenderAdapter _renderAdapter; private readonly RenderAdapter _renderAdapter;
public Handler( public Handler(
ILogger<Handler> logger, ILogger<Handler> logger,
IPalacePalConfiguration configuration,
FloorService floorService,
TerritoryState territoryState,
RenderAdapter renderAdapter) RenderAdapter renderAdapter)
: base(logger) : base(logger)
{ {
_configuration = configuration;
_floorService = floorService;
_territoryState = territoryState;
_renderAdapter = renderAdapter; _renderAdapter = renderAdapter;
} }
protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout, ref bool saveMarkers) protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout)
{ {
if (_configuration.Mode == EMode.Offline) // TODO filter stuff if offline
{
LocalState.UpdateAll();
_floorService.FloorMarkers.Clear();
_floorService.EphemeralMarkers.Clear();
_territoryState.LastTerritory = 0;
recreateLayout = true;
saveMarkers = true;
}
_renderAdapter.ConfigUpdated(); _renderAdapter.ConfigUpdated();
} }
} }

View File

@ -1,16 +1,12 @@
using Account; using Account;
using Pal.Common; using Pal.Common;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using Microsoft.Extensions.DependencyInjection;
using System.Numerics;
using Dalamud.Game.Gui;
using Dalamud.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Database; using Pal.Client.Database;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Extensions; using Pal.Client.Floors;
using Pal.Client.Properties; using Pal.Client.Properties;
using Pal.Client.Windows; using Pal.Client.Windows;
@ -31,63 +27,46 @@ namespace Pal.Client.Scheduled
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport> internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport>
{ {
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly Chat _chat; private readonly Chat _chat;
private readonly FloorService _floorService;
private readonly ImportService _importService; private readonly ImportService _importService;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
public Handler( public Handler(
ILogger<Handler> logger, ILogger<Handler> logger,
IServiceScopeFactory serviceScopeFactory,
Chat chat, Chat chat,
FloorService floorService,
ImportService importService, ImportService importService,
ConfigWindow configWindow) ConfigWindow configWindow)
: base(logger) : base(logger)
{ {
_serviceScopeFactory = serviceScopeFactory;
_chat = chat; _chat = chat;
_floorService = floorService;
_importService = importService; _importService = importService;
_configWindow = configWindow; _configWindow = configWindow;
} }
protected override void Run(QueuedImport import, ref bool recreateLayout, ref bool saveMarkers) protected override void Run(QueuedImport import, ref bool recreateLayout)
{ {
recreateLayout = true; recreateLayout = true;
saveMarkers = true;
try try
{ {
if (!Validate(import)) if (!Validate(import))
return; return;
List<Guid> oldExportIds = _importService.FindForServer(import.Export.ServerUrl)
.Select(x => x.Id)
.ToList();
foreach (var remoteFloor in import.Export.Floors) using (var scope = _serviceScopeFactory.CreateScope())
{ {
ushort territoryType = (ushort)remoteFloor.TerritoryType; using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
var localState = _floorService.GetFloorMarkers(territoryType); (import.ImportedTraps, import.ImportedHoardCoffers) = _importService.Import(import.Export);
localState.UndoImport(oldExportIds);
ImportFloor(import, remoteFloor, localState);
localState.Save();
} }
_importService.RemoveAllByIds(oldExportIds);
_importService.RemoveById(import.ExportId);
_importService.Add(new ImportHistory
{
Id = import.ExportId,
RemoteUrl = import.Export.ServerUrl,
ExportedAt = import.Export.CreatedAt.ToDateTime(),
ImportedAt = DateTime.UtcNow,
});
_configWindow.UpdateLastImport(); _configWindow.UpdateLastImport();
_logger.LogInformation( _logger.LogInformation(
$"Imported {import.ExportId} for {import.ImportedTraps} traps, {import.ImportedHoardCoffers} hoard coffers"); "Imported {ExportId} for {Traps} traps, {Hoard} hoard coffers", import.ExportId,
import.ImportedTraps, import.ImportedHoardCoffers);
_chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, _chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps,
import.ImportedHoardCoffers)); import.ImportedHoardCoffers));
} }
@ -103,7 +82,8 @@ namespace Pal.Client.Scheduled
if (import.Export.ExportVersion != ExportConfig.ExportVersion) if (import.Export.ExportVersion != ExportConfig.ExportVersion)
{ {
_logger.LogError( _logger.LogError(
"Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}", import.Export.ExportVersion, ExportConfig.ExportVersion); "Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}",
import.Export.ExportVersion, ExportConfig.ExportVersion);
_chat.Error(Localization.Error_ImportFailed_IncompatibleVersion); _chat.Error(Localization.Error_ImportFailed_IncompatibleVersion);
return false; return false;
} }
@ -127,28 +107,6 @@ namespace Pal.Client.Scheduled
return true; return true;
} }
private void ImportFloor(QueuedImport import, 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)
import.ImportedTraps++;
else if (localMarker.Type == Marker.EType.Hoard)
import.ImportedHoardCoffers++;
}
remoteMarker.Imports.Add(import.ExportId);
}
}
} }
} }
} }

View File

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Floors;
using Pal.Client.Floors.Tasks;
using Pal.Client.Net; using Pal.Client.Net;
namespace Pal.Client.Scheduled namespace Pal.Client.Scheduled
@ -14,10 +17,11 @@ namespace Pal.Client.Scheduled
public required SyncType Type { get; init; } public required SyncType Type { get; init; }
public required ushort TerritoryType { get; init; } public required ushort TerritoryType { get; init; }
public required bool Success { get; init; } public required bool Success { get; init; }
public required List<Marker> Markers { get; init; } public required IReadOnlyList<PersistentLocation> Locations { get; init; }
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedSyncResponse> internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedSyncResponse>
{ {
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IPalacePalConfiguration _configuration; private readonly IPalacePalConfiguration _configuration;
private readonly FloorService _floorService; private readonly FloorService _floorService;
private readonly TerritoryState _territoryState; private readonly TerritoryState _territoryState;
@ -25,46 +29,56 @@ namespace Pal.Client.Scheduled
public Handler( public Handler(
ILogger<Handler> logger, ILogger<Handler> logger,
IServiceScopeFactory serviceScopeFactory,
IPalacePalConfiguration configuration, IPalacePalConfiguration configuration,
FloorService floorService, FloorService floorService,
TerritoryState territoryState, TerritoryState territoryState,
DebugState debugState) DebugState debugState)
: base(logger) : base(logger)
{ {
_serviceScopeFactory = serviceScopeFactory;
_configuration = configuration; _configuration = configuration;
_floorService = floorService; _floorService = floorService;
_territoryState = territoryState; _territoryState = territoryState;
_debugState = debugState; _debugState = debugState;
} }
protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout, ref bool saveMarkers) protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout)
{ {
recreateLayout = true; recreateLayout = true;
saveMarkers = true;
try try
{ {
var remoteMarkers = queued.Markers; var remoteMarkers = queued.Locations;
var currentFloor = _floorService.GetFloorMarkers(queued.TerritoryType); var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType);
if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) if (memoryTerritory != null && _configuration.Mode == EMode.Online && queued.Success &&
remoteMarkers.Count > 0)
{ {
switch (queued.Type) switch (queued.Type)
{ {
case SyncType.Download: case SyncType.Download:
case SyncType.Upload: case SyncType.Upload:
List<PersistentLocation> newLocations = new();
foreach (var remoteMarker in remoteMarkers) 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. // 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); PersistentLocation? localLocation =
if (localMarker != null) memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker);
if (localLocation != null)
{ {
localMarker.NetworkId = remoteMarker.NetworkId; localLocation.NetworkId = remoteMarker.NetworkId;
continue; continue;
} }
if (queued.Type == SyncType.Download) if (queued.Type == SyncType.Download)
currentFloor.Markers.Add(remoteMarker); {
memoryTerritory.Locations.Add(remoteMarker);
newLocations.Add(remoteMarker);
} }
}
if (newLocations.Count > 0)
new SaveNewLocations(_serviceScopeFactory, memoryTerritory, newLocations).Start();
break; break;
@ -73,11 +87,23 @@ namespace Pal.Client.Scheduled
_configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
if (partialAccountId == null) if (partialAccountId == null)
break; break;
List<PersistentLocation> locationsToUpdate = new();
foreach (var remoteMarker in remoteMarkers) foreach (var remoteMarker in remoteMarkers)
{ {
Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); PersistentLocation? localLocation =
if (localMarker != null) memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker);
localMarker.RemoteSeenOn.Add(partialAccountId); if (localLocation != null)
{
localLocation.RemoteSeenOn.Add(partialAccountId);
locationsToUpdate.Add(localLocation);
}
}
if (locationsToUpdate.Count > 0)
{
new MarkRemoteSeen(_serviceScopeFactory, memoryTerritory, locationsToUpdate,
partialAccountId).Start();
} }
break; break;
@ -91,22 +117,22 @@ namespace Pal.Client.Scheduled
if (queued.Type == SyncType.Download) if (queued.Type == SyncType.Download)
{ {
if (queued.Success) if (queued.Success)
_territoryState.TerritorySyncState = SyncState.Complete; _territoryState.TerritorySyncState = ESyncState.Complete;
else else
_territoryState.TerritorySyncState = SyncState.Failed; _territoryState.TerritorySyncState = ESyncState.Failed;
} }
} }
catch (Exception e) catch (Exception e)
{ {
_debugState.SetFromException(e); _debugState.SetFromException(e);
if (queued.Type == SyncType.Download) if (queued.Type == SyncType.Download)
_territoryState.TerritorySyncState = SyncState.Failed; _territoryState.TerritorySyncState = ESyncState.Failed;
} }
} }
} }
} }
public enum SyncState public enum ESyncState
{ {
NotAttempted, NotAttempted,
NotNeeded, NotNeeded,

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Floors;
using Pal.Client.Windows; using Pal.Client.Windows;
using Pal.Common; using Pal.Common;
@ -20,28 +21,18 @@ namespace Pal.Client.Scheduled
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport> internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport>
{ {
private readonly ImportService _importService; private readonly ImportService _importService;
private readonly FloorService _floorService;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
public Handler(ILogger<Handler> logger, ImportService importService, FloorService floorService, ConfigWindow configWindow) public Handler(ILogger<Handler> logger, ImportService importService, ConfigWindow configWindow)
: base(logger) : base(logger)
{ {
_importService = importService; _importService = importService;
_floorService = floorService;
_configWindow = configWindow; _configWindow = configWindow;
} }
protected override void Run(QueuedUndoImport queued, ref bool recreateLayout, ref bool saveMarkers) protected override void Run(QueuedUndoImport queued, ref bool recreateLayout)
{ {
recreateLayout = true; recreateLayout = true;
saveMarkers = true;
foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues())
{
var localState = _floorService.GetFloorMarkers((ushort)territoryType);
localState.UndoImport(new List<Guid> { queued.ExportId });
localState.Save();
}
_importService.RemoveById(queued.ExportId); _importService.RemoveById(queued.ExportId);
_configWindow.UpdateLastImport(); _configWindow.UpdateLastImport();

View File

@ -25,6 +25,7 @@ using Pal.Client.Configuration;
using Pal.Client.Database; using Pal.Client.Database;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Floors;
namespace Pal.Client.Windows namespace Pal.Client.Windows
{ {
@ -382,24 +383,25 @@ namespace Pal.Client.Windows
ImGui.Text($"{_debugState.DebugMessage}"); ImGui.Text($"{_debugState.DebugMessage}");
ImGui.Indent(); ImGui.Indent();
if (_floorService.FloorMarkers.TryGetValue(_territoryState.LastTerritory, out var currentFloor)) MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory != null)
{ {
if (_trapConfig.Show) if (_trapConfig.Show)
{ {
int traps = currentFloor.Markers.Count(x => x.Type == Marker.EType.Trap); int traps = memoryTerritory.Locations.Count(x => x.Type == MemoryLocation.EType.Trap);
ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}");
} }
if (_hoardConfig.Show) if (_hoardConfig.Show)
{ {
int hoardCoffers = currentFloor.Markers.Count(x => x.Type == Marker.EType.Hoard); int hoardCoffers = memoryTerritory.Locations.Count(x => x.Type == MemoryLocation.EType.Hoard);
ImGui.Text($"{hoardCoffers} known hoard coffer{(hoardCoffers == 1 ? "" : "s")}"); ImGui.Text($"{hoardCoffers} known hoard coffer{(hoardCoffers == 1 ? "" : "s")}");
} }
if (_silverConfig.Show) if (_silverConfig.Show)
{ {
int silverCoffers = int silverCoffers =
_floorService.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); _floorService.EphemeralLocations.Count(x => x.Type == MemoryLocation.EType.SilverCoffer);
ImGui.Text( ImGui.Text(
$"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor");
} }