add the whole API stuff first iteration

This commit is contained in:
Stanley Dimant
2022-06-19 01:57:37 +02:00
parent 176eb2a344
commit 1312086a8d
10 changed files with 692 additions and 92 deletions

View File

@@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "..\..\
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "..\..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{2F26FC2D-03DF-445F-A87B-8708D621E86C}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "..\..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{2F26FC2D-03DF-445F-A87B-8708D621E86C}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "..\server\MareSynchronosServer\MareSynchronos.API\MareSynchronos.API.csproj", "{4C92F86D-9C84-4F58-9C1A-671AEBACA256}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -41,6 +43,14 @@ Global
{2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|Any CPU.Build.0 = Release|Any CPU {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|Any CPU.Build.0 = Release|Any CPU
{2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|x64.ActiveCfg = Release|Any CPU {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|x64.ActiveCfg = Release|Any CPU
{2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|x64.Build.0 = Release|Any CPU {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|x64.Build.0 = Release|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Debug|x64.ActiveCfg = Debug|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Debug|x64.Build.0 = Debug|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Release|Any CPU.Build.0 = Release|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Release|x64.ActiveCfg = Release|Any CPU
{4C92F86D-9C84-4F58-9C1A-671AEBACA256}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -17,48 +17,58 @@ using Penumbra.PlayerWatch;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Configuration;
using Dalamud.Game.ClientState.Objects.SubKinds;
using MareSynchronos.API;
using MareSynchronos.FileCacheDB;
namespace MareSynchronos.Managers namespace MareSynchronos.Managers
{ {
public class CharacterManager : IDisposable public class CharacterManager : IDisposable
{ {
private readonly ClientState clientState; private readonly ClientState clientState;
private readonly Framework framework; private readonly Framework _framework;
private readonly ApiController apiController; private readonly ApiController apiController;
private readonly ObjectTable objectTable; private readonly ObjectTable objectTable;
private readonly IpcManager ipcManager; private readonly IpcManager ipcManager;
private readonly FileReplacementFactory factory; private readonly FileReplacementFactory factory;
private readonly Configuration _pluginConfiguration;
private readonly IPlayerWatcher watcher; private readonly IPlayerWatcher watcher;
private Task? playerChangedTask = null; private Task? playerChangedTask = null;
public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, FileReplacementFactory factory) public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, FileReplacementFactory factory,
Configuration pluginConfiguration)
{ {
this.clientState = clientState; this.clientState = clientState;
this.framework = framework; this._framework = framework;
this.apiController = apiController; this.apiController = apiController;
this.objectTable = objectTable; this.objectTable = objectTable;
this.ipcManager = ipcManager; this.ipcManager = ipcManager;
this.factory = factory; this.factory = factory;
_pluginConfiguration = pluginConfiguration;
watcher = PlayerWatchFactory.Create(framework, clientState, objectTable); watcher = PlayerWatchFactory.Create(framework, clientState, objectTable);
clientState.TerritoryChanged += ClientState_TerritoryChanged;
framework.Update += Framework_Update;
ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent;
} }
private void IpcManager_PenumbraRedrawEvent(object? sender, EventArgs e) private void IpcManager_PenumbraRedrawEvent(object? objectTableIndex, EventArgs e)
{ {
var actorName = ((string)sender!); var objTableObj = objectTable[(int)objectTableIndex!];
PluginLog.Debug("Penumbra redraw " + actorName); if (objTableObj!.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player)
if (actorName == GetPlayerName())
{ {
PlayerChanged(actorName); if (objTableObj.Name.ToString() == GetPlayerName())
{
PluginLog.Debug("Penumbra Redraw Event");
PlayerChanged(GetPlayerName());
}
} }
} }
private readonly Dictionary<(string, int), CharacterCacheDto> _characterCache = new();
Dictionary<string, string> localPlayers = new(); Dictionary<string, string> localPlayers = new();
private DateTime lastCheck = DateTime.Now; private DateTime lastCheck = DateTime.Now;
@@ -71,13 +81,15 @@ namespace MareSynchronos.Managers
if (DateTime.Now < lastCheck.AddSeconds(5)) return; if (DateTime.Now < lastCheck.AddSeconds(5)) return;
List<string> localPlayersList = new(); List<string> localPlayersList = new();
List<string> newPlayers = new();
foreach (var obj in objectTable) foreach (var obj in objectTable)
{ {
if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
string playerName = obj.Name.ToString(); string playerName = obj.Name.ToString();
if (playerName == clientState.LocalPlayer.Name.ToString()) continue; if (playerName == GetPlayerName()) continue;
var hashedName = Crypto.GetHash(playerName); var pObj = (PlayerCharacter)obj;
var hashedName = Crypto.GetHash256(pObj.Name.ToString() + pObj.HomeWorld.Id.ToString());
localPlayersList.Add(hashedName);
localPlayers[hashedName] = pObj.Name.ToString();
} }
foreach (var item in localPlayers.ToList()) foreach (var item in localPlayers.ToList())
@@ -88,8 +100,6 @@ namespace MareSynchronos.Managers
} }
} }
if (newPlayers.Any())
PluginLog.Debug("New players: " + string.Join(",", newPlayers.Select(p => p + ":" + localPlayers[p])));
lastCheck = DateTime.Now; lastCheck = DateTime.Now;
} }
catch (Exception ex) catch (Exception ex)
@@ -128,7 +138,7 @@ namespace MareSynchronos.Managers
PluginLog.Debug("Waiting for charater to be drawn"); PluginLog.Debug("Waiting for charater to be drawn");
while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something
{ {
PluginLog.Debug("Waiting for character to finish drawing"); //PluginLog.Debug("Waiting for character to finish drawing");
Thread.Sleep(10); Thread.Sleep(10);
} }
PluginLog.Debug("Character finished drawing"); PluginLog.Debug("Character finished drawing");
@@ -142,14 +152,34 @@ namespace MareSynchronos.Managers
Thread.Sleep(50); Thread.Sleep(50);
} }
_ = apiController.SendCharacterData(cache.Result); _ = apiController.SendCharacterData(cache.Result.ToCharacterCacheDto(), GetLocalPlayers().Select(d => d.Key).ToList());
}); });
} }
private Dictionary<string, PlayerCharacter> GetLocalPlayers()
{
Dictionary<string, PlayerCharacter> allLocalPlayers = new();
foreach (var obj in objectTable)
{
if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
string playerName = obj.Name.ToString();
if (playerName == GetPlayerName()) continue;
var playerObject = (PlayerCharacter)obj;
allLocalPlayers.Add(Crypto.GetHash256(playerObject.Name.ToString() + playerObject.HomeWorld.Id.ToString()), playerObject);
}
return allLocalPlayers;
}
public unsafe CharacterCache BuildCharacterCache() public unsafe CharacterCache BuildCharacterCache()
{ {
var cache = new CharacterCache(); var cache = new CharacterCache();
while (clientState.LocalPlayer == null)
{
PluginLog.Debug("Character is null but it shouldn't be, waiting");
Thread.Sleep(50);
}
var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject();
for (var idx = 0; idx < model->SlotCount; ++idx) for (var idx = 0; idx < model->SlotCount; ++idx)
{ {
@@ -213,21 +243,193 @@ namespace MareSynchronos.Managers
private void ClientState_TerritoryChanged(object? sender, ushort e) private void ClientState_TerritoryChanged(object? sender, ushort e)
{ {
localPlayers.Clear(); localPlayers.Clear();
_ = Task.Run(async () =>
{
while (clientState.LocalPlayer == null)
{
await Task.Delay(250);
}
await AssignLocalPlayersData();
});
}
private async Task AssignLocalPlayersData()
{
PluginLog.Debug("Temp assigning local players from cache");
var currentLocalPlayers = GetLocalPlayers();
foreach (var player in _characterCache)
{
if (currentLocalPlayers.ContainsKey(player.Key.Item1))
{
await Task.Run(() => ApiControllerOnCharacterReceived(null, new CharacterReceivedEventArgs
{
CharacterNameHash = player.Key.Item1,
CharacterData = player.Value
}));
}
}
PluginLog.Debug("Updating local players from service");
await apiController.GetCharacterData(currentLocalPlayers
.ToDictionary(
k => k.Key,
k => (int)k.Value.ClassJob.Id));
} }
public void Dispose() public void Dispose()
{ {
framework.Update -= Framework_Update; ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent;
_framework.Update -= Framework_Update;
clientState.TerritoryChanged -= ClientState_TerritoryChanged; clientState.TerritoryChanged -= ClientState_TerritoryChanged;
apiController.Connected -= ApiController_Connected;
apiController.Disconnected -= ApiController_Disconnected;
apiController.CharacterReceived -= ApiControllerOnCharacterReceived;
apiController.RemovedFromWhitelist -= ApiControllerOnRemovedFromWhitelist;
apiController.AddedToWhitelist -= ApiControllerOnAddedToWhitelist;
watcher.Disable();
watcher.PlayerChanged -= Watcher_PlayerChanged; watcher.PlayerChanged -= Watcher_PlayerChanged;
watcher?.Dispose(); watcher?.Dispose();
} }
internal void StartWatchingPlayer() internal void StartWatchingPlayer()
{ {
watcher.AddPlayerToWatch(clientState.LocalPlayer!.Name.ToString()); watcher.AddPlayerToWatch(GetPlayerName());
watcher.PlayerChanged += Watcher_PlayerChanged; watcher.PlayerChanged += Watcher_PlayerChanged;
watcher.Enable(); watcher.Enable();
apiController.Connected += ApiController_Connected;
apiController.Disconnected += ApiController_Disconnected;
apiController.CharacterReceived += ApiControllerOnCharacterReceived;
apiController.RemovedFromWhitelist += ApiControllerOnRemovedFromWhitelist;
apiController.AddedToWhitelist += ApiControllerOnAddedToWhitelist;
PluginLog.Debug("Watching Player, ApiController is Connected: " + apiController.IsConnected);
if (apiController.IsConnected)
{
ApiController_Connected(null, EventArgs.Empty);
}
}
private void ApiControllerOnRemovedFromWhitelist(object? sender, EventArgs e)
{
var characterHash = (string?)sender;
if (string.IsNullOrEmpty(characterHash)) return;
var players = GetLocalPlayers();
foreach (var entry in _characterCache.Where(c => c.Key.Item1 == characterHash))
{
_characterCache.Remove(entry.Key);
}
var playerName = players.SingleOrDefault(p => p.Key == characterHash).Value.Name.ToString() ?? null;
if (playerName != null)
{
PluginLog.Debug("You got removed from whitelist, restoring glamourer state for " + playerName);
ipcManager.GlamourerRevertCharacterCustomization(playerName);
}
}
private void ApiControllerOnAddedToWhitelist(object? sender, EventArgs e)
{
var characterHash = (string?)sender;
if (string.IsNullOrEmpty(characterHash)) return;
var players = GetLocalPlayers();
if (players.ContainsKey(characterHash))
{
PluginLog.Debug("You got added to a whitelist, restoring data for " + characterHash);
_ = apiController.GetCharacterData(new Dictionary<string, int> { { characterHash, (int)players[characterHash].ClassJob.Id } });
}
}
private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e)
{
PlayerCharacter? playerObject = null;
PluginLog.Debug("Received hash for " + e.CharacterNameHash);
foreach (var obj in objectTable)
{
if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
string playerName = obj.Name.ToString();
if (playerName == GetPlayerName()) continue;
playerObject = (PlayerCharacter)obj;
var hashedName = Crypto.GetHash256(playerObject.Name.ToString() + playerObject.HomeWorld.Id.ToString());
if (e.CharacterNameHash == hashedName)
{
break;
}
playerObject = null;
}
if (playerObject == null)
{
PluginLog.Debug("Found no suitable hash for " + e.CharacterNameHash);
return;
}
else
{
PluginLog.Debug("Found suitable player for hash: " + playerObject.Name.ToString());
}
_characterCache[(e.CharacterNameHash, e.CharacterData.JobId)] = e.CharacterData;
foreach (var file in e.CharacterData.FileReplacements)
{
var hash = file.Hash;
bool hasLocalFile;
using (var db = new FileCacheContext())
{
hasLocalFile = db.FileCaches.Any(f => f.Hash == hash);
}
if (hasLocalFile) continue;
PluginLog.Debug("Downloading file for " + hash);
var task = apiController.DownloadData(hash);
while (!task.IsCompleted)
{
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
PluginLog.Debug("Download finished: " + hash);
var extractedFile = LZ4.LZ4Codec.Unwrap(task.Result);
var ext = file.GamePaths.First().Split(".", StringSplitOptions.None).Last();
var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash + "." + ext);
File.WriteAllBytes(filePath, extractedFile);
PluginLog.Debug("File written to : " + filePath);
using (var db = new FileCacheContext())
{
db.Add(new FileCache
{
Filepath = filePath.ToLower(),
Hash = file.Hash,
LastModifiedDate = DateTime.Now.Ticks.ToString(),
});
db.SaveChanges();
}
PluginLog.Debug("Added hash to db: " + hash);
}
PluginLog.Debug("Assigned hash to visible player: " + playerObject.Name.ToString());
ipcManager.GlamourerApplyCharacterCustomization(e.CharacterData.GlamourerData, playerObject.Name.ToString());
}
private void ApiController_Disconnected(object? sender, EventArgs args)
{
PluginLog.Debug(nameof(ApiController_Disconnected));
_framework.Update -= Framework_Update;
ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent;
clientState.TerritoryChanged -= ClientState_TerritoryChanged;
}
private void ApiController_Connected(object? sender, EventArgs args)
{
PluginLog.Debug(nameof(ApiController_Connected));
PluginLog.Debug("MyHashedName:" + Crypto.GetHash256(GetPlayerName() + clientState.LocalPlayer!.HomeWorld.Id));
var apiTask = apiController.SendCharacterName(Crypto.GetHash256(GetPlayerName() + clientState.LocalPlayer!.HomeWorld.Id));
var assignTask = AssignLocalPlayersData();
Task.WaitAll(apiTask, assignTask);
_framework.Update += Framework_Update;
ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent;
clientState.TerritoryChanged += ClientState_TerritoryChanged;
} }
public void StopWatchPlayer(string name) public void StopWatchPlayer(string name)

View File

@@ -15,9 +15,10 @@ namespace MareSynchronos.Managers
private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization; private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization;
private ICallGateSubscriber<int> penumbraApiVersion; private ICallGateSubscriber<int> penumbraApiVersion;
private ICallGateSubscriber<int> glamourerApiVersion; private ICallGateSubscriber<int> glamourerApiVersion;
private ICallGateSubscriber<string, string> penumbraObjectIsRedrawn; private ICallGateSubscriber<IntPtr, int, object?> penumbraObjectIsRedrawn;
private ICallGateSubscriber<string, int, object>? penumbraRedraw; private ICallGateSubscriber<string, int, object>? penumbraRedraw;
private ICallGateSubscriber<string, string, string[]>? penumbraReverseResolvePath; private ICallGateSubscriber<string, string, string[]>? penumbraReverseResolvePath;
private ICallGateSubscriber<string, object> glamourerRevertCustomization;
public bool Initialized { get; private set; } = false; public bool Initialized { get; private set; } = false;
@@ -36,7 +37,8 @@ namespace MareSynchronos.Managers
penumbraReverseResolvePath = pluginInterface.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath"); penumbraReverseResolvePath = pluginInterface.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath");
penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion"); penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion");
glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion"); glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion");
penumbraObjectIsRedrawn = pluginInterface.GetIpcSubscriber<string, string>("Penumbra.ObjectIsRedrawn"); glamourerRevertCustomization = pluginInterface.GetIpcSubscriber<string, object>("Glamourer.RevertCharacterCustomization");
penumbraObjectIsRedrawn = pluginInterface.GetIpcSubscriber<IntPtr, int, object?>("Penumbra.GameObjectRedrawn");
penumbraObjectIsRedrawn.Subscribe(RedrawEvent); penumbraObjectIsRedrawn.Subscribe(RedrawEvent);
penumbraInit.Subscribe(RedrawSelf); penumbraInit.Subscribe(RedrawSelf);
@@ -67,9 +69,9 @@ namespace MareSynchronos.Managers
} }
} }
private void RedrawEvent(string actorName) private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
{ {
PenumbraRedrawEvent?.Invoke(actorName, EventArgs.Empty); PenumbraRedrawEvent?.Invoke(objectTableIndex, EventArgs.Empty);
} }
private void RedrawSelf() private void RedrawSelf()
@@ -120,6 +122,12 @@ namespace MareSynchronos.Managers
glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName); glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName);
} }
public void GlamourerRevertCharacterCustomization(string characterName)
{
if (!CheckGlamourerAPI()) return;
glamourerRevertCustomization!.InvokeAction(characterName);
}
public void PenumbraRedraw(string actorName) public void PenumbraRedraw(string actorName)
{ {
if (!CheckPenumbraAPI()) return; if (!CheckPenumbraAPI()) return;

View File

@@ -27,6 +27,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.7" /> <PackageReference Include="DalamudPackager" Version="2.1.7" />
<PackageReference Include="lz4net" Version="1.0.15.93" /> <PackageReference Include="lz4net" Version="1.0.15.93" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.17"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.17">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -39,6 +40,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" /> <ProjectReference Include="..\..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" />
<ProjectReference Include="..\..\..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj" /> <ProjectReference Include="..\..\..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj" />
<ProjectReference Include="..\..\server\MareSynchronosServer\MareSynchronos.API\MareSynchronos.API.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -5,12 +5,24 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MareSynchronos.API;
namespace MareSynchronos.Models namespace MareSynchronos.Models
{ {
[JsonObject(MemberSerialization.OptIn)] [JsonObject(MemberSerialization.OptIn)]
public class CharacterCache public class CharacterCache
{ {
public CharacterCacheDto ToCharacterCacheDto()
{
return new CharacterCacheDto()
{
FileReplacements = AllReplacements.Select(f => f.ToFileReplacementDto()).ToList(),
GlamourerData = GlamourerString,
Hash = CacheHash,
JobId = (int)JobId
};
}
[JsonProperty] [JsonProperty]
public List<FileReplacement> AllReplacements => public List<FileReplacement> AllReplacements =>
FileReplacements.Where(f => f.HasFileReplacement) FileReplacements.Where(f => f.HasFileReplacement)

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using MareSynchronos.FileCacheDB; using MareSynchronos.FileCacheDB;
using System.IO; using System.IO;
using MareSynchronos.API;
using MareSynchronos.Utils; using MareSynchronos.Utils;
namespace MareSynchronos.Models namespace MareSynchronos.Models
@@ -14,6 +15,16 @@ namespace MareSynchronos.Models
[JsonObject(MemberSerialization.OptIn)] [JsonObject(MemberSerialization.OptIn)]
public class FileReplacement public class FileReplacement
{ {
public FileReplacementDto ToFileReplacementDto()
{
return new FileReplacementDto
{
GamePaths = GamePaths,
Hash = Hash,
ImcData = ImcData
};
}
private readonly string penumbraDirectory; private readonly string penumbraDirectory;
[JsonProperty] [JsonProperty]
@@ -88,13 +99,20 @@ namespace MareSynchronos.Models
var fileAddedDuringCompute = db.FileCaches.SingleOrDefault(f => f.Filepath == fi.FullName.ToLower()); var fileAddedDuringCompute = db.FileCaches.SingleOrDefault(f => f.Filepath == fi.FullName.ToLower());
if (fileAddedDuringCompute != null) return fileAddedDuringCompute.Hash; if (fileAddedDuringCompute != null) return fileAddedDuringCompute.Hash;
db.Add(new FileCache() try
{ {
Hash = hash, db.Add(new FileCache()
Filepath = fi.FullName.ToLower(), {
LastModifiedDate = fi.LastWriteTimeUtc.Ticks.ToString() Hash = hash,
}); Filepath = fi.FullName.ToLower(),
db.SaveChanges(); LastModifiedDate = fi.LastWriteTimeUtc.Ticks.ToString()
});
db.SaveChanges();
}
catch (Exception ex)
{
PluginLog.Error(ex, "Error adding files to database. Most likely not an issue though.");
}
return hash; return hash;
} }

View File

@@ -91,7 +91,7 @@ namespace MareSynchronos
} }
characterManager = new CharacterManager( characterManager = new CharacterManager(
clientState, framework, apiController, objectTable, ipcManager, new FileReplacementFactory(ipcManager)); clientState, framework, apiController, objectTable, ipcManager, new FileReplacementFactory(ipcManager), Configuration);
characterManager.StartWatchingPlayer(); characterManager.StartWatchingPlayer();
ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString()); ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString());
}); });
@@ -131,7 +131,7 @@ namespace MareSynchronos
{ {
Stopwatch st = Stopwatch.StartNew(); Stopwatch st = Stopwatch.StartNew();
File.WriteAllBytes(lc4hcPath, LZ4Codec.Encode(File.ReadAllBytes(fileCache.Filepath), 0, (int)new FileInfo(fileCache.Filepath).Length)); File.WriteAllBytes(lc4hcPath, LZ4Codec.WrapHC(File.ReadAllBytes(fileCache.Filepath), 0, (int)new FileInfo(fileCache.Filepath).Length));
st.Stop(); st.Stop();
PluginLog.Debug("Compressed " + new FileInfo(fileCache.Filepath).Length + " bytes to " + new FileInfo(lc4hcPath).Length + " bytes in " + st.Elapsed); PluginLog.Debug("Compressed " + new FileInfo(fileCache.Filepath).Length + " bytes to " + new FileInfo(lc4hcPath).Length + " bytes in " + st.Elapsed);
File.Copy(fileCache.Filepath, newFilePath); File.Copy(fileCache.Filepath, newFilePath);
@@ -226,6 +226,11 @@ namespace MareSynchronos
PluginLog.Debug("Mod created to " + modDirectoryPath); PluginLog.Debug("Mod created to " + modDirectoryPath);
}); });
} }
if (string.IsNullOrEmpty(args))
{
PluginUi.Toggle();
}
} }
} }
} }

