Update project

This commit is contained in:
Nathan C 2025-03-15 22:10:24 -04:00
parent 93231e5419
commit 6f95fcde97
No known key found for this signature in database
GPG Key ID: 6094F8F8D02EA281
27 changed files with 494 additions and 248 deletions

View File

@ -4,7 +4,7 @@ namespace QuestShare.Common
{ {
public class Constants public class Constants
{ {
public static readonly string Version = "3"; public static readonly string Version = "1";
} }
public static class StringExtensions public static class StringExtensions

View File

@ -24,5 +24,6 @@ namespace QuestShare.Common
BannedTooManyBadRequests, BannedTooManyBadRequests,
AlreadyRegistered, AlreadyRegistered,
AlreadyJoined, AlreadyJoined,
ServerMaintenance,
} }
} }

View File

@ -21,6 +21,7 @@ namespace QuestShare.Common
public required string OwnerCharacterId { get; set; } public required string OwnerCharacterId { get; set; }
public int ActiveQuestId { get; set; } public int ActiveQuestId { get; set; }
public byte ActiveQuestStep { get; set; } public byte ActiveQuestStep { get; set; }
public bool IsValid { get; set; } = true;
} }
public record OwnedSession public record OwnedSession

View File

@ -19,8 +19,8 @@ public class ConfigurationManager
public List<Objects.ShareCode> KnownShareCodes { get; set; } = []; public List<Objects.ShareCode> KnownShareCodes { get; set; } = [];
public int ActiveQuestId { get; set; } = 0; public int ActiveQuestId { get; set; } = 0;
public byte ActiveQuestStep { get; set; } = 0; public byte ActiveQuestStep { get; set; } = 0;
public Dictionary<long, string> KnownCharacters { get; set; } = []; public Dictionary<string, string> ShareCodeOwners { get; set; } = [];
public ApiConfiguration[] ApiConfigurations { get; set; } = [new ApiConfiguration { DisplayName = "Primary Server - US East", Url = "https://api.nathanc.tech/Hub", Active = false }]; public List<ApiConfiguration> ApiConfigurations { get; set; } = [];
public string ApiUrl => ApiConfigurations.FirstOrDefault(x => x.Active)?.Url ?? "https://api.nathanc.tech/Hub"; 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 string ApiDisplayName => ApiConfigurations.FirstOrDefault(x => x.Active)?.DisplayName ?? "Primary Server - US East";
} }
@ -44,14 +44,14 @@ public class ConfigurationManager
if (ClientState.LocalContentId != 0) if (ClientState.LocalContentId != 0)
{ {
LocalContentId = ClientState.LocalContentId; LocalContentId = ClientState.LocalContentId;
Framework.Run(Load); Load();
} }
} }
public void OnLogin() public void OnLogin()
{ {
LocalContentId = ClientState.LocalContentId; LocalContentId = ClientState.LocalContentId;
Framework.Run(Load); Load();
} }
public void Dispose() public void Dispose()
@ -77,18 +77,12 @@ public class ConfigurationManager
if (deserialized != null) if (deserialized != null)
{ {
Instance = deserialized; Instance = deserialized;
#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 else
{ {
Log.Error($"Failed to deserialize configuration for {LocalContentId}"); Log.Error($"Failed to deserialize configuration for {LocalContentId}, using defaults.");
Instance = new Configuration();
Save();
} }
} }
} }

View File

@ -54,11 +54,17 @@ namespace QuestShare.Common
GameQuests.Add(new GameQuest(quest.RowId)); GameQuests.Add(new GameQuest(quest.RowId));
} }
} }
if (activeQuest != null) if (activeQuest != null && !GameQuests.Any(q => q.QuestId == activeQuest.QuestId))
{
Log.Debug($"Active quest {activeQuest.QuestId} was not found in GameQuests, assuming completed.");
HostService.Update(0, 0);
return;
}
else if (activeQuest != null)
{ {
SetActiveFlag(activeQuest.QuestId); SetActiveFlag(activeQuest.QuestId);
} }
else if (ConfigurationManager.Instance.OwnedSession != null) else if (ConfigurationManager.Instance.OwnedSession != null && ConfigurationManager.Instance.OwnedSession.ActiveQuestId != 0)
{ {
var quest = GetQuestById((uint)ConfigurationManager.Instance.OwnedSession.ActiveQuestId); var quest = GetQuestById((uint)ConfigurationManager.Instance.OwnedSession.ActiveQuestId);
SetActiveFlag(quest.QuestId); SetActiveFlag(quest.QuestId);
@ -149,6 +155,8 @@ namespace QuestShare.Common
Log.Debug("No next quest in chain"); Log.Debug("No next quest in chain");
GameQuests.Remove(q); GameQuests.Remove(q);
LoadQuests(); LoadQuests();
HostService.Update(0, 0);
return;
} }
} }
HostService.Update((int)q.QuestId, q.CurrentStep); HostService.Update((int)q.QuestId, q.CurrentStep);

View File

@ -11,7 +11,7 @@ namespace QuestShare;
public sealed class Plugin : IDalamudPlugin public sealed class Plugin : IDalamudPlugin
{ {
public static string Name => "Quest Share"; public static string Name => "Quest Share";
public static string Version => "1.0.0"; public static string Version => "0.0.0.1";
public static string PluginDataPath { get; private set; } = null!; public static string PluginDataPath { get; private set; } = null!;
internal static ConfigurationManager Configuration { get; private set; } = null!; internal static ConfigurationManager Configuration { get; private set; } = null!;
private static List<IService> Services = []; private static List<IService> Services = [];
@ -33,8 +33,10 @@ public sealed class Plugin : IDalamudPlugin
]; ];
GameQuestManager.Initialize(); GameQuestManager.Initialize();
LogStream = new StringWriter(); LogStream = new StringWriter();
#if DEBUG
Console.SetOut(LogStream); Console.SetOut(LogStream);
Console.SetError(LogStream); Console.SetError(LogStream);
#endif
Framework.Update += OnFramework; Framework.Update += OnFramework;
Log.Debug($"Token: {ConfigurationManager.Instance.Token}"); Log.Debug($"Token: {ConfigurationManager.Instance.Token}");
foreach (var service in Services) foreach (var service in Services)
@ -65,6 +67,7 @@ public sealed class Plugin : IDalamudPlugin
private void OnFramework(IFramework framework) private void OnFramework(IFramework framework)
{ {
#if DEBUG
// check if there's logs to write // check if there's logs to write
if (LogStream != null && LogStream.ToString() != "") if (LogStream != null && LogStream.ToString() != "")
{ {
@ -72,5 +75,7 @@ public sealed class Plugin : IDalamudPlugin
LogStream.GetStringBuilder().Clear(); LogStream.GetStringBuilder().Clear();
Log.Write(LogEventLevel.Debug, null, toWrite); Log.Write(LogEventLevel.Debug, null, toWrite);
} }
#endif
} }
} }

