├── .gitignore ├── .gitattributes ├── images └── icon.png ├── UsedName ├── packages.lock.json ├── UsedName.json ├── ExcelResolver.cs ├── GUI │ ├── SubscriptionWindow.cs │ ├── EditingWindow.cs │ ├── MainWindow.cs │ └── ConfigWindow.cs ├── Service.cs ├── Localization.cs ├── Plugin.cs ├── Translations.Designer.cs ├── UsedName.csproj ├── Commands.cs ├── Resources │ └── zh_CN.json ├── ContextMenu.cs ├── Translations.resx ├── Manager │ ├── PlayersNamesManager.cs │ └── GameDataManager.cs ├── Struct.cs └── Configuration.cs ├── UsedName.sln ├── README-CN.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | obj/ 3 | bin/ 4 | *.user -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittleNightmare/UsedName/HEAD/images/icon.png -------------------------------------------------------------------------------- /UsedName/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net9.0-windows7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[12.0.0, )", 8 | "resolved": "12.0.0", 9 | "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /UsedName/UsedName.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "LittleNightmare", 3 | "Name": "Used Name", 4 | "Punchline": "A simple tool to record players' used names and nickname", 5 | "Description": "A simple tool to record players' used names and nickname base on SocialList. Right click on the target to add/sub/check the used names and nickname.\n", 6 | "InternalName": "UsedName", 7 | "AssemblyVersion": "0.8.7.0", 8 | "DalamudApiLevel": 12, 9 | "RepoUrl": "https://github.com/LittleNightmare/UsedName", 10 | "ApplicableVersion": "any", 11 | "Tags": [ 12 | "social" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /UsedName.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32328.378 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UsedName", "UsedName\UsedName.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Release|x64 = Release|x64 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 15 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 16 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 17 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /UsedName/ExcelResolver.cs: -------------------------------------------------------------------------------- 1 | using Dalamud; 2 | using Dalamud.Game; 3 | using Lumina.Excel; 4 | 5 | namespace UsedName; 6 | 7 | public class ExcelResolver where T : struct, Lumina.Excel.IExcelRow 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The ID of the classJob. 13 | internal ExcelResolver(uint id) 14 | { 15 | this.Id = id; 16 | } 17 | 18 | /// 19 | /// Gets the ID to be resolved. 20 | /// 21 | public uint Id { get; } 22 | 23 | /// 24 | /// Gets GameData linked to this excel row. 25 | /// 26 | public T? GameData => Service.DataManager.GetExcelSheet()?.GetRow(this.Id); 27 | 28 | /// 29 | /// Gets GameData linked to this excel row with the specified language. 30 | /// 31 | /// The language. 32 | /// The ExcelRow in the specified language. 33 | public T? GetWithLanguage(ClientLanguage language) => Service.DataManager.GetExcelSheet(language)?.GetRow(this.Id); 34 | } -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # UsedName 2 | 3 | 🌎 [[English]](https://github.com/LittleNightmare/UsedName/blob/Memory/README.md) [**简体中文**] 4 | 5 | 一个针对FFXIV无备注名情况开发的插件。目前可以记录曾用名,添加昵称。可以自动更新曾用名(在打开对应列表时)支持范围包括:小队列表,好友名单,部队成员,以及订阅过的玩家 6 | 7 | ## 如何使用 8 | 9 | 使用'/pname update'或'/pname'更新好友列表 10 | 11 | 使用'/pname main'打开主窗口 12 | 13 | 使用'/pname sub'打开订阅窗口 14 | 15 | 使用'/pname search xxxx'搜索xxxx的曾用名。**建议**使用右键菜单搜索 16 | 17 | 使用'/pname nick xxxx aaaa'设置xxxx的昵称为aaaa,仅支持好友 18 | 19 | 使用'/pname config'显示插件设置 20 | 21 | ## 特殊使用案例 22 | 23 | ### 储存路人名字 24 | 25 | 你可以使用`UsedName`插件记录遇到的熟人和想要拉黑并记录理由的玩家 26 | 27 | #### 简略 28 | 29 | 1. 打开插件设置,勾选`启用自动更新`和子选项`启动订阅功能` 30 | 2. 右键玩家并选择`Subscribe`,将该玩家添加到订阅列表中。 31 | 3. 打开玩家搜索并搜索该玩家姓名。 32 | 4. 找到该玩家并订阅成功。当您打开除黑名单外的**任何窗口**时,将自动更新相关信息。 33 | 34 | #### 详细 35 | 36 | 1. 首先,从`/pname config`打开插件设置,勾选`启用自动更新`,和子选项`启动订阅功能`。同时你也可以设置一下`订阅按钮`的显示名字 37 | 2. 当遇到一个玩家时,右键该玩家,从弹出的菜单中选择`Subscription`或者您修改后的`订阅`按钮。这将在`/pname sub`打开的`订阅窗口`中,显示该玩家的名字 38 | 3. 打开玩家搜索,搜索该玩家的姓名 39 | 4. 当你通过玩家搜索找到玩家时,你订阅的玩家名字会从`订阅窗口`中消失,这意味着,你已经成功订阅了该玩家。从此时起,当你打开下列TODO中列出的**任何窗口**中(黑名单除外)存在订阅玩家时,将自动更新相关信息 40 | 41 | #### 使用场景 42 | * 你在游戏中遇到了一个很好的队友或其他玩家,但你因为种种原因不加好友,你可以使用订阅功能来记录他们的名字,以便以后联系他 43 | * 您在游戏中遇到了一个有问题的玩家,你可以使用订阅功能来记录他们的名字和原因,当你忘记时,还有通过查找插件中储存的玩家名找到他 44 | 45 | # 已知问题 46 | 目前没有,如果你有发现,请给我提一个Issue 47 | 48 | # TODO 49 | - [x] GUI能游戏内添加/记录昵称 50 | - [x] 给能有ContentID的地方,支持功能。~~事实上,我觉得目前的应该够了。~~ 目前可以通过订阅其他玩家来记录信息,下面是可以支持自动获取全部的列表。 如果你想要额外的,可以给我提个issue 51 | - [x] 好友列表 52 | - [x] 小队列表 53 | - [ ] 玩家搜索 54 | - [ ] 原始服务器在线成员 55 | - [x] 部队成员 56 | - [ ] 入队申请 57 | - [ ] 新人频道(指导者 & 新人/回归者) 58 | - [ ] 黑名单支持 59 | - [ ] 区分`找不到该角色`和普通跨服好友 60 | - [ ] 好友列表显示昵称 61 | 62 | # 感谢 63 | 感谢FFLogsViewer各个分支的参考,还有zhouhuichen741从远路把我拽回来。 64 | -------------------------------------------------------------------------------- /UsedName/GUI/SubscriptionWindow.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Windowing; 2 | using ImGuiNET; 3 | using System; 4 | using System.Linq; 5 | using System.Numerics; 6 | 7 | namespace UsedName.GUI; 8 | 9 | public class SubscriptionWindow : Window, IDisposable 10 | { 11 | 12 | public SubscriptionWindow() : base( 13 | "Used Name Subscription", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) 14 | { 15 | this.SizeConstraints = new WindowSizeConstraints 16 | { 17 | MinimumSize = new Vector2(200, 400), 18 | MaximumSize = new Vector2(float.MaxValue, float.MaxValue) 19 | }; 20 | 21 | } 22 | 23 | public void Dispose() 24 | { 25 | GC.SuppressFinalize(this); 26 | } 27 | 28 | // private string _searchContent = ""; 29 | 30 | public override void Draw() 31 | { 32 | if (ImGui.BeginTable($"SocialList##Subscription", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable)) 33 | { 34 | var index = 0; 35 | //TODO: not case sensitive 36 | var list = Service.PlayersNamesManager.Subscriptions.ToList(); 37 | foreach (var name in list) 38 | { 39 | ImGui.TableNextRow(); 40 | ImGui.TableNextColumn(); 41 | ImGui.Text(name); 42 | ImGui.TableNextColumn(); 43 | if (ImGui.Button("Remove".Loc() + $"##Sub{index}")) 44 | { 45 | Service.PlayersNamesManager.Subscriptions.Remove(name); 46 | } 47 | ImGui.TableNextColumn(); 48 | index++; 49 | } 50 | ImGui.EndTable(); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /UsedName/Service.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game; 2 | using Dalamud.IoC; 3 | using Dalamud.Plugin; 4 | //using XivCommon; 5 | using UsedName.GUI; 6 | using UsedName.Manager; 7 | using Dalamud.Plugin.Services; 8 | 9 | namespace UsedName 10 | { 11 | internal class Service 12 | { 13 | internal static Configuration Configuration { get; set; } = null!; 14 | internal static Commands Commands { get; set; } = null!; 15 | internal static GameDataManager GameDataManager { get; set; } = null!; 16 | internal static PlayersNamesManager PlayersNamesManager { get; set; } = null!; 17 | internal static MainWindow MainWindow { get; set; } = null!; 18 | internal static ConfigWindow ConfigWindow { get; set; } = null!; 19 | internal static EditingWindow EditingWindow { get; set; } = null!; 20 | internal static SubscriptionWindow SubscriptionWindow { get; set; } = null!; 21 | //internal static XivCommonBase Common { get; set; } = null!; 22 | internal static Localization Loc { get; set; } = null!; 23 | 24 | [PluginService] 25 | internal static IDalamudPluginInterface PluginInterface { get; private set; } = null!; 26 | [PluginService] 27 | internal static ISigScanner Scanner { get; private set; } = null!; 28 | [PluginService] 29 | internal static IClientState ClientState { get; private set; } = null!; 30 | [PluginService] 31 | internal static IDataManager DataManager { get; private set; } = null!; 32 | [PluginService] 33 | internal static IChatGui Chat { get; private set; } = null!; 34 | [PluginService] 35 | internal static ICommandManager CommandManager { get; private set; } = null!; 36 | [PluginService] 37 | internal static IGameInteropProvider GameInteropProvider { get; private set; } = null!; 38 | [PluginService] 39 | internal static IPluginLog PluginLog { get; private set; } = null!; 40 | [PluginService] 41 | internal static IContextMenu ContextMenu { get; set; } = null!; 42 | } 43 | } -------------------------------------------------------------------------------- /UsedName/GUI/EditingWindow.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Windowing; 2 | using ImGuiNET; 3 | using System; 4 | using System.Numerics; 5 | 6 | namespace UsedName.GUI; 7 | 8 | public class EditingWindow : Window, IDisposable 9 | { 10 | 11 | public EditingWindow() : base( 12 | "Used Name Editing", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) 13 | { 14 | this.SizeConstraints = new WindowSizeConstraints 15 | { 16 | MinimumSize = new Vector2(375, 330), 17 | MaximumSize = new Vector2(float.MaxValue, float.MaxValue) 18 | }; 19 | 20 | } 21 | 22 | public void Dispose() 23 | { 24 | GC.SuppressFinalize(this); 25 | } 26 | 27 | // Editing Window is opened by source which could provide 100% accurate ContentId 28 | public bool TrustOpen = false; 29 | 30 | public override void Draw() 31 | { 32 | ImGui.Text(Service.PlayersNamesManager.TempPlayerName + "'s current nick name:".Loc()); 33 | 34 | ulong targetID = Service.PlayersNamesManager.TempPlayerID; 35 | if (Service.Configuration.playersNameList.TryGetValue(targetID, out var tar1) && tar1.currentName == Service.PlayersNamesManager.TempPlayerName) 36 | { 37 | if (!TrustOpen) 38 | { 39 | // if not trust open, warning user that this is not 100% accurate 40 | ImGui.TextColored(new Vector4(1, 0, 0, 1), "WARNING: There may be other players with the same name\nPlease verify target before editing".Loc()); 41 | } 42 | var nickName = Service.Configuration.playersNameList[targetID].nickName; 43 | // var nickName = target.nickName; 44 | if (ImGui.InputText("##CurrentNickName", ref nickName, 250)) 45 | { 46 | Service.Configuration.playersNameList[targetID].nickName = nickName; 47 | Service.Configuration.StoreNames(); 48 | } 49 | } 50 | else 51 | { 52 | ImGui.Text(String.Format("NO PLAYER FOUND. Please makesure {0} is your friend.\nThen, try update FriendList".Loc(), Service.PlayersNamesManager.TempPlayerName)); 53 | ImGui.Spacing(); 54 | if (ImGui.Button("Update FriendList".Loc())) 55 | { 56 | //Service.GameDataManager.UpdateDataFromXivCommon(); 57 | Service.Chat.Print("The current function is not available"); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /UsedName/Localization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using System.IO; 5 | 6 | namespace UsedName 7 | { 8 | public static class LocalizationExtensions 9 | { 10 | public static string Loc(this string message) 11 | { 12 | return Service.Loc.Localize(message); 13 | } 14 | } 15 | 16 | class Localization 17 | { 18 | internal string currentLanguage; 19 | private Dictionary languageDict = new() { }; 20 | public Localization() 21 | { 22 | currentLanguage = Service.Configuration.Language; 23 | this.LoadLanguage(currentLanguage); 24 | } 25 | 26 | public string Localize(string message) 27 | { 28 | if (this.currentLanguage == "en") return message; 29 | if (languageDict.ContainsKey(message)) 30 | { 31 | return languageDict[message]; 32 | } 33 | #if DEBUG 34 | languageDict.Add(message, message); 35 | #endif 36 | return message; 37 | } 38 | internal void LoadLanguage(string language) 39 | { 40 | this.currentLanguage = language; 41 | if (language == "en") return; 42 | Translations.Culture = new System.Globalization.CultureInfo(language); 43 | var str = language switch{ 44 | _ => Translations.zh_CN 45 | }; 46 | try 47 | { 48 | this.languageDict = JsonConvert.DeserializeObject>(str); 49 | } 50 | catch (Exception) 51 | { 52 | #if DEBUG 53 | this.StoreLanguage(); 54 | #endif 55 | } 56 | 57 | 58 | 59 | } 60 | 61 | internal string[] GetLanguages() 62 | { 63 | string[] languages = { "en", "zh_CN" }; 64 | return languages; 65 | } 66 | 67 | #if DEBUG 68 | public void StoreLanguage() 69 | { 70 | var str = JsonConvert.SerializeObject(this.languageDict); 71 | string TempPath = @"F:\ffxiv\"; 72 | try 73 | { 74 | File.WriteAllText(Path.Combine(Path.GetDirectoryName(TempPath), this.currentLanguage + ".json"), str); 75 | } 76 | catch (Exception e) 77 | { 78 | 79 | } 80 | 81 | } 82 | 83 | #endif 84 | } 85 | } -------------------------------------------------------------------------------- /UsedName/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Windowing; 2 | using Dalamud.IoC; 3 | using Dalamud.Plugin; 4 | using System; 5 | using UsedName.GUI; 6 | using UsedName.Manager; 7 | // using XivCommon; 8 | 9 | namespace UsedName 10 | { 11 | public sealed class UsedName : IDalamudPlugin 12 | { 13 | 14 | private readonly WindowSystem windowSystem; 15 | 16 | 17 | public UsedName(IDalamudPluginInterface pluginInterface) 18 | { 19 | pluginInterface.Create(); 20 | 21 | Service.Configuration = Service.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); 22 | Service.Configuration.Initialize(); 23 | 24 | // Service.Common = new XivCommonBase(pluginInterface); 25 | 26 | 27 | Service.Loc = new Localization(); 28 | 29 | Service.PlayersNamesManager = new PlayersNamesManager(); 30 | Service.GameDataManager = new GameDataManager(); 31 | Service.Commands = new Commands(); 32 | this.windowSystem = new WindowSystem("UsedName"); 33 | 34 | Service.MainWindow = new MainWindow(); 35 | Service.ConfigWindow = new ConfigWindow(); 36 | Service.EditingWindow = new EditingWindow(); 37 | Service.SubscriptionWindow = new SubscriptionWindow(); 38 | 39 | this.windowSystem.AddWindow(Service.MainWindow); 40 | this.windowSystem.AddWindow(Service.ConfigWindow); 41 | this.windowSystem.AddWindow(Service.EditingWindow); 42 | this.windowSystem.AddWindow(Service.SubscriptionWindow); 43 | 44 | ContextMenu.Enable(); 45 | Service.PluginInterface.UiBuilder.OpenMainUi += DrawMainUI; 46 | Service.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; 47 | Service.PluginInterface.UiBuilder.Draw += DrawUI; 48 | 49 | } 50 | 51 | public void Dispose() 52 | { 53 | Service.Commands.Dispose(); 54 | Service.GameDataManager.Dispose(); 55 | //Service.Common.Dispose(); 56 | ContextMenu.Disable(); 57 | Service.Configuration.Save(storeName: true); 58 | 59 | Service.PluginInterface.UiBuilder.Draw -= DrawUI; 60 | Service.PluginInterface.UiBuilder.OpenConfigUi -= DrawConfigUI; 61 | Service.PluginInterface.UiBuilder.OpenMainUi -= DrawMainUI; 62 | #if DEBUG 63 | Service.Loc.StoreLanguage(); 64 | #endif 65 | GC.SuppressFinalize(this); 66 | } 67 | private void DrawUI() => this.windowSystem.Draw(); 68 | private void DrawConfigUI() 69 | { 70 | Service.ConfigWindow.IsOpen = true; 71 | } 72 | 73 | private void DrawMainUI() 74 | { 75 | Service.MainWindow.IsOpen = true; 76 | } 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /UsedName/Translations.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace UsedName { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class Translations { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Translations() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UsedName.Translations", typeof(Translations).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找类似 { 65 | /// "Update FriendList": "更新好友列表", 66 | /// "Language:": "语言:", 67 | /// "Name Change Check": "检查姓名变更", 68 | /// "Enable Search In Context": "启用右键菜单搜索", 69 | /// "Search in Context String": "搜索按钮", 70 | /// "Enable Add Nick Name": "启用右键添加昵称", 71 | /// "Add Nick Name String": "添加昵称按钮", 72 | /// "Show player who changed name when update FriendList": "更新好友列表时,显示姓名变更", 73 | /// "Update FriendList completed": "更新好友列表完成", 74 | /// "Parameter error, length is '{0}'": "参数错误,长度为'{0}'", 75 | /// "Invalid parameter: ": "非法参数: ", 76 | /// " changed name to ": [字符串的其余部分被截断]"; 的本地化字符串。 77 | /// 78 | public static string zh_CN { 79 | get { 80 | return ResourceManager.GetString("zh_CN", resourceCulture); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UsedName 2 | 3 | 🌎 [**English**] [简体中文](https://github.com/LittleNightmare/UsedName/blob/Memory/README-CN.md) 4 | 5 | A plugin developed for the case of FFXIV without a note name. Currently it is possible to record used names and add nicknames. The used names can be updated automatically (upon opening the corresponding list). Supported: Party List, Friend List, Company Member, and Subscribed Players 6 | 7 | ## How to use 8 | 9 | Use '/pname' or '/pname update' to update data from FriendList 10 | 11 | Use '/pname main' to open Main window 12 | 13 | Use '/pname sub' to open Subscription window 14 | 15 | Use '/pname search firstname lastname' to search 'firstname lastname's used name. I **recommend** using the right-click menu to search 16 | 17 | Use '/pname nick firstname lastname nickname' set 'firstname lastname's nickname to 'nickname', only support player from FriendList (Format require:first last nickname; first last nick name) 18 | 19 | Use '/pname config' show plugin's setting 20 | 21 | ## Special Use Case: 22 | 23 | ### Storing Names of Passersby 24 | 25 | You can use the `UsedName` plugin to record the names of acquaintances you have encountered, as well as players you wish to block and the reasons for doing so. 26 | 27 | #### Brief Instructions: 28 | 29 | 1. Open the plugin settings and check `Enable Auto update` and the sub-option `Enable Subscription` 30 | 2. Right-click on a player and select `Subscription` to add that player to your subscription list 31 | 3. Open the PlayerSearch and search for the player's name 32 | 4. Once you have found the player, you have successfully subscribed to them. The relevant information will automatically update whenever you open **any window** except for the blacklist 33 | 34 | #### Detailed Instructions: 35 | 36 | 1. Open the plugin settings by typing `/pname config` and check `Enable Auto update` and the sub-option "Enable Subscription" You can also set the display name of the `Subscription` button. 37 | 2. When you encounter a player, right-click on that player and select `Subscription` or your modified `Subscribe String`. This will display the player's name in the subscription window that opens with the command "/pname sub." 38 | 3. Open the PlayerSearch and search for the player's name. 39 | 4. Once you find the player, their name will disappear from the subscription window, indicating that you have successfully subscribed to them. From this point on, relevant information will automatically update whenever you open **any window** that listed in TODO except for the blacklist. 40 | 41 | #### Usage Scenarios: 42 | 43 | * You encounter a good teammate or other player in the game, but for various reasons, you don't want to add them as a friend. You can use the subscription function to record their name so that you can contact them later. 44 | * You encounter a problematic player in the game. You can use the subscription function to record their name and reason for blocking them. You can easily find them later by searching for their name in the plugin's stored player names. 45 | 46 | # Know Issue 47 | Not exist. If you find any problem, please make an Issue here 48 | 49 | # TODO 50 | - [x] Add/edit player's infomation through GUI 51 | - [x] Support everywhere with ContentID. ~~In fact, I think current support parts is enough.~~ Currently, you can record information by subscribing to other players, and the following is a list that can support automatic update to get all records of the list. If you want addition list, please write it in issue 52 | - [x] FriendList 53 | - [x] PartyList 54 | - [ ] PlayerSearch 55 | - [ ] Members Online and on Home World 56 | - [x] Company Member 57 | - [ ] Application of Company 58 | - [ ] Novice Network(Mentor & New Adventurer/Returner) 59 | - [ ] Support Blacklist 60 | - [ ] Distinguish `Unable to Retrieve` with noral cross world friend 61 | - [ ] Show nick name in FriendList 62 | 63 | # Thanks 64 | 65 | Thanks to each branch of FFLOGSViewer, and zhouhuichen741 pulled me back from far way 66 | -------------------------------------------------------------------------------- /UsedName/UsedName.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LittleNightmare 5 | 6 | 0.8.9.0 7 | a plugin to record your firend's used name 8 | 9 | https://github.com/LittleNightmare/UsedName 10 | Debug;Release 11 | true 12 | 13 | 14 | 15 | net9.0-windows 16 | x64 17 | enable 18 | latest 19 | true 20 | false 21 | true 22 | 23 | 24 | 25 | x64 26 | portable 27 | 28 | 29 | README.md 30 | icon.png 31 | 32 | 33 | 34 | false 35 | 36 | 37 | $(AppData)\XIVLauncher\addon\Hooks\dev\ 38 | 39 | 40 | $(DALAMUD_HOME) 41 | 42 | 43 | 44 | 45 | True 46 | \ 47 | 48 | 49 | True 50 | \ 51 | 52 | 53 | 54 | 55 | 56 | PreserveNewest 57 | false 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | $(DalamudLibPath)FFXIVClientStructs.dll 73 | false 74 | 75 | 76 | $(DalamudLibPath)Newtonsoft.Json.dll 77 | false 78 | 79 | 80 | $(DalamudLibPath)Dalamud.dll 81 | false 82 | 83 | 84 | $(DalamudLibPath)ImGui.NET.dll 85 | false 86 | 87 | 88 | $(DalamudLibPath)ImGuiScene.dll 89 | false 90 | 91 | 92 | $(DalamudLibPath)Lumina.dll 93 | false 94 | 95 | 96 | $(DalamudLibPath)Lumina.Excel.dll 97 | false 98 | 99 | 100 | 101 | 102 | 103 | True 104 | True 105 | Translations.resx 106 | 107 | 108 | 109 | 110 | 111 | PublicResXFileCodeGenerator 112 | Translations.Designer.cs 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /UsedName/Commands.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.Command; 2 | using System; 3 | 4 | namespace UsedName 5 | { 6 | public class Commands : IDisposable 7 | { 8 | private const string CommandName = "/pname"; 9 | 10 | public Commands() 11 | { 12 | Service.CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand) 13 | { 14 | HelpMessage = "Use '/pname' or '/pname update' to update data from FriendList\n".Loc() + 15 | "Use '/pname main' to open Main window\n".Loc() + 16 | "Use '/pname sub' show plugin's Subscription window\n".Loc() + 17 | "Use '/pname search firstname lastname' to search 'firstname lastname's used name. I **recommend** using the right-click menu to search\n".Loc() + 18 | "Use '/pname nick firstname lastname nickname' set 'firstname lastname's nickname to 'nickname'\n".Loc() + 19 | "(Format require:first last nickname; first last nick name)\n".Loc() + 20 | "Use '/pname config' show plugin's setting".Loc() 21 | }); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | Service.CommandManager.RemoveHandler(CommandName); 27 | } 28 | 29 | public void OnCommand(string command, string args) 30 | { 31 | if (args == "update") 32 | { 33 | //Service.GameDataManager.UpdateDataFromXivCommon(); 34 | Service.Chat.Print("Command `/pname update` currently not available".Loc()); 35 | 36 | } 37 | else if (args.StartsWith("search")) 38 | { 39 | var temp = args.Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); 40 | string targetName; 41 | if (temp.Length == 2) 42 | { 43 | targetName = temp[1]; 44 | } 45 | else if (temp.Length == 3) 46 | { 47 | targetName = temp[1] + " " + temp[2]; 48 | } 49 | else 50 | { 51 | Service.Chat.PrintError(string.Format("Parameter error, length is '{0}'".Loc(), temp.Length)); 52 | return; 53 | } 54 | Service.PlayersNamesManager.SearchPlayerResult(targetName); 55 | } 56 | else if (args.StartsWith("nick")) 57 | { 58 | // "PalyerName nickname", "Palyer Name nick name", "Palyer Name nickname" 59 | string[] parseName = ParseNameText(args.Substring(4)); 60 | string targetName = parseName[0]; 61 | string nickName = parseName[1]; 62 | Service.PlayersNamesManager.AddNickName(targetName, nickName); 63 | } 64 | else if (args.StartsWith("config")) 65 | { 66 | Service.ConfigWindow.Toggle(); 67 | } 68 | else if (args.StartsWith("main") || args == "") 69 | { 70 | Service.MainWindow.Toggle(); 71 | } 72 | else if (args.StartsWith("sub")) 73 | { 74 | Service.SubscriptionWindow.Toggle(); 75 | } 76 | else 77 | { 78 | Service.Chat.PrintError("Invalid parameter: ".Loc() + args); 79 | } 80 | } 81 | 82 | // name from command, try to solve "Palyer Name nick name", "Palyer Name nickname", not support "PalyerName nick name" 83 | public string[] ParseNameText(string text) 84 | { 85 | var playerName = ""; 86 | var nickName = ""; 87 | var name = text.Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); 88 | 89 | if (name.Length == 4) 90 | { 91 | playerName = name[0] + " " + name[1]; 92 | nickName = name[2] + " " + name[3]; 93 | } 94 | else if (name.Length == 3) 95 | { 96 | playerName = name[0] + " " + name[1]; 97 | nickName = name[2]; 98 | } 99 | else if (name.Length == 2) 100 | { 101 | playerName = name[0]; 102 | nickName = name[1]; 103 | } 104 | else if (name.Length == 1) 105 | { 106 | playerName = name[0]; 107 | } 108 | 109 | return new string[] { playerName, nickName }; 110 | 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /UsedName/Resources/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "Update FriendList": "更新好友列表", 3 | "Language:": "语言:", 4 | "Name Change Check": "检查姓名变更", 5 | "Enable Search In Context": "启用右键菜单搜索", 6 | "Search in Context String": "搜索按钮", 7 | "Enable Add Nick Name": "启用右键添加昵称", 8 | "Add Nick Name String": "添加昵称按钮", 9 | "Show player who changed name when update FriendList": "更新好友列表时,显示姓名变更", 10 | "Update FriendList completed": "更新好友列表完成", 11 | "Parameter error, length is '{0}'": "参数错误,长度为'{0}'", 12 | "Invalid parameter: ": "非法参数: ", 13 | " changed name to ": " 更改姓名为: ", 14 | "Search result(s) for target [{0}]:": "[{0}]的搜索结果:", 15 | "Cannot find player '{0}', Please try using '/pname update' to update FriendList, or check the spelling": "无法找到玩家'{0}',请尝试用'/pname update'更新好友列表,或检查拼写", 16 | "Find multiple '{0}', please search for players using the exact namet": "找到多个'{0}',请使用准确的名字搜索", 17 | "The nickname of {0} has been set to {1}": "{0}的昵称设置为{1}", 18 | "Used Name: Edit nick name": "Used Name: 修改昵称", 19 | "'s current nick name:": "的当前昵称:", 20 | "NO PLAYER FOUND. Please makesure {0} is your friend.\nThen, try update FriendList": "无法找到玩家。请确定 {0} 是你的好友\n并更新好友好友列表", 21 | "Enable Auto Update": "启用自动更新", 22 | "Automatically record the checked social list.\nOptionally visible when checked.\nAnd automatically get updates\nwhen the following lists are opened.": "自动记录勾选的社交列表\n可选项勾选后可见\n并在下列列表打开时\n自动获取更新", 23 | "Update From PartyList": "小队列表更新", 24 | "Update From FriendList": "好友列表更新", 25 | "Renew Opcode": "更新Opcode", 26 | "Update From PlayerSearch": "玩家搜索更新", 27 | "Auto Renew Opcode": "自动更新Opcode", 28 | "Manually renew for Opcode updates, please open PartyList after click button immediately to renew Opcode": "手动更新Opcode,请点击按钮后,立刻打开小队列表完成更新操作", 29 | "Auto renew Opcode after game update, please open PartyList to renew Opcode": "当游戏更新后,自动更新Opcode。请打开小队列表完成更新操作", 30 | "Opcode detected\n Game version: {0}\nOpcode: {1}": "检测到Opcode\n 游戏版本: {0}\nOpcode: {1}", 31 | "\nIf you find that the plugin cannot update the player information automatically, it may be that the detected opcode is wrong. Please click Renew Opcode to solve it": "\n如果您发现插件无法自动更新玩家信息,可能是检测到的opcode是错的。请点击 更新Opcode 来解决这个问题", 32 | "Use '/pname' or '/pname update' to update data from FriendList\n": "使用'/pname'或'/pname update'更新好友列表数据\n", 33 | "Use '/pname search firstname lastname' to search 'firstname lastname's used name. I **recommend** using the right-click menu to search\n": "使用'/pname search 玩家'寻找玩家的曾用名。 我**建议**使用右键菜单,右键玩家搜索\n", 34 | "Use '/pname nick firstname lastname nickname' set 'firstname lastname's nickname to 'nickname'\n": "使用'/pname nick 玩家 昵称'设置'玩家'的昵称为昵称\n", 35 | "(Format require:first last nickname; first last nick name)\n": "(格式要求: 玩家名字 昵称)\n", 36 | "Use '/pname config' show plugin's setting": "使用'/pname config'打开插件设置", 37 | "Update Player List Fail\nMay cause by incompatible version of XivCommon\nPlease contact to developer": "更新好友列表失败\n可能是由不兼容的XivCommon版本引起\n请联络开发人员", 38 | "Use '/pname main' to open Main window\n": "使用'/pname main'打开主窗口\n", 39 | "Open Main Window": "打开主窗口", 40 | "Holding LeftCtrl to Remove this record. It will be re-added on update base on your setting,\nbut will not contain the previous data (e.g. used names, nickname)": "按住 左Ctrl 移除这条记录. 它会根据你的设定重新添加,\n但不会有之前的数据(比如曾用名、昵称)", 41 | "Search:": "搜索:", 42 | "Enter player's name here": "输入玩家名", 43 | "CurrentName": "现用名", 44 | "NickName": "昵称", 45 | "FirstUsedName": "第一曾用名", 46 | "ShowMoreUsedName": "显示更多曾用名", 47 | "Edit": "编辑", 48 | "Remove": "移除", 49 | "Show": "显示", 50 | "Setting": "设置", 51 | "Modify Store Path": "更改储存路径", 52 | "Store Path:": "储存路径:", 53 | "Reset Path": "重置路径", 54 | "You modify path of storeNames.json, but there is other storeNames.json at orginal path\nPlease, delete one that you don't want to use after game close\n": "你修改了储存路径,但在默认路径仍有一个storeNNames.json\n请在关闭游戏后,删除一个你不想使用的", 55 | "Use '/pname sub' show plugin's Subscription window\n": "使用'/pname sub'打开订阅窗口\n", 56 | "Open Subscription Window": "打开订阅窗口", 57 | "Update From CompanyMember": "部队成员更新", 58 | "Enable Subscription": "启动订阅功能", 59 | "Subscription String": "订阅按钮", 60 | "Add a new subscription list.\nDuring updates, if a subscribed player's name exists,\nthis player will be added to the plugin's stored players'name list": "新增一个订阅列表\n在更新时,如果存在订阅的玩家名字,\n会将这个玩家添加到插件储存的好友列表中", 61 | "Added {0} to subscription list": "添加 {0} 到订阅列表", 62 | "PartyList": "小队列表", 63 | "FriendList": "好友名单", 64 | "LinkShell": "通讯贝", 65 | "PlayerSearch": "玩家搜索", 66 | "MembersOnlineAndOnHomeWorld": "原始服务器在线成员", 67 | "CompanyMember": "部队成员", 68 | "ApplicationOfCompany": "入队申请", 69 | "Mentor": "指导者", 70 | "NewAdventurer": "新人", 71 | "Players in the subscription list will be\nautomatically removed after successful capture of information\nor cleared when closing the game": "订阅名单里的玩家\n会在成功获取到信息后\n自动移除,或者在关闭游戏时清空", 72 | "WARNING: There may be other players with the same name\nPlease verify target before editing": "警告: 可能有同名玩家\n请谨慎编辑", 73 | "Successfully added {0} to plguin's player list": "{0} 成功添加到插件的好友列表", 74 | "Command `/pname update` currently not available": "命令`/pname update`目前不可用", 75 | "Show CID": "显示好友的CID", 76 | "Left click to copy": "左键点击以复制文本", 77 | "Show stored CID on the Main Window": "在主窗口显示储存的CID" 78 | } -------------------------------------------------------------------------------- /UsedName/ContextMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Dalamud.Game.Gui.ContextMenu; 4 | using Dalamud.Game.Text.SeStringHandling; 5 | using Lumina.Excel.Sheets; 6 | 7 | namespace UsedName; 8 | 9 | public class ContextMenu 10 | { 11 | public static void Enable() 12 | { 13 | Service.ContextMenu.OnMenuOpened -= OnOpenContextMenu; 14 | Service.ContextMenu.OnMenuOpened += OnOpenContextMenu; 15 | } 16 | 17 | public static void Disable() 18 | { 19 | Service.ContextMenu.OnMenuOpened -= OnOpenContextMenu; 20 | } 21 | 22 | private static bool IsMenuValid(IMenuArgs menuOpenedArgs) 23 | { 24 | if (menuOpenedArgs.Target is not MenuTargetDefault menuTargetDefault) 25 | { 26 | return false; 27 | } 28 | switch (menuOpenedArgs.AddonName) 29 | { 30 | case null: // Nameplate/Model menu 31 | case "LookingForGroup": 32 | case "PartyMemberList": 33 | case "FriendList": 34 | case "FreeCompany": 35 | case "SocialList": 36 | case "ContactList": 37 | case "ChatLog": 38 | case "_PartyList": 39 | case "LinkShell": 40 | case "CrossWorldLinkshell": 41 | case "ContentMemberList": // Eureka/Bozja/... 42 | case "BeginnerChatList": 43 | return menuTargetDefault.TargetName != null && menuTargetDefault.TargetHomeWorld.RowId != 0 && menuTargetDefault.TargetHomeWorld.RowId != 65535; 44 | case "BlackList": 45 | return menuTargetDefault.TargetName != string.Empty; 46 | 47 | default: 48 | return false; 49 | } 50 | } 51 | 52 | private static void OnOpenContextMenu(IMenuOpenedArgs menuOpenedArgs) 53 | { 54 | if (menuOpenedArgs.Target is not MenuTargetDefault menuTargetDefault) 55 | { 56 | return; 57 | } 58 | if (!IsMenuValid(menuOpenedArgs)) 59 | return; 60 | 61 | if (Service.Configuration.EnableSearchInContext) 62 | { 63 | menuOpenedArgs.AddMenuItem(new MenuItem 64 | { 65 | PrefixChar = 'U', 66 | Name = Service.Configuration.SearchString, 67 | OnClicked = Search 68 | }); 69 | } 70 | 71 | var playerName = (menuTargetDefault.TargetName ?? new SeString()).ToString(); 72 | var playerInPluginFriendList = Service.PlayersNamesManager.SearchPlayer(playerName).Count >= 1; 73 | 74 | if (Service.Configuration.EnableAddNickName) 75 | { 76 | menuOpenedArgs.AddMenuItem(new MenuItem 77 | { 78 | PrefixChar = 'U', 79 | Name = Service.Configuration.AddNickNameString, 80 | OnClicked = AddNickName 81 | }); 82 | } 83 | 84 | if (Service.Configuration.EnableSubscription && !playerInPluginFriendList && 85 | !Service.PlayersNamesManager.Subscriptions.Exists(x => x == playerName)) 86 | { 87 | menuOpenedArgs.AddMenuItem(new MenuItem 88 | { 89 | PrefixChar = 'U', 90 | Name = Service.Configuration.SubscriptionString, 91 | OnClicked = AddSubscription 92 | }); 93 | } 94 | } 95 | 96 | private static void AddNickName(IMenuArgs args) 97 | { 98 | if (args.Target is not MenuTargetDefault menuTargetDefault) 99 | { 100 | return; 101 | } 102 | var playerName = (menuTargetDefault.TargetName ?? new SeString()).ToString(); 103 | Service.PlayersNamesManager.TempPlayerName = playerName; 104 | var searchResult = Service.PlayersNamesManager.SearchPlayer(playerName); 105 | Service.PlayersNamesManager.TempPlayerID = searchResult.Count > 0 ? searchResult.First().Key : (ulong)0; 106 | Service.EditingWindow.TrustOpen = false; 107 | Service.EditingWindow.IsOpen = true; 108 | } 109 | 110 | private static void AddSubscription(IMenuArgs args) 111 | { 112 | if (args.Target is not MenuTargetDefault menuTargetDefault) 113 | { 114 | return; 115 | } 116 | var world = new ExcelResolver(menuTargetDefault.TargetHomeWorld.RowId); 117 | if (world == null) 118 | return; 119 | var playerName = (menuTargetDefault.TargetName ?? new SeString()).ToString(); 120 | if (string.IsNullOrEmpty(playerName)) 121 | return; 122 | Service.PlayersNamesManager.Subscriptions.Add(playerName); 123 | Service.PlayersNamesManager.Subscriptions.Sort(); 124 | Service.Chat.Print(String.Format("Added {0} to subscription list".Loc(), playerName)); 125 | } 126 | 127 | private static void Search(IMenuItemClickedArgs args) 128 | { 129 | if (args.Target is not MenuTargetDefault menuTargetDefault) 130 | { 131 | return; 132 | } 133 | var target = (menuTargetDefault.TargetName ?? new SeString()).ToString(); ; 134 | if (!string.IsNullOrEmpty(target)) 135 | { 136 | Service.PlayersNamesManager.SearchPlayerResult(target); 137 | } 138 | else 139 | { 140 | Service.Chat.PrintError("Cannot find".Loc()); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /UsedName/GUI/MainWindow.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Windowing; 2 | using ImGuiNET; 3 | using System; 4 | using System.Numerics; 5 | 6 | namespace UsedName.GUI; 7 | 8 | public class MainWindow : Window, IDisposable 9 | { 10 | 11 | public MainWindow() : base( 12 | "Used Name Main", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) 13 | { 14 | this.SizeConstraints = new WindowSizeConstraints 15 | { 16 | MinimumSize = new Vector2(375, 330), 17 | MaximumSize = new Vector2(float.MaxValue, float.MaxValue) 18 | }; 19 | 20 | } 21 | 22 | public void Dispose() 23 | { 24 | GC.SuppressFinalize(this); 25 | } 26 | 27 | private string[] TableColum 28 | { 29 | get 30 | { 31 | if (Service.Configuration.ShowCidInList) 32 | { 33 | return ["CID", "CurrentName", "NickName", "FirstUsedName", "ShowMoreUsedName", "Edit", "Remove"]; 34 | } 35 | 36 | return ["CurrentName", "NickName", "FirstUsedName", "ShowMoreUsedName", "Edit", "Remove"]; 37 | } 38 | } 39 | 40 | private string _searchContent = ""; 41 | 42 | public override void Draw() 43 | { 44 | if (ImGui.Button("Setting".Loc())) 45 | { 46 | Service.ConfigWindow.Toggle(); 47 | } 48 | ImGui.SameLine(); 49 | ImGui.Text("Search:".Loc()); 50 | ImGui.SameLine(); 51 | ImGui.SetNextItemWidth(200); 52 | ImGui.InputTextWithHint("##searchContent", "Enter player's name here".Loc(), ref _searchContent, 250); 53 | 54 | if (ImGui.BeginTable($"SocialList##{_searchContent}", TableColum.Length, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable)) 55 | { 56 | foreach (var t in TableColum) 57 | { 58 | var c = t.Loc(); 59 | ImGui.TableSetupColumn(c, ImGuiTableColumnFlags.None, c.Length); 60 | } 61 | ImGui.TableHeadersRow(); 62 | var index = 0; 63 | //TODO: not case sensitive 64 | foreach (var (id, player) in Service.PlayersNamesManager.SearchPlayer(_searchContent,true,false)) 65 | { 66 | ImGui.TableNextRow(); 67 | if (Service.Configuration.ShowCidInList) 68 | { 69 | ImGui.TableNextColumn(); 70 | ImGui.Text(id.ToString()); 71 | if (ImGui.IsItemHovered()) 72 | { 73 | ImGui.SetTooltip("Left click to copy".Loc()); 74 | if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) ImGui.SetClipboardText(id.ToString()); 75 | } 76 | } 77 | ImGui.TableNextColumn(); 78 | ImGui.Text(player.currentName); 79 | if (ImGui.IsItemHovered()) 80 | { 81 | ImGui.SetTooltip("Left click to copy".Loc()); 82 | if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) ImGui.SetClipboardText(player.currentName); 83 | } 84 | ImGui.TableNextColumn(); 85 | ImGui.Text(player.nickName); 86 | if (ImGui.IsItemHovered()) 87 | { 88 | ImGui.SetTooltip("Left click to copy".Loc()); 89 | if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) ImGui.SetClipboardText(player.nickName); 90 | } 91 | ImGui.TableNextColumn(); 92 | ImGui.Text(player.firstUsedname); 93 | if (ImGui.IsItemHovered()) 94 | { 95 | ImGui.SetTooltip("Left click to copy".Loc()); 96 | if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) ImGui.SetClipboardText(player.firstUsedname); 97 | } 98 | ImGui.TableNextColumn(); 99 | if (ImGui.Button("Show".Loc() + $"##{index}")) 100 | { 101 | var temp = string.IsNullOrEmpty(player.nickName) ? "" : "(" + player.nickName + ")"; 102 | Service.Chat.Print($"{player.currentName}{temp}: [{string.Join(",", player.usedNames)}]"); 103 | } 104 | ImGui.TableNextColumn(); 105 | if (ImGui.Button("Edit".Loc() + $"##{index}")) 106 | { 107 | Service.PlayersNamesManager.TempPlayerName = player.currentName; 108 | Service.PlayersNamesManager.TempPlayerID = id; 109 | Service.EditingWindow.TrustOpen = true; 110 | Service.EditingWindow.IsOpen = true; 111 | } 112 | ImGui.TableNextColumn(); 113 | if (ImGui.Button("Remove".Loc() + $"##{index}") && ImGui.IsKeyPressed(ImGuiKey.LeftCtrl)) 114 | { 115 | Service.PlayersNamesManager.RemovePlayer(id); 116 | } 117 | if (ImGui.IsItemHovered()) 118 | { 119 | ImGui.SetTooltip("Holding LeftCtrl to Remove this record. It will be re-added on update base on your setting,\nbut will not contain the previous data (e.g. used names, nickname)".Loc()); 120 | } 121 | ImGui.TableNextColumn(); 122 | index++; 123 | } 124 | ImGui.EndTable(); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /UsedName/Translations.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | Resources\zh_CN.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 123 | 124 | -------------------------------------------------------------------------------- /UsedName/Manager/PlayersNamesManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace UsedName.Manager 7 | { 8 | public class PlayersNamesManager 9 | { 10 | internal string? TempPlayerName; 11 | internal ulong TempPlayerID; 12 | 13 | public List Subscriptions = new List(); 14 | 15 | //public List NotInGameFriendListFriend() 16 | //{ 17 | // var currentFriendList = Service.GameDataManager.GetDataFromXivCommon(); 18 | // if (currentFriendList == null) return new List(); 19 | // var currentFriendListIDs = currentFriendList.Keys; 20 | // var savedFriendListIDs = Service.Configuration.playersNameList.Keys; 21 | // return savedFriendListIDs.Except(currentFriendListIDs).ToList(); 22 | //} 23 | 24 | public void UpdatePlayerNames(IDictionary currentPlayersList, IList newlyAddedPlayersFromSub, bool showHint = true) 25 | { 26 | var savedFriendList = Service.Configuration.playersNameList; 27 | bool same = true; 28 | 29 | foreach (var player in currentPlayersList) 30 | { 31 | var contentId = player.Key; 32 | var name = player.Value; 33 | if (savedFriendList.ContainsKey(contentId)) 34 | { 35 | if (!savedFriendList[contentId].currentName.Equals(name)) 36 | { 37 | same = false; 38 | if (Service.Configuration.ShowNameChange) 39 | { 40 | var temp = string.IsNullOrEmpty(savedFriendList[contentId].nickName) ? savedFriendList[contentId].currentName : $"({savedFriendList[contentId].nickName})"; 41 | Service.Chat.Print(temp + " changed name to ".Loc() + $"{name}"); 42 | } 43 | savedFriendList[contentId].usedNames.Add(savedFriendList[contentId].currentName); 44 | savedFriendList[contentId].currentName = name; 45 | } 46 | } 47 | else 48 | { 49 | same = false; 50 | savedFriendList.Add(contentId, new Configuration.PlayersNames(name, "", new List { })); 51 | } 52 | 53 | } 54 | if (!same) 55 | { 56 | Service.Configuration.playersNameList = savedFriendList; 57 | Service.Configuration.StoreNames(); 58 | } 59 | if (showHint) 60 | { 61 | if (newlyAddedPlayersFromSub.Count > 0) 62 | { 63 | foreach (var player in newlyAddedPlayersFromSub) 64 | { 65 | Service.Chat.Print(string.Format("Successfully added {0} to plguin's player list".Loc(), player)); 66 | } 67 | } 68 | 69 | Service.Chat.Print("Update FriendList completed".Loc()); 70 | } 71 | } 72 | 73 | public IDictionary SearchPlayer(string targetName, bool useNickName = false, bool strictCompare = true) 74 | { 75 | var result = new Dictionary(); 76 | targetName = targetName.ToLower(); 77 | bool Compare(string fullname, string nickName, List usedNames) 78 | { 79 | if (strictCompare) 80 | { 81 | return fullname.Equals(targetName) || (useNickName && nickName.Equals(targetName)) || 82 | usedNames.Any(name => name.Equals(targetName)); 83 | } 84 | return fullname.Contains(targetName) || (useNickName && nickName.ToLower().Contains(targetName)) || 85 | usedNames.Any(name => name.Contains(targetName)); 86 | } 87 | foreach (var player in Service.Configuration.playersNameList) 88 | { 89 | var current = player.Value.currentName.ToLower(); 90 | var nickName = player.Value.nickName.ToLower(); 91 | if (Compare(current, nickName, player.Value.usedNames)) 92 | { 93 | result.Add(player.Key, player.Value); 94 | } 95 | } 96 | return result; 97 | } 98 | 99 | public string SearchPlayerResult(string targetName) 100 | { 101 | StringBuilder resultBuilder = new StringBuilder(); 102 | foreach (var player in SearchPlayer(targetName, true, false)) 103 | { 104 | var temp = string.IsNullOrEmpty(player.Value.nickName) ? "" : "(" + player.Value.nickName + ")"; 105 | resultBuilder.Append($"{player.Value.currentName}{temp}: [{string.Join(",", player.Value.usedNames)}]\n"); 106 | } 107 | string result = resultBuilder.ToString(); 108 | Service.Chat.Print(string.Format("Search result(s) for target [{0}]:".Loc(), targetName) + $"\n{result}"); 109 | return result; 110 | } 111 | 112 | public void AddNickName(string playerName, string nickName) 113 | { 114 | var player = SearchPlayer(playerName); 115 | if (player.Count == 0) 116 | { 117 | Service.Chat.PrintError(string.Format("Cannot find player '{0}', Please try using '/pname update' to update FriendList, or check the spelling".Loc(), playerName)); 118 | return; 119 | } 120 | if (player.Count > 1) 121 | { 122 | Service.Chat.PrintError(string.Format("Find multiple '{0}', please search for players using the exact name".Loc(), playerName)); 123 | return; 124 | } 125 | Service.Configuration.playersNameList[player.First().Key].nickName = nickName; 126 | Service.Configuration.StoreNames(); 127 | Service.Chat.Print(string.Format("The nickname of {0} has been set to {1}".Loc(), playerName, nickName)); 128 | } 129 | 130 | 131 | public void RemovePlayer(ulong id) 132 | { 133 | Service.Chat.Print(string.Format("Remove player {0} from list".Loc(), Service.Configuration.playersNameList[id].currentName)); 134 | Service.Configuration.playersNameList.Remove(id); 135 | Service.Configuration.StoreNames(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /UsedName/Struct.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Utility; 2 | using Lumina.Excel.Sheets; 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using static FFXIVClientStructs.FFXIV.Client.UI.Info.InfoProxyCommonList.CharacterData; 8 | 9 | namespace UsedName.Structs; 10 | 11 | public enum ListType : byte 12 | { 13 | PartyList = 1, 14 | FriendList = 2, 15 | LinkShell = 3, 16 | PlayerSearch = 4, 17 | [Display(ShortName = "MembersOnline")] 18 | MembersOnlineAndOnHomeWorld = 5, 19 | CompanyMember = 6, 20 | ApplicationOfCompany = 7, 21 | Mentor = 12, 22 | NewAdventurer = 13 23 | } 24 | 25 | [StructLayout(LayoutKind.Explicit, Size = 0x470)] 26 | public unsafe struct SocialListResult 27 | { 28 | [FieldOffset(0x00)] public ulong CommunityID; 29 | [FieldOffset(0x08)] public ushort NextIndex; 30 | [FieldOffset(0x0A)] public ushort Index; 31 | [FieldOffset(0x0C)] public byte ListTypeByte; 32 | [FieldOffset(0x0D)] public byte RequestKey; 33 | [FieldOffset(0x0E)] public byte RequestParam; 34 | [FieldOffset(0x0F)] private byte __padding1; 35 | [FieldOffset(0x10)] 36 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] 37 | public CharacterEntry[] CharacterEntries; 38 | 39 | public ListType? ListType 40 | { 41 | get 42 | { 43 | if (Enum.IsDefined(typeof(ListType), ListTypeByte)) 44 | { 45 | return (ListType)ListTypeByte; 46 | } 47 | return null; 48 | } 49 | set { } 50 | } 51 | } 52 | 53 | 54 | 55 | [StructLayout(LayoutKind.Explicit, Size = 0x70, Pack = 1)] 56 | public unsafe struct CharacterEntry 57 | { 58 | [FieldOffset(0x00)] public ulong CharacterID; //LocalContentId 59 | // TODO: Check Timestamp if it still works, or i need merge Timestamp and TerritoryID to ulong 60 | //[FieldOffset(0x08)] public uint Timestamp; 61 | //[FieldOffset(0x0C)] public uint TerritoryID; 62 | //[FieldOffset(0x10)] public ulong PlatformAccountUniqueID; 63 | //[FieldOffset(0x18)] public uint HierarchyID; 64 | //[FieldOffset(0x1C)] public ushort TerritoryTypeID; 65 | //[FieldOffset(0x1E)] public ushort ContentFinderConditionID; 66 | //[FieldOffset(0x20)] public byte GrandCompanyID; 67 | //[FieldOffset(0x21)] public byte Region; 68 | //[FieldOffset(0x22)] public byte SelectRegion; 69 | //[FieldOffset(0x23)] public byte IsSearchComment; 70 | //[FieldOffset(0x28)] public ulong OnlineStatusBytes; 71 | //[FieldOffset(0x30)] public byte CurrentClassID; 72 | //[FieldOffset(0x31)] public byte SelectClassID; 73 | //[FieldOffset(0x32)] public ushort CurrentLevel; 74 | //[FieldOffset(0x34)] public ushort SelectLevel; 75 | //[FieldOffset(0x36)] public byte Identity; 76 | //[FieldOffset(0x37)] public byte MinionaireInCount; 77 | //[FieldOffset(0x38)] public byte MinionaireFlag; 78 | //[FieldOffset(0x39)] public byte __padding1; 79 | // [FieldOffset(0x08)] public uint Timestamp; 80 | // [FieldOffset(0x0C)] public uint TerritoryID; 81 | // [FieldOffset(0x10)] public byte HierarchyStatus; 82 | // [FieldOffset(0x11)] public byte HierarchyType; 83 | // [FieldOffset(0x12)] public byte HierarchyGroup; 84 | // [FieldOffset(0x13)] public byte IsDeleted; 85 | // [FieldOffset(0x14)] public ushort TerritoryTypeID; 86 | // [FieldOffset(0x16)] public byte GrandCompanyID; 87 | // [FieldOffset(0x17)] public byte Region; 88 | // [FieldOffset(0x18)] public byte SelectRegion; 89 | // [FieldOffset(0x19)] public byte IsSearchComment; 90 | // [FieldOffset(0x1A)] public byte __padding1; 91 | // [FieldOffset(0x1B)] public byte __padding2; 92 | // [FieldOffset(0x1C)] public ulong OnlineStatusBytes; 93 | // [FieldOffset(0x24)] public byte CurrentClassID; 94 | // [FieldOffset(0x25)] public byte SelectClassID; 95 | // [FieldOffset(0x26)] public ushort CurrentLevel; 96 | // [FieldOffset(0x28)] public ushort SelectLevel; 97 | // [FieldOffset(0x2A)] public byte Identity; 98 | 99 | /// 100 | /// Only seems to be set for certain kind of social lists, e.g. friend list/FC members doesn't include any. 101 | /// 102 | [FieldOffset(0x18)] public readonly ulong AccountId; 103 | 104 | [FieldOffset(0x24)] public ushort TerritoryTypeID; 105 | [FieldOffset(0x28)] public FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany GrandCompanyId; 106 | [FieldOffset(0x29)] public Language ClientLanguage; 107 | [FieldOffset(0x2A)] public LanguageMask Languages; 108 | [FieldOffset(0x2B)] public byte HasSearchComment; 109 | [FieldOffset(0x30)] public ulong OnlineStatusBytes; 110 | [FieldOffset(0x38)] public byte CurrentJobId; 111 | [FieldOffset(0x3A)] public byte CurrentJobLevel; 112 | [FieldOffset(0x42)] public ushort HomeWorldID; 113 | [FieldOffset(0x44)] private fixed byte CharacterNameBytes[32]; 114 | [FieldOffset(0x64)] private fixed byte FcTagBytes[7]; 115 | 116 | public unsafe string CharacterName 117 | { 118 | get 119 | { 120 | fixed (byte* ptr = this.CharacterNameBytes) 121 | { 122 | return Encoding.UTF8.GetString(ptr, 31).TrimEnd('\0'); 123 | } 124 | } 125 | set { } 126 | } 127 | 128 | public string FcTag 129 | { 130 | get 131 | { 132 | fixed (byte* ptr = this.FcTagBytes) 133 | { 134 | return Encoding.UTF8.GetString(ptr, 6).TrimEnd('\0'); 135 | } 136 | } 137 | set { } 138 | } 139 | public ExcelResolver CurrentClassJob => new(this.CurrentJobId); 140 | public ExcelResolver HomeWorld => new(this.HomeWorldID); 141 | //public ExcelResolver ContentFinderCondition => new(this.ContentFinderConditionID); 142 | public ExcelResolver? TerritoryType 143 | { 144 | get 145 | { 146 | if (this.TerritoryTypeID != 0) 147 | { 148 | return new ExcelResolver(this.TerritoryTypeID); 149 | } 150 | else 151 | { 152 | return null; 153 | } 154 | } 155 | set { } 156 | } 157 | 158 | //public List> OnlineStatus 159 | //{ 160 | // get 161 | // { 162 | // BitArray os = new BitArray(BitConverter.GetBytes(this.OnlineStatusBytes)); 163 | // var list = new List>(); 164 | // foreach (var i in os) 165 | // { 166 | // list.Add(new ExcelResolver((uint)i)); 167 | // } 168 | // return list; 169 | // } 170 | // set { } 171 | //} 172 | 173 | //override public string ToString() 174 | //{ 175 | // Type entryType = typeof(CharacterEntry); 176 | // FieldInfo[] fields = entryType.GetFields(BindingFlags.Public | BindingFlags.Instance); 177 | // StringBuilder sb = new StringBuilder(); 178 | // foreach (var field in fields) 179 | // { 180 | // sb.Append($"{field.Name}:{field.GetValue(this)} "); 181 | // } 182 | // sb.Append($"CharacterName:{this.CharacterName} "); 183 | // sb.Append($"FcTag:{this.FcTag} "); 184 | // _ = sb.Append($"CurrentClassJob:{CurrentClassJob.GameData.NameEnglish.ToString()} "); 185 | // // sb.Append($"OnlineStatus:{string.Join(", ", this.OnlineStatus)} "); 186 | // _ = sb.Append($"ContentFinderCondition:{ContentFinderCondition.GameData.Name.ToString()} "); 187 | // _ = sb.Append($"TerritoryType:{this.TerritoryType?.GameData.PlaceName.Value.Name.ToString()} "); 188 | // return sb.ToString(); 189 | //} 190 | } 191 | public sealed class Character 192 | { 193 | internal CharacterEntry Struct; 194 | public byte ListType; 195 | internal Character(CharacterEntry entry, byte listType) 196 | { 197 | this.Struct = entry; 198 | this.ListType = listType; 199 | } 200 | 201 | //public DateTime? LocalDateTime 202 | //{ 203 | // get 204 | // { 205 | // if (this.ListType == 6 || this.ListType == 2) 206 | // { 207 | // //PluginLog.Log($"{this.Struct.TimeStamp}"); 208 | // return DateTimeOffset.FromUnixTimeSeconds((uint)this.Struct.Timestamp).LocalDateTime; 209 | // //return null; 210 | // } 211 | // else return null; 212 | // } 213 | // set { } 214 | //} 215 | 216 | public unsafe void DumpBytes() 217 | { 218 | var mem = stackalloc byte[0x68]; 219 | Marshal.StructureToPtr(this.Struct, (IntPtr)mem, false); 220 | Util.DumpMemory((IntPtr)mem, 0x68); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /UsedName/Configuration.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Configuration; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text.Encodings.Web; 7 | using System.Text.Json; 8 | using System.Linq; 9 | using Dalamud.Game; 10 | using Dalamud.Utility; 11 | 12 | namespace UsedName 13 | { 14 | [Serializable] 15 | public class Configuration : IPluginConfiguration 16 | { 17 | public int Version { get; set; } = 3; 18 | 19 | public string? Language = null; 20 | 21 | public bool ShowNameChange = true; 22 | 23 | public bool EnableSearchInContext = true; 24 | public string SearchString = "Search Used Name"; 25 | 26 | public bool EnableAddNickName = true; 27 | public string AddNickNameString = "Add Nick Name"; 28 | 29 | public bool EnableAutoUpdate = false; 30 | public bool UpdateFromPartyList = false; 31 | public bool UpdateFromFriendList = true; 32 | public bool UpdateFromCompanyMember = false; 33 | public bool UpdateFromPlayerSearch = false; 34 | public bool ShowCidInList = false; 35 | 36 | 37 | public class PlayersNames 38 | { 39 | public string currentName { get; set; } 40 | 41 | public string nickName { get; set; } 42 | public List usedNames { get; set; } 43 | 44 | [JsonIgnore] 45 | public string firstUsedname 46 | { 47 | get 48 | { 49 | return this.usedNames.Where(n => !n.IsNullOrEmpty()).ToList().Count >= 1 ? this.usedNames.First(n => !n.IsNullOrEmpty()) : ""; 50 | } 51 | set { } 52 | } 53 | 54 | public PlayersNames(string CurrentName, string NickName, List UsedNames) 55 | { 56 | this.nickName = NickName; 57 | this.currentName = CurrentName; 58 | this.usedNames = UsedNames; 59 | } 60 | 61 | } 62 | [JsonIgnore] 63 | public IDictionary playersNameList = new Dictionary(); 64 | // Separate data and setting 65 | [JsonProperty("playersNameList")] 66 | private IDictionary playersNameListTemp 67 | { 68 | // get is intentionally omitted here 69 | set { playersNameList = value; } 70 | } 71 | 72 | public bool EnableSubscription = false; 73 | public string SubscriptionString = "Subscription"; 74 | 75 | public bool modifyStorePath = false; 76 | public string storeNamesPath = String.Empty; 77 | 78 | 79 | // the below exist just to make saving less cumbersome 80 | 81 | public void Initialize() 82 | { 83 | if (string.IsNullOrEmpty(this.Language)) 84 | { 85 | this.Language = Service.ClientState.ClientLanguage switch 86 | { 87 | ClientLanguage.English => "en", 88 | ClientLanguage.Japanese => "en", 89 | ClientLanguage.French => "en", 90 | ClientLanguage.German => "en", 91 | // ClientLanguage.ChineseSimplified only exist in CN ver of dalamud 92 | // ClientLanguage.ChineseSimplified => "zh_CN", 93 | _ => "zh_CN", 94 | }; 95 | } 96 | var defaultPath = Path.Join(Service.PluginInterface.ConfigDirectory.FullName, "storeNames.json"); 97 | if (String.IsNullOrEmpty(storeNamesPath)) 98 | { 99 | Service.PluginInterface.ConfigDirectory.Create(); 100 | storeNamesPath = defaultPath; 101 | } 102 | // TODO cannot keep user data due to not completely move configs e.g. user only move UsedName.json not include storeNames.json 103 | // Check path problem 104 | if (!defaultPath.Equals(storeNamesPath)) 105 | { 106 | if (modifyStorePath) 107 | { 108 | if (File.Exists(defaultPath) && File.Exists(storeNamesPath)) 109 | { 110 | var hint = "You modify path of storeNames.json, but there is other storeNames.json at orginal path\n" + 111 | "Please, delete one that you don't want to use after game close\n".Loc() + 112 | $"Your Path(Current Loading): {storeNamesPath}\nOrginal Path:{defaultPath}"; 113 | Service.PluginLog.Warning(hint); 114 | Service.Chat.PrintError(hint); 115 | } 116 | } 117 | else 118 | { 119 | // it may happen when user move Dalamud to another place. assume as good user 120 | storeNamesPath = defaultPath; 121 | 122 | } 123 | } 124 | // if not exist, it means old version config. The old playerNameList would load. Just save it. 125 | if (File.Exists(storeNamesPath)) 126 | { 127 | LoadStoreName(); 128 | } 129 | Save(storeName: true); 130 | } 131 | 132 | private void LoadStoreName() 133 | { 134 | using (StreamReader r = new StreamReader(storeNamesPath)) 135 | { 136 | string json = r.ReadToEnd(); 137 | // init load playersNameList would be empty. it is ok, just load here 138 | if (playersNameList.Equals(new Dictionary())) 139 | { 140 | playersNameList = System.Text.Json.JsonSerializer.Deserialize>(json); 141 | } 142 | else 143 | { 144 | // not empty means playerNameList not only contain in old config, but also new config. So, merge them. 145 | var temp = System.Text.Json.JsonSerializer.Deserialize>(json); 146 | if (temp != null && !temp.Equals((new Dictionary()))) 147 | { 148 | playersNameList = mergePlayerList(playersNameList, temp); 149 | } 150 | } 151 | } 152 | } 153 | 154 | public void Save(bool storeName = false) 155 | { 156 | if (storeName) 157 | { 158 | StoreNames(); 159 | } 160 | Service.PluginInterface!.SavePluginConfig(this); 161 | } 162 | 163 | public void StoreNames() 164 | { 165 | string jsonString = System.Text.Json.JsonSerializer.Serialize(playersNameList, new JsonSerializerOptions() { WriteIndented = true, Encoder= JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); 166 | using (StreamWriter outputFile = new StreamWriter(storeNamesPath, false, System.Text.Encoding.UTF8)) 167 | { 168 | outputFile.WriteLine(jsonString); 169 | } 170 | } 171 | 172 | internal IDictionary mergePlayerList(IDictionary main, IDictionary sub) 173 | { 174 | var result = main; 175 | foreach(var item in sub) 176 | { 177 | if (!result.ContainsKey(item.Key)) 178 | { 179 | result.Add(item.Key, item.Value); 180 | }else if(result[item.Key].Equals(item.Value)) 181 | { 182 | continue; 183 | } 184 | else 185 | { 186 | if (result[item.Key].currentName != item.Value.currentName) 187 | { 188 | if(item.Value.usedNames.Contains(result[item.Key].currentName)) 189 | { 190 | result[item.Key] = item.Value; 191 | } 192 | 193 | } 194 | var tempUsedName = result[item.Key].usedNames; 195 | tempUsedName.AddRange(item.Value.usedNames); 196 | result[item.Key].usedNames = tempUsedName.Distinct().ToList(); 197 | if (result[item.Key].nickName != item.Value.nickName) 198 | { 199 | if (String.IsNullOrEmpty(result[item.Key].nickName)) 200 | { 201 | result[item.Key].nickName = item.Value.nickName; 202 | } 203 | 204 | // use result[item.Key].nickName in other situations 205 | } 206 | } 207 | } 208 | return result; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /UsedName/GUI/ConfigWindow.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Windowing; 2 | using ImGuiNET; 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Numerics; 7 | using UsedName.Structs; 8 | 9 | namespace UsedName.GUI 10 | { 11 | internal class ConfigWindow : Window, IDisposable 12 | { 13 | public ConfigWindow() : base( 14 | "Used Name Settings", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) 15 | { 16 | this.SizeConstraints = new WindowSizeConstraints 17 | { 18 | MinimumSize = new Vector2(375, 330), 19 | MaximumSize = new Vector2(float.MaxValue, float.MaxValue) 20 | }; 21 | } 22 | 23 | public void Dispose() 24 | { 25 | GC.SuppressFinalize(this); 26 | } 27 | 28 | public override void Draw() 29 | { 30 | if (ImGui.Button("Update FriendList".Loc())) 31 | { 32 | //Service.GameDataManager.UpdateDataFromXivCommon(); 33 | Service.Chat.Print("The current function is not available"); 34 | } 35 | ImGui.SameLine(); 36 | if (ImGui.Button("Open Main Window".Loc())) 37 | { 38 | Service.MainWindow.Toggle(); 39 | } 40 | ImGui.SameLine(); 41 | if (ImGui.Button("Open Subscription Window".Loc())) 42 | { 43 | Service.SubscriptionWindow.Toggle(); 44 | } 45 | ImGui.Spacing(); 46 | ImGui.Text("Language:".Loc()); 47 | ImGui.SameLine(); 48 | ImGui.SetNextItemWidth(200); 49 | if (ImGui.BeginCombo("##Language", Service.Configuration.Language)) 50 | { 51 | var languages = Service.Loc.GetLanguages(); 52 | foreach (var lang in languages) 53 | { 54 | if (ImGui.Selectable(lang, lang == Service.Configuration.Language)) 55 | { 56 | Service.Configuration.Language = lang; 57 | Service.Loc.LoadLanguage(lang); 58 | Service.Configuration.Save(); 59 | } 60 | } 61 | ImGui.EndCombo(); 62 | } 63 | 64 | // checkbox EnableAutoUpdate 65 | if (ImGui.Checkbox("Enable Auto Update".Loc(), ref Service.Configuration.EnableAutoUpdate)) 66 | { 67 | Service.Configuration.Save(); 68 | if (Service.Configuration.EnableAutoUpdate) 69 | { 70 | Service.GameDataManager.GetSocialListHook?.Enable(); 71 | } 72 | else 73 | { 74 | Service.GameDataManager.GetSocialListHook?.Disable(); 75 | } 76 | } 77 | var typeNames = Enum.GetNames(typeof(ListType)).Select(x => x.Loc()); 78 | if (ImGui.IsItemHovered()) 79 | { 80 | ImGui.SetTooltip("Automatically record the checked social list.\nOptionally visible when checked.\nAnd automatically get updates\nwhen the following lists are opened.".Loc()+ "\n\n" + string.Join("\n",typeNames)); 81 | } 82 | if (Service.Configuration.EnableAutoUpdate) 83 | { 84 | ImGui.Spacing(); 85 | ImGui.Indent(); 86 | if (ImGui.Checkbox("Update From PartyList".Loc(), ref Service.Configuration.UpdateFromPartyList)) 87 | { 88 | Service.Configuration.Save(); 89 | } 90 | if (ImGui.Checkbox("Update From FriendList".Loc(), ref Service.Configuration.UpdateFromFriendList)) 91 | { 92 | Service.Configuration.Save(); 93 | } 94 | if (ImGui.Checkbox("Update From CompanyMember".Loc(), ref Service.Configuration.UpdateFromCompanyMember)) 95 | { 96 | Service.Configuration.Save(); 97 | } 98 | if (ImGui.Checkbox("Update From PlayerSearch".Loc(), ref Service.Configuration.UpdateFromPlayerSearch)) 99 | { 100 | Service.Configuration.Save(); 101 | } 102 | if (ImGui.Checkbox("Enable Subscription".Loc(), ref Service.Configuration.EnableSubscription)) 103 | { 104 | Service.Configuration.Save(); 105 | } 106 | if (ImGui.IsItemHovered()) 107 | { 108 | ImGui.SetTooltip("Add a new subscription list.\nDuring updates, if a subscribed player's name exists,\nthis player will be added to the plugin's stored players'name list".Loc()+"\n"+ 109 | "Players in the subscription list will be\nautomatically removed after successful capture of information\nor cleared when closing the game".Loc()); 110 | } 111 | if (Service.Configuration.EnableSubscription) 112 | { 113 | ImGui.Spacing(); 114 | ImGui.Indent(); 115 | ImGui.TextUnformatted("Subscription String".Loc()); 116 | ImGui.SameLine(); 117 | if (ImGui.InputText("##SubscriptionString", ref Service.Configuration.SubscriptionString, 15)) 118 | { 119 | Service.Configuration.Save(); 120 | } 121 | ImGui.Unindent(); 122 | } 123 | ImGui.Unindent(); 124 | } 125 | 126 | if (ImGui.Checkbox("Name Change Check".Loc(), ref Service.Configuration.ShowNameChange)) 127 | { 128 | Service.Configuration.Save(); 129 | } 130 | if (ImGui.IsItemHovered()) 131 | { 132 | ImGui.SetTooltip("Show player who changed name when update FriendList".Loc()); 133 | } 134 | if (ImGui.Checkbox("Enable Search In Context".Loc(), ref Service.Configuration.EnableSearchInContext)) 135 | { 136 | Service.Configuration.Save(); 137 | } 138 | 139 | if (Service.Configuration.EnableSearchInContext) 140 | { 141 | ImGui.Spacing(); 142 | ImGui.Indent(); 143 | ImGui.TextUnformatted("Search in Context String".Loc()); 144 | ImGui.SameLine(); 145 | if (ImGui.InputText("##SearchInContextString", ref Service.Configuration.SearchString, 15)) 146 | { 147 | Service.Configuration.Save(); 148 | } 149 | ImGui.Unindent(); 150 | 151 | } 152 | if (ImGui.Checkbox("Enable Add Nick Name".Loc(), ref Service.Configuration.EnableAddNickName)) 153 | { 154 | Service.Configuration.Save(); 155 | } 156 | if (Service.Configuration.EnableAddNickName) 157 | { 158 | ImGui.Spacing(); 159 | ImGui.Indent(); 160 | ImGui.TextUnformatted("Add Nick Name String".Loc()); 161 | ImGui.SameLine(); 162 | if (ImGui.InputText("##AddNickNameString", ref Service.Configuration.AddNickNameString, 15)) 163 | { 164 | Service.Configuration.Save(); 165 | } 166 | ImGui.Unindent(); 167 | } 168 | if(ImGui.Checkbox("Show CID".Loc(), ref Service.Configuration.ShowCidInList)) 169 | { 170 | Service.Configuration.Save(); 171 | } 172 | if (ImGui.IsItemHovered()) 173 | { 174 | ImGui.SetTooltip("Show stored CID on the Main Window".Loc()); 175 | } 176 | 177 | if (ImGui.Checkbox("Modify Store Path".Loc(), ref Service.Configuration.modifyStorePath)) 178 | { 179 | Service.Configuration.Save(); 180 | } 181 | if (Service.Configuration.modifyStorePath) 182 | { 183 | ImGui.Spacing(); 184 | ImGui.Indent(); 185 | ImGui.TextUnformatted("Store Path:".Loc()); 186 | ImGui.SameLine(); 187 | var storePath = Service.Configuration.storeNamesPath; 188 | if (ImGui.InputText("##StorePath", ref storePath, 260)) 189 | { 190 | if (storePath != Service.Configuration.storeNamesPath && 191 | Directory.Exists(Path.GetDirectoryName(storePath))) 192 | { 193 | Service.Configuration.storeNamesPath = storePath; 194 | Service.Configuration.Save(true); 195 | } 196 | else 197 | { 198 | storePath = Service.Configuration.storeNamesPath; 199 | } 200 | } 201 | ImGui.SameLine(); 202 | if (ImGui.Button("Reset Path".Loc())) 203 | { 204 | var path = Path.Join(Service.PluginInterface.ConfigDirectory.FullName, "storeNames.json"); 205 | if (!Directory.Exists(Path.GetDirectoryName(path))) 206 | { 207 | Service.PluginInterface.ConfigDirectory.Create(); 208 | } 209 | Service.Configuration.storeNamesPath = path; 210 | Service.Configuration.modifyStorePath = false; 211 | Service.Configuration.Save(true); 212 | 213 | } 214 | ImGui.Unindent(); 215 | } 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /UsedName/Manager/GameDataManager.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Hooking; 2 | using Dalamud.Utility; 3 | using FFXIVClientStructs.FFXIV.Client.UI.Agent; 4 | using FFXIVClientStructs.FFXIV.Client.UI.Info; 5 | using Lumina.Excel.Sheets; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Runtime.InteropServices; 10 | using UsedName.Structs; 11 | using static FFXIVClientStructs.FFXIV.Client.UI.Info.InfoProxyCommonList.CharacterData; 12 | 13 | namespace UsedName.Manager 14 | { 15 | internal class GameDataManager : IDisposable 16 | { 17 | internal unsafe delegate void GetSocialListDelegate(uint targetId, IntPtr SocialList); 18 | internal Hook GetSocialListHook { get; set; } = null!; 19 | public GameDataManager() 20 | { 21 | if (Service.Scanner.TryScanText("48 89 5c 24 ?? 56 48 ?? ?? ?? 48 ?? ?? ?? ?? ?? ?? 48 ?? ?? e8 ?? ?? ?? ?? 48 ?? ?? 48 ?? ?? 0f 84 ?? ?? ?? ?? 0f", out var ptr0)) 22 | { 23 | this.GetSocialListHook = Service.GameInteropProvider.HookFromAddress(ptr0, GetSocialListDetour); 24 | } 25 | if (Service.Configuration.EnableAutoUpdate) 26 | this.GetSocialListHook?.Enable(); 27 | 28 | // first time 29 | //if (Service.Configuration.playersNameList.Count <= 0) 30 | //{ 31 | // this.UpdateDataFromXivCommon(); 32 | //} 33 | } 34 | public void Dispose() 35 | { 36 | this.GetSocialListHook?.Disable(); 37 | this.GetSocialListHook?.Dispose(); 38 | } 39 | 40 | 41 | private void GetSocialListDetour(uint targetId, IntPtr data) 42 | { 43 | #if DEBUG 44 | int startIndex = 0x10; 45 | int endIndex = startIndex + 0x70; 46 | var bytes = new byte[endIndex - startIndex]; 47 | Marshal.Copy(data + startIndex, bytes, 0, bytes.Length); 48 | Service.PluginLog.Debug($"GetSocialListDetour 1: {BitConverter.ToString(bytes)}"); 49 | startIndex = endIndex; 50 | endIndex = startIndex + 0x70; 51 | Marshal.Copy(data + startIndex, bytes, 0, bytes.Length); 52 | Service.PluginLog.Debug($"GetSocialListDetour 2: {BitConverter.ToString(bytes)}"); 53 | startIndex = endIndex; 54 | endIndex = startIndex + 0x70; 55 | Marshal.Copy(data + startIndex, bytes, 0, bytes.Length); 56 | Service.PluginLog.Debug($"GetSocialListDetour 3: {BitConverter.ToString(bytes)}"); 57 | //int startIndex = 0; 58 | //int endIndex = startIndex + 0x70 + 50; 59 | //var bytes = new byte[endIndex - startIndex]; 60 | //Marshal.Copy(data + startIndex, bytes, 0, bytes.Length); 61 | //Service.PluginLog.Debug($"GetSocialListDetour: {BitConverter.ToString(bytes)}"); 62 | #endif 63 | SocialListResult socialList; 64 | try 65 | { 66 | socialList = Marshal.PtrToStructure(data); 67 | } 68 | catch (Exception) 69 | { 70 | this.GetSocialListHook?.Original(targetId, data); 71 | return; 72 | } 73 | 74 | this.GetSocialListHook?.Original(targetId, data); 75 | var listType = socialList.ListType; 76 | Service.PluginLog.Debug($"CommunityID:{socialList.CommunityID:X}:{socialList.Index}:{socialList.NextIndex}:{socialList.RequestKey}:{socialList.RequestParam}"); 77 | Service.PluginLog.Debug($"ListType:{socialList.ListType}"); 78 | // type: 1 = Party List; 2 = Friend List; 3 = Linkshells 4 = Player Search; 79 | // 5 = Members Online and on Home World; 6 = company member; 7 = Application of Company; 80 | // 10 = Mentor;11 = New Adventurer/Returner; 81 | 82 | if (listType is null) 83 | { 84 | #if DEBUG 85 | var hint = $"UsedName: Find Unknown type: {socialList.ListTypeByte}, please contact developer"; 86 | Service.Chat.Print(hint); 87 | foreach (var character in socialList.CharacterEntries) 88 | { 89 | hint += $"\n{character.CharacterID:X}:{character.CharacterName}"; 90 | //hint += $"\n{character.CharacterID:X}:{character.CharacterName}"; 91 | } 92 | Service.PluginLog.Warning(hint); 93 | #endif 94 | return; 95 | } 96 | bool recordAllPlayersInList = (listType == ListType.PartyList && Service.Configuration.UpdateFromPartyList) || 97 | (listType == ListType.FriendList && Service.Configuration.UpdateFromFriendList) || 98 | (listType == ListType.CompanyMember && Service.Configuration.UpdateFromCompanyMember) || 99 | (listType == ListType.PlayerSearch && Service.Configuration.UpdateFromPlayerSearch); 100 | 101 | var subList = new List(Service.PlayersNamesManager.Subscriptions); 102 | // if (!recordAllPlayersInList && subList.Count <= 0) 103 | // return; 104 | var result = new Dictionary(); 105 | // var notInGameFriendListFriend = Service.PlayersNamesManager.NotInGameFriendListFriend(); 106 | foreach (var c in socialList.CharacterEntries) 107 | { 108 | #if DEBUG 109 | Service.PluginLog.Debug($"{c.CharacterName} {c.FcTag} {c.CharacterID} " + 110 | $"AccId:{c.AccountId} " + 111 | $"Job:{c.CurrentClassJob.GameData.Value.Abbreviation} ({c.CurrentClassJob.Id}) " + 112 | $"JobLvl:{c.CurrentJobLevel} " + 113 | $"HWorld:{c.HomeWorld.GameData.Value.Name} ({c.HomeWorld.Id}) " + 114 | $"HasComment:{(c.HasSearchComment != 0).ToString()} " + 115 | $"GC:{(FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany)c.GrandCompanyId} " + 116 | $"Languages:{c.Languages} " + 117 | $"ClientLanguage:{c.ClientLanguage} " + 118 | $"Statuses:{(InfoProxyCommonList.CharacterData.OnlineStatus)c.OnlineStatusBytes} " + 119 | $"Territory:{c.TerritoryType?.GameData.Value.PlaceName.Value.Name}"); 120 | #endif 121 | if (c.CharacterID == 0 || 122 | c.CharacterID == Service.ClientState.LocalContentId || 123 | c.CharacterName.IsNullOrEmpty()) 124 | continue; 125 | 126 | if (subList.RemoveAll(x => x == c.CharacterName) > 0 || 127 | // notInGameFriendListFriend.Exists(id => id == c.CharacterID) || 128 | Service.Configuration.playersNameList.ContainsKey(c.CharacterID) || 129 | recordAllPlayersInList) 130 | { 131 | if (!result.TryAdd(c.CharacterID, c.CharacterName)) 132 | { 133 | Service.PluginLog.Warning($"Duplicate entry {c.CharacterID} {c.CharacterName}"); 134 | } 135 | } 136 | //if (c.ContentId == 0 || 137 | // c.ContentId == Service.ClientState.LocalContentId || 138 | // c.NameString.IsNullOrEmpty()) 139 | // continue; 140 | 141 | //if (subList.RemoveAll(x => x == c.NameString) > 0 || 142 | // // notInGameFriendListFriend.Exists(id => id == c.CharacterID) || 143 | // Service.Configuration.playersNameList.ContainsKey(c.ContentId) || 144 | // recordAllPlayersInList) 145 | //{ 146 | // if (!result.TryAdd(c.ContentId, c.NameString)) 147 | // { 148 | // Service.PluginLog.Warning($"Duplicate entry {c.ContentId} {c.NameString}"); 149 | // } 150 | //} 151 | 152 | } 153 | // show different of subList 154 | var difference = subList.Except(Service.PlayersNamesManager.Subscriptions).ToList(); 155 | if (result.Count <= 0) 156 | return; 157 | Service.PlayersNamesManager.UpdatePlayerNames(result, difference, false); 158 | 159 | } 160 | 161 | 162 | //internal IDictionary? GetDataFromXivCommon() 163 | //{ 164 | // var friendList = Service.Common.Functions.FriendList.List; 165 | // if (friendList.Count <= 0) 166 | // { 167 | // return null; 168 | // } 169 | // var friendListEnumerator = Service.Common.Functions.FriendList.List.GetEnumerator(); 170 | // IDictionary currentPlayersList = new Dictionary(); 171 | // while (friendListEnumerator.MoveNext()) 172 | // { 173 | // var player = friendListEnumerator.Current; 174 | // var contentId = player.ContentId; 175 | // var name = player.Name.ToString(); 176 | // try 177 | // { 178 | // currentPlayersList.Add(contentId, name); 179 | // } 180 | // catch (ArgumentException e) 181 | // { 182 | // Service.PluginLog.Warning($"{e}"); 183 | // Service.PluginLog.Warning($"Unknown problem at {name}-{contentId}"); 184 | // Service.Chat.PrintError(Service.Loc.Localize($"Update Player List Fail\nMay cause by incompatible version of XivCommon\nPlease contact to developer")); 185 | // return null; 186 | // } 187 | 188 | // } 189 | // return currentPlayersList; 190 | //} 191 | //internal void UpdateDataFromXivCommon() 192 | //{ 193 | // var currentPlayersList = GetDataFromXivCommon(); 194 | // if(currentPlayersList is { Count: > 0 }) 195 | // Service.PlayersNamesManager.UpdatePlayerNames(currentPlayersList, new List()); 196 | //} 197 | /* 198 | public XivCommon.Functions.FriendList.FriendListEntry GetPlayerByNameFromFriendList(string name) 199 | { 200 | var friendList = Service.Common.Functions.FriendList.List.GetEnumerator(); 201 | while (friendList.MoveNext()) 202 | { 203 | var player = friendList.Current; 204 | if (player.Name.ToString().Equals(name)) 205 | { 206 | return player; 207 | } 208 | } 209 | return new XivCommon.Functions.FriendList.FriendListEntry(); 210 | } 211 | */ 212 | 213 | } 214 | } 215 | --------------------------------------------------------------------------------