diff --git a/Pal.Client/Commands/PalNearCommand.cs b/Pal.Client/Commands/PalNearCommand.cs index b24c959..53e9b8a 100644 --- a/Pal.Client/Commands/PalNearCommand.cs +++ b/Pal.Client/Commands/PalNearCommand.cs @@ -3,6 +3,7 @@ using System.Linq; using Dalamud.Game.ClientState; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Rendering; namespace Pal.Client.Commands @@ -32,30 +33,33 @@ namespace Pal.Client.Commands break; case "tnear": - DebugNearest(m => m.Type == Marker.EType.Trap); + DebugNearest(m => m.Type == MemoryLocation.EType.Trap); break; case "hnear": - DebugNearest(m => m.Type == Marker.EType.Hoard); + DebugNearest(m => m.Type == MemoryLocation.EType.Hoard); break; } } - private void DebugNearest(Predicate predicate) + private void DebugNearest(Predicate predicate) { if (!_territoryState.IsInDeepDungeon()) return; - var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); + var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType); + if (state == null) + return; + var playerPosition = _clientState.LocalPlayer?.Position; if (playerPosition == null) return; _chat.Message($"{playerPosition}"); - var nearbyMarkers = state.Markers + var nearbyMarkers = state.Locations .Where(m => predicate(m)) .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) .Take(5) .ToList(); diff --git a/Pal.Client/Configuration/Legacy/JsonMigration.cs b/Pal.Client/Configuration/Legacy/JsonMigration.cs index c73e7ea..c657043 100644 --- a/Pal.Client/Configuration/Legacy/JsonMigration.cs +++ b/Pal.Client/Configuration/Legacy/JsonMigration.cs @@ -114,6 +114,9 @@ namespace Pal.Client.Configuration.Legacy .Cast() .Distinct() .ToList(), + + Imported = o.WasImported, + SinceVersion = o.SinceVersion ?? "0.0", }; clientLocation.RemoteEncounters = o.RemoteSeenOn diff --git a/Pal.Client/Database/ClientLocation.cs b/Pal.Client/Database/ClientLocation.cs index ac0714f..e545edd 100644 --- a/Pal.Client/Database/ClientLocation.cs +++ b/Pal.Client/Database/ClientLocation.cs @@ -30,6 +30,17 @@ namespace Pal.Client.Database /// public List ImportedBy { get; set; } = new(); + /// + /// Whether this location was originally imported. + /// + public bool Imported { get; set; } + + + /// + /// To make rollbacks of local data easier, keep track of the plugin version which was used to create this location initially. + /// + public string SinceVersion { get; set; } = "0.0"; + public enum EType { Trap = 1, diff --git a/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs new file mode 100644 index 0000000..24dd296 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs @@ -0,0 +1,148 @@ +// +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 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.Property("ImportedById") + .HasColumnType("TEXT"); + + b.Property("ImportedLocationsLocalId") + .HasColumnType("INTEGER"); + + b.HasKey("ImportedById", "ImportedLocationsLocalId"); + + b.HasIndex("ImportedLocationsLocalId"); + + b.ToTable("LocationImports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Property("LocalId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Imported") + .HasColumnType("INTEGER"); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.Property("SinceVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TerritoryType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("LocalId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ExportedAt") + .HasColumnType("TEXT"); + + b.Property("ImportedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Imports"); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.cs b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.cs new file mode 100644 index 0000000..130be35 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + /// + public partial class AddImportedAndSinceVersionToClientLocation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Imported", + table: "Locations", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SinceVersion", + table: "Locations", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Imported", + table: "Locations"); + + migrationBuilder.DropColumn( + name: "SinceVersion", + table: "Locations"); + } + } +} diff --git a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs index e0813c3..963f393 100644 --- a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs +++ b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs @@ -38,9 +38,16 @@ namespace Pal.Client.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Imported") + .HasColumnType("INTEGER"); + b.Property("Seen") .HasColumnType("INTEGER"); + b.Property("SinceVersion") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("TerritoryType") .HasColumnType("INTEGER"); @@ -58,7 +65,7 @@ namespace Pal.Client.Database.Migrations b.HasKey("LocalId"); - b.ToTable("Locations", (string)null); + b.ToTable("Locations"); }); modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => @@ -78,7 +85,7 @@ namespace Pal.Client.Database.Migrations b.HasKey("Id"); - b.ToTable("Imports", (string)null); + b.ToTable("Imports"); }); modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => @@ -99,7 +106,7 @@ namespace Pal.Client.Database.Migrations b.HasIndex("ClientLocationId"); - b.ToTable("RemoteEncounters", (string)null); + b.ToTable("RemoteEncounters"); }); modelBuilder.Entity("ClientLocationImportHistory", b => @@ -120,13 +127,18 @@ namespace Pal.Client.Database.Migrations modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => { b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") - .WithMany() + .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 } } diff --git a/Pal.Client/DependencyInjection/FloorService.cs b/Pal.Client/DependencyInjection/FloorService.cs deleted file mode 100644 index 8cdcb98..0000000 --- a/Pal.Client/DependencyInjection/FloorService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Concurrent; - -namespace Pal.Client.DependencyInjection -{ - internal sealed class FloorService - { - public ConcurrentDictionary FloorMarkers { get; } = new(); - public ConcurrentBag EphemeralMarkers { get; set; } = new(); - - public LocalState GetFloorMarkers(ushort territoryType) - { - return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt)); - } - } -} diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs index 8db9d10..c501410 100644 --- a/Pal.Client/DependencyInjection/FrameworkService.cs +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -14,9 +14,11 @@ using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using Pal.Client.Configuration; using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Net; using Pal.Client.Rendering; using Pal.Client.Scheduled; +using Pal.Common; namespace Pal.Client.DependencyInjection { @@ -84,44 +86,45 @@ namespace Pal.Client.DependencyInjection try { bool recreateLayout = false; - bool saveMarkers = false; while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - HandleQueued(queued, ref recreateLayout, ref saveMarkers); + HandleQueued(queued, ref recreateLayout); if (_territoryState.LastTerritory != _clientState.TerritoryType) { _territoryState.LastTerritory = _clientState.TerritoryType; - _territoryState.TerritorySyncState = SyncState.NotAttempted; + _territoryState.TerritorySyncState = ESyncState.NotAttempted; NextUpdateObjects.Clear(); - if (_territoryState.IsInDeepDungeon()) - _floorService.GetFloorMarkers(_territoryState.LastTerritory); - _floorService.EphemeralMarkers.Clear(); + _floorService.ChangeTerritory(_territoryState.LastTerritory); _territoryState.PomanderOfSight = PomanderState.Inactive; _territoryState.PomanderOfIntuition = PomanderState.Inactive; recreateLayout = true; _debugState.Reset(); } - if (!_territoryState.IsInDeepDungeon()) + if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory)) 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)); } while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - HandleQueued(queued, ref recreateLayout, ref saveMarkers); + HandleQueued(queued, ref recreateLayout); - var currentFloor = _floorService.GetFloorMarkers(_territoryState.LastTerritory); + (IReadOnlyList visiblePersistentMarkers, + IReadOnlyList visibleEphemeralMarkers) = + GetRelevantGameObjects(); - IList visibleMarkers = GetRelevantGameObjects(); - HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, - recreateLayout); - HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); + ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory; + HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout); + + if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout)) + RecreateEphemeralLayout(); } catch (Exception e) { @@ -131,183 +134,161 @@ namespace Pal.Client.DependencyInjection #region Render Markers - private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, + private void HandlePersistentLocations(ETerritoryType territoryType, + IReadOnlyList visiblePersistentMarkers, bool recreateLayout) { - var currentFloorMarkers = currentFloor.Markers; - - bool updateSeenMarkers = false; - var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - foreach (var visibleMarker in visibleMarkers) + bool recreatePersistentLocations = _floorService.MergePersistentLocations( + territoryType, + visiblePersistentMarkers, + recreateLayout, + out List locationsToSync); + recreatePersistentLocations |= CheckLocationsForPomanders(visiblePersistentMarkers); + if (locationsToSync.Count > 0) { - Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); - if (knownMarker != null) - { - 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, - // markers discovered afterwards are automatically marked seen. - if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && - !knownMarker.RemoteSeenOn.Contains(partialAccountId)) - updateSeenMarkers = true; - - continue; - } - - currentFloorMarkers.Add(visibleMarker); - recreateLayout = true; - saveMarkers = true; + Task.Run(async () => + await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, locationsToSync)); } - if (!recreateLayout && currentFloorMarkers.Count > 0 && + UploadLocations(); + + if (recreatePersistentLocations) + RecreatePersistentLayout(visiblePersistentMarkers); + } + + private bool CheckLocationsForPomanders(IReadOnlyList visibleLocations) + { + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory is { Locations.Count: > 0 } && (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) { try { - foreach (var marker in currentFloorMarkers) + foreach (var location in memoryTerritory.Locations) { - uint desiredColor = DetermineColor(marker, visibleMarkers); - if (marker.RenderElement == null || !marker.RenderElement.IsValid) - { - recreateLayout = true; - break; - } + uint desiredColor = DetermineColor(location, visibleLocations); + if (location.RenderElement == null || !location.RenderElement.IsValid) + return true; - if (marker.RenderElement.Color != desiredColor) - marker.RenderElement.Color = desiredColor; + if (location.RenderElement.Color != desiredColor) + location.RenderElement.Color = desiredColor; } } catch (Exception e) { _debugState.SetFromException(e); - recreateLayout = true; + return true; } } - if (updateSeenMarkers && partialAccountId != null) + return false; + } + + private void UploadLocations() + { + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory == null) + return; + + List locationsToUpload = memoryTerritory.Locations + .Where(loc => loc.NetworkId == null && loc.UploadRequested == false) + .ToList(); + if (locationsToUpload.Count > 0) { - 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)); - } + foreach (var location in locationsToUpload) + location.UploadRequested = true; - if (saveMarkers) - { - currentFloor.Save(); - - if (_territoryState.TerritorySyncState == SyncState.Complete) - { - var markersToUpload = currentFloorMarkers - .Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); - if (markersToUpload.Count > 0) - { - foreach (var marker in markersToUpload) - marker.UploadRequested = true; - Task.Run(async () => - await UploadMarkersForTerritory(_territoryState.LastTerritory, markersToUpload)); - } - } - } - - if (recreateLayout) - { - _renderAdapter.ResetLayer(ELayer.TrapHoard); - - List elements = new(); - foreach (var marker in currentFloorMarkers) - { - if (marker.Seen || _configuration.Mode == EMode.Online || - marker is { WasImported: true, Imports.Count: > 0 }) - { - if (marker.Type == Marker.EType.Trap) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), - _configuration.DeepDungeons.Traps); - } - else if (marker.Type == Marker.EType.Hoard) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), - _configuration.DeepDungeons.HoardCoffers); - } - } - } - - if (elements.Count == 0) - return; - - _renderAdapter.SetLayer(ELayer.TrapHoard, elements); + Task.Run(async () => + await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload)); } } - private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) + private void RecreatePersistentLayout(IReadOnlyList visibleMarkers) { - recreateLayout |= - _floorService.EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); - recreateLayout |= - visibleMarkers.Any(visibleMarker => _floorService.EphemeralMarkers.All(x => x != visibleMarker)); + _renderAdapter.ResetLayer(ELayer.TrapHoard); - if (recreateLayout) + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory == null) + return; + + List elements = new(); + foreach (var location in memoryTerritory.Locations) { - _renderAdapter.ResetLayer(ELayer.RegularCoffers); - _floorService.EphemeralMarkers.Clear(); - - List elements = new(); - foreach (var marker in visibleMarkers) + if (location.Type == MemoryLocation.EType.Trap) { - _floorService.EphemeralMarkers.Add(marker); - - if (marker.Type == Marker.EType.SilverCoffer && _configuration.DeepDungeons.SilverCoffers.Show) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), - _configuration.DeepDungeons.SilverCoffers); - } + CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers), + _configuration.DeepDungeons.Traps); + } + else if (location.Type == MemoryLocation.EType.Hoard) + { + CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers), + _configuration.DeepDungeons.HoardCoffers); } - - if (elements.Count == 0) - return; - - _renderAdapter.SetLayer(ELayer.RegularCoffers, elements); } + + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.TrapHoard, elements); } - private uint DetermineColor(Marker marker, IList visibleMarkers) + private void RecreateEphemeralLayout() { - switch (marker.Type) + _renderAdapter.ResetLayer(ELayer.RegularCoffers); + + List elements = new(); + foreach (var location in _floorService.EphemeralLocations) { - case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive || - !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || - visibleMarkers.Any(x => x == marker): + if (location.Type == MemoryLocation.EType.SilverCoffer && + _configuration.DeepDungeons.SilverCoffers.Show) + { + CreateRenderElement(location, elements, DetermineColor(location), + _configuration.DeepDungeons.SilverCoffers); + } + } + + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.RegularCoffers, elements); + } + + private uint DetermineColor(PersistentLocation location, IReadOnlyList visibleLocations) + { + switch (location.Type) + { + case MemoryLocation.EType.Trap + when _territoryState.PomanderOfSight == PomanderState.Inactive || + !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || + visibleLocations.Any(x => x == location): return _configuration.DeepDungeons.Traps.Color; - case Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive || - !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || - visibleMarkers.Any(x => x == marker): + case MemoryLocation.EType.Hoard + when _territoryState.PomanderOfIntuition == PomanderState.Inactive || + !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || + visibleLocations.Any(x => x == location): 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: - return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); + return RenderData.ColorInvisible; } } - private void CreateRenderElement(Marker marker, List 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 elements, uint color, MarkerConfiguration config) { if (!config.Show) return; - var element = _renderAdapter.CreateElement(marker.Type, marker.Position, color, config.Fill); - marker.RenderElement = element; + var element = _renderAdapter.CreateElement(location.Type, location.Position, color, config.Fill); + location.RenderElement = element; elements.Add(element); } @@ -325,7 +306,7 @@ namespace Pal.Client.DependencyInjection Type = SyncType.Download, TerritoryType = territoryId, Success = success, - Markers = downloadedMarkers + Locations = downloadedMarkers }); } catch (Exception e) @@ -334,17 +315,17 @@ namespace Pal.Client.DependencyInjection } } - private async Task UploadMarkersForTerritory(ushort territoryId, List markersToUpload) + private async Task UploadLocationsForTerritory(ushort territoryId, List locationsToUpload) { try { - var (success, uploadedMarkers) = await _remoteApi.UploadMarker(territoryId, markersToUpload); + var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload); LateEventQueue.Enqueue(new QueuedSyncResponse { Type = SyncType.Upload, TerritoryType = territoryId, Success = success, - Markers = uploadedMarkers + Locations = uploadedLocations }); } catch (Exception e) @@ -353,17 +334,18 @@ namespace Pal.Client.DependencyInjection } } - private async Task SyncSeenMarkersForTerritory(ushort territoryId, List markersToUpdate) + private async Task SyncSeenMarkersForTerritory(ushort territoryId, + IReadOnlyList locationsToUpdate) { try { - var success = await _remoteApi.MarkAsSeen(territoryId, markersToUpdate); + var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate); LateEventQueue.Enqueue(new QueuedSyncResponse { Type = SyncType.MarkSeen, TerritoryType = territoryId, Success = success, - Markers = markersToUpdate, + Locations = locationsToUpdate, }); } catch (Exception e) @@ -374,9 +356,10 @@ namespace Pal.Client.DependencyInjection #endregion - private IList GetRelevantGameObjects() + private (IReadOnlyList, IReadOnlyList) GetRelevantGameObjects() { - List result = new(); + List persistentLocations = new(); + List ephemeralLocations = new(); for (int i = 246; i < _objectTable.Length; i++) { GameObject? obj = _objectTable[i]; @@ -391,16 +374,31 @@ namespace Pal.Client.DependencyInjection case 2007185: case 2007186: 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; case 2007542: 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; 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; } } @@ -409,18 +407,25 @@ namespace Pal.Client.DependencyInjection { var obj = _objectTable.FirstOrDefault(x => x.Address == address); 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()); var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType); - handler.RunIfCompatible(queued, ref recreateLayout, ref saveMarkers); + handler.RunIfCompatible(queued, ref recreateLayout); } } } diff --git a/Pal.Client/DependencyInjection/ImportService.cs b/Pal.Client/DependencyInjection/ImportService.cs index 1d5f3cb..4d074dd 100644 --- a/Pal.Client/DependencyInjection/ImportService.cs +++ b/Pal.Client/DependencyInjection/ImportService.cs @@ -3,21 +3,28 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Account; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Pal.Client.Database; +using Pal.Client.Floors; +using Pal.Client.Floors.Tasks; +using Pal.Common; namespace Pal.Client.DependencyInjection { internal sealed class ImportService { private readonly IServiceProvider _serviceProvider; + private readonly FloorService _floorService; - public ImportService(IServiceProvider serviceProvider) + public ImportService(IServiceProvider serviceProvider, FloorService floorService) { _serviceProvider = serviceProvider; + _floorService = floorService; } + /* public void Add(ImportHistory history) { using var scope = _serviceProvider.CreateScope(); @@ -26,6 +33,7 @@ namespace Pal.Client.DependencyInjection dbContext.Imports.Add(history); dbContext.SaveChanges(); } + */ public async Task 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); } + /* public List FindForServer(string server) { if (string.IsNullOrEmpty(server)) @@ -44,18 +53,58 @@ namespace Pal.Client.DependencyInjection using var dbContext = scope.ServiceProvider.GetRequiredService(); return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList(); - } + }*/ - public void RemoveAllByIds(List ids) + public (int traps, int hoard) Import(ExportRoot import) { using var scope = _serviceProvider.CreateScope(); using var dbContext = scope.ServiceProvider.GetRequiredService(); - 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 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(); + + _floorService.ResetAll(); + return (traps, hoard); } public void RemoveById(Guid id) - => RemoveAllByIds(new List { id }); + { + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id)); + + // TODO filter here, update territories + dbContext.SaveChanges(); + + _floorService.ResetAll(); + } } } diff --git a/Pal.Client/DependencyInjection/TerritoryState.cs b/Pal.Client/DependencyInjection/TerritoryState.cs index 15e21d4..75c3197 100644 --- a/Pal.Client/DependencyInjection/TerritoryState.cs +++ b/Pal.Client/DependencyInjection/TerritoryState.cs @@ -17,7 +17,7 @@ namespace Pal.Client.DependencyInjection } public ushort LastTerritory { get; set; } - public SyncState TerritorySyncState { get; set; } + public ESyncState TerritorySyncState { get; set; } public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 3ef0b58..ac3b722 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -24,6 +24,7 @@ using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection.Logging; using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Net; using Pal.Client.Properties; using Pal.Client.Rendering; @@ -63,7 +64,8 @@ namespace Pal.Client CommandManager commandManager, DataManager dataManager) { - _logger.LogInformation("Building service container"); + _logger.LogInformation("Building service container for {Assembly}", + typeof(DependencyInjectionContext).Assembly.FullName); // set up legacy services #pragma warning disable CS0612 diff --git a/Pal.Client/Floors/EphemeralLocation.cs b/Pal.Client/Floors/EphemeralLocation.cs new file mode 100644 index 0000000..ccf8a4b --- /dev/null +++ b/Pal.Client/Floors/EphemeralLocation.cs @@ -0,0 +1,24 @@ +using System; + +namespace Pal.Client.Floors +{ + /// + /// This is a currently-visible marker. + /// + 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); + } + } +} diff --git a/Pal.Client/Floors/FloorService.cs b/Pal.Client/Floors/FloorService.cs new file mode 100644 index 0000000..535b418 --- /dev/null +++ b/Pal.Client/Floors/FloorService.cs @@ -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 _territories; + + private ConcurrentBag _ephemeralLocations = new(); + + public FloorService(IPalacePalConfiguration configuration, IServiceScopeFactory serviceScopeFactory) + { + _configuration = configuration; + _serviceScopeFactory = serviceScopeFactory; + _territories = Enum.GetValues().ToDictionary(o => o, o => new MemoryTerritory(o)); + } + + public IReadOnlyCollection EphemeralLocations => _ephemeralLocations; + + public void ChangeTerritory(ushort territoryType) + { + _ephemeralLocations = new ConcurrentBag(); + + 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 visibleLocations, + bool recreateLayout, + out List 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 markAsSeen = new(); + List 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; + } + + /// Whether the locations have changed + public bool MergeEphemeralLocations(IReadOnlyList 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(); + } + } + } +} diff --git a/Pal.Client/Floors/MemoryLocation.cs b/Pal.Client/Floors/MemoryLocation.cs new file mode 100644 index 0000000..296bfcd --- /dev/null +++ b/Pal.Client/Floors/MemoryLocation.cs @@ -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 +{ + /// + /// Base class for and . + /// + 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) + }; + } + } +} diff --git a/Pal.Client/Floors/MemoryTerritory.cs b/Pal.Client/Floors/MemoryTerritory.cs new file mode 100644 index 0000000..dc835ec --- /dev/null +++ b/Pal.Client/Floors/MemoryTerritory.cs @@ -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 +{ + /// + /// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60. + /// + 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 Locations { get; } = new(); + public object LockObj { get; } = new(); + + public void Initialize(IEnumerable locations) + { + Locations.Clear(); + foreach (var location in locations) + Locations.Add(location); + + IsReady = true; + IsLoading = false; + } + + public IEnumerable GetRemovableLocations(EMode mode) + { + // TODO there was better logic here; + return Locations.Where(x => !x.Seen); + } + + public void Reset() + { + Locations.Clear(); + IsReady = false; + IsLoading = false; + } + } +} diff --git a/Pal.Client/Floors/PersistentLocation.cs b/Pal.Client/Floors/PersistentLocation.cs new file mode 100644 index 0000000..d1db189 --- /dev/null +++ b/Pal.Client/Floors/PersistentLocation.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Pal.Client.Database; + +namespace Pal.Client.Floors +{ + /// + /// A loaded in memory, with certain extra attributes as needed. + /// + internal sealed class PersistentLocation : MemoryLocation + { + /// + public int? LocalId { get; set; } + + /// + /// Network id for the server you're currently connected to. + /// + public Guid? NetworkId { get; set; } + + /// + /// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests. + /// + public bool UploadRequested { get; set; } + + /// + /// + public List RemoteSeenOn { get; set; } = new(); + + /// + /// Whether this marker was requested to be seen, to avoid duplicate requests. + /// + 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); + } + } +} diff --git a/Pal.Client/Floors/Tasks/DbTask.cs b/Pal.Client/Floors/Tasks/DbTask.cs new file mode 100644 index 0000000..017f96d --- /dev/null +++ b/Pal.Client/Floors/Tasks/DbTask.cs @@ -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(); + + Run(dbContext); + }); + } + + protected abstract void Run(PalClientContext dbContext); + } +} diff --git a/Pal.Client/Floors/Tasks/LoadTerritory.cs b/Pal.Client/Floors/Tasks/LoadTerritory.cs new file mode 100644 index 0000000..caa4291 --- /dev/null +++ b/Pal.Client/Floors/Tasks/LoadTerritory.cs @@ -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 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) + }; + } + } +} diff --git a/Pal.Client/Floors/Tasks/MarkAsSeen.cs b/Pal.Client/Floors/Tasks/MarkAsSeen.cs new file mode 100644 index 0000000..3e1b767 --- /dev/null +++ b/Pal.Client/Floors/Tasks/MarkAsSeen.cs @@ -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 _locations; + + public MarkAsSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, + IReadOnlyList 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(); + } + } + } +} diff --git a/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs b/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs new file mode 100644 index 0000000..16a20c8 --- /dev/null +++ b/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs @@ -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 _locations; + private readonly string _accountId; + + public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory, + MemoryTerritory territory, + IReadOnlyList locations, + string accountId) + : base(serviceScopeFactory) + { + _territory = territory; + _locations = locations; + _accountId = accountId; + } + + protected override void Run(PalClientContext dbContext) + { + lock (_territory.LockObj) + { + List 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(); + } + } + } +} diff --git a/Pal.Client/Floors/Tasks/SaveNewLocations.cs b/Pal.Client/Floors/Tasks/SaveNewLocations.cs new file mode 100644 index 0000000..4489740 --- /dev/null +++ b/Pal.Client/Floors/Tasks/SaveNewLocations.cs @@ -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 _newLocations; + + public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, + List 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 locations) + { + lock (territory.LockObj) + { + Dictionary 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) + }; + } + } +} diff --git a/Pal.Client/LocalState.cs b/Pal.Client/LocalState.cs deleted file mode 100644 index 8534c50..0000000 --- a/Pal.Client/LocalState.cs +++ /dev/null @@ -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 -{ - /// - /// JSON for a single floor set (e.g. 51-60). - /// - 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 Markers { get; set; } = new(); - - public LocalState(uint territoryType) - { - TerritoryType = territoryType; - } - - private void ApplyFilters() - { - if (Mode == EMode.Offline) - Markers = new ConcurrentBag(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(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(JsonSerializer.Deserialize>(content, JsonSerializerOptions) ?? new()), - }; - } - else - { - var save = JsonSerializer.Deserialize(content, JsonSerializerOptions); - if (save == null) - return null; - - localState = new LocalState(territoryType) - { - Markers = new ConcurrentBag(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(Markers) - }, JsonSerializerOptions)); - } - } - - public string GetSaveLocation() => GetSaveLocation(TerritoryType); - - private static string GetSaveLocation(uint territoryType) => Path.Join(PluginConfigDirectory, $"{territoryType}.json"); - - public static void ForEach(Action 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 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 Markers { get; set; } = new(); - } - } -} diff --git a/Pal.Client/Marker.cs b/Pal.Client/Marker.cs deleted file mode 100644 index e2e79b9..0000000 --- a/Pal.Client/Marker.cs +++ /dev/null @@ -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; } - - /// - /// Whether we have encountered the trap/coffer at this location in-game. - /// - public bool Seen { get; set; } - - /// - /// Network id for the server you're currently connected to. - /// - [JsonIgnore] - public Guid? NetworkId { get; set; } - - /// - /// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests. - /// - [JsonIgnore] - public bool UploadRequested { get; set; } - - /// - /// 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). - /// - public List RemoteSeenOn { get; set; } = new(); - - /// - /// Whether this marker was requested to be seen, to avoid duplicate requests. - /// - [JsonIgnore] - public bool RemoteSeenRequested { get; set; } - - /// - /// 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. - /// - public List Imports { get; set; } = new(); - - public bool WasImported { get; set; } - - /// - /// To make rollbacks of local data easier, keep track of the version which was used to write the marker initially. - /// - 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 - } - } -} diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs index 259b1ea..cb4dbde 100644 --- a/Pal.Client/Net/RemoteApi.PalaceService.cs +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -5,24 +5,25 @@ using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; +using Pal.Client.Floors; namespace Pal.Client.Net { internal partial class RemoteApi { - public async Task<(bool, List)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) + public async Task<(bool, List)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) { if (!await Connect(cancellationToken)) return (false, new()); var palaceClient = new PalaceService.PalaceServiceClient(_channel); 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)> UploadMarker(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + public async Task<(bool, List)> UploadLocations(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) { - if (markers.Count == 0) + if (locations.Count == 0) return (true, new()); if (!await Connect(cancellationToken)) @@ -33,7 +34,7 @@ namespace Pal.Client.Net { TerritoryType = territoryType, }; - uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject + uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject { Type = (ObjectType)m.Type, X = m.Position.X, @@ -41,12 +42,12 @@ namespace Pal.Client.Net Z = m.Position.Z })); 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 MarkAsSeen(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + public async Task MarkAsSeen(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) { - if (markers.Count == 0) + if (locations.Count == 0) return true; if (!await Connect(cancellationToken)) @@ -54,15 +55,22 @@ namespace Pal.Client.Net var palaceClient = new PalaceService.PalaceServiceClient(_channel); var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; - foreach (var marker in markers) + foreach (var marker in locations) seenRequest.NetworkIds.Add(marker.NetworkId.ToString()); var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); return seenReply.Success; } - private Marker CreateMarkerFromNetworkObject(PalaceObject obj) => - new Marker((Marker.EType)obj.Type, new Vector3(obj.X, obj.Y, obj.Z), Guid.Parse(obj.NetworkId)); + private PersistentLocation CreateLocationFromNetworkObject(PalaceObject obj) + { + 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)> FetchStatistics(CancellationToken cancellationToken = default) { diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 83b35a3..6815210 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -13,7 +13,7 @@ namespace Pal.Client.Net #if DEBUG public const string RemoteUrl = "http://localhost:5145"; #else - public const string RemoteUrl = "https://pal.liza.sh"; + //public const string RemoteUrl = "https://pal.liza.sh"; #endif private readonly string _userAgent = $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; diff --git a/Pal.Client/Rendering/IRenderer.cs b/Pal.Client/Rendering/IRenderer.cs index 0ac7fd3..1856403 100644 --- a/Pal.Client/Rendering/IRenderer.cs +++ b/Pal.Client/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Numerics; using Pal.Client.Configuration; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -12,7 +13,7 @@ namespace Pal.Client.Rendering 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); } diff --git a/Pal.Client/Rendering/MarkerConfig.cs b/Pal.Client/Rendering/MarkerConfig.cs index 2ef9dde..0bc25cf 100644 --- a/Pal.Client/Rendering/MarkerConfig.cs +++ b/Pal.Client/Rendering/MarkerConfig.cs @@ -1,20 +1,23 @@ using System.Collections.Generic; +using Pal.Client.Floors; namespace Pal.Client.Rendering { internal sealed class MarkerConfig { private static readonly MarkerConfig EmptyConfig = new(); - private static readonly Dictionary MarkerConfigs = new() + + private static readonly Dictionary MarkerConfigs = new() { - { 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 } }, + { MemoryLocation.EType.Trap, new MarkerConfig { Radius = 1.7f } }, + { MemoryLocation.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } }, + { MemoryLocation.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, }; public float OffsetY { get; private init; } 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); } } diff --git a/Pal.Client/Rendering/RenderAdapter.cs b/Pal.Client/Rendering/RenderAdapter.cs index 5ccb1a1..dfc2287 100644 --- a/Pal.Client/Rendering/RenderAdapter.cs +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -4,6 +4,7 @@ using System.Numerics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -56,7 +57,7 @@ namespace Pal.Client.Rendering public void ResetLayer(ELayer 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); public ERenderer GetConfigValue() diff --git a/Pal.Client/Rendering/SimpleRenderer.cs b/Pal.Client/Rendering/SimpleRenderer.cs index d7724f5..3143def 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -9,6 +9,7 @@ using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -53,7 +54,7 @@ namespace Pal.Client.Rendering 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); return new SimpleElement @@ -73,9 +74,13 @@ namespace Pal.Client.Rendering TerritoryType = _clientState.TerritoryType, Elements = new List { - (SimpleElement)CreateElement(Marker.EType.Trap, _clientState.LocalPlayer?.Position ?? default, + (SimpleElement)CreateElement( + MemoryLocation.EType.Trap, + _clientState.LocalPlayer?.Position ?? default, trapColor), - (SimpleElement)CreateElement(Marker.EType.Hoard, _clientState.LocalPlayer?.Position ?? default, + (SimpleElement)CreateElement( + MemoryLocation.EType.Hoard, + _clientState.LocalPlayer?.Position ?? default, hoardColor) }, ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout @@ -120,15 +125,15 @@ namespace Pal.Client.Rendering switch (e.Type) { - case Marker.EType.Hoard: + case MemoryLocation.EType.Hoard: // ignore distance if this is a found hoard coffer if (_territoryState.PomanderOfIntuition == PomanderState.Active && _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) break; - goto case Marker.EType.Trap; + goto case MemoryLocation.EType.Trap; - case Marker.EType.Trap: + case MemoryLocation.EType.Trap: var playerPos = _clientState.LocalPlayer?.Position; if (playerPos == null) return; @@ -189,7 +194,7 @@ namespace Pal.Client.Rendering public sealed class SimpleElement : IRenderElement { 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 uint Color { get; set; } public required float Radius { get; init; } diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 82dd7f2..bdcee3d 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -13,6 +13,7 @@ using Dalamud.Game.ClientState; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; namespace Pal.Client.Rendering { @@ -57,7 +58,8 @@ namespace Pal.Client.Rendering } 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); } }); @@ -78,7 +80,7 @@ namespace Pal.Client.Rendering private string ToLayerName(ELayer 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); Element element = new Element(ElementType.CircleAtFixedCoordinates) @@ -109,8 +111,8 @@ namespace Pal.Client.Rendering var elements = new List { - CreateElement(Marker.EType.Trap, pos.Value, trapColor), - CreateElement(Marker.EType.Hoard, pos.Value, hoardColor), + CreateElement(MemoryLocation.EType.Trap, pos.Value, trapColor), + CreateElement(MemoryLocation.EType.Hoard, pos.Value, hoardColor), }; if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index 2796971..c86ad36 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -8,7 +8,7 @@ namespace Pal.Client.Scheduled { internal interface IHandler { - void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers); + void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout); } internal abstract class Handler : IHandler @@ -21,14 +21,14 @@ namespace Pal.Client.Scheduled _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) { _logger.LogInformation("Handling {QueuedType}", queued.GetType()); - Run(t, ref recreateLayout, ref saveMarkers); + Run(t, ref recreateLayout); } else { diff --git a/Pal.Client/Scheduled/QueuedConfigUpdate.cs b/Pal.Client/Scheduled/QueuedConfigUpdate.cs index d0e71ca..9810f94 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; using Pal.Client.Rendering; namespace Pal.Client.Scheduled @@ -9,38 +10,19 @@ namespace Pal.Client.Scheduled { internal sealed class Handler : IQueueOnFrameworkThread.Handler { - private readonly IPalacePalConfiguration _configuration; - private readonly FloorService _floorService; - private readonly TerritoryState _territoryState; private readonly RenderAdapter _renderAdapter; public Handler( ILogger logger, - IPalacePalConfiguration configuration, - FloorService floorService, - TerritoryState territoryState, RenderAdapter renderAdapter) : base(logger) { - _configuration = configuration; - _floorService = floorService; - _territoryState = territoryState; _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) - { - LocalState.UpdateAll(); - _floorService.FloorMarkers.Clear(); - _floorService.EphemeralMarkers.Clear(); - _territoryState.LastTerritory = 0; - - recreateLayout = true; - saveMarkers = true; - } - + // TODO filter stuff if offline _renderAdapter.ConfigUpdated(); } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 2e35c71..89a947b 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -1,16 +1,12 @@ using Account; using Pal.Common; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Game.Gui; -using Dalamud.Logging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; using Pal.Client.DependencyInjection; -using Pal.Client.Extensions; +using Pal.Client.Floors; using Pal.Client.Properties; using Pal.Client.Windows; @@ -31,63 +27,46 @@ namespace Pal.Client.Scheduled internal sealed class Handler : IQueueOnFrameworkThread.Handler { + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly Chat _chat; - private readonly FloorService _floorService; private readonly ImportService _importService; private readonly ConfigWindow _configWindow; public Handler( ILogger logger, + IServiceScopeFactory serviceScopeFactory, Chat chat, - FloorService floorService, ImportService importService, ConfigWindow configWindow) : base(logger) { + _serviceScopeFactory = serviceScopeFactory; _chat = chat; - _floorService = floorService; _importService = importService; _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; - saveMarkers = true; try { if (!Validate(import)) return; - List 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; - var localState = _floorService.GetFloorMarkers(territoryType); - - localState.UndoImport(oldExportIds); - ImportFloor(import, remoteFloor, localState); - - localState.Save(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + (import.ImportedTraps, import.ImportedHoardCoffers) = _importService.Import(import.Export); } - _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(); _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, import.ImportedHoardCoffers)); } @@ -103,7 +82,8 @@ namespace Pal.Client.Scheduled if (import.Export.ExportVersion != ExportConfig.ExportVersion) { _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); return false; } @@ -127,28 +107,6 @@ namespace Pal.Client.Scheduled 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); - } - } } } } diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index edf72ef..beed1aa 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; +using Pal.Client.Floors; +using Pal.Client.Floors.Tasks; using Pal.Client.Net; namespace Pal.Client.Scheduled @@ -14,10 +17,11 @@ namespace Pal.Client.Scheduled public required SyncType Type { get; init; } public required ushort TerritoryType { get; init; } public required bool Success { get; init; } - public required List Markers { get; init; } + public required IReadOnlyList Locations { get; init; } internal sealed class Handler : IQueueOnFrameworkThread.Handler { + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IPalacePalConfiguration _configuration; private readonly FloorService _floorService; private readonly TerritoryState _territoryState; @@ -25,47 +29,57 @@ namespace Pal.Client.Scheduled public Handler( ILogger logger, + IServiceScopeFactory serviceScopeFactory, IPalacePalConfiguration configuration, FloorService floorService, TerritoryState territoryState, DebugState debugState) : base(logger) { + _serviceScopeFactory = serviceScopeFactory; _configuration = configuration; _floorService = floorService; _territoryState = territoryState; _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; - saveMarkers = true; try { - var remoteMarkers = queued.Markers; - var currentFloor = _floorService.GetFloorMarkers(queued.TerritoryType); - if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) + var remoteMarkers = queued.Locations; + var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType); + if (memoryTerritory != null && _configuration.Mode == EMode.Online && queued.Success && + remoteMarkers.Count > 0) { switch (queued.Type) { case SyncType.Download: case SyncType.Upload: + List newLocations = new(); 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) + PersistentLocation? localLocation = + memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker); + if (localLocation != null) { - localMarker.NetworkId = remoteMarker.NetworkId; + localLocation.NetworkId = remoteMarker.NetworkId; continue; } 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; case SyncType.MarkSeen: @@ -73,11 +87,23 @@ namespace Pal.Client.Scheduled _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); if (partialAccountId == null) break; + + List locationsToUpdate = new(); foreach (var remoteMarker in remoteMarkers) { - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - localMarker.RemoteSeenOn.Add(partialAccountId); + PersistentLocation? localLocation = + memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker); + if (localLocation != null) + { + localLocation.RemoteSeenOn.Add(partialAccountId); + locationsToUpdate.Add(localLocation); + } + } + + if (locationsToUpdate.Count > 0) + { + new MarkRemoteSeen(_serviceScopeFactory, memoryTerritory, locationsToUpdate, + partialAccountId).Start(); } break; @@ -91,22 +117,22 @@ namespace Pal.Client.Scheduled if (queued.Type == SyncType.Download) { if (queued.Success) - _territoryState.TerritorySyncState = SyncState.Complete; + _territoryState.TerritorySyncState = ESyncState.Complete; else - _territoryState.TerritorySyncState = SyncState.Failed; + _territoryState.TerritorySyncState = ESyncState.Failed; } } catch (Exception e) { _debugState.SetFromException(e); if (queued.Type == SyncType.Download) - _territoryState.TerritorySyncState = SyncState.Failed; + _territoryState.TerritorySyncState = ESyncState.Failed; } } } } - public enum SyncState + public enum ESyncState { NotAttempted, NotNeeded, diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index 9e0eb2c..f5b0d3c 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; +using Pal.Client.Floors; using Pal.Client.Windows; using Pal.Common; @@ -20,28 +21,18 @@ namespace Pal.Client.Scheduled internal sealed class Handler : IQueueOnFrameworkThread.Handler { private readonly ImportService _importService; - private readonly FloorService _floorService; private readonly ConfigWindow _configWindow; - public Handler(ILogger logger, ImportService importService, FloorService floorService, ConfigWindow configWindow) + public Handler(ILogger logger, ImportService importService, ConfigWindow configWindow) : base(logger) { _importService = importService; - _floorService = floorService; _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; - saveMarkers = true; - - foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) - { - var localState = _floorService.GetFloorMarkers((ushort)territoryType); - localState.UndoImport(new List { queued.ExportId }); - localState.Save(); - } _importService.RemoveById(queued.ExportId); _configWindow.UpdateLastImport(); diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index fed5212..5c7c44e 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -25,6 +25,7 @@ using Pal.Client.Configuration; using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.Extensions; +using Pal.Client.Floors; namespace Pal.Client.Windows { @@ -382,24 +383,25 @@ namespace Pal.Client.Windows ImGui.Text($"{_debugState.DebugMessage}"); ImGui.Indent(); - if (_floorService.FloorMarkers.TryGetValue(_territoryState.LastTerritory, out var currentFloor)) + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory != null) { 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")}"); } 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")}"); } if (_silverConfig.Show) { int silverCoffers = - _floorService.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); + _floorService.EphemeralLocations.Count(x => x.Type == MemoryLocation.EType.SilverCoffer); ImGui.Text( $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); }