Revert gpose actors on plugin unload or when NoSnap triggers
This commit is contained in:
@@ -204,6 +204,13 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RevertNow(ILogger logger, Guid applicationId, int objectIndex)
|
||||||
|
{
|
||||||
|
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||||
|
logger.LogTrace("[{applicationId}] Immediately reverting object index {objId}", applicationId, objectIndex);
|
||||||
|
_glamourerRevert.Invoke(objectIndex, LockCode);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RevertByNameAsync(ILogger logger, string name, Guid applicationId)
|
public async Task RevertByNameAsync(ILogger logger, string name, Guid applicationId)
|
||||||
{
|
{
|
||||||
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||||
|
|||||||
@@ -271,6 +271,13 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RedrawNow(ILogger logger, Guid applicationId, int objectIndex)
|
||||||
|
{
|
||||||
|
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
||||||
|
logger.LogTrace("[{applicationId}] Immediately redrawing object index {objId}", applicationId, objectIndex);
|
||||||
|
_penumbraRedraw.Invoke(objectIndex);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId)
|
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId)
|
||||||
{
|
{
|
||||||
if (!APIAvailable) return;
|
if (!APIAvailable) return;
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ public class PairHandlerFactory
|
|||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
|
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
|
||||||
private readonly VisibilityService _visibilityService;
|
private readonly VisibilityService _visibilityService;
|
||||||
|
private readonly NoSnapService _noSnapService;
|
||||||
|
|
||||||
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
||||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||||
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
||||||
ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory,
|
ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory,
|
||||||
MareConfigService configService, VisibilityService visibilityService)
|
MareConfigService configService, VisibilityService visibilityService, NoSnapService noSnapService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
@@ -49,12 +50,13 @@ public class PairHandlerFactory
|
|||||||
_pairAnalyzerFactory = pairAnalyzerFactory;
|
_pairAnalyzerFactory = pairAnalyzerFactory;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_visibilityService = visibilityService;
|
_visibilityService = visibilityService;
|
||||||
|
_noSnapService = noSnapService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PairHandler Create(Pair pair)
|
public PairHandler Create(Pair pair)
|
||||||
{
|
{
|
||||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
|
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
|
||||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||||
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService);
|
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService, _noSnapService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
private readonly VisibilityService _visibilityService;
|
private readonly VisibilityService _visibilityService;
|
||||||
|
private readonly NoSnapService _noSnapService;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||||
private Guid _applicationId;
|
private Guid _applicationId;
|
||||||
private Task? _applicationTask;
|
private Task? _applicationTask;
|
||||||
@@ -55,7 +56,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
FileCacheManager fileDbManager, MareMediator mediator,
|
FileCacheManager fileDbManager, MareMediator mediator,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
ServerConfigurationManager serverConfigManager,
|
ServerConfigurationManager serverConfigManager,
|
||||||
MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
|
MareConfigService configService, VisibilityService visibilityService,
|
||||||
|
NoSnapService noSnapService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
Pair = pair;
|
Pair = pair;
|
||||||
PairAnalyzer = pairAnalyzer;
|
PairAnalyzer = pairAnalyzer;
|
||||||
@@ -69,6 +71,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_visibilityService = visibilityService;
|
_visibilityService = visibilityService;
|
||||||
|
_noSnapService = noSnapService;
|
||||||
|
|
||||||
_visibilityService.StartTracking(Pair.Ident);
|
_visibilityService.StartTracking(Pair.Ident);
|
||||||
|
|
||||||
@@ -317,6 +320,24 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterGposeClones()
|
||||||
|
{
|
||||||
|
var name = PlayerName;
|
||||||
|
if (name == null)
|
||||||
|
return;
|
||||||
|
_ = _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
foreach (var actor in _dalamudUtil.GetGposeCharactersFromObjectTable())
|
||||||
|
{
|
||||||
|
if (actor == null) continue;
|
||||||
|
var gposeName = actor.Name.TextValue;
|
||||||
|
if (!name.Equals(gposeName, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
_noSnapService.AddGposer(actor.ObjectIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async Task UndoApplicationAsync(Guid applicationId = default)
|
private async Task UndoApplicationAsync(Guid applicationId = default)
|
||||||
{
|
{
|
||||||
Logger.LogDebug($"Undoing application of {Pair.UserPair}");
|
Logger.LogDebug($"Undoing application of {Pair.UserPair}");
|
||||||
@@ -333,6 +354,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false);
|
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false);
|
||||||
_penumbraCollection = Guid.Empty;
|
_penumbraCollection = Guid.Empty;
|
||||||
|
RegisterGposeClones();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
||||||
|
|||||||
@@ -13,18 +13,20 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
|||||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly NoSnapService _noSnapService;
|
||||||
private readonly Dictionary<string, HandledCharaDataEntry> _handledCharaData = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, HandledCharaDataEntry> _handledCharaData = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _handledCharaData;
|
public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _handledCharaData;
|
||||||
|
|
||||||
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
|
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
|
||||||
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
|
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
|
||||||
IpcManager ipcManager)
|
IpcManager ipcManager, NoSnapService noSnapService)
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_ipcManager = ipcManager;
|
_ipcManager = ipcManager;
|
||||||
|
_noSnapService = noSnapService;
|
||||||
mediator.Subscribe<GposeEndMessage>(this, msg =>
|
mediator.Subscribe<GposeEndMessage>(this, msg =>
|
||||||
{
|
{
|
||||||
foreach (var chara in _handledCharaData)
|
foreach (var chara in _handledCharaData)
|
||||||
@@ -90,13 +92,18 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (handled == null) return false;
|
if (handled == null) return false;
|
||||||
_handledCharaData.Remove(handled.Name);
|
_handledCharaData.Remove(handled.Name);
|
||||||
await _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus)).ConfigureAwait(false);
|
await _dalamudUtilService.RunOnFrameworkThread(async () =>
|
||||||
|
{
|
||||||
|
RemoveGposer(handled);
|
||||||
|
await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
|
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
|
||||||
{
|
{
|
||||||
_handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry);
|
_handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry);
|
||||||
|
_ = _dalamudUtilService.RunOnFrameworkThread(() => AddGposer(handledCharaDataEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
|
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
|
||||||
@@ -127,4 +134,23 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
|||||||
if (handler.Address == nint.Zero) return null;
|
if (handler.Address == nint.Zero) return null;
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int GetGposerObjectIndex(string name)
|
||||||
|
{
|
||||||
|
return _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.ObjectIndex ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddGposer(HandledCharaDataEntry handled)
|
||||||
|
{
|
||||||
|
int objectIndex = GetGposerObjectIndex(handled.Name);
|
||||||
|
if (objectIndex > 0)
|
||||||
|
_noSnapService.AddGposer(objectIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveGposer(HandledCharaDataEntry handled)
|
||||||
|
{
|
||||||
|
int objectIndex = GetGposerObjectIndex(handled.Name);
|
||||||
|
if (objectIndex > 0)
|
||||||
|
_noSnapService.RemoveGposer(objectIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -15,16 +16,23 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
["Snappy"] = false,
|
["Snappy"] = false,
|
||||||
["Meddle.Plugin"] = false
|
["Meddle.Plugin"] = false
|
||||||
};
|
};
|
||||||
|
private static readonly HashSet<int> _gposers = new();
|
||||||
|
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
|
||||||
public static bool AnyLoaded { get; private set; } = false;
|
public static bool AnyLoaded { get; private set; } = false;
|
||||||
|
|
||||||
public MareMediator Mediator { get; init; }
|
public MareMediator Mediator { get; init; }
|
||||||
|
|
||||||
public NoSnapService(ILogger<NoSnapService> logger, IDalamudPluginInterface pi, MareMediator mediator)
|
public NoSnapService(ILogger<NoSnapService> logger, IDalamudPluginInterface pi, MareMediator mediator,
|
||||||
|
IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
Mediator = mediator;
|
Mediator = mediator;
|
||||||
|
_hostApplicationLifetime = hostApplicationLifetime;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
foreach (var pluginName in _listOfPlugins.Keys)
|
foreach (var pluginName in _listOfPlugins.Keys)
|
||||||
{
|
{
|
||||||
var plugin = pi.InstalledPlugins.FirstOrDefault(p => p.InternalName.Equals(pluginName, StringComparison.Ordinal));
|
var plugin = pi.InstalledPlugins.FirstOrDefault(p => p.InternalName.Equals(pluginName, StringComparison.Ordinal));
|
||||||
@@ -38,9 +46,80 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mediator.Subscribe<GposeEndMessage>(this, msg => ClearGposeList());
|
||||||
|
Mediator.Subscribe<CutsceneEndMessage>(this, msg => ClearGposeList());
|
||||||
|
|
||||||
Update();
|
Update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddGposer(int objectIndex)
|
||||||
|
{
|
||||||
|
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Immediately reverting object index {id}", objectIndex);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Guid applicationId = Guid.NewGuid();
|
||||||
|
_ipcManager.Glamourer.RevertNow(_logger, applicationId, objectIndex);
|
||||||
|
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, objectIndex);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Registering gposer object index {id}", objectIndex);
|
||||||
|
lock (_gposers)
|
||||||
|
_gposers.Add(objectIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveGposer(int objectIndex)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Un-registering gposer object index {id}", objectIndex);
|
||||||
|
lock (_gposers)
|
||||||
|
_gposers.Remove(objectIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearGposeList()
|
||||||
|
{
|
||||||
|
if (_gposers.Count > 0)
|
||||||
|
_logger.LogInformation("Clearing gposer list");
|
||||||
|
lock (_gposers)
|
||||||
|
_gposers.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RevertGposers()
|
||||||
|
{
|
||||||
|
List<int>? gposersList = null;
|
||||||
|
|
||||||
|
lock (_gposers)
|
||||||
|
{
|
||||||
|
if (_gposers.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Reverting gposers");
|
||||||
|
gposersList = _gposers.ToList();
|
||||||
|
_gposers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gposersList == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dalamudUtilService.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
Guid applicationId = Guid.NewGuid();
|
||||||
|
|
||||||
|
foreach (var gposer in gposersList)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ipcManager.Glamourer.RevertNow(_logger, applicationId, gposer);
|
||||||
|
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, gposer);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -48,6 +127,7 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
RevertGposers();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +143,7 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
if (AnyLoaded)
|
if (AnyLoaded)
|
||||||
{
|
{
|
||||||
|
RevertGposers();
|
||||||
var pluginList = string.Join(", ", _listOfPlugins.Where(p => p.Value).Select(p => p.Key));
|
var pluginList = string.Join(", ", _listOfPlugins.Where(p => p.Value).Select(p => p.Key));
|
||||||
Mediator.Publish(new NotificationMessage("Incompatible plugin loaded", $"Synced player appearances will not apply until incompatible plugins are disabled: {pluginList}.",
|
Mediator.Publish(new NotificationMessage("Incompatible plugin loaded", $"Synced player appearances will not apply until incompatible plugins are disabled: {pluginList}.",
|
||||||
NotificationType.Error));
|
NotificationType.Error));
|
||||||
|
|||||||
@@ -75,6 +75,19 @@ public class PluginWatcherService : MediatorSubscriberBase
|
|||||||
Logger.LogError(e, "PluginWatcherService exception");
|
Logger.LogError(e, "PluginWatcherService exception");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Continue scanning plugins during gpose as well
|
||||||
|
Mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "PluginWatcherService exception");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
|
|||||||
Reference in New Issue
Block a user