24 Commits

Author SHA1 Message Date
75f88d5104 Potential fix for Honorific throwing away data. Still testing efficacy.
All checks were successful
Build and Update Repo / build-and-update-repo (push) Successful in 1m13s
2025-09-24 20:37:12 +01:00
51004f3017 Update syncshell chat.
All checks were successful
Build and Update Repo / build-and-update-repo (push) Successful in 1m12s
Update /ss to /ps
Remove whitespaces from sending in syncshell chat.
2025-09-21 17:39:40 +01:00
60c1668492 1.7.1.2
All checks were successful
Build and Update Repo / build-and-update-repo (push) Successful in 1m9s
2025-09-14 01:09:29 +01:00
4f07dba409 Update check for other clients to correctly find Snowcloak. 2025-09-12 19:42:13 +01:00
003fc77628 Update workflows. 2025-09-12 19:41:41 +01:00
41b5122e12 Once more, with feeling (I hate git actions).
All checks were successful
Release / tag-and-release (push) Successful in 1m12s
2025-09-12 16:12:00 +01:00
b9524dbe73 Fix funny typo 2025-09-12 16:06:25 +01:00
166fc0be91 Update tagging systewm
Some checks failed
Release / tag-and-release (push) Has been cancelled
2025-09-12 15:30:08 +01:00
6519ba6b6c Attempt #3 the remix 2025-09-12 15:22:28 +01:00
c1dbe3663e Update Workflow 2025-09-12 15:09:45 +01:00
e52fb6a452 Add back file random dating (and test runner)
Some checks failed
Release / tag-and-release (push) Has been cancelled
2025-09-12 14:52:07 +01:00
14ee5fe6ce Add gitea runner 2025-09-12 14:50:45 +01:00
6b7f9a6ea0 Rename IpcCallerMare, implement checking for Lightless/Snowcloak.
- Do not sync with pairs handled by Lightless/Snowcloak.
- Easy to add extra clients into this list.
2025-09-11 15:28:04 +01:00
74c83abe91 Update submodules. 2025-09-11 01:00:10 +01:00
00cc07efa4 Remove nefarious services that use Reflection, and intefere with other plugins.
Remove display of registration for main service.
2025-09-10 20:00:04 +01:00
4853afd1eb Added Color Table Hook from MemShield, currently Experimental and default on as a test. 2025-09-02 19:51:36 +01:00
5cac38f446 Update Penumbra.Api 2025-09-02 18:58:07 +01:00
30aab1739d Updated Moodles IPC 2025-09-02 11:34:46 +01:00
a7e8ff9a19 Remove extra code used for my personal message signing process :) 2025-08-30 20:47:03 +01:00
545b06aad6 PetNicknames API Update + removed code used for personal use. Added default json. 2025-08-30 20:40:29 +01:00
062d8bf6b2 Update default configuration, Update Version once more, with feeling. 2025-08-29 17:06:59 +01:00
a8a2d8a48f Updated Version Number 2025-08-29 16:54:31 +01:00
9b0134aa7d Update var typings in places, Update UI Colors. 2025-08-29 16:49:41 +01:00
243b0b8300 Update README.md 2025-08-26 21:44:50 +01:00
36 changed files with 457 additions and 960 deletions

View File

@@ -0,0 +1,97 @@
name: Build and Update Repo
on:
push:
tags:
- '*'
env:
PLUGIN_NAME: ClubPenguinSync
DOTNET_VERSION: 9.x
jobs:
build-and-update-repo:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Club Penguin
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x
- name: Download Dalamud
run: |
cd /
mkdir -p ~/.xlcore/dalamud/Hooks/dev
curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
unzip latest.zip -d ~/.xlcore/dalamud/Hooks/dev
- name: Build Club Penguin
run: |
dotnet restore
dotnet build --configuration Release --no-restore
- name: Prepare Club Penguin Repo
run: |
mkdir -p output
mv MareSynchronos/bin/x64/Release/ClubPenguinSync/latest.zip /repo/download/ClubPenguinSync-${{ gitea.ref_name }}.zip
- name: Update repo.json with version
env:
VERSION: ${{ gitea.ref_name }}
run: |
set -e
pluginJsonPath="MareSynchronos/bin/x64/Release/${PLUGIN_NAME}.json"
repoJsonPath="/repo/repo.json"
version="${VERSION}"
downloadUrl="https://clubpenguin.drgn.rocks/download/ClubPenguinSync-${{ gitea.ref_name }}.zip"
# Read plugin JSON
pluginJson=$(cat "$pluginJsonPath")
internalName=$(jq -r '.InternalName' <<< "$pluginJson")
dalamudApiLevel=$(jq -r '.DalamudApiLevel' <<< "$pluginJson")
# Read repo JSON (force array if not already)
repoJsonRaw=$(cat "$repoJsonPath")
if echo "$repoJsonRaw" | jq 'type' | grep -q '"array"'; then
repoJson="$repoJsonRaw"
else
repoJson="[$repoJsonRaw]"
fi
# Update matching plugin entry
updatedRepoJson=$(jq \
--arg internalName "$internalName" \
--arg dalamudApiLevel "$dalamudApiLevel" \
--arg version "$version" \
--arg downloadUrl "$downloadUrl" \
'
map(
if .InternalName == $internalName
then
.DalamudApiLevel = $dalamudApiLevel
| .TestingDalamudApiLevel = $dalamudApiLevel
| .AssemblyVersion = $version
| .TestingAssemblyVersion = $version
| .DownloadLinkInstall = $downloadUrl
| .DownloadLinkTesting = $downloadUrl
| .DownloadLinkUpdate = $downloadUrl
else
.
end
)
' <<< "$repoJson")
# Write back to file
echo "$updatedRepoJson" > "$repoJsonPath"
# Output the content of the file
cat "$repoJsonPath"

2
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "MareAPI"]
path = MareAPI
url = https://git.lop-sync.com/huggingway/LopAPI.git
url = https://git.drgn.rocks/t0w0bi/ClubPenguinApi.git
[submodule "Penumbra.Api"]
path = Penumbra.Api
url = https://github.com/Ottermandias/Penumbra.Api.git

View File

@@ -0,0 +1,14 @@
{
"Author": "tb",
"Name": "Club Penguin Sync",
"Punchline": "Social modding for cool penguins!",
"Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.",
"InternalName": "ClubPenguinSync",
"ApplicableVersion": "any",
"Tags": [
"customization"
],
"IconUrl": "https://clubpenguin.drgn.rocks/icon.png",
"RepoUrl": "https://git.drgn.rocks/t0w0bi/ClubPenguinClient/",
"CanUnloadAsync": true
}

View File

