rework and changes

This commit is contained in:
Nathan C 2025-03-01 20:38:45 -05:00
parent ef35c59d13
commit ded3fb3e2c
No known key found for this signature in database
GPG Key ID: 6094F8F8D02EA281
63 changed files with 1707 additions and 2117 deletions

View File

@ -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<AuthRequest.Request, AuthRequest.Response>
#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;
}
}
}

View File

@ -6,14 +6,14 @@ using System.Threading.Tasks;
namespace QuestShare.Common.API
{
public class Authorize : IAPIEndpoint<Authorize.Request, Authorize.Response>
public sealed class Authorize : IAPIEndpoint<Authorize.Request, Authorize.Response>
{
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<Objects.ShareCode> 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<Objects.Session> Sessions { get; set; } = [];
public Objects.OwnedSession? OwnedSession { get; set; }
}
}
}

View File

@ -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<Cancel.Request, Cancel.Response>
public sealed class Cancel : IAPIEndpoint<Cancel.Request, Cancel.Response>
{
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

View File

@ -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,
}
}

View File

@ -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<Register.Request, Register.Response>
public sealed class GroupJoin : IAPIEndpoint<GroupJoin.Request, GroupJoin.Response>
{
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<ulong> 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; }
}
}
}

View File

@ -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<GroupLeave.Request, GroupLeave.Response>
{
@ -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; }
}
}
}

View File

@ -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<T1, T2> where T1 : IRequest where T2 : IResponse;
}

View File

@ -1,21 +0,0 @@
namespace QuestShare.Common.API.Party
{
public class PartyCheck : IAPIEndpoint<PartyCheck.Request, PartyCheck.Response>
{
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<ulong> 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!;
}
}
}

View File

@ -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<GetShareInfo.Request, GetShareInfo.Response>
public sealed class Register : IAPIEndpoint<Register.Request, Register.Response>
{
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<ulong> Members { get; set; }
public required string ShareCode { get; set; }
}
}
}

View File

@ -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<GroupJoin.Request, GroupJoin.Response>
{
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<ulong> Members { get; set; } = null!;
}
}
}

View File

@ -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
}
}

View File

@ -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<Resume.Request, Resume.Response>
{
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<ulong>? 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<ulong>? Members { get; set; } = [];
public bool IsGroup { get; set; } = false;
public bool IsHost { get; set; } = false;
}
}
}

View File

@ -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<Update.Request, Update.Response>
public sealed class Update : IAPIEndpoint<Update.Request, Update.Response>
{
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<ulong> PartyMembers { get; set; } = [];
public bool IsPartyChange { get; set; } = false;
public required Objects.OwnedSession Session { get; set; }
public List<string> 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; }
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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<Objects.ShareCode> KnownShareCodes { get; set; } = [];
public int ActiveQuestId { get; set; } = 0;
public byte ActiveQuestStep { get; set; } = 0;
public Dictionary<long, string> 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<Configuration>(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)}");
}

View File

@ -1,12 +0,0 @@
namespace QuestShare.Common
{
internal static class GameEventManager
{
public static void Initialize()
{
}
public static void Dispose()
{
}
}
}

View File

@ -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<ApiService>();
var share = (ShareService)Plugin.GetService<ShareService>();
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<ApiService>();
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<QuestDialogue> TextSheetForQuest(Lumina.Excel.Sheets.Quest q)
{
var qid = q.Id.ToString();

View File

@ -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; }
}
}

View File