View File

@ -2,12 +2,13 @@
<Project Sdk="Dalamud.NET.SDK/11.2.0"> <Project Sdk="Dalamud.NET.SDK/11.2.0">
<PropertyGroup> <PropertyGroup>
<Version>0.0.0.1</Version> <Version>0.0.0.1</Version>
<Description>A sample plugin.</Description> <Description>Share quests with your friends so they can follow you on your journey.</Description>
<PackageProjectUrl>https://github.com/goatcorp/SamplePlugin</PackageProjectUrl> <PackageProjectUrl>https://github.com/Era-FFXIV/QuestShare</PackageProjectUrl>
<PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression> <PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<RepositoryUrl>https://github.com/Era-FFXIV/QuestShare</RepositoryUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Include="QuestShare.json" /> <Content Include="QuestShare.json" />

View File

@ -2,13 +2,13 @@
"Author": "Era", "Author": "Era",
"Name": "Quest Share", "Name": "Quest Share",
"Punchline": "Share your quest progress with others.", "Punchline": "Share your quest progress with others.",
"Description": "blah", "Description": "Follow your friends on their questing journey by seeing their progress through a shared quest. Provides markers for quest destinations and (soon) teleporting features.",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"Tags": [ "Tags": [
"utility", "utility",
"vendor", "questing",
"shop" "coop"
], ],
"RepoUrl": "", "RepoUrl": "https://github.com/Era-FFXIV/QuestShare",
"AcceptsFeedback": false "AcceptsFeedback": false
} }

View File

@ -30,19 +30,51 @@ namespace QuestShare.Services.API
ConfigurationManager.Instance.Token = response.Token; ConfigurationManager.Instance.Token = response.Token;
foreach(var session in response.Sessions) foreach(var session in response.Sessions)
{ {
if (!session.IsValid)
{
Log.Warning("Session {ShareCode} is invalid.", session.ShareCode);
share.RemoveSession(session.ShareCode);
ShareService.RemoveKnownShareCode(session.ShareCode);
continue;
}
share.AddSession(session); share.AddSession(session);
} }
if (response.OwnedSession != null) if (response.OwnedSession != null)
{ {
ConfigurationManager.Instance.OwnedSession = response.OwnedSession; ConfigurationManager.Instance.OwnedSession = response.OwnedSession;
if (response.OwnedSession.Session != null)
{
Log.Debug("Setting active quest to {QuestId} - {QuestStep}", response.OwnedSession.Session.ActiveQuestId, response.OwnedSession.Session.ActiveQuestStep);
GameQuestManager.SetActiveFlag((uint)response.OwnedSession.Session.ActiveQuestId);
}
} }
ConfigurationManager.Save(); ConfigurationManager.Save();
ShareService.RecheckShareCodes();
} }
else else
{ {
Log.Error("Failed to authorize: {Error}", response.Error); Log.Error("Failed to authorize: {Error}", response.Error);
UiService.LastErrorMessage = $"Failed to authorize: {response.Error}"; UiService.LastErrorMessage = $"Failed to authorize: {response.Error}";
_ = ((ApiService)Plugin.GetService<ApiService>()).Disconnect(); _ = ((ApiService)Plugin.GetService<ApiService>()).Disconnect();
if (response.Error == Error.InvalidVersion)
{
UiService.LastServerMessage = "Invalid version detected, please update the plugin.";
((ApiService)Plugin.GetService<ApiService>()).IsLockedOut = true;
}
else if (response.Error == Error.InvalidToken)
{
UiService.LastServerMessage = "Invalid token detected, please reauthorize.";
((ApiService)Plugin.GetService<ApiService>()).IsLockedOut = true;
}
else if (response.Error == Error.BannedTooManyBadRequests)
{
UiService.LastServerMessage = "You are temporarily banned due to too many bad requests.";
((ApiService)Plugin.GetService<ApiService>()).IsLockedOut = true;
}
else if (response.Error == Error.ServerMaintenance)
{
UiService.LastServerMessage = "Server is currently undergoing maintenance. Please try again later.";
}
} }
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -21,6 +21,7 @@ namespace QuestShare.Services.API
var share = (ShareService)Plugin.GetService<ShareService>(); var share = (ShareService)Plugin.GetService<ShareService>();
Log.Information("Successfully joined group."); Log.Information("Successfully joined group.");
share.AddSession(resp.Session); share.AddSession(resp.Session);
ShareService.RecheckShareCodes();
var api = (ApiService)Plugin.GetService<ApiService>(); var api = (ApiService)Plugin.GetService<ApiService>();
api.OnGroupJoin(new ApiService.GroupJoinEventArgs { Session = resp.Session, IsSuccess = true }); api.OnGroupJoin(new ApiService.GroupJoinEventArgs { Session = resp.Session, IsSuccess = true });
} }

View File