@@ -0,0 +1,106 @@
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using MareSynchronos.MareConfiguration;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Core;
using System.Buffers;
namespace MareSynchronos.Interop;
public unsafe sealed class ColorTableHook : IDisposable
{
// Based on https://github.com/Exter-N/MemShield/blob/main/MemShield/Plugin.cs
private readonly ILogger<ColorTableHook> _logger;
private const int ColorTableSize = 2048;
private const int ColorDyeTableSize = 128;
private readonly MareConfigService _configService;
#region signatures
#pragma warning disable CS0649
[Signature("E8 ?? ?? ?? ?? 49 89 04 3E",
DetourName = nameof(PrepareColorTableDetour))]
private Hook<MaterialResourceHandle.Delegates.PrepareColorTable> _prepareColorTable = null!;
#pragma warning restore CS0649
#endregion
public ColorTableHook(ILogger<ColorTableHook> logger, IGameInteropProvider gameInteropProvider, MareConfigService configService)
{
_logger = logger;
_configService = configService;
logger.LogInformation("Initializing ColorTableHook");
gameInteropProvider.InitializeFromAttributes(this);
if (_configService.Current.AttemptColorTableProtection)
{
_prepareColorTable.Enable();
}
}
public void Dispose()
{
if (_configService.Current.AttemptColorTableProtection)
{
_prepareColorTable.Disable();
}
_prepareColorTable.Dispose();
}
public void EnableHooks()
{
_prepareColorTable.Enable();
}
public void DisableHooks()
{
_prepareColorTable.Disable();
}
private static int GetDataSetExpectedSize(uint dataFlags)
=> (dataFlags & 16) != 0
? ColorTableSize + ((dataFlags & 8) != 0 ? ColorDyeTableSize : 0)
: 0;
private Texture* PrepareColorTableDetour(MaterialResourceHandle* @this, byte stain0Id, byte stain1Id)
{
if(!_configService.Current.AttemptColorTableProtection)
{
return _prepareColorTable.Original(@this, stain0Id, stain1Id);
}
var expectedSize = GetDataSetExpectedSize(@this->DataFlags);
if (@this->DataSetSize >= expectedSize)
return _prepareColorTable.Original(@this, stain0Id, stain1Id);
var originalDataSetPtr = @this->DataSet;
var originalDataSet = new ReadOnlySpan<byte>(originalDataSetPtr, @this->DataSetSize);
var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(expectedSize);
try
{
var bufferSpan = buffer.AsSpan(..expectedSize);
originalDataSet.CopyTo(bufferSpan);
bufferSpan[@this->DataSetSize..].Clear();
fixed (byte* bufferPtr = buffer)
{
@this->DataSet = bufferPtr;
try
{
return _prepareColorTable.Original(@this, stain0Id, stain1Id);
}
finally
{
@this->DataSet = originalDataSetPtr;
}
}
}
finally
{
pool.Return(buffer);
}
}
}

View File

@@ -3,6 +3,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
@@ -26,7 +27,7 @@ public unsafe sealed class GameChatHooks : IDisposable
// Based on https://git.anna.lgbt/anna/ExtraChat/src/branch/main/client/ExtraChat/GameFunctions.cs
private readonly ILogger<GameChatHooks> _logger;
private readonly Action<int, byte[]> _ssCommandHandler;
private readonly Action<int, byte[]> _psCommandHandler;
#region signatures
#pragma warning disable CS0649
@@ -130,10 +131,10 @@ public unsafe sealed class GameChatHooks : IDisposable
}
}
public GameChatHooks(ILogger<GameChatHooks> logger, IGameInteropProvider gameInteropProvider, Action<int, byte[]> ssCommandHandler)
public GameChatHooks(ILogger<GameChatHooks> logger, IGameInteropProvider gameInteropProvider, Action<int, byte[]> psCommandHandler)
{
_logger = logger;
_ssCommandHandler = ssCommandHandler;
_psCommandHandler = psCommandHandler;
logger.LogInformation("Initializing GameChatHooks");
gameInteropProvider.InitializeFromAttributes(this);
@@ -209,19 +210,41 @@ public unsafe sealed class GameChatHooks : IDisposable
if (isReply)
_nextMessageIsReply = utcNow + TimeSpan.FromMilliseconds(100);
// If it is a command, check if it begins with /ss first so we can handle the message directly
// If it is a command, check if it begins with /ps first so we can handle the message directly
// Letting Dalamud handle the commands causes all of the special payloads to be dropped
if (isCommand && messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/ss")))
if (isCommand && messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/ps")))
{
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
{
var cmdString = $"/ss{i} ";
var cmdString = $"/ps{i} ";
if (messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes(cmdString)))
{
var ssChatBytes = ProcessChatMessage(message);
ssChatBytes = ssChatBytes.Skip(cmdString.Length).ToArray();
_ssCommandHandler?.Invoke(i, ssChatBytes);
return;
var psChatBytes = ProcessChatMessage(message);
psChatBytes = psChatBytes.Skip(cmdString.Length).ToArray();
if (psChatBytes.Length > 0)
{
bool isBlank = true;
int j;
for (j = 0; j < psChatBytes.Length; j++)
{
if(!char.IsWhiteSpace((char)psChatBytes[j]))
{
isBlank = false;
break;
}
}
if (isBlank)
{
SendMessageHook!.OriginalDisposeSafe(thisPtr, message, uiModule);
return;
}
_psCommandHandler?.Invoke(i, psChatBytes);
return;
}
}
}
}

View File

@@ -14,6 +14,7 @@ public sealed class IpcCallerHonorific : IIpcCaller
private readonly ICallGateSubscriber<int, object> _honorificClearCharacterTitle;
private readonly ICallGateSubscriber<object> _honorificDisposing;
private readonly ICallGateSubscriber<string> _honorificGetLocalCharacterTitle;
private readonly ICallGateSubscriber<int, string> _honorificGetCharacterTitle;
private readonly ICallGateSubscriber<string, object> _honorificLocalCharacterTitleChanged;
private readonly ICallGateSubscriber<object> _honorificReady;
private readonly ICallGateSubscriber<int, string, object> _honorificSetCharacterTitle;
@@ -29,6 +30,7 @@ public sealed class IpcCallerHonorific : IIpcCaller
_dalamudUtil = dalamudUtil;
_honorificApiVersion = pi.GetIpcSubscriber<(uint, uint)>("Honorific.ApiVersion");
_honorificGetLocalCharacterTitle = pi.GetIpcSubscriber<string>("Honorific.GetLocalCharacterTitle");
_honorificGetCharacterTitle = pi.GetIpcSubscriber<int, string>("Honorific.GetCharacterTitle");
_honorificClearCharacterTitle = pi.GetIpcSubscriber<int, object>("Honorific.ClearCharacterTitle");
_honorificSetCharacterTitle = pi.GetIpcSubscriber<int, string, object>("Honorific.SetCharacterTitle");
_honorificLocalCharacterTitleChanged = pi.GetIpcSubscriber<string, object>("Honorific.LocalCharacterTitleChanged");
@@ -84,6 +86,23 @@ public sealed class IpcCallerHonorific : IIpcCaller
return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title));
}
public async Task<string> GetTitleForPlayer(IntPtr character)
{
if (!APIAvailable) return string.Empty;
string title = await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj is IPlayerCharacter pc)
{
return _honorificGetCharacterTitle.InvokeFunc(pc.ObjectIndex);
}
return string.Empty;
}).ConfigureAwait(false);
return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title));
}
public async Task SetTitleAsync(IntPtr character, string honorificDataB64)
{
if (!APIAvailable) return;

View File

@@ -1,44 +0,0 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Interop.Ipc;
public sealed class IpcCallerMare : DisposableMediatorSubscriberBase
{
private readonly ICallGateSubscriber<List<nint>> _mareHandledGameAddresses;
private readonly List<nint> _emptyList = [];
private bool _pluginLoaded;
public IpcCallerMare(ILogger<IpcCallerMare> logger, IDalamudPluginInterface pi, MareMediator mediator) : base(logger, mediator)
{
_mareHandledGameAddresses = pi.GetIpcSubscriber<List<nint>>("MareSynchronos.GetHandledAddresses");
_pluginLoaded = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false;
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "MareSynchronos", (msg) =>
{
_pluginLoaded = msg.IsLoaded;
});
}
public bool APIAvailable { get; private set; } = false;
// Must be called on framework thread
public IReadOnlyList<nint> GetHandledGameAddresses()
{
if (!_pluginLoaded) return _emptyList;
try
{
return _mareHandledGameAddresses.InvokeFunc();
}
catch
{
return _emptyList;
}
}
}

View File

