Skip to content

Commit 4bd0f3e

Browse files
committed
syncshell updates
1 parent edf71a9 commit 4bd0f3e

4 files changed

Lines changed: 251 additions & 11 deletions

File tree

Snowcloak/Configuration/Configurations/SnowcloakConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public class SnowcloakConfig : ISnowcloakConfiguration
8888
public bool UseMultithreadedCompression { get; set; } = false;
8989
public CompressionType PreferredDownloadType { get; set; } = CompressionType.ZSTD;
9090
public bool AutoJoinVenueSyncshells { get; set; } = true;
91+
public List<VenueAutoJoinedSyncshell> AutoJoinedVenueSyncshells { get; set; } = [];
9192
public List<VenueReminderBookmark> VenueReminderBookmarks { get; set; } = [];
9293
public bool AllowBbCodeImages { get; set; } = true;
9394
public ElezenStrings.Colour PairRequestNameColors { get; set; } = new(Foreground: 0x87D745u, Glow: 0x7A6E0Eu);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Snowcloak.Configuration.Models;
2+
3+
[Serializable]
4+
public sealed class VenueAutoJoinedSyncshell
5+
{
6+
public string GroupGid { get; set; } = string.Empty;
7+
public string? GroupAlias { get; set; }
8+
public string? GroupHexString { get; set; }
9+
public uint WorldId { get; set; }
10+
public uint TerritoryId { get; set; }
11+
public uint DivisionId { get; set; }
12+
public uint WardId { get; set; }
13+
public uint PlotId { get; set; }
14+
public uint RoomId { get; set; }
15+
public bool IsApartment { get; set; }
16+
public DateTime JoinedAtUtc { get; set; } = DateTime.UtcNow;
17+
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
18+
public DateTime? LeaveAfterUtc { get; set; }
19+
public bool LeaveWarningShown { get; set; }
20+
}

Snowcloak/Services/Venue/VenueSyncshellService.cs

Lines changed: 229 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Snowcloak.API.Dto.Group;
55
using Snowcloak.API.Dto.Venue;
66
using Snowcloak.Configuration;
7+
using Snowcloak.Configuration.Models;
78
using Snowcloak.PlayerData.Pairs;
89
using Snowcloak.Services.Housing;
910
using Snowcloak.Services.Mediator;
@@ -18,6 +19,8 @@ namespace Snowcloak.Services.Venue;
1819

1920
public sealed class VenueSyncshellService : DisposableMediatorSubscriberBase, IHostedService
2021
{
22+
private static readonly TimeSpan AutoLeaveGracePeriod = TimeSpan.FromMinutes(90);
23+
private static readonly TimeSpan AutoLeaveWarningPeriod = TimeSpan.FromMinutes(5);
2124
private readonly ApiController _apiController;
2225
private readonly SnowcloakConfigService _configService;
2326
private readonly PairManager _pairManager;
@@ -37,17 +40,19 @@ public VenueSyncshellService(ILogger<VenueSyncshellService> logger, SnowMediator
3740

3841
Mediator.Subscribe<HousingPlotEnteredMessage>(this, msg => _ = HandleHousingPlotEntered(msg.Location));
3942
Mediator.Subscribe<HousingPlotLeftMessage>(this, msg => HandleHousingPlotLeft(msg.Location));
40-
Mediator.Subscribe<DisconnectedMessage>(this, _ => ClearState());
43+
Mediator.Subscribe<ConnectedMessage>(this, _ => RestorePersistedVenueState());
44+
Mediator.Subscribe<DisconnectedMessage>(this, _ => ClearRuntimeState());
4145
}
4246

4347
public Task StartAsync(CancellationToken cancellationToken)
4448
{
49+
RestorePersistedVenueState();
4550
return Task.CompletedTask;
4651
}
4752

4853
public Task StopAsync(CancellationToken cancellationToken)
4954
{
50-
ClearState();
55+
ClearRuntimeState();
5156
return Task.CompletedTask;
5257
}
5358

@@ -68,12 +73,15 @@ public async Task<bool> JoinVenueShellAsync(Guid promptId)
6873
{
6974
Logger.LogInformation("Joined venue syncshell {GID} for {Venue}", joinGroupId, prompt.Venue.VenueName);
7075
DisableAutoJoinedSyncshellChat(joinGroupId);
76+
var autoJoinedVenue = new AutoJoinedVenue(prompt.Venue.JoinInfo.Group, prompt.Location);
77+
CancelPendingRemoval(joinGroupId, clearPersistedDeadline: true);
7178
lock (_syncRoot)
7279
{
73-
_autoJoinedVenues[joinGroupId] = new(prompt.Venue.JoinInfo.Group, prompt.Location);
74-
CancelPendingRemoval(joinGroupId);
80+
_autoJoinedVenues[joinGroupId] = autoJoinedVenue;
7581
_activePrompt = null;
7682
}
83+
84+
UpsertPersistedVenue(autoJoinedVenue, leaveAfterUtc: null);
7785
}
7886

7987
return joined;
@@ -105,7 +113,7 @@ internal bool IsAutoJoined(GroupData group)
105113
}
106114
}
107115