View File

@@ -6,7 +6,10 @@ using ImGuiNET;
using MareSynchronos.Managers; using MareSynchronos.Managers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using System; using System;
using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Configuration;
using MareSynchronos.API;
namespace MareSynchronos namespace MareSynchronos
{ {
@@ -49,7 +52,17 @@ namespace MareSynchronos
return; return;
} }
if (string.IsNullOrEmpty(apiController.SecretKey))
if (apiController.SecretKey != "-" && !apiController.IsConnected && apiController.ServerAlive)
{
if (ImGui.Button("Reset Secret Key"))
{
configuration.ClientSecret.Clear();
configuration.Save();
apiController.RestartHeartbeat();
}
}
else if (apiController.SecretKey == "-")
{ {
DrawIntroContent(); DrawIntroContent();
} }
@@ -67,15 +80,95 @@ namespace MareSynchronos
ImGui.Separator(); ImGui.Separator();
ImGui.Text("Your UID"); ImGui.Text("Your UID");
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGreen, apiController.UID); if (apiController.ServerAlive)
ImGui.SameLine();
if (ImGui.Button("Copy UID"))
{ {
ImGui.SetClipboardText(apiController.UID); ImGui.TextColored(ImGuiColors.ParsedGreen, apiController.UID);
ImGui.SameLine();
if (ImGui.Button("Copy UID"))
{
ImGui.SetClipboardText(apiController.UID);
}
ImGui.Text("Share this UID to other Mare users so they can add you to their whitelist.");
ImGui.Separator();
DrawWhiteListContent();
ImGui.Separator();
string cachePath = configuration.CacheFolder;
if (ImGui.InputText("CachePath", ref cachePath, 255))
{
configuration.CacheFolder = cachePath;
configuration.Save();
}
}
else
{
ImGui.TextColored(ImGuiColors.DalamudRed, "Service unavailable");
} }
ImGui.Text("Share this UID to other Mare users so they can add you to their whitelist.");
} }
private void DrawWhiteListContent()
{
if (!apiController.ServerAlive) return;
ImGui.Text("Whitelists");
if (ImGui.BeginTable("WhitelistTable", 5))
{
ImGui.TableSetupColumn("Pause");
ImGui.TableSetupColumn("UID");
ImGui.TableSetupColumn("Sync");
ImGui.TableSetupColumn("Paused (you/other)");
ImGui.TableSetupColumn("");
ImGui.TableHeadersRow();
foreach (var item in apiController.WhitelistEntries.ToList())
{
ImGui.TableNextColumn();
bool isPaused = item.IsPaused;
if (ImGui.Checkbox("Paused##" + item.OtherUID, ref isPaused))
{
_ = apiController.SendWhitelistPauseChange(item.OtherUID, isPaused);
}
ImGui.TableNextColumn();
ImGui.TextColored(GetBoolColor(item.IsSynced && !item.IsPausedFromOthers && !item.IsPaused),
item.OtherUID);
ImGui.TableNextColumn();
ImGui.TextColored(GetBoolColor(item.IsSynced), !item.IsSynced ? "Has not added you" : "On both whitelists");
ImGui.TableNextColumn();
ImGui.TextColored(GetBoolColor((!item.IsPausedFromOthers && !item.IsPaused)), item.IsPaused + " / " + item.IsPausedFromOthers);
ImGui.TableNextColumn();
if (ImGui.Button("Delete##" + item.OtherUID))
{
_ = apiController.SendWhitelistRemoval(item.OtherUID);
apiController.WhitelistEntries.Remove(item);
}
ImGui.TableNextRow();
}
ImGui.EndTable();
}
var whitelistEntry = tempDto.OtherUID;
if (ImGui.InputText("Add new whitelist entry", ref whitelistEntry, 20))
{
tempDto.OtherUID = whitelistEntry;
}
ImGui.SameLine();
if (ImGui.Button("Add"))
{
if (apiController.WhitelistEntries.All(w => w.OtherUID != tempDto.OtherUID))
{
apiController.WhitelistEntries.Add(new WhitelistDto()
{
OtherUID = tempDto.OtherUID
});
_ = apiController.SendWhitelistAddition(tempDto.OtherUID);
tempDto.OtherUID = string.Empty;
}
}
}
private WhitelistDto tempDto = new WhitelistDto() { OtherUID = string.Empty };
private int serverSelectionIndex = 0; private int serverSelectionIndex = 0;
private async void DrawIntroContent() private async void DrawIntroContent()
@@ -141,13 +234,19 @@ namespace MareSynchronos
if (apiController.UseCustomService) if (apiController.UseCustomService)
{ {
string serviceAddress = configuration.ApiUri; string serviceAddress = configuration.ApiUri;
ImGui.InputText("Service address", ref serviceAddress, 255); if (ImGui.InputText("Service address", ref serviceAddress, 255))
configuration.ApiUri = serviceAddress; {
configuration.Save(); if (configuration.ApiUri != serviceAddress)
{
configuration.ApiUri = serviceAddress;
apiController.RestartHeartbeat();
configuration.Save();
}
}
} }
PrintServerState(); PrintServerState();
if (apiController.IsConnected) if (apiController.ServerAlive)
{ {
if (ImGui.Button("Register")) if (ImGui.Button("Register"))
{ {
@@ -159,6 +258,8 @@ namespace MareSynchronos
} }
} }
private Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
private bool OtherPluginStateOk() private bool OtherPluginStateOk()
{ {
var penumbraExists = ipcManager.CheckPenumbraAPI(); var penumbraExists = ipcManager.CheckPenumbraAPI();
@@ -186,8 +287,8 @@ namespace MareSynchronos
{ {
ImGui.Text("Service status of " + (string.IsNullOrEmpty(configuration.ApiUri) ? mainServer : configuration.ApiUri)); ImGui.Text("Service status of " + (string.IsNullOrEmpty(configuration.ApiUri) ? mainServer : configuration.ApiUri));
ImGui.SameLine(); ImGui.SameLine();
var color = apiController.IsConnected ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; var color = apiController.ServerAlive ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
ImGui.TextColored(color, apiController.IsConnected ? "Available" : "Unavailable"); ImGui.TextColored(color, apiController.ServerAlive ? "Available" : "Unavailable");
} }
} }
} }