@@ -27,9 +27,9 @@ public sealed class IpcCallerMoodles : IIpcCaller
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtr");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtr");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtr");
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
_moodlesOnChange.Subscribe(OnMoodlesChange);
@@ -47,7 +47,7 @@ public sealed class IpcCallerMoodles : IIpcCaller
{
try
{
APIAvailable = _moodlesApiVersion.InvokeFunc() == 1;
APIAvailable = _moodlesApiVersion.InvokeFunc() == 3;
}
catch
{

View File

@@ -0,0 +1,83 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Interop.Ipc;
public sealed class IpcCallerOtherSync : DisposableMediatorSubscriberBase
{
private readonly ICallGateSubscriber<List<nint>> _lightlessHandledGameAddresses;
private readonly ICallGateSubscriber<List<nint>> _snowcloakHandledGameAddresses;
private readonly List<nint> _emptyList = [];
private bool _lightlessLoaded;
private bool _snowcloakLoaded;
public IpcCallerOtherSync(ILogger<IpcCallerOtherSync> logger, IDalamudPluginInterface pi, MareMediator mediator) : base(logger, mediator)
{
_lightlessHandledGameAddresses = pi.GetIpcSubscriber<List<nint>>("LightlessSync.GetHandledAddresses");
_snowcloakHandledGameAddresses = pi.GetIpcSubscriber<List<nint>>("SnowcloakSync.GetHandledAddresses");
_lightlessLoaded = PluginWatcherService.GetInitialPluginState(pi, "LightlessSync")?.IsLoaded ?? false;
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "LightlessSync", (msg) =>
{
_lightlessLoaded = msg.IsLoaded;
});
_snowcloakLoaded = PluginWatcherService.GetInitialPluginState(pi, "Snowcloak")?.IsLoaded ?? false;
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "Snowcloak", (msg) =>
{
_snowcloakLoaded = msg.IsLoaded;
});
}
public bool APIAvailable { get; private set; } = false;
// Must be called on framework thread
public IReadOnlyList<nint> GetHandledGameAddresses()
{
if (!_lightlessLoaded && !_snowcloakLoaded) return _emptyList;
try
{
return GetLightlessHandledGameAddresses().Concat(GetSnowcloakHandledGameAddresses()).ToList();
}
catch
{
return _emptyList;
}
}
private List<nint> GetLightlessHandledGameAddresses()
{
if (!_lightlessLoaded) return _emptyList;
try
{
return _lightlessHandledGameAddresses.InvokeFunc();
}
catch
{
return _emptyList;
}
}
private List<nint> GetSnowcloakHandledGameAddresses()
{
if (!_snowcloakLoaded) return _emptyList;
try
{
return _snowcloakHandledGameAddresses.InvokeFunc();
}
catch
{
return _emptyList;
}
}
}

View File

@@ -231,7 +231,7 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa
return await _dalamudUtil.RunOnFrameworkThread(() =>
{
var collName = identity + "_" + uid;
var ec = _penumbraCreateNamedTemporaryCollection.Invoke(identity, collName, out var collId);
PenumbraApiEc ec = _penumbraCreateNamedTemporaryCollection.Invoke(identity, collName, out var collId);
if (ec == PenumbraApiEc.InvalidCredentials)
{
for (int i = 0; i < 5; ++i)

View File

@@ -30,12 +30,12 @@ public sealed class IpcCallerPetNames : IIpcCaller
_dalamudUtil = dalamudUtil;
_mareMediator = mareMediator;
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.Ready");
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.Disposing");
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.OnReady");
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.OnDisposing");
_apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion");
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.Enabled");
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.IsEnabled");
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.PlayerDataChanged");
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.OnPlayerDataChanged");
_getPlayerData = pi.GetIpcSubscriber<string>("PetRenamer.GetPlayerData");
_setPlayerData = pi.GetIpcSubscriber<string, object>("PetRenamer.SetPlayerData");
_clearPlayerData = pi.GetIpcSubscriber<ushort, object>("PetRenamer.ClearPlayerData");
@@ -56,7 +56,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
APIAvailable = _enabled?.InvokeFunc() ?? false;
if (APIAvailable)
{
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 };
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 };
}
}
catch

View File

@@ -5,7 +5,6 @@ using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -15,25 +14,12 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
{
private readonly ILogger<IpcProvider> _logger;
private readonly IDalamudPluginInterface _pi;
private readonly MareConfigService _mareConfig;
private readonly CharaDataManager _charaDataManager;
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider;
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
private ICallGateProvider<List<nint>>? _handledGameAddresses;
private readonly List<GameObjectHandler> _activeGameObjectHandlers = [];
private ICallGateProvider<string, IGameObject, bool>? _loadFileProviderMare;
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProviderMare;
private ICallGateProvider<List<nint>>? _handledGameAddressesMare;
private bool _marePluginEnabled = false;
private bool _impersonating = false;
private DateTime _unregisterTime = DateTime.UtcNow;
private CancellationTokenSource _registerDelayCts = new();
public bool MarePluginEnabled => _marePluginEnabled;
public bool ImpersonationActive => _impersonating;
public MareMediator Mediator { get; init; }
public IpcProvider(ILogger<IpcProvider> logger, IDalamudPluginInterface pi, MareConfigService mareConfig,
@@ -41,7 +27,6 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
{
_logger = logger;
_pi = pi;
_mareConfig = mareConfig;
_charaDataManager = charaDataManager;
Mediator = mareMediator;
@@ -55,15 +40,6 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
if (msg.OwnedObject) return;
_activeGameObjectHandlers.Remove(msg.GameObjectHandler);
});
/*
_marePluginEnabled = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false;
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "MareSynchronos", p => {
_marePluginEnabled = p.IsLoaded;
HandleMareImpersonation(automatic: true);
});
*/
_marePluginEnabled = true;
}
public Task StartAsync(CancellationToken cancellationToken)
@@ -76,72 +52,10 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("ClubPenguinSync.GetHandledAddresses");
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
_loadFileProviderMare = _pi.GetIpcProvider<string, IGameObject, bool>("MareSynchronos.LoadMcdf");
_loadFileAsyncProviderMare = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("MareSynchronos.LoadMcdfAsync");
_handledGameAddressesMare = _pi.GetIpcProvider<List<nint>>("MareSynchronos.GetHandledAddresses");
HandleMareImpersonation(automatic: true);
_logger.LogInformation("Started IpcProviderService");
return Task.CompletedTask;
}
public void HandleMareImpersonation(bool automatic = false)
{
if (_marePluginEnabled)
{
if (_impersonating)
{
_loadFileProviderMare?.UnregisterFunc();
_loadFileAsyncProviderMare?.UnregisterFunc();
_handledGameAddressesMare?.UnregisterFunc();
_impersonating = false;
_unregisterTime = DateTime.UtcNow;
_logger.LogDebug("Unregistered MareSynchronos API");
}
}
else
{
if (_mareConfig.Current.MareAPI)
{
var cancelToken = _registerDelayCts.Token;
_ = Task.Run(async () =>
{
// Wait before registering to reduce the chance of a race condition
if (automatic)
await Task.Delay(5000).ConfigureAwait(false);
if (cancelToken.IsCancellationRequested)
return;
if (_marePluginEnabled)
{
_logger.LogDebug("Not registering MareSynchronos API: Mare plugin is loaded");
return;
}
_loadFileProviderMare?.RegisterFunc(LoadMcdf);
_loadFileAsyncProviderMare?.RegisterFunc(LoadMcdfAsync);
_handledGameAddressesMare?.RegisterFunc(MareGetHandledAddresses);
_impersonating = true;
_logger.LogDebug("Registered MareSynchronos API");
}, cancelToken);
}
else
{
_registerDelayCts = _registerDelayCts.CancelRecreate();
if (_impersonating)
{
_loadFileProviderMare?.UnregisterFunc();
_loadFileAsyncProviderMare?.UnregisterFunc();
_handledGameAddressesMare?.UnregisterFunc();
_impersonating = false;
_unregisterTime = DateTime.UtcNow;
_logger.LogDebug("Unregistered MareSynchronos API");
}
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Stopping IpcProvider Service");
@@ -149,14 +63,6 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
_loadFileAsyncProvider?.UnregisterFunc();
_handledGameAddresses?.UnregisterFunc();
_registerDelayCts.Cancel();
if (_impersonating)
{
_loadFileProviderMare?.UnregisterFunc();
_loadFileAsyncProviderMare?.UnregisterFunc();
_handledGameAddressesMare?.UnregisterFunc();
}
Mediator.UnsubscribeAll(this);
return Task.CompletedTask;
}
@@ -186,19 +92,4 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
{
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
}
private List<nint> MareGetHandledAddresses()
{
if (!_impersonating)
{
if ((DateTime.UtcNow - _unregisterTime).TotalSeconds >= 1.0)
{
_logger.LogWarning("GetHandledAddresses called when it should not be registered");
_handledGameAddressesMare?.UnregisterFunc();
}
return [];
}
return GetHandledAddresses();
}
}

View File

@@ -31,7 +31,8 @@ public class MareConfig : IMareConfiguration
public bool LogPerformance { get; set; } = false;
public bool LogTraceLog { get; set; } = false;
public bool LogEvents { get; set; } = true;
public bool HoldCombatApplication { get; set; } = false;
public bool HoldCombatApplication { get; set; } = true;
public bool AttemptColorTableProtection { get; set; } = true;
public double MaxLocalCacheInGiB { get; set; } = 20;
public bool OpenGposeImportOnGposeStart { get; set; } = false;
public bool OpenPopupOnAdd { get; set; } = true;

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup>
<AssemblyName>ClubPenguinSync</AssemblyName>
<Version>1.7.0.0</Version>
<Version>1.7.1.4</Version>
<PackageProjectUrl>https://github.com/Rawrington/ClubPenguinSync/</PackageProjectUrl>
</PropertyGroup>

View File

@@ -27,14 +27,13 @@ public class PairHandlerFactory
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
private readonly VisibilityService _visibilityService;
private readonly NoSnapService _noSnapService;
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory,
MareConfigService configService, VisibilityService visibilityService, NoSnapService noSnapService)
MareConfigService configService, VisibilityService visibilityService)
{
_loggerFactory = loggerFactory;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
@@ -50,13 +49,12 @@ public class PairHandlerFactory
_pairAnalyzerFactory = pairAnalyzerFactory;
_configService = configService;
_visibilityService = visibilityService;
_noSnapService = noSnapService;
}
public PairHandler Create(Pair pair)
{
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService, _noSnapService);
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService);
}
}