@ -26,8 +26,19 @@ namespace QuestShare.Services.API
{ {
Log.Information("Session started successfully"); Log.Information("Session started successfully");
ConfigurationManager.Instance.OwnedSession = response.Session; ConfigurationManager.Instance.OwnedSession = response.Session;
// check if hashes match
if (response.Session!.OwnerCharacterId != ClientState.LocalContentId.ToString().SaltedHash(response.Session.ShareCode))
{
Log.Error($"Mismatched owner character ID! {response.Session!.OwnerCharacterId} != {ClientState.LocalContentId.ToString().SaltedHash(response.Session.ShareCode)}");
}
ConfigurationManager.Save(); ConfigurationManager.Save();
HostService.UpdateParty(); if (GameQuestManager.GetActiveQuest() != null)
{
HostService.Update(GameQuestManager.GetActiveQuest()!.QuestId, GameQuestManager.GetActiveQuest()!.CurrentStep);
} else
{
HostService.UpdateParty();
}
} }
else else
{ {

View File

@ -11,18 +11,18 @@ namespace QuestShare.Services
internal class ApiService : IService internal class ApiService : IService
{ {
private readonly string socketUrl = ConfigurationManager.Instance.ApiUrl;
private HubConnection ApiConnection { get; set; } = null!; private HubConnection ApiConnection { get; set; } = null!;
internal bool IsConnected => ApiConnection.State == HubConnectionState.Connected; internal bool IsConnected => ApiConnection.State == HubConnectionState.Connected;
internal bool IsLockedOut { get; set; } = false;
internal HubConnectionState ConnectionState => ApiConnection.State; internal HubConnectionState ConnectionState => ApiConnection.State;
internal bool IsAuthorized { get; private set; } = false;
private bool isDisposing = false; private bool isDisposing = false;
internal static string Token => ConfigurationManager.Instance.Token; internal static string Token => ConfigurationManager.Instance.Token;
private readonly List<IAPIHandler> apiHandlers = []; private readonly List<IAPIHandler> apiHandlers = [];
private int retryCount = 0;
public void Initialize() public void Initialize()
{ {
var builder = new HubConnectionBuilder().WithUrl(socketUrl).ConfigureLogging(logging => var builder = new HubConnectionBuilder().WithUrl(ConfigurationManager.Instance.ApiUrl).ConfigureLogging(logging =>
{ {
logging.SetMinimumLevel(LogLevel.Information).AddConsole(); logging.SetMinimumLevel(LogLevel.Information).AddConsole();
}); });
@ -31,13 +31,20 @@ namespace QuestShare.Services
{ {
if (isDisposing) return; if (isDisposing) return;
Log.Warning($"Connection closed... {error}"); Log.Warning($"Connection closed... {error}");
Log.Warning($"Connection closed, retrying... {error}"); if (retryCount < 3)
await Task.Delay(new Random().Next(0, 5) * 1000); {
await ApiConnection.StartAsync(); retryCount++;
await Task.Delay(new Random().Next(0, 5) * 1000);
await ApiConnection.StartAsync();
} else
{
Log.Error("Failed to reconnect after 3 attempts, giving up.");
}
}; };
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
ApiConnection.Reconnected += async (error) => ApiConnection.Reconnected += async (error) =>
{ {
retryCount = 0;
Log.Information("Connection reconnected"); Log.Information("Connection reconnected");
}; };
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
@ -58,6 +65,10 @@ namespace QuestShare.Services
ClientState.Login += OnLogin; ClientState.Login += OnLogin;
ClientState.Logout += OnLogout; ClientState.Logout += OnLogout;
Framework.Update += SavePersistedConfig; Framework.Update += SavePersistedConfig;
if (ConfigurationManager.Instance.ConnectOnStartup)
{
Task.Run(Connect);
}
} }
public void Shutdown() public void Shutdown()
{ {
@ -83,7 +94,6 @@ namespace QuestShare.Services
public void OnLogout(int code, int type) public void OnLogout(int code, int type)
{ {
ConfigurationManager.Save(); ConfigurationManager.Save();
IsAuthorized = false;
ApiConnection.StopAsync().ConfigureAwait(false); ApiConnection.StopAsync().ConfigureAwait(false);
} }
@ -92,8 +102,15 @@ namespace QuestShare.Services
ConfigurationManager.Instance.Token = Token; ConfigurationManager.Instance.Token = Token;
} }
public void OnUrlChange()
{
Shutdown();
Initialize();
}
public async Task Connect() public async Task Connect()
{ {
Log.Debug($"Attempting to connect to {ConfigurationManager.Instance.ApiUrl}");
try try
{ {
if (IsConnected) await ApiConnection.StopAsync(); if (IsConnected) await ApiConnection.StopAsync();
@ -107,6 +124,7 @@ namespace QuestShare.Services
else else
{ {
Log.Information("Connected to socket server"); Log.Information("Connected to socket server");
retryCount = 0;
} }
}); });
} }

View File

@ -1,4 +1,5 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -24,6 +25,7 @@ namespace QuestShare.Services
{ {
ClientState.Login += OnLogin; ClientState.Login += OnLogin;
ClientState.Logout += OnLogout; ClientState.Logout += OnLogout;
CharacterId = ClientState.LocalContentId;
} }
public void Shutdown() public void Shutdown()
@ -51,6 +53,9 @@ namespace QuestShare.Services
SkipPartyCheck = false, SkipPartyCheck = false,
Session = session, Session = session,
}; };
Log.Debug($"Hashing {CharacterId} with {shareCode} to get ${session.OwnerCharacterId}");
Log.Debug(JsonConvert.SerializeObject(ownedSession));
Log.Debug(JsonConvert.SerializeObject(session));
ApiService.DispatchSessionStart(ownedSession); ApiService.DispatchSessionStart(ownedSession);
} }
@ -125,5 +130,10 @@ namespace QuestShare.Services
var api = (ApiService)Plugin.GetService<ApiService>(); var api = (ApiService)Plugin.GetService<ApiService>();
ApiService.DispatchCancel(); ApiService.DispatchCancel();
} }
internal static void Update(uint questId, byte currentStep)
{
Update((int)questId, currentStep);
}
} }
} }

View File

@ -5,8 +5,8 @@ namespace QuestShare.Services
internal class ShareService : IService internal class ShareService : IService
{ {
internal static List<Objects.ShareCode> ShareCodes => ConfigurationManager.Instance.KnownShareCodes; internal static List<Objects.ShareCode> ShareCodes => ConfigurationManager.Instance.KnownShareCodes;
internal static Dictionary<string, string> ShareCodeOwners => ConfigurationManager.Instance.ShareCodeOwners;
internal List<Objects.Session> Sessions { get; private set; } = []; internal List<Objects.Session> Sessions { get; private set; } = [];
internal Dictionary<long, string> CharacterLookup { get; private set; } = [];
public void Initialize() public void Initialize()
{ {
@ -21,38 +21,46 @@ namespace QuestShare.Services
ClientState.Login -= OnLogin; ClientState.Login -= OnLogin;
ClientState.Logout -= OnLogout; ClientState.Logout -= OnLogout;
Framework.Update -= OnFrameworkUpdate; Framework.Update -= OnFrameworkUpdate;
CharacterLookup.Clear();
Sessions.Clear(); Sessions.Clear();
} }
private void OnLogin() private void OnLogin()
{ {
CharacterLookup.Clear();
CharacterLookup.Add((long)ClientState.LocalContentId, ClientState.LocalPlayer!.Name.TextValue);
foreach (var character in ConfigurationManager.Instance.KnownCharacters)
{
CharacterLookup.Add(character.Key, character.Value);
}
} }
private void OnLogout(int code, int state) private void OnLogout(int code, int state)
{ {
ConfigurationManager.Save();
CharacterLookup.Clear();
Sessions.Clear(); Sessions.Clear();
} }
private int pCount = 0; private int pCount = 0;
private static bool RecheckPending = false;
public static void RecheckShareCodes()
{
RecheckPending = true;
}
private void OnFrameworkUpdate(IFramework framework) private void OnFrameworkUpdate(IFramework framework)
{ {
if (PartyList.Count != pCount) if (PartyList.Count != pCount || RecheckPending)
{ {
Log.Debug("Party list changed");
foreach (var partyMember in PartyList) foreach (var partyMember in PartyList)
{ {
AddKnownCharacter(partyMember.ContentId, partyMember.Name.TextValue); Log.Debug($"Checking party member {partyMember.Name.TextValue} {partyMember.ContentId}");
foreach (var session in Sessions)
{
Log.Debug($"Checking session {session.ShareCode} - {session.OwnerCharacterId} ==? {partyMember.ContentId.ToString().SaltedHash(session.ShareCode)}");
if (session.OwnerCharacterId == partyMember.ContentId.ToString().SaltedHash(session.ShareCode))
{
Log.Debug($"Setting share code owner for {session.ShareCode} to {partyMember.Name.TextValue}");
ConfigurationManager.Instance.ShareCodeOwners[session.ShareCode] = partyMember.Name.TextValue;
}
}
} }
pCount = PartyList.Count; pCount = PartyList.Count;
RecheckPending = false;
} }
} }
@ -111,20 +119,13 @@ namespace QuestShare.Services
ConfigurationManager.Instance.KnownShareCodes.RemoveAll(sc => sc.Code == shareCode); ConfigurationManager.Instance.KnownShareCodes.RemoveAll(sc => sc.Code == shareCode);
} }
public static void AddKnownCharacter(long contentId, string characterId) public static string GetShareCodeOwner(string shareCode)
{ {
if (ConfigurationManager.Instance.KnownCharacters.ContainsKey(contentId)) if (ConfigurationManager.Instance.ShareCodeOwners.ContainsKey(shareCode))
{ {
return; return ConfigurationManager.Instance.ShareCodeOwners[shareCode];
} }
ConfigurationManager.Instance.KnownCharacters.Add(contentId, characterId); return "Unknown";
ConfigurationManager.Save();
}
public static void RemoveKnownCharacter(long contentId)
{
ConfigurationManager.Instance.KnownCharacters.Remove(contentId);
ConfigurationManager.Save();
} }
} }