@ -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<IService> Services = [];
internal static StringWriter LogStream { get; private set; } = null!;
public Plugin(IDalamudPluginInterface pluginInterface)
@ -23,33 +22,45 @@ public sealed class Plugin : IDalamudPlugin
pluginInterface.Create<Service>(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<T>() where T : IService
{
return Services.FirstOrDefault(s => s is T)!;
}
private void OnFramework(IFramework framework)

View File

@ -9,9 +9,6 @@
<ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Include="Windows\MainWindow\MainWindow.cs_" />
</ItemGroup>
<ItemGroup>
<Content Include="QuestShare.json" />
</ItemGroup>
@ -23,4 +20,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Addons\" />
</ItemGroup>
</Project>

View File

@ -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>());
ApiService.DispatchAuthorize();
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,50 @@
namespace QuestShare.Services.API
{
internal class Authorize_Client
{
public static void HandleDispatch()
{
if (ClientState.LocalContentId == 0)
{
_ = ((ApiService)Plugin.GetService<ApiService>()).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<ApiService>()).Invoke(nameof(Authorize), request).ConfigureAwait(false);
}
public static Task HandleResponse(Authorize.Response response)
{
var share = (ShareService)Plugin.GetService<ShareService>();
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<ApiService>()).Disconnect();
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,41 @@
namespace QuestShare.Services.API
{
internal class Cancel_Client
{
public static void HandleDispatch()
{
var api = (ApiService)Plugin.GetService<ApiService>();
var party = (PartyService)Plugin.GetService<PartyService>();
var share = (ShareService)Plugin.GetService<ShareService>();
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<ShareService>();
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<ShareService>();
share.RemoveSession(cancelBroadcast.ShareCode);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,44 @@
namespace QuestShare.Services.API
{
internal class GroupJoin_Client
{
public static void HandleDispatch(Objects.ShareCode shareCode) {
var api = (ApiService)Plugin.GetService<ApiService>();
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<ShareService>();
Log.Information("Successfully joined group.");
share.AddSession(resp.Session);
var api = (ApiService)Plugin.GetService<ApiService>();
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<ApiService>();
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<ShareService>();
share.AddSession(broadcast.Session);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,36 @@
namespace QuestShare.Services.API
{
internal class GroupLeave_Client
{
public static void HandleDispatch(Objects.Session session)
{
var api = (ApiService)Plugin.GetService<ApiService>();
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<ShareService>();
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.");
}
}
}

View File

@ -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<ApiService>()).Invoke(Method, request);
}
}
}

View File

@ -0,0 +1,36 @@
using Newtonsoft.Json;
namespace QuestShare.Services.API
{
internal class Register_Client
{
public static void HandleDispatch()
{
var api = (ApiService)Plugin.GetService<ApiService>();
_ = 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<HostService>();
host.Start(response.ShareCode);
}
else
{
Log.Error("Failed to register as host: {0}", response.Error);
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,43 @@
using Newtonsoft.Json;
namespace QuestShare.Services.API
{
internal class Update_Client
{
public static void HandleDispatch(Objects.OwnedSession session, List<string> partyMembers, bool broadcast = true)
{
var api = (ApiService)Plugin.GetService<ApiService>();
_ = 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<ShareService>()).UpdateSession(response.Session);
return Task.CompletedTask;
}
}
}

View File

@ -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<IAPIHandler> 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<AuthRequest.Response>(nameof(AuthRequest), AuthRequest_Client.HandleResponse);
ApiConnection.On<Authorize.Response>(nameof(Authorize), Authorize_Client.HandleResponse);
ApiConnection.On<Register.Response>(nameof(Register), Register_Client.HandleResponse);
ApiConnection.On<GroupJoin.Response>(nameof(GroupJoin), GroupJoin_Client.HandleResponse);
ApiConnection.On<GroupLeave.Response>(nameof(GroupLeave), GroupLeave_Client.HandleResponse);
ApiConnection.On<Cancel.Response>(nameof(Cancel), Cancel_Client.HandleResponse);
ApiConnection.On<Update.Response>(nameof(Update), Update_Client.HandleResponse);
ApiConnection.On<Update.UpdateBroadcast>(nameof(Update.UpdateBroadcast), UpdateBroadcast_Client.HandleResponse);
ApiConnection.On<Cancel.CancelBroadcast>(nameof(Cancel.CancelBroadcast), Cancel_Client.HandleBroadcast);
ApiConnection.On<GroupJoin.GroupJoinBroadcast>(nameof(GroupJoin.GroupJoinBroadcast), GroupJoin_Client.HandleBroadcast);
ApiConnection.On<GroupLeave.GroupLeaveBroadcast>(nameof(GroupLeave.GroupLeaveBroadcast), GroupLeave_Client.HandleBroadcast);
ApiConnection.On<SessionStart.Response>(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<string> partyMembers) => Update_Client.HandleDispatch(session, partyMembers, true);
public static void DispatchConfigChange(Objects.OwnedSession session, List<string> partyMembers) => Update_Client.HandleDispatch(session, partyMembers, false);
public static void DispatchSessionStart(Objects.OwnedSession session) => SessionStart_Client.HandleDispatch(session);
public event EventHandler<GroupJoinEventArgs>? 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; }
}
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,8 @@
namespace QuestShare.Services
{
internal interface IService
{
void Initialize();
void Shutdown();
}
}

View File

@ -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<long> 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<string> GetPartyMembers(Objects.Session session)
{
var members = new List<string>();
foreach (var member in PartyList)
{
members.Add(member.ContentId.ToString().SaltedHash(session.ShareCode));
}
return members;
}
}
}

View File

@ -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<SharedMember> SharedMembers = [];
internal static List<ulong> 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<Objects.ShareCode> ShareCodes => ConfigurationManager.Instance.KnownShareCodes;
internal List<Objects.Session> Sessions { get; private set; } = [];
internal Dictionary<long, string> 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<ulong>();
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<Dictionary<ulong, string>>(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<Dictionary<ulong, string>>(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<ulong, string> { { 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();
}
}

View File

@ -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<SocketEventArgs> OnCancelEvent = delegate { };
public event EventHandler<SocketEventArgs> OnGroupEvent = delegate { };
public event EventHandler<SocketEventArgs> OnUngroupEvent = delegate { };
public event EventHandler<SocketEventArgs> OnGetEvent = delegate { };
public event EventHandler<SocketEventArgs> OnRegisterEvent = delegate { };
public event EventHandler<SocketEventArgs> OnUpdateEvent = delegate { };
public event EventHandler<TokenCheckEventArgs> 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<Register.Response>(nameof(Register), Client_Register);
connection.On<Update.Response>(nameof(Update), Client_Update);
connection.On<Update.UpdateBroadcast>(nameof(Update.UpdateBroadcast), Client_UpdateBroadcast);
connection.On<GetShareInfo.Response>(nameof(GetShareInfo), Client_GetShareInfo);
connection.On<Cancel.Response>(nameof(Cancel), Client_Cancel);
connection.On<Cancel.CancelBroadcast>(nameof(Cancel.CancelBroadcast), Client_CancelBroadast);
connection.On<GroupJoin.Response>(nameof(GroupJoin), Client_GroupJoin);
connection.On<GroupLeave.Response>(nameof(GroupLeave), Client_GroupLeave);
connection.On<Authorize.Response>(nameof(Authorize), Client_Authorize);
connection.On<GroupNotify.GroupNotifyBroadcast>(nameof(GroupNotify.GroupNotifyBroadcast), Client_GroupNotify);
connection.On<Resume.Response>(nameof(Resume), Client_Resume);
connection.On<Authorize.AuthBroadcast>(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<ulong>();
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; }
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -1,6 +1,5 @@
using Dalamud.Interface.Utility;
using ImGuiNET;
using System;
using System.Numerics;
namespace QuestShare.Windows

View File

@ -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<ApiService>();
private ShareService ShareService => (ShareService)Plugin.GetService<ShareService>();
private PartyService PartyService => (PartyService)Plugin.GetService<PartyService>();
private HostService HostService => (HostService)Plugin.GetService<HostService>();
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();
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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.Session>();
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,
});
}
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.SignalR;
using QuestShare.Common;
using QuestShare.Server.Managers;
namespace QuestShare.Server.Hubs
@ -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,

View File

@ -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(),
});
}
}
}