View File

@@ -32,7 +32,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
private readonly ServerConfigurationManager _serverConfigManager;
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly VisibilityService _visibilityService;
private readonly NoSnapService _noSnapService;
private CancellationTokenSource? _applicationCancellationTokenSource = new();
private Guid _applicationId;
private Task? _applicationTask;
@@ -55,8 +54,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
FileCacheManager fileDbManager, MareMediator mediator,
PlayerPerformanceService playerPerformanceService,
ServerConfigurationManager serverConfigManager,
MareConfigService configService, VisibilityService visibilityService,
NoSnapService noSnapService) : base(logger, mediator)
MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
{
Pair = pair;
PairAnalyzer = pairAnalyzer;
@@ -70,7 +68,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_serverConfigManager = serverConfigManager;
_configService = configService;
_visibilityService = visibilityService;
_noSnapService = noSnapService;
_visibilityService.StartTracking(Pair.Ident);
@@ -152,7 +149,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
public string? PlayerName { get; private set; }
public string PlayerNameHash => Pair.Ident;
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
public async void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
{
if (_configService.Current.HoldCombatApplication && _dalamudUtil.IsInCombatOrPerforming)
{
@@ -178,7 +175,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}",
applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero);
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
this, forceApplyCustomization, forceApplyMods: false)
this, forceApplyCustomization, forceApplyMods: false, string.Empty)
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
_cachedData = characterData;
@@ -204,7 +201,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
$"Not applying character data: {reasons}")));
Logger.LogDebug("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons);
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
this, forceApplyCustomization, forceApplyMods: false)
this, forceApplyCustomization, forceApplyMods: false, _ipcManager.Honorific.GetTitleForPlayer(PlayerCharacter).GetAwaiter().GetResult())
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
_cachedData = characterData;
@@ -231,7 +228,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_forceApplyMods |= forceApplyCustomization;
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
string oldHonorificTitle = _ipcManager.Honorific.GetTitleForPlayer(PlayerCharacter).GetAwaiter().GetResult();
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods, oldHonorificTitle);
if (_charaHandler != null && _forceApplyMods)
{
@@ -332,7 +330,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
var gposeName = actor.Name.TextValue;
if (!name.Equals(gposeName, StringComparison.Ordinal))
continue;
_noSnapService.AddGposer(actor.ObjectIndex);
}
});
}
@@ -385,10 +382,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
}
}
}
else if (_dalamudUtil.IsInCutscene && !string.IsNullOrEmpty(name))
{
_noSnapService.AddGposerNamed(name);
}
}
catch (Exception ex)
{

View File

@@ -133,7 +133,7 @@ public class Pair : DisposableMediatorSubscriberBase
{
Name = name,
OnClicked = action,
PrefixColor = 529,
PrefixColor = 555,
PrefixChar = 'C'
});
}
@@ -215,11 +215,6 @@ public class Pair : DisposableMediatorSubscriberBase
if (_serverConfigurationManager.IsUidBlacklisted(UserData.UID))
HoldApplication("Blacklist", maxValue: 1);
if (NoSnapService.AnyLoaded)
HoldApplication("NoSnap", maxValue: 1);
else
UnholdApplication("NoSnap", skipApplication: true);
CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced);
}

View File

@@ -161,10 +161,10 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<CharaDataGposeTogetherManager>();
collection.AddSingleton<VfxSpawnManager>();
collection.AddSingleton<ColorTableHook>();
collection.AddSingleton<BlockedCharacterHandler>();
collection.AddSingleton<IpcProvider>();
collection.AddSingleton<VisibilityService>();
collection.AddSingleton<RepoChangeService>();
collection.AddSingleton<EventAggregator>();
collection.AddSingleton<DalamudUtilService>();
collection.AddSingleton<DtrEntry>();
@@ -178,10 +178,9 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IpcCallerMoodles>();
collection.AddSingleton<IpcCallerPetNames>();
collection.AddSingleton<IpcCallerBrio>();
collection.AddSingleton<IpcCallerMare>();
collection.AddSingleton<IpcCallerOtherSync>();
collection.AddSingleton<IpcManager>();
collection.AddSingleton<NotificationService>();
collection.AddSingleton<NoSnapService>();
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -247,8 +246,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
collection.AddHostedService(p => p.GetRequiredService<RepoChangeService>());
collection.AddHostedService(p => p.GetRequiredService<NoSnapService>());
})
.Build();

View File

@@ -13,20 +13,18 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly DalamudUtilService _dalamudUtilService;
private readonly IpcManager _ipcManager;
private readonly NoSnapService _noSnapService;
private readonly Dictionary<string, HandledCharaDataEntry> _handledCharaData = new(StringComparer.Ordinal);
public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _handledCharaData;
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
IpcManager ipcManager, NoSnapService noSnapService)
IpcManager ipcManager)
: base(logger, mediator)
{
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_dalamudUtilService = dalamudUtilService;
_ipcManager = ipcManager;
_noSnapService = noSnapService;
mediator.Subscribe<GposeEndMessage>(this, msg =>
{
foreach (var chara in _handledCharaData)
@@ -94,7 +92,6 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
_handledCharaData.Remove(handled.Name);
await _dalamudUtilService.RunOnFrameworkThread(async () =>
{
RemoveGposer(handled);
await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false);
}).ConfigureAwait(false);
return true;
@@ -103,7 +100,6 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
{
_handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry);
_ = _dalamudUtilService.RunOnFrameworkThread(() => AddGposer(handledCharaDataEntry));
}
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
@@ -134,23 +130,4 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
if (handler.Address == nint.Zero) return null;
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);
}
}

View File

