commit 07b3100fc523f1a5caef048343af2139b6db8b2e Author: Anna Clemens Date: Sun May 30 16:22:26 2021 -0400 chore: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1db30bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,365 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# Packaging +pack/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..4153cd3 --- /dev/null +++ b/LICENCE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. diff --git a/RoleplayersToolbox.sln b/RoleplayersToolbox.sln new file mode 100755 index 0000000..a74e8cc --- /dev/null +++ b/RoleplayersToolbox.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoleplayersToolbox", "RoleplayersToolbox\RoleplayersToolbox.csproj", "{EE70A649-AAD3-485E-A1FA-234A192461DF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release Official|Any CPU = Release Official|Any CPU + Release Illegal|Any CPU = Release Illegal|Any CPU + Debug Official|Any CPU = Debug Official|Any CPU + Debug Illegal|Any CPU = Debug Illegal|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Release Official|Any CPU.ActiveCfg = Release Official|Any CPU + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Release Official|Any CPU.Build.0 = Release Official|Any CPU + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Release Illegal|Any CPU.ActiveCfg = Release Illegal|Any CPU + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Release Illegal|Any CPU.Build.0 = Release Illegal|Any CPU + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Debug Official|Any CPU.ActiveCfg = Debug Official|Any CPU + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Debug Official|Any CPU.Build.0 = Debug Official|Any CPU + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Debug Illegal|Any CPU.ActiveCfg = Debug Illegal|Any CPU + {EE70A649-AAD3-485E-A1FA-234A192461DF}.Debug Illegal|Any CPU.Build.0 = Debug Illegal|Any CPU + EndGlobalSection +EndGlobal diff --git a/RoleplayersToolbox/Commands.cs b/RoleplayersToolbox/Commands.cs new file mode 100755 index 0000000..ddd616b --- /dev/null +++ b/RoleplayersToolbox/Commands.cs @@ -0,0 +1,22 @@ +using System; +using Dalamud.Game.Command; + +namespace RoleplayersToolbox { + internal class Commands : IDisposable { + private Plugin Plugin { get; } + + internal Commands(Plugin plugin) { + this.Plugin = plugin; + + this.Plugin.Interface.CommandManager.AddHandler("/rptools", new CommandInfo(this.OnCommand)); + } + + public void Dispose() { + this.Plugin.Interface.CommandManager.RemoveHandler("/rptools"); + } + + private void OnCommand(string command, string arguments) { + this.Plugin.Ui.ShowInterface ^= true; + } + } +} diff --git a/RoleplayersToolbox/Configuration.cs b/RoleplayersToolbox/Configuration.cs new file mode 100755 index 0000000..cb3adb3 --- /dev/null +++ b/RoleplayersToolbox/Configuration.cs @@ -0,0 +1,12 @@ +using System; +using Dalamud.Configuration; +using RoleplayersToolbox.Tools; + +namespace RoleplayersToolbox { + [Serializable] + internal class Configuration : IPluginConfiguration { + public int Version { get; set; } = 1; + + public ToolConfig Tools { get; set; } = new(); + } +} diff --git a/RoleplayersToolbox/DalamudPlugin.cs b/RoleplayersToolbox/DalamudPlugin.cs new file mode 100755 index 0000000..0b73038 --- /dev/null +++ b/RoleplayersToolbox/DalamudPlugin.cs @@ -0,0 +1,18 @@ +using Dalamud.Plugin; + +namespace RoleplayersToolbox { + // ReSharper disable once UnusedType.Global + public class DalamudPlugin : IDalamudPlugin { + public string Name => "The Roleplayer's Toolbox"; + + private Plugin Plugin { get; set; } = null!; + + public void Initialize(DalamudPluginInterface pluginInterface) { + this.Plugin = new Plugin(pluginInterface); + } + + public void Dispose() { + this.Plugin.Dispose(); + } + } +} diff --git a/RoleplayersToolbox/FodyWeavers.xml b/RoleplayersToolbox/FodyWeavers.xml new file mode 100755 index 0000000..2dfb1f4 --- /dev/null +++ b/RoleplayersToolbox/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/RoleplayersToolbox/Plugin.cs b/RoleplayersToolbox/Plugin.cs new file mode 100755 index 0000000..528d8a8 --- /dev/null +++ b/RoleplayersToolbox/Plugin.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Dalamud.Plugin; +using Lumina; +using RoleplayersToolbox.Tools; +using RoleplayersToolbox.Tools.Housing; +using RoleplayersToolbox.Tools.Targeting; +using XivCommon; +#if ILLEGAL +using RoleplayersToolbox.Tools.Illegal.Emote; +using RoleplayersToolbox.Tools.Illegal.EmoteSnap; + +#endif + +namespace RoleplayersToolbox { + internal class Plugin : IDisposable { + internal DalamudPluginInterface Interface { get; } + internal GameData? GameData { get; } + internal Configuration Config { get; } + internal XivCommonBase Common { get; } + internal List Tools { get; } = new(); + internal PluginUi Ui { get; } + private Commands Commands { get; } + + public Plugin(DalamudPluginInterface pluginInterface) { + this.Interface = pluginInterface; + this.GameData = (GameData?) this.Interface.Data + .GetType() + .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) + ?.GetValue(this.Interface.Data); + this.Config = this.Interface.GetPluginConfig() as Configuration ?? new Configuration(); + this.Common = new XivCommonBase(pluginInterface, Hooks.ContextMenu | Hooks.PartyFinderListings); + + this.Ui = new PluginUi(this); + + this.Tools.Add(new HousingTool(this)); + this.Tools.Add(new TargetingTool(this)); + + #if ILLEGAL + this.Tools.Add(new EmoteTool(this)); + this.Tools.Add(new EmoteSnapTool(this)); + #endif + + this.Commands = new Commands(this); + + if (this.GameData == null) { + PluginLog.LogWarning("Could not find GameData - some features will be disabled"); + } + } + + public void Dispose() { + this.Commands.Dispose(); + this.Ui.Dispose(); + + foreach (var tool in this.Tools) { + if (tool is IDisposable disposable) { + disposable.Dispose(); + } + } + + this.Tools.Clear(); + + this.Common.Dispose(); + } + + internal void SaveConfig() { + this.Interface.SavePluginConfig(this.Config); + } + } +} diff --git a/RoleplayersToolbox/PluginUi.cs b/RoleplayersToolbox/PluginUi.cs new file mode 100755 index 0000000..009a0e8 --- /dev/null +++ b/RoleplayersToolbox/PluginUi.cs @@ -0,0 +1,92 @@ +using System; +using System.Numerics; +using Dalamud.Plugin; +using ImGuiNET; + +namespace RoleplayersToolbox { + internal class PluginUi : IDisposable { + internal Plugin Plugin { get; } + + private bool _showInterface; + + internal bool ShowInterface { + get => this._showInterface; + set => this._showInterface = value; + } + + internal PluginUi(Plugin plugin) { + this.Plugin = plugin; + + this.Plugin.Interface.UiBuilder.OnBuildUi += this.Draw; + this.Plugin.Interface.UiBuilder.OnOpenConfigUi += this.OpenConfig; + } + + public void Dispose() { + this.Plugin.Interface.UiBuilder.OnOpenConfigUi -= this.OpenConfig; + this.Plugin.Interface.UiBuilder.OnBuildUi -= this.Draw; + } + + private void OpenConfig(object? sender = null, object? args = null) { + this.ShowInterface = true; + } + + private void Draw() { + this.DrawSettings(); + + foreach (var tool in this.Plugin.Tools) { + try { + tool.DrawAlways(); + } catch (Exception ex) { + PluginLog.LogError(ex, $"Error drawing tool: {tool.Name}"); + } + } + } + + private void DrawSettings() { + if (!this.ShowInterface) { + return; + } + + ImGui.SetNextWindowSize(new Vector2(450, 300), ImGuiCond.FirstUseEver); + + if (!ImGui.Begin("The Roleplayer's Toolbox", ref this._showInterface)) { + ImGui.End(); + return; + } + + if (ImGui.BeginTabBar("rp-toolbox-tabs")) { + var anyChanged = false; + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var tool in this.Plugin.Tools) { + if (!ImGui.BeginTabItem($"{tool.Name}")) { + continue; + } + + if (ImGui.BeginChild($"{tool.Name} child", new Vector2(-1, -1))) { + ImGui.PushTextWrapPos(); + + try { + tool.DrawSettings(ref anyChanged); + } catch (Exception ex) { + PluginLog.LogError(ex, $"Error drawing settings for tool: {tool.Name}"); + } + + ImGui.PopTextWrapPos(); + ImGui.EndChild(); + } + + ImGui.EndTabItem(); + } + + if (anyChanged) { + this.Plugin.SaveConfig(); + } + + ImGui.EndTabBar(); + } + + ImGui.End(); + } + } +} diff --git a/RoleplayersToolbox/RoleplayersToolbox.csproj b/RoleplayersToolbox/RoleplayersToolbox.csproj new file mode 100755 index 0000000..a7dd6da --- /dev/null +++ b/RoleplayersToolbox/RoleplayersToolbox.csproj @@ -0,0 +1,59 @@ + + + net48 + 1.0.0 + latest + enable + true + Release Official;Release Illegal;Debug Official;Debug Illegal + AnyCPU + + + TRACE;RELEASE; + true + + + TRACE;RELEASE;ILLEGAL + true + + + TRACE;DEBUG; + true + + + TRACE;DEBUG;ILLEGAL; + true + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + False + + + + + + + + + diff --git a/RoleplayersToolbox/Teleport.cs b/RoleplayersToolbox/Teleport.cs new file mode 100755 index 0000000..b0d57d7 --- /dev/null +++ b/RoleplayersToolbox/Teleport.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; +using RoleplayersToolbox.Tools.Housing; + +namespace RoleplayersToolbox { + public class Teleport { + private static class Signatures { + internal const string Teleport = "E8 ?? ?? ?? ?? 48 8B 4B 10 84 C0 48 8B 01 74 2C"; + internal const string TelepoAddress = "48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 84 C0 74 15 48 8B CB E8 ?? ?? ?? ?? 48 8B CB 48 83 C4 20"; + } + + private delegate bool TeleportDelegate(IntPtr tpStatusPtr, uint aetheryteId, byte subIndex); + + private readonly IntPtr _telepo; + private readonly TeleportDelegate? _teleport; + + private DataManager Data { get; } + + internal Teleport(Plugin plugin) { + this.Data = plugin.Interface.Data; + + plugin.Interface.TargetModuleScanner.TryGetStaticAddressFromSig(Signatures.TelepoAddress, out this._telepo); + + if (plugin.Interface.TargetModuleScanner.TryScanText(Signatures.Teleport, out var teleportPtr)) { + this._teleport = Marshal.GetDelegateForFunctionPointer(teleportPtr); + } + } + + internal void TeleportToHousingArea(HousingArea area) { + if (this._telepo == IntPtr.Zero || this._teleport == null) { + return; + } + + var aetheryte = this.Data.GetExcelSheet().FirstOrDefault(aeth => aeth.IsAetheryte && aeth.Territory.Row == area.CityStateTerritoryType()); + if (aetheryte == null) { + return; + } + + this._teleport(this._telepo, aetheryte.RowId, 0); + } + } +} diff --git a/RoleplayersToolbox/Tools/BaseTool.cs b/RoleplayersToolbox/Tools/BaseTool.cs new file mode 100755 index 0000000..bd53f1b --- /dev/null +++ b/RoleplayersToolbox/Tools/BaseTool.cs @@ -0,0 +1,11 @@ +namespace RoleplayersToolbox.Tools { + internal abstract class BaseTool : ITool { + public abstract string Name { get; } + + public abstract void DrawSettings(ref bool anyChanged); + + public virtual void DrawAlways() { + // do nothing + } + } +} diff --git a/RoleplayersToolbox/Tools/Housing/DestinationInfo.cs b/RoleplayersToolbox/Tools/Housing/DestinationInfo.cs new file mode 100755 index 0000000..dda124f --- /dev/null +++ b/RoleplayersToolbox/Tools/Housing/DestinationInfo.cs @@ -0,0 +1,55 @@ +using Lumina.Excel.GeneratedSheets; + +namespace RoleplayersToolbox.Tools.Housing { + internal class DestinationInfo { + private HousingInfo Info { get; } + private HousingArea? _area; + private uint? _plot; + + public World? World { get; set; } + + public HousingArea? Area { + get => this._area; + set { + this._area = value; + this.CalculateClosest(); + } + } + + public uint? Ward { get; set; } + + public uint? Plot { + get => this._plot; + set { + this._plot = value; + this.CalculateClosest(); + } + } + + public HousingAethernet? ClosestAethernet { get; private set; } + + internal DestinationInfo(HousingInfo info, World? world, HousingArea? area, uint? ward, uint? plot) { + this.Info = info; + + this.World = world; + this.Area = area; + this.Ward = ward; + this.Plot = plot; + } + + internal DestinationInfo(HousingInfo info) { + this.Info = info; + } + + private void CalculateClosest() { + if (this.Area == null || this.Plot == null) { + this.ClosestAethernet = null; + return; + } + + this.ClosestAethernet = this.Info.Distances.GetClosest(this.Area.Value, this.Plot.Value); + } + + internal bool AnyNull() => this.World == null || this.Area == null || this.Ward == null || this.Plot == null; + } +} diff --git a/RoleplayersToolbox/Tools/Housing/HousingArea.cs b/RoleplayersToolbox/Tools/Housing/HousingArea.cs new file mode 100755 index 0000000..e4667ac --- /dev/null +++ b/RoleplayersToolbox/Tools/Housing/HousingArea.cs @@ -0,0 +1,34 @@ +using System; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; + +namespace RoleplayersToolbox.Tools.Housing { + internal enum HousingArea { + Mist = 339, + LavenderBeds = 340, + Goblet = 341, + Shirogane = 641, + } + + internal static class HousingAreaExtensions { + public static string Name(this HousingArea area) => area switch { + HousingArea.Mist => "Mist", + HousingArea.LavenderBeds => "Lavender Beds", + HousingArea.Goblet => "Goblet", + HousingArea.Shirogane => "Shirogane", + _ => throw new ArgumentOutOfRangeException(nameof(area), area, null), + }; + + public static ushort CityStateTerritoryType(this HousingArea area) => area switch { + HousingArea.Mist => 129, + HousingArea.LavenderBeds => 132, + HousingArea.Goblet => 130, + HousingArea.Shirogane => 628, + _ => throw new ArgumentOutOfRangeException(nameof(area), area, null), + }; + + public static TerritoryType CityState(this HousingArea area, DataManager data) { + return data.GetExcelSheet().GetRow(area.CityStateTerritoryType()); + } + } +} diff --git a/RoleplayersToolbox/Tools/Housing/HousingConfig.cs b/RoleplayersToolbox/Tools/Housing/HousingConfig.cs new file mode 100755 index 0000000..4cfcc72 --- /dev/null +++ b/RoleplayersToolbox/Tools/Housing/HousingConfig.cs @@ -0,0 +1,17 @@ +using System; +using RoleplayersToolbox.Tools.Housing; + +namespace RoleplayersToolbox.Tools { + internal partial class ToolConfig { + public HousingConfig Housing { get; set; } = new(); + } +} + +namespace RoleplayersToolbox.Tools.Housing { + [Serializable] + internal class HousingConfig { + public bool PlaceFlagOnSelect = true; + public bool CloseMapOnApproach = true; + public bool ClearFlagOnApproach = true; + } +} diff --git a/RoleplayersToolbox/Tools/Housing/HousingDistances.cs b/RoleplayersToolbox/Tools/Housing/HousingDistances.cs new file mode 100755 index 0000000..d7cad55 --- /dev/null +++ b/RoleplayersToolbox/Tools/Housing/HousingDistances.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; + +namespace RoleplayersToolbox.Tools.Housing { + internal class HousingDistances { + private static Dictionary> Overrides { get; } = new() { + [HousingArea.LavenderBeds] = new() { + [14] = 1966102, // Lavender East + [15] = 1966102, + [44] = 1966110, // Lavender South Subdivision + [45] = 1966110, + }, + [HousingArea.Shirogane] = new() { + [5] = 1966135, // Southern Shirogane + }, + }; + + private DataManager Data { get; } + private Dictionary> Closest { get; } + + public HousingDistances(DataManager data, Dictionary> closest) { + this.Data = data; + this.Closest = closest; + } + + internal HousingAethernet? GetClosest(HousingArea area, uint plot) { + if (Overrides.TryGetValue(area, out var overridePlots)) { + if (overridePlots.TryGetValue(plot, out var overrideId)) { + var overrideAethernet = this.Data.GetExcelSheet().GetRow(overrideId); + if (overrideAethernet != null) { + return overrideAethernet; + } + } + } + + if (!this.Closest.TryGetValue(area, out var plots)) { + return null; + } + + return plots.TryGetValue(plot, out var aethernet) ? aethernet : null; + } + } +} diff --git a/RoleplayersToolbox/Tools/Housing/HousingInfo.cs b/RoleplayersToolbox/Tools/Housing/HousingInfo.cs new file mode 100755 index 0000000..ec01d78 --- /dev/null +++ b/RoleplayersToolbox/Tools/Housing/HousingInfo.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using Dalamud.Data; +using Dalamud.Plugin; +using Lumina; +using Lumina.Data.Files; +using Lumina.Data.Parsing.Layer; +using Lumina.Excel.GeneratedSheets; + +namespace RoleplayersToolbox.Tools.Housing { + internal class HousingInfo { + private DataManager Data { get; } + private GameData? GameData { get; } + private Dictionary LgbObjects { get; } = new(); + internal HousingDistances Distances { get; } + + internal HousingInfo(Plugin plugin) { + this.Data = plugin.Interface.Data; + this.GameData = plugin.GameData; + this.Distances = this.PrecalculateClosest(); + } + + private HousingAethernet? CalculateClosest(HousingArea area, uint plot) { + // subtract 1 from the subrow because Lumina is zero-indexed even though the sheet isn't + var info = this.Data.GetExcelSheet().GetRow((uint) area, plot - 1); + if (info == null) { + return null; + } + + var (x, y, z) = (info.X, info.Y, info.Z); + + (HousingAethernet aethernet, double distance)? shortest = null; + foreach (var aethernet in this.Data.GetExcelSheet()) { + if (aethernet.TerritoryType.Row != (uint) area) { + continue; + } + + var level = aethernet.Level.Row; + if (!this.LgbObjects.TryGetValue(level, out var obj)) { + continue; + } + + var translation = obj.Transform.Translation; + var xDiff = translation.X - x; + var yDiff = translation.Y - y; + var zDiff = translation.Z - z; + + var sumOfSquares = Math.Pow(xDiff, 2) + Math.Pow(yDiff, 2) + Math.Pow(zDiff, 2); + var distance = Math.Sqrt(sumOfSquares); + + if (shortest == null || shortest.Value.distance > distance) { + shortest = (aethernet, distance); + } + } + + return shortest?.aethernet; + } + + private HousingDistances PrecalculateClosest() { + var allClosest = new Dictionary>(); + + foreach (var area in (HousingArea[]) Enum.GetValues(typeof(HousingArea))) { + this.LoadObjectsFromArea(area); + + for (var plot = 1u; plot < 63; plot++) { + var closest = this.CalculateClosest(area, plot); + if (closest == null) { + continue; + } + + if (!allClosest.ContainsKey(area)) { + allClosest[area] = new Dictionary(); + } + + allClosest[area][plot] = closest; + } + } + + return new HousingDistances(this.Data, allClosest); + } + + internal LgbFile? GetLgbFromPath(string path) { + if (this.GameData == null) { + return null; + } + + try { + return this.GameData.GetFile(path); + } catch (Exception ex) { + PluginLog.LogError(ex, $"Error reading lgb file: {path}"); + return null; + } + } + + internal LgbFile? GetLgbFromArea(HousingArea area) { + var territory = this.Data.GetExcelSheet().GetRow((uint) area); + if (territory == null) { + return null; + } + + var path = territory.Bg.ToString(); + path = path.Substring(0, path.LastIndexOf('/')); + return this.GetLgbFromPath($"bg/{path}/planmap.lgb"); + } + + internal void LoadObjectsFromFile(LgbFile lgb) { + foreach (var layer in lgb.Layers) { + foreach (var obj in layer.InstanceObjects) { + this.LgbObjects[obj.InstanceId] = obj; + } + } + } + + internal void LoadObjectsFromPath(string path) { + var lgb = this.GetLgbFromPath(path); + if (lgb == null) { + return; + } + + this.LoadObjectsFromFile(lgb); + } + + internal void LoadObjectsFromArea(HousingArea area) { + var lgb = this.GetLgbFromArea(area); + if (lgb == null) { + return; + } + + this.LoadObjectsFromFile(lgb); + } + } +} diff --git a/RoleplayersToolbox/Tools/Housing/HousingTool.cs b/RoleplayersToolbox/Tools/Housing/HousingTool.cs new file mode 100755 index 0000000..ed3d54e --- /dev/null +++ b/RoleplayersToolbox/Tools/Housing/HousingTool.cs @@ -0,0 +1,477 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Game.Command; +using Dalamud.Game.Internal; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using XivCommon.Functions.ContextMenu; + +namespace RoleplayersToolbox.Tools.Housing { + internal class HousingTool : BaseTool, IDisposable { + private static class Signatures { + internal const string AddonMapHide = "40 53 48 83 EC 30 0F B6 91 ?? ?? ?? ?? 48 8B D9 E8 ?? ?? ?? ??"; + internal const string HousingPointer = "48 8B 05 ?? ?? ?? ?? 48 83 78 ?? ?? 74 16 48 8D 8F ?? ?? ?? ?? 66 89 5C 24 ?? 48 8D 54 24 ?? E8 ?? ?? ?? ?? 48 8B 7C 24"; + } + + // Updated: 5.55 + // 48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 80 3D ?? ?? ?? ?? ?? + private const int AgentMapId = 38; + + // AgentMap.vf8 has this offset if the sig above doesn't work + private const int AgentMapFlagSetOffset = 0x5997; + + private delegate IntPtr AddonMapHideDelegate(IntPtr addon); + + public override string Name => "Housing"; + private Plugin Plugin { get; } + private HousingConfig Config { get; } + private HousingInfo Info { get; } + private Teleport Teleport { get; } + + private DestinationInfo? _destination; + + private DestinationInfo? Destination { + get => this._destination; + set { + this._destination = value; + + if (value == null) { + this.ClearFlagAndCloseMap(); + } else if (this.Config.PlaceFlagOnSelect) { + this.FlagDestinationOnMap(); + } + } + } + + // Updated: 5.55 + // 48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC 20 49 8B 00 + private unsafe ushort CurrentWard { + get { + var objPtr = Util.FollowPointerChain(this._housingPointer, new[] { 0, 8 }); + return (ushort) (*(ushort*) (objPtr + 0x96a2) + 1); + } + } + + private readonly AddonMapHideDelegate? _addonMapHide; + private readonly IntPtr _housingPointer; + + internal HousingTool(Plugin plugin) { + this.Plugin = plugin; + this.Config = plugin.Config.Tools.Housing; + this.Info = new HousingInfo(plugin); + this.Teleport = new Teleport(plugin); + + if (this.Plugin.Interface.TargetModuleScanner.TryScanText(Signatures.AddonMapHide, out var addonMapHidePtr)) { + this._addonMapHide = Marshal.GetDelegateForFunctionPointer(addonMapHidePtr); + } + + this.Plugin.Interface.TargetModuleScanner.TryGetStaticAddressFromSig(Signatures.HousingPointer, out this._housingPointer); + + this.Plugin.Common.Functions.ContextMenu.OpenContextMenu += this.OnContextMenu; + this.Plugin.Interface.Framework.OnUpdateEvent += this.OnFramework; + this.Plugin.Interface.CommandManager.AddHandler("/route", new CommandInfo(this.OnCommand)); + } + + public void Dispose() { + this.Plugin.Interface.CommandManager.RemoveHandler("/route"); + this.Plugin.Interface.Framework.OnUpdateEvent -= this.OnFramework; + this.Plugin.Common.Functions.ContextMenu.OpenContextMenu -= this.OnContextMenu; + } + + public override void DrawSettings(ref bool anyChanged) { + anyChanged |= ImGui.Checkbox("Place flag and open map after selecting destination", ref this.Config.PlaceFlagOnSelect); + anyChanged |= ImGui.Checkbox("Clear flag on approach", ref this.Config.ClearFlagOnApproach); + anyChanged |= ImGui.Checkbox("Close map on approach", ref this.Config.CloseMapOnApproach); + + ImGui.Separator(); + + if (ImGui.Button("Open routing window")) { + this.Destination = new DestinationInfo(this.Info); + } + + ImGui.TextUnformatted("You can also use the /route command for this."); + ImGui.TextUnformatted("Ex: /route jen lb w5 p3"); + } + + public override void DrawAlways() { + if (this.Destination == null) { + return; + } + + if (!ImGui.Begin("Housing destination", ImGuiWindowFlags.AlwaysAutoResize)) { + ImGui.End(); + return; + } + + ImGui.TextUnformatted("Routing to..."); + + var anyChanged = false; + + var world = this.Destination.World; + if (ImGui.BeginCombo("World", world?.Name?.ToString() ?? string.Empty)) { + var dataCentre = this.Plugin.Interface.ClientState.LocalPlayer?.HomeWorld?.GameData?.DataCenter?.Row; + + foreach (var availWorld in this.Plugin.Interface.Data.GetExcelSheet()) { + if (availWorld.DataCenter.Row != dataCentre || !availWorld.IsPublic) { + continue; + } + + if (!ImGui.Selectable(availWorld.Name.ToString())) { + continue; + } + + this.Destination.World = availWorld; + anyChanged = true; + } + + ImGui.EndCombo(); + } + + var area = this.Destination.Area; + if (ImGui.BeginCombo("Housing area", area?.Name() ?? string.Empty)) { + foreach (var housingArea in (HousingArea[]) Enum.GetValues(typeof(HousingArea))) { + if (!ImGui.Selectable(housingArea.Name(), area == housingArea)) { + continue; + } + + this.Destination.Area = housingArea; + anyChanged = true; + } + + ImGui.EndCombo(); + } + + var ward = (int) (this.Destination.Ward ?? 0); + if (ImGui.InputInt("Ward", ref ward)) { + this.Destination.Ward = (uint) Math.Max(1, Math.Min(60, ward)); + anyChanged = true; + } + + var plot = (int) (this.Destination.Plot ?? 0); + if (ImGui.InputInt("Plot", ref plot)) { + this.Destination.Plot = (uint) Math.Max(1, Math.Min(60, plot)); + anyChanged = true; + } + + if (ImGui.Button("Clear")) { + this.Destination = null; + } + + if (this.Destination?.Area != null) { + ImGui.SameLine(); + + var name = this.Destination.Area.Value.CityState(this.Plugin.Interface.Data).PlaceName.Value.Name; + if (ImGui.Button($"Teleport to {name}")) { + this.Teleport.TeleportToHousingArea(this.Destination.Area.Value); + } + } + + if (anyChanged) { + this.FlagDestinationOnMap(); + } + + ImGui.End(); + } + + private void OnCommand(string command, string arguments) { + var player = this.Plugin.Interface.ClientState.LocalPlayer; + if (player == null) { + return; + } + + this.Destination = InfoExtractor.Extract(arguments, player.HomeWorld.GameData.DataCenter.Row, this.Plugin.Interface.Data, this.Info); + } + + private void OnContextMenu(ContextMenuOpenArgs args) { + if (args.ParentAddonName != "LookingForGroup" || args.ContentIdLower == 0) { + return; + } + + args.Items.Add(new NormalContextMenuItem("Select as Destination", this.SelectDestination)); + } + + private void SelectDestination(ContextMenuItemSelectedArgs args) { + var listing = this.Plugin.Common.Functions.PartyFinder.CurrentListings.Values.FirstOrDefault(listing => listing.ContentIdLower == args.ContentIdLower); + if (listing == null) { + return; + } + + this.ClearFlag(); + this.Destination = InfoExtractor.Extract(listing.Description.TextValue, listing.World.Value.DataCenter.Row, this.Plugin.Interface.Data, this.Info); + } + + private void OnFramework(Framework framework) { + this.ClearIfNear(); + this.HighlightSelectString(); + this.HighlightResidentialTeleport(); + this.HighlightWorldTravel(); + } + + private void ClearIfNear() { + var destination = this.Destination; + if (destination == null || destination.AnyNull()) { + return; + } + + var info = this.Plugin.Interface.Data.GetExcelSheet().GetRow((uint) destination.Area!.Value, (uint) destination.Plot! - 1); + if (info == null) { + return; + } + + var player = this.Plugin.Interface.ClientState.LocalPlayer; + if (player == null) { + return; + } + + // ensure on correct world + if (player.CurrentWorld.GameData != destination.World) { + return; + } + + // ensure in correct zone + if (this.Plugin.Interface.ClientState.TerritoryType != (ushort) destination.Area) { + return; + } + + // ensure in correct ward + if (this.CurrentWard != destination.Ward) { + return; + } + + var localPos = player.Position; + var localPosCorrected = new Vector3(localPos.X, localPos.Z, localPos.Y); + var distance = Util.DistanceBetween(localPosCorrected, new Vector3(info.X, info.Y, info.Z)); + + if (distance >= 15) { + return; + } + + this._destination = null; + + if (this.Config.ClearFlagOnApproach) { + this.ClearFlag(); + } + + if (this.Config.CloseMapOnApproach) { + this.CloseMap(); + } + } + + private void ClearFlagAndCloseMap() { + this.ClearFlag(); + this.CloseMap(); + } + + private unsafe void ClearFlag() { + var mapAgent = this.Plugin.Common.Functions.GetAgentByInternalId(AgentMapId); + if (mapAgent != IntPtr.Zero) { + *(byte*) (mapAgent + AgentMapFlagSetOffset) = 0; + } + } + + private void CloseMap() { + var addon = this.Plugin.Interface.Framework.Gui.GetAddonByName("AreaMap", 1); + if (addon != null) { + this._addonMapHide?.Invoke(addon.Address); + } + } + + private void FlagDestinationOnMap() { + if (this.Destination?.Area == null || this.Destination?.Plot == null) { + return; + } + + this.FlagHouseOnMap(this.Destination.Area.Value, this.Destination.Plot.Value); + } + + private void FlagHouseOnMap(HousingArea area, uint plot) { + var info = this.Plugin.Interface.Data.GetExcelSheet().GetRow((uint) area, plot - 1); + if (info == null) { + return; + } + + var map = info.Map.Value; + var terr = map?.TerritoryType?.Value; + + if (terr == null) { + return; + } + + var mapLink = new MapLinkPayload( + this.Plugin.Interface.Data, + terr.RowId, + map!.RowId, + (int) (info.X * 1_000f), + (int) (info.Z * 1_000f) + ); + + this.Plugin.Interface.Framework.Gui.OpenMapWithMapLink(mapLink); + } + + private unsafe void HighlightResidentialTeleport() { + var addon = this.Plugin.Interface.Framework.Gui.GetAddonByName("HousingSelectBlock", 1); + if (addon == null) { + return; + } + + var shouldSet = false; + + var player = this.Plugin.Interface.ClientState.LocalPlayer; + if (player != null && this.Destination?.World != null) { + shouldSet = player.CurrentWorld.GameData == this.Destination.World; + } + + if (this.Destination?.Area == null) { + shouldSet = false; + } else { + var currentArea = this.Plugin.Interface.ClientState.TerritoryType; + shouldSet = shouldSet && (currentArea == (ushort) this.Destination.Area || currentArea == this.Destination.Area.Value.CityStateTerritoryType()); + } + + var unit = (AtkUnitBase*) addon.Address; + var uld = unit->UldManager; + if (uld.NodeListCount < 1) { + return; + } + + var parentNode = uld.NodeList[0]; + + var siblingCount = 0; + var prev = parentNode->ChildNode; + while ((prev = prev->PrevSiblingNode) != null) { + siblingCount += 1; + if (siblingCount == 8) { + break; + } + } + + var radioContainer = prev; + var radioButton = radioContainer->ChildNode; + do { + var component = (AtkComponentNode*) radioButton; + var radioUld = component->Component->UldManager; + if (radioUld.NodeListCount < 4) { + return; + } + + var textNode = (AtkTextNode*) radioUld.NodeList[3]; + var text = Util.ReadSeString((IntPtr) textNode->NodeText.StringPtr, this.Plugin.Interface.SeStringManager); + HighlightIf(radioButton, shouldSet && text.TextValue == $"{this.Destination?.Ward}"); + } while ((radioButton = radioButton->PrevSiblingNode) != null); + } + + private unsafe void HighlightSelectString() { + var addon = this.Plugin.Interface.Framework.Gui.GetAddonByName("SelectString", 1); + if (addon == null) { + return; + } + + var select = (AddonSelectString*) addon.Address; + var list = select->PopupMenu.List; + if (list == null) { + return; + } + + this.HighlightSelectStringItems(list); + } + + private bool ShouldHighlight(SeString str) { + var text = str.TextValue; + + var sameWorld = this.Destination?.World == this.Plugin.Interface.ClientState.LocalPlayer?.CurrentWorld?.GameData; + if (!sameWorld && this.Destination?.World != null) { + return text == " Visit Another World Server."; + } + + // TODO: figure out how to use HousingAethernet.Order with current one missing + var placeName = this.Destination?.ClosestAethernet?.PlaceName?.Value?.Name?.ToString(); + if (this.CurrentWard == this.Destination?.Ward && placeName != null && text.StartsWith(placeName) && text.Length == placeName.Length + 1) { + return true; + } + + // ReSharper disable once InvertIf + if (this.Destination?.Ward != null && this.Plugin.Interface.ClientState.TerritoryType == this.Destination?.Area?.CityStateTerritoryType()) { + switch (text) { + case " Residential District Aethernet.": + case "Go to specified ward. (Review Tabs)": + return true; + } + } + + return false; + } + + private unsafe void HighlightSelectStringItems(AtkComponentList* list) { + for (var i = 0; i < list->ListLength; i++) { + var item = list->ItemRendererList + i; + var button = item->AtkComponentListItemRenderer->AtkComponentButton; + var buttonText = Util.ReadSeString((IntPtr) button.ButtonTextNode->NodeText.StringPtr, this.Plugin.Interface.SeStringManager); + + var component = (AtkComponentBase*) item->AtkComponentListItemRenderer; + + HighlightIf(&component->OwnerNode->AtkResNode, this.ShouldHighlight(buttonText)); + } + } + + private unsafe void HighlightWorldTravel() { + var player = this.Plugin.Interface.ClientState.LocalPlayer; + if (player == null) { + return; + } + + var world = this.Destination?.World; + + var addon = this.Plugin.Interface.Framework.Gui.GetAddonByName("WorldTravelSelect", 1); + if (addon == null) { + return; + } + + var unit = (AtkUnitBase*) addon.Address; + var root = unit->RootNode; + if (root == null) { + return; + } + + var windowComponent = (AtkComponentNode*) root->ChildNode; + var informationBox = (AtkComponentNode*) windowComponent->AtkResNode.PrevSiblingNode; + var informationBoxBorder = (AtkNineGridNode*) informationBox->AtkResNode.PrevSiblingNode; + var worldListComponent = (AtkComponentNode*) informationBoxBorder->AtkResNode.PrevSiblingNode; + var listChild = worldListComponent->Component->UldManager.RootNode; + + var prev = listChild; + if (prev == null) { + return; + } + + do { + if ((uint) prev->Type != 1010) { + continue; + } + + var comp = (AtkComponentNode*) prev; + var res = comp->Component->UldManager.RootNode->PrevSiblingNode->PrevSiblingNode->PrevSiblingNode; + var text = (AtkTextNode*) res->ChildNode; + var str = Util.ReadSeString((IntPtr) text->NodeText.StringPtr, this.Plugin.Interface.SeStringManager); + HighlightIf(&text->AtkResNode, str.TextValue == world?.Name?.ToString()); + } while ((prev = prev->PrevSiblingNode) != null); + } + + private static unsafe void HighlightIf(AtkResNode* node, bool cond) { + if (cond) { + node->MultiplyRed = 0; + node->MultiplyGreen = 100; + node->MultiplyBlue = 0; + } else { + node->MultiplyRed = 100; + node->MultiplyGreen = 100; + node->MultiplyBlue = 100; + } + } + } +} diff --git a/RoleplayersToolbox/Tools/Housing/InfoExtractor.cs b/RoleplayersToolbox/Tools/Housing/InfoExtractor.cs new file mode 100755 index 0000000..47c582d --- /dev/null +++ b/RoleplayersToolbox/Tools/Housing/InfoExtractor.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Dalamud.Data; +using F23.StringSimilarity; +using Lumina.Excel.GeneratedSheets; + +namespace RoleplayersToolbox.Tools.Housing { + internal static class InfoExtractor { + private static readonly IReadOnlyDictionary HousingAreaNames = new Dictionary { + [HousingArea.LavenderBeds] = new[] { + new Regex(@"\blavender beds\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\blb\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\blav\s?beds\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\blav\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\blav\s?b\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + }, + [HousingArea.Goblet] = new[] { + new Regex(@"\bgoblet\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\bgob\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + }, + [HousingArea.Mist] = new[] { + new Regex(@"\bmist\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\bmists\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + }, + [HousingArea.Shirogane] = new[] { + new Regex(@"\bshirogane\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\bshiro\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), + }, + }; + + private static readonly JaroWinkler JaroWinkler = new(); + + private static readonly Regex CombinedWardPlot = new(@"w(?:ard)?\W{0,2}(\d{1,2})\W{0,2}p(?:lot)?\W{0,2}(\d{1,2})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex WardOnly = new(@"w(?:ard)?\W{0,2}(\d{1,2})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex PlotOnly = new(@"p(?:lot)?\W{0,2}(\d{1,2})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex DesperationCombined = new(@"(\d{1,2})\W{1,2}(\d{1,2})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static DestinationInfo Extract(string source, uint dataCentre, DataManager data, HousingInfo info) { + var world = FindWorld(source, dataCentre, data); + var area = FindHousingArea(source); + var (ward, plot) = FindWardPlot(source); + return new DestinationInfo(info, world, area, ward, plot); + } + + private static readonly Regex NonWord = new(@"\W", RegexOptions.Compiled); + + private static World? FindWorld(string source, uint dataCentre, DataManager data) { + var words = NonWord.Split(source).Where(word => word.ToLowerInvariant() != "gg").ToArray(); + var mostSimilar = data.Excel.GetSheet() + .Where(world => world.DataCenter.Row == dataCentre) + .SelectMany(world => { + var name = world.Name.ToString().ToLowerInvariant(); + return words.Select(word => (world, JaroWinkler.Similarity(name, word.ToLowerInvariant()))); + }) + .Where(entry => entry.Item2 > 0.75) + .OrderByDescending(entry => entry.Item2) + .FirstOrDefault(); + return mostSimilar == default ? null : mostSimilar.world; + } + + private static HousingArea? FindHousingArea(string source) { + foreach (var entry in HousingAreaNames) { + if (entry.Value.Any(regex => regex.IsMatch(source))) { + return entry.Key; + } + } + + return null; + } + + private static (uint? ward, uint? plot) FindWardPlot(string source) { + var combined = CombinedWardPlot.Match(source); + string? wardStr = null; + string? plotStr = null; + if (combined.Groups.Count == 3) { + wardStr = combined.Groups[1].Captures[0].Value; + plotStr = combined.Groups[2].Captures[0].Value; + goto Parse; + } + + var wardOnly = WardOnly.Match(source); + if (wardOnly.Groups.Count == 2) { + wardStr = wardOnly.Groups[1].Captures[0].Value; + } + + var plotOnly = PlotOnly.Match(source); + if (plotOnly.Groups.Count == 2) { + plotStr = plotOnly.Groups[1].Captures[0].Value; + } + + if (wardStr == null && plotStr == null) { + var desperation = DesperationCombined.Match(source); + if (desperation.Groups.Count == 3) { + wardStr = desperation.Groups[1].Captures[0].Value; + plotStr = desperation.Groups[2].Captures[0].Value; + } + } + + Parse: + uint? ward = null; + uint? plot = null; + + if (wardStr != null && uint.TryParse(wardStr, out var w)) { + ward = w; + } + + if (plotStr != null && uint.TryParse(plotStr, out var p)) { + plot = p; + } + + return (ward, plot); + } + } +} diff --git a/RoleplayersToolbox/Tools/ITool.cs b/RoleplayersToolbox/Tools/ITool.cs new file mode 100755 index 0000000..330e550 --- /dev/null +++ b/RoleplayersToolbox/Tools/ITool.cs @@ -0,0 +1,7 @@ +namespace RoleplayersToolbox.Tools { + internal interface ITool { + string Name { get; } + void DrawSettings(ref bool anyChanged); + void DrawAlways(); + } +} diff --git a/RoleplayersToolbox/Tools/Illegal/Emote/Emote.cs b/RoleplayersToolbox/Tools/Illegal/Emote/Emote.cs new file mode 100755 index 0000000..3478951 --- /dev/null +++ b/RoleplayersToolbox/Tools/Illegal/Emote/Emote.cs @@ -0,0 +1,20 @@ +#if ILLEGAL + +using System; + +namespace RoleplayersToolbox.Tools.Illegal.Emote { + internal enum Emote : uint { + ObjectSit = 96, + Sleep = 88, + } + + internal static class EmoteExt { + internal static string Name(this Emote emote) => emote switch { + Emote.ObjectSit => "Object sit", + Emote.Sleep => "Sleep", + _ => throw new ArgumentOutOfRangeException(nameof(emote), emote, null), + }; + } +} + +#endif diff --git a/RoleplayersToolbox/Tools/Illegal/Emote/EmoteTool.cs b/RoleplayersToolbox/Tools/Illegal/Emote/EmoteTool.cs new file mode 100755 index 0000000..df703db --- /dev/null +++ b/RoleplayersToolbox/Tools/Illegal/Emote/EmoteTool.cs @@ -0,0 +1,80 @@ +#if ILLEGAL + +using System; +using Dalamud.Hooking; +using ImGuiNET; + +namespace RoleplayersToolbox.Tools.Illegal.Emote { + internal class EmoteTool : BaseTool, IDisposable { + private static class Signatures { + internal const string SetActionOnHotbar = "E8 ?? ?? ?? ?? 4C 39 6F 08"; + } + + private delegate IntPtr SetActionOnHotbarDelegate(IntPtr a1, IntPtr a2, byte actionType, uint actionId); + + public override string Name => "Emotes"; + private Plugin Plugin { get; } + private Hook? SetActionOnHotbarHook { get; } + private bool Custom { get; set; } + private Emote? Emote { get; set; } + + internal EmoteTool(Plugin plugin) { + this.Plugin = plugin; + + if (this.Plugin.Interface.TargetModuleScanner.TryScanText(Signatures.SetActionOnHotbar, out var setPtr)) { + this.SetActionOnHotbarHook = new Hook(setPtr, new SetActionOnHotbarDelegate(this.SetActionOnHotbarDetour)); + this.SetActionOnHotbarHook.Enable(); + } + } + + public void Dispose() { + this.SetActionOnHotbarHook?.Dispose(); + } + + public override void DrawSettings(ref bool anyChanged) { + if (this.SetActionOnHotbarHook == null) { + ImGui.TextUnformatted("An update broke this tool. Please let Anna know."); + return; + } + + ImGui.TextUnformatted("Click one of the options below, then drag anything onto your hotbar. Instead of what you dragged, your hotbar will have that emote instead."); + + foreach (var emote in (Emote[]) Enum.GetValues(typeof(Emote))) { + if (ImGui.RadioButton(emote.Name(), !this.Custom && this.Emote == emote)) { + this.Custom = false; + this.Emote = emote; + } + } + + if (ImGui.RadioButton("Custom", this.Custom)) { + this.Custom = true; + this.Emote = null; + } + + if (this.Custom) { + var id = (int) (this.Emote ?? 0); + if (ImGui.InputInt("###custom-emote", ref id)) { + this.Emote = (Emote?) Math.Max(0, id); + } + } + + if (this.Emote != null && ImGui.Button("Cancel")) { + this.Custom = false; + this.Emote = null; + } + } + + private IntPtr SetActionOnHotbarDetour(IntPtr a1, IntPtr a2, byte actionType, uint actionId) { + var emote = this.Emote; + if (emote == null) { + return this.SetActionOnHotbarHook!.Original(a1, a2, actionType, actionId); + } + + this.Custom = false; + this.Emote = null; + return this.SetActionOnHotbarHook!.Original(a1, a2, 6, (uint) emote); + } + } +} + +#endif diff --git a/RoleplayersToolbox/Tools/Illegal/EmoteSnap/EmoteSnapConfig.cs b/RoleplayersToolbox/Tools/Illegal/EmoteSnap/EmoteSnapConfig.cs new file mode 100755 index 0000000..0f302c1 --- /dev/null +++ b/RoleplayersToolbox/Tools/Illegal/EmoteSnap/EmoteSnapConfig.cs @@ -0,0 +1,19 @@ +#if ILLEGAL + +using System; +using RoleplayersToolbox.Tools.Illegal.EmoteSnap; + +namespace RoleplayersToolbox.Tools { + internal partial class ToolConfig { + public EmoteSnapConfig EmoteSnap { get; set; } = new(); + } +} + +namespace RoleplayersToolbox.Tools.Illegal.EmoteSnap { + [Serializable] + internal class EmoteSnapConfig { + public bool DisableDozeSnap; + } +} + +#endif diff --git a/RoleplayersToolbox/Tools/Illegal/EmoteSnap/EmoteSnapTool.cs b/RoleplayersToolbox/Tools/Illegal/EmoteSnap/EmoteSnapTool.cs new file mode 100755 index 0000000..41cafff --- /dev/null +++ b/RoleplayersToolbox/Tools/Illegal/EmoteSnap/EmoteSnapTool.cs @@ -0,0 +1,49 @@ +#if ILLEGAL + +using System; +using Dalamud.Hooking; +using ImGuiNET; + +namespace RoleplayersToolbox.Tools.Illegal.EmoteSnap { + internal class EmoteSnapTool : BaseTool, IDisposable { + private static class Signatures { + internal const string ShouldSnap = "E8 ?? ?? ?? ?? 84 C0 74 46 4C 8D 6D C7"; + } + + private delegate byte ShouldSnapDelegate(IntPtr a1, IntPtr a2); + + public override string Name => "Emote Snap"; + + private Plugin Plugin { get; } + private EmoteSnapConfig Config { get; } + private Hook? ShouldSnapHook { get; } + + internal EmoteSnapTool(Plugin plugin) { + this.Plugin = plugin; + this.Config = this.Plugin.Config.Tools.EmoteSnap; + + if (this.Plugin.Interface.TargetModuleScanner.TryScanText(Signatures.ShouldSnap, out var snapPtr)) { + this.ShouldSnapHook = new Hook(snapPtr, new ShouldSnapDelegate(this.ShouldSnapDetour)); + this.ShouldSnapHook.Enable(); + } + } + + public void Dispose() { + this.ShouldSnapHook?.Dispose(); + } + + public override void DrawSettings(ref bool anyChanged) { + anyChanged |= ImGui.Checkbox("Disable /doze snap", ref this.Config.DisableDozeSnap); + + ImGui.TextUnformatted("Check this box to prevent /doze and the sleep emote from snapping. In order to use the sleep emote, you need to have it on your bar."); + } + + private byte ShouldSnapDetour(IntPtr a1, IntPtr a2) { + return this.Config.DisableDozeSnap + ? (byte) 0 + : this.ShouldSnapHook!.Original(a1, a2); + } + } +} + +#endif diff --git a/RoleplayersToolbox/Tools/Targeting/TargetingConfig.cs b/RoleplayersToolbox/Tools/Targeting/TargetingConfig.cs new file mode 100755 index 0000000..69d9274 --- /dev/null +++ b/RoleplayersToolbox/Tools/Targeting/TargetingConfig.cs @@ -0,0 +1,17 @@ +using System; +using RoleplayersToolbox.Tools.Targeting; + +namespace RoleplayersToolbox.Tools { + internal partial class ToolConfig { + public TargetingConfig Targeting { get; set; } = new(); + } +} + +namespace RoleplayersToolbox.Tools.Targeting { + [Serializable] + internal class TargetingConfig { + public bool LeftClickExamine; + public bool RightClickExamine; + public bool KeepTarget; + } +} diff --git a/RoleplayersToolbox/Tools/Targeting/TargetingTool.cs b/RoleplayersToolbox/Tools/Targeting/TargetingTool.cs new file mode 100755 index 0000000..263a0df --- /dev/null +++ b/RoleplayersToolbox/Tools/Targeting/TargetingTool.cs @@ -0,0 +1,112 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Actors; +using Dalamud.Game.ClientState.Structs; +using Dalamud.Hooking; +using ImGuiNET; + +namespace RoleplayersToolbox.Tools.Targeting { + internal class TargetingTool : BaseTool, IDisposable { + private static class Signatures { + internal const string LeftClickTarget = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 84 C0 74 16"; + internal const string RightClickTarget = "E8 ?? ?? ?? ?? 48 8B CE E8 ?? ?? ?? ?? 48 85 C0 74 1B"; + } + + private unsafe delegate void* ClickTargetDelegate(void** a1, void* a2, bool a3); + + public override string Name => "Targeting"; + private Plugin Plugin { get; } + private TargetingConfig Config { get; } + private Hook? LeftClickHook { get; } + private Hook? RightClickHook { get; } + + internal TargetingTool(Plugin plugin) { + this.Plugin = plugin; + this.Config = this.Plugin.Config.Tools.Targeting; + + if (this.Plugin.Interface.TargetModuleScanner.TryScanText(Signatures.LeftClickTarget, out var leftClickPtr)) { + unsafe { + this.LeftClickHook = new Hook(leftClickPtr, new ClickTargetDelegate(this.LeftClickDetour)); + } + + this.LeftClickHook.Enable(); + } + + if (this.Plugin.Interface.TargetModuleScanner.TryScanText(Signatures.RightClickTarget, out var rightClickPtr)) { + unsafe { + this.RightClickHook = new Hook(rightClickPtr, new ClickTargetDelegate(this.RightClickDetour)); + } + + this.RightClickHook.Enable(); + } + } + + public void Dispose() { + this.LeftClickHook?.Dispose(); + this.RightClickHook?.Dispose(); + } + + public override void DrawSettings(ref bool anyChanged) { + anyChanged |= ImGui.Checkbox("Enable left click to examine", ref this.Config.LeftClickExamine); + anyChanged |= ImGui.Checkbox("Enable right click to examine", ref this.Config.RightClickExamine); + anyChanged |= ImGui.Checkbox("Prevent removing or changing current target", ref this.Config.KeepTarget); + } + + private unsafe void* LeftClickDetour(void** a1, void* clickedOn, bool a3) { + var target = a1[16]; + + if (clickedOn == null) { + if (this.Config.KeepTarget) { + return this.LeftClickHook!.Original(a1, target, a3); + } + + goto Original; + } + + if (this.Config.LeftClickExamine) { + var actorStruct = Marshal.PtrToStructure((IntPtr) clickedOn); + if (actorStruct.ObjectKind == ObjectKind.Player) { + this.Plugin.Common.Functions.Examine.OpenExamineWindow(actorStruct.ActorId); + // tell game current target was left-clicked + return this.LeftClickHook!.Original(a1, target, a3); + } + } + + if (this.Config.KeepTarget && clickedOn != target) { + return this.LeftClickHook!.Original(a1, target, a3); + } + + Original: + return this.LeftClickHook!.Original(a1, clickedOn, a3); + } + + private unsafe void* RightClickDetour(void** a1, void* clickedOn, bool a3) { + if (clickedOn == null) { + goto Original; + } + + var target = a1[16]; + + if (this.Config.RightClickExamine) { + if (clickedOn == target) { + // allow right-clicking on target + goto Original; + } + + var actorStruct = Marshal.PtrToStructure((IntPtr) clickedOn); + if (actorStruct.ObjectKind == ObjectKind.Player) { + this.Plugin.Common.Functions.Examine.OpenExamineWindow(actorStruct.ActorId); + // tell game nothing was right-clicked + return this.RightClickHook!.Original(a1, null, a3); + } + } + + if (this.Config.KeepTarget && clickedOn != target) { + return this.RightClickHook!.Original(a1, null, a3); + } + + Original: + return this.RightClickHook!.Original(a1, clickedOn, a3); + } + } +} diff --git a/RoleplayersToolbox/Tools/ToolConfig.cs b/RoleplayersToolbox/Tools/ToolConfig.cs new file mode 100755 index 0000000..1adcaa4 --- /dev/null +++ b/RoleplayersToolbox/Tools/ToolConfig.cs @@ -0,0 +1,7 @@ +using System; + +namespace RoleplayersToolbox.Tools { + [Serializable] + internal partial class ToolConfig { + } +} diff --git a/RoleplayersToolbox/Util.cs b/RoleplayersToolbox/Util.cs new file mode 100755 index 0000000..e5a15de --- /dev/null +++ b/RoleplayersToolbox/Util.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Game; +using Dalamud.Game.Text.SeStringHandling; + +namespace RoleplayersToolbox { + internal static class Util { + public static double DistanceBetween(Vector3 a, Vector3 b) { + var xDiff = a.X - b.X; + var yDiff = a.Y - b.Y; + var zDiff = a.Z - b.Z; + var sumOfSquares = Math.Pow(xDiff, 2) + Math.Pow(yDiff, 2) + Math.Pow(zDiff, 2); + return Math.Sqrt(sumOfSquares); + } + + public static bool TryScanText(this SigScanner scanner, string sig, out IntPtr result) { + result = IntPtr.Zero; + try { + result = scanner.ScanText(sig); + return true; + } catch (KeyNotFoundException) { + return false; + } + } + + public static bool TryGetStaticAddressFromSig(this SigScanner scanner, string sig, out IntPtr result) { + result = IntPtr.Zero; + try { + result = scanner.GetStaticAddressFromSig(sig); + return true; + } catch (KeyNotFoundException) { + return false; + } + } + + public static SeString ReadSeString(IntPtr ptr, SeStringManager manager) { + var bytes = ReadTerminatedBytes(ptr); + return manager.Parse(bytes); + } + + public static string ReadString(IntPtr ptr) { + var bytes = ReadTerminatedBytes(ptr); + return Encoding.UTF8.GetString(bytes); + } + + private static unsafe byte[] ReadTerminatedBytes(IntPtr ptr) { + if (ptr == IntPtr.Zero) { + return new byte[0]; + } + + var bytes = new List(); + + var bytePtr = (byte*) ptr; + while (*bytePtr != 0) { + bytes.Add(*bytePtr); + bytePtr += 1; + } + + return bytes.ToArray(); + } + + internal static IntPtr FollowPointerChain(IntPtr start, IEnumerable offsets) { + if (start == IntPtr.Zero) { + return IntPtr.Zero; + } + + foreach (var offset in offsets) { + start = Marshal.ReadIntPtr(start + offset); + if (start == IntPtr.Zero) { + return IntPtr.Zero; + } + } + + return start; + } + } +}