View File

@ -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}");
}
}
}

View File

@ -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,
}
});
}
}

View File

@ -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;
}
}
}
}

View File

@ -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}.");
}
}
}

View File

@ -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<ulong> 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
});
}
}
}
}

View File

@ -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,
});
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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<Client> 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<Client?> 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<List<SessionMember>> 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;

View File

@ -1,48 +0,0 @@
namespace QuestShare.Server.Managers
{
public class PartyManager
{
private static List<Party> Parties = [];
public static void CreateParty(string host, List<string> 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<string> GetPartyMembers(string host)
{
var party = Parties.Find(p => p.Host == host);
return party?.Members ?? new List<string>();
}
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<string> Members { get; set; } = null!;
}
}

View File

@ -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<Session?> GetSession(Client client)
{
using var context = new QuestShareContext();
return await context.Sessions.Where(s => s.Owner.ClientId == client.ClientId).FirstOrDefaultAsync();
}
public static async Task<Session?> 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<string> 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<Session?> 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<string> 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<List<SessionMember>> 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}");
}
}
}

View File

@ -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<ulong, List<ulong>> 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<Share> 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<List<Client>> 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<ulong> 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<ulong> GetBroadcastPartyMembers(ulong hostCharacterId)
{
return PartyMembers.GetValueOrDefault(hostCharacterId, []);
}
public static string GenerateShareCode()
{
return Guid.NewGuid().ToString().Substring(0, 8).ToUpper();
}
}
}