View File

@ -8,6 +8,7 @@ namespace QuestShare.Services
public static WindowSystem WindowSystem = new("QuestShare"); public static WindowSystem WindowSystem = new("QuestShare");
public static MainWindow MainWindow { get; private set; } = new(); public static MainWindow MainWindow { get; private set; } = new();
public static string LastErrorMessage { get; set; } = string.Empty; public static string LastErrorMessage { get; set; } = string.Empty;
public static string LastServerMessage { get; set; } = string.Empty;
public void Initialize() public void Initialize()
{ {

View File

@ -7,6 +7,7 @@ using Dalamud.Interface.Windowing;
using ImGuiNET; using ImGuiNET;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using QuestShare.Services; using QuestShare.Services;
using static QuestShare.Common.ConfigurationManager;
using static QuestShare.Services.ApiService; using static QuestShare.Services.ApiService;
namespace QuestShare.Windows.MainWindow; namespace QuestShare.Windows.MainWindow;
@ -47,6 +48,7 @@ public class MainWindow : Window, IDisposable
ImGui.TextUnformatted("Server Status: "); ImGui.SameLine(); ImGui.TextUnformatted("Server Status: "); ImGui.SameLine();
DrawConnectionState(); DrawConnectionState();
ImGui.SameLine(); ImGui.SameLine();
ImGui.BeginDisabled(ApiService.IsLockedOut);
if (ApiService.IsConnected) if (ApiService.IsConnected)
{ {
if (ImGuiComponents.IconButton(FontAwesomeIcon.Unlink, ImGuiColors.DPSRed)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Unlink, ImGuiColors.DPSRed))
@ -58,9 +60,12 @@ public class MainWindow : Window, IDisposable
{ {
if (ImGuiComponents.IconButton(FontAwesomeIcon.Link, ImGuiColors.DPSRed)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Link, ImGuiColors.DPSRed))
{ {
UiService.LastErrorMessage = "";
UiService.LastServerMessage = "";
_ = ApiService.Connect(); _ = ApiService.Connect();
} }
} }
ImGui.EndDisabled();
// ImGui.SameLine(); // ImGui.SameLine();
ImGui.Separator(); ImGui.Separator();
using (ImRaii.TabBar("MainTabBar", ImGuiTabBarFlags.NoCloseWithMiddleMouseButton)) using (ImRaii.TabBar("MainTabBar", ImGuiTabBarFlags.NoCloseWithMiddleMouseButton))
@ -84,14 +89,19 @@ public class MainWindow : Window, IDisposable
} }
} }
ImGui.Separator(); ImGui.Separator();
if (UiService.LastErrorMessage != null) if (UiService.LastErrorMessage != "")
{ {
// ImGui.TextColored(ImGuiColors.DPSRed, UiService.LastErrorMessage); ImGui.TextColored(ImGuiColors.DPSRed, UiService.LastErrorMessage);
} }
} }
private void DrawConnectionState() private void DrawConnectionState()
{ {
if (UiService.LastServerMessage != "")
{
ImGui.TextColored(ImGuiColors.DalamudYellow, UiService.LastServerMessage);
return;
}
switch (this.ApiService.ConnectionState) switch (this.ApiService.ConnectionState)
{ {
case HubConnectionState.Connecting: case HubConnectionState.Connecting:
@ -116,135 +126,117 @@ public class MainWindow : Window, IDisposable
private void DrawHostTab() private void DrawHostTab()
{ {
if (HostService.ActiveSession != null && HostService.ActiveSession.ShareCode != null) generatePending = false; if (HostService.ActiveSession != null && HostService.ActiveSession.ShareCode != null) generatePending = false;
var isEnabled = ConfigurationManager.Instance.EnableHosting; ImGui.TextUnformatted("Share Code:");
if (ImGuiComponents.ToggleButton(Namespace + "/Enable Hosting", ref isEnabled))
{
ConfigurationManager.Instance.EnableHosting = isEnabled;
ConfigurationManager.Save();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted("Enable Hosting"); if (HostService.ActiveSession != null)
ImGui.Separator();
if (isEnabled)
{ {
ImGui.TextUnformatted("Share Code:"); ImGui.TextColored(ImGuiColors.HealerGreen, HostService.ActiveSession.ShareCode);
ImGui.SameLine(); if (ImGui.IsItemClicked())
if (HostService.ActiveSession != null)
{ {
ImGui.TextColored(ImGuiColors.HealerGreen, HostService.ActiveSession.ShareCode); ImGui.SetClipboardText(HostService.ActiveSession.ShareCode);
if (ImGui.IsItemClicked()) }
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 (ToggleButtonWithHelpMarker("Allow new Joins", "Allows new players to join the group.", ref allowJoins))
{
HostService.SetAllowJoins(allowJoins);
}
if (ToggleButtonWithHelpMarker("Skip Party Check", "Allows new players to join the group without being in your party first.", ref skipPartyCheck))
{
HostService.SetSkipPartyCheck(skipPartyCheck);
}
if (ToggleButtonWithHelpMarker("Send Updates", "Sends quest updates to the server.", ref sendUpdates))
{
HostService.SetIsActive(sendUpdates);
}
var track = Instance.TrackMSQ;
if (ToggleButtonWithHelpMarker("Track MSQ", "Automatically track the Main Scenario Quest.", ref track))
{
Instance.TrackMSQ = track;
Save();
}
ImGui.BeginDisabled(track);
using (var combo = ImRaii.Combo("##Quests", GameQuestManager.GetActiveQuest()?.QuestName ?? "---SELECT---", ImGuiComboFlags.HeightRegular))
{
if (combo)
{ {
ImGui.SetClipboardText(HostService.ActiveSession.ShareCode); foreach (var quest in GameQuestManager.GameQuests.OrderBy(q => q.QuestName))
}
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))
{ {
if (ImGui.Selectable(quest.QuestName)) selectedQuest = quest;
{ GameQuestManager.SetActiveFlag(quest.QuestId);
selectedQuest = quest; HostService.Update((int)quest.QuestId, quest.CurrentStep);
GameQuestManager.SetActiveFlag(quest.QuestId); Save();
HostService.Update((int)quest.QuestId, quest.CurrentStep);
ConfigurationManager.Save();
}
} }
} }
} }
ImGui.SameLine(); }
if (ImGui.Button("Refresh")) ImGui.SameLine();
{ if (ImGui.Button("Refresh"))
GameQuestManager.LoadQuests(); {
} GameQuestManager.LoadQuests();
ImGui.EndDisabled(); }
ImGui.SameLine(); ImGui.EndDisabled();
var track = ConfigurationManager.Instance.TrackMSQ;
if (ImGui.Checkbox("Track MSQ", ref track)) // add a line of space
{ ImGui.TextUnformatted(" ");
ConfigurationManager.Instance.TrackMSQ = track; if (selectedQuest == null && GameQuestManager.GetActiveQuest() == null)
ConfigurationManager.Save(); {
} ImGui.TextUnformatted("No quest selected.");
if (selectedQuest == null && GameQuestManager.GetActiveQuest() == null) return;
{ }
ImGui.TextUnformatted("No quest selected."); else if (GameQuestManager.GetActiveQuest() != null)
return; {
} selectedQuest = GameQuestManager.GetActiveQuest();
else if (GameQuestManager.GetActiveQuest() != null) }
{ ImGui.TextUnformatted("Active Quest:");
selectedQuest = GameQuestManager.GetActiveQuest(); ImGui.SameLine();
} ImGui.TextUnformatted(selectedQuest!.QuestName);
ImGui.TextUnformatted("Active Quest:"); ImGui.Separator();
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; var steps = selectedQuest.QuestSteps;
for (var i = 0; i < steps!.Count; i++) for (var i = 0; i < steps!.Count; i++)
{
var questText = Instance.HideFutureStepsHost && i + 1 > selectedQuest.CurrentStep ? "???" : steps[i];
if (i + 1 == selectedQuest.CurrentStep || (selectedQuest.CurrentStep == 0 && i == 0) || (selectedQuest.CurrentStep == 0xFF && i + 1 == steps.Count))
{ {
ImGui.TextColored(ImGuiColors.HealerGreen, questText);
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 if (i + 1 < selectedQuest.CurrentStep)
} else {
ImGui.BeginDisabled(generatePending);
if (ImGui.Button("Generate New"))
{ {
ApiService.DispatchRegister(); ImGui.TextColored(ImGuiColors.DalamudYellow, questText);
}
else
{
ImGui.TextUnformatted(questText);
} }
ImGui.EndDisabled();
} }
} }
else
{
ImGui.BeginDisabled(generatePending);
if (ImGui.Button("Generate New"))
{
DispatchRegister();
}
ImGui.EndDisabled();
}
} }
@ -270,7 +262,7 @@ public class MainWindow : Window, IDisposable
var payload = new Objects.ShareCode { CharacterId = ClientState.LocalContentId.ToString().SaltedHash(enteredShareCode), Code = enteredShareCode }; var payload = new Objects.ShareCode { CharacterId = ClientState.LocalContentId.ToString().SaltedHash(enteredShareCode), Code = enteredShareCode };
isJoining = true; isJoining = true;
ApiService.GroupJoined += OnGroupJoin; ApiService.GroupJoined += OnGroupJoin;
ApiService.DispatchGroup(payload); DispatchGroup(payload);
} }
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.Separator(); ImGui.Separator();
@ -289,7 +281,7 @@ public class MainWindow : Window, IDisposable
DrawSessionDetails(session); DrawSessionDetails(session);
if (ImGui.Button("Leave Group")) if (ImGui.Button("Leave Group"))
{ {
ApiService.DispatchUngroup(session); DispatchUngroup(session);
isLeaving = true; isLeaving = true;
} }
} }
@ -300,7 +292,7 @@ public class MainWindow : Window, IDisposable
private void DrawSessionDetails(Objects.Session session) private void DrawSessionDetails(Objects.Session session)
{ {
ImGui.TextUnformatted($"Owner: {session.OwnerCharacterId}"); ImGui.TextUnformatted($"Owner: {ShareService.GetShareCodeOwner(session.ShareCode)}");
var activeQuest = session.ActiveQuestId; var activeQuest = session.ActiveQuestId;
var activeStep = session.ActiveQuestStep; var activeStep = session.ActiveQuestStep;
if (activeQuest != 0) if (activeQuest != 0)
@ -309,21 +301,17 @@ public class MainWindow : Window, IDisposable
var steps = questInfo.QuestSteps; var steps = questInfo.QuestSteps;
ImGui.TextUnformatted(questInfo.QuestData.Name.ExtractText()); ImGui.TextUnformatted(questInfo.QuestData.Name.ExtractText());
ImGui.TextUnformatted("Current Step:");
ImGui.SameLine();
ImGui.TextUnformatted(activeStep.ToString());
ImGui.Separator();
ImGui.TextUnformatted("Quest Steps:");
ImGui.Separator(); ImGui.Separator();
for (var i = 0; i < steps.Count; i++) for (var i = 0; i < steps.Count; i++)
{ {
var questText = Instance.HideFutureStepsMember && i + 1 > activeStep ? "???" : steps[i];
if (i + 1 == activeStep || (i + 1 == steps.Count && activeStep == 0xFF)) if (i + 1 == activeStep || (i + 1 == steps.Count && activeStep == 0xFF))
{ {
ImGui.TextColored(ImGuiColors.HealerGreen, steps[i]); ImGui.TextColored(ImGuiColors.HealerGreen, questText);
} }
else else
{ {
ImGui.TextUnformatted(steps[i]); ImGui.TextUnformatted(questText);
} }
} }
if (ImGui.Button("Get Marker")) if (ImGui.Button("Get Marker"))
@ -350,31 +338,36 @@ public class MainWindow : Window, IDisposable
}*/ }*/
} else } else
{ {
ImGui.TextUnformatted("No active quest or host is offline."); ImGui.TextUnformatted("No active quest.");
} }
} }
string newServerDisplayName = "";
string newServerUrl = "";
private void DrawSettingsTab() private void DrawSettingsTab()
{ {
var selectedApiServer = ConfigurationManager.Instance.ApiDisplayName; var selectedApiServer = Instance.ApiDisplayName;
ImGui.BeginDisabled(Instance.ApiConfigurations.Count < 1);
if (ImGui.BeginCombo("API Server", selectedApiServer)) if (ImGui.BeginCombo("API Server", selectedApiServer))
{ {
foreach (var server in ConfigurationManager.Instance.ApiConfigurations) foreach (var server in Instance.ApiConfigurations)
{ {
var isSelected = selectedApiServer == server.DisplayName; var isSelected = selectedApiServer == server.DisplayName;
if (ImGui.Selectable(server.DisplayName, isSelected)) if (ImGui.Selectable(server.DisplayName, isSelected))
{ {
var index = Array.FindIndex(ConfigurationManager.Instance.ApiConfigurations, x => x.DisplayName == server.DisplayName); var index = Array.FindIndex<ApiConfiguration>([.. Instance.ApiConfigurations], x => x.DisplayName == server.DisplayName);
ConfigurationManager.Instance.ApiConfigurations[index].Active = true; Instance.ApiConfigurations[index].Active = true;
foreach (var config in ConfigurationManager.Instance.ApiConfigurations) foreach (var config in Instance.ApiConfigurations)
{ {
if (config.DisplayName != server.DisplayName) if (config.DisplayName != server.DisplayName)
{ {
config.Active = false; config.Active = false;
} }
} }
ConfigurationManager.Save(); Save();
Framework.Run(ApiService.OnUrlChange);
} }
if (isSelected) if (isSelected)
{ {
@ -383,29 +376,117 @@ public class MainWindow : Window, IDisposable
} }
ImGui.EndCombo(); ImGui.EndCombo();
} }
ImGui.EndDisabled();
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Add")) if (ImGui.Button("Add"))
{ {
// does nothing yet // pop up a dialog to add a new server
ImGui.OpenPopup("Add Server");
} }
var connectOnStartup = ConfigurationManager.Instance.ConnectOnStartup; ImGui.SameLine();
if (ImGui.Checkbox("Connect on Startup", ref connectOnStartup)) ImGui.BeginDisabled(Instance.ApiConfigurations.Count < 1);
if (ImGui.Button("Delete"))
{ {
ConfigurationManager.Instance.ConnectOnStartup = connectOnStartup; // pop up a dialog to delete the selected server
ConfigurationManager.Save(); ImGui.OpenPopup("Delete?");
} }
var autoShareMsq = ConfigurationManager.Instance.AutoShareMsq; ImGui.EndDisabled();
if (ImGui.Checkbox("Auto Share MSQ", ref autoShareMsq)) var center = ImGui.GetMainViewport().GetCenter();
ImGui.SetNextWindowPos(center, ImGuiCond.Appearing, new Vector2(0.5f, 0.5f));
var open = true;
using (var addServer = ImRaii.PopupModal("Add Server", ref open, ImGuiWindowFlags.AlwaysAutoResize))
{ {
ConfigurationManager.Instance.AutoShareMsq = autoShareMsq; if (addServer)
ConfigurationManager.Save(); {
ImGui.TextUnformatted("Add a new API server configuration.");
ImGui.InputText("Display Name", ref newServerDisplayName, 64);
ImGui.InputText("URL", ref newServerUrl, 200);
ImGui.TextUnformatted("Note: The URL should be the full URL to the hub endpoint and MUST be ws:// or wss:// (preferred)");
bool isValid = Uri.TryCreate(newServerUrl, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeWs || uriResult.Scheme == Uri.UriSchemeWss);
ImGui.BeginDisabled(!isValid || string.IsNullOrEmpty(newServerDisplayName) || string.IsNullOrEmpty(newServerUrl));
if (ImGui.Button("Save"))
{
if (Instance.ApiConfigurations.Count == 0)
{
// add default server first
Instance.ApiConfigurations.Add(new ApiConfiguration { DisplayName = ConfigurationManager.Instance.ApiDisplayName, Url = ConfigurationManager.Instance.ApiUrl, Active = false });
}
Instance.ApiConfigurations.Add(new ApiConfiguration { DisplayName = newServerDisplayName, Url = newServerUrl, Active = true });
Save();
ImGui.CloseCurrentPopup();
newServerUrl = "";
newServerDisplayName = "";
}
ImGui.EndDisabled();
ImGui.SameLine();
if (ImGui.Button("Cancel"))
{
ImGui.CloseCurrentPopup();
newServerUrl = "";
newServerDisplayName = "";
}
}
} }
var autoShareNewQuests = ConfigurationManager.Instance.AutoShareNewQuests; var deleteServer = false;
if (ImGui.Checkbox("Auto Share New Quests", ref autoShareNewQuests)) using (var delServer = ImRaii.PopupModal("Delete?", ref deleteServer, ImGuiWindowFlags.AlwaysAutoResize))
{ {
ConfigurationManager.Instance.AutoShareNewQuests = autoShareNewQuests; if (delServer)
ConfigurationManager.Save(); {
ImGui.TextUnformatted("Are you sure you want to delete this server?");
if (ImGui.Button("Yes"))
{
var index = Array.FindIndex<ApiConfiguration>([.. Instance.ApiConfigurations], x => x.DisplayName == selectedApiServer);
Instance.ApiConfigurations.RemoveAt(index);
Save();
ImGui.CloseCurrentPopup();
Framework.Run(ApiService.OnUrlChange);
}
ImGui.SameLine();
if (ImGui.Button("No"))
{
ImGui.CloseCurrentPopup();
}
}
} }
var connectOnStartup = Instance.ConnectOnStartup;
if (ToggleButtonWithHelpMarker("Connect on Startup", "Automatically connect to the selected API server when the game is started.", ref connectOnStartup))
{
Instance.ConnectOnStartup = connectOnStartup;
Save();
}
var autoShareMsq = Instance.AutoShareMsq;
if (ToggleButtonWithHelpMarker("Auto Share MSQ", "Automatically share the Main Scenario Quest with your group when it is accepted.", ref autoShareMsq))
{
Instance.AutoShareMsq = autoShareMsq;
Save();
}
// TODO: Implement this feature
/*var autoShareNewQuests = Instance.AutoShareNewQuests;
if (ToggleButtonWithHelpMarker("Auto Share New Quests", "Automatically share new quests with your group when they are accepted.", ref autoShareNewQuests))
{
Instance.AutoShareNewQuests = autoShareNewQuests;
Save();
}*/
var hideFutureStepsHost = Instance.HideFutureStepsHost;
if (ToggleButtonWithHelpMarker("Hide Future Steps (Host)", "Hides future steps of the quest from the UI when viewing your hosted quest. This does not affect the quest sharing process.", ref hideFutureStepsHost))
{
Instance.HideFutureStepsHost = hideFutureStepsHost;
Save();
}
var hideFutureStepsMember = Instance.HideFutureStepsMember;
if (ToggleButtonWithHelpMarker("Hide Future Steps (Member)", "Hides future steps of the quest from the UI when viewing a shared quest. This does not affect the quest sharing process.", ref hideFutureStepsMember))
{
Instance.HideFutureStepsMember = hideFutureStepsMember;
Save();
}
}
private static bool ToggleButtonWithHelpMarker(string label, string helpText, ref bool v)
{
ImGui.TextUnformatted(label);
ImGui.SameLine();
var result = ImGuiComponents.ToggleButton(label, ref v);
ImGuiComponents.HelpMarker(helpText);
return result;
} }
} }