@@ -72,7 +72,7 @@ public class ChatService : DisposableMediatorSubscriberBase
{
var chatMsg = message.ChatMsg;
var prefix = new SeStringBuilder();
prefix.AddText("[BnnuyChat] ");
prefix.AddText("[PenguinChat] ");
_chatGui.Print(new XivChatEntry{
MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent],
Name = chatMsg.SenderName,
@@ -121,7 +121,7 @@ public class ChatService : DisposableMediatorSubscriberBase
}
if (color != 0)
msg.AddUiForeground((ushort)color);
msg.AddText($"[SS{shellNumber}]<");
msg.AddText($"[PS{shellNumber}]<");
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
{
// Don't link to your own character
@@ -179,7 +179,7 @@ public class ChatService : DisposableMediatorSubscriberBase
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
{
// Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"PS [{shellNumber}]", StringComparison.Ordinal))
SwitchChatShell(shellNumber);
}
}
@@ -200,7 +200,7 @@ public class ChatService : DisposableMediatorSubscriberBase
// BUG: This doesn't always update the chat window e.g. when renaming a group
_gameChatHooks.Value.ChatChannelOverride = new()
{
ChannelName = $"SS [{shellNumber}]: {name}",
ChannelName = $"PS [{shellNumber}]: {name}",
ChatMessageHandler = chatBytes => SendChatShell(shellNumber, chatBytes)
};
return;

View File

@@ -1,5 +1,6 @@
using Dalamud.Game.Command;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
@@ -17,7 +18,7 @@ public sealed class CommandManagerService : IDisposable
private const string _commandName = "/sync";
private const string _commandName2 = "/clubpenguin";
private const string _ssCommandPrefix = "/ss";
private const string _psCommandPrefix = "/ps";
private readonly ApiController _apiController;
private readonly ICommandManager _commandManager;
@@ -52,7 +53,7 @@ public sealed class CommandManagerService : IDisposable
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
{
_commandManager.AddHandler($"{_ssCommandPrefix}{i}", new CommandInfo(OnChatCommand)
_commandManager.AddHandler($"{_psCommandPrefix}{i}", new CommandInfo(OnChatCommand)
{
ShowInHelp = false
});
@@ -65,7 +66,7 @@ public sealed class CommandManagerService : IDisposable
_commandManager.RemoveHandler(_commandName2);
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
_commandManager.RemoveHandler($"{_psCommandPrefix}{i}");
}
private void OnCommand(string command, string args)
@@ -139,9 +140,9 @@ public sealed class CommandManagerService : IDisposable
if (_mareConfigService.Current.DisableSyncshellChat)
return;
int shellNumber = int.Parse(command[_ssCommandPrefix.Length..]);
int shellNumber = int.Parse(command[_psCommandPrefix.Length..]);
if (args.Length == 0)
if (args.Length == 0 || args.IsNullOrWhitespace())
{
_chatService.SwitchChatShell(shellNumber);
}

View File

@@ -1,226 +0,0 @@
using Dalamud.Plugin;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Text.Json.Serialization;
namespace MareSynchronos.Services;
public sealed class NoSnapService : IHostedService, IMediatorSubscriber
{
private record NoSnapConfig
{
[JsonPropertyName("listOfPlugins")]
public string[]? ListOfPlugins { get; set; }
}
private readonly ILogger<NoSnapService> _logger;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Dictionary<string, bool> _listOfPlugins = new(StringComparer.Ordinal)
{
["QQSnapper"] = false,
["QQSnappy"] = false,
["QQMeddle.Plugin"] = false,
};
private static readonly HashSet<int> _gposers = new();
private static readonly HashSet<string> _gposersNamed = new(StringComparer.Ordinal);
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly DalamudUtilService _dalamudUtilService;
private readonly IpcManager _ipcManager;
private readonly RemoteConfigurationService _remoteConfig;
public static bool AnyLoaded { get; private set; } = false;
public static string ActivePlugins { get; private set; } = string.Empty;
public MareMediator Mediator { get; init; }
public NoSnapService(ILogger<NoSnapService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator,
IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager,
RemoteConfigurationService remoteConfig)
{
_logger = logger;
_pluginInterface = pluginInterface;
Mediator = mediator;
_hostApplicationLifetime = hostApplicationLifetime;
_dalamudUtilService = dalamudUtilService;
_ipcManager = ipcManager;
_remoteConfig = remoteConfig;
Mediator.Subscribe<GposeEndMessage>(this, msg => ClearGposeList());
Mediator.Subscribe<CutsceneEndMessage>(this, msg => ClearGposeList());
}
public void AddGposer(int objectIndex)
{
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
{
_logger.LogTrace("Immediately reverting object index {id}", objectIndex);
RevertAndRedraw(objectIndex);
return;
}
_logger.LogTrace("Registering gposer object index {id}", objectIndex);
lock (_gposers)
_gposers.Add(objectIndex);
}
public void RemoveGposer(int objectIndex)
{
_logger.LogTrace("Un-registering gposer object index {id}", objectIndex);
lock (_gposers)
_gposers.Remove(objectIndex);
}
public void AddGposerNamed(string name)
{
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
{
_logger.LogTrace("Immediately reverting {name}", name);
RevertAndRedraw(name);
return;
}
_logger.LogTrace("Registering gposer {name}", name);
lock (_gposers)
_gposersNamed.Add(name);
}
private void ClearGposeList()
{
if (_gposers.Count > 0 || _gposersNamed.Count > 0)
_logger.LogTrace("Clearing gposer list");
lock (_gposers)
_gposers.Clear();
lock (_gposersNamed)
_gposersNamed.Clear();
}
private void RevertAndRedraw(int objIndex, Guid applicationId = default)
{
if (applicationId == default)
applicationId = Guid.NewGuid();
try
{
_ipcManager.Glamourer.RevertNow(_logger, applicationId, objIndex);
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, objIndex);
}
catch { }
}
private void RevertAndRedraw(string name, Guid applicationId = default)
{
if (applicationId == default)
applicationId = Guid.NewGuid();
try
{
_ipcManager.Glamourer.RevertByNameNow(_logger, applicationId, name);
var addr = _dalamudUtilService.GetPlayerCharacterFromCachedTableByName(name);
if (addr != 0)
{
var obj = _dalamudUtilService.CreateGameObject(addr);
if (obj != null)
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, obj.ObjectIndex);
}
}
catch { }
}
private void RevertGposers()
{
List<int>? gposersList = null;
List<string>? gposersList2 = null;
lock (_gposers)
{
if (_gposers.Count > 0)
{
gposersList = _gposers.ToList();
_gposers.Clear();
}
}
lock (_gposersNamed)
{
if (_gposersNamed.Count > 0)
{
gposersList2 = _gposersNamed.ToList();
_gposersNamed.Clear();
}
}
if (gposersList == null && gposersList2 == null)
return;
_logger.LogInformation("Reverting gposers");
_dalamudUtilService.RunOnFrameworkThread(() =>
{
Guid applicationId = Guid.NewGuid();
foreach (var gposer in gposersList ?? [])
RevertAndRedraw(gposer, applicationId);
foreach (var gposerName in gposersList2 ?? [])
RevertAndRedraw(gposerName, applicationId);
}).GetAwaiter().GetResult();
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var config = await _remoteConfig.GetConfigAsync<NoSnapConfig>("noSnap").ConfigureAwait(false) ?? new();
if (config.ListOfPlugins != null)
{
_listOfPlugins.Clear();
foreach (var pluginName in config.ListOfPlugins)
_listOfPlugins.TryAdd(pluginName, value: false);
}
foreach (var pluginName in _listOfPlugins.Keys)
{
_listOfPlugins[pluginName] = PluginWatcherService.GetInitialPluginState(_pluginInterface, pluginName)?.IsLoaded ?? false;
Mediator.SubscribeKeyed<PluginChangeMessage>(this, pluginName, (msg) =>
{
_listOfPlugins[pluginName] = msg.IsLoaded;
_logger.LogDebug("{pluginName} isLoaded = {isLoaded}", pluginName, msg.IsLoaded);
Update();
});
}
Update();
}
public Task StopAsync(CancellationToken cancellationToken)
{
RevertGposers();
return Task.CompletedTask;
}
private void Update()
{
bool anyLoadedNow = _listOfPlugins.Values.Any(p => p);
if (AnyLoaded != anyLoadedNow)
{
AnyLoaded = anyLoadedNow;
Mediator.Publish(new RecalculatePerformanceMessage(null));
if (AnyLoaded)
{
RevertGposers();
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}.",
NotificationType.Error));
ActivePlugins = pluginList;
}
else
{
ActivePlugins = string.Empty;
}
}
}
}