View File

@ -60,13 +60,6 @@ namespace QuestShare.Server.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CharacterId")
.HasColumnType("numeric(20,0)");
b.Property<string>("ConnectedShareCode")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ConnectionId")
.IsRequired()
.HasColumnType("text");
@ -76,31 +69,102 @@ namespace QuestShare.Server.Migrations
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("current_timestamp");
b.Property<string>("KnownShareCodes")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("LastUpdated")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("current_timestamp");
b.Property<Guid?>("SessionMemberClientSessionId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("ShareId")
b.Property<Guid>("SessionId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("BroadcastParty")
b.Property<bool>("AllowJoins")
.HasColumnType("boolean");
b.Property<DateTime>("Created")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("current_timestamp");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<DateTime>("LastUpdated")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("current_timestamp");
b.Property<string>("OwnerCharacterId")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("OwnerClientId")
.HasColumnType("uuid");
b.Property<string>("PartyMembers")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ReservedConnectionId")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("SessionMemberClientSessionId")
.HasColumnType("uuid");
b.Property<string>("ShareCode")
.IsRequired()
.HasColumnType("text");
b.Property<int>("SharedQuestId")
.HasColumnType("integer");
b.Property<byte>("SharedQuestStep")
.HasColumnType("smallint");
b.Property<bool>("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<Guid>("ClientSessionId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ClientId")
.HasColumnType("uuid");
b.Property<DateTime>("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<string>("ShareCode")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("ShareHostClientId")
b.Property<Guid>("SessionId")
.HasColumnType("uuid");
b.Property<long>("SharedQuestId")
.HasColumnType("bigint");
b.HasKey("ClientSessionId");
b.Property<byte>("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
}

View File

@ -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<string> KnownShareCodes { get; set; } = [];
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime Created { get; set; }

View File

@ -14,9 +14,10 @@ namespace QuestShare.Server.Models
{
// Database.EnsureCreated();
}
public DbSet<Share> Shares { get; set; } = null!;
public DbSet<Session> Sessions { get; set; } = null!;
public DbSet<Client> Clients { get; set; } = null!;
public DbSet<Ban> Bans { get; set; } = null!;
public DbSet<SessionMember> SessionMembers { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@ -29,13 +30,12 @@ namespace QuestShare.Server.Models
modelBuilder.Entity<Client>().ToTable("Clients");
modelBuilder.Entity<Client>().Property(a => a.Created).HasDefaultValueSql("current_timestamp");
modelBuilder.Entity<Client>().Property(a => a.LastUpdated).HasDefaultValueSql("current_timestamp").ValueGeneratedOnAddOrUpdate();
modelBuilder.Entity<Share>().ToTable("QuestShares");
modelBuilder.Entity<Share>().Property(a => a.Created).HasDefaultValueSql("current_timestamp");
modelBuilder.Entity<Share>().Property(a => a.LastUpdated).HasDefaultValueSql("current_timestamp").ValueGeneratedOnAddOrUpdate();
modelBuilder.Entity<Share>(x =>
{
x.HasOne(a => a.ShareHost).WithMany();
});
modelBuilder.Entity<Client>().Property(a => a.KnownShareCodes).HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
);
new SessionsConfiguration().Configure(modelBuilder.Entity<Session>());
new ClientSessionConfiguration().Configure(modelBuilder.Entity<SessionMember>());
modelBuilder.Entity<Ban>().ToTable("Bans");
}
}

View File

@ -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<string> 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<Session>
{
public void Configure(EntityTypeBuilder<Session> 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<List<string>>(v ?? "")!
);
}
}
}

View File

@ -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; }
}
}

View File

@ -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<QuestShareContext>();
await context.Database.MigrateAsync();
}
app.Run();
}
}

View File

@ -17,7 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
@ -26,6 +26,8 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
<ItemGroup>

View File

@ -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"
}
]