View File

@ -49,41 +49,61 @@ namespace QuestShare.Server.Hubs
var sessions = new List<Objects.Session>(); var sessions = new List<Objects.Session>();
Objects.OwnedSession? ownedSession = null; Objects.OwnedSession? ownedSession = null;
foreach(var share in request.ShareCodes) foreach (var share in request.ShareCodes)
{ {
var session = await SessionManager.GetSession(share.Code); var session = await SessionManager.GetSession(share.Code);
if (session != null) if (session != null)
{ {
var members = await ClientManager.GetClientsInSession(session); 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)
{ {
if (session.OwnerCharacterId == share.CharacterId && client.ClientId == session.Owner.ClientId && ownedSession == null) ownedSession = new Objects.OwnedSession
{ {
ownedSession = new Objects.OwnedSession Session = new Objects.Session
{
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, OwnerCharacterId = session.OwnerCharacterId,
ShareCode = session.ShareCode,
ActiveQuestId = session.SharedQuestId, ActiveQuestId = session.SharedQuestId,
ActiveQuestStep = session.SharedQuestStep, ActiveQuestStep = session.SharedQuestStep
ShareCode = share.Code, },
}); SkipPartyCheck = session.SkipPartyCheck,
} IsActive = session.IsActive,
AllowJoins = session.AllowJoins,
};
}
else if (members.Any(m => m.Client.ClientId == client.ClientId))
{
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 Groups.AddToGroupAsync(Context.ConnectionId, session.ShareCode);
} }
else
{
sessions.Add(new Objects.Session
{
OwnerCharacterId = "",
ActiveQuestId = 0,
ActiveQuestStep = 0,
ShareCode = share.Code,
IsValid = false,
});
}
}
else
{
sessions.Add(new Objects.Session
{
OwnerCharacterId = "",
ActiveQuestId = 0,
ActiveQuestStep = 0,
ShareCode = share.Code,
IsValid = false,
});
} }
} }
await Clients.Caller.SendAsync(nameof(Authorize), new Authorize.Response await Clients.Caller.SendAsync(nameof(Authorize), new Authorize.Response

View File

@ -36,6 +36,36 @@ namespace QuestShare.Server.Hubs
}); });
return; return;
} }
if (!session.AllowJoins)
{
Log.Warning($"[GroupJoin] Session {request.SessionInfo.Code} does not allow joins.");
await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response
{
Success = false,
Error = Error.InvalidParty,
});
return;
}
if (session.Owner.ClientId == client!.ClientId)
{
Log.Warning($"[GroupJoin] Client {client} is the owner of session {session.ShareCode}");
await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response
{
Success = false,
Error = Error.InvalidParty,
});
return;
}
if (!session.SkipPartyCheck && !session.PartyMembers.Contains(request.SessionInfo.CharacterId))
{
Log.Warning($"[GroupJoin] Client {client} is not joined to party hosted by {session.OwnerCharacterId}.");
await Clients.Caller.SendAsync(nameof(GroupJoin), new GroupJoin.Response
{
Success = false,
Error = Error.InvalidParty,
});
return;
}
await ClientManager.AddClientSession(client!.ClientId, session.SessionId); await ClientManager.AddClientSession(client!.ClientId, session.SessionId);
await Groups.AddToGroupAsync(Context.ConnectionId, session.ShareCode.ToString()); await Groups.AddToGroupAsync(Context.ConnectionId, session.ShareCode.ToString());
await ClientManager.AddKnownShareCode(client!, session.ShareCode); await ClientManager.AddKnownShareCode(client!, session.ShareCode);
@ -46,6 +76,8 @@ namespace QuestShare.Server.Hubs
{ {
OwnerCharacterId = session.OwnerCharacterId, OwnerCharacterId = session.OwnerCharacterId,
ShareCode = session.ShareCode, ShareCode = session.ShareCode,
ActiveQuestId = session.SharedQuestId,
ActiveQuestStep = session.SharedQuestStep
}, },
}); });
await Clients.GroupExcept(Context.ConnectionId, session.ShareCode.ToString()).SendAsync(nameof(GroupJoin.GroupJoinBroadcast), new GroupJoin.GroupJoinBroadcast await Clients.GroupExcept(Context.ConnectionId, session.ShareCode.ToString()).SendAsync(nameof(GroupJoin.GroupJoinBroadcast), new GroupJoin.GroupJoinBroadcast

View File

@ -36,18 +36,25 @@ namespace QuestShare.Server.Hubs
}); });
return; return;
} }
await SessionManager.UpdateActiveQuest(request.Session.ShareCode, request.Session.ActiveQuestId, request.Session.ActiveQuestStep);
await SessionManager.SetPartyMembers(session!.ShareCode, [.. request.PartyMembers]); await SessionManager.SetPartyMembers(session!.ShareCode, [.. request.PartyMembers]);
await Clients.Caller.SendAsync(nameof(Update), new Update.Response
if (request.IsQuestUpdate)
{ {
Success = true, await SessionManager.UpdateActiveQuest(request.Session.ShareCode, request.Session.ActiveQuestId, request.Session.ActiveQuestStep);
Error = Error.None, // Broadcast to party
}); await Clients.GroupExcept(session.ShareCode.ToString(), Context.ConnectionId).SendAsync(nameof(Update.UpdateBroadcast), new Update.UpdateBroadcast
// Broadcast to party {
await Clients.GroupExcept(session.ShareCode.ToString(), Context.ConnectionId).SendAsync(nameof(Update.UpdateBroadcast), new Update.UpdateBroadcast Session = request.Session.Session,
});
} else
{ {
Session = request.Session.Session, await SessionManager.SetSessionSettings(session.ShareCode, request.Session);
}); await Clients.Caller.SendAsync(nameof(Update), new Update.Response
{
Success = true,
Error = Error.None,
});
}
} }
} }
} }