108-
private void ClearState()
116+
private void ClearRuntimeState()
109117
{
110118
Logger.LogDebug("Clearing venue syncshell state");
111119
lock (_syncRoot)
@@ -121,7 +129,7 @@ private void ClearState()
121129
}
122130
}
123131

124-
private void CancelPendingRemoval(string groupId)
132+
private void CancelPendingRemoval(string groupId, bool clearPersistedDeadline = false)
125133
{
126134
lock (_syncRoot)
127135
{
@@ -131,6 +139,11 @@ private void CancelPendingRemoval(string groupId)
131139
_pendingRemovalTokens.Remove(groupId);
132140
}
133141
}
142+
143+
if (clearPersistedDeadline)
144+
{
145+
ClearPersistedLeaveDeadline(groupId);
146+
}
134147
}
135148

136149
private void CancelPendingRemovalForLocation(HousingPlotLocation location)
@@ -144,7 +157,7 @@ private void CancelPendingRemovalForLocation(HousingPlotLocation location)
144157

145158
foreach (var groupId in groupIds)
146159
{
147-
CancelPendingRemoval(groupId);
160+
CancelPendingRemoval(groupId, clearPersistedDeadline: true);
148161
}
149162
}
150163

@@ -217,9 +230,19 @@ private void HandleHousingPlotLeft(HousingPlotLocation location)
217230
}
218231

