427 lines
17 KiB
C#
427 lines
17 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using Dalamud.Game;
|
|
using Dalamud.Game.ClientState;
|
|
using Dalamud.Game.ClientState.Objects;
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Dalamud.Logging;
|
|
using ImGuiNET;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Pal.Client.Configuration;
|
|
using Pal.Client.Extensions;
|
|
using Pal.Client.Net;
|
|
using Pal.Client.Rendering;
|
|
using Pal.Client.Scheduled;
|
|
|
|
namespace Pal.Client.DependencyInjection
|
|
{
|
|
internal sealed class FrameworkService : IDisposable
|
|
{
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly Framework _framework;
|
|
private readonly ConfigurationManager _configurationManager;
|
|
private readonly IPalacePalConfiguration _configuration;
|
|
private readonly ClientState _clientState;
|
|
private readonly TerritoryState _territoryState;
|
|
private readonly FloorService _floorService;
|
|
private readonly DebugState _debugState;
|
|
private readonly RenderAdapter _renderAdapter;
|
|
private readonly ObjectTable _objectTable;
|
|
private readonly RemoteApi _remoteApi;
|
|
|
|
internal Queue<IQueueOnFrameworkThread> EarlyEventQueue { get; } = new();
|
|
internal Queue<IQueueOnFrameworkThread> LateEventQueue { get; } = new();
|
|
internal ConcurrentQueue<nint> NextUpdateObjects { get; } = new();
|
|
|
|
public FrameworkService(
|
|
IServiceProvider serviceProvider,
|
|
Framework framework,
|
|
ConfigurationManager configurationManager,
|
|
IPalacePalConfiguration configuration,
|
|
ClientState clientState,
|
|
TerritoryState territoryState,
|
|
FloorService floorService,
|
|
DebugState debugState,
|
|
RenderAdapter renderAdapter,
|
|
ObjectTable objectTable,
|
|
RemoteApi remoteApi)
|
|
{
|
|
_serviceProvider = serviceProvider;
|
|
_framework = framework;
|
|
_configurationManager = configurationManager;
|
|
_configuration = configuration;
|
|
_clientState = clientState;
|
|
_territoryState = territoryState;
|
|
_floorService = floorService;
|
|
_debugState = debugState;
|
|
_renderAdapter = renderAdapter;
|
|
_objectTable = objectTable;
|
|
_remoteApi = remoteApi;
|
|
|
|
_framework.Update += OnUpdate;
|
|
_configurationManager.Saved += OnSaved;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_framework.Update -= OnUpdate;
|
|
_configurationManager.Saved -= OnSaved;
|
|
}
|
|
|
|
private void OnSaved(object? sender, IPalacePalConfiguration? config)
|
|
=> EarlyEventQueue.Enqueue(new QueuedConfigUpdate());
|
|
|
|
private void OnUpdate(Framework framework)
|
|
{
|
|
if (_configuration.FirstUse)
|
|
return;
|
|
|
|
try
|
|
{
|
|
bool recreateLayout = false;
|
|
bool saveMarkers = false;
|
|
|
|
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
|
HandleQueued(queued, ref recreateLayout, ref saveMarkers);
|
|
|
|
if (_territoryState.LastTerritory != _clientState.TerritoryType)
|
|
{
|
|
_territoryState.LastTerritory = _clientState.TerritoryType;
|
|
_territoryState.TerritorySyncState = SyncState.NotAttempted;
|
|
NextUpdateObjects.Clear();
|
|
|
|
if (_territoryState.IsInDeepDungeon())
|
|
_floorService.GetFloorMarkers(_territoryState.LastTerritory);
|
|
_floorService.EphemeralMarkers.Clear();
|
|
_territoryState.PomanderOfSight = PomanderState.Inactive;
|
|
_territoryState.PomanderOfIntuition = PomanderState.Inactive;
|
|
recreateLayout = true;
|
|
_debugState.Reset();
|
|
}
|
|
|
|
if (!_territoryState.IsInDeepDungeon())
|
|
return;
|
|
|
|
if (_configuration.Mode == EMode.Online && _territoryState.TerritorySyncState == SyncState.NotAttempted)
|
|
{
|
|
_territoryState.TerritorySyncState = SyncState.Started;
|
|
Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory));
|
|
}
|
|
|
|
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
|
HandleQueued(queued, ref recreateLayout, ref saveMarkers);
|
|
|
|
var currentFloor = _floorService.GetFloorMarkers(_territoryState.LastTerritory);
|
|
|
|
IList<Marker> visibleMarkers = GetRelevantGameObjects();
|
|
HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers,
|
|
recreateLayout);
|
|
HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_debugState.SetFromException(e);
|
|
}
|
|
}
|
|
|
|
#region Render Markers
|
|
|
|
private void HandlePersistentMarkers(LocalState currentFloor, IList<Marker> visibleMarkers, bool saveMarkers,
|
|
bool recreateLayout)
|
|
{
|
|
var currentFloorMarkers = currentFloor.Markers;
|
|
|
|
bool updateSeenMarkers = false;
|
|
var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
|
|
foreach (var visibleMarker in visibleMarkers)
|
|
{
|
|
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;
|
|
}
|
|
|
|
if (!recreateLayout && currentFloorMarkers.Count > 0 &&
|
|
(_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
|
|
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander))
|
|
{
|
|
try
|
|
{
|
|
foreach (var marker in currentFloorMarkers)
|
|
{
|
|
uint desiredColor = DetermineColor(marker, visibleMarkers);
|
|
if (marker.RenderElement == null || !marker.RenderElement.IsValid)
|
|
{
|
|
recreateLayout = true;
|
|
break;
|
|
}
|
|
|
|
if (marker.RenderElement.Color != desiredColor)
|
|
marker.RenderElement.Color = desiredColor;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_debugState.SetFromException(e);
|
|
recreateLayout = true;
|
|
}
|
|
}
|
|
|
|
if (updateSeenMarkers && partialAccountId != null)
|
|
{
|
|
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)
|
|
{
|
|
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<IRenderElement> 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);
|
|
}
|
|
}
|
|
|
|
private void HandleEphemeralMarkers(IList<Marker> visibleMarkers, bool recreateLayout)
|
|
{
|
|
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);
|
|
_floorService.EphemeralMarkers.Clear();
|
|
|
|
List<IRenderElement> elements = new();
|
|
foreach (var marker in visibleMarkers)
|
|
{
|
|
_floorService.EphemeralMarkers.Add(marker);
|
|
|
|
if (marker.Type == Marker.EType.SilverCoffer && _configuration.DeepDungeons.SilverCoffers.Show)
|
|
{
|
|
CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers),
|
|
_configuration.DeepDungeons.SilverCoffers);
|
|
}
|
|
}
|
|
|
|
if (elements.Count == 0)
|
|
return;
|
|
|
|
_renderAdapter.SetLayer(ELayer.RegularCoffers, elements);
|
|
}
|
|
}
|
|
|
|
private uint DetermineColor(Marker marker, IList<Marker> visibleMarkers)
|
|
{
|
|
switch (marker.Type)
|
|
{
|
|
case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive ||
|
|
!_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
|
|
visibleMarkers.Any(x => x == marker):
|
|
return _configuration.DeepDungeons.Traps.Color;
|
|
case Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive ||
|
|
!_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander ||
|
|
visibleMarkers.Any(x => x == marker):
|
|
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));
|
|
}
|
|
}
|
|
|
|
private void CreateRenderElement(Marker marker, List<IRenderElement> elements, uint color,
|
|
MarkerConfiguration config)
|
|
{
|
|
if (!config.Show)
|
|
return;
|
|
|
|
var element = _renderAdapter.CreateElement(marker.Type, marker.Position, color, config.Fill);
|
|
marker.RenderElement = element;
|
|
elements.Add(element);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Up-/Download
|
|
|
|
private async Task DownloadMarkersForTerritory(ushort territoryId)
|
|
{
|
|
try
|
|
{
|
|
var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
|
|
LateEventQueue.Enqueue(new QueuedSyncResponse
|
|
{
|
|
Type = SyncType.Download,
|
|
TerritoryType = territoryId,
|
|
Success = success,
|
|
Markers = downloadedMarkers
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_debugState.SetFromException(e);
|
|
}
|
|
}
|
|
|
|
private async Task UploadMarkersForTerritory(ushort territoryId, List<Marker> markersToUpload)
|
|
{
|
|
try
|
|
{
|
|
var (success, uploadedMarkers) = await _remoteApi.UploadMarker(territoryId, markersToUpload);
|
|
LateEventQueue.Enqueue(new QueuedSyncResponse
|
|
{
|
|
Type = SyncType.Upload,
|
|
TerritoryType = territoryId,
|
|
Success = success,
|
|
Markers = uploadedMarkers
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_debugState.SetFromException(e);
|
|
}
|
|
}
|
|
|
|
private async Task SyncSeenMarkersForTerritory(ushort territoryId, List<Marker> markersToUpdate)
|
|
{
|
|
try
|
|
{
|
|
var success = await _remoteApi.MarkAsSeen(territoryId, markersToUpdate);
|
|
LateEventQueue.Enqueue(new QueuedSyncResponse
|
|
{
|
|
Type = SyncType.MarkSeen,
|
|
TerritoryType = territoryId,
|
|
Success = success,
|
|
Markers = markersToUpdate,
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_debugState.SetFromException(e);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private IList<Marker> GetRelevantGameObjects()
|
|
{
|
|
List<Marker> result = new();
|
|
for (int i = 246; i < _objectTable.Length; i++)
|
|
{
|
|
GameObject? obj = _objectTable[i];
|
|
if (obj == null)
|
|
continue;
|
|
|
|
switch ((uint)Marshal.ReadInt32(obj.Address + 128))
|
|
{
|
|
case 2007182:
|
|
case 2007183:
|
|
case 2007184:
|
|
case 2007185:
|
|
case 2007186:
|
|
case 2009504:
|
|
result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true });
|
|
break;
|
|
|
|
case 2007542:
|
|
case 2007543:
|
|
result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true });
|
|
break;
|
|
|
|
case 2007357:
|
|
result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true });
|
|
break;
|
|
}
|
|
}
|
|
|
|
while (NextUpdateObjects.TryDequeue(out nint address))
|
|
{
|
|
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 });
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers)
|
|
{
|
|
Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType());
|
|
var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType);
|
|
|
|
handler.RunIfCompatible(queued, ref recreateLayout, ref saveMarkers);
|
|
}
|
|
}
|
|
}
|