View File

@ -9,12 +9,12 @@ namespace QuestShare.Server.Managers
public static async Task<Session?> GetSession(Client client) public static async Task<Session?> GetSession(Client client)
{ {
using var context = new QuestShareContext(); using var context = new QuestShareContext();
return await context.Sessions.Where(s => s.Owner.ClientId == client.ClientId).FirstOrDefaultAsync(); return await context.Sessions.Where(s => s.Owner.ClientId == client.ClientId).Include(session => session.Owner).FirstOrDefaultAsync();
} }
public static async Task<Session?> GetSession(string ShareCode) public static async Task<Session?> GetSession(string ShareCode)
{ {
using var context = new QuestShareContext(); using var context = new QuestShareContext();
var session = await context.Sessions.Where(s => s.ShareCode == ShareCode).FirstOrDefaultAsync(); var session = await context.Sessions.Where(s => s.ShareCode == ShareCode).Include(session => session.Owner).FirstOrDefaultAsync();
return session; return session;
} }
@ -47,6 +47,7 @@ namespace QuestShare.Server.Managers
s.AllowJoins = session.AllowJoins; s.AllowJoins = session.AllowJoins;
s.SkipPartyCheck = session.SkipPartyCheck; s.SkipPartyCheck = session.SkipPartyCheck;
await context.SaveChangesAsync(); await context.SaveChangesAsync();
await AddMemberToSession(s, s.OwnerCharacterId);
return s; return s;
} }
@ -92,7 +93,7 @@ namespace QuestShare.Server.Managers
public static async Task<List<SessionMember>> GetMembersInSession(Session session) public static async Task<List<SessionMember>> GetMembersInSession(Session session)
{ {
using var context = new QuestShareContext(); using var context = new QuestShareContext();
var s = await context.SessionMembers.Where(s => s.Session.SessionId == session.SessionId).Include("Sessions").Include("Clients").ToListAsync(); var s = await context.SessionMembers.Where(s => s.Session.SessionId == session.SessionId).Include(sm => sm.Session).Include(sm => sm.Client).ToListAsync();
return s; return s;
} }
@ -111,5 +112,22 @@ namespace QuestShare.Server.Managers
var records = await context.SaveChangesAsync(); var records = await context.SaveChangesAsync();
Log.Debug($"[UPDATE] Updated {records} quests for session {shareCode}"); Log.Debug($"[UPDATE] Updated {records} quests for session {shareCode}");
} }
public static async Task SetSessionSettings(string shareCode, Objects.OwnedSession sessionObj)
{
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 settings for session {shareCode}");
return;
}
session.IsActive = sessionObj.IsActive;
session.AllowJoins = sessionObj.AllowJoins;
session.SkipPartyCheck = sessionObj.SkipPartyCheck;
var records = await context.SaveChangesAsync();
Log.Debug($"[UPDATE] Updated {records} settings for session {shareCode}");
}
} }
} }

