public void Initialize(DalamudPluginInterface pluginInterface) { generalStopwatch.Start(); cacheTimer.Start(); #if DEBUG drawConfigWindow = true; #endif this.PluginInterface = pluginInterface; LoadConfig(); PluginInterface.Framework.OnUpdateEvent += FrameworkUpdate; IconManager = new IconManager(pluginInterface); actionManagerStatic = pluginInterface.TargetModuleScanner.GetStaticAddressFromSig("48 89 05 ?? ?? ?? ?? C3 CC C2 00 00 CC CC CC CC CC CC CC CC CC CC CC CC CC 48 8D 0D ?? ?? ?? ?? E9 ?? ?? ?? ??"); blueSpellBook = (uint *)(pluginInterface.TargetModuleScanner.GetStaticAddressFromSig("0F B7 0D ?? ?? ?? ?? 84 C0") + 0x2A); PluginLog.Verbose($"Blue Spell Book: {(ulong) blueSpellBook:X}"); ActionManager = new ActionManager(this, actionManagerStatic); pluginInterface.UiBuilder.OnOpenConfigUi += OnOpenConfigUi; PluginInterface.UiBuilder.OnBuildUi += this.BuildUI; pluginInterface.ClientState.TerritoryChanged += TerritoryChanged; TerritoryChanged(this, pluginInterface.ClientState.TerritoryType); updateRetainerListHook = new Hook <UpdateRetainerListDelegate>(pluginInterface.TargetModuleScanner.ScanText("40 53 48 83 EC 20 48 8B 01 48 8B D9 FF 50 20 84 C0 74 0F 48 8B 03 48 8B CB 48 83 C4 20 5B 48 FF 60 18 E8"), new UpdateRetainerListDelegate(UpdateRetainerListDetour)); updateRetainerListHook.Enable(); Client = new ClientInterface(pluginInterface.TargetModuleScanner, pluginInterface.Data); SetupCommands(); }
public bool Execute(string message) { // First try to process the command through Dalamud. if (Dalamud.Commands.ProcessCommand(message)) { PluginLog.Verbose("Executed Dalamud command \"{Message:l}\".", message); return(true); } if (_uiModulePtr == IntPtr.Zero) { PluginLog.Error("Can not execute \"{Message:l}\" because no uiModulePtr is available.", message); return(false); } // Then prepare a string to send to the game itself. var(text, length) = PrepareString(message); var payload = PrepareContainer(text, length); _processChatBox.Invoke(_uiModulePtr, payload, IntPtr.Zero, (byte)0); Marshal.FreeHGlobal(payload); Marshal.FreeHGlobal(text); return(false); }
/// <summary> /// Check if the sender of this message is set as the owner of this plugin, and send an error message to the specified channel if not null. /// </summary> /// <param name="user">User in question.</param> /// <param name="errorMessageChannel">Channel for error message.</param> /// <returns>True if the user is the owner of this plugin.</returns> private async Task <bool> EnsureOwner(IUser user, ISocketMessageChannel errorMessageChannel = null) { PluginLog.Verbose("EnsureOwner: " + user.Username + "#" + user.Discriminator); if (user.Username + "#" + user.Discriminator == this.plugin.Config.DiscordOwnerName) { return(true); } if (ulong.TryParse(this.plugin.Config.DiscordOwnerName, out ulong parsed)) { if (user.Id == parsed) { return(true); } } if (errorMessageChannel == null) { return(false); } await SendGenericEmbed(errorMessageChannel, "You are not allowed to run commands for this bot.\n\nIf this is your bot, please use the \"/pdiscord\" command in-game to enter your username.", "Error", EmbedColorError); return(false); }
private async Task SocketClientOnReady() { this.State = DiscordState.Ready; await this.specialChars.TryFindEmote(this.socketClient); PluginLog.Verbose("DiscordHandler READY!!"); }
/// <summary> /// Get the webhook for the respective channel, or create one if it doesn't exist. /// </summary> /// <param name="channel">The channel to get the webhook for</param> /// <returns><see cref="IWebhook"/> for the respective channel.</returns> private async Task <DiscordWebhookClient> GetOrCreateWebhookClient(SocketChannel channel) { if (!(channel is SocketTextChannel textChannel)) { throw new ArgumentNullException(nameof(textChannel)); } if (!this.plugin.Config.ChannelConfigs.TryGetValue(channel.Id, out var channelConfig)) { throw new ArgumentException("No configuration for channel.", nameof(channel)); } IWebhook hook; if (channelConfig.WebhookId != 0) { hook = await textChannel.GetWebhookAsync(channelConfig.WebhookId) ?? await textChannel.CreateWebhookAsync("FFXIV Bridge Worker"); } else { hook = await textChannel.CreateWebhookAsync("FFXIV Bridge Worker"); } this.plugin.Config.ChannelConfigs[channel.Id].WebhookId = hook.Id; this.plugin.Config.Save(); PluginLog.Verbose("Webhook for {0} OK!! {1}", channel.Id, hook.Id); return(new DiscordWebhookClient(hook)); }
public void Dispose() { PluginLog.Verbose("Discord DISPOSE!!"); this.MessageQueue?.Stop(); this.socketClient?.LogoutAsync().GetAwaiter().GetResult(); this.socketClient?.Dispose(); }
public void Initialize(DalamudPluginInterface pluginInterface) { generalStopwatch.Start(); cacheTimer.Start(); #if DEBUG drawConfigWindow = true; #endif this.PluginInterface = pluginInterface; LoadConfig(); PluginInterface.Framework.OnUpdateEvent += FrameworkUpdate; IconManager = new IconManager(pluginInterface); actionManagerStatic = pluginInterface.TargetModuleScanner.GetStaticAddressFromSig("48 89 05 ?? ?? ?? ?? C3 CC C2 00 00 CC CC CC CC CC CC CC CC CC CC CC CC CC 48 8D 0D ?? ?? ?? ?? E9 ?? ?? ?? ??"); blueSpellBook = (uint *)(pluginInterface.TargetModuleScanner.GetStaticAddressFromSig("0F B7 0D ?? ?? ?? ?? 84 C0") + 0x2A); PluginLog.Verbose($"Blue Spell Book: {(ulong) blueSpellBook:X}"); ActionManager = new ActionManager(this, actionManagerStatic); pluginInterface.UiBuilder.OnOpenConfigUi += OnOpenConfigUi; PluginInterface.UiBuilder.OnBuildUi += this.BuildUI; pluginInterface.ClientState.TerritoryChanged += TerritoryChanged; TerritoryChanged(this, pluginInterface.ClientState.TerritoryType); SetupCommands(); }
// We need to set the correct collection for the actual material path that is loaded // before actually loading the file. private bool MtrlLoadHandler(Utf8String split, Utf8String path, ResourceManager *resourceManager, SeFileDescriptor *fileDescriptor, int priority, bool isSync, out byte ret) { ret = 0; if (fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl) { return(false); } var lastUnderscore = split.LastIndexOf(( byte )'_'); var name = lastUnderscore == -1 ? split.ToString() : split.Substring(0, lastUnderscore).ToString(); if (Penumbra.CollectionManager.ByName(name, out var collection)) { #if DEBUG PluginLog.Verbose("Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path); #endif SetCollection(path, collection); } else { #if DEBUG PluginLog.Verbose("Using MtrlLoadHandler with no collection for path {$Path:l}.", path); #endif } // Force isSync = true for this call. I don't really understand why, // or where the difference even comes from. // Was called with True on my client and with false on other peoples clients, // which caused problems. ret = Penumbra.ResourceLoader.DefaultLoadResource(path, resourceManager, fileDescriptor, priority, true); PathCollections.TryRemove(path, out _); return(true); }
public async Task Start() { if (string.IsNullOrEmpty(this.plugin.Config.DiscordToken)) { this.State = DiscordState.TokenInvalid; PluginLog.Error("Token empty, cannot start bot."); return; } try { await this.socketClient.LoginAsync(TokenType.Bot, this.plugin.Config.DiscordToken); await this.socketClient.StartAsync(); } catch (Exception ex) { PluginLog.Error(ex, "Token invalid, cannot start bot."); } this.MessageQueue.Start(); lodestoneClient = await LodestoneClient.GetClientAsync(); PluginLog.Verbose("DiscordHandler START!!"); }
public void SendEvent(MidiEvent midiEvent) { var keyboard = Plugin.pluginInterface.Framework.Gui.GetAddonByName("PerformanceModeWide", 1); if (keyboard == null) { return; } if (midiEvent is NoteOnEvent noteOnEvent) { var noteNum = noteOnEvent.NoteNumber - 48 + Plugin.config.NoteNumberOffset; var adaptedOctave = 0; if (Plugin.config.AdaptNotesOOR) { while (noteNum < 0) { noteNum += 12; adaptedOctave++; } while (noteNum > 36) { noteNum -= 12; adaptedOctave--; } } PluginLog.Verbose($"{noteOnEvent.GetNoteName().ToString().Replace("Sharp", "#")}{noteOnEvent.GetNoteOctave()} ({noteNum})" + $"{(noteNum < 0 || noteNum > 36 ? "(out of range)" : string.Empty)}" + $"{(adaptedOctave != 0 ? $"[adapted {adaptedOctave} Oct]" : string.Empty)}"); if (noteNum < 0 || noteNum > 36) { return; } playlib.PressKey(keyboard.Address, noteNum); } else if (midiEvent is NoteOffEvent noteOffEvent) { var noteNum = noteOffEvent.NoteNumber - 48 + Plugin.config.NoteNumberOffset; var adaptedOctave = 0; if (Plugin.config.AdaptNotesOOR) { while (noteNum < 0) { noteNum += 12; adaptedOctave++; } while (noteNum > 36) { noteNum -= 12; adaptedOctave--; } } if (noteNum < 0 || noteNum > 36) { return; } playlib.ReleaseKey(keyboard.Address, noteNum); } }
// Only create, do not update. private void CreateCache() { if (_cache == null) { CalculateEffectiveFileList(); PluginLog.Verbose("Created new cache for collection {Name:l}.", Name); } }
protected override void Setup64Bit(SigScanner scanner) { ActiveSpellsAddress = scanner.GetStaticAddressFromSig(ActionManagerSignature, 316); ClientGameUiHotbarAddress = scanner.GetStaticAddressFromSig(ClientGameUiHotbarSignature, 1); IsActionUnlockedAddress = scanner.ScanText(IsActionUnlockedSignature); PluginLog.Verbose("===== BLU DEX ====="); PluginLog.Verbose($"{nameof(ClientGameUiHotbarAddress)} {ClientGameUiHotbarAddress.ToInt64():X}"); PluginLog.Verbose($"{nameof(IsActionUnlockedAddress)} {IsActionUnlockedAddress.ToInt64():X}"); }
public void UwuCommand(string command, string args) { config.Enabed = false; config.Save(); // You may want to assign these references to private variables for convenience. // Keep in mind that the local player does not exist until after logging in. var chat = this._pi.Framework.Gui.Chat; chat.Print($"Goodbye Owofied chat. ;_;"); PluginLog.Verbose("OwO has been disabled."); }
private Gatherable?FindItemLogging(string itemName) { var item = _world.FindItemByName(itemName); var output = item == null ? $"Could not find corresponding item to \"{itemName}\"." : $"Identified [{item.ItemId}: {item.NameList[_language]}] for \"{itemName}\"."; _chat.Print(output); PluginLog.Verbose(output); return(item); }
public World(DalamudPluginInterface pi, GatherBuddyConfiguration config) { _pi = pi; Language = pi.ClientState.ClientLanguage; Territories = new TerritoryManager(); Aetherytes = new AetheryteManager(pi, Territories); Items = new ItemManager(pi); Nodes = new NodeManager(pi, config, this, Aetherytes, Items); Fish = new FishManager(pi, this, Aetherytes); PluginLog.Verbose("{Count} regions collected.", Territories.Regions.Count); PluginLog.Verbose("{Count} territories collected.", Territories.Territories); }
/// <inheritdoc/> protected override void Setup64Bit(SigScanner scanner) { this.ComboTimer = scanner.GetStaticAddressFromSig("F3 0F 11 05 ?? ?? ?? ?? F3 0F 10 45 ?? E8"); this.GetAdjustedActionId = scanner.ScanText("E8 ?? ?? ?? ?? 8B F8 3B DF"); // Client::Game::ActionManager.GetAdjustedActionId this.IsActionIdReplaceable = scanner.ScanText("81 F9 ?? ?? ?? ?? 7F 35"); PluginLog.Verbose("===== X I V C O M B O ====="); PluginLog.Verbose($"{nameof(this.GetAdjustedActionId)} 0x{this.GetAdjustedActionId:X}"); PluginLog.Verbose($"{nameof(this.IsActionIdReplaceable)} 0x{this.IsActionIdReplaceable:X}"); PluginLog.Verbose($"{nameof(this.ComboTimer)} 0x{this.ComboTimer:X}"); PluginLog.Verbose($"{nameof(this.LastComboMove)} 0x{this.LastComboMove:X}"); }
private void Ring(Alarm alarm, int currentMinute) { if (alarm.SoundId > Sounds.Unknown) { _sounds.Play(alarm.SoundId); } if (alarm.PrintMessage && _config.AlarmFormat.Length > 0) { _pi.Framework.Gui.Chat.PrintError(ReplaceFormatPlaceholders(_config.AlarmFormat, alarm, currentMinute)); PluginLog.Verbose(ReplaceFormatPlaceholders(GatherBuddyConfiguration.DefaultAlarmFormat, alarm, currentMinute)); } LastAlarm = alarm; }
public void TryCreateTeleporterWatcher(DalamudPluginInterface pi, bool useTeleport) { const string teleporterPluginConfigFile = "TeleporterPlugin.json"; _teleporterLanguage = _language; if (!useTeleport || _teleporterWatcher != null) { _teleporterWatcher?.Dispose(); _teleporterWatcher = null; return; } var dir = new DirectoryInfo(pi.GetPluginConfigDirectory()); if (!dir.Exists || (dir.Parent?.Exists ?? false)) { return; } dir = dir.Parent; var file = new FileInfo(Path.Combine(dir !.FullName, teleporterPluginConfigFile)); if (file.Exists) { ParseTeleporterFile(file.FullName); } void OnTeleporterConfigChange(object source, FileSystemEventArgs args) { PluginLog.Verbose("Reloading Teleporter Config."); if (args.ChangeType != WatcherChangeTypes.Changed && args.ChangeType != WatcherChangeTypes.Created) { return; } ParseTeleporterFile(args.FullPath); } _teleporterWatcher = new FileSystemWatcher { Path = dir.FullName, NotifyFilter = NotifyFilters.LastWrite, Filter = teleporterPluginConfigFile, }; _teleporterWatcher.Changed += OnTeleporterConfigChange; _teleporterWatcher !.EnableRaisingEvents = true; }
protected override void Setup64Bit(SigScanner scanner) { ComboTimer = scanner.GetStaticAddressFromSig("48 89 2D ?? ?? ?? ?? 85 C0 74 0F"); GetIcon = scanner.ScanText("E8 ?? ?? ?? ?? 8B F8 3B DF"); // Client::Game::ActionManager.GetAdjustedActionId IsIconReplaceable = scanner.ScanText("81 F9 ?? ?? ?? ?? 7F 39 81 F9 ?? ?? ?? ??"); GetActionCooldown = scanner.ScanText("E8 ?? ?? ?? ?? 0F 57 FF 48 85 C0"); PluginLog.Verbose("===== H O T B A R S ====="); PluginLog.Verbose($"GetIcon address 0x{GetIcon.ToInt64():X}"); PluginLog.Verbose($"IsIconReplaceable 0x{IsIconReplaceable.ToInt64():X}"); PluginLog.Verbose($"ComboTimer 0x{ComboTimer.ToInt64():X}"); PluginLog.Verbose($"LastComboMove 0x{LastComboMove.ToInt64():X}"); }
private bool DrawDeletionConfirmationWindow() { if (!DeleteConfirmationVisible) { return(false); } var ret = false; ImGui.SetNextWindowSize(new Vector2(232, 100), ImGuiCond.Always); if (ImGui.Begin("Remove this channel config?", ref this.deleteConfirmationVisible, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) { ImGui.Text("Are you sure you want to delete this?"); ImGui.Text("This cannot be undone."); if (ImGui.Button("Yes")) { PluginLog.Verbose("Killing the thing."); this.configuration.ChatTypeConfigurations.Remove(currentEntry); PluginLog.Verbose($"Removed the configuration for {currentEntry.Channel.ToString()}"); Save(); currentEntry = null; // Visible = false; // Visible = true; ret = true; DeleteConfirmationVisible = false; } ImGui.SameLine(); if (ImGui.Button("No")) { Visible = false; Visible = true; DeleteConfirmationVisible = false; } } ImGui.End(); return(ret); }
protected override void Setup64Bit(SigScanner scanner) { AddonSelectYesNoOnSetupAddress = scanner.ScanText(AddonSelectYesNoOnSetupSignature); AddonSalvageDialongOnSetupAddress = scanner.ScanText(AddonSalvageDialogOnSetupSignature); AddonMaterializeDialongOnSetupAddress = scanner.ScanText(AddonMaterializeDialogOnSetupSignature); AddonItemInspectionResultOnSetupAddress = scanner.ScanText(AddonItemInspectionResultOnSetupSignature); AddonRetainerTaskAskOnSetupAddress = scanner.ScanText(AddonRetainerTaskAskOnSetupSignature); AddonRetainerTaskResultOnSetupAddress = scanner.ScanText(AddonRetainerTaskResultOnSetupSignature); PluginLog.Verbose("===== YES ALREADY ====="); PluginLog.Verbose($"{nameof(AddonSelectYesNoOnSetupAddress)} {AddonSelectYesNoOnSetupAddress.ToInt64():X}"); PluginLog.Verbose($"{nameof(AddonSalvageDialongOnSetupAddress)} {AddonSalvageDialongOnSetupAddress.ToInt64():X}"); PluginLog.Verbose($"{nameof(AddonMaterializeDialongOnSetupAddress)} {AddonMaterializeDialongOnSetupAddress.ToInt64():X}"); PluginLog.Verbose($"{nameof(AddonItemInspectionResultOnSetupAddress)} {AddonItemInspectionResultOnSetupAddress.ToInt64():X}"); PluginLog.Verbose($"{nameof(AddonRetainerTaskAskOnSetupAddress)} {AddonRetainerTaskAskOnSetupAddress.ToInt64():X}"); PluginLog.Verbose($"{nameof(AddonRetainerTaskResultOnSetupAddress)} {AddonRetainerTaskResultOnSetupAddress.ToInt64():X}"); }
protected override void Setup64Bit(SigScanner scanner) { ComboTimer = scanner.GetStaticAddressFromSig("E8 ?? ?? ?? ?? 80 7E 21 00", 0x178); // this.GetIcon = scanner.ScanText("48 89 5c 24 08 48 89 6c 24 10 48 89 74 24 18 57 48 83 ec 30 8b da be dd 1c 00 00 bd d3 0d 00 00"); // 5.35 GetIcon = scanner.ScanText("E8 ?? ?? ?? ?? 8B F8 3B DF"); // 5.4 // this.IsIconReplaceable = scanner.ScanText("81 f9 2e 01 00 00 7f 39 81 f9 2d 01 00 00 0f 8d 11 02 00 00 83 c1 eb"); // 5.35 IsIconReplaceable = scanner.ScanText("81 F9 ?? ?? ?? ?? 7F 39 81 F9 ?? ?? ?? ??"); // 5.4 BuffVTableAddr = scanner.GetStaticAddressFromSig("48 89 05 ?? ?? ?? ?? 88 05 ?? ?? ?? ?? 88 05 ?? ?? ?? ??"); PluginLog.Verbose("===== H O T B A R S ====="); PluginLog.Verbose($"GetIcon address 0x{GetIcon.ToInt64():X}"); PluginLog.Verbose($"IsIconReplaceable 0x{IsIconReplaceable.ToInt64():X}"); PluginLog.Verbose($"ComboTimer 0x{ComboTimer.ToInt64():X}"); PluginLog.Verbose($"LastComboMove 0x{LastComboMove.ToInt64():X}"); }
internal static bool DrawConfigUi(Configuration config, DalamudPluginInterface pi, Action <IPluginConfiguration> save, IReadOnlyCollection <ClassJob> jobs, IReadOnlyCollection <FFXIVAction> allActions, ref IEnumerator <VibrationPattern.Step?>?patternEnumerator) { var shouldDrawConfigUi = true; var changed = false; var scale = ImGui.GetIO().FontGlobalScale; ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowSize(new Vector2(575 * scale, 400 * scale), ImGuiCond.FirstUseEver); ImGui.SetNextWindowSizeConstraints(new Vector2(350 * scale, 200 * scale), new Vector2(float.MaxValue, float.MaxValue)); if (!ImGui.Begin($"{GentleTouch.PluginName} Configuration", ref shouldDrawConfigUi, ImGuiWindowFlags.NoCollapse)) { ImGui.End(); return(shouldDrawConfigUi); } if (config.OnboardingStep != Onboarding.Done) { changed |= DrawRisksWarning(config, ref shouldDrawConfigUi, scale); changed |= DrawOnboarding(config, jobs, allActions, scale); } ImGui.BeginTabBar("ConfigurationTabs", ImGuiTabBarFlags.NoTooltip); changed |= DrawGeneralTab(config, scale); changed |= DrawPatternTab(config, scale, ref patternEnumerator); changed |= DrawTriggerTab(config, pi, scale, jobs, allActions); ImGui.EndTabBar(); ImGui.End(); if (!changed) { return(shouldDrawConfigUi); } #if DEBUG PluginLog.Verbose("Config changed, saving..."); #endif save(config); return(shouldDrawConfigUi); }
private static async Task <dynamic> Get(string endpoint, bool noCache = false) { PluginLog.Verbose("XIVAPI FETCH: {0}", endpoint); if (CachedResponses.TryGetValue(endpoint, out var val) && !noCache) { return(val); } var client = new HttpClient(); var response = await client.GetAsync(URL + endpoint); var result = await response.Content.ReadAsStringAsync(); var obj = JObject.Parse(result); if (!noCache) { CachedResponses.TryAdd(endpoint, obj); } return(obj); }
public void Draw() { if (!this.isVisible) { return; } ImGui.Begin("Discord Bridge Setup", ref this.isVisible); ImGui.Text("In this window, you can set up the XIVLauncher Discord Bridge.\n\n" + "To begin, enter your discord bot token and username below, then click \"Save\".\n" + "As soon as the red text says \"connected\", click the \"Join my server\" button and add the bot to one of your personal servers.\n" + $"You can then use the {this.plugin.Config.DiscordBotPrefix}help command in your discord server to specify channels."); ImGui.Dummy(new Vector2(10, 10)); ImGui.InputText("Enter your bot token", ref this.token, 100); ImGui.InputText("Enter your Username(e.g. user#0000)", ref this.username, 50); ImGui.Dummy(new Vector2(10, 10)); ImGui.Text("Status: "); ImGui.SameLine(); var message = this.plugin.Discord.State switch { DiscordState.None => "Not started", DiscordState.Ready => "Connected!", DiscordState.TokenInvalid => "Token empty or invalid.", _ => "Unknown" }; ImGui.TextColored(this.plugin.Discord.State == DiscordState.Ready ? fineColor : errorColor, message); if (this.plugin.Discord.State == DiscordState.Ready && ImGui.Button("Join my server")) { Process.Start( $"https://discordapp.com/oauth2/authorize?client_id={this.plugin.Discord.UserId}&scope=bot&permissions=537258064"); } ImGui.Dummy(new Vector2(10, 10)); if (ImGui.Button("How does this work?")) { Process.Start(Constant.HelpLink); } ImGui.SameLine(); if (ImGui.Button("Save")) { PluginLog.Verbose("Reloading Discord..."); this.plugin.Config.DiscordToken = this.token; this.plugin.Config.DiscordOwnerName = this.username; this.plugin.Config.Save(); this.plugin.Discord.Dispose(); this.plugin.Discord = new DiscordHandler(this.plugin); this.plugin.Discord.Start(); } } }
// Save the current sort order. // Does not save or copy the backup in the current mod directory, // as this is done on mod directory changes only. private void SaveFilesystem() { SaveToFile(new FileInfo(ModFileSystemFile), SaveMod, true); PluginLog.Verbose("Saved mod filesystem."); }
public NodeManager(DalamudPluginInterface pi, GatherBuddyConfiguration config, World territories, AetheryteManager aetherytes, ItemManager gatherables) { var baseSheet = pi.Data.GetExcelSheet <GatheringPointBase>(); var nodeSheet = pi.Data.GetExcelSheet <GatheringPoint>(); Dictionary <uint, Node> baseIdToNode = new((int)baseSheet.RowCount); NodeIdToNode = new Dictionary <uint, Node>((int)nodeSheet.RowCount); foreach (var nodeRow in nodeSheet) { var baseId = nodeRow.GatheringPointBase.Row; if (baseId >= baseSheet.RowCount) { continue; } if (baseIdToNode.TryGetValue(baseId, out var node)) { NodeIdToNode[nodeRow.RowId] = node; if ((node.Nodes !.Territory?.Id ?? 0) != nodeRow.TerritoryType.Row) { PluginLog.Error($"Different gathering nodes to the same base {baseId} have different territories."); } if (!node.Nodes.Nodes.ContainsKey(nodeRow.RowId)) { node.Nodes.Nodes[nodeRow.RowId] = null; } continue; } if (nodeRow.TerritoryType.Row < 2) { continue; } node = new Node { PlaceNameEn = FFName.FromPlaceName(pi, nodeRow.PlaceName.Row)[Dalamud.ClientLanguage.English], Nodes = new SubNodes() { Territory = territories.FindOrAddTerritory(nodeRow.TerritoryType.Value), }, }; node.Nodes.Nodes[nodeRow.RowId] = null; if (node.Nodes.Territory == null) { continue; } var(times, type) = GetTimes(pi, nodeRow.RowId); node.Times = times; var baseRow = baseSheet.GetRow(baseId); node.Meta = new NodeMeta(baseRow, type); if (node.Meta.GatheringType >= GatheringType.Spearfishing) { continue; } node.Items = new NodeItems(node, baseRow.Item, gatherables); if (node.Items.NoItems()) { PluginLog.Debug("Gathering node {RowId} has no items, skipped.", nodeRow.RowId); continue; } baseIdToNode[baseId] = node; NodeIdToNode[nodeRow.RowId] = node; } Records = new NodeRecorder(pi, this, config.Records); PluginLog.Verbose("{Count} unique gathering nodes collected.", NodeIdToNode.Count); PluginLog.Verbose("{Count} base gathering nodes collected.", baseIdToNode.Count); ApplyHiddenItemsAndCoordinates(gatherables, aetherytes, baseIdToNode); }
public async Task SendChatEvent(string message, string senderName, string senderWorld, XivChatType chatType, string avatarUrl = "") { // set fields for true chat messages or custom via ipc if (chatType != XivChatTypeExtensions.IpcChatType) { // Special case for outgoing tells, these should be sent under Incoming tells if (chatType == XivChatType.TellOutgoing) { chatType = XivChatType.TellIncoming; } } else { senderWorld = null; } // default avatar url to logo link if empty if (string.IsNullOrEmpty(avatarUrl)) { avatarUrl = Constant.LogoLink; } var applicableChannels = this.plugin.Config.ChannelConfigs.Where(x => x.Value.ChatTypes.Contains(chatType)); if (!applicableChannels.Any()) { return; } message = this.specialChars.TransformToUnicode(message); try { switch (chatType) { case XivChatType.Echo: break; case (XivChatType)61: // npc talk break; case (XivChatType)68: // npc announce break; default: // don't even bother searching if it's gonna be invalid bool doSearch = true; if (string.IsNullOrEmpty(senderName)) { PluginLog.Verbose($"Sender Name was null or empty: {senderName}"); doSearch = false; } if (string.IsNullOrEmpty(senderWorld)) { PluginLog.Verbose($"Sender World was null or empty: {senderWorld}"); doSearch = false; } if (senderName == "Sonar" || !senderName.Contains(" ")) { PluginLog.Verbose($"Sender Name was a plugin or invalid: {senderName}"); doSearch = false; } if (doSearch) { var playerCacheName = $"{senderName}@{senderWorld}"; PluginLog.Verbose($"Searching for {playerCacheName}"); if (CachedResponses.TryGetValue(playerCacheName, out LodestoneCharacter lschar)) { PluginLog.Verbose($"Retrived cached data for {lschar.Name} {lschar.Avatar.ToString()}"); avatarUrl = lschar.Avatar.ToString(); } else { PluginLog.Verbose($"Searching lodestone for {playerCacheName}"); lschar = await lodestoneClient.SearchCharacter(new CharacterSearchQuery() { CharacterName = senderName, World = senderWorld, }).Result.Results.FirstOrDefault(result => result.Name == senderName).GetCharacter(); CachedResponses.TryAdd(playerCacheName, lschar); PluginLog.Verbose($"Adding cached data for {lschar.Name} {lschar.Avatar}"); avatarUrl = lschar.Avatar.ToString(); } // avatarUrl = (await XivApiClient.GetCharacterSearch(senderName, senderWorld)).AvatarUrl; } break; } } catch (Exception ex) { if (string.IsNullOrEmpty(senderName)) { PluginLog.Error($"senderName was null or empty. How did we get this far?"); senderName = "Bridge Error - sendername"; } else { PluginLog.Error(ex, $"Cannot fetch XIVAPI character search for {senderName} on {senderWorld}"); } } var displayName = senderName + (string.IsNullOrEmpty(senderWorld) || string.IsNullOrEmpty(senderName) ? "" : $"@{senderWorld}"); this.plugin.Config.PrefixConfigs.TryGetValue(chatType, out var prefix); var chatTypeText = this.plugin.Config.CustomSlugsConfigs.TryGetValue(chatType, out var x) ? x : chatType.GetSlug(); foreach (var channelConfig in applicableChannels) { var socketChannel = this.socketClient.GetChannel(channelConfig.Key); if (socketChannel == null) { PluginLog.Error("Could not find channel {0} for {1}", channelConfig.Key, chatType); var channelConfigs = this.plugin.Config.ChannelConfigs; channelConfigs.Remove(channelConfig.Key); this.plugin.Config.ChannelConfigs = channelConfigs; PluginLog.Log("Removing channel {0}'s config because it no longer exists or cannot be accessed.", channelConfig.Key); this.plugin.Config.Save(); continue; } var webhookClient = await GetOrCreateWebhookClient(socketChannel); var messageContent = chatType != XivChatTypeExtensions.IpcChatType ? $"{prefix}**[{chatTypeText}]** {message}" : $"{prefix} {message}"; // check for duplicates before sending // straight up copied from the previous bot, but I have no way to test this myself. var recentMessages = (socketChannel as SocketTextChannel).GetCachedMessages(); var recentMsg = recentMessages.FirstOrDefault(msg => msg.Content == messageContent); if (this.plugin.Config.DuplicateCheckMS > 0 && recentMsg != null) { long msgDiff = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - recentMsg.Timestamp.ToUnixTimeMilliseconds(); if (msgDiff < this.plugin.Config.DuplicateCheckMS) { PluginLog.Log($"[IN TESTING]\n DIFF:{msgDiff}ms Skipping duplicate message: {messageContent}"); return; } } await webhookClient.SendMessageAsync( messageContent, username : displayName, avatarUrl : avatarUrl, allowedMentions : new AllowedMentions(AllowedMentionTypes.Roles | AllowedMentionTypes.Users | AllowedMentionTypes.None) ); // the message to a list of recently sent messages. // If someone else sent the same thing at the same time // both will need to be checked and the earlier timestamp kept // while the newer one is removed // refer to https://discord.com/channels/581875019861328007/684745859497590843/791207648619266060 } }
// Clear the current cache. private void ClearCache() { _cache?.Dispose(); _cache = null; PluginLog.Verbose("Cleared cache of collection {Name:l}.", Name); }
private async Task SocketClientOnMessageReceived(SocketMessage message) { if (message.Author.IsBot || message.Author.IsWebhook) { return; } var args = message.Content.Split(); // if it doesn't start with the bot prefix, ignore it. if (!args[0].StartsWith(this.plugin.Config.DiscordBotPrefix)) { return; } /* * // this is only needed for debugging purposes. * foreach (var s in args) * { * PluginLog.Verbose(s); * } */ PluginLog.Verbose("Received command: {0}", args[0]); try { if (args[0] == this.plugin.Config.DiscordBotPrefix + "setchannel" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length == 1) { await SendGenericEmbed(message.Channel, $"You need to specify some chat kinds to use.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } var kinds = args[1].Split(',').Select(x => x.ToLower()); // Is there any chat type that's not recognized? if (kinds .Any(x => XivChatTypeExtensions.TypeInfoDict.All(y => y.Value.Slug != x) && x != "any")) { PluginLog.Verbose("Could not find kinds"); await SendGenericEmbed(message.Channel, $"One or more of the chat kinds you specified could not be found.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } if (!this.plugin.Config.ChannelConfigs.TryGetValue(message.Channel.Id, out var config)) { config = new DiscordChannelConfig(); } foreach (var selectedKind in kinds) { PluginLog.Verbose(selectedKind); if (selectedKind == "any") { config.SetUnique(DefaultChatTypes); } else { var chatType = XivChatTypeExtensions.GetBySlug(selectedKind); config.SetUnique(chatType); } } this.plugin.Config.ChannelConfigs[message.Channel.Id] = config; this.plugin.Config.Save(); await SendGenericEmbed(message.Channel, $"OK! This channel has been set to receive the following chat kinds:\n\n```\n{config.ChatTypes.Select(x => $"{x.GetFancyName()}").Aggregate((x, y) => x + "\n" + y)}```", "Chat kinds set", EmbedColorFine); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "unsetchannel" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length == 1) { await SendGenericEmbed(message.Channel, $"You need to specify some chat kinds to use.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } var kinds = args[1].Split(',').Select(x => x.ToLower()); // Is there any chat type that's not recognized? if (kinds.Any(x => XivChatTypeExtensions.TypeInfoDict.All(y => y.Value.Slug != x) && x != "any")) { await SendGenericEmbed(message.Channel, $"One or more of the chat kinds you specified could not be found.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } if (!this.plugin.Config.ChannelConfigs.TryGetValue(message.Channel.Id, out var config)) { config = new DiscordChannelConfig(); } foreach (var selectedKind in kinds) { if (selectedKind == "any") { config.UnsetUnique(DefaultChatTypes); } else { var chatType = XivChatTypeExtensions.GetBySlug(selectedKind); config.UnsetUnique(chatType); } } this.plugin.Config.ChannelConfigs[message.Channel.Id] = config; this.plugin.Config.Save(); if (config.ChatTypes.Count() == 0) { await SendGenericEmbed(message.Channel, $"All chat kinds have been removed from this channel.", "Chat Kinds unset", EmbedColorFine); } await SendGenericEmbed(message.Channel, $"OK! This channel will still receive the following chat kinds:\n\n```\n{config.ChatTypes.Select(x => $"{x.GetSlug()} - {x.GetFancyName()}").Aggregate((x, y) => x + "\n" + y)}```", "Chat kinds unset", EmbedColorFine); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "setprefix" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length < 3) { await SendGenericEmbed(message.Channel, $"You need to specify some chat kinds and a prefix to use.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } var kinds = args[1].Split(',').Select(x => x.ToLower()); // Is there any chat type that's not recognized? if (kinds.Any(x => XivChatTypeExtensions.TypeInfoDict.All(y => y.Value.Slug != x) && x != "any")) { await SendGenericEmbed(message.Channel, $"One or more of the chat kinds you specified could not be found.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } if (args[2] == "none") { args[2] = string.Empty; } foreach (var selectedKind in kinds) { var type = XivChatTypeExtensions.GetBySlug(selectedKind); this.plugin.Config.PrefixConfigs[type] = args[2]; } this.plugin.Config.Save(); await SendGenericEmbed(message.Channel, $"OK! The following prefixes are set:\n\n```\n{this.plugin.Config.PrefixConfigs.Select(x => $"{x.Key.GetFancyName()} - {x.Value}").Aggregate((x, y) => x + "\n" + y)}```", "Prefix set", EmbedColorFine); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "unsetprefix" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length < 2) { await SendGenericEmbed(message.Channel, $"You need to specify some chat kinds and a prefix to use.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } var kinds = args[1].Split(',').Select(x => x.ToLower()); // Is there any chat type that's not recognized? if (kinds.Any(x => XivChatTypeExtensions.TypeInfoDict.All(y => y.Value.Slug != x) && x != "any")) { await SendGenericEmbed(message.Channel, $"One or more of the chat kinds you specified could not be found.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } foreach (var selectedKind in kinds) { var type = XivChatTypeExtensions.GetBySlug(selectedKind); this.plugin.Config.PrefixConfigs.Remove(type); } this.plugin.Config.Save(); if (this.plugin.Config.PrefixConfigs.Count() == 0) { await SendGenericEmbed(message.Channel, $"All prefixes have been removed.", "Prefix unset", EmbedColorFine); } else // this doesn't seem to trigger when there's only one entry left. I don't know why. { await SendGenericEmbed(message.Channel, $"OK! The prefix for {XivChatTypeExtensions.GetBySlug(args[2])} has been removed.\n\n" + $"The following prefixes are still set:\n\n```\n{this.plugin.Config.PrefixConfigs.Select(x => $"{x.Key.GetFancyName()} - {x.Value}").Aggregate((x, y) => x + "\n" + y)}```", "Prefix unset", EmbedColorFine); } return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "setchattypename" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length < 3) { await SendGenericEmbed(message.Channel, $"You need to specify one or more chat kinds and a custom name.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } var kinds = args[1].Split(',').Select(x => x.ToLower()); var chatChannelOverride = string.Join(" ", args.Skip(2)).Trim('"'); // PluginLog.Information($"arg1: {args[1]}; arg2: {chatChannelOverride}"); // Is there any chat type that's not recognized? if (kinds.Any(x => XivChatTypeExtensions.TypeInfoDict.All(y => y.Value.Slug != x) && x != "any")) { await SendGenericEmbed(message.Channel, $"One or more of the chat kinds you specified could not be found.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } if (chatChannelOverride == "none") { foreach (var selectedKind in kinds) { var type = XivChatTypeExtensions.GetBySlug(selectedKind); this.plugin.Config.CustomSlugsConfigs[type] = type.GetSlug(); } await SendGenericEmbed(message.Channel, $"OK! The following custom chat type names have been set:\n\n```\n{this.plugin.Config.CustomSlugsConfigs.Select(x => $"{x.Key.GetFancyName()} - {x.Value}").Aggregate((x, y) => x + "\n" + y)}```", "Custom chat type set", EmbedColorFine); } else { foreach (var selectedKind in kinds) { var type = XivChatTypeExtensions.GetBySlug(selectedKind); this.plugin.Config.CustomSlugsConfigs[type] = chatChannelOverride; } await SendGenericEmbed(message.Channel, $"OK! The following custom chat type names have been set:\n\n```\n{this.plugin.Config.CustomSlugsConfigs.Select(x => $"{x.Key.GetFancyName()} - {x.Value}").Aggregate((x, y) => x + "\n" + y)}```", "Custom chat type set", EmbedColorFine); } this.plugin.Config.Save(); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "unsetchattypename" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length < 2) { await SendGenericEmbed(message.Channel, $"One or more of the chat kinds you specified could not be found.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } var kinds = args[1].Split(',').Select(x => x.ToLower()); PluginLog.Information($"Unsetting custom type name for arg1: {args[1]}"); // Is there any chat type that's not recognized? if (kinds.Any(x => XivChatTypeExtensions.TypeInfoDict.All(y => y.Value.Slug != x) && x != "any")) { await SendGenericEmbed(message.Channel, $"One or more of the chat kinds you specified could not be found.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } foreach (var selectedKind in kinds) { var type = XivChatTypeExtensions.GetBySlug(selectedKind); this.plugin.Config.CustomSlugsConfigs[type] = type.GetSlug(); } await SendGenericEmbed(message.Channel, $"OK! The following custom chat type names have been set:\n\n```\n{this.plugin.Config.CustomSlugsConfigs.Select(x => $"{x.Key.GetFancyName()} - {x.Value}").Aggregate((x, y) => x + "\n" + y)}```", "Custom chat type unset", EmbedColorFine); this.plugin.Config.Save(); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "setduplicatems" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length == 1) { await SendGenericEmbed(message.Channel, $"You need to specify a number in milliseconds to use.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } var kinds = args[1].Split(',').Select(x => x.ToLower()); // Make sure that it's a number (or assume it is) int newDelay = int.Parse(args[1]); this.plugin.Config.DuplicateCheckMS = newDelay; this.plugin.Config.Save(); await SendGenericEmbed(message.Channel, $"OK! Any messages with the same content within the last **{newDelay}** milliseconds will be skipped, preventing duplicate posts.", "Duplicate Message Check", EmbedColorFine); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "toggledf" && await EnsureOwner(message.Author, message.Channel)) { if (!this.plugin.Config.ChannelConfigs.TryGetValue(message.Channel.Id, out var config)) { config = new DiscordChannelConfig(); } config.IsContentFinder = !config.IsContentFinder; this.plugin.Config.ChannelConfigs[message.Channel.Id] = config; this.plugin.Config.Save(); await SendGenericEmbed(message.Channel, $"OK! This channel has been {(config.IsContentFinder ? "enabled" : "disabled")} from receiving Duty Finder notifications.", "Duty Finder set", EmbedColorFine); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "setcfprefix" && await EnsureOwner(message.Author, message.Channel)) { // Are there parameters? if (args.Length < 2) { await SendGenericEmbed(message.Channel, $"You need to specify a prefix to use, or type \"none\" if you want to remove it.\nCheck the ``{this.plugin.Config.DiscordBotPrefix}help`` command for more information.", "Error", EmbedColorError); return; } if (args[1] == "none") { args[1] = string.Empty; } this.plugin.Config.CFPrefixConfig = args[1]; this.plugin.Config.Save(); await SendGenericEmbed(message.Channel, $"OK! The following prefix was set:\n\n```\n{this.plugin.Config.CFPrefixConfig}```", "Prefix set", EmbedColorFine); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "listchannel" && await EnsureOwner(message.Author, message.Channel)) { if (!this.plugin.Config.ChannelConfigs.TryGetValue(message.Channel.Id, out var config)) { await SendGenericEmbed(message.Channel, $"You didn't set up any channel kinds for this channel yet.\nPlease use the ``{this.plugin.Config.DiscordBotPrefix}setchannel`` command to do this.", "Error", EmbedColorError); return; } if (config == null || config.ChatTypes.Count == 0) { await SendGenericEmbed(message.Channel, $"There are no channel kinds set for this channel right now.\nPlease use the ``{this.plugin.Config.DiscordBotPrefix}setchannel`` command to do this.", "Error", EmbedColorFine); return; } await SendGenericEmbed(message.Channel, $"OK! This channel has been set to receive the following chat kinds:\n\n```\n{config.ChatTypes.Select(x => $"{x.GetFancyName()}").Aggregate((x, y) => x + "\n" + y)}```", "Chat kinds set", EmbedColorFine); return; } if (args[0] == this.plugin.Config.DiscordBotPrefix + "help") { PluginLog.Verbose("Help time"); var builder = new EmbedBuilder() .WithTitle("Discord Bridge Help") .WithDescription("You can use the following commands to set up the Discord bridge.") .WithColor(new Color(EmbedColorFine)) .AddField($"{this.plugin.Config.DiscordBotPrefix}setchannel", "Select, which kinds of chat should arrive in this channel.\n" + $"Format: ``{this.plugin.Config.DiscordBotPrefix}setchannel <kind1,kind2,...>``\n\n" + $"See [this link for a list of all available chat kinds]({Constant.KindListLink}) or type ``any`` to enable it for all regular chat messages.") //$"The following chat kinds are available:\n```all - All regular chat\n{XivChatTypeExtensions.TypeInfoDict.Select(x => $"{x.Value.Slug} - {x.Value.FancyName}").Aggregate((x, y) => x + "\n" + y)}```") .AddField($"{this.plugin.Config.DiscordBotPrefix}unsetchannel", "Works like the previous command, but removes kinds of chat from the list of kinds that are sent to this channel.") .AddField($"{this.plugin.Config.DiscordBotPrefix}listchannel", "List all chat kinds that are sent to this channel.") .AddField($"{this.plugin.Config.DiscordBotPrefix}toggledf", "Enable or disable sending duty finder updates to this channel.") .AddField($"{this.plugin.Config.DiscordBotPrefix}setduplicatems", "Set time in milliseconds that the bot will check to see if any past messages were the same. Default is 0 ms.") .AddField($"{this.plugin.Config.DiscordBotPrefix}setprefix", "Set a prefix for chat kinds. " + $"This can be an emoji or a string that will be prepended to every chat message that will arrive with this chat kind. " + $"You can also set it to `none` if you want to remove it.\n" + $"Format: ``{this.plugin.Config.DiscordBotPrefix}setchannel <kind1,kind2,...> <prefix>``") .AddField($"{this.plugin.Config.DiscordBotPrefix}setcfprefix", "Set a prefix for duty finder posts. " + $"You can also set it to `none` if you want to remove it.\n" + $"Format: ``{this.plugin.Config.DiscordBotPrefix}setcfprefix <prefix>``") .AddField($"{this.plugin.Config.DiscordBotPrefix}setchattypename ", "Set custom text for chat kinds. " + $"This can be an emoji or a string that will replace the short name of a chat kind for every chat message that will arrive with this chat kind. " + $"You can also set it to `none` if you want to remove it.\n" + $"Format: ``{this.plugin.Config.DiscordBotPrefix}setchattypename <kind1,kind2,...> <custom text>``") .AddField($"{this.plugin.Config.DiscordBotPrefix}unsetprefix", "Remove prefix set for a chat kind. \n" + $"Format: ``{this.plugin.Config.DiscordBotPrefix}unsetprefix <kind>``") .AddField($"{this.plugin.Config.DiscordBotPrefix}unsetchattypename", "Remove custom name for a chat kind. \n" + $"Format: ``{this.plugin.Config.DiscordBotPrefix}unsetchattypename <kind>``") .AddField("Need more help?", $"You can [read the full step-by-step guide]({Constant.HelpLink}) or [join our Discord server]({Constant.DiscordJoinLink}) to ask for help.") .WithFooter(footer => { footer .WithText("Dalamud Discord Bridge") .WithIconUrl(Constant.LogoLink); }) .WithThumbnailUrl(Constant.LogoLink); var embed = builder.Build(); var m = await message.Channel.SendMessageAsync( null, embed : embed) .ConfigureAwait(false); ; PluginLog.Verbose(m.Id.ToString()); return; } } catch (Exception ex) { PluginLog.Error(ex, "Could not handle incoming Discord message."); } }