View File

@@ -172,12 +172,6 @@ public sealed class RemoteConfigurationService
byte[] pub = Convert.FromBase64String(pubKey);
return Ed25519.Verify(sig, msg, pub);
}
private static byte[] SignForMe(string message, ulong ts, byte[] privKey)
{
byte[] msg = [.. BitConverter.GetBytes(ts), .. Encoding.UTF8.GetBytes(message)];
return Ed25519.Sign(msg, privKey);
}
private void LoadConfig(JsonObject jsonDoc)
{

View File

@@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace MareSynchronos.Services;
public record RepoChangeConfig
{
[JsonPropertyName("current_repo")]
public string? CurrentRepo { get; set; }
[JsonPropertyName("valid_repos")]
public string[]? ValidRepos { get; set; }
}

View File

@@ -1,401 +0,0 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Reflection;
namespace MareSynchronos.Services;
/* Reflection code based almost entirely on ECommons DalamudReflector
MIT License
Copyright (c) 2023 NightmareXIV
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
public sealed class RepoChangeService : IHostedService
{
#region Reflection Helpers
private const BindingFlags AllFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
private const BindingFlags StaticFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
private const BindingFlags InstanceFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
private static object GetFoP(object obj, string name)
{
Type? type = obj.GetType();
while (type != null)
{
var fieldInfo = type.GetField(name, AllFlags);
if (fieldInfo != null)
{
return fieldInfo.GetValue(obj)!;
}
var propertyInfo = type.GetProperty(name, AllFlags);
if (propertyInfo != null)
{
return propertyInfo.GetValue(obj)!;
}
type = type.BaseType;
}
throw new Exception($"Reflection GetFoP failed (not found: {obj.GetType().Name}.{name})");
}
private static T GetFoP<T>(object obj, string name)
{
return (T)GetFoP(obj, name);
}
private static void SetFoP(object obj, string name, object value)
{
var type = obj.GetType();
var field = type.GetField(name, AllFlags);
if (field != null)
{
field.SetValue(obj, value);
}
else
{
var prop = type.GetProperty(name, AllFlags)!;
if (prop == null)
throw new Exception($"Reflection SetFoP failed (not found: {type.Name}.{name})");
prop.SetValue(obj, value);
}
}
private static object? Call(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
{
MethodInfo? info;
var type = obj.GetType();
if (!matchExactArgumentTypes)
{
info = type.GetMethod(name, AllFlags);
}
else
{
info = type.GetMethod(name, AllFlags, @params.Select(x => x.GetType()).ToArray());
}
if (info == null)
throw new Exception($"Reflection Call failed (not found: {type.Name}.{name})");
return info.Invoke(obj, @params);
}
private static T Call<T>(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
{
return (T)Call(obj, name, @params, matchExactArgumentTypes)!;
}
#endregion
#region Dalamud Reflection
public object GetService(string serviceFullName)
{
return _pluginInterface.GetType().Assembly.
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType(serviceFullName, true)!).
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
}
private object GetPluginManager()
{
return _pluginInterface.GetType().Assembly.
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", true)!).
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
}
private void ReloadPluginMasters()
{
var mgr = GetService("Dalamud.Plugin.Internal.PluginManager");
var pluginReload = mgr.GetType().GetMethod("SetPluginReposFromConfigAsync", BindingFlags.Instance | BindingFlags.Public)!;
pluginReload.Invoke(mgr, [true]);
}
public void SaveDalamudConfig()
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var configSave = conf?.GetType().GetMethod("QueueSave", BindingFlags.Instance | BindingFlags.Public);
configSave?.Invoke(conf, null);
}
private IEnumerable<object> GetRepoByURL(string repoURL)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
yield return r;
}
}
private bool HasRepo(string repoURL)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private void AddRepo(string repoURL, bool enabled)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
return;
}
var instance = Activator.CreateInstance(_pluginInterface.GetType().Assembly.GetType("Dalamud.Configuration.ThirdPartyRepoSettings")!)!;
SetFoP(instance, "Url", repoURL);
SetFoP(instance, "IsEnabled", enabled);
GetFoP<System.Collections.IList>(conf, "ThirdRepoList").Add(instance!);
}
private void RemoveRepo(string repoURL)
{
var toRemove = new List<object>();
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IList)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
toRemove.Add(r);
}
foreach (var r in toRemove)
repolist.Remove(r);
}
public List<(object LocalPlugin, string InstalledFromUrl)> GetLocalPluginsByName(string internalName)
{
List<(object LocalPlugin, string RepoURL)> result = [];
var pluginManager = GetPluginManager();
var installedPlugins = (System.Collections.IList)pluginManager.GetType().GetProperty("InstalledPlugins")!.GetValue(pluginManager)!;
foreach (var plugin in installedPlugins)
{
if (((string)plugin.GetType().GetProperty("InternalName")!.GetValue(plugin)!).Equals(internalName, StringComparison.Ordinal))
{
var type = plugin.GetType();
if (type.Name.Equals("LocalDevPlugin", StringComparison.Ordinal))
continue;
var manifest = GetFoP(plugin, "manifest");
string installedFromUrl = (string)GetFoP(manifest, "InstalledFromUrl");
result.Add((plugin, installedFromUrl));
}
}
return result;
}
#endregion
private readonly ILogger<RepoChangeService> _logger;
private readonly RemoteConfigurationService _remoteConfig;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IFramework _framework;
public RepoChangeService(ILogger<RepoChangeService> logger, RemoteConfigurationService remoteConfig, IDalamudPluginInterface pluginInterface, IFramework framework)
{
_logger = logger;
_remoteConfig = remoteConfig;
_pluginInterface = pluginInterface;
_framework = framework;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting RepoChange Service");
var repoChangeConfig = await _remoteConfig.GetConfigAsync<RepoChangeConfig>("repoChange").ConfigureAwait(false) ?? new();
var currentRepo = repoChangeConfig.CurrentRepo;
var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList();
if (!currentRepo.IsNullOrEmpty() && !validRepos.Contains(currentRepo, StringComparer.Ordinal))
validRepos.Add(currentRepo);
if (validRepos.Count == 0)
{
_logger.LogInformation("No valid repos configured, skipping");
return;
}
await _framework.RunOnTick(() =>
{
try
{
var internalName = Assembly.GetExecutingAssembly().GetName().Name!;
var localPlugins = GetLocalPluginsByName(internalName);
var suffix = string.Empty;
if (localPlugins.Count == 0)
{
_logger.LogInformation("Skipping: No intalled plugin found");
return;
}
var hasValidCustomRepoUrl = false;
foreach (var vr in validRepos)
{
var vrCN = vr.Replace(".json", "_CN.json", StringComparison.Ordinal);
var vrKR = vr.Replace(".json", "_KR.json", StringComparison.Ordinal);
if (HasRepo(vr) || HasRepo(vrCN) || HasRepo(vrKR))
{
hasValidCustomRepoUrl = true;
break;
}
}
List<string> oldRepos = [];
var pluginRepoUrl = localPlugins[0].InstalledFromUrl;
if (pluginRepoUrl.Contains("_CN.json", StringComparison.Ordinal))
suffix = "_CN";
else if (pluginRepoUrl.Contains("_KR.json", StringComparison.Ordinal))
suffix = "_KR";
bool hasOldPluginRepoUrl = false;
foreach (var plugin in localPlugins)
{
foreach (var vr in validRepos)
{
var validRepo = vr.Replace(".json", $"{suffix}.json");
if (!plugin.InstalledFromUrl.Equals(validRepo, StringComparison.Ordinal))
{
oldRepos.Add(plugin.InstalledFromUrl);
hasOldPluginRepoUrl = true;
}
}
}
if (hasValidCustomRepoUrl)
{
if (hasOldPluginRepoUrl)
_logger.LogInformation("Result: Repo URL is up to date, but plugin install source is incorrect");
else
_logger.LogInformation("Result: Repo URL is up to date");
}
else
{
_logger.LogInformation("Result: Repo URL needs to be replaced");
}
if (currentRepo.IsNullOrEmpty())
{
_logger.LogWarning("No current repo URL configured");
return;
}
// Pre-test plugin repo url rewriting to ensure it succeeds before replacing the custom repo URL
if (hasOldPluginRepoUrl)
{
foreach (var plugin in localPlugins)
{
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
if (manifest == null)
throw new Exception("Plugin manifest is null");
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
if (manifestFile == null)
throw new Exception("Plugin manifestFile is null");
var repo = GetFoP(manifest, "InstalledFromUrl");
if (((string)repo).IsNullOrEmpty())
throw new Exception("Plugin repo url is null or empty");
SetFoP(manifest, "InstalledFromUrl", repo);
}
}
if (!hasValidCustomRepoUrl)
{
try
{
foreach (var oldRepo in oldRepos)
{
_logger.LogInformation("* Removing old repo: {r}", oldRepo);
RemoveRepo(oldRepo);
}
}
finally
{
_logger.LogInformation("* Adding current repo: {r}", currentRepo);
AddRepo(currentRepo, true);
}
}
// This time do it for real, and crash the game if we fail, to avoid saving a broken state
if (hasOldPluginRepoUrl)
{
try
{
_logger.LogInformation("* Updating plugins");
foreach (var plugin in localPlugins)
{
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
if (manifest == null)
throw new Exception("Plugin manifest is null");
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
if (manifestFile == null)
throw new Exception("Plugin manifestFile is null");
var repo = GetFoP(manifest, "InstalledFromUrl");
if (((string)repo).IsNullOrEmpty())
throw new Exception("Plugin repo url is null or empty");
SetFoP(manifest, "InstalledFromUrl", currentRepo);
Call(manifest, "Save", [manifestFile, "RepoChange"]);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while changing plugin install repo");
foreach (var oldRepo in oldRepos)
{
_logger.LogInformation("* Restoring old repo: {r}", oldRepo);
AddRepo(oldRepo, true);
}
}
}
if (!hasValidCustomRepoUrl || hasOldPluginRepoUrl)
{
_logger.LogInformation("* Saving dalamud config");
SaveDalamudConfig();
_logger.LogInformation("* Reloading plugin masters");
ReloadPluginMasters();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in RepoChangeService");
}
}, default, 10, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Started RepoChangeService");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_ = cancellationToken;
_logger.LogDebug("Stopping RepoChange Service");
return Task.CompletedTask;
}
}

View File

@@ -496,14 +496,14 @@ public class ServerConfigurationManager
private void EnsureMainExists()
{
bool lopExists = false;
bool clubExists = false;
for (int i = 0; i < _configService.Current.ServerStorage.Count; ++i)
{
var x = _configService.Current.ServerStorage[i];
if (x.ServerUri.Equals(ApiController.ClubPenguinServiceUri, StringComparison.OrdinalIgnoreCase))
lopExists = true;
clubExists = true;
}
if (!lopExists)
if (!clubExists)
{
_logger.LogDebug("Re-adding missing server {uri}", ApiController.ClubPenguinServiceUri);
_configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.ClubPenguinServiceUri, ServerName = ApiController.ClubPenguinServer });

View File

@@ -12,21 +12,21 @@ public class VisibilityService : DisposableMediatorSubscriberBase
{
NotVisible,
Visible,
MareHandled
OtherSyncHandled
};
private readonly DalamudUtilService _dalamudUtil;
private readonly ConcurrentDictionary<string, TrackedPlayerStatus> _trackedPlayerVisibility = new(StringComparer.Ordinal);
private readonly List<string> _makeVisibleNextFrame = new();
private readonly IpcCallerMare _mare;
private readonly HashSet<nint> cachedMareAddresses = new();
private readonly IpcCallerOtherSync _otherSync;
private readonly HashSet<nint> cachedOtherSyncAddresses = new();
private uint _cachedAddressSum = 0;
private uint _cachedAddressSumDebounce = 1;
public VisibilityService(ILogger<VisibilityService> logger, MareMediator mediator, IpcCallerMare mare, DalamudUtilService dalamudUtil)
public VisibilityService(ILogger<VisibilityService> logger, MareMediator mediator, IpcCallerOtherSync otherSync, DalamudUtilService dalamudUtil)
: base(logger, mediator)
{
_mare = mare;
_otherSync = otherSync;
_dalamudUtil = dalamudUtil;
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
}
@@ -44,19 +44,19 @@ public class VisibilityService : DisposableMediatorSubscriberBase
private void FrameworkUpdate()
{
var mareHandledAddresses = _mare.GetHandledGameAddresses();
var otherSyncHandledAddresses = _otherSync.GetHandledGameAddresses();
uint addressSum = 0;
foreach (var addr in mareHandledAddresses)
foreach (var addr in otherSyncHandledAddresses)
addressSum ^= (uint)addr.GetHashCode();
if (addressSum != _cachedAddressSum)
{
if (addressSum == _cachedAddressSumDebounce)
{
cachedMareAddresses.Clear();
foreach (var addr in mareHandledAddresses)
cachedMareAddresses.Add(addr);
cachedOtherSyncAddresses.Clear();
foreach (var addr in otherSyncHandledAddresses)
cachedOtherSyncAddresses.Add(addr);
_cachedAddressSum = addressSum;
}
else
@@ -69,11 +69,11 @@ public class VisibilityService : DisposableMediatorSubscriberBase
{
string ident = player.Key;
var findResult = _dalamudUtil.FindPlayerByNameHash(ident);
var isMareHandled = cachedMareAddresses.Contains(findResult.Address);
var isVisible = findResult.ObjectId != 0 && !isMareHandled;
var isOtherSyncHandled = cachedOtherSyncAddresses.Contains(findResult.Address);
var isVisible = findResult.ObjectId != 0 && !isOtherSyncHandled;
if (player.Value == TrackedPlayerStatus.MareHandled && !isMareHandled)
_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.NotVisible, comparisonValue: TrackedPlayerStatus.MareHandled);
if (player.Value == TrackedPlayerStatus.OtherSyncHandled && !isOtherSyncHandled)
_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.NotVisible, comparisonValue: TrackedPlayerStatus.OtherSyncHandled);
if (player.Value == TrackedPlayerStatus.NotVisible && isVisible)
{
@@ -85,17 +85,17 @@ public class VisibilityService : DisposableMediatorSubscriberBase
else
_makeVisibleNextFrame.Add(ident);
}
else if (player.Value == TrackedPlayerStatus.NotVisible && isMareHandled)
else if (player.Value == TrackedPlayerStatus.NotVisible && isOtherSyncHandled)
{
// Send a technically redundant visibility update with the added intent of triggering PairHandler to undo the application by name
if (_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.MareHandled, comparisonValue: TrackedPlayerStatus.NotVisible))
if (_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.OtherSyncHandled, comparisonValue: TrackedPlayerStatus.NotVisible))
Mediator.Publish<PlayerVisibilityMessage>(new(ident, IsVisible: false, Invalidate: true));
}
else if (player.Value == TrackedPlayerStatus.Visible && !isVisible)
{
var newTrackedStatus = isMareHandled ? TrackedPlayerStatus.MareHandled : TrackedPlayerStatus.NotVisible;
var newTrackedStatus = isOtherSyncHandled ? TrackedPlayerStatus.OtherSyncHandled : TrackedPlayerStatus.NotVisible;
if (_trackedPlayerVisibility.TryUpdate(ident, newValue: newTrackedStatus, comparisonValue: TrackedPlayerStatus.Visible))
Mediator.Publish<PlayerVisibilityMessage>(new(ident, IsVisible: false, Invalidate: isMareHandled));
Mediator.Publish<PlayerVisibilityMessage>(new(ident, IsVisible: false, Invalidate: isOtherSyncHandled));
}
if (!isVisible)

View File

@@ -105,7 +105,7 @@ public class CompactUi : WindowMediatorSubscriberBase
protected override void DrawInternal()
{
if (_serverManager.CurrentApiUrl.Equals(ApiController.ClubPenguinServiceUri, StringComparison.Ordinal))
UiSharedService.AccentColor = new Vector4(1.0f, 0.8666f, 0.06666f, 1.0f);
UiSharedService.AccentColor = new Vector4(0.70196078431f, 0.54901960784f, 1.0f, 1.0f);
else
UiSharedService.AccentColor = ImGuiColors.ParsedGreen;
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y - 1f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y);

View File

@@ -246,7 +246,7 @@ internal sealed class GroupPanel
if (!_mareConfig.Current.DisableSyncshellChat && shellConfig.Enabled)
{
ImGui.TextUnformatted($"[{shellNumber}]");
UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber);
UiSharedService.AttachToolTip("Chat command prefix: /ps" + shellNumber);
}
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
ImGui.SameLine();

View File

@@ -247,7 +247,7 @@ This service is provided as-is.
ImGui.BeginDisabled(_registrationInProgress || _uiShared.ApiController.ServerState == ServerState.Connecting || _uiShared.ApiController.ServerState == ServerState.Reconnecting);
_ = _uiShared.DrawServiceSelection(selectOnChange: true, intro: true);
if (true) // Enable registration button for all servers
if (_serverConfigurationManager.CurrentApiUrl == null || !_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.ClubPenguinServiceUri, StringComparison.Ordinal)) // Enable registration button for all servers
{
ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0);
ImGui.Separator();
@@ -297,6 +297,12 @@ This service is provided as-is.
ImGui.TextWrapped(_registrationMessage);
}
}
else
{
ImGui.Separator();
UiSharedService.TextWrapped("You must join the discord to register a Club Penguin Sync account on the main server.");
UiSharedService.TextWrapped("Use the /signup command to register an account within the discord follow the instructiosn in the #how-to-setup channel.");
}
ImGui.Separator();

View File

@@ -8,6 +8,7 @@ using Dalamud.Utility;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Comparer;
using MareSynchronos.FileCache;
using MareSynchronos.Interop;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
@@ -25,6 +26,7 @@ using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text.Json;
namespace MareSynchronos.UI;
@@ -50,6 +52,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly PlayerPerformanceService _playerPerformanceService;
private readonly AccountRegistrationService _registerService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly ColorTableHook _colorTableHook;
private readonly UiSharedService _uiShared;
private bool _deleteAccountPopupModalShown = false;
private string _lastTab = string.Empty;
@@ -77,11 +80,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
FileCacheManager fileCacheManager,
FileCompactor fileCompactor, ApiController apiController,
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mediator, "Club Penguin Sync Settings", performanceCollector)
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService, ColorTableHook colorTableHook) : base(logger, mediator, "Club Penguin Sync Settings", performanceCollector)
{
_configService = configService;
_pairManager = pairManager;
_chatService = chatService;
_colorTableHook = colorTableHook;
_guiHookService = guiHookService;
_serverConfigurationManager = serverConfigurationManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
@@ -581,43 +585,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.BigText("Advanced");
bool mareApi = _configService.Current.MareAPI;
using (var disabled = ImRaii.Disabled(true))
{
bool dummyFalse = false;
if (ImGui.Checkbox("Enable Mare Synchronos API", ref dummyFalse))
{
_configService.Current.MareAPI = mareApi;
_configService.Save();
_ipcProvider.HandleMareImpersonation();
}
_uiShared.DrawHelpText("Enables handling of the Mare Synchronos API. This currently includes:\n\n" +
" - MCDF loading support for other plugins\n" +
" - Blocking Moodles applications to paired users\n\n" +
"If the Mare Synchronos plugin is loaded while this option is enabled, control of its API will be relinquished.");
}
using (_ = ImRaii.PushIndent())
{
ImGui.SameLine(300.0f * ImGuiHelpers.GlobalScale);
UiSharedService.ColorTextWrapped("Mare API impersonation is currently unavailable", ImGuiColors.DalamudGrey2);
/*
if (_ipcProvider.ImpersonationActive)
{
UiSharedService.ColorTextWrapped("Mare API active!", ImGuiColors.HealerGreen);
}
else
{
if (!mareApi)
UiSharedService.ColorTextWrapped("Mare API inactive: Option is disabled", ImGuiColors.DalamudYellow);
else if (_ipcProvider.MarePluginEnabled)
UiSharedService.ColorTextWrapped("Mare API inactive: Mare plugin is loaded", ImGuiColors.DalamudYellow);
else
UiSharedService.ColorTextWrapped("Mare API inactive: Unknown reason", ImGuiColors.DalamudRed);
}
*/
}
bool logEvents = _configService.Current.LogEvents;
if (ImGui.Checkbox("Log Event Viewer data to disk", ref logEvents))
{
@@ -640,6 +607,22 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Save();
}
bool attemptColorTableProtection = _configService.Current.AttemptColorTableProtection;
if (ImGui.Checkbox("[EXPERIMENTAL] Attempt to fix potential ColorTable crashes.", ref attemptColorTableProtection))
{
if (attemptColorTableProtection)
{
_colorTableHook.EnableHooks();
}
else
{
_colorTableHook.DisableHooks();
}
//this shouldnt need the mediator as this is the ONLY source of truth.
_configService.Current.AttemptColorTableProtection = attemptColorTableProtection;
_configService.Save();
}
bool serializedApplications = _configService.Current.SerialApplication;
if (ImGui.Checkbox("Serialized player applications", ref serializedApplications))
{

View File

@@ -852,16 +852,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Club Penguin Sync.");
return false;
}
else if (NoSnapService.AnyLoaded)
{
IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
ImGui.SameLine();
var cursorX = ImGui.GetCursorPosX();
ImGui.TextColored(ImGuiColors.DalamudYellow, "Synced player appearances will not apply until incompatible plugins are disabled:");
ImGui.SetCursorPosX(cursorX + 16.0f);
ImGui.TextColored(ImGuiColors.DalamudYellow, NoSnapService.ActivePlugins);
return false;
}
return true;
}