View File

@@ -18,5 +18,11 @@ namespace MareSynchronos.Utils
using SHA1CryptoServiceProvider cryptoProvider = new(); using SHA1CryptoServiceProvider cryptoProvider = new();
return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", "");
} }
public static string GetHash256(string stringToHash)
{
using SHA256CryptoServiceProvider cryptoProvider = new();
return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", "");
}
} }
} }

View File

@@ -2,20 +2,38 @@
using Dalamud.Logging; using Dalamud.Logging;
using MareSynchronos.Models; using MareSynchronos.Models;
using System; using System;
using System.Buffers.Text;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices.ComTypes;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Game.ClientState.Objects.Types;
using LZ4;
using MareSynchronos.API;
using MareSynchronos.FileCacheDB;
using Microsoft.AspNetCore.SignalR.Client;
namespace MareSynchronos.WebAPI namespace MareSynchronos.WebAPI
{ {
public class CharacterReceivedEventArgs : EventArgs
{
public CharacterCacheDto CharacterData { get; set; }
public string CharacterNameHash { get; set; }
}
public class ApiController : IDisposable public class ApiController : IDisposable
{ {
private readonly Configuration pluginConfiguration; private readonly Configuration pluginConfiguration;
private const string mainService = "https://localhost:6591"; private const string MainService = "https://darkarchon.internet-box.ch:5001";
public string UID { get; private set; } = string.Empty; public string UID { get; private set; } = string.Empty;
public string SecretKey => pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? pluginConfiguration.ClientSecret[ApiUri] : string.Empty; public string SecretKey => pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? pluginConfiguration.ClientSecret[ApiUri] : "-";
private string CacheFolder => pluginConfiguration.CacheFolder; private string CacheFolder => pluginConfiguration.CacheFolder;
public bool UseCustomService public bool UseCustomService
{ {
@@ -23,89 +41,295 @@ namespace MareSynchronos.WebAPI
set set
{ {
pluginConfiguration.UseCustomService = value; pluginConfiguration.UseCustomService = value;
_ = Heartbeat();
pluginConfiguration.Save(); pluginConfiguration.Save();
} }
} }
private string ApiUri => UseCustomService ? pluginConfiguration.ApiUri : mainService; private string ApiUri => UseCustomService ? pluginConfiguration.ApiUri : MainService;
public bool IsConnected { get; set; } public bool ServerAlive =>
(_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected;
public bool IsConnected => !string.IsNullOrEmpty(UID);
Task heartbeatTask; public event EventHandler? Connected;
CancellationTokenSource cts; public event EventHandler? Disconnected;
public event EventHandler<CharacterReceivedEventArgs>? CharacterReceived;
public event EventHandler? RemovedFromWhitelist;
public event EventHandler? AddedToWhitelist;
public List<WhitelistDto> WhitelistEntries { get; set; } = new List<WhitelistDto>();
readonly CancellationTokenSource cts;
private HubConnection? _heartbeatHub;
private IDisposable? _fileUploadRequest;
private HubConnection? _fileHub;
private HubConnection? _userHub;
public ApiController(Configuration pluginConfiguration) public ApiController(Configuration pluginConfiguration)
{ {
this.pluginConfiguration = pluginConfiguration; this.pluginConfiguration = pluginConfiguration;
cts = new CancellationTokenSource(); cts = new CancellationTokenSource();
heartbeatTask = Task.Run(async () => _ = Heartbeat();
{
PluginLog.Debug("Starting heartbeat to " + ApiUri);
while (true && !cts.IsCancellationRequested)
{
await Heartbeat();
await Task.Delay(TimeSpan.FromSeconds(15), cts.Token);
}
PluginLog.Debug("Stopping heartbeat");
}, cts.Token);
} }
public async Task Heartbeat() public async Task Heartbeat()
{ {
try while (!ServerAlive && !cts.Token.IsCancellationRequested)
{ {
PluginLog.Debug("Sending heartbeat to " + ApiUri); try
if (ApiUri != mainService) throw new Exception(); {
IsConnected = true; PluginLog.Debug("Attempting to establish heartbeat connection to " + ApiUri);
_heartbeatHub = new HubConnectionBuilder()
.WithUrl(ApiUri + "/heartbeat", options =>
{
if (!string.IsNullOrEmpty(SecretKey))
{
options.Headers.Add("Authorization", SecretKey);
}
#if DEBUG
options.HttpMessageHandlerFactory = (message) =>
{
if (message is HttpClientHandler clientHandler)
clientHandler.ServerCertificateCustomValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true;
return message;
};
#endif
}).Build();
PluginLog.Debug("Heartbeat service built to: " + ApiUri);
await _heartbeatHub.StartAsync(cts.Token);
UID = await _heartbeatHub!.InvokeAsync<string>("Heartbeat");
PluginLog.Debug("Heartbeat started: " + ApiUri);
try
{
await InitializeHubConnections();
await LoadInitialData();
Connected?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
PluginLog.Error(ex, "Error during Heartbeat initialization");
}
_heartbeatHub.Closed += OnHeartbeatHubOnClosed;
_heartbeatHub.Reconnected += OnHeartbeatHubOnReconnected;
PluginLog.Debug("Heartbeat established to: " + ApiUri);
}
catch (Exception ex)
{
PluginLog.Error(ex, "Creating heartbeat failure");
}
} }
catch }
private async Task LoadInitialData()
{
var whiteList = await _userHub!.InvokeAsync<List<WhitelistDto>>("GetWhitelist");
WhitelistEntries = whiteList.ToList();
}
public void RestartHeartbeat()
{
PluginLog.Debug("Restarting heartbeat");
_heartbeatHub!.Closed -= OnHeartbeatHubOnClosed;
_heartbeatHub!.Reconnected -= OnHeartbeatHubOnReconnected;
Task.Run(async () =>
{ {
IsConnected = false; await _heartbeatHub.StopAsync(cts.Token);
await _heartbeatHub.DisposeAsync();
_heartbeatHub = null!;
_ = Heartbeat();
});
}
private async Task OnHeartbeatHubOnReconnected(string? s)
{
PluginLog.Debug("Reconnected: " + ApiUri);
UID = await _heartbeatHub!.InvokeAsync<string>("Heartbeat");
}
private Task OnHeartbeatHubOnClosed(Exception? exception)
{
PluginLog.Debug("Connection closed: " + ApiUri);
Disconnected?.Invoke(null, EventArgs.Empty);
RestartHeartbeat();
return Task.CompletedTask;
}
private async Task DisposeHubConnections()
{
if (_fileHub != null)
{
PluginLog.Debug("Disposing File Hub");
_fileUploadRequest?.Dispose();
await _fileHub!.StopAsync();
await _fileHub!.DisposeAsync();
} }
if (_userHub != null)
{
PluginLog.Debug("Disposing User Hub");
await _userHub.StopAsync();
await _userHub.DisposeAsync();
}
}
private async Task InitializeHubConnections()
{
await DisposeHubConnections();
PluginLog.Debug("Creating User Hub");
_userHub = new HubConnectionBuilder()
.WithUrl(ApiUri + "/user", options =>
{
options.Headers.Add("Authorization", SecretKey);
#if DEBUG
options.HttpMessageHandlerFactory = (message) =>
{
if (message is HttpClientHandler clientHandler)
clientHandler.ServerCertificateCustomValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true;
return message;
};
#endif
})
.Build();
await _userHub.StartAsync();
_userHub.On<WhitelistDto, string>("UpdateWhitelist", UpdateLocalWhitelist);
_userHub.On<CharacterCacheDto, string>("ReceiveCharacterData", ReceiveCharacterData);
PluginLog.Debug("Creating File Hub");
_fileHub = new HubConnectionBuilder()
.WithUrl(ApiUri + "/files", options =>
{
options.Headers.Add("Authorization", SecretKey);
#if DEBUG
options.HttpMessageHandlerFactory = (message) =>
{
if (message is HttpClientHandler clientHandler)
clientHandler.ServerCertificateCustomValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true;
return message;
};
#endif
})
.Build();
await _fileHub.StartAsync(cts.Token);
_fileUploadRequest = _fileHub!.On<string>("FileRequest", UploadFile);
}
private void UpdateLocalWhitelist(WhitelistDto dto, string characterIdentifier)
{
var entry = WhitelistEntries.SingleOrDefault(e => e.OtherUID == dto.OtherUID);
if (entry == null)
{
RemovedFromWhitelist?.Invoke(characterIdentifier, EventArgs.Empty);
return;
}
if ((entry.IsPausedFromOthers != dto.IsPausedFromOthers || entry.IsSynced != dto.IsSynced || entry.IsPaused != dto.IsPaused)
&& !dto.IsPaused && dto.IsSynced && !dto.IsPausedFromOthers)
{
AddedToWhitelist?.Invoke(characterIdentifier, EventArgs.Empty);
}
entry.IsPaused = dto.IsPaused;
entry.IsPausedFromOthers = dto.IsPausedFromOthers;
entry.IsSynced = dto.IsSynced;
if (dto.IsPaused || dto.IsPausedFromOthers || !dto.IsSynced)
{
RemovedFromWhitelist?.Invoke(characterIdentifier, EventArgs.Empty);
}
}
private async Task UploadFile(string fileHash)
{
PluginLog.Debug("Requested fileHash: " + fileHash);
await using var db = new FileCacheContext();
var fileCache = db.FileCaches.First(f => f.Hash == fileHash);
var compressedFile = LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath), 0,
(int)new FileInfo(fileCache.Filepath).Length);
var response = await _fileHub!.InvokeAsync<bool>("UploadFile", fileHash, compressedFile, cts.Token);
PluginLog.Debug("Success: " + response);
} }
public async Task Register() public async Task Register()
{ {
if (!ServerAlive) return;
PluginLog.Debug("Registering at service " + ApiUri); PluginLog.Debug("Registering at service " + ApiUri);
var response = ("RandomSecretKey", "RandomUID"); var response = await _userHub!.InvokeAsync<string>("Register");
pluginConfiguration.ClientSecret[ApiUri] = response.Item1; pluginConfiguration.ClientSecret[ApiUri] = response;
UID = response.Item2; pluginConfiguration.Save();
PluginLog.Debug(pluginConfiguration.ClientSecret[ApiUri]); RestartHeartbeat();
// pluginConfiguration.Save();
} }
public async Task UploadFile(string filePath) public async Task SendCharacterData(CharacterCacheDto character, List<string> visibleCharacterIds)
{
PluginLog.Debug("Uploading file " + filePath + " to " + ApiUri);
}
public async Task<byte[]> DownloadFile(string hash)
{
PluginLog.Debug("Downloading file from service " + ApiUri);
return Array.Empty<byte>();
}
public async Task<List<string>> SendCharacterData(CharacterCache character)
{ {
if (!IsConnected || SecretKey == "-") return;
PluginLog.Debug("Sending Character data to service " + ApiUri); PluginLog.Debug("Sending Character data to service " + ApiUri);
List<string> list = new(); await _fileHub!.InvokeAsync("SendFiles", character.FileReplacements, cts.Token);
return list;
while (await _fileHub!.InvokeAsync<bool>("IsUploadFinished"))
{
await Task.Delay(TimeSpan.FromSeconds(0.5));
PluginLog.Debug("Waiting for uploads to finish");
}
await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds);
} }
public async Task<CharacterCache> GetCharacterData(string uid) public Task ReceiveCharacterData(CharacterCacheDto character, string characterHash)
{ {
PluginLog.Debug("Getting character data for " + uid + " from service " + ApiUri); PluginLog.Debug("Received DTO for " + characterHash);
CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs()
{
CharacterData = character,
CharacterNameHash = characterHash
});
return Task.CompletedTask;
}
CharacterCache characterCache = new(); public async Task<byte[]> DownloadData(string hash)
return characterCache; {
return await _fileHub!.InvokeAsync<byte[]>("DownloadFile", hash);
}
public async Task GetCharacterData(Dictionary<string, int> hashedCharacterNames)
{
await _userHub!.InvokeAsync("GetCharacterData",
hashedCharacterNames);
}
public async Task SendWhitelistPauseChange(string uid, bool paused)
{
if (!IsConnected || SecretKey == "-") return;
await _userHub!.SendAsync("SendWhitelistPauseChange", uid, paused);
}
public async Task SendWhitelistAddition(string uid)
{
if (!IsConnected || SecretKey == "-") return;
await _userHub!.SendAsync("SendWhitelistAddition", uid);
}
public async Task SendWhitelistRemoval(string uid)
{
if (!IsConnected || SecretKey == "-") return;
await _userHub!.SendAsync("SendWhitelistRemoval", uid);
} }
public async Task SendWhitelist() public async Task SendWhitelist()
{ {
PluginLog.Debug("Sending whitelist to service " + ApiUri); if (!IsConnected || SecretKey == "-") return;
await _userHub!.SendAsync("SendWhitelist", WhitelistEntries.ToList());
WhitelistEntries = (await _userHub!.InvokeAsync<List<WhitelistDto>>("GetWhitelist")).ToList();
} }
public async Task<List<string>> GetWhitelist() public async Task<List<string>> GetWhitelist()
@@ -119,6 +343,18 @@ namespace MareSynchronos.WebAPI
public void Dispose() public void Dispose()
{ {
cts?.Cancel(); cts?.Cancel();
_ = DisposeHubConnections();
}
public async Task SendCharacterName(string hashedName)
{
await _userHub!.SendAsync("SendCharacterNameHash", hashedName);
}
public async Task SendVisibilityData(List<string> visibilities)
{
if (!IsConnected) return;
await _userHub!.SendAsync("SendVisibilityList", visibilities);
} }
} }
} }