219232
private void ScheduleAutoLeave(AutoJoinedVenue venue)
233+
{
234+
ScheduleAutoLeave(venue, DateTime.UtcNow.Add(AutoLeaveGracePeriod), persistDeadline: true);
235+
}
236+
237+
private void ScheduleAutoLeave(AutoJoinedVenue venue, DateTime leaveAfterUtc, bool persistDeadline, bool leaveWarningShown = false)
220238
{
221239
CancelPendingRemoval(venue.Group.GID);
222240

241+
if (persistDeadline)
242+
{
243+
UpsertPersistedVenue(venue, leaveAfterUtc);
244+
}
245+
223246
var tokenSource = new CancellationTokenSource();
224247
lock (_syncRoot)
225248
{
@@ -230,18 +253,38 @@ private void ScheduleAutoLeave(AutoJoinedVenue venue)
230253
{
231254
try
232255
{
233-
await Task.Delay(TimeSpan.FromMinutes(2), tokenSource.Token).ConfigureAwait(false);
256+
await ShowAutoLeaveWarningWhenDue(venue, leaveAfterUtc, leaveWarningShown, tokenSource.Token).ConfigureAwait(false);
257+
258+
var delay = leaveAfterUtc - DateTime.UtcNow;
259+
if (delay < TimeSpan.Zero)
260+
{
261+
delay = TimeSpan.Zero;
262+
}
263+
264+
await Task.Delay(delay, tokenSource.Token).ConfigureAwait(false);
234265
if (tokenSource.Token.IsCancellationRequested)
235266
return;
236267

237268
if (!_apiController.IsConnected)
238269
{
239-
Logger.LogWarning("Could not auto-leave venue syncshell {GID} because the client is not connected", venue.Group.GID);
270+
Logger.LogInformation("Deferring auto-leave for venue syncshell {GID} until the client reconnects", venue.Group.GID);
271+
return;
272+
}
273+
274+
if (!IsGroupMember(venue.Group.GID))
275+
{
276+
RemovePersistedVenue(venue.Group.GID);
277+
lock (_syncRoot)
278+
{
279+
_autoJoinedVenues.Remove(venue.Group.GID);
280+
}
281+
240282
return;
241283
}
242284

243285
await _apiController.GroupLeave(new GroupDto(venue.Group)).ConfigureAwait(false);
244286
Logger.LogInformation("Auto-left venue syncshell {GID} after grace period", venue.Group.GID);
287+
RemovePersistedVenue(venue.Group.GID);
245288
lock (_syncRoot)
246289
{
247290
_autoJoinedVenues.Remove(venue.Group.GID);
@@ -259,8 +302,13 @@ private void ScheduleAutoLeave(AutoJoinedVenue venue)
259302
{
260303
lock (_syncRoot)
261304
{
262-
_pendingRemovalTokens.Remove(venue.Group.GID);
305+
if (_pendingRemovalTokens.TryGetValue(venue.Group.GID, out var pending) && ReferenceEquals(pending, tokenSource))
306+
{
307+
_pendingRemovalTokens.Remove(venue.Group.GID);
308+
}
263309
}
310+
311+
tokenSource.Dispose();
264312
}
265313
}, tokenSource.Token);
266314
}
@@ -293,6 +341,177 @@ private void RemoveStaleAutoJoinedVenues()
293341
}
294342
}
295343
}
344+
345+
foreach (var staleGroupId in staleGroupIds)
346+
{
347+
RemovePersistedVenue(staleGroupId);
348+
}
349+
}
350+
351+
private void RestorePersistedVenueState()
352+
{
353+
List<AutoJoinedVenue> venuesWithPendingLeave = [];
354+
var persisted = GetPersistedVenues();
355+
356+
lock (_syncRoot)
357+
{
358+
foreach (var entry in persisted)
359+
{
360+
if (string.IsNullOrWhiteSpace(entry.GroupGid))
361+
{
362+
continue;
363+
}
364+
365+
var venue = new AutoJoinedVenue(
366+
new GroupData(entry.GroupGid, entry.GroupAlias, entry.GroupHexString),
367+
new HousingPlotLocation(entry.WorldId, entry.TerritoryId, entry.DivisionId, entry.WardId, entry.PlotId, entry.RoomId, entry.IsApartment));
368+
369+
_autoJoinedVenues[entry.GroupGid] = venue;
370+
if (entry.LeaveAfterUtc.HasValue)
371+
{
372+
venuesWithPendingLeave.Add(venue);
373+
}
374+
}
375+
}
376+
377+
foreach (var venue in venuesWithPendingLeave)
378+
{
379+
var entry = persisted.FirstOrDefault(v => string.Equals(v.GroupGid, venue.Group.GID, StringComparison.Ordinal));
380+
if (entry?.LeaveAfterUtc != null)
381+
{
382+
ScheduleAutoLeave(venue, NormalizeUtc(entry.LeaveAfterUtc.Value), persistDeadline: false, entry.LeaveWarningShown);
383+
}
384+
}
385+
386+
if (_apiController.IsConnected)
387+
{
388+
RemoveStaleAutoJoinedVenues();
389+
}
390+
}
391+
392+
private List<VenueAutoJoinedSyncshell> GetPersistedVenues()
393+
{
394+
return _configService.Current.AutoJoinedVenueSyncshells ??= [];
395+
}
396+
397+
private void UpsertPersistedVenue(AutoJoinedVenue venue, DateTime? leaveAfterUtc)
398+
{
399+
var persisted = GetPersistedVenues();
400+
var entry = persisted.SingleOrDefault(v => string.Equals(v.GroupGid, venue.Group.GID, StringComparison.Ordinal));
401+
if (entry == null)
402+
{
403+
entry = new VenueAutoJoinedSyncshell
404+
{
405+
GroupGid = venue.Group.GID,
406+
JoinedAtUtc = DateTime.UtcNow
407+
};
408+
persisted.Add(entry);
409+
}
410+
411+
entry.GroupAlias = venue.Group.Alias;
412+
entry.GroupHexString = venue.Group.HexString;
413+
entry.WorldId = venue.Location.WorldId;
414+
entry.TerritoryId = venue.Location.TerritoryId;
415+
entry.DivisionId = venue.Location.DivisionId;
416+
entry.WardId = venue.Location.WardId;
417+
entry.PlotId = venue.Location.PlotId;
418+
entry.RoomId = venue.Location.RoomId;
419+
entry.IsApartment = venue.Location.IsApartment;
420+
entry.UpdatedAtUtc = DateTime.UtcNow;
421+
entry.LeaveAfterUtc = leaveAfterUtc.HasValue ? NormalizeUtc(leaveAfterUtc.Value) : null;
422+
entry.LeaveWarningShown = false;
423+
_configService.Save();
424+
}
425+
426+
private void ClearPersistedLeaveDeadline(string groupId)
427+
{
428+
var entry = GetPersistedVenues().SingleOrDefault(v => string.Equals(v.GroupGid, groupId, StringComparison.Ordinal));
429+
if (entry?.LeaveAfterUtc == null)
430+
{
431+
return;
432+
}
433+
434+
entry.LeaveAfterUtc = null;
435+
entry.LeaveWarningShown = false;
436+
entry.UpdatedAtUtc = DateTime.UtcNow;
437+
_configService.Save();
438+
}
439+
440+
private async Task ShowAutoLeaveWarningWhenDue(AutoJoinedVenue venue, DateTime leaveAfterUtc, bool warningAlreadyShown, CancellationToken token)
441+
{
442+
if (warningAlreadyShown)
443+
{
444+
return;
445+
}
446+
447+
var now = DateTime.UtcNow;
448+
if (leaveAfterUtc <= now)
449+
{
450+
return;
451+
}
452+
453+
var warningAtUtc = leaveAfterUtc - AutoLeaveWarningPeriod;
454+
var warningDelay = warningAtUtc - now;
455+
if (warningDelay > TimeSpan.Zero)
456+
{
457+
await Task.Delay(warningDelay, token).ConfigureAwait(false);
458+
}
459+
460+
if (token.IsCancellationRequested || leaveAfterUtc <= DateTime.UtcNow)
461+
{
462+
return;
463+
}
464+
465+
PublishAutoLeaveWarning(venue);
466+
MarkPersistedLeaveWarningShown(venue.Group.GID);
467+
}
468+
469+
private void PublishAutoLeaveWarning(AutoJoinedVenue venue)
470+
{
471+
Mediator.Publish(new NotificationMessage(
472+
"Venue syncshell auto-leave",
473+
$"You will leave {venue.Group.AliasOrGID} in 5 minutes unless you return to the venue.",
474+
NotificationType.Warning,
475+
TimeSpan.FromSeconds(10)));
476+
477+
Logger.LogInformation("Venue syncshell {GID} will auto-leave in {Minutes} minutes", venue.Group.GID, AutoLeaveWarningPeriod.TotalMinutes);
478+
}
479+
480+
private void MarkPersistedLeaveWarningShown(string groupId)
481+
{
482+
var entry = GetPersistedVenues().SingleOrDefault(v => string.Equals(v.GroupGid, groupId, StringComparison.Ordinal));
483+
if (entry == null || entry.LeaveWarningShown)
484+
{
485+
return;
486+
}
487+
488+
entry.LeaveWarningShown = true;
489+
entry.UpdatedAtUtc = DateTime.UtcNow;
490+
_configService.Save();
491+
}
492+
493+
private void RemovePersistedVenue(string groupId)
494+
{
495+
var persisted = GetPersistedVenues();
496+
if (persisted.RemoveAll(v => string.Equals(v.GroupGid, groupId, StringComparison.Ordinal)) > 0)
497+
{
498+
_configService.Save();
499+
}
500+
}
501+
502+
private bool IsGroupMember(string groupId)
503+
{
504+
return _pairManager.Groups.Keys.Any(g => string.Equals(g.GID, groupId, StringComparison.Ordinal));
505+
}
506+
507+
private static DateTime NormalizeUtc(DateTime timestamp)
508+
{
509+
return timestamp.Kind switch
510+
{
511+
DateTimeKind.Utc => timestamp,
512+
DateTimeKind.Local => timestamp.ToUniversalTime(),
513+
_ => DateTime.SpecifyKind(timestamp, DateTimeKind.Utc)
514+
};
296515
}
297516

298517

Snowcloak/Snowcloak.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
33
<PropertyGroup>
44
<AssemblyName>Snowcloak</AssemblyName>
5-
<Version>2.6.1.0</Version>
5+
<Version>2.6.2</Version>
66
<PackageProjectUrl>https://github.com/Eauldane/SnowcloakClient/</PackageProjectUrl>
77
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
88
</PropertyGroup>

0 commit comments

Comments
 (0)