View File

@ -21,7 +21,9 @@ namespace QuestShare.Server.Models
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.UseNpgsql(Environment.GetEnvironmentVariable("QUESTSHARE_DATABASE")); optionsBuilder
.UseLazyLoadingProxies()
.UseNpgsql(Environment.GetEnvironmentVariable("QUESTSHARE_DATABASE"));
} }

View File

@ -13,7 +13,7 @@ namespace QuestShare.Server.Models
[Key] [Key]
public Guid SessionId { get; set; } public Guid SessionId { get; set; }
public string OwnerCharacterId { get; set; } = ""; public string OwnerCharacterId { get; set; } = "";
public required Client Owner { get; set; } public virtual required Client Owner { get; set; }
public required string ShareCode { get; set; } public required string ShareCode { get; set; }
public required string ReservedConnectionId { get; set; } public required string ReservedConnectionId { get; set; }
public int SharedQuestId { get; set; } = 0; public int SharedQuestId { get; set; } = 0;

View File

@ -9,8 +9,8 @@ namespace QuestShare.Server.Models
public class SessionMember public class SessionMember
{ {
public Guid ClientSessionId { get; set; } public Guid ClientSessionId { get; set; }
public required Client Client { get; set; } public virtual required Client Client { get; set; }
public required Session Session { get; set; } public virtual required Session Session { get; set; }
// public required string CharacterId { get; set; } // public required string CharacterId { get; set; }
public DateTime Created { get; set; } public DateTime Created { get; set; }
public DateTime LastUpdated { get; set; } public DateTime LastUpdated { get; set; }

View File

@ -12,7 +12,7 @@ namespace QuestShare.Server
{ {
public class Program public class Program
{ {
public static async Task Main(string[] args) public static void Main(string[] args)
{ {
var log = new LoggerConfiguration() var log = new LoggerConfiguration()
.WriteTo.Console() .WriteTo.Console()

View File

@ -3,7 +3,8 @@
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development",
"QUESTSHARE_DATABASE": "Host=sol.nate.lan;User ID=dalamud;Password=dalamud1meteor;Database=questshare"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "http://localhost:5087" "applicationUrl": "http://localhost:5087"

View File

@ -10,16 +10,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.3" /> <PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />

View File

@ -12,9 +12,9 @@
], ],
"RepoUrl": "https://git.nathanc.tech/nate/QuestShare/", "RepoUrl": "https://git.nathanc.tech/nate/QuestShare/",
"AcceptsFeedback": false, "AcceptsFeedback": false,
"DownloadLinkInstall": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-4/latest.zip", "DownloadLinkInstall": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-5/latest.zip",
"DownloadLinkTesting": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-4/latest.zip", "DownloadLinkTesting": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-5/latest.zip",
"DownloadLinkUpdate": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-4/latest.zip", "DownloadLinkUpdate": "https://git.nathanc.tech/nate/QuestShare/releases/download/alpha-5/latest.zip",
"DownloadCount": 1, "DownloadCount": 1,
"LastUpdate": "1739859999", "LastUpdate": "1739859999",
"IsHide": false, "IsHide": false,
@ -22,7 +22,7 @@
"IconUrl": "", "IconUrl": "",
"DalamudApiLevel": 11, "DalamudApiLevel": 11,
"InternalName": "QuestShare", "InternalName": "QuestShare",
"AssemblyVersion": "1.0.1.0" "AssemblyVersion": "1.0.2.0"
} }
] ]