From 7987076b604436447ea6cd67aac5ffdae6d5ddcb Mon Sep 17 00:00:00 2001 From: Dawnsorrow Date: Thu, 26 Feb 2026 22:31:19 -0600 Subject: [PATCH] Initial commit: HSPosition plugin and plugin-repo for Gitea Made-with: Cursor --- .gitignore | 18 +++++ HSPosition.sln | 25 +++++++ HSPosition/Configuration.cs | 29 ++++++++ HSPosition/HSPosition.csproj | 19 +++++ HSPosition/Plugin.cs | 65 ++++++++++++++++ HSPosition/PositionalOverlay.cs | 114 +++++++++++++++++++++++++++++ HSPosition/Windows/ConfigWindow.cs | 107 +++++++++++++++++++++++++++ HSPosition/packages.lock.json | 19 +++++ HSPosition/plugin.json | 12 +++ README.md | 36 +++++++++ plugin-repo/README.md | 80 ++++++++++++++++++++ plugin-repo/pluginmaster.json | 22 ++++++ 12 files changed, 546 insertions(+) create mode 100644 .gitignore create mode 100644 HSPosition.sln create mode 100644 HSPosition/Configuration.cs create mode 100644 HSPosition/HSPosition.csproj create mode 100644 HSPosition/Plugin.cs create mode 100644 HSPosition/PositionalOverlay.cs create mode 100644 HSPosition/Windows/ConfigWindow.cs create mode 100644 HSPosition/packages.lock.json create mode 100644 HSPosition/plugin.json create mode 100644 README.md create mode 100644 plugin-repo/README.md create mode 100644 plugin-repo/pluginmaster.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b4ecb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Build +bin/ +obj/ +*.user +*.suo +packages/ +*.dll +*.pdb +*.zip +!plugin-repo/ + +# IDE +.idea/ +.vscode/ +*.csproj.user + +# Keep plugin-repo (manifest) +# packages.lock.json diff --git a/HSPosition.sln b/HSPosition.sln new file mode 100644 index 0000000..9699869 --- /dev/null +++ b/HSPosition.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSPosition", "HSPosition\HSPosition.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} + EndGlobalSection +EndGlobal diff --git a/HSPosition/Configuration.cs b/HSPosition/Configuration.cs new file mode 100644 index 0000000..03288b2 --- /dev/null +++ b/HSPosition/Configuration.cs @@ -0,0 +1,29 @@ +using Dalamud.Configuration; +using Dalamud.Plugin; +using System; +using System.Numerics; + +namespace HSPosition; + +[Serializable] +public class Configuration : IPluginConfiguration +{ + public int Version { get; set; } = 1; + + public bool Enabled { get; set; } = true; + + /// Radius in yalms. Game target ring is ~2–2.5; use MatchGameRingSize in UI to sync. + public float RingRadius { get; set; } = 2.5f; + public float RingThickness { get; set; } = 3f; + public int RingSegments { get; set; } = 64; + + /// Degrees added to relative angle before classifying (0–360). Use to align with game if ring color is offset. + public float AngleOffsetDegrees { get; set; } = 0f; + /// If true, flip the angle (use when ring positionals are mirrored). + public bool InvertAngle { get; set; } = false; + + // Colors as RGBA (0-1) + public Vector4 ColorFront { get; set; } = new(0.2f, 0.8f, 0.2f, 0.85f); // Green + public Vector4 ColorFlank { get; set; } = new(0.9f, 0.7f, 0.1f, 0.85f); // Amber + public Vector4 ColorRear { get; set; } = new(0.9f, 0.2f, 0.2f, 0.85f); // Red +} diff --git a/HSPosition/HSPosition.csproj b/HSPosition/HSPosition.csproj new file mode 100644 index 0000000..7c269c9 --- /dev/null +++ b/HSPosition/HSPosition.csproj @@ -0,0 +1,19 @@ + + + + 0.0.0.1 + https://brassnet.ddns.net:33983/Dawnsorrow/HSPosition + AGPL-3.0-or-later + false + HSPosition + HSPosition + Shows a colored ground ring under your target based on your positional (front, flank, or rear) so you can easily tell if you need to move for abilities. + Ground ring color shows front, flank, or rear for positionals. + + + + PreserveNewest + false + + + diff --git a/HSPosition/Plugin.cs b/HSPosition/Plugin.cs new file mode 100644 index 0000000..b4bee54 --- /dev/null +++ b/HSPosition/Plugin.cs @@ -0,0 +1,65 @@ +using Dalamud.Game.Command; +using Dalamud.Interface.Windowing; +using Dalamud.IoC; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using HSPosition.Windows; + +namespace HSPosition; + +public sealed class Plugin : IDalamudPlugin +{ + [PluginService] internal static IDalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService] internal static ICommandManager CommandManager { get; private set; } = null!; + [PluginService] internal static ITargetManager TargetManager { get; private set; } = null!; + [PluginService] internal static IObjectTable ObjectTable { get; private set; } = null!; + [PluginService] internal static IGameGui GameGui { get; private set; } = null!; + [PluginService] internal static IClientState ClientState { get; private set; } = null!; + [PluginService] internal static IPluginLog Log { get; private set; } = null!; + + private const string CommandName = "/hspos"; + + public Configuration Configuration { get; } + + private readonly WindowSystem WindowSystem = new("HSPosition"); + private ConfigWindow ConfigWindow { get; } + + private PositionalOverlay Overlay { get; } + + public Plugin() + { + Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + + ConfigWindow = new ConfigWindow(this); + Overlay = new PositionalOverlay(this); + + WindowSystem.AddWindow(ConfigWindow); + + CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand) + { + HelpMessage = "Open HSPosition configure (ring color, placement, thickness)" + }); + + PluginInterface.UiBuilder.Draw += OnDraw; + PluginInterface.UiBuilder.OpenConfigUi += () => ConfigWindow.Toggle(); + + Log.Information("HSPosition loaded: target ground ring shows front/flank/rear position."); + } + + public void Dispose() + { + PluginInterface.UiBuilder.Draw -= OnDraw; + PluginInterface.UiBuilder.OpenConfigUi -= () => { }; + + WindowSystem.RemoveAllWindows(); + CommandManager.RemoveHandler(CommandName); + } + + private void OnCommand(string command, string args) => ConfigWindow.Toggle(); + + private void OnDraw() + { + Overlay.Draw(); + WindowSystem.Draw(); + } +} diff --git a/HSPosition/PositionalOverlay.cs b/HSPosition/PositionalOverlay.cs new file mode 100644 index 0000000..df9bd63 --- /dev/null +++ b/HSPosition/PositionalOverlay.cs @@ -0,0 +1,114 @@ +using System; +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Objects.Types; + +namespace HSPosition; + +/// +/// FFXIV target ring: front = 180° (half circle in front of target), rear = 90° gap at back, flank = sides of back half. +/// We use: Front ±90°, Flank 90°–135° and -90° to -135°, Rear 135°–180° and -135° to -180°. +/// +public enum Positional +{ + Front, + Flank, + Rear +} + +public class PositionalOverlay +{ + private readonly Plugin plugin; + + public PositionalOverlay(Plugin plugin) + { + this.plugin = plugin; + } + + public void Draw() + { + if (!plugin.Configuration.Enabled) return; + + var target = Plugin.TargetManager.Target; + if (target == null || target.Address == nint.Zero) return; + + var player = Plugin.ObjectTable.LocalPlayer; + if (player == null || player.Address == nint.Zero) return; + + var targetPos = target.Position; + var targetRot = target.Rotation; + var playerPos = player.Position; + + var positional = GetPositional(targetPos, targetRot, playerPos); + var color = GetColor(positional); + + DrawGroundRing(targetPos, color); + } + + /// + /// Returns the player's positional relative to the target. + /// Game: front = 180° half-circle, rear = 90° gap at back, flank = sides (90°–135° from center). + /// Target rotation in radians (-π to π). Uses config AngleOffsetDegrees and InvertAngle for alignment. + /// + private Positional GetPositional(Vector3 targetPos, float targetRot, Vector3 playerPos) + { + float dx = playerPos.X - targetPos.X; + float dz = playerPos.Z - targetPos.Z; + + if (MathF.Abs(dx) < 1e-5f && MathF.Abs(dz) < 1e-5f) + return Positional.Front; + + float angleToPlayer = MathF.Atan2(dx, dz); + float relative = angleToPlayer - targetRot; + + if (plugin.Configuration.InvertAngle) + relative = -relative; + + relative += plugin.Configuration.AngleOffsetDegrees * (MathF.PI / 180f); + + while (relative > MathF.PI) relative -= 2f * MathF.PI; + while (relative < -MathF.PI) relative += 2f * MathF.PI; + + float deg = MathF.Abs(relative * (180f / MathF.PI)); + + // Game-style: front = 180° (±90°), flank = sides of back half (90°–135°), rear = back center (135°–180°) + if (deg <= 90f) return Positional.Front; + if (deg <= 135f) return Positional.Flank; + return Positional.Rear; + } + + private uint GetColor(Positional p) + { + var v = p switch + { + Positional.Front => plugin.Configuration.ColorFront, + Positional.Flank => plugin.Configuration.ColorFlank, + Positional.Rear => plugin.Configuration.ColorRear, + _ => plugin.Configuration.ColorFront + }; + return ImGui.ColorConvertFloat4ToU32(v); + } + + private void DrawGroundRing(Vector3 worldCenter, uint color) + { + var gui = Plugin.GameGui; + float r = plugin.Configuration.RingRadius; + int segments = Math.Clamp(plugin.Configuration.RingSegments, 8, 128); + float thickness = MathF.Max(1f, plugin.Configuration.RingThickness); + + for (int i = 0; i < segments; i++) + { + float t0 = (float)i / segments * 2f * MathF.PI; + float t1 = (float)(i + 1) / segments * 2f * MathF.PI; + + var p0 = worldCenter + new Vector3(r * MathF.Sin(t0), 0, r * MathF.Cos(t0)); + var p1 = worldCenter + new Vector3(r * MathF.Sin(t1), 0, r * MathF.Cos(t1)); + + if (!gui.WorldToScreen(p0, out var s0)) continue; + if (!gui.WorldToScreen(p1, out var s1)) continue; + + var drawList = ImGui.GetBackgroundDrawList(); + drawList.AddLine(s0, s1, color, thickness); + } + } +} diff --git a/HSPosition/Windows/ConfigWindow.cs b/HSPosition/Windows/ConfigWindow.cs new file mode 100644 index 0000000..cbfbaab --- /dev/null +++ b/HSPosition/Windows/ConfigWindow.cs @@ -0,0 +1,107 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Windowing; + +namespace HSPosition.Windows; + +public class ConfigWindow : Window +{ + private readonly Plugin plugin; + + public ConfigWindow(Plugin plugin) : base("HSPosition Configure", ImGuiWindowFlags.AlwaysAutoResize) + { + this.plugin = plugin; + } + + public override void Draw() + { + var cfg = plugin.Configuration; + + var enabled = cfg.Enabled; + if (ImGui.Checkbox("Enable ground ring", ref enabled)) + { + cfg.Enabled = enabled; + Save(); + } + + ImGui.Separator(); + ImGui.Text("Ring placement and thickness"); + ImGui.Spacing(); + ImGui.TextWrapped("The game's target ring cannot be recolored by plugins. We draw a colored ring on top of it."); + if (ImGui.Button("Match game ring size")) + { + cfg.RingRadius = 2.4f; + cfg.RingThickness = 2.5f; + cfg.RingSegments = 64; + Save(); + } + ImGui.Spacing(); + + var ringRadius = cfg.RingRadius; + if (ImGui.SliderFloat("Ring radius (yalms)", ref ringRadius, 1f, 8f, "%.1f")) + { + cfg.RingRadius = ringRadius; + Save(); + } + var ringThickness = cfg.RingThickness; + if (ImGui.SliderFloat("Ring line thickness", ref ringThickness, 1f, 12f, "%.0f")) + { + cfg.RingThickness = ringThickness; + Save(); + } + var ringSegments = cfg.RingSegments; + if (ImGui.SliderInt("Ring segments", ref ringSegments, 16, 128)) + { + cfg.RingSegments = ringSegments; + Save(); + } + + ImGui.Separator(); + ImGui.Text("Positional angle (if colors feel offset)"); + ImGui.Spacing(); + + var angleOffset = cfg.AngleOffsetDegrees; + if (ImGui.SliderFloat("Angle offset (degrees)", ref angleOffset, 0f, 360f, "%.0f")) + { + cfg.AngleOffsetDegrees = angleOffset; + Save(); + } + var invertAngle = cfg.InvertAngle; + if (ImGui.Checkbox("Invert angle (mirror front/rear)", ref invertAngle)) + { + cfg.InvertAngle = invertAngle; + Save(); + } + + ImGui.Separator(); + ImGui.Text("Ring colors (Front / Flank / Rear)"); + ImGui.Spacing(); + + var colorFront = cfg.ColorFront; + if (ImGui.ColorEdit4("Front", ref colorFront, ImGuiColorEditFlags.AlphaBar)) + { + cfg.ColorFront = colorFront; + Save(); + } + var colorFlank = cfg.ColorFlank; + if (ImGui.ColorEdit4("Flank", ref colorFlank, ImGuiColorEditFlags.AlphaBar)) + { + cfg.ColorFlank = colorFlank; + Save(); + } + var colorRear = cfg.ColorRear; + if (ImGui.ColorEdit4("Rear", ref colorRear, ImGuiColorEditFlags.AlphaBar)) + { + cfg.ColorRear = colorRear; + Save(); + } + + ImGui.Separator(); + ImGui.TextWrapped("Our ring is drawn on top of the game's target ring and changes color by your positional (Front / Flank / Rear). Use 'Match game ring size' so they align."); + } + + private void Save() + { + Plugin.PluginInterface.SavePluginConfig(plugin.Configuration); + } +} diff --git a/HSPosition/packages.lock.json b/HSPosition/packages.lock.json new file mode 100644 index 0000000..34448a6 --- /dev/null +++ b/HSPosition/packages.lock.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "dependencies": { + "net10.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[14.0.2, )", + "resolved": "14.0.2", + "contentHash": "dQJeq+8eyHzra4Cg5eZ/3LAeS3/UpvvLriYJGSncMK9LqJ7Q4B6jwcOsxo3PfxVd15xj+IzVFxkPqIBmPQu8/w==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" + } + } + } +} \ No newline at end of file diff --git a/HSPosition/plugin.json b/HSPosition/plugin.json new file mode 100644 index 0000000..5dc5b77 --- /dev/null +++ b/HSPosition/plugin.json @@ -0,0 +1,12 @@ +{ + "Name": "HSPosition", + "Author": "HSPosition", + "Description": "Shows a colored ground ring under your target based on your positional (front, flank, or rear) so you can easily tell if you need to move for abilities.", + "Punchline": "Ground ring color shows front, flank, or rear for positionals.", + "InternalName": "HSPosition", + "AssemblyVersion": "0.0.0.1", + "RepoUrl": "https://brassnet.ddns.net:33983/Dawnsorrow/HSPosition", + "ApplicableVersion": "any", + "DalamudApiLevel": 11, + "LoadPriority": 0 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9242c3f --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# HSPosition + +A Dalamud plugin for FFXIV that draws a **colored ground ring** under your current target. + +**Publishing to your own repo:** See the `plugin-repo/` folder for a single-repo setup (pluginmaster + README) you can push to Gitea so others can add one URL and get all your plugins. The color reflects your positional: + +- **Green** = Front +- **Amber** = Flank +- **Red** = Rear + +So you can see at a glance whether you need to move for positional abilities. + +## Build + +- .NET 10 SDK and Dalamud dev install (e.g. XIVLauncher with Dalamud, run game once). +- Build: `dotnet build HSPosition.sln -c Debug` (or Release). +- Output: `HSPosition/bin/x64/Debug/` (or `Release/`). + +## Install (dev) + +1. **xlsettings** → Experimental → add the full path to `HSPosition.dll` to **Dev Plugin Locations**. +2. **xlplugins** → Dev Tools → Installed Dev Plugins → enable **HSPosition**. + +## Usage + +- Target something; a ring appears under it. Color = your current positional (front/flank/rear). +- **/hspos** — open configure menu (or use the **Configure** button on the plugin in the installer). +- Configure: enable/disable, ring radius, line thickness, segments, angle offset/invert, and colors for front/flank/rear. + +## Positional angles + +- **Front**: ±45° in front of the target. +- **Flank**: 45°–135° to the left or right. +- **Rear**: 135°–180° behind the target. + +Matches FFXIV’s usual ~90° cones for positionals. diff --git a/plugin-repo/README.md b/plugin-repo/README.md new file mode 100644 index 0000000..70acb1c --- /dev/null +++ b/plugin-repo/README.md @@ -0,0 +1,80 @@ +# My Dalamud Plugin Repository + +This repo is a **single custom repository** for Dalamud. Users add one URL and see all your plugins in the installer. + +## What users add in Dalamud + +1. Open **xlsettings** (or Dalamud Settings). +2. Go to **Experimental** → **Custom Plugin Repositories**. +3. Add this URL (use the **raw** URL to `pluginmaster.json` on your Gitea): + + ``` + https://brassnet.ddns.net:33983/Dawnsorrow/DalamudPlugins/raw/main/pluginmaster.json + ``` + +4. Save. In **xlplugins**, your repo will appear and list all plugins in `pluginmaster.json`. + +--- + +## Repo layout + +- **pluginmaster.json** – List of all your plugins. This file’s raw URL is the “repo link” users add. +- **releases/** – Optional: put `latest.zip` builds here and use raw URLs in pluginmaster (see below). +- Or use **Gitea Releases** on this repo for each plugin (recommended). + +--- + +## Adding a new plugin to the repo + +1. **Edit `pluginmaster.json`** and add a new object to the array (copy an existing entry and change fields): + + - `Author`, `Name`, `Description`, `Punchline`, `InternalName` + - `AssemblyVersion` (e.g. `"0.0.0.1"`) + - `RepoUrl` – link to the plugin’s source (can be another Gitea repo or same repo folder) + - `ApplicableVersion`: `"any"` + - `DalamudApiLevel`: e.g. `14` + - `LastUpdate`: Unix timestamp (seconds), e.g. `1730000000` + - `DownloadLinkInstall` and `DownloadLinkUpdate`: URL to the plugin’s **zip** (see below) + +2. **Host the plugin zip** in one of these ways: + + **Option A – Releases on this repo (recommended)** + - Create a release (e.g. tag `HSPosition-0.0.0.1` or `v0.0.0.1`). + - Upload the plugin zip as `latest.zip`. + - Use the release download URL in pluginmaster, e.g. + `https://brassnet.ddns.net:33983/Dawnsorrow/DalamudPlugins/releases/download/HSPosition-0.0.0.1/latest.zip` + + **Option B – Raw file in repo** + - Put `HSPosition-0.0.0.1.zip` in a `releases/` folder and commit. + - Use the raw file URL: + `https://brassnet.ddns.net:33983/Dawnsorrow/DalamudPlugins/raw/main/releases/HSPosition-0.0.0.1.zip` + + **Option C – Separate repo per plugin** + - Build and release each plugin in its own Gitea repo. + - In pluginmaster, set `DownloadLinkInstall` / `DownloadLinkUpdate` to that repo’s release zip URL. + +3. This repo is configured for **brassnet.ddns.net:33983** and user **Dawnsorrow**. For new plugins, set download URLs to `https://brassnet.ddns.net:33983/Dawnsorrow/DalamudPlugins/...` (releases or raw). + +--- + +## Building and packaging HSPosition (or any plugin) + +1. From the plugin project directory, build in **Release** so the packager produces a zip: + ```bash + dotnet build HSPosition.sln -c Release + ``` +2. The zip is at: + `HSPosition/bin/x64/Release/HSPosition/latest.zip` +3. Upload that zip to a Gitea Release (e.g. create release tag `HSPosition-0.0.0.1`, attach `latest.zip`), or put it in `releases/` and use the raw URL. + +--- + +## Updating a plugin + +1. Bump the plugin’s version (e.g. in `.csproj` and `plugin.json`). +2. Build in Release and upload the new zip (new release or replace file in `releases/`). +3. In `pluginmaster.json` update: + - `AssemblyVersion` (and tag/release name if you use it in the URL). + - `LastUpdate` (current Unix timestamp: `date +%s`). + - `DownloadLinkInstall` and `DownloadLinkUpdate` if the zip URL changed (e.g. new release tag). +4. Commit and push. Users will get the update when they check for updates in the plugin installer. diff --git a/plugin-repo/pluginmaster.json b/plugin-repo/pluginmaster.json new file mode 100644 index 0000000..99e1b22 --- /dev/null +++ b/plugin-repo/pluginmaster.json @@ -0,0 +1,22 @@ +[ + { + "Author": "HSPosition", + "Name": "HSPosition", + "Punchline": "Ground ring color shows front, flank, or rear for positionals.", + "Description": "Shows a colored ground ring under your target based on your positional (front, flank, or rear) so you can easily tell if you need to move for abilities.", + "Changelog": "Initial release.", + "IsHide": false, + "InternalName": "HSPosition", + "AssemblyVersion": "0.0.0.1", + "TestingAssemblyVersion": null, + "IsTestingExclusive": false, + "RepoUrl": "https://brassnet.ddns.net:33983/Dawnsorrow/HSPosition", + "ApplicableVersion": "any", + "DalamudApiLevel": 14, + "DownloadCount": 0, + "LastUpdate": 1730000000, + "DownloadLinkInstall": "https://brassnet.ddns.net:33983/Dawnsorrow/HSPosition/releases/download/HSPosition-0.0.0.1/latest.zip", + "DownloadLinkUpdate": "https://brassnet.ddns.net:33983/Dawnsorrow/HSPosition/releases/download/HSPosition-0.0.0.1/latest.zip", + "DownloadLinkTesting": null + } +]