Initial commit: HSPosition plugin and plugin-repo for Gitea

Made-with: Cursor
This commit is contained in:
Dawnsorrow
2026-02-26 22:31:19 -06:00
commit 7987076b60
12 changed files with 546 additions and 0 deletions
+18
View File
@@ -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
+25
View File
@@ -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
+29
View File
@@ -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;
/// <summary>Radius in yalms. Game target ring is ~22.5; use MatchGameRingSize in UI to sync.</summary>
public float RingRadius { get; set; } = 2.5f;
public float RingThickness { get; set; } = 3f;
public int RingSegments { get; set; } = 64;
/// <summary>Degrees added to relative angle before classifying (0360). Use to align with game if ring color is offset.</summary>
public float AngleOffsetDegrees { get; set; } = 0f;
/// <summary>If true, flip the angle (use when ring positionals are mirrored).</summary>
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
}
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Dalamud.NET.Sdk/14.0.2">
<PropertyGroup>
<Version>0.0.0.1</Version>
<PackageProjectUrl>https://brassnet.ddns.net:33983/Dawnsorrow/HSPosition</PackageProjectUrl>
<PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression>
<IsPackable>false</IsPackable>
<Name>HSPosition</Name>
<Author>HSPosition</Author>
<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.</Description>
<Punchline>Ground ring color shows front, flank, or rear for positionals.</Punchline>
</PropertyGroup>
<ItemGroup>
<Content Include="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
</Content>
</ItemGroup>
</Project>
+65
View File
@@ -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();
}
}
+114
View File
@@ -0,0 +1,114 @@
using System;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.Types;
namespace HSPosition;
/// <summary>
/// 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°.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
}
+107
View File
@@ -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);
}
}
+19
View File
@@ -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=="
}
}
}
}
+12
View File
@@ -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
}
+36
View File
@@ -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 FFXIVs usual ~90° cones for positionals.
+80
View File
@@ -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 files 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 plugins 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 plugins **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 repos 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 plugins 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.
+22
View File
@@ -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
}
]