View File

@@ -56,7 +56,7 @@ public static class VariousExtensions
}
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(this CharacterData newData, Guid applicationBase,
CharacterData? oldData, ILogger logger, PairHandler cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
CharacterData? oldData, ILogger logger, PairHandler cachedPlayer, bool forceApplyCustomization, bool forceApplyMods, string oldHonorificData)
{
oldData ??= new();
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
@@ -182,7 +182,7 @@ public static class VariousExtensions
charaDataToUpdate[objectKind].Add(PlayerChanges.Heels);
}
bool honorificDataDifferent = !string.Equals(oldData.HonorificData, newData.HonorificData, StringComparison.Ordinal);
bool honorificDataDifferent = !string.Equals(oldHonorificData, newData.HonorificData, StringComparison.Ordinal);
if (honorificDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.HonorificData)))
{
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff honorific data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Honorific);

View File

@@ -425,6 +425,18 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private void PersistFileToStorage(string fileHash, string filePath, long? compressedSize = null)
{
var fi = new FileInfo(filePath);
Func<DateTime> RandomDayInThePast()
{
DateTime start = new(1995, 1, 1, 1, 1, 1, DateTimeKind.Local);
Random gen = new();
int range = (DateTime.Today - start).Days;
return () => start.AddDays(gen.Next(range));
}
fi.CreationTime = RandomDayInThePast().Invoke();
fi.LastAccessTime = DateTime.Today;
fi.LastWriteTime = RandomDayInThePast().Invoke();
try
{
var entry = _fileDbManager.CreateCacheEntry(filePath, fileHash);

View File

@@ -1,3 +1,3 @@
# Loporrit Sync Dalamud Plugin
# Club Penguin Sync
Readme TBD
Readme L8r