diff --git a/QuestShare.Common/API/AuthRequest.cs b/QuestShare.Common/API/AuthRequest.cs new file mode 100644 index 0000000..28e31c3 --- /dev/null +++ b/QuestShare.Common/API/AuthRequest.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QuestShare.Common.API +{ +#pragma warning disable CS0618 // Type or member is obsolete + public sealed class AuthRequest : IAPIEndpoint +#pragma warning restore CS0618 // Type or member is obsolete + { + [Obsolete("This constructor is for serialization only. Do not use it in code.")] + public class Request : IRequest + { + public Request() { } + public required string Version { get; set; } + public required string Token { get; set; } + } + public class Response : IResponse + { + public Response() { } + public Error Error { get; set; } = Error.None; + public bool Success { get; set; } = true; + } + } +} diff --git a/QuestShare.Common/API/Authorize.cs b/QuestShare.Common/API/Authorize.cs index 3ff8df4..16b44f5 100644 --- a/QuestShare.Common/API/Authorize.cs +++ b/QuestShare.Common/API/Authorize.cs @@ -6,14 +6,14 @@ using System.Threading.Tasks; namespace QuestShare.Common.API { - public class Authorize : IAPIEndpoint + public sealed class Authorize : IAPIEndpoint { public class Request : IRequest { public Request() { } public string Version { get; set; } = null!; public string Token { get; set; } = null!; - public ulong CharacterId { get; set; } = 0; + public List ShareCodes { get; set; } = []; } public class Response : IResponse @@ -22,11 +22,8 @@ namespace QuestShare.Common.API public Error Error { get; set; } = Error.None; public bool Success { get; set; } public string Token { get; set; } = null!; - } - - public class AuthBroadcast - { - public AuthBroadcast() { } + public List Sessions { get; set; } = []; + public Objects.OwnedSession? OwnedSession { get; set; } } } } diff --git a/QuestShare.Common/API/Share/Cancel.cs b/QuestShare.Common/API/Cancel.cs similarity index 63% rename from QuestShare.Common/API/Share/Cancel.cs rename to QuestShare.Common/API/Cancel.cs index 4dc27ae..1160aea 100644 --- a/QuestShare.Common/API/Share/Cancel.cs +++ b/QuestShare.Common/API/Cancel.cs @@ -4,16 +4,17 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace QuestShare.Common.API.Share +namespace QuestShare.Common.API { - public class Cancel : IAPIEndpoint + public sealed class Cancel : IAPIEndpoint { public class Request : IRequest { public Request() { } - public string Version { get; set; } = null!; - public string Token { get; set; } = null!; - public string ShareCode { get; set; } = null!; + public required string Version { get; set; } + public required string Token { get; set; } + public required string ShareCode { get; set; } + public required string OwnerCharacterId { get; set; } } public class Response : IResponse diff --git a/QuestShare.Common/API/Errors.cs b/QuestShare.Common/API/Errors.cs deleted file mode 100644 index c85299d..0000000 --- a/QuestShare.Common/API/Errors.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace QuestShare.Common.API -{ - public enum Error - { - None, - InvalidToken, - InvalidShareCode, - InvalidVersion, - InvalidQuests, - ShareNotFound, - Unauthorized, - UnknownError, - InternalServerError, - InvalidCharacterName, - InvalidNotifyType, - InvalidParty, - InvalidMember, - InvalidQuest, - InvalidCharacterId, - BannedTooManyBadRequests, - AlreadyRegistered, - AlreadyJoined, - InvalidHostClient, - } -} diff --git a/QuestShare.Common/API/Share/Register.cs b/QuestShare.Common/API/GroupJoin.cs similarity index 51% rename from QuestShare.Common/API/Share/Register.cs rename to QuestShare.Common/API/GroupJoin.cs index 25d5fef..fd91a18 100644 --- a/QuestShare.Common/API/Share/Register.cs +++ b/QuestShare.Common/API/GroupJoin.cs @@ -1,25 +1,19 @@ -using Newtonsoft.Json; -using QuestShare.Common.API; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace QuestShare.Common.API.Share +namespace QuestShare.Common.API { - public class Register : IAPIEndpoint + public sealed class GroupJoin : IAPIEndpoint { public class Request : IRequest { public Request() { } public required string Token { get; set; } - public required ulong CharacterId { get; set; } public required string Version { get; set; } - public uint SharedQuestId { get; set; } - public byte SharedQuestStep { get; set; } - public bool BroadcastParty { get; set; } = false; - public List PartyMembers { get; set; } = []; + public required Objects.ShareCode SessionInfo { get; set; } } public class Response : IResponse @@ -27,8 +21,13 @@ namespace QuestShare.Common.API.Share public Response() { } public Error Error { get; set; } = Error.None; public bool Success { get; set; } = false; - public required string ShareCode { get; set; } + public Objects.Session? Session { get; set; } + } + + public class GroupJoinBroadcast + { + public GroupJoinBroadcast() { } + public required Objects.Session Session { get; set; } } } - } diff --git a/QuestShare.Common/API/Share/GroupLeave.cs b/QuestShare.Common/API/GroupLeave.cs similarity index 66% rename from QuestShare.Common/API/Share/GroupLeave.cs rename to QuestShare.Common/API/GroupLeave.cs index 5325f45..f2439cc 100644 --- a/QuestShare.Common/API/Share/GroupLeave.cs +++ b/QuestShare.Common/API/GroupLeave.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace QuestShare.Common.API.Share +namespace QuestShare.Common.API { public sealed class GroupLeave : IAPIEndpoint { @@ -12,14 +12,21 @@ namespace QuestShare.Common.API.Share { public Request() { } public string Version { get; set; } = null!; - public string ShareCode { get; set; } = null!; public string Token { get; set; } = null!; + public required Objects.Session Session { get; set; } } public class Response : IResponse { public Response() { } public Error Error { get; set; } = Error.None; public bool Success { get; set; } = false; + public Objects.Session? Session { get; set; } + } + + public class GroupLeaveBroadcast + { + public GroupLeaveBroadcast() { } + public required Objects.Session Session { get; set; } } } } diff --git a/QuestShare.Common/API/Interfaces.cs b/QuestShare.Common/API/Interfaces.cs deleted file mode 100644 index e38d319..0000000 --- a/QuestShare.Common/API/Interfaces.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace QuestShare.Common.API -{ - public interface IRequest - { - string Version { get; set; } - string Token { get; set; } - } - - public interface IResponse - { - Error Error { get; set; } - bool Success { get; set; } - } - - public interface IAPIEndpoint where T1 : IRequest where T2 : IResponse; -} diff --git a/QuestShare.Common/API/Party/PartyCheck.cs b/QuestShare.Common/API/Party/PartyCheck.cs deleted file mode 100644 index 1ce72ef..0000000 --- a/QuestShare.Common/API/Party/PartyCheck.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace QuestShare.Common.API.Party -{ - public class PartyCheck : IAPIEndpoint - { - public class Request : IRequest - { - public Request() { } - public string Version { get; set; } = null!; - public string Token { get; set; } = null!; - public required ulong CharacterId { get; set; } = 0; - public required List PartyMembers { get; set; } - } - public class Response : IResponse - { - public Response() { } - public Error Error { get; set; } = Error.None; - public bool Success { get; set; } - public string ShareCode { get; set; } = null!; - } - } -} diff --git a/QuestShare.Common/API/Share/GetShareInfo.cs b/QuestShare.Common/API/Register.cs similarity index 66% rename from QuestShare.Common/API/Share/GetShareInfo.cs rename to QuestShare.Common/API/Register.cs index 8633c4a..3aeee18 100644 --- a/QuestShare.Common/API/Share/GetShareInfo.cs +++ b/QuestShare.Common/API/Register.cs @@ -1,19 +1,19 @@ +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace QuestShare.Common.API.Share +namespace QuestShare.Common.API { - public sealed class GetShareInfo : IAPIEndpoint + public sealed class Register : IAPIEndpoint { public class Request : IRequest { public Request() { } - public required string Version { get; set; } public required string Token { get; set; } - public required string ShareCode { get; set; } + public required string Version { get; set; } } public class Response : IResponse @@ -21,9 +21,7 @@ namespace QuestShare.Common.API.Share public Response() { } public Error Error { get; set; } = Error.None; public bool Success { get; set; } = false; - public uint SharedQuestId { get; set; } - public byte SharedQuestStep { get; set; } - public required List Members { get; set; } + public required string ShareCode { get; set; } } } } diff --git a/QuestShare.Common/API/Share/GroupJoin.cs b/QuestShare.Common/API/Share/GroupJoin.cs deleted file mode 100644 index 14cd095..0000000 --- a/QuestShare.Common/API/Share/GroupJoin.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace QuestShare.Common.API.Share -{ - public class GroupJoin : IAPIEndpoint - { - public class Request : IRequest - { - public Request() { } - public required string Token { get; set; } - public required string Version { get; set; } - public string ShareCode { get; set; } = ""; - public required ulong CharacterId { get; set; } - public bool RequestPartyQuickJoin { get; set; } = false; - public ulong QuickJoinCharacterId { get; set; } = 0; - } - - public class Response : IResponse - { - public Response() { } - public Error Error { get; set; } = Error.None; - public bool Success { get; set; } = false; - public uint SharedQuestId { get; set; } - public byte SharedQuestStep { get; set; } - public string ShareCode { get; set; } = null!; - public List Members { get; set; } = null!; - } - } -} diff --git a/QuestShare.Common/API/Share/GroupNotify.cs b/QuestShare.Common/API/Share/GroupNotify.cs deleted file mode 100644 index 89602db..0000000 --- a/QuestShare.Common/API/Share/GroupNotify.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace QuestShare.Common.API.Share -{ - public sealed class GroupNotify - { - public class GroupNotifyBroadcast - { - public GroupNotifyBroadcast() { } - public required string ShareCode { get; set; } - public required ulong CharacterId { get; set; } - public required NotifyType NotifyType { get; set; } - } - } - - public enum NotifyType - { - Join, - Leave, - JoinViaParty, - Rejoin, - Disconnected - } -} diff --git a/QuestShare.Common/API/Share/Resume.cs b/QuestShare.Common/API/Share/Resume.cs deleted file mode 100644 index d6f1dbc..0000000 --- a/QuestShare.Common/API/Share/Resume.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace QuestShare.Common.API.Share -{ - public class Resume : IAPIEndpoint - { - public class Request : IRequest - { - public Request() { } - public required string Version { get; set; } - public required string Token { get; set; } - public string ShareCode { get; set; } = ""; - public List? Members { get; set; } = []; - } - public class Response : IResponse - { - public Response() { } - public Error Error { get; set; } = Error.None; - public bool Success { get; set; } = false; - public uint SharedQuestId { get; set; } - public byte SharedQuestStep { get; set; } - public string ShareCode { get; set; } = ""; - public List? Members { get; set; } = []; - public bool IsGroup { get; set; } = false; - public bool IsHost { get; set; } = false; - } - } - -} diff --git a/QuestShare.Common/API/Share/Update.cs b/QuestShare.Common/API/Update.cs similarity index 53% rename from QuestShare.Common/API/Share/Update.cs rename to QuestShare.Common/API/Update.cs index fcd11d7..9816272 100644 --- a/QuestShare.Common/API/Share/Update.cs +++ b/QuestShare.Common/API/Update.cs @@ -4,20 +4,18 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace QuestShare.Common.API.Share +namespace QuestShare.Common.API { - public class Update : IAPIEndpoint + public sealed class Update : IAPIEndpoint { public class Request : IRequest { public Request() { } public required string Version { get; set; } public required string Token { get; set; } - public uint SharedQuestId { get; set; } - public byte SharedQuestStep { get; set; } - public required bool BroadcastParty { get; set; } - public List PartyMembers { get; set; } = []; - public bool IsPartyChange { get; set; } = false; + public required Objects.OwnedSession Session { get; set; } + public List PartyMembers { get; set; } = []; + public bool IsQuestUpdate { get; set; } = false; } public class Response : IResponse @@ -30,9 +28,7 @@ namespace QuestShare.Common.API.Share public class UpdateBroadcast { public UpdateBroadcast() { } - public string ShareCode { get; set; } = null!; - public uint SharedQuestId { get; set; } - public byte SharedQuestStep { get; set; } + public required Objects.Session Session { get; set; } } } } diff --git a/QuestShare.Common/Common.cs b/QuestShare.Common/Common.cs index 569482e..d5317b5 100644 --- a/QuestShare.Common/Common.cs +++ b/QuestShare.Common/Common.cs @@ -1,7 +1,35 @@ +using System.Text; + namespace QuestShare.Common { public class Constants { - public static readonly string Version = "1"; + public static readonly string Version = "3"; + } + + public static class StringExtensions + { + public static string ToBase64(this string str) + { + return Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); + } + public static string FromBase64(this string str) + { + return Encoding.UTF8.GetString(Convert.FromBase64String(str)); + } + + public static string ToSHA256(this string str) + { + var bytes = Encoding.UTF8.GetBytes(str); + // 10 reps of SHA256 + for (var i = 0; i < 10; i++) + bytes = System.Security.Cryptography.SHA256.HashData(bytes); + return BitConverter.ToString(bytes).Replace("-", "").ToLower(); + } + + public static string SaltedHash(this string str, string salt) + { + return (str + salt).ToSHA256(); + } } } diff --git a/QuestShare.Dalamud/Addons/AddonPartyList.cs b/QuestShare.Dalamud/Addons/AddonPartyList.cs deleted file mode 100644 index 8e5af51..0000000 --- a/QuestShare.Dalamud/Addons/AddonPartyList.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using FFXIVClientStructs.FFXIV.Component.GUI; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace QuestShare.Addons -{ - internal class AddonPartyList - { - public static int MemberCount { get; private set; } - public static event EventHandler? OnMemberCountChanged; - public static void Initialize() - { - AddonLifecycle.RegisterListener(AddonEvent.PostRefresh, OnPostRefresh); - } - - public static void Dispose() - { - AddonLifecycle.UnregisterListener(AddonEvent.PostRefresh, OnPostRefresh); - } - - private static void OnPostRefresh(AddonEvent e, AddonArgs args) - { - unsafe - { - var addon = (FFXIVClientStructs.FFXIV.Client.UI.AddonPartyList*)args.Addon; - if (addon == null) - { - return; - } - int count = addon->MemberCount; - if (count != MemberCount) - { - MemberCount = addon->MemberCount; - OnMemberCountChanged?.Invoke(null, EventArgs.Empty); - } - } - } - } -} diff --git a/QuestShare.Dalamud/Common/ConfigurationManager.cs b/QuestShare.Dalamud/Common/ConfigurationManager.cs index 09e0a56..baceba8 100644 --- a/QuestShare.Dalamud/Common/ConfigurationManager.cs +++ b/QuestShare.Dalamud/Common/ConfigurationManager.cs @@ -1,4 +1,3 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Character; using Newtonsoft.Json; using QuestShare.Services; @@ -9,42 +8,55 @@ public class ConfigurationManager public class Configuration { public string Token { get; set; } = string.Empty; - public string ShareCode { get; set; } = string.Empty; - public string LastShareCode { get; set; } = string.Empty; - public ShareMode LastShareMode { get; set; } = ShareMode.None; public bool ConnectOnStartup { get; set; } = false; - public bool ResumeOnStartup { get; set; } = false; - public bool LastConnectionState { get; set; } = false; public bool AutoShareMsq { get; set; } = false; public bool AutoShareNewQuests { get; set; } = false; - public bool BroadcastToParty { get; set; } = false; public bool TrackMSQ { get; set; } = false; + public bool HideFutureStepsHost { get; set; } = true; + public bool HideFutureStepsMember { get; set; } = false; + public bool EnableHosting { get; set; } = false; + public Objects.OwnedSession? OwnedSession { get; set; } + public List KnownShareCodes { get; set; } = []; + public int ActiveQuestId { get; set; } = 0; + public byte ActiveQuestStep { get; set; } = 0; + public Dictionary KnownCharacters { get; set; } = []; + public ApiConfiguration[] ApiConfigurations { get; set; } = [new ApiConfiguration { DisplayName = "Primary Server - US East", Url = "https://api.nathanc.tech/Hub", Active = false }]; + public string ApiUrl => ApiConfigurations.FirstOrDefault(x => x.Active)?.Url ?? "https://api.nathanc.tech/Hub"; + public string ApiDisplayName => ApiConfigurations.FirstOrDefault(x => x.Active)?.DisplayName ?? "Primary Server - US East"; } - - public Configuration Instance { get; private set; } = new Configuration(); - private ulong localContentId = 0; + + public class ApiConfiguration + { + public required string DisplayName; + public required string Url; + public bool Active = false; + } + + public static Configuration Instance { get; private set; } = new Configuration(); + private static ulong LocalContentId = 0; public ConfigurationManager() { Log.Debug("ConfigurationManager constructor"); + ClientState.Login += OnLogin; ClientState.Logout += OnLogout; if (ClientState.LocalContentId != 0) { - localContentId = ClientState.LocalContentId; + LocalContentId = ClientState.LocalContentId; Framework.Run(Load); } } public void OnLogin() { - localContentId = ClientState.LocalContentId; + LocalContentId = ClientState.LocalContentId; Framework.Run(Load); } public void Dispose() { - Framework.Run(Save); + Save(); ClientState.Login -= OnLogin; ClientState.Logout -= OnLogout; } @@ -52,30 +64,31 @@ public class ConfigurationManager public void OnLogout(int code, int state) { Framework.RunOnTick(Save); - ShareService.SetShareCode(string.Empty); - ShareService.SetHostedShareCode(string.Empty); - ShareService.Token = string.Empty; } public void Load() { - if (File.Exists(Path.Join(PluginInterface.ConfigDirectory.FullName, $"{localContentId}.json"))) + if (File.Exists(Path.Join(PluginInterface.ConfigDirectory.FullName, $"{LocalContentId}.json"))) { - var config = File.ReadAllText(Path.Join(PluginInterface.ConfigDirectory.FullName, $"{localContentId}.json")); + var config = File.ReadAllText(Path.Join(PluginInterface.ConfigDirectory.FullName, $"{LocalContentId}.json")); if (config != null) { var deserialized = JsonConvert.DeserializeObject(config); if (deserialized != null) { Instance = deserialized; - - ShareService.SetShareCode(Instance.LastShareCode); - ShareService.Token = Instance.Token; - Log.Warning($"Writing token {Instance.Token} to ShareService"); +#if DEBUG + if (Instance.ApiConfigurations.All(x => x.DisplayName != "Local")) + Instance.ApiConfigurations.Append(new ApiConfiguration { DisplayName = "Local", Url = "http://localhost:8080/Hub", Active = true }); + foreach (var item in Instance.ApiConfigurations) + { + if (item.DisplayName != "Local") item.Active = false; + } +#endif } else { - Log.Error($"Failed to deserialize configuration for {localContentId}"); + Log.Error($"Failed to deserialize configuration for {LocalContentId}"); } } } @@ -85,10 +98,11 @@ public class ConfigurationManager Save(); } } - public void Save() + public static void Save() { Log.Debug("Saving configuration"); - File.WriteAllText(Path.Join(PluginInterface.ConfigDirectory.FullName, $"{localContentId}.json"), JsonConvert.SerializeObject(Instance)); + File.WriteAllText(Path.Join(PluginInterface.ConfigDirectory.FullName, $"{LocalContentId}.json"), JsonConvert.SerializeObject(Instance)); + Log.Debug($"Wrote config: {JsonConvert.SerializeObject(Instance)}"); } diff --git a/QuestShare.Dalamud/Common/GameEventManager.cs b/QuestShare.Dalamud/Common/GameEventManager.cs deleted file mode 100644 index d70a467..0000000 --- a/QuestShare.Dalamud/Common/GameEventManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace QuestShare.Common -{ - internal static class GameEventManager - { - public static void Initialize() - { - } - public static void Dispose() - { - } - } -} diff --git a/QuestShare.Dalamud/Common/GameQuestManager.cs b/QuestShare.Dalamud/Common/GameQuestManager.cs index 98b5ca9..b9cb03c 100644 --- a/QuestShare.Dalamud/Common/GameQuestManager.cs +++ b/QuestShare.Dalamud/Common/GameQuestManager.cs @@ -2,6 +2,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -57,6 +58,11 @@ namespace QuestShare.Common { SetActiveFlag(activeQuest.QuestId); } + else if (ConfigurationManager.Instance.OwnedSession != null) + { + var quest = GetQuestById((uint)ConfigurationManager.Instance.OwnedSession.ActiveQuestId); + SetActiveFlag(quest.QuestId); + } } private static void OnLogin() @@ -72,7 +78,9 @@ namespace QuestShare.Common public unsafe static bool TrackMsq() { - if (ShareService.IsMsqTracking && ShareService.IsHost) + var api = (ApiService)Plugin.GetService(); + var share = (ShareService)Plugin.GetService(); + if (ConfigurationManager.Instance.TrackMSQ) { var questId = (uint)AgentScenarioTree.Instance()->Data->CurrentScenarioQuest; if (questId == 0) return false; @@ -85,13 +93,13 @@ namespace QuestShare.Common if (GameQuests.Contains(quest) && GetActiveQuest()?.QuestId != questId) { SetActiveFlag(questId); - SocketClientService.DispatchUpdate(false); + HostService.Update((int)questId, quest.CurrentStep); } else { GameQuests.Add(quest); SetActiveFlag(questId); - SocketClientService.DispatchUpdate(false); + HostService.Update((int)questId, quest.CurrentStep); } return true; } @@ -108,6 +116,7 @@ namespace QuestShare.Common if (ClientState.LocalContentId == 0) return; TrackMsq(); var q = GetActiveQuest(); + var api = (ApiService)Plugin.GetService(); if (q != null && q.QuestId == LastQuestId) { var step = QuestManager.GetQuestSequence(q.QuestId); @@ -124,6 +133,7 @@ namespace QuestShare.Common { Log.Debug("No previous quest in chain"); GameQuests.Remove(q); + LoadQuests(); return; } Log.Debug($"Previous quest in chain was {prevQuest.RowId}"); @@ -138,16 +148,16 @@ namespace QuestShare.Common { Log.Debug("No next quest in chain"); GameQuests.Remove(q); - + LoadQuests(); } } - SocketClientService.DispatchUpdate(false); + HostService.Update((int)q.QuestId, q.CurrentStep); } else { LastStep = step; Log.Debug($"Quest step changed to {step}"); - SocketClientService.DispatchUpdate(false); + HostService.Update((int)q.QuestId, q.CurrentStep); } } } @@ -222,7 +232,7 @@ namespace QuestShare.Common } } } - public Map GetMapLocation() + public Lumina.Excel.Sheets.Map GetMapLocation() { return QuestData.TodoParams.FirstOrDefault(param => param.ToDoCompleteSeq == CurrentStep) .ToDoLocation.FirstOrDefault(location => location is not { RowId: 0 }).Value.Map.Value; @@ -231,17 +241,18 @@ namespace QuestShare.Common public MapLinkPayload GetMapLink(byte step) { var toDoData = QuestData.TodoParams.Select(t => t.ToDoLocation).ToList(); - var data = toDoData.ElementAt(step == 0xFF ? QuestSteps.Count-1 : step).FirstOrDefault(); + Log.Debug($"Step: {step}/{0xFF} - Total steps: {QuestSteps.Count} - ToDoParams Count: {toDoData.Count}"); + var data = toDoData.ElementAt(step > QuestSteps.Count ? QuestSteps.Count-1 : step).FirstOrDefault(); + if (!data.IsValid) + { + Log.Error("Invalid ToDoLocation data"); + return new MapLinkPayload(0, 0, 0, 0); + } var coords = MapUtil.WorldToMap(new Vector3(data.Value.X, data.Value.Y, data.Value.Z), data.Value.Map.Value.OffsetX, data.Value.Map.Value.OffsetY, 0, data.Value.Map.Value.SizeFactor); var mapLink = new MapLinkPayload(data.Value.Territory.Value.RowId, data.Value.Map.Value.RowId, coords.X, coords.Y); return mapLink; } - public void GetQuestDetail() - { - - } - internal static ExcelSheet TextSheetForQuest(Lumina.Excel.Sheets.Quest q) { var qid = q.Id.ToString(); diff --git a/QuestShare.Dalamud/Common/Utility.cs b/QuestShare.Dalamud/Common/Utility.cs deleted file mode 100644 index 502466c..0000000 --- a/QuestShare.Dalamud/Common/Utility.cs +++ /dev/null @@ -1,39 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using Lumina.Excel.Sheets; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace QuestShare.Common -{ - internal static class Maps - { - public static int MarkerToMap(double coord, double scale) - => (int)(2 * coord / scale + 100.9); - - public static int NodeToMap(double coord, double scale) - => (int)(2 * coord + 2048 / scale + 100.9); - - public static int IntegerToInternal(int coord, double scale) - => (int)(coord - 100 - 2048 / scale) / 2; - - public static unsafe void SetFlagMarker(AgentMap* instance, Location location, uint iconId = 60561U) - { - instance->IsFlagMarkerSet = 0; - var x = IntegerToInternal(location.XCoord, location.SizeFactor); - var y = IntegerToInternal(location.YCoord, location.SizeFactor); - instance->SetFlagMapMarker(location.Id, location.Data.Map.RowId, x, y, iconId); - } - } - internal class Location(int X, int Y) - { - public uint Id { get; init; } - public float SizeFactor => (Data.Map.ValueNullable?.SizeFactor ?? 100f) / 100f; - - public int XCoord { get; init; } = X; - public int YCoord { get; init; } = Y; - public TerritoryType Data { get; init; } - } -} diff --git a/QuestShare.Dalamud/Plugin.cs b/QuestShare.Dalamud/Plugin.cs index 40df99c..55b2f0e 100644 --- a/QuestShare.Dalamud/Plugin.cs +++ b/QuestShare.Dalamud/Plugin.cs @@ -1,10 +1,10 @@ global using static QuestShare.Service; global using QuestShare.Common; +global using QuestShare.Common.API; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Serilog.Events; using QuestShare.Services; -using QuestShare.Addons; namespace QuestShare; @@ -14,8 +14,7 @@ public sealed class Plugin : IDalamudPlugin public static string Version => "1.0.0"; public static string PluginDataPath { get; private set; } = null!; internal static ConfigurationManager Configuration { get; private set; } = null!; - internal static SocketClientService SocketClient { get; private set; } = null!; - + private static List Services = []; internal static StringWriter LogStream { get; private set; } = null!; public Plugin(IDalamudPluginInterface pluginInterface) @@ -23,33 +22,45 @@ public sealed class Plugin : IDalamudPlugin pluginInterface.Create(pluginInterface); // redirect console output to plugin log Configuration = new ConfigurationManager(); - WindowManager.Initialize(); - GameEventManager.Initialize(); - Commands.Initialize(); + Services = + [ + new ApiService(), + new CommandService(), + new ShareService(), + new PartyService(), + new UiService(), + new HostService() + ]; GameQuestManager.Initialize(); - AddonPartyList.Initialize(); LogStream = new StringWriter(); Console.SetOut(LogStream); Console.SetError(LogStream); - Service.Framework.Update += OnFramework; - Log.Debug($"Share Code: {Configuration.Instance.ShareCode} - Token: {Configuration.Instance.Token}"); - SocketClient = new SocketClientService(); + Framework.Update += OnFramework; + Log.Debug($"Token: {ConfigurationManager.Instance.Token}"); + foreach (var service in Services) + { + Log.Debug($"Initializing {service.GetType().Name}"); + service.Initialize(); + } } public void Dispose() { - WindowManager.Dispose(); - GameEventManager.Dispose(); - Commands.Dispose(); - Configuration.Save(); LogStream.Dispose(); - AddonPartyList.Dispose(); - Service.Framework.Update -= OnFramework; - SocketClient.Dispose(); - Configuration.Dispose(); - ShareService.Dispose(); + foreach (var service in Services) + { + service.Shutdown(); + } + ConfigurationManager.Save(); ClientState.Login -= Configuration.OnLogin; ClientState.Logout -= Configuration.OnLogout; + Framework.Update -= OnFramework; + Configuration.Dispose(); + } + + internal static IService GetService() where T : IService + { + return Services.FirstOrDefault(s => s is T)!; } private void OnFramework(IFramework framework) diff --git a/QuestShare.Dalamud/QuestShare.csproj b/QuestShare.Dalamud/QuestShare.csproj index c4e3f5b..6bff34b 100644 --- a/QuestShare.Dalamud/QuestShare.csproj +++ b/QuestShare.Dalamud/QuestShare.csproj @@ -9,9 +9,6 @@ enable net8.0-windows - - - @@ -23,4 +20,7 @@ + + + diff --git a/QuestShare.Dalamud/Services/API/AuthRequest.cs b/QuestShare.Dalamud/Services/API/AuthRequest.cs new file mode 100644 index 0000000..1ec36b1 --- /dev/null +++ b/QuestShare.Dalamud/Services/API/AuthRequest.cs @@ -0,0 +1,13 @@ +namespace QuestShare.Services.API +{ + internal class AuthRequest_Client + { + + public static Task HandleResponse(AuthRequest.Response response) + { + var api = ((ApiService)Plugin.GetService()); + ApiService.DispatchAuthorize(); + return Task.CompletedTask; + } + } +} diff --git a/QuestShare.Dalamud/Services/API/Authorize.cs b/QuestShare.Dalamud/Services/API/Authorize.cs new file mode 100644 index 0000000..7ce47bc --- /dev/null +++ b/QuestShare.Dalamud/Services/API/Authorize.cs @@ -0,0 +1,50 @@ +namespace QuestShare.Services.API +{ + internal class Authorize_Client + { + public static void HandleDispatch() + { + if (ClientState.LocalContentId == 0) + { + _ = ((ApiService)Plugin.GetService()).Disconnect(); + return; + } + var knownCodes = ShareService.ShareCodes.Select(s => s).ToList(); + if (HostService.ActiveSession != null) + { + knownCodes.Add(new Objects.ShareCode { CharacterId = HostService.ActiveSession.OwnerCharacterId, Code = HostService.ActiveSession.ShareCode }); + } + var request = new Authorize.Request() + { + Token = ApiService.Token, + Version = Constants.Version, + ShareCodes = knownCodes, + }; + ((ApiService)Plugin.GetService()).Invoke(nameof(Authorize), request).ConfigureAwait(false); + } + public static Task HandleResponse(Authorize.Response response) + { + var share = (ShareService)Plugin.GetService(); + if (response.Success) + { + ConfigurationManager.Instance.Token = response.Token; + foreach(var session in response.Sessions) + { + share.AddSession(session); + } + if (response.OwnedSession != null) + { + ConfigurationManager.Instance.OwnedSession = response.OwnedSession; + } + ConfigurationManager.Save(); + } + else + { + Log.Error("Failed to authorize: {Error}", response.Error); + UiService.LastErrorMessage = $"Failed to authorize: {response.Error}"; + _ = ((ApiService)Plugin.GetService()).Disconnect(); + } + return Task.CompletedTask; + } + } +} diff --git a/QuestShare.Dalamud/Services/API/Cancel.cs b/QuestShare.Dalamud/Services/API/Cancel.cs new file mode 100644 index 0000000..0cc5808 --- /dev/null +++ b/QuestShare.Dalamud/Services/API/Cancel.cs @@ -0,0 +1,41 @@ +namespace QuestShare.Services.API +{ + internal class Cancel_Client + { + public static void HandleDispatch() + { + var api = (ApiService)Plugin.GetService(); + var party = (PartyService)Plugin.GetService(); + var share = (ShareService)Plugin.GetService(); + var request = new Cancel.Request + { + Token = ApiService.Token, + Version = Constants.Version, + ShareCode = HostService.ActiveSession!.ShareCode, + OwnerCharacterId = HostService.ActiveSession!.OwnerCharacterId + }; + _ = api.Invoke(nameof(Cancel), request); + } + + public static Task HandleResponse(Cancel.Response cancelResponse) + { + if (cancelResponse.Success) + { + var share = (ShareService)Plugin.GetService(); + ConfigurationManager.Instance.OwnedSession = null; + } + else + { + UiService.LastErrorMessage = "Failed to cancel the party."; + } + return Task.CompletedTask; + } + + public static Task HandleBroadcast(Cancel.CancelBroadcast cancelBroadcast) + { + var share = (ShareService)Plugin.GetService(); + share.RemoveSession(cancelBroadcast.ShareCode); + return Task.CompletedTask; + } + } +} diff --git a/QuestShare.Dalamud/Services/API/GroupJoin.cs b/QuestShare.Dalamud/Services/API/GroupJoin.cs new file mode 100644 index 0000000..61eb391 --- /dev/null +++ b/QuestShare.Dalamud/Services/API/GroupJoin.cs @@ -0,0 +1,44 @@ +namespace QuestShare.Services.API +{ + internal class GroupJoin_Client + { + + public static void HandleDispatch(Objects.ShareCode shareCode) { + var api = (ApiService)Plugin.GetService(); + var request = new GroupJoin.Request() + { + Token = ApiService.Token, + Version = Constants.Version, + SessionInfo = shareCode + }; + api.Invoke(nameof(GroupJoin), request).ConfigureAwait(false); + } + + public static Task HandleResponse(GroupJoin.Response resp) + { + if (resp.Success && resp.Session != null) + { + var share = (ShareService)Plugin.GetService(); + Log.Information("Successfully joined group."); + share.AddSession(resp.Session); + var api = (ApiService)Plugin.GetService(); + api.OnGroupJoin(new ApiService.GroupJoinEventArgs { Session = resp.Session, IsSuccess = true }); + } + else + { + Log.Error("Failed to join group: {Error}", resp.Error); + var api = (ApiService)Plugin.GetService(); + api.OnGroupJoin(new ApiService.GroupJoinEventArgs { Session = null, IsSuccess = false }); + UiService.LastErrorMessage = $"Failed to join group. {resp.Error}"; + } + return Task.CompletedTask; + } + + public static Task HandleBroadcast(GroupJoin.GroupJoinBroadcast broadcast) + { + var share = (ShareService)Plugin.GetService(); + share.AddSession(broadcast.Session); + return Task.CompletedTask; + } + } +} diff --git a/QuestShare.Dalamud/Services/API/GroupLeave.cs b/QuestShare.Dalamud/Services/API/GroupLeave.cs new file mode 100644 index 0000000..f096b43 --- /dev/null +++ b/QuestShare.Dalamud/Services/API/GroupLeave.cs @@ -0,0 +1,36 @@ +namespace QuestShare.Services.API +{ + internal class GroupLeave_Client + { + public static void HandleDispatch(Objects.Session session) + { + var api = (ApiService)Plugin.GetService(); + var request = new GroupLeave.Request + { + Token = ApiService.Token, + Version = Constants.Version, + Session = session + }; + api.Invoke(nameof(GroupLeave), request).ConfigureAwait(false); + } + + public static Task HandleResponse(GroupLeave.Response response) + { + if (response.Success && response.Session != null) + { + var share = (ShareService)Plugin.GetService(); + share.RemoveSession(response.Session); + } + else + { + UiService.LastErrorMessage = "Failed to leave the party."; + } + return Task.CompletedTask; + } + + public static void HandleBroadcast(GroupLeave.GroupLeaveBroadcast broadcast) + { + Log.Debug($"[GroupLeave] {broadcast.Session.OwnerCharacterId} left the party."); + } + } +} diff --git a/QuestShare.Dalamud/Services/API/IAPIHandler.cs b/QuestShare.Dalamud/Services/API/IAPIHandler.cs new file mode 100644 index 0000000..4215f1b --- /dev/null +++ b/QuestShare.Dalamud/Services/API/IAPIHandler.cs @@ -0,0 +1,14 @@ +namespace QuestShare.Services.API +{ + internal interface IAPIHandler + { + string Method => GetType().Name; + void HandleDispatch(); + void HandleDispatch(dynamic? data); + Task HandleResponse(IResponse response); + void InvokeHandler(IAPIHandler handler, IRequest request) + { + _ = ((ApiService)Plugin.GetService()).Invoke(Method, request); + } + } +} diff --git a/QuestShare.Dalamud/Services/API/Register.cs b/QuestShare.Dalamud/Services/API/Register.cs new file mode 100644 index 0000000..c7b7db9 --- /dev/null +++ b/QuestShare.Dalamud/Services/API/Register.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace QuestShare.Services.API +{ + internal class Register_Client + { + public static void HandleDispatch() + { + var api = (ApiService)Plugin.GetService(); + _ = api.Invoke(nameof(Register), new Common.API.Register.Request + { + Version = Constants.Version, + Token = ApiService.Token, + }); + } + + public void HandleDispatch(dynamic? data) + { + throw new NotImplementedException(); + } + + public static Task HandleResponse(Register.Response response) + { + if (response.Success) + { + var host = (HostService)Plugin.GetService(); + host.Start(response.ShareCode); + } + else + { + Log.Error("Failed to register as host: {0}", response.Error); + } + return Task.CompletedTask; + } + } +} diff --git a/QuestShare.Dalamud/Services/API/Update.cs b/QuestShare.Dalamud/Services/API/Update.cs new file mode 100644 index 0000000..f90f332 --- /dev/null +++ b/QuestShare.Dalamud/Services/API/Update.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; + +namespace QuestShare.Services.API +{ + internal class Update_Client + { + public static void HandleDispatch(Objects.OwnedSession session, List partyMembers, bool broadcast = true) + { + var api = (ApiService)Plugin.GetService(); + _ = api.Invoke(nameof(Update), new Update.Request + { + Token = ApiService.Token, + Version = Constants.Version, + Session = session, + PartyMembers = partyMembers, + IsQuestUpdate = broadcast + }); + } + + public static Task HandleResponse(Update.Response response) + { + if (response.Success) + { + Log.Debug("Successfully updated quest status."); + } + else + { + Log.Error("Failed to update quest status: {0}", response.Error); + UiService.LastErrorMessage = $"Failed to update quest status. {response.Error}"; + } + return Task.CompletedTask; + } + } + + internal class UpdateBroadcast_Client + { + public static Task HandleResponse(Update.UpdateBroadcast response) + { + ((ShareService)Plugin.GetService()).UpdateSession(response.Session); + return Task.CompletedTask; + } + } +} diff --git a/QuestShare.Dalamud/Services/ApiService.cs b/QuestShare.Dalamud/Services/ApiService.cs new file mode 100644 index 0000000..9e23e18 --- /dev/null +++ b/QuestShare.Dalamud/Services/ApiService.cs @@ -0,0 +1,162 @@ + +using Dalamud.Plugin.Services; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using QuestShare.Services.API; +using System.Diagnostics; + +namespace QuestShare.Services +{ + + internal class ApiService : IService + { + private readonly string socketUrl = ConfigurationManager.Instance.ApiUrl; + private HubConnection ApiConnection { get; set; } = null!; + internal bool IsConnected => ApiConnection.State == HubConnectionState.Connected; + internal HubConnectionState ConnectionState => ApiConnection.State; + internal bool IsAuthorized { get; private set; } = false; + private bool isDisposing = false; + internal static string Token => ConfigurationManager.Instance.Token; + private readonly List apiHandlers = []; + + public void Initialize() + { + var builder = new HubConnectionBuilder().WithUrl(socketUrl).ConfigureLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Information).AddConsole(); + }); + ApiConnection = builder.Build(); + ApiConnection.Closed += async (error) => + { + if (isDisposing) return; + Log.Warning($"Connection closed... {error}"); + Log.Warning($"Connection closed, retrying... {error}"); + await Task.Delay(new Random().Next(0, 5) * 1000); + await ApiConnection.StartAsync(); + }; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + ApiConnection.Reconnected += async (error) => + { + Log.Information("Connection reconnected"); + }; +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + ApiConnection.On(nameof(AuthRequest), AuthRequest_Client.HandleResponse); + ApiConnection.On(nameof(Authorize), Authorize_Client.HandleResponse); + ApiConnection.On(nameof(Register), Register_Client.HandleResponse); + ApiConnection.On(nameof(GroupJoin), GroupJoin_Client.HandleResponse); + ApiConnection.On(nameof(GroupLeave), GroupLeave_Client.HandleResponse); + ApiConnection.On(nameof(Cancel), Cancel_Client.HandleResponse); + ApiConnection.On(nameof(Update), Update_Client.HandleResponse); + ApiConnection.On(nameof(Update.UpdateBroadcast), UpdateBroadcast_Client.HandleResponse); + ApiConnection.On(nameof(Cancel.CancelBroadcast), Cancel_Client.HandleBroadcast); + ApiConnection.On(nameof(GroupJoin.GroupJoinBroadcast), GroupJoin_Client.HandleBroadcast); + ApiConnection.On(nameof(GroupLeave.GroupLeaveBroadcast), GroupLeave_Client.HandleBroadcast); + ApiConnection.On(nameof(SessionStart), SessionStart_Client.HandleResponse); + + ClientState.Login += OnLogin; + ClientState.Logout += OnLogout; + Framework.Update += SavePersistedConfig; + } + public void Shutdown() + { + isDisposing = true; + if (IsConnected) + { + ApiConnection.StopAsync(); + } + ApiConnection.DisposeAsync(); + ClientState.Login -= OnLogin; + ClientState.Logout -= OnLogout; + Framework.Update -= SavePersistedConfig; + } + + public void OnLogin() + { + if (Token != "" && ConfigurationManager.Instance.ConnectOnStartup) + { + Task.Run(Connect); + } + } + + public void OnLogout(int code, int type) + { + ConfigurationManager.Save(); + IsAuthorized = false; + ApiConnection.StopAsync().ConfigureAwait(false); + } + + private void SavePersistedConfig(IFramework _) + { + ConfigurationManager.Instance.Token = Token; + } + + public async Task Connect() + { + try + { + if (IsConnected) await ApiConnection.StopAsync(); + isDisposing = false; + await ApiConnection.StartAsync().ContinueWith(task => + { + if (task.IsFaulted) + { + Log.Error("Failed to connect to socket server"); + } + else + { + Log.Information("Connected to socket server"); + } + }); + } + catch (Exception ex) + { + Log.Error(ex.Message); + } + } + + public async Task Disconnect() + { + isDisposing = true; + await ApiConnection.StopAsync(); + } + + internal async Task Invoke(string methodName, object request) + { + if (!IsConnected) await Connect(); + Log.Debug($"Invoking {methodName} with {JsonConvert.SerializeObject(request)}"); + var s = new StackTrace(); + Log.Debug(s.ToString()); + await ApiConnection.InvokeAsync(methodName, request).ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error($"Failed to invoke {methodName}: {t.Exception}"); + } + }); + } + + public static void DispatchAuthorize() => Authorize_Client.HandleDispatch(); + public static void DispatchRegister() => Register_Client.HandleDispatch(); + public static void DispatchGroup(Objects.ShareCode shareCode) => GroupJoin_Client.HandleDispatch(shareCode); + public static void DispatchUngroup(Objects.Session session) => GroupLeave_Client.HandleDispatch(session); + public static void DispatchCancel() => Cancel_Client.HandleDispatch(); + public static void DispatchUpdate(Objects.OwnedSession session, List partyMembers) => Update_Client.HandleDispatch(session, partyMembers, true); + public static void DispatchConfigChange(Objects.OwnedSession session, List partyMembers) => Update_Client.HandleDispatch(session, partyMembers, false); + public static void DispatchSessionStart(Objects.OwnedSession session) => SessionStart_Client.HandleDispatch(session); + + public event EventHandler? GroupJoined; + + internal void OnGroupJoin(GroupJoinEventArgs e) + { + GroupJoined?.Invoke(this, e); + } + + public class GroupJoinEventArgs : EventArgs + { + public bool IsSuccess { get; set; } + public Objects.Session? Session { get; set; } + } + } +} diff --git a/QuestShare.Dalamud/Common/Commands.cs b/QuestShare.Dalamud/Services/CommandService.cs similarity index 65% rename from QuestShare.Dalamud/Common/Commands.cs rename to QuestShare.Dalamud/Services/CommandService.cs index b716fec..5d601aa 100644 --- a/QuestShare.Dalamud/Common/Commands.cs +++ b/QuestShare.Dalamud/Services/CommandService.cs @@ -1,26 +1,26 @@ using Dalamud.Game.Command; -namespace QuestShare.Common +namespace QuestShare.Services { - internal static class Commands + internal class CommandService : IService { - public static void Initialize() + public void Initialize() { CommandManager.AddHandler("/questshare", new CommandInfo(OnCommand) { HelpMessage = "Open the Quest Share window." }); } - public static void Dispose() + public void Shutdown() { CommandManager.RemoveHandler("/questshare"); } private static void OnCommand(string command, string args) { - Log.Information("Command received: {command} {args}"); + Log.Information($"Command received: {command} {args}"); if (command == "/questshare") { - WindowManager.ToggleMainUI(); + UiService.ToggleMainUI(); } } } diff --git a/QuestShare.Dalamud/Services/IService.cs b/QuestShare.Dalamud/Services/IService.cs new file mode 100644 index 0000000..462e203 --- /dev/null +++ b/QuestShare.Dalamud/Services/IService.cs @@ -0,0 +1,8 @@ +namespace QuestShare.Services +{ + internal interface IService + { + void Initialize(); + void Shutdown(); + } +} diff --git a/QuestShare.Dalamud/Services/PartyService.cs b/QuestShare.Dalamud/Services/PartyService.cs new file mode 100644 index 0000000..7bbde0b --- /dev/null +++ b/QuestShare.Dalamud/Services/PartyService.cs @@ -0,0 +1,66 @@ +using Dalamud.Plugin.Services; + +namespace QuestShare.Services +{ + internal class PartyService : IService + { + public void Initialize() + { + Framework.Update += OnFramework; + } + + public void Shutdown() + { + Framework.Update -= OnFramework; + } + + public long PartyId { get; private set; } + private List PartyMembers { get; set; } = []; + + private void OnFramework(IFramework framework) + { + if (PartyList.Length > 0 && PartyId == 0) + { + PartyId = PartyList.PartyId; + Log.Debug($"Joined party {PartyId}"); + PartyMembers.Clear(); + foreach (var member in PartyList) + { + Log.Debug($"Party member {member.Name.TextValue} - {member.ContentId}"); + PartyMembers.Add(member.ContentId); + } + } + else if (PartyList.Length == 0 && PartyId != 0) + { + PartyId = 0; + Log.Debug($"Left party"); + PartyMembers.Clear(); + } + else if (PartyList.Length != PartyMembers.Count) + { + var newMembers = PartyList.Where(x => !PartyMembers.Contains(x.ContentId)).ToList(); + var leftMembers = PartyMembers.Where(x => !PartyList.Any(y => y.ContentId == x)).ToList(); + foreach (var member in newMembers) + { + Log.Debug($"Party member {member.Name.TextValue} - {member.ContentId}"); + PartyMembers.Add(member.ContentId); + } + foreach (var member in leftMembers) + { + Log.Debug($"Party member left {member}"); + PartyMembers.Remove(member); + } + } + } + + public List GetPartyMembers(Objects.Session session) + { + var members = new List(); + foreach (var member in PartyList) + { + members.Add(member.ContentId.ToString().SaltedHash(session.ShareCode)); + } + return members; + } + } +} diff --git a/QuestShare.Dalamud/Services/ShareService.cs b/QuestShare.Dalamud/Services/ShareService.cs index 11dec3b..2c3a359 100644 --- a/QuestShare.Dalamud/Services/ShareService.cs +++ b/QuestShare.Dalamud/Services/ShareService.cs @@ -1,133 +1,130 @@ -using FFXIVClientStructs.FFXIV.Client.UI; -using Newtonsoft.Json; -using QuestShare.Common.API; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Dalamud.Plugin.Services; namespace QuestShare.Services { - internal class ShareService + internal class ShareService : IService { - internal static uint ActiveQuestId { get; set; } = 0; - internal static byte ActiveQuestStep { get; set; } = 0; - internal static List SharedMembers = []; - internal static List PartyMembers = []; - internal static bool IsGrouped { get; set; } = false; - internal static bool IsHost { get; set; } = false; - internal static bool IsRegistered { get; set; } = false; - internal static bool IsMsqTracking { get; set; } = false; - internal static string ShareCode { get; private set; } = ""; - internal static string HostedShareCode { get; private set; } = ""; - internal static string Token { get; set; } = ""; + internal static List ShareCodes => ConfigurationManager.Instance.KnownShareCodes; + internal List Sessions { get; private set; } = []; + internal Dictionary CharacterLookup { get; private set; } = []; - public static void Initialize() + public void Initialize() { - Addons.AddonPartyList.OnMemberCountChanged += OnPartyChanged; ClientState.Login += OnLogin; ClientState.Logout += OnLogout; + Framework.Update += OnFrameworkUpdate; } - public static void Dispose() + public void Shutdown() { - OnLogout(0,0); - Addons.AddonPartyList.OnMemberCountChanged -= OnPartyChanged; + OnLogout(0, 0); ClientState.Login -= OnLogin; ClientState.Logout -= OnLogout; + Framework.Update -= OnFrameworkUpdate; + CharacterLookup.Clear(); + Sessions.Clear(); } - public static void OnLogin() + private void OnLogin() { - ShareCode = Plugin.Configuration.Instance.LastShareCode; - Token = Plugin.Configuration.Instance.Token; + CharacterLookup.Clear(); + CharacterLookup.Add((long)ClientState.LocalContentId, ClientState.LocalPlayer!.Name.TextValue); + foreach (var character in ConfigurationManager.Instance.KnownCharacters) + { + CharacterLookup.Add(character.Key, character.Value); + } } - public static void OnLogout(int code, int state) + private void OnLogout(int code, int state) { - Plugin.Configuration.Instance.Token = Token; - Plugin.Configuration.Instance.LastShareCode = ShareCode; - Plugin.Configuration.Save(); - SharedMembers = []; - PartyMembers = []; - IsGrouped = false; - IsHost = false; - IsRegistered = false; - ShareCode = ""; - HostedShareCode = ""; - Token = ""; - ActiveQuestId = 0; - ActiveQuestStep = 0; + ConfigurationManager.Save(); + CharacterLookup.Clear(); + Sessions.Clear(); } - public static void SetShareCode(string shareCode) - { - ShareCode = shareCode; - } + private int pCount = 0; - public static void SetHostedShareCode(string shareCode) + private void OnFrameworkUpdate(IFramework framework) { - HostedShareCode = shareCode; + if (PartyList.Count != pCount) + { + foreach (var partyMember in PartyList) + { + AddKnownCharacter(partyMember.ContentId, partyMember.Name.TextValue); + } + pCount = PartyList.Count; + } } public static void SetActiveQuest(uint questId, byte questStep) { Log.Debug($"Setting active quest to {questId} - {questStep}"); - ActiveQuestId = questId; - ActiveQuestStep = questStep; - } - private static void OnPartyChanged(object? sender, EventArgs e) - { - var members = new List(); - foreach (var member in PartyList) - { - members.Add((ulong)member.ContentId); - } - PartyMembers = members; - SocketClientService.DispatchUpdate(true); + HostService.Update((int)questId, questStep); } - public class SharedMember + public void AddSession(Objects.Session session) { - public required ulong CharacterId { get; set; } - public string CharacterName => ResolveCharacterName(CharacterId); - public bool IsHost { get; set; } = false; - public bool IsMe { get; set; } = false; - + if (Sessions.Any(s => s.ShareCode == session.ShareCode)) + { + return; + } + Sessions.Add(session); + AddKnownShareCode(new Objects.ShareCode() { Code = session.ShareCode, CharacterId = ClientState.LocalContentId.ToString().SaltedHash(session.ShareCode) }); } - public static string ResolveCharacterName(ulong characterId) + public void RemoveSession(Objects.Session session) { - var member = SharedMembers.FirstOrDefault(m => m.CharacterId == characterId); - if (member != null && member.CharacterName == "") - { - if (File.Exists(Path.Join(Plugin.PluginDataPath, "CharacterNames.json"))) - { - var characterNames = JsonConvert.DeserializeObject>(File.ReadAllText(Path.Join(Plugin.PluginDataPath, "CharacterNames.json"))); - if (characterNames != null && characterNames.ContainsKey(member.CharacterId)) - { - return characterNames[member.CharacterId]; - } - } - return member.CharacterName; - } - return ""; + Sessions.Remove(session); + RemoveKnownShareCode(session.ShareCode); } - public static void SaveCharacterName(ulong characterId, string characterName) + public void RemoveSession(string shareCode) { - if (File.Exists(Path.Join(Plugin.PluginDataPath, "CharacterNames.json"))) + var session = Sessions.FirstOrDefault(s => s.ShareCode == shareCode); + if (session != null) { - var characterNames = JsonConvert.DeserializeObject>(File.ReadAllText(Path.Join(Plugin.PluginDataPath, "CharacterNames.json"))); - characterNames ??= []; - characterNames[characterId] = characterName; - File.WriteAllText(Path.Join(Plugin.PluginDataPath, "CharacterNames.json"), JsonConvert.SerializeObject(characterNames)); + RemoveSession(session); } - else + } + + public void UpdateSession(Objects.Session session) + { + var existing = Sessions.FirstOrDefault(s => s.ShareCode == session.ShareCode); + if (existing != null) { - File.WriteAllText(Path.Join(Plugin.PluginDataPath, "CharacterNames.json"), JsonConvert.SerializeObject(new Dictionary { { characterId, characterName } })); + Sessions.Remove(existing); } + Sessions.Add(session); + } + + public static void AddKnownShareCode(Objects.ShareCode shareCode) + { + if (ConfigurationManager.Instance.KnownShareCodes.Any(sc => sc.Code == shareCode.Code)) + { + return; + } + ConfigurationManager.Instance.KnownShareCodes.Add(shareCode); + } + + public static void RemoveKnownShareCode(string shareCode) + { + ConfigurationManager.Instance.KnownShareCodes.RemoveAll(sc => sc.Code == shareCode); + } + + public static void AddKnownCharacter(long contentId, string characterId) + { + if (ConfigurationManager.Instance.KnownCharacters.ContainsKey(contentId)) + { + return; + } + ConfigurationManager.Instance.KnownCharacters.Add(contentId, characterId); + ConfigurationManager.Save(); + } + + public static void RemoveKnownCharacter(long contentId) + { + ConfigurationManager.Instance.KnownCharacters.Remove(contentId); + ConfigurationManager.Save(); } } diff --git a/QuestShare.Dalamud/Services/SocketClientService.cs b/QuestShare.Dalamud/Services/SocketClientService.cs deleted file mode 100644 index 3e36df0..0000000 --- a/QuestShare.Dalamud/Services/SocketClientService.cs +++ /dev/null @@ -1,475 +0,0 @@ - -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using QuestShare.Common.API; -using QuestShare.Common.API.Share; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Text.Json.Nodes; -using System.Threading.Tasks; - -namespace QuestShare.Services -{ - - internal class SocketClientService - { - private readonly string socketUrl = "https://api.nathanc.tech/Hub"; - internal static HubConnection connection { get; private set; } = null!; - internal static bool IsConnected => connection.State == HubConnectionState.Connected; - internal static bool IsAuthorized { get; private set; } = false; - private static bool IsDisposing = false; - - - public event EventHandler OnCancelEvent = delegate { }; - public event EventHandler OnGroupEvent = delegate { }; - public event EventHandler OnUngroupEvent = delegate { }; - public event EventHandler OnGetEvent = delegate { }; - public event EventHandler OnRegisterEvent = delegate { }; - public event EventHandler OnUpdateEvent = delegate { }; - public event EventHandler OnTokenCheckEvent = delegate { }; - - public SocketClientService() - { - var builder = new HubConnectionBuilder().WithUrl(socketUrl).ConfigureLogging(logging => - { - logging.SetMinimumLevel(LogLevel.Information).AddConsole(); - }); - connection = builder.Build(); - connection.Closed += async (error) => - { - if (IsDisposing) return; - Log.Warning($"Connection closed... {error}"); - Log.Warning($"Connection closed, retrying... {error}"); - await Task.Delay(new Random().Next(0, 5) * 1000); - await connection.StartAsync(); - }; - connection.Reconnected += async (error) => - { - Log.Information("Connection reconnected"); - }; - connection.On(nameof(Register), Client_Register); - connection.On(nameof(Update), Client_Update); - connection.On(nameof(Update.UpdateBroadcast), Client_UpdateBroadcast); - connection.On(nameof(GetShareInfo), Client_GetShareInfo); - connection.On(nameof(Cancel), Client_Cancel); - connection.On(nameof(Cancel.CancelBroadcast), Client_CancelBroadast); - connection.On(nameof(GroupJoin), Client_GroupJoin); - connection.On(nameof(GroupLeave), Client_GroupLeave); - connection.On(nameof(Authorize), Client_Authorize); - connection.On(nameof(GroupNotify.GroupNotifyBroadcast), Client_GroupNotify); - connection.On(nameof(Resume), Client_Resume); - connection.On(nameof(Authorize.AuthBroadcast), Client_AuthRequest); - ClientState.Login += OnLogin; - ClientState.Logout += OnLogout; - } - public void Dispose() - { - IsDisposing = true; - if (IsConnected) - { - connection.StopAsync(); - } - connection.DisposeAsync(); - ClientState.Login -= OnLogin; - ClientState.Logout -= OnLogout; - } - - public void OnLogin() - { - if (Plugin.Configuration.Instance.Token != "") - { - ShareService.Token = Plugin.Configuration.Instance.Token; - DispatchAuthorize(); - } - } - - public void OnLogout(int code, int type) - { - ShareService.Token = ""; - Plugin.Configuration.Save(); - IsAuthorized = false; - connection.StopAsync().ConfigureAwait(false); - } - - public async Task Connect() - { - try - { - if (IsConnected) await connection.StopAsync(); - IsDisposing = false; - await connection.StartAsync().ContinueWith(task => - { - if (task.IsFaulted) - { - Log.Error("Failed to connect to socket server"); - } - else - { - Log.Information("Connected to socket server"); - } - }); - } - catch (Exception ex) - { - Log.Error(ex.Message); - } - } - - public static async Task Disconnect() - { - IsDisposing = true; - await connection.StopAsync(); - } - - private async Task Invoke(string methodName, object request) - { - if (!IsConnected) await Connect(); - Log.Debug($"Invoking {methodName} with {JsonConvert.SerializeObject(request)}"); - var s = new StackTrace(); - Log.Debug(s.ToString()); - await connection.InvokeAsync(methodName, request); - } - - public static void DispatchAuthorize() - { - Plugin.SocketClient.Invoke(nameof(Authorize), new Authorize.Request - { - Version = Constants.Version, - Token = ShareService.Token, - CharacterId = ClientState.LocalContentId - }).ConfigureAwait(false); - } - public static void DispatchRegister() - { - if (!IsAuthorized) - { - Log.Warning("Not authorized to register"); - return; - } - var activeQuest = GameQuestManager.GetActiveQuest(); - Plugin.SocketClient.Invoke(nameof(Register), new Register.Request - { - Version = Constants.Version, - Token = ShareService.Token, - CharacterId = ClientState.LocalContentId, - SharedQuestId = activeQuest != null ? activeQuest.QuestId : 0, - SharedQuestStep = activeQuest != null ? activeQuest.CurrentStep : (byte)0, - }).ConfigureAwait(false); - } - public static void DispatchGroup(string shareCode) - { - Plugin.SocketClient.Invoke(nameof(GroupJoin), new GroupJoin.Request - { - Token = ShareService.Token, - ShareCode = shareCode, - Version = Constants.Version, - CharacterId = ClientState.LocalContentId - }).ConfigureAwait(false); - } - public static void DispatchUngroup() - { - Plugin.SocketClient.Invoke(nameof(GroupLeave), new GroupLeave.Request - { - Token = ShareService.Token, - ShareCode = ShareService.ShareCode, - Version = Constants.Version, - }).ConfigureAwait(false); - } - - public static void DispatchResume() - { - var members = new List(); - foreach (var member in PartyList) - { - members.Add((ulong)member.ContentId); - } - Plugin.SocketClient.Invoke(nameof(Resume), new Resume.Request - { - Token = ShareService.Token, - Version = Constants.Version, - Members = members, - ShareCode = Plugin.Configuration.Instance.LastShareCode, - }).ConfigureAwait(false); - } - public static void DispatchGetShareInfo() - { - Plugin.SocketClient.Invoke(nameof(GetShareInfo), new GetShareInfo.Request - { - Token = ShareService.Token, - Version = Constants.Version, - ShareCode = ShareService.ShareCode - }).ConfigureAwait(false); - } - public static void DispatchCancel() - { - Plugin.SocketClient.Invoke(nameof(Cancel), new Cancel.Request - { - Token = ShareService.Token, - Version = Constants.Version, - ShareCode = ShareService.HostedShareCode - }).ConfigureAwait(false); - } - public static void DispatchUpdate(bool partyUpdate) - { - if (!ShareService.IsRegistered && !ShareService.IsGrouped) - { - Log.Error("Not registered or grouped"); - return; - } - var activeQuest = GameQuestManager.GetActiveQuest(); - Plugin.SocketClient.Invoke(nameof(Update), new Update.Request - { - Token = ShareService.Token, - Version = Constants.Version, - SharedQuestId = activeQuest != null ? activeQuest.QuestId : 0, - SharedQuestStep = activeQuest != null ? activeQuest.CurrentStep : (byte)0, - BroadcastParty = Plugin.Configuration.Instance.BroadcastToParty, - PartyMembers = ShareService.IsHost ? ShareService.PartyMembers : [], - IsPartyChange = partyUpdate - }).ConfigureAwait(false); - } - - - #region Server Methods - private Task Client_Register(Register.Response response) - { - Log.Debug($"Client_Register({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - ShareService.SetHostedShareCode(response.ShareCode); - ShareService.IsHost = true; - ShareService.IsGrouped = false; - ShareService.IsRegistered = true; - OnRegisterEvent.Invoke(this, new SocketEventArgs { Success = true }); - } - else - { - OnRegisterEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); - } - return Task.CompletedTask; - } - - private Task Client_Update(Update.Response response) - { - Log.Debug($"Client_Update({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - OnUpdateEvent.Invoke(this, new SocketEventArgs { Success = true }); - } - else - { - OnUpdateEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); - } - return Task.CompletedTask; - } - - private Task Client_UpdateBroadcast(Update.UpdateBroadcast broadcast) - { - Log.Debug($"Client_UpdateBroadcast({JsonConvert.SerializeObject(broadcast)})"); - ShareService.SetActiveQuest(broadcast.SharedQuestId, broadcast.SharedQuestStep); - Log.Debug($"Updated quest: {broadcast.SharedQuestId} - {broadcast.SharedQuestStep}"); - return Task.CompletedTask; - } - - private Task Client_GroupJoin(GroupJoin.Response response) - { - Log.Debug($"Client_GroupJoin({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - Plugin.Configuration.Instance.LastShareCode = response.ShareCode; - Plugin.Configuration.Save(); - ShareService.IsHost = false; - ShareService.IsGrouped = true; - ShareService.SetActiveQuest(response.SharedQuestId, response.SharedQuestStep); - ShareService.SetShareCode(response.ShareCode); - OnGroupEvent.Invoke(this, new SocketEventArgs { Success = true }); - Log.Debug($"Joined group: {response.ShareCode}"); - } - else - { - Log.Error($"Failed to join group: {response.Error}"); - OnGroupEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); - } - return Task.CompletedTask; - } - - private Task Client_GroupLeave(GroupLeave.Response response) - { - Log.Debug($"Client_GroupLeave({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - ShareService.IsGrouped = false; - ShareService.IsHost = false; - ShareService.ActiveQuestId = 0; - ShareService.ActiveQuestStep = 0; - Plugin.Configuration.Instance.LastShareCode = ""; - ShareService.SetShareCode(""); - OnUngroupEvent.Invoke(this, new SocketEventArgs { Success = true }); - Log.Debug("Left group"); - } - else - { - Log.Error($"Failed to leave group: {response.Error}"); - OnUngroupEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); - } - return Task.CompletedTask; - } - - private Task Client_Resume(Resume.Response response) - { - Log.Debug($"Client_Resume({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - if (response.IsHost) - { - ShareService.PartyMembers = response.Members ?? []; - ShareService.IsHost = response.IsHost; - ShareService.IsGrouped = false; - ShareService.IsRegistered = true; - ShareService.SetHostedShareCode(response.ShareCode); - GameQuestManager.SetActiveFlag(response.SharedQuestId); - } else - { - ShareService.PartyMembers = response.Members ?? []; - ShareService.IsHost = false; - ShareService.IsGrouped = true; - ShareService.SetActiveQuest(response.SharedQuestId, response.SharedQuestStep); - ShareService.SetShareCode(response.ShareCode); - } - OnGetEvent.Invoke(this, new SocketEventArgs { Success = true }); - Log.Debug($"Resumed share: {response.ShareCode}"); - DispatchUpdate(false); - } - else - { - Log.Warning($"Failed to resume share: {response.Error}"); - ShareService.ActiveQuestId = 0; - ShareService.ActiveQuestStep = 0; - ShareService.SetShareCode(""); - ShareService.IsGrouped = false; - ShareService.IsHost = false; - ShareService.PartyMembers = []; - OnGetEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); - } - return Task.CompletedTask; - } - - private Task Client_Authorize(Authorize.Response response) - { - Log.Debug($"Client_Authorize({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - ShareService.Token = response.Token; - Plugin.Configuration.Instance.Token = response.Token; - Plugin.Configuration.Save(); - Log.Debug("Logged in"); - IsAuthorized = true; - } - else - { - Log.Error($"Failed to authorize: {response.Error}"); - } - DispatchResume(); - return Task.CompletedTask; - } - - private Task Client_AuthRequest(Authorize.AuthBroadcast _) - { - Log.Debug("Received auth request"); - DispatchAuthorize(); - return Task.CompletedTask; - } - - private Task Client_Cancel(Cancel.Response response) - { - Log.Debug($"Client_Cancel({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - ShareService.SetActiveQuest(0, 0); - ShareService.SetShareCode(""); - ShareService.IsGrouped = false; - ShareService.IsHost = false; - OnCancelEvent.Invoke(this, new SocketEventArgs { Success = true }); - Log.Debug("Cancelled share"); - } - else - { - Log.Error($"Failed to cancel share: {response.Error}"); - ShareService.SetActiveQuest(0, 0); - ShareService.SetShareCode(""); - ShareService.IsGrouped = false; - ShareService.IsHost = false; - OnCancelEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); - } - return Task.CompletedTask; - } - - private Task Client_CancelBroadast(Cancel.CancelBroadcast broadcast) - { - Log.Debug($"Client_CancelBroadcast({JsonConvert.SerializeObject(broadcast)})"); - ShareService.SetActiveQuest(0, 0); - ShareService.SetShareCode(""); - ShareService.IsGrouped = false; - ShareService.IsHost = false; - OnCancelEvent.Invoke(this, new SocketEventArgs { Success = true }); - Log.Debug("Cancelled share"); - return Task.CompletedTask; - } - - private Task Client_GetShareInfo(GetShareInfo.Response response) - { - Log.Debug($"Client_GetShareInfo({JsonConvert.SerializeObject(response)})"); - if (response.Success) - { - ShareService.SetActiveQuest(response.SharedQuestId, response.SharedQuestStep); - ShareService.PartyMembers = response.Members ?? []; - OnGetEvent.Invoke(this, new SocketEventArgs { Success = true }); - Log.Debug($"Received share info: {response}"); - } - else - { - Log.Error($"Failed to get share info: {response.Error}"); - OnGetEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); - } - return Task.CompletedTask; - } - - private Task Client_GroupNotify(GroupNotify.GroupNotifyBroadcast broadcast) - { - Log.Debug($"Client_GroupNotify({JsonConvert.SerializeObject(broadcast)})"); - if (broadcast.NotifyType == NotifyType.Join) - { - ShareService.SharedMembers.Add(new ShareService.SharedMember { CharacterId = broadcast.CharacterId }); - } - else if (broadcast.NotifyType == NotifyType.Leave) - { - ShareService.SharedMembers.RemoveAll(m => m.CharacterId == broadcast.CharacterId); - } - else if (broadcast.NotifyType == NotifyType.JoinViaParty) - { - ShareService.SharedMembers.Add(new ShareService.SharedMember { CharacterId = broadcast.CharacterId }); - } - else if (broadcast.NotifyType == NotifyType.Rejoin) - { - ShareService.SharedMembers.Add(new ShareService.SharedMember { CharacterId = broadcast.CharacterId }); - } - return Task.CompletedTask; - } - #endregion - public class SocketEventArgs : EventArgs - { - public bool Success { get; set; } - public string Message { get; set; } = ""; - } - public class TokenCheckEventArgs : EventArgs - { - public bool TokenValid { get; set; } - public bool ShareCodeValid { get; set; } - } - } -} diff --git a/QuestShare.Dalamud/Common/WindowManager.cs b/QuestShare.Dalamud/Services/UiService.cs similarity index 69% rename from QuestShare.Dalamud/Common/WindowManager.cs rename to QuestShare.Dalamud/Services/UiService.cs index b510d70..1de3295 100644 --- a/QuestShare.Dalamud/Common/WindowManager.cs +++ b/QuestShare.Dalamud/Services/UiService.cs @@ -1,18 +1,16 @@ using Dalamud.Interface.Windowing; -using QuestShare.Windows.ConfigWindow; using QuestShare.Windows.MainWindow; -namespace QuestShare.Common +namespace QuestShare.Services { - internal static class WindowManager + internal class UiService : IService { - public static WindowSystem WindowSystem = new("SamplePlugin"); - public static ConfigWindow ConfigWindow { get; private set; } = new(); + public static WindowSystem WindowSystem = new("QuestShare"); public static MainWindow MainWindow { get; private set; } = new(); + public static string LastErrorMessage { get; set; } = string.Empty; - public static void Initialize() + public void Initialize() { - WindowSystem.AddWindow(ConfigWindow); WindowSystem.AddWindow(MainWindow); PluginInterface.UiBuilder.Draw += DrawUI; @@ -24,18 +22,17 @@ namespace QuestShare.Common // Adds another button that is doing the same but for the main ui of the plugin PluginInterface.UiBuilder.OpenMainUi += ToggleMainUI; } - public static void Dispose() + public void Shutdown() { WindowSystem.RemoveAllWindows(); PluginInterface.UiBuilder.Draw -= DrawUI; PluginInterface.UiBuilder.OpenConfigUi -= ToggleConfigUI; PluginInterface.UiBuilder.OpenMainUi -= ToggleMainUI; MainWindow.Dispose(); - ConfigWindow.Dispose(); } private static void DrawUI() => WindowSystem.Draw(); - public static void ToggleConfigUI() => ConfigWindow.Toggle(); + public static void ToggleConfigUI() => MainWindow.Toggle(); public static void ToggleMainUI() => MainWindow.Toggle(); } } diff --git a/QuestShare.Dalamud/Windows/ConfigWindow/ConfigWindow.cs b/QuestShare.Dalamud/Windows/ConfigWindow/ConfigWindow.cs deleted file mode 100644 index 04c6e28..0000000 --- a/QuestShare.Dalamud/Windows/ConfigWindow/ConfigWindow.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Numerics; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using QuestShare; -using QuestShare.Common; - -namespace QuestShare.Windows.ConfigWindow; - -public class ConfigWindow : Window, IDisposable -{ - private ConfigurationManager Configuration { get; set; } - - // We give this window a constant ID using ### - // This allows for labels being dynamic, like "{FPS Counter}fps###XYZ counter window", - // and the window ID will always be "###XYZ counter window" for ImGui - public ConfigWindow() : base(Plugin.Name + " Config###ConfigWindow") - { - Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar | - ImGuiWindowFlags.NoScrollWithMouse; - - Size = new Vector2(232, 90); - SizeCondition = ImGuiCond.Always; - - Configuration = Plugin.Configuration; - } - - public void Dispose() { } - - public override void PreDraw() - { - } - - public override void Draw() - { - var connectOnStartup = Configuration.Instance.ConnectOnStartup; - var resumeOnStartup = Configuration.Instance.ResumeOnStartup; - var autoShareMsq = Configuration.Instance.AutoShareMsq; - var autoShareNewQuests = Configuration.Instance.AutoShareNewQuests; - var BroadcastToParty = Configuration.Instance.BroadcastToParty; - - ImGui.PushItemWidth(ImGui.GetWindowWidth() * 0.5f); - ImGui.Checkbox("Connect on startup", ref connectOnStartup); - ImGui.Checkbox("Resume on startup", ref resumeOnStartup); - ImGui.Checkbox("Auto share MSQ", ref autoShareMsq); - ImGui.Checkbox("Auto share new quests", ref autoShareNewQuests); - ImGui.Checkbox("Require party", ref BroadcastToParty); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Require joiners to be in your party. You do not have to be partied after others join your group."); - } - ImGui.PopItemWidth(); - } -} diff --git a/QuestShare.Dalamud/Windows/ImGuiUtils.cs b/QuestShare.Dalamud/Windows/ImGuiUtils.cs index 7b6dd0e..fc781f8 100644 --- a/QuestShare.Dalamud/Windows/ImGuiUtils.cs +++ b/QuestShare.Dalamud/Windows/ImGuiUtils.cs @@ -1,6 +1,5 @@ using Dalamud.Interface.Utility; using ImGuiNET; -using System; using System.Numerics; namespace QuestShare.Windows diff --git a/QuestShare.Dalamud/Windows/MainWindow/MainWindow.cs b/QuestShare.Dalamud/Windows/MainWindow/MainWindow.cs index 856d405..3dbc0a5 100644 --- a/QuestShare.Dalamud/Windows/MainWindow/MainWindow.cs +++ b/QuestShare.Dalamud/Windows/MainWindow/MainWindow.cs @@ -1,13 +1,13 @@ -using System; using System.Numerics; -using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Utility; using ImGuiNET; -using QuestShare.Common; +using Microsoft.AspNetCore.SignalR.Client; using QuestShare.Services; +using static QuestShare.Services.ApiService; namespace QuestShare.Windows.MainWindow; @@ -29,257 +29,383 @@ public class MainWindow : Window, IDisposable public void Dispose() { } + private ApiService ApiService => (ApiService)Plugin.GetService(); + private ShareService ShareService => (ShareService)Plugin.GetService(); + private PartyService PartyService => (PartyService)Plugin.GetService(); + private HostService HostService => (HostService)Plugin.GetService(); - - private string enteredShareCode = Plugin.Configuration.Instance.LastShareCode; - private bool unsavedChanges = false; private GameQuest? selectedQuest = GameQuestManager.GetActiveQuest(); - private ShareMode shareMode = Plugin.Configuration.Instance.LastShareMode; - private bool isConnecting = false; + + private enum ActiveTab + { + Host, + Join, + Settings + } public override void Draw() { - var connected = SocketClientService.IsConnected; - var token = ShareService.Token; - ImGui.TextUnformatted("Server Status: "); ImGui.SameLine(); - if (isConnecting) + DrawConnectionState(); + ImGui.SameLine(); + if (ApiService.IsConnected) { - ImGui.TextColored(ImGuiColors.DalamudYellow, "Connecting..."); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Unlink, ImGuiColors.DPSRed)) + { + _ = ApiService.Disconnect(); + } } else { - ImGui.TextColored(connected ? ImGuiColors.HealerGreen : ImGuiColors.DPSRed, connected ? "Connected" : "Disconnected"); - } - ImGui.SameLine(); - ImGui.TextUnformatted($"Share Mode: {shareMode.ToString()}"); - ImGui.Separator(); - if (ImGui.Button("Connect")) - { - Plugin.SocketClient.Connect().ConfigureAwait(false); - isConnecting = true; - } - ImGui.SameLine(); - if (ImGui.Button("Disconnect")) - { - SocketClientService.Disconnect().ConfigureAwait(false); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Link, ImGuiColors.DPSRed)) + { + _ = ApiService.Connect(); + } } // ImGui.SameLine(); ImGui.Separator(); - if (!SocketClientService.IsConnected) + using (ImRaii.TabBar("MainTabBar", ImGuiTabBarFlags.NoCloseWithMiddleMouseButton)) { - ImGui.TextColored(ImGuiColors.DPSRed, "Not connected to server."); - return; - } - isConnecting = false; - ImGui.TextUnformatted("Share Mode:"); - // ImGui.BeginDisabled(ShareService.IsHost || ShareService.IsGrouped || ShareService.IsRegistered); - /*if (ShareService.IsHost && SocketClientService.IsConnected) - { - shareMode = ShareMode.Host; - } else if (ShareService.IsGrouped && SocketClientService.IsConnected) - { - shareMode = ShareMode.Member; - } else if (SocketClientService.IsConnected) - { - shareMode = ShareMode.None; - } */ // TODO: Fix this - if (ImGui.RadioButton("Host", shareMode == ShareMode.Host)) - { - shareMode = ShareMode.Host; - Plugin.Configuration.Instance.LastShareMode = ShareMode.Host; - Plugin.Configuration.Save(); - } - ImGui.SameLine(); - if (ImGui.RadioButton("Receive", shareMode == ShareMode.Member)) - { - shareMode = ShareMode.Member; - Plugin.Configuration.Instance.LastShareMode = ShareMode.Member; - Plugin.Configuration.Save(); - } - // ImGui.EndDisabled(); - ImGui.Separator(); - if (shareMode == ShareMode.Host) - { - ImGui.TextUnformatted("Registered: "); ImGui.SameLine(); ImGui.TextColored(ShareService.IsHost ? ImGuiColors.HealerGreen : ImGuiColors.DPSRed, ShareService.IsHost.ToString()); - ImGui.SameLine(); - if (!ShareService.IsHost) + ImGui.BeginDisabled(!ApiService.IsConnected); + if (ImGui.BeginTabItem("Host Group")) { - ImGui.BeginDisabled(!connected); - if (ImGui.Button("Register")) - { - SocketClientService.DispatchRegister(); - } - ImGui.EndDisabled(); + DrawHostTab(); + ImGui.EndTabItem(); } - else + if (ImGui.BeginTabItem("Join Group")) { - ImGui.BeginDisabled(!connected); - if (ImGui.Button("Cancel")) - { - SocketClientService.DispatchCancel(); - } - ImGui.EndDisabled(); - } - ImGui.SameLine(); - ImGui.TextUnformatted(" "); - ImGui.SameLine(); - ImGui.TextUnformatted($"Share Code:"); - ImGui.SameLine(); - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen); - ImGui.TextUnformatted(ShareService.HostedShareCode); - if (ImGui.IsItemClicked()) - { - ImGui.SetClipboardText(ShareService.HostedShareCode); - } - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Click to copy to clipboard"); - } - ImGui.PopStyleColor(); - ImGui.Separator(); - ImGui.BeginDisabled(ShareService.IsMsqTracking); - using (var combo = ImRaii.Combo("##Quests", GameQuestManager.GetActiveQuest()?.QuestName ?? "---SELECT---", ImGuiComboFlags.HeightRegular)) - { - if (combo) - { - foreach (var quest in GameQuestManager.GameQuests.OrderBy(q => q.QuestName)) - { - if (ImGui.Selectable(quest.QuestName)) - { - selectedQuest = quest; - GameQuestManager.SetActiveFlag(quest.QuestId); - SocketClientService.DispatchUpdate(false); - } - } - } - } - ImGui.SameLine(); - if (ImGui.Button("Refresh")) - { - GameQuestManager.LoadQuests(); + DrawJoinTab(); + ImGui.EndTabItem(); } ImGui.EndDisabled(); - ImGui.SameLine(); - var track = Plugin.Configuration.Instance.TrackMSQ; - if (ImGui.Checkbox("Track MSQ", ref track)) + if (ImGui.BeginTabItem("Settings")) { - Plugin.Configuration.Instance.TrackMSQ = track; - ShareService.IsMsqTracking = track; - Plugin.Configuration.Save(); - } - if (selectedQuest == null && GameQuestManager.GetActiveQuest() == null) - { - ImGui.TextUnformatted("No quest selected."); - return; - } else if (GameQuestManager.GetActiveQuest() != null) - { - selectedQuest = GameQuestManager.GetActiveQuest(); - } - ImGui.TextUnformatted("Active Quest:"); - ImGui.SameLine(); - ImGui.TextUnformatted(selectedQuest!.QuestName); - ImGui.TextUnformatted("Current Step:"); - ImGui.SameLine(); - ImGui.TextUnformatted(selectedQuest.CurrentStep.ToString()); - ImGui.Separator(); - ImGui.TextUnformatted("Quest Steps:"); - ImGui.Separator(); - - var steps = selectedQuest.QuestSteps; - for (var i = 0; i < steps!.Count; i++) - { - - if (i + 1 == selectedQuest.CurrentStep || (selectedQuest.CurrentStep == 0 && i == 0) || (selectedQuest.CurrentStep == 0xFF && i + 1 == steps.Count)) - { - ImGui.TextColored(ImGuiColors.HealerGreen, steps[i]); - } - else if (i+1 < selectedQuest.CurrentStep) - { - ImGui.TextColored(ImGuiColors.DalamudYellow, steps[i]); - } - else - { - ImGui.TextUnformatted("???"); - } + DrawSettingsTab(); + ImGui.EndTabItem(); } } - else if (shareMode == ShareMode.Member) + ImGui.Separator(); + if (UiService.LastErrorMessage != null) { - ImGui.SetNextItemWidth(100); - ImGui.InputTextWithHint("##ShareCode", "Share Code", ref enteredShareCode, 10); + // ImGui.TextColored(ImGuiColors.DPSRed, UiService.LastErrorMessage); + } + } + + private void DrawConnectionState() + { + switch (this.ApiService.ConnectionState) + { + case HubConnectionState.Connecting: + ImGui.TextColored(ImGuiColors.DalamudYellow, "Connecting..."); + break; + case HubConnectionState.Connected: + ImGui.TextColored(ImGuiColors.HealerGreen, "Connected"); + break; + case HubConnectionState.Disconnected: + ImGui.TextColored(ImGuiColors.DPSRed, "Disconnected"); + break; + case HubConnectionState.Reconnecting: + ImGui.TextColored(ImGuiColors.DalamudYellow, "Reconnecting..."); + break; + default: + break; + } + } + + private bool generatePending = false; + + private void DrawHostTab() + { + if (HostService.ActiveSession != null && HostService.ActiveSession.ShareCode != null) generatePending = false; + var isEnabled = ConfigurationManager.Instance.EnableHosting; + if (ImGuiComponents.ToggleButton(Namespace + "/Enable Hosting", ref isEnabled)) + { + ConfigurationManager.Instance.EnableHosting = isEnabled; + ConfigurationManager.Save(); + } + ImGui.SameLine(); + ImGui.TextUnformatted("Enable Hosting"); + ImGui.Separator(); + if (isEnabled) + { + ImGui.TextUnformatted("Share Code:"); ImGui.SameLine(); - if (!ShareService.IsGrouped) + if (HostService.ActiveSession != null) { - ImGui.BeginDisabled(enteredShareCode.Length < 1); - if (ImGui.Button("Join Group")) + ImGui.TextColored(ImGuiColors.HealerGreen, HostService.ActiveSession.ShareCode); + if (ImGui.IsItemClicked()) { - Plugin.Configuration.Instance.LastShareCode = enteredShareCode; - SocketClientService.DispatchGroup(enteredShareCode); + ImGui.SetClipboardText(HostService.ActiveSession.ShareCode); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Click to copy to clipboard."); + } + ImGui.SameLine(); + if (ImGui.Button("Cancel")) + { + DispatchCancel(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Cancel the current session. This will permanently remove the share code and all connected clients."); + } + var allowJoins = HostService.AllowJoins; + var skipPartyCheck = HostService.SkipPartyCheck; + var sendUpdates = HostService.IsActive; + if (ImGui.Checkbox("Allow new Joins", ref allowJoins)) + { + HostService.SetAllowJoins(allowJoins); + } + ImGui.SameLine(); + if (ImGui.Checkbox("Skip Party Check", ref skipPartyCheck)) + { + HostService.SetSkipPartyCheck(skipPartyCheck); + } + ImGui.SameLine(); + if (ImGui.Checkbox("Send Updates", ref sendUpdates)) + { + HostService.SetIsActive(sendUpdates); + } + + var shareMsq = ConfigurationManager.Instance.TrackMSQ; + ImGui.BeginDisabled(shareMsq); + using (var combo = ImRaii.Combo("##Quests", GameQuestManager.GetActiveQuest()?.QuestName ?? "---SELECT---", ImGuiComboFlags.HeightRegular)) + { + if (combo) + { + foreach (var quest in GameQuestManager.GameQuests.OrderBy(q => q.QuestName)) + { + if (ImGui.Selectable(quest.QuestName)) + { + selectedQuest = quest; + GameQuestManager.SetActiveFlag(quest.QuestId); + HostService.Update((int)quest.QuestId, quest.CurrentStep); + ConfigurationManager.Save(); + } + } + } + } + ImGui.SameLine(); + if (ImGui.Button("Refresh")) + { + GameQuestManager.LoadQuests(); + } + ImGui.EndDisabled(); + ImGui.SameLine(); + var track = ConfigurationManager.Instance.TrackMSQ; + if (ImGui.Checkbox("Track MSQ", ref track)) + { + ConfigurationManager.Instance.TrackMSQ = track; + ConfigurationManager.Save(); + } + if (selectedQuest == null && GameQuestManager.GetActiveQuest() == null) + { + ImGui.TextUnformatted("No quest selected."); + return; + } + else if (GameQuestManager.GetActiveQuest() != null) + { + selectedQuest = GameQuestManager.GetActiveQuest(); + } + ImGui.TextUnformatted("Active Quest:"); + ImGui.SameLine(); + ImGui.TextUnformatted(selectedQuest!.QuestName); + ImGui.TextUnformatted("Current Step:"); + ImGui.SameLine(); + ImGui.TextUnformatted(selectedQuest.CurrentStep.ToString()); + ImGui.Separator(); + ImGui.TextUnformatted("Quest Steps:"); + ImGui.Separator(); + + var steps = selectedQuest.QuestSteps; + for (var i = 0; i < steps!.Count; i++) + { + + if (i + 1 == selectedQuest.CurrentStep || (selectedQuest.CurrentStep == 0 && i == 0) || (selectedQuest.CurrentStep == 0xFF && i + 1 == steps.Count)) + { + ImGui.TextColored(ImGuiColors.HealerGreen, steps[i]); + } + else if (i + 1 < selectedQuest.CurrentStep) + { + ImGui.TextColored(ImGuiColors.DalamudYellow, steps[i]); + } + else + { + ImGui.TextUnformatted("???"); + } + } + + } else { + ImGui.BeginDisabled(generatePending); + if (ImGui.Button("Generate New")) + { + ApiService.DispatchRegister(); } ImGui.EndDisabled(); } - else - { - if (ImGui.Button("Leave Group")) - { - SocketClientService.DispatchUngroup(); - } - ImGui.Separator(); - ImGui.TextUnformatted("Host Active Quest:"); - ImGui.SameLine(); - var activeQuest = ShareService.ActiveQuestId; - var activeStep = ShareService.ActiveQuestStep; - if (activeQuest != 0) - { - var questInfo = GameQuestManager.GetQuestById(activeQuest); - ImGui.TextUnformatted(questInfo.QuestData.Name.ExtractText()); - ImGui.TextUnformatted("Current Step:"); - ImGui.SameLine(); - ImGui.TextUnformatted(activeStep.ToString()); - ImGui.Separator(); - ImGui.TextUnformatted("Quest Steps:"); - ImGui.Separator(); - var steps = questInfo.QuestSteps; - for (var i = 0; i < steps.Count; i++) - { - if (i+1 == activeStep || (i+1 == steps.Count && activeStep == 0xFF)) - { - ImGui.TextColored(ImGuiColors.HealerGreen, steps[i]); - } - else - { - ImGui.TextUnformatted(steps[i]); - } - } - if (ImGui.Button("Get Marker")) - { - var marker = questInfo.GetMapLink((byte)(activeStep - 1)); - if (marker != null) - GameGui.OpenMapWithMapLink(marker); - else - Log.Error("No map link available for this quest."); - } - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Generates a map marker for the current step's destination."); - } - ImGui.SameLine(); - /*if (ImGui.Button("Teleport")) - { - // attempt to generate a path to the next step - } - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Teleports to nearest aetheryte of quest destination."); - }*/ - } - else + } + + } + + private string enteredShareCode = ""; + private bool isJoining = false; + private bool isLeaving = false; + private void OnGroupJoin(object? sender, GroupJoinEventArgs args) + { + isJoining = false; + ApiService.GroupJoined -= OnGroupJoin; + } + private void DrawJoinTab() + { + ImGui.TextUnformatted("Enter Share Code:"); + ImGui.SameLine(); + ImGui.BeginDisabled(isJoining); + ImGui.InputText("##ShareCode", ref enteredShareCode, 8); + ImGui.SameLine(); + var btn = "Join"; + if (isJoining) btn = "Joining..."; + if (ImGui.Button(btn)) + { + var payload = new Objects.ShareCode { CharacterId = ClientState.LocalContentId.ToString().SaltedHash(enteredShareCode), Code = enteredShareCode }; + isJoining = true; + ApiService.GroupJoined += OnGroupJoin; + ApiService.DispatchGroup(payload); + } + ImGui.EndDisabled(); + ImGui.Separator(); + ImGui.TextUnformatted("Currently Joined Groups"); + if (ShareService.Sessions.Count == 0) + { + ImGui.TextUnformatted("No groups joined."); + } + else + { + foreach (var session in ShareService.Sessions) + { + using var tree = ImRaii.TreeNode($"Session: {session.ShareCode}"); + if (tree) { - ImGui.TextUnformatted("No active quest."); + DrawSessionDetails(session); + if (ImGui.Button("Leave Group")) + { + ApiService.DispatchUngroup(session); + isLeaving = true; + } } } } } + + private void DrawSessionDetails(Objects.Session session) + { + ImGui.TextUnformatted($"Owner: {session.OwnerCharacterId}"); + var activeQuest = session.ActiveQuestId; + var activeStep = session.ActiveQuestStep; + if (activeQuest != 0) + { + var questInfo = GameQuestManager.GetQuestById((uint)activeQuest); + var steps = questInfo.QuestSteps; + + ImGui.TextUnformatted(questInfo.QuestData.Name.ExtractText()); + ImGui.TextUnformatted("Current Step:"); + ImGui.SameLine(); + ImGui.TextUnformatted(activeStep.ToString()); + ImGui.Separator(); + ImGui.TextUnformatted("Quest Steps:"); + ImGui.Separator(); + for (var i = 0; i < steps.Count; i++) + { + if (i + 1 == activeStep || (i + 1 == steps.Count && activeStep == 0xFF)) + { + ImGui.TextColored(ImGuiColors.HealerGreen, steps[i]); + } + else + { + ImGui.TextUnformatted(steps[i]); + } + } + if (ImGui.Button("Get Marker")) + { + var marker = questInfo.GetMapLink((byte)(activeStep - 1)); + if (marker != null) + GameGui.OpenMapWithMapLink(marker); + else + Log.Error("No map link available for this quest."); + + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Generates a map marker for the current step's destination."); + } + // ImGui.SameLine(); + /*if (ImGui.Button("Teleport")) + { + // attempt to generate a path to the next step + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Teleports to nearest aetheryte of quest destination."); + }*/ + } else + { + ImGui.TextUnformatted("No active quest or host is offline."); + } + + } + + private void DrawSettingsTab() + { + var selectedApiServer = ConfigurationManager.Instance.ApiDisplayName; + if (ImGui.BeginCombo("API Server", selectedApiServer)) + { + foreach (var server in ConfigurationManager.Instance.ApiConfigurations) + { + var isSelected = selectedApiServer == server.DisplayName; + if (ImGui.Selectable(server.DisplayName, isSelected)) + { + var index = Array.FindIndex(ConfigurationManager.Instance.ApiConfigurations, x => x.DisplayName == server.DisplayName); + ConfigurationManager.Instance.ApiConfigurations[index].Active = true; + foreach (var config in ConfigurationManager.Instance.ApiConfigurations) + { + if (config.DisplayName != server.DisplayName) + { + config.Active = false; + } + } + ConfigurationManager.Save(); + } + if (isSelected) + { + ImGui.SetItemDefaultFocus(); + } + } + ImGui.EndCombo(); + } + ImGui.SameLine(); + if (ImGui.Button("Add")) + { + // does nothing yet + } + var connectOnStartup = ConfigurationManager.Instance.ConnectOnStartup; + if (ImGui.Checkbox("Connect on Startup", ref connectOnStartup)) + { + ConfigurationManager.Instance.ConnectOnStartup = connectOnStartup; + ConfigurationManager.Save(); + } + var autoShareMsq = ConfigurationManager.Instance.AutoShareMsq; + if (ImGui.Checkbox("Auto Share MSQ", ref autoShareMsq)) + { + ConfigurationManager.Instance.AutoShareMsq = autoShareMsq; + ConfigurationManager.Save(); + } + var autoShareNewQuests = ConfigurationManager.Instance.AutoShareNewQuests; + if (ImGui.Checkbox("Auto Share New Quests", ref autoShareNewQuests)) + { + ConfigurationManager.Instance.AutoShareNewQuests = autoShareNewQuests; + ConfigurationManager.Save(); + } + + } } diff --git a/QuestShare.Dalamud/Windows/MainWindow/MainWindow.cs_ b/QuestShare.Dalamud/Windows/MainWindow/MainWindow.cs_ deleted file mode 100644 index 10a2f38..0000000 --- a/QuestShare.Dalamud/Windows/MainWindow/MainWindow.cs_ +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Numerics; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using QuestShare.Common; -using QuestShare.Services; - -namespace QuestShare.Windows.MainWindow; - -public class MainWindow : Window, IDisposable -{ - public MainWindow() - : base(Plugin.Name + "###Main", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) - { - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(600, 650), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) - }; - } - - public override void OnOpen() - { - var questId = 69286; - // find the quest by id - - } - - public void Dispose() { } - - private string enteredShareCode = Plugin.Configuration.LastShareCode; - private bool unsavedChanges = false; - private uint activeQuestId = 0; - private byte activeQuestStep = 0; - public override void Draw() - { - ImGui.TextUnformatted($"Token: {ShareService.Token}"); - ImGui.TextUnformatted($"Share Code: {ShareService.ShareCode}"); - ImGui.TextUnformatted($"Socket Status: {SocketClientService.IsConnected}"); - ImGui.TextUnformatted($"Is Hosting: {ShareService.IsHost} - Is Grouped: {ShareService.IsGrouped}"); - ImGui.Separator(); - if (ImGui.Button("Connect")) - { - Plugin.SocketClient.Connect().ConfigureAwait(false); - } - ImGui.SameLine(); - if (ImGui.Button("Disconnect")) - { - SocketClientService.connection.StopAsync().ConfigureAwait(false); - } - if (ImGui.Button("Register")) - { - SocketClientService.DispatchRegister(); - } - ImGui.SameLine(); - ImGui.BeginDisabled(!ShareService.IsGrouped && !ShareService.IsHost); - if (ImGui.Button("Cancel")) - { - SocketClientService.DispatchCancel(); - } - ImGui.EndDisabled(); - ImGui.SetNextItemWidth(100); - ImGui.InputTextWithHint("##ShareCode", "Share Code", ref enteredShareCode, 10); - ImGui.SameLine(); - ImGui.BeginDisabled(enteredShareCode.Length < 1); - if (ImGui.Button("Pair")) - { - SocketClientService.DispatchGroup(enteredShareCode); - } - ImGui.EndDisabled(); - ImGui.Separator(); - if (ShareService.IsHost) - { - ImGui.TextUnformatted("Host Controls:"); - ImGui.TextUnformatted("Share Code: "); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.HealerGreen, ShareService.ShareCode); - ImGui.TextUnformatted("Quests:"); - ImGui.BeginChild("##Quests", new Vector2(0, ImGui.GetTextLineHeightWithSpacing() * 10), true); - ImGui.BeginTable("##QuestTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingFixedFit, new Vector2(0, -ImGui.GetTextLineHeightWithSpacing())); - ImGui.TableSetupColumn("Sync", ImGuiTableColumnFlags.WidthFixed, 50); - ImGui.TableSetupColumn("Active", ImGuiTableColumnFlags.WidthFixed, 50); - ImGui.TableSetupColumn("Quest", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableHeadersRow(); - foreach (var quest in GameQuestManager.GameQuests) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - var isSynced = quest.IsSynced; - ImGui.BeginDisabled(quest.IsActive); - if (ImGui.Checkbox($"##{quest.QuestId}_Sync", ref isSynced)) - { - GameQuestManager.SetSyncFlag(quest.QuestId, isSynced); - unsavedChanges = true; - } - ImGui.EndDisabled(); - ImGui.TableNextColumn(); - var isActive = quest.IsActive; - ImGui.BeginDisabled(!isSynced); - if (ImGui.Checkbox($"##{quest.QuestId}_Active", ref isActive)) - { - GameQuestManager.SetActiveFlag(quest.QuestId, isActive); - unsavedChanges = true; - } - ImGui.EndDisabled(); - ImGui.TableNextColumn(); - ImGui.TextColored(isActive ? ImGuiColors.HealerGreen : ImGuiColors.DalamudGrey, quest.QuestData.Name.ExtractText()); - if (isActive) - { - activeQuestId = quest.QuestId; - activeQuestStep = quest.CurrentStep; - } - } - ImGui.EndTable(); - ImGui.EndDisabled(); - ImGui.EndChild(); - ImGui.BeginDisabled(!unsavedChanges); - if (ImGui.Button("Save")) - { - SocketClientService.DispatchUpdate(false); - unsavedChanges = false; - } - ImGui.EndDisabled(); - } - else - { - ImGui.TextUnformatted("Click Register to host a share session, or enter a share code to join."); - } - - if (ShareService.IsGrouped || ShareService.IsHost) - { - ImGui.Separator(); - ImGui.TextUnformatted("Members:"); - ImGui.BeginChild("##Members", new Vector2(0, ImGui.GetTextLineHeightWithSpacing() * 5), true); - foreach (var member in ShareService.SharedMembers) - { - ImGui.TextUnformatted(member.CharacterName); - } - ImGui.EndChild(); - } - ImGui.TextUnformatted("Current Active Quest Steps:"); - if (activeQuestId != 0) - { - ImGui.TextUnformatted($"Quest: {GameQuestManager.GameQuests.FirstOrDefault(q => q.QuestId == activeQuestId)?.QuestData.Name.ExtractText()}"); - ImGui.TextUnformatted($"Step: {activeQuestStep}"); - ImGui.TextUnformatted("Steps:"); - ImGui.BeginChild("##QuestSteps", new Vector2(0, ImGui.GetTextLineHeightWithSpacing()*5), true); - var steps = GameQuestManager.GameQuests.FirstOrDefault(q => q.QuestId == activeQuestId)?.QuestSteps; - var counter = 0; - if (steps != null) - { - foreach (var step in steps) - { - if (counter+1 == activeQuestStep || (activeQuestStep == 0xFF && steps.Count == counter+1)) - { - ImGui.TextColored(ImGuiColors.HealerGreen, step); - } - else - { - ImGui.TextUnformatted(step); - } - counter++; - } - } else - { - ImGui.TextUnformatted("No steps found."); - } - ImGui.EndChild(); - } - else - { - ImGui.TextUnformatted("No active quest."); - } - } -} diff --git a/QuestShare.Server/Hubs/Methods/Authorize.cs b/QuestShare.Server/Hubs/Methods/Authorize.cs index 57e637a..8021869 100644 --- a/QuestShare.Server/Hubs/Methods/Authorize.cs +++ b/QuestShare.Server/Hubs/Methods/Authorize.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.SignalR; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using QuestShare.Common; using QuestShare.Server.Managers; namespace QuestShare.Server.Hubs @@ -16,7 +18,7 @@ namespace QuestShare.Server.Hubs return; } var error = Error.None; - if (request.CharacterId == 0) error = Error.InvalidCharacterId; + Log.Debug($"[AUTHORIZE] Client {Context.ConnectionId} attempting to authorize. {JsonConvert.SerializeObject(request)}"); if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; if (error != Error.None) { @@ -29,54 +31,69 @@ namespace QuestShare.Server.Hubs Context.Abort(); return; } - var client = ClientManager.GetClient(Context.ConnectionId, request.Token); - var clientCharacterId = ClientManager.GetClient(request.CharacterId); - if (client == null && clientCharacterId == null) + var client = await ClientManager.GetClient(Context.ConnectionId, request.Token); + if (client == null) { // create new client - var token = ClientManager.AddClient(Context.ConnectionId, request.CharacterId); - Context.Items.Add("Token", token); - Log.Information($"[AUTHORIZE] Client {Context.ConnectionId} authorized with token {token}."); - await Clients.Caller.SendAsync(nameof(Authorize), new Authorize.Response - { - Success = true, - Token = token, - }); - } - else if (client == null && clientCharacterId != null) - { - error = Error.Unauthorized; - if (BanManager.CheckBadRequests(Context, nameof(Authorize))) - { - error = Error.BannedTooManyBadRequests; - } - Log.Warning($"[AUTHORIZE] MISMATCH Client {Context.ConnectionId} failed authorization with error {error}."); - await Clients.Caller.SendAsync(nameof(Authorize), new Authorize.Response - { - Success = false, - Error = error, - }); - Context.Abort(); - } - else if (client != null && clientCharacterId != null) - { - Log.Information($"[AUTHORIZE] Client {Context.ConnectionId} reauthorized with token {client.Token}."); + client = await ClientManager.AddClient(Context.ConnectionId); Context.Items.Add("Token", client.Token); - ClientManager.ChangeClientConnectionId(clientCharacterId.ConnectionId, Context.ConnectionId); - await Clients.Caller.SendAsync(nameof(Authorize), new Authorize.Response - { - Success = true, - Token = client.Token, - }); + Log.Information($"[AUTHORIZE] Client {Context.ConnectionId} authorized with token {client.Token}."); + } else { - await Clients.Caller.SendAsync(nameof(Authorize), new Authorize.Response - { - Success = false, - Error = Error.Unauthorized, - }); + Log.Information($"[AUTHORIZE] Client {Context.ConnectionId} reauthorized with token {client.Token}."); + Context.Items.Add("Token", client.Token); + await ClientManager.ChangeClientConnectionId(client.ConnectionId, Context.ConnectionId); } + var sessions = new List(); + Objects.OwnedSession? ownedSession = null; + + foreach(var share in request.ShareCodes) + { + var session = await SessionManager.GetSession(share.Code); + if (session != null) + { + var members = await ClientManager.GetClientsInSession(session); + if (members.Any(m => m.Client.ClientId == client.ClientId)) + { + if (session.OwnerCharacterId == share.CharacterId && client.ClientId == session.Owner.ClientId && ownedSession == null) + { + ownedSession = new Objects.OwnedSession + { + Session = new Objects.Session { + OwnerCharacterId = session.OwnerCharacterId, + ShareCode = session.ShareCode, + ActiveQuestId = session.SharedQuestId, + ActiveQuestStep = session.SharedQuestStep + }, + SkipPartyCheck = session.SkipPartyCheck, + IsActive = session.IsActive, + AllowJoins = session.AllowJoins, + }; + } + else + { + sessions.Add(new Objects.Session + { + OwnerCharacterId = session.OwnerCharacterId, + ActiveQuestId = session.SharedQuestId, + ActiveQuestStep = session.SharedQuestStep, + ShareCode = share.Code, + }); + } + await Groups.AddToGroupAsync(Context.ConnectionId, session.ShareCode); + } + } + } + await Clients.Caller.SendAsync(nameof(Authorize), new Authorize.Response + { + Success = true, + Error = Error.None, + Token = client.Token, + Sessions = sessions, + OwnedSession = ownedSession, + }); } } } diff --git a/QuestShare.Server/Hubs/Methods/Cancel.cs b/QuestShare.Server/Hubs/Methods/Cancel.cs index 396f195..80d5c5d 100644 --- a/QuestShare.Server/Hubs/Methods/Cancel.cs +++ b/QuestShare.Server/Hubs/Methods/Cancel.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.SignalR; +using QuestShare.Common; using QuestShare.Server.Managers; namespace QuestShare.Server.Hubs { - + public partial class ShareHub : Hub { [HubMethodName(nameof(Cancel))] @@ -14,7 +15,7 @@ namespace QuestShare.Server.Hubs Context.Abort(); return; } - var client = ClientManager.GetClient(Context.ConnectionId); + var client = await ClientManager.GetClient(Context.ConnectionId); if (client == null) { await Clients.Caller.SendAsync(nameof(Cancel), new Cancel.Response @@ -27,8 +28,10 @@ namespace QuestShare.Server.Hubs var error = Error.None; if (request.Token == "") error = Error.InvalidToken; else if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - var share = ShareManager.GetShare(client); - if (share == null) error = Error.Unauthorized; + var session = await SessionManager.GetSession(request.ShareCode); + if (session == null) error = Error.Unauthorized; + if (session != null && session.OwnerCharacterId != request.OwnerCharacterId) error = Error.InvalidSession; + if (session != null && session.Owner.ClientId != client.ClientId) error = Error.Unauthorized; if (error != Error.None) { await Clients.Caller.SendAsync(nameof(Cancel), new Cancel.Response @@ -38,23 +41,22 @@ namespace QuestShare.Server.Hubs }); return; } - ShareManager.RemoveShare(share!.ShareCode); - await Groups.RemoveFromGroupAsync(Context.ConnectionId, share.ShareCode); - // remove all members from the group - var members = await ShareManager.GetShareMembers(share); - if (members != null && members.Count > 0) + await Groups.RemoveFromGroupAsync(Context.ConnectionId, session!.ShareCode.ToString()); + if (session.PartyMembers.Count > 0) { // broadcast to party - await Clients.GroupExcept(share.ShareCode, Context.ConnectionId).SendAsync(nameof(Cancel), new Cancel.CancelBroadcast + await Clients.GroupExcept(session.SessionId.ToString(), Context.ConnectionId).SendAsync(nameof(Cancel.CancelBroadcast), new Cancel.CancelBroadcast { - ShareCode = share.ShareCode, + ShareCode = request.ShareCode }); + var members = await ClientManager.GetClientsInSession(session); foreach (var member in members) { - await Groups.RemoveFromGroupAsync(member.ConnectionId, share.ShareCode); - await ShareManager.RemoveGroupMember(share, member); + await Groups.RemoveFromGroupAsync(member.Client.ConnectionId, session.ShareCode.ToString()); + await ClientManager.RemoveClientSession(member.Client); } } + await SessionManager.RemoveSession(session!.ShareCode); await Clients.Caller.SendAsync(nameof(Cancel), new Cancel.Response { Success = true, diff --git a/QuestShare.Server/Hubs/Methods/GetShareInfo.cs b/QuestShare.Server/Hubs/Methods/GetShareInfo.cs deleted file mode 100644 index f8da867..0000000 --- a/QuestShare.Server/Hubs/Methods/GetShareInfo.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using QuestShare.Server.Managers; - -namespace QuestShare.Server.Hubs -{ - public partial class ShareHub : Hub - { - [HubMethodName(nameof(GetShareInfo))] - public async Task Server_GetShareInfo(GetShareInfo.Request request) - { - if (BanManager.IsBanned(Context)) - { - Context.Abort(); - return; - } - var error = Error.None; - if (request.ShareCode == "") error = Error.InvalidShareCode; - if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - var client = ClientManager.GetClient(Context.ConnectionId, request.Token); - var share = ShareManager.GetShare(request.ShareCode); - if (client == null) error = Error.Unauthorized; - if (share == null) error = Error.ShareNotFound; - if (error != Error.None) - { - await Clients.Caller.SendAsync(nameof(GetShareInfo), new GetShareInfo.Response - { - Success = false, - Error = error, - Members = [], - }); - return; - } - if (share == null) - { - await Clients.Caller.SendAsync(nameof(GetShareInfo), new GetShareInfo.Response - { - Success = false, - Error = Error.ShareNotFound, - SharedQuestId = share!.SharedQuestId, - SharedQuestStep = share.SharedQuestStep, - Members = [], - }); - return; - } - var members = await ShareManager.GetShareMembers(share); - await Clients.Caller.SendAsync(nameof(GetShareInfo), new GetShareInfo.Response - { - Success = true, - SharedQuestId = share.SharedQuestId, - SharedQuestStep = share.SharedQuestStep, - Members = members.Select(m => m.CharacterId).ToList(), - }); - } - } -} diff --git a/QuestShare.Server/Hubs/Methods/GroupJoin.cs b/QuestShare.Server/Hubs/Methods/GroupJoin.cs index a46a1cd..07cbc27 100644 --- a/QuestShare.Server/Hubs/Methods/GroupJoin.cs +++ b/QuestShare.Server/Hubs/Methods/GroupJoin.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.SignalR; using Newtonsoft.Json; +using QuestShare.Common; using QuestShare.Server.Managers; using QuestShare.Server.Models; @@ -11,11 +12,9 @@ namespace QuestShare.Server.Hubs public async Task Server_GroupJoin(GroupJoin.Request request) { var error = Error.None; - if (request.ShareCode == "") error = Error.InvalidShareCode; if (request.Token == "") error = Error.InvalidToken; if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - if (request.CharacterId == 0) error = Error.InvalidCharacterId; - var client = ClientManager.GetClient(Context.ConnectionId, request.Token); + var client = await ClientManager.GetClient(Context.ConnectionId, request.Token); if (client == null) error = Error.Unauthorized; if (error != Error.None) { @@ -26,80 +25,38 @@ namespace QuestShare.Server.Hubs }); return; } - if (request.RequestPartyQuickJoin) + var session = await SessionManager.GetSession(request.SessionInfo.Code); + if (session == null) { - if (ShareManager.HasBroadcastParty(request.QuickJoinCharacterId)) - { - var host = ClientManager.GetClient(request.QuickJoinCharacterId); - if (host == null) - { - await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response - { - Success = false, - Error = Error.InvalidHostClient, - }); - return; - } - var share = ShareManager.GetShare(host); - - if (share == null) - { - await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response - { - Success = false, - Error = Error.ShareNotFound, - }); - return; - } - var members = await ShareManager.GetShareMembers(share); - await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response - { - Success = true, - ShareCode = share.ShareCode, - Members = members.Select(m => m.CharacterId).ToList(), - SharedQuestId = share.SharedQuestId, - SharedQuestStep = share.SharedQuestStep, - }); - await Clients.GroupExcept(share.ShareCode, Context.ConnectionId).SendAsync(nameof(GroupNotify), new GroupNotify.GroupNotifyBroadcast - { - CharacterId = request.CharacterId, - NotifyType = NotifyType.JoinViaParty, - ShareCode = share.ShareCode, - }); - return; - } - } - else - { - var share = ShareManager.GetShare(request.ShareCode); - if (share == null) - { - await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response - { - Success = false, - Error = Error.ShareNotFound, - }); - return; - } - - await Groups.AddToGroupAsync(Context.ConnectionId, share.ShareCode); - await ShareManager.AddGroupMember(share, client!); - var members = await ShareManager.GetShareMembers(share); + Log.Warning($"[GroupJoin] Session {request.SessionInfo.Code} not found."); await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response { - Success = true, - ShareCode = share.ShareCode, - Members = members.Select(m => m.CharacterId).ToList(), - SharedQuestId = share.SharedQuestId, - SharedQuestStep = share.SharedQuestStep, - }); - await Clients.GroupExcept(share.ShareCode, Context.ConnectionId).SendAsync(nameof(GroupNotify.GroupNotifyBroadcast), new GroupNotify.GroupNotifyBroadcast - { - CharacterId = request.CharacterId, - NotifyType = NotifyType.Join, - ShareCode = share.ShareCode, + Success = false, + Error = Error.InvalidParty, }); + return; } + await ClientManager.AddClientSession(client!.ClientId, session.SessionId); + await Groups.AddToGroupAsync(Context.ConnectionId, session.ShareCode.ToString()); + await ClientManager.AddKnownShareCode(client!, session.ShareCode); + await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response + { + Success = true, + Session = new Objects.Session + { + OwnerCharacterId = session.OwnerCharacterId, + ShareCode = session.ShareCode, + }, + }); + await Clients.GroupExcept(Context.ConnectionId, session.ShareCode.ToString()).SendAsync(nameof(GroupJoin.GroupJoinBroadcast), new GroupJoin.GroupJoinBroadcast + { + Session = new Objects.Session + { + OwnerCharacterId = session.OwnerCharacterId, + ShareCode = session.ShareCode, + }, + }); + Log.Debug($"[GroupJoin] {client} joined session {request.SessionInfo.Code}"); } } } diff --git a/QuestShare.Server/Hubs/Methods/GroupLeave.cs b/QuestShare.Server/Hubs/Methods/GroupLeave.cs index 6cb09f2..fce22f4 100644 --- a/QuestShare.Server/Hubs/Methods/GroupLeave.cs +++ b/QuestShare.Server/Hubs/Methods/GroupLeave.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.SignalR; +using QuestShare.Common; using QuestShare.Server.Managers; namespace QuestShare.Server.Hubs @@ -18,10 +19,8 @@ namespace QuestShare.Server.Hubs var error = Error.None; if (request.Token == "") error = Error.InvalidToken; else if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - var client = ClientManager.GetClient(Context.ConnectionId); + var client = await ClientManager.GetClient(Context.ConnectionId); if (client == null) error = Error.Unauthorized; - var share = ShareManager.GetShare(request.ShareCode); - if (share == null) error = Error.ShareNotFound; if (error != Error.None) { await Clients.Caller.SendAsync(nameof(GroupLeave), new GroupLeave.Response @@ -31,17 +30,46 @@ namespace QuestShare.Server.Hubs }); return; } - await ShareManager.RemoveGroupMember(share!, client!); + var session = await SessionManager.GetSession(request.Session.ShareCode); + if (session == null) + { + await Clients.Caller.SendAsync(nameof(GroupLeave), new GroupLeave.Response + { + Success = false, + Error = Error.InvalidSession, + }); + return; + } + var smember = await SessionManager.GetMembersInSession(session); + if (!smember.Any(s => s.Client.ClientId == client!.ClientId)) + { + await Clients.Caller.SendAsync(nameof(GroupLeave), new GroupLeave.Response + { + Success = false, + Error = Error.InvalidMember, + }); + return; + } + await SessionManager.RemoveMemberFromSession(session, client!); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, session.ShareCode.ToString()); + await ClientManager.RemoveKnownShareCode(client!, session.ShareCode); await Clients.Caller.SendAsync(nameof(GroupLeave), new GroupLeave.Response { Success = true, + Session = new Objects.Session + { + ShareCode = session.ShareCode, + OwnerCharacterId = session.OwnerCharacterId, + } }); // broadcast to party - await Clients.GroupExcept(share!.ShareCode, Context.ConnectionId).SendAsync(nameof(GroupNotify), new GroupNotify.GroupNotifyBroadcast + await Clients.GroupExcept(session.SessionId.ToString(), Context.ConnectionId).SendAsync(nameof(GroupLeave.GroupLeaveBroadcast), new GroupLeave.GroupLeaveBroadcast { - ShareCode = share.ShareCode, - CharacterId = client!.CharacterId, - NotifyType = NotifyType.Leave, + Session = new Objects.Session + { + ShareCode = session.ShareCode, + OwnerCharacterId = session.OwnerCharacterId, + } }); } } diff --git a/QuestShare.Server/Hubs/Methods/PartyCheck.cs b/QuestShare.Server/Hubs/Methods/PartyCheck.cs deleted file mode 100644 index 0afa49e..0000000 --- a/QuestShare.Server/Hubs/Methods/PartyCheck.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using QuestShare.Common.API.Party; -using QuestShare.Server.Managers; - -namespace QuestShare.Server.Hubs -{ - public partial class ShareHub : Hub - { - [HubMethodName(nameof(PartyCheck))] - public async Task Server_PartyCheck(PartyCheck.Request request) - { - if (BanManager.IsBanned(Context)) - { - Context.Abort(); - return; - } - var error = Error.None; - if (request.CharacterId == 0) error = Error.InvalidCharacterId; - if (request.Token == "") error = Error.InvalidToken; - if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - var client = ClientManager.GetClient(Context.ConnectionId, request.Token); - if (client == null) - { - error = Error.Unauthorized; - if (BanManager.CheckBadRequests(Context, nameof(PartyCheck))) - { - error = Error.BannedTooManyBadRequests; - } - } - if (error != Error.None) - { - await Clients.Caller.SendAsync(nameof(PartyCheck), new PartyCheck.Response - { - Success = false, - Error = error, - }); - return; - } - - } - } -} diff --git a/QuestShare.Server/Hubs/Methods/Register.cs b/QuestShare.Server/Hubs/Methods/Register.cs index b3db09b..5c4a639 100644 --- a/QuestShare.Server/Hubs/Methods/Register.cs +++ b/QuestShare.Server/Hubs/Methods/Register.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.SignalR; using Newtonsoft.Json; -using QuestShare.Common.API.Share; +using QuestShare.Common; using QuestShare.Server.Hubs; using QuestShare.Server.Managers; using QuestShare.Server.Models; @@ -21,63 +21,23 @@ namespace QuestShare.Server.Hubs return; } var error = Error.None; - if (request.CharacterId == 0) error = Error.InvalidCharacterId; - if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - var client = ClientManager.GetClient(Context.ConnectionId); - if (client == null || client.Token != request.Token) + if (request.Version != Constants.Version) error = Error.InvalidVersion; + var client = await ClientManager.GetClient(Context.ConnectionId, request.Token); + if (client == null) error = Error.InvalidToken; + if (client != null) { - error = Error.Unauthorized; - if (BanManager.CheckBadRequests(Context, nameof(Register))) - { - error = Error.BannedTooManyBadRequests; - } + var existingSession = await SessionManager.GetSession(client); + if (existingSession != null) error = Error.AlreadyRegistered; } - if (error != Error.None) { - Log.Warning($"[REGISTER] Client {Context.ConnectionId} failed registration with error {error}."); - await Clients.Caller.SendAsync(nameof(Register), new Register.Response - { - Success = false, - Error = error, - ShareCode = "", - }); + Log.Warning($"[REGISTER] Client {Context.ConnectionId} failed to register: {error}"); + await Clients.Caller.SendAsync(nameof(Register), new Register.Response { Error = error, Success = false, ShareCode = "" }); return; } - if (request.BroadcastParty && request.PartyMembers.Count > 0) - { - foreach (var partyMember in request.PartyMembers) - { - ShareManager.AddBroadcastPartyMember(request.CharacterId, partyMember); - } - } - if (ShareManager.GetShare(client!) != null) - { - Log.Warning($"[REGISTER] Client {Context.ConnectionId} already registered."); - await Clients.Caller.SendAsync(nameof(Register), new Register.Response - { - Success = false, - Error = Error.AlreadyRegistered, - ShareCode = "" - }); - return; - } - var share = new Share - { - ShareCode = ShareManager.GenerateShareCode(), - ShareHost = client!, - BroadcastParty = request.BroadcastParty, - SharedQuestId = request.SharedQuestId, - SharedQuestStep = request.SharedQuestStep, - }; - var newShare = await ShareManager.AddShare(share); - Log.Information($"[REGISTER] Client {Context.ConnectionId} registered share {share.ShareCode}."); - await Clients.Caller.SendAsync(nameof(Register), new Register.Response - { - Success = true, - Error = Error.None, - ShareCode = share.ShareCode, - }); + var session = await SessionManager.GenerateSession(Context.ConnectionId, client!); + await Clients.Caller.SendAsync(nameof(Register), new Register.Response { Error = Error.None, Success = true, ShareCode = session }); + Log.Information($"[REGISTER] Client {Context.ConnectionId} generated share code {session}."); } } } diff --git a/QuestShare.Server/Hubs/Methods/Resume.cs b/QuestShare.Server/Hubs/Methods/Resume.cs deleted file mode 100644 index 72c5c80..0000000 --- a/QuestShare.Server/Hubs/Methods/Resume.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using QuestShare.Server.Managers; - -namespace QuestShare.Server.Hubs -{ - public partial class ShareHub : Hub - { - [HubMethodName(nameof(Resume))] - public async Task Server_Resume(Resume.Request request) - { - if (BanManager.IsBanned(Context)) - { - Log.Error($"[RESUME] Client {Context.ConnectionId} is banned."); - Context.Abort(); - return; - } - var error = Error.None; - var client = ClientManager.GetClient(Context.ConnectionId, request.Token); - if (client == null) - { - error = Error.Unauthorized; - if (BanManager.CheckBadRequests(Context, nameof(Resume))) - { - error = Error.BannedTooManyBadRequests; - } - } - else if (request.Token == "") error = Error.InvalidToken; - else if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - if (error != Error.None) - { - Log.Warning($"[RESUME] Client {Context.ConnectionId} failed resume with error {error}."); - await Clients.Caller.SendAsync(nameof(Resume), new Resume.Response - { - Success = false, - Error = error, - }); - return; - } - var share = ShareManager.GetShare(client!); - if (share == null) - { - if (request.ShareCode == "") - { - Log.Warning($"[RESUME] Client {Context.ConnectionId} failed resume with error {error}."); - await Clients.Caller.SendAsync(nameof(Resume), new Resume.Response - { - Success = false, - Error = Error.ShareNotFound, - }); - return; - } - share = ShareManager.GetShare(request.ShareCode); - if (share == null) - { - Log.Warning($"[RESUME] Client {Context.ConnectionId} failed resume with error {error}."); - await Clients.Caller.SendAsync(nameof(Resume), new Resume.Response - { - Success = false, - Error = Error.ShareNotFound, - }); - return; - } - await Groups.AddToGroupAsync(Context.ConnectionId, share.ShareCode); - var members = await ShareManager.GetShareMembers(share); - await Clients.Caller.SendAsync(nameof(Resume), new Resume.Response - { - Success = true, - SharedQuestId = share.SharedQuestId, - SharedQuestStep = share.SharedQuestStep, - Members = members.Select(m => m.CharacterId).ToList(), - IsGroup = true, - ShareCode = share.ShareCode - }); - await Clients.GroupExcept(share.ShareCode, Context.ConnectionId).SendAsync(nameof(Resume), new GroupNotify.GroupNotifyBroadcast - { - CharacterId = client!.CharacterId, - NotifyType = NotifyType.Rejoin, - ShareCode = share.ShareCode, - }); - Log.Information($"[RESUME] Client {Context.ConnectionId} resumed share {share.ShareCode}. (Member)"); - } - else - { - Log.Information($"[RESUME] Client {Context.ConnectionId} resumed share {share.ShareCode}. (Host)"); - List members = []; - if (share.BroadcastParty) - { - if (share.ShareHost.CharacterId == client!.CharacterId && ShareManager.HasBroadcastParty(share.ShareHost.CharacterId)) - { - members = request.Members ?? []; - ShareManager.SetBroadcastPartyMembers(client!.CharacterId, members); - } else - { - members = ShareManager.GetBroadcastPartyMembers(share.ShareHost.CharacterId); - } - } - await Clients.Caller.SendAsync(nameof(Resume), new Resume.Response - { - Success = true, - SharedQuestId = share.SharedQuestId, - SharedQuestStep = share.SharedQuestStep, - Members = members ?? [], - IsHost = true, - ShareCode = share.ShareCode - }); - - } - } - } -} diff --git a/QuestShare.Server/Hubs/Methods/Update.cs b/QuestShare.Server/Hubs/Methods/Update.cs index ec25ebd..3080a29 100644 --- a/QuestShare.Server/Hubs/Methods/Update.cs +++ b/QuestShare.Server/Hubs/Methods/Update.cs @@ -1,8 +1,7 @@ using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.SignalR; using Newtonsoft.Json; -using QuestShare.Common.API; -using QuestShare.Common.API.Share; +using QuestShare.Common; using QuestShare.Server.Managers; using QuestShare.Server.Models; @@ -20,22 +19,13 @@ namespace QuestShare.Server.Hubs Context.Abort(); return; } - if (ClientManager.GetClient(Context.ConnectionId) == null) - { - await Clients.Caller.SendAsync(nameof(Update), new Update.Response - { - Success = false, - Error = Error.Unauthorized, - }); - return; - } - var client = ClientManager.GetClient(Context.ConnectionId); + var client = await ClientManager.GetClient(Context.ConnectionId); var error = Error.None; - if (request.Token == "") error = Error.InvalidToken; + if (request.Token == "" || request.Token != client!.Token) error = Error.InvalidToken; if (client == null) error = Error.Unauthorized; else if (request.Version != Common.Constants.Version) error = Error.InvalidVersion; - var share = ShareManager.GetShare(client!); - if (share == null) error = Error.ShareNotFound; + var session = await SessionManager.GetSession(request.Session.ShareCode); + if (session == null) error = Error.InvalidSession; if (error != Error.None) { Log.Warning($"[UPDATE] Client {Context.ConnectionId} failed update with error {error}."); @@ -46,25 +36,17 @@ namespace QuestShare.Server.Hubs }); return; } - if (request.IsPartyChange) - { - ShareManager.SetBroadcastPartyMembers(share!.ShareHost.CharacterId, request.PartyMembers); - } else - { - await ShareManager.UpdateActiveQuest(share!.ShareCode, request.SharedQuestId, request.SharedQuestStep); - // broadcast to party - Log.Debug($"[UPDATE] Broadcasting quests to party members for share {share.ShareCode}, excluding {Context.ConnectionId}"); - await Clients.GroupExcept(share.ShareCode, Context.ConnectionId).SendAsync(nameof(Update.UpdateBroadcast), new Update.UpdateBroadcast - { - ShareCode = share.ShareCode, - SharedQuestId = request.SharedQuestId, - SharedQuestStep = request.SharedQuestStep, - }); - } - Log.Debug($"[UPDATE] Client {Context.ConnectionId} updated quest for share {share.ShareCode}."); + await SessionManager.UpdateActiveQuest(request.Session.ShareCode, request.Session.ActiveQuestId, request.Session.ActiveQuestStep); + await SessionManager.SetPartyMembers(session!.ShareCode, [.. request.PartyMembers]); await Clients.Caller.SendAsync(nameof(Update), new Update.Response { Success = true, + Error = Error.None, + }); + // Broadcast to party + await Clients.GroupExcept(session.ShareCode.ToString(), Context.ConnectionId).SendAsync(nameof(Update.UpdateBroadcast), new Update.UpdateBroadcast + { + Session = request.Session.Session, }); } } diff --git a/QuestShare.Server/Hubs/ShareHub.cs b/QuestShare.Server/Hubs/ShareHub.cs index 39ba2e2..ddb8e58 100644 --- a/QuestShare.Server/Hubs/ShareHub.cs +++ b/QuestShare.Server/Hubs/ShareHub.cs @@ -16,7 +16,7 @@ namespace QuestShare.Server.Hubs Log.Warning($"Client {Context.ConnectionId} is banned."); Context.Abort(); } - Clients.Caller.SendAsync(nameof(Authorize.AuthBroadcast), new Authorize.AuthBroadcast + Clients.Caller.SendAsync(nameof(AuthRequest), new AuthRequest.Response { }); return base.OnConnectedAsync(); @@ -28,12 +28,6 @@ namespace QuestShare.Server.Hubs { Log.Error(exception, "Exception occurred on disconnect."); } - if (Context.Items["Token"] != null) - { - using var db = new QuestShareContext(); - var client = db.Clients.FirstOrDefault(c => c.Token == Context.Items["Token"]!.ToString()); - if (client != null) db.Clients.Remove(client); - } return base.OnDisconnectedAsync(exception); } } diff --git a/QuestShare.Server/Managers/CleanupManager.cs b/QuestShare.Server/Managers/CleanupManager.cs index 61ae05f..ba69413 100644 --- a/QuestShare.Server/Managers/CleanupManager.cs +++ b/QuestShare.Server/Managers/CleanupManager.cs @@ -22,9 +22,9 @@ namespace QuestShare.Server.Managers private static void CleanupTask(object? timerState) { using var shareContext = new QuestShareContext(); - var toDelete = shareContext.Shares.Where(s => s.LastUpdated < DateTime.Now.AddMinutes(-CleanupTimeMinutes)).ToList(); + var toDelete = shareContext.Sessions.Where(s => s.LastUpdated < DateTime.Now.AddMinutes(-CleanupTimeMinutes)).ToList(); Console.WriteLine($"Deleting {toDelete.Count} old shares."); - shareContext.Shares.RemoveRange(toDelete); + shareContext.Sessions.RemoveRange(toDelete); shareContext.SaveChanges(); } } diff --git a/QuestShare.Server/Managers/ClientManager.cs b/QuestShare.Server/Managers/ClientManager.cs index 0bbf74c..40833e8 100644 --- a/QuestShare.Server/Managers/ClientManager.cs +++ b/QuestShare.Server/Managers/ClientManager.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using QuestShare.Server.Models; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -7,20 +8,19 @@ namespace QuestShare.Server.Managers { public static class ClientManager { - public static string AddClient(string connectionId, ulong characterId) + public static async Task AddClient(string connectionId) { using var context = new QuestShareContext(); var token = GenerateToken(); - context.Clients.Add(new Client + var c = context.Clients.Add(new Client { ConnectionId = connectionId, - CharacterId = characterId, Token = token }); - context.SaveChanges(); - return token; + await context.SaveChangesAsync(); + return c.Entity; } - public static void RemoveClient(string connectionId) + public static async Task RemoveClient(string connectionId) { using var context = new QuestShareContext(); var client = context.Clients.FirstOrDefault(c => c.ConnectionId == connectionId); @@ -28,12 +28,12 @@ namespace QuestShare.Server.Managers { context.Clients.Remove(client); } - context.SaveChanges(); + await context.SaveChangesAsync(); } - public static Client? GetClient(string connectionId, string token = "") + public static async Task GetClient(string connectionId, string token = "") { using var context = new QuestShareContext(); - var client = context.Clients.FirstOrDefault(c => c.ConnectionId == connectionId || c.Token == token); + var client = await context.Clients.Where(c => c.ConnectionId == connectionId || c.Token == token).FirstOrDefaultAsync(); if (client != null) { if (token != "" && client.Token != token) @@ -44,7 +44,7 @@ namespace QuestShare.Server.Managers if (client.ConnectionId != connectionId) { Log.Information($"[ClientManager] Changing connection ID from {connectionId} to {client.ConnectionId} for token {token}"); - ChangeClientConnectionId(client.ConnectionId, connectionId); + await ChangeClientConnectionId(client.ConnectionId, connectionId); } return client; } else @@ -53,32 +53,81 @@ namespace QuestShare.Server.Managers return null; } } - public static Client? GetClient(ulong characterId) + + public static async Task> GetClientsInSession(Session session) { using var context = new QuestShareContext(); - var client = context.Clients.FirstOrDefault(c => c.CharacterId == characterId); - if (client != null) - { - return client; - } - else - { - return null; - } + var clients = await context.SessionMembers.Where(c => c.Session == session).Include(s => s.Session).Include(sm => sm.Client).ToListAsync(); + return clients; } - public static void ChangeClientConnectionId(string oldConnectionId, string newConnectionId) + public static async Task RemoveClientSession(Client client) { using var context = new QuestShareContext(); - var client = context.Clients.FirstOrDefault(c => c.ConnectionId == oldConnectionId); + var cs = await context.SessionMembers.Where(cs => cs.Client.ClientId == client.ClientId).FirstOrDefaultAsync(); + if (cs == null) + { + Log.Warning($"[ClientManager] Unable to find client session for {client.ClientId}"); + return; + } + context.SessionMembers.Remove(cs); + Log.Debug($"[ClientManager] Removing client {client.ClientId} from session"); + await context.SaveChangesAsync(); + } + + public static async Task AddClientSession(Guid ClientId, Guid SessionId) + { + using var context = new QuestShareContext(); + var client = await context.Clients.Where(c => c.ClientId == ClientId).FirstOrDefaultAsync(); + var session = await context.Sessions.Where(s => s.SessionId == SessionId).FirstOrDefaultAsync(); + if (client == null || session == null) + { + Log.Warning($"[ClientManager] Unable to find client {ClientId} or session {SessionId}"); + return; + } + await context.SessionMembers.AddAsync(new SessionMember + { + Client = client, + Session = session + }); + Log.Debug($"[ClientManager] Adding client {client.ClientId} to session {session.SessionId}"); + await context.SaveChangesAsync(); + } + + public static async Task ChangeClientConnectionId(string oldConnectionId, string newConnectionId) + { + using var context = new QuestShareContext(); + var client = await context.Clients.Where(c => c.ConnectionId == oldConnectionId).FirstOrDefaultAsync(); if (client != null) { client.ConnectionId = newConnectionId; - context.SaveChanges(); + await context.SaveChangesAsync(); } } - private static string GenerateToken() + public static async Task AddKnownShareCode(Client client, string shareCode) + { + using var context = new QuestShareContext(); + var c = await context.Clients.Where(c => c.ClientId == client.ClientId).FirstOrDefaultAsync(); + if (c != null) + { + c.KnownShareCodes.Add(shareCode); + await context.SaveChangesAsync(); + } + } + + public static async Task RemoveKnownShareCode(Client client, string shareCode) + { + using var context = new QuestShareContext(); + var c = await context.Clients.Where(c => c.ClientId == client.ClientId).FirstOrDefaultAsync(); + if (c != null) + { + c.KnownShareCodes.Remove(shareCode); + await context.SaveChangesAsync(); + } + } + + public static string GenerateToken() { var random = RandomNumberGenerator.GetHexString(32, true); return random; diff --git a/QuestShare.Server/Managers/PartyManager.cs b/QuestShare.Server/Managers/PartyManager.cs deleted file mode 100644 index 7af51ed..0000000 --- a/QuestShare.Server/Managers/PartyManager.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace QuestShare.Server.Managers -{ - public class PartyManager - { - private static List Parties = []; - - public static void CreateParty(string host, List members) - { - members.Add(host); - Parties.Add(new Party - { - Host = host, - Members = members - }); - } - public static void JoinParty(string host, string member) - { - var party = Parties.Find(p => p.Host == host); - party?.Members.Add(member); - } - public static void LeaveParty(string host, string member) - { - var party = Parties.Find(p => p.Host == host); - party?.Members.Remove(member); - } - public static void DeleteParty(string host) - { - Parties.RemoveAll(p => p.Host == host); - } - public static List GetPartyMembers(string host) - { - var party = Parties.Find(p => p.Host == host); - return party?.Members ?? new List(); - } - public static bool IsInParty(string member, string shareCode) - { - var share = Parties.Find(p => p.HostShareCode == shareCode); - return share?.Members.Contains(member) ?? false; - } - } - - internal sealed record Party - { - public string Host { get; set; } = null!; - public string HostShareCode { get; set; } = null!; - public List Members { get; set; } = null!; - } -} diff --git a/QuestShare.Server/Managers/SessionManager.cs b/QuestShare.Server/Managers/SessionManager.cs new file mode 100644 index 0000000..caba8c5 --- /dev/null +++ b/QuestShare.Server/Managers/SessionManager.cs @@ -0,0 +1,115 @@ +using Microsoft.EntityFrameworkCore; +using QuestShare.Common; +using QuestShare.Server.Models; + +namespace QuestShare.Server.Managers +{ + public class SessionManager + { + public static async Task GetSession(Client client) + { + using var context = new QuestShareContext(); + return await context.Sessions.Where(s => s.Owner.ClientId == client.ClientId).FirstOrDefaultAsync(); + } + public static async Task GetSession(string ShareCode) + { + using var context = new QuestShareContext(); + var session = await context.Sessions.Where(s => s.ShareCode == ShareCode).FirstOrDefaultAsync(); + return session; + } + + public static async Task GenerateSession(string connectionId, Client client) + { + using var context = new QuestShareContext(); + var token = ClientManager.GenerateToken(); + var c = await context.Clients.Where(c => c.ClientId == client.ClientId).FirstOrDefaultAsync(); + var s = context.Sessions.Add(new Session + { + ShareCode = token[..8].ToUpperInvariant(), + ReservedConnectionId = connectionId, + Owner = c!, + }); + await context.SaveChangesAsync(); + return token[..8].ToUpperInvariant(); + } + + public static async Task CreateSession(Client owner, string connectionId, Objects.OwnedSession session) + { + using var context = new QuestShareContext(); + var s = await context.Sessions.Where(s => s.ShareCode == session.Session.ShareCode && s.ReservedConnectionId == connectionId).FirstOrDefaultAsync(); + if (s == null) + { + Log.Error($"[SessionManager] Failed to create session for {session.Session.OwnerCharacterId} with share code {session.Session.ShareCode}"); + return null; + } + s.OwnerCharacterId = session.Session.OwnerCharacterId; + s.IsActive = session.IsActive; + s.AllowJoins = session.AllowJoins; + s.SkipPartyCheck = session.SkipPartyCheck; + await context.SaveChangesAsync(); + return s; + } + + public static async Task RemoveSession(string shareCode) + { + using var context = new QuestShareContext(); + var session = await context.Sessions.Where(s => s.ShareCode == shareCode).FirstOrDefaultAsync(); + if (session != null) + { + context.Sessions.Remove(session); + await context.SaveChangesAsync(); + } + } + + public static async Task SetPartyMembers(string shareCode, List partyMembers) + { + using var context = new QuestShareContext(); + var s = await context.Sessions.Where(s => s.ShareCode == shareCode).FirstOrDefaultAsync(); + if (s != null) + { + s.PartyMembers = partyMembers; + await context.SaveChangesAsync(); + } + } + + public static async Task AddMemberToSession(Session session, string member) + { + using var context = new QuestShareContext(); + var s = await context.Sessions.Where(s => s.SessionId == session.SessionId).FirstOrDefaultAsync(); + if (s != null) + { + s.AddMember(member); + await context.SaveChangesAsync(); + } + } + + public static async Task RemoveMemberFromSession(Session session, Client client) + { + using var context = new QuestShareContext(); + await context.SessionMembers.Where(s => s.Session.SessionId == session.SessionId && s.Client.ClientId == client.ClientId).ExecuteDeleteAsync(); + } + + public static async Task> GetMembersInSession(Session session) + { + using var context = new QuestShareContext(); + var s = await context.SessionMembers.Where(s => s.Session.SessionId == session.SessionId).Include("Sessions").Include("Clients").ToListAsync(); + return s; + } + + public static async Task UpdateActiveQuest(string shareCode, int questId, byte questStep) + { + using var context = new QuestShareContext(); + var session = await context.Sessions.Where(s => s.ShareCode == shareCode).FirstOrDefaultAsync(); + if (session == null) + { + // log error + Console.Error.WriteLine($"Failed to update quests for session {shareCode}"); + return; + } + session.SharedQuestStep = questStep; + session.SharedQuestId = questId; + var records = await context.SaveChangesAsync(); + Log.Debug($"[UPDATE] Updated {records} quests for session {shareCode}"); + } + } +} diff --git a/QuestShare.Server/Managers/ShareManager.cs b/QuestShare.Server/Managers/ShareManager.cs deleted file mode 100644 index 46f68a3..0000000 --- a/QuestShare.Server/Managers/ShareManager.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using QuestShare.Server.Models; - -namespace QuestShare.Server.Managers -{ - public class ShareManager - { - - // party members are only stored in memory, so they will be lost when the server restarts - private static readonly Dictionary> PartyMembers = []; - public static Share? GetShare(string shareCode) - { - using var context = new QuestShareContext(); - return context.Shares.Where(s => s.ShareCode == shareCode).FirstOrDefault(); - } - public static Share? GetShare(Client host) - { - using var context = new QuestShareContext(); - return context.Shares.Where(s => s.ShareHost == host).FirstOrDefault(); - } - - public static async Task AddShare(Share share) - { - using var context = new QuestShareContext(); - context.Entry(share.ShareHost).State = EntityState.Unchanged; - var s = context.Shares.Add(share); - await context.SaveChangesAsync(); - return s.Entity; - } - - public static void RemoveShare(string shareCode) - { - using var context = new QuestShareContext(); - var share = context.Shares.Where(s => s.ShareCode == shareCode).FirstOrDefault(); - if (share != null) - { - context.Shares.Remove(share); - // remove all share group members - var shareMembers = context.Clients.Where(c => c.ConnectedShareCode == shareCode); - foreach (var member in shareMembers) - { - member.ConnectedShareCode = ""; - } - context.SaveChanges(); - } - } - - public static async Task UpdateActiveQuest(string shareCode, uint questId, byte questStep) - { - using var context = new QuestShareContext(); - var share = await context.Shares.Where(s => s.ShareCode == shareCode).FirstOrDefaultAsync(); - if (share == null) - { - // log error - Console.Error.WriteLine($"Failed to update quests for share {shareCode}"); - return; - } - share.SharedQuestStep = questStep; - share.SharedQuestId = questId; - var records = await context.SaveChangesAsync(); - Log.Debug($"[UPDATE] Updated {records} quests for share {shareCode}"); - } - - public static async Task AddGroupMember(Share share, Client client) - { - using var context = new QuestShareContext(); - var clientDb = await context.Clients.Where(c => c.CharacterId == client.CharacterId).FirstAsync(); - clientDb.ConnectedShareCode = share.ShareCode; - await context.SaveChangesAsync(); - } - - public static async Task RemoveGroupMember(Share share, Client client) - { - using var context = new QuestShareContext(); - var clientDb = await context.Clients.Where(c => c.CharacterId == client.CharacterId).FirstOrDefaultAsync(); - if (clientDb != null) - { - clientDb.ConnectedShareCode = ""; - await context.SaveChangesAsync(); - } - } - - public static async Task> GetShareMembers(Share share) - { - using var context = new QuestShareContext(); - var members = await context.Clients.Where(c => c.ConnectedShareCode == share.ShareCode).ToListAsync(); - return members; - } - - public static void AddBroadcastPartyMember(ulong hostCharacterId, ulong characterId) - { - if (!PartyMembers.TryGetValue(hostCharacterId, out var value)) - { - value = ([]); - PartyMembers[hostCharacterId] = value; - } - - value.Add(characterId); - } - - public static void RemoveBroadcastPartyMember(ulong hostCharacterId, ulong characterId) - { - if (PartyMembers.TryGetValue(hostCharacterId, out var value)) - { - value.Remove(characterId); - } - } - - public static void SetBroadcastPartyMembers(ulong hostCharacterId, List characterIds) - { - PartyMembers[hostCharacterId] = characterIds; - } - - public static void DisbandBroadcastParty(ulong hostCharacterId) - { - PartyMembers.Remove(hostCharacterId); - } - - public static bool HasBroadcastParty(ulong hostCharacterId) - { - return PartyMembers.ContainsKey(hostCharacterId); - } - - public static List GetBroadcastPartyMembers(ulong hostCharacterId) - { - return PartyMembers.GetValueOrDefault(hostCharacterId, []); - } - - public static string GenerateShareCode() - { - return Guid.NewGuid().ToString().Substring(0, 8).ToUpper(); - } - } -} diff --git a/QuestShare.Server/Migrations/QuestShareContextModelSnapshot.cs b/QuestShare.Server/Migrations/QuestShareContextModelSnapshot.cs index 755d3f8..11db3c2 100644 --- a/QuestShare.Server/Migrations/QuestShareContextModelSnapshot.cs +++ b/QuestShare.Server/Migrations/QuestShareContextModelSnapshot.cs @@ -60,13 +60,6 @@ namespace QuestShare.Server.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CharacterId") - .HasColumnType("numeric(20,0)"); - - b.Property("ConnectedShareCode") - .IsRequired() - .HasColumnType("text"); - b.Property("ConnectionId") .IsRequired() .HasColumnType("text"); @@ -76,31 +69,102 @@ namespace QuestShare.Server.Migrations .HasColumnType("timestamp with time zone") .HasDefaultValueSql("current_timestamp"); + b.Property("KnownShareCodes") + .IsRequired() + .HasColumnType("text"); + b.Property("LastUpdated") .ValueGeneratedOnAddOrUpdate() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("current_timestamp"); + b.Property("SessionMemberClientSessionId") + .HasColumnType("uuid"); + b.Property("Token") .IsRequired() .HasColumnType("text"); b.HasKey("ClientId"); + b.HasIndex("SessionMemberClientSessionId"); + b.ToTable("Clients", (string)null); }); - modelBuilder.Entity("QuestShare.Server.Models.Share", b => + modelBuilder.Entity("QuestShare.Server.Models.Session", b => { - b.Property("ShareId") + b.Property("SessionId") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("BroadcastParty") + b.Property("AllowJoins") .HasColumnType("boolean"); b.Property("Created") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastUpdated") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp"); + + b.Property("OwnerCharacterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerClientId") + .HasColumnType("uuid"); + + b.Property("PartyMembers") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReservedConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SessionMemberClientSessionId") + .HasColumnType("uuid"); + + b.Property("ShareCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SharedQuestId") + .HasColumnType("integer"); + + b.Property("SharedQuestStep") + .HasColumnType("smallint"); + + b.Property("SkipPartyCheck") + .HasColumnType("boolean"); + + b.HasKey("SessionId"); + + b.HasIndex("OwnerClientId"); + + b.HasIndex("SessionMemberClientSessionId"); + + b.ToTable("Sessions", (string)null); + }); + + modelBuilder.Entity("QuestShare.Server.Models.SessionMember", b => + { + b.Property("ClientSessionId") .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("Created") + .ValueGeneratedOnAddOrUpdate() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("current_timestamp"); @@ -109,35 +173,57 @@ namespace QuestShare.Server.Migrations .HasColumnType("timestamp with time zone") .HasDefaultValueSql("current_timestamp"); - b.Property("ShareCode") - .IsRequired() - .HasColumnType("text"); - - b.Property("ShareHostClientId") + b.Property("SessionId") .HasColumnType("uuid"); - b.Property("SharedQuestId") - .HasColumnType("bigint"); + b.HasKey("ClientSessionId"); - b.Property("SharedQuestStep") - .HasColumnType("smallint"); + b.HasIndex("ClientId"); - b.HasKey("ShareId"); + b.HasIndex("SessionId"); - b.HasIndex("ShareHostClientId"); - - b.ToTable("QuestShares", (string)null); + b.ToTable("SessionMembers", (string)null); }); - modelBuilder.Entity("QuestShare.Server.Models.Share", b => + modelBuilder.Entity("QuestShare.Server.Models.Client", b => { - b.HasOne("QuestShare.Server.Models.Client", "ShareHost") + b.HasOne("QuestShare.Server.Models.SessionMember", null) .WithMany() - .HasForeignKey("ShareHostClientId") + .HasForeignKey("SessionMemberClientSessionId"); + }); + + modelBuilder.Entity("QuestShare.Server.Models.Session", b => + { + b.HasOne("QuestShare.Server.Models.Client", "Owner") + .WithMany() + .HasForeignKey("OwnerClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("ShareHost"); + b.HasOne("QuestShare.Server.Models.SessionMember", null) + .WithMany() + .HasForeignKey("SessionMemberClientSessionId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("QuestShare.Server.Models.SessionMember", b => + { + b.HasOne("QuestShare.Server.Models.Client", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("QuestShare.Server.Models.Session", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Session"); }); #pragma warning restore 612, 618 } diff --git a/QuestShare.Server/Models/Client.cs b/QuestShare.Server/Models/Client.cs index f3e1c83..d7465ac 100644 --- a/QuestShare.Server/Models/Client.cs +++ b/QuestShare.Server/Models/Client.cs @@ -7,10 +7,9 @@ namespace QuestShare.Server.Models { [Key] public Guid ClientId { get; set; } - public required ulong CharacterId { get; set; } public required string ConnectionId { get; set; } = null!; public required string Token { get; set; } = null!; - public string ConnectedShareCode { get; set; } = ""; + public List KnownShareCodes { get; set; } = []; [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public DateTime Created { get; set; } diff --git a/QuestShare.Server/Models/QuestShareContext.cs b/QuestShare.Server/Models/QuestShareContext.cs index 48c5340..f876fa3 100644 --- a/QuestShare.Server/Models/QuestShareContext.cs +++ b/QuestShare.Server/Models/QuestShareContext.cs @@ -14,9 +14,10 @@ namespace QuestShare.Server.Models { // Database.EnsureCreated(); } - public DbSet Shares { get; set; } = null!; + public DbSet Sessions { get; set; } = null!; public DbSet Clients { get; set; } = null!; public DbSet Bans { get; set; } = null!; + public DbSet SessionMembers { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -29,13 +30,12 @@ namespace QuestShare.Server.Models modelBuilder.Entity().ToTable("Clients"); modelBuilder.Entity().Property(a => a.Created).HasDefaultValueSql("current_timestamp"); modelBuilder.Entity().Property(a => a.LastUpdated).HasDefaultValueSql("current_timestamp").ValueGeneratedOnAddOrUpdate(); - modelBuilder.Entity().ToTable("QuestShares"); - modelBuilder.Entity().Property(a => a.Created).HasDefaultValueSql("current_timestamp"); - modelBuilder.Entity().Property(a => a.LastUpdated).HasDefaultValueSql("current_timestamp").ValueGeneratedOnAddOrUpdate(); - modelBuilder.Entity(x => - { - x.HasOne(a => a.ShareHost).WithMany(); - }); + modelBuilder.Entity().Property(a => a.KnownShareCodes).HasConversion( + v => string.Join(',', v), + v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() + ); + new SessionsConfiguration().Configure(modelBuilder.Entity()); + new ClientSessionConfiguration().Configure(modelBuilder.Entity()); modelBuilder.Entity().ToTable("Bans"); } } diff --git a/QuestShare.Server/Models/Session.cs b/QuestShare.Server/Models/Session.cs new file mode 100644 index 0000000..8ee089d --- /dev/null +++ b/QuestShare.Server/Models/Session.cs @@ -0,0 +1,56 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Newtonsoft.Json; +using QuestShare.Common; + +namespace QuestShare.Server.Models +{ + public class Session + { + [Key] + public Guid SessionId { get; set; } + public string OwnerCharacterId { get; set; } = ""; + public required Client Owner { get; set; } + public required string ShareCode { get; set; } + public required string ReservedConnectionId { get; set; } + public int SharedQuestId { get; set; } = 0; + public byte SharedQuestStep { get; set; } = 0; + public List PartyMembers { get; set; } = []; + public bool IsActive { get; set; } = true; + public bool SkipPartyCheck { get; set; } = false; + public bool AllowJoins { get; set; } = true; + public DateTime Created { get; set; } + public DateTime LastUpdated { get; set; } + + public bool IsMember(string characterId) + { + return PartyMembers.Contains(characterId); + } + public void AddMember(string characterId) + { + PartyMembers.Add(characterId); + } + public void RemoveMember(string characterId) + { + PartyMembers.Remove(characterId); + } + + } + + public class SessionsConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Sessions"); + builder.Property(s => s.Created).HasDefaultValueSql("current_timestamp").ValueGeneratedOnAddOrUpdate(); ; + builder.Property(s => s.LastUpdated).HasDefaultValueSql("current_timestamp").ValueGeneratedOnAddOrUpdate(); + builder.Property(s => s.PartyMembers).HasConversion( + v => JsonConvert.SerializeObject(v), + v => JsonConvert.DeserializeObject>(v ?? "")! + ); + } + } +} diff --git a/QuestShare.Server/Models/Share.cs b/QuestShare.Server/Models/Share.cs deleted file mode 100644 index 095648c..0000000 --- a/QuestShare.Server/Models/Share.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; - -namespace QuestShare.Server.Models -{ - public class Share - { - [Key] - public Guid ShareId { get; set; } - public virtual required Client ShareHost { get; set; } - public required string ShareCode { get; set; } - public uint SharedQuestId { get; set; } - public byte SharedQuestStep { get; set; } - - [DefaultValue(false)] - public bool BroadcastParty { get; set; } - public DateTime Created { get; set; } - public DateTime LastUpdated { get; set; } - - } -} diff --git a/QuestShare.Server/Program.cs b/QuestShare.Server/Program.cs index 605305e..8c7697d 100644 --- a/QuestShare.Server/Program.cs +++ b/QuestShare.Server/Program.cs @@ -1,15 +1,18 @@ -global using QuestShare.Common.API.Share; global using QuestShare.Common.API; global using Serilog; using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.EntityFrameworkCore; +using QuestShare.Common; +using QuestShare.Server.Models; +using System.Diagnostics; using System.Net; namespace QuestShare.Server { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { var log = new LoggerConfiguration() .WriteTo.Console() @@ -52,20 +55,20 @@ namespace QuestShare.Server logging.AddDebug(); logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Information); logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Information); - }).UseSerilog() ; - var secretKey = Environment.GetEnvironmentVariable("QUESTSHARE_SECRET"); - if (string.IsNullOrEmpty(secretKey)) - { - Console.WriteLine("Please set the QUESTSHARE_SECRET environment variable. A hardcoded default is being used."); - Environment.SetEnvironmentVariable("QUESTSHARE_SECRET", "A1B2C3D4E5F6G7H8I9J0"); - } + }).UseSerilog(); var database = Environment.GetEnvironmentVariable("QUESTSHARE_DATABASE"); if (string.IsNullOrEmpty(database)) { - Console.WriteLine("Please set the QUESTSHARE_DATABASE environment variable. A hardcoded default is being used."); - Environment.SetEnvironmentVariable("QUESTSHARE_DATABASE", "Host=sol.nate.lan;User ID=dalamud;Password=dalamud1meteor;Database=questshare"); + Console.WriteLine("Please set the QUESTSHARE_DATABASE environment variable."); + Environment.Exit(-1); } var app = builder.Build(); + Log.Information($"Starting QuestShare Server - API Version {Constants.Version}"); + using (var scope = app.Services.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.MigrateAsync(); + } app.Run(); } } diff --git a/QuestShare.Server/QuestShare.Server.csproj b/QuestShare.Server/QuestShare.Server.csproj index 0bb56c9..bd37534 100644 --- a/QuestShare.Server/QuestShare.Server.csproj +++ b/QuestShare.Server/QuestShare.Server.csproj @@ -17,7 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -26,6 +26,8 @@ + + diff --git a/plogonmaster.json b/plogonmaster.json index 7857a37..58051c3 100644 --- a/plogonmaster.json +++ b/plogonmaster.json @@ -12,17 +12,17 @@ ], "RepoUrl": "https://git.nathanc.tech/nate/QuestShare/", "AcceptsFeedback": false, - "DownloadLinkInstall": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-3/latest.zip", - "DownloadLinkTesting": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-3/latest.zip", - "DownloadLinkUpdate": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-3/latest.zip", + "DownloadLinkInstall": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-4/latest.zip", + "DownloadLinkTesting": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-4/latest.zip", + "DownloadLinkUpdate": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-4/latest.zip", "DownloadCount": 1, - "LastUpdate": "1739849999", + "LastUpdate": "1739859999", "IsHide": false, "IsTestingExclusive": false, "IconUrl": "", "DalamudApiLevel": 11, "InternalName": "QuestShare", - "AssemblyVersion": "1.0.0.5" + "AssemblyVersion": "1.0.1.0" } ]