Merge official Dalamud sample plugin into template
CI / build (push) Failing after 18s

- Replace Plugin.cs/ConfigWindow.cs skeleton with working sample code
- Add MainWindow.cs (goat-image demo + PlayerState/Lumina queries)
- Rename src/PluginConfiguration.cs → src/Configuration.cs (sample naming)
- Add Data/goat.png sample asset
- Add src/packages.lock.json (NuGet lockfile from sample)
- Add PluginNameTemplate.sln solution file
- Bump csproj from Dalamud.NET.Sdk 13.0.0 → 15.0.0
- Bump yaml dalamud_api_level: 13 → 15
- Update README with sample-removal walkthrough and SDK-bump section

Template now builds end-to-end out of the box. Goat demo
intact for verification; strip per README when implementing
the real plugin.
This commit is contained in:
2026-05-09 17:15:26 +02:00
parent 4a2e840888
commit 6e9a4abd8a
11 changed files with 363 additions and 77 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+13 -7
View File
@@ -1,8 +1,7 @@
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
<!--
Replace `Dalamud.NET.Sdk/13.0.0` with the SDK version current at the time
you start your plugin. Check https://github.com/goatcorp/Dalamud.NET.Sdk
for the latest tag.
SDK 15.0.0 is current as of 2026-05. Check https://github.com/goatcorp/Dalamud.NET.Sdk
for the latest tag when you bump this template.
-->
<PropertyGroup>
@@ -20,12 +19,19 @@
<ItemGroup>
<!--
Add additional NuGet refs here as needed. Dalamud.NET.Sdk pulls in
Dalamud, ImGui.NET, FFXIVClientStructs, Lumina, etc. by default.
Sample asset bundled with the working demo (the goat-image referenced
by MainWindow.cs). Drop your real plugin assets here and adjust the
include-path. CopyToOutputDirectory ensures the file lands next to the
compiled DLL so PluginInterface.AssemblyLocation finds it.
-->
<Content Include="Data\goat.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
</Content>
</ItemGroup>
<ItemGroup>
<None Include="images\icon.png" Pack="true" PackagePath="\" />
<!-- Plugin icon used by the Dalamud manifest. Drop your real icon at images/icon.png. -->
<None Include="images\icon.png" Pack="true" PackagePath="\" Condition="Exists('images\icon.png')" />
</ItemGroup>
</Project>
+21
View File
@@ -0,0 +1,21 @@
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}") = "PluginNameTemplate", "PluginNameTemplate.csproj", "{B12C5E91-7A3D-4E8F-A2C1-9D4E5F6A7B8C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B12C5E91-7A3D-4E8F-A2C1-9D4E5F6A7B8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B12C5E91-7A3D-4E8F-A2C1-9D4E5F6A7B8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B12C5E91-7A3D-4E8F-A2C1-9D4E5F6A7B8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B12C5E91-7A3D-4E8F-A2C1-9D4E5F6A7B8C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+6 -4
View File
@@ -14,7 +14,9 @@ icon_url: https://gitea.hellion-forge.cloud/Hellion-Forge/PluginNameTemplate/raw
assembly_version: 0.1.0
testing_assembly_version: 0.1.0
dalamud_api_level: 13
# Dalamud.NET.Sdk 15.0.0 targets API level 15. Check the SDK release notes
# when bumping (the API level usually moves with major SDK updates).
dalamud_api_level: 15
categories:
- utility
@@ -24,7 +26,7 @@ tags:
changelog: |
v0.1.0
- Initial release.
- Initial release based on the official Dalamud sample plugin (goat demo intact).
# Don't override DalamudPackager defaults (Dalamud.NET.Sdk 13+ handles icon and
# image_urls automatically via the .csproj <None Include="images\icon.png" />).
# Don't override DalamudPackager defaults. The SDK reads icon and image_urls
# from the .csproj <None Include="images\icon.png" /> entry automatically.
+54 -22
View File
@@ -1,8 +1,10 @@
# Dalamud Plugin Template
A starting point for FFXIV/Dalamud plugins on the [Hellion Forge](https://gitea.hellion-forge.cloud/).
A starting point for FFXIV/Dalamud plugins on the [Hellion Forge](https://gitea.hellion-forge.cloud/Hellion-Forge).
Distilled from the [HellionChat](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat) plugin patterns: csproj layout, configuration handling, window scaffolding, custom-repo manifest, Forge-Auto-Announce workflow, and the version-bump checklist.
Built on the **official goatcorp [Dalamud Sample Plugin](https://github.com/goatcorp/SamplePlugin)** (working code with goat-image demo, MainWindow with PlayerState/Lumina queries, ConfigWindow with movable-toggle), wrapped with Hellion-Forge conventions: custom-repo manifest, Forge-Auto-Announce workflow, version-bump checklist, MIT license.
The sample code is intentionally kept intact so the template builds and runs out of the box. Strip the goat demo when you implement your real plugin (see "Replacing the sample" below).
---
@@ -10,45 +12,64 @@ Distilled from the [HellionChat](https://gitea.hellion-forge.cloud/JonKazama-Hel
1. Click **"Use this template"** at the top of the repository page on the Forge.
2. Pick a name like `MyPlugin` and clone your new repo locally.
3. Find-and-replace `PluginNameTemplate` everywhere with your plugin's name (case-sensitive).
3. Find-and-replace `PluginNameTemplate` everywhere with your plugin's name (case-sensitive):
```bash
git ls-files | xargs sed -i 's/PluginNameTemplate/MyPlugin/g'
git mv PluginNameTemplate.csproj MyPlugin.csproj
git mv PluginNameTemplate.yaml MyPlugin.yaml
git mv PluginNameTemplate.sln MyPlugin.sln
```
4. Replace `images/icon.png` with your plugin icon (512x512 PNG, transparent background).
5. Update `repo.json` with your real `DownloadLinkInstall` URLs once your CI publishes releases.
6. Implement your plugin in `src/Plugin.cs` and friends.
6. Strip the goat demo (or keep it for development reference — your call).
7. Implement your plugin in `src/Plugin.cs` and friends.
After the rename, this README should be replaced with your plugin's actual README — the template-usage notes don't belong in your shipped plugin.
---
## Replacing the sample
The template ships with the goat demo working end-to-end so you can verify your build setup before writing any code. To remove it cleanly:
1. **Empty the windows.** `src/Windows/MainWindow.cs` and `src/Windows/ConfigWindow.cs` — replace the goat-image / config-toggle demos with your real UI. Keep the class structure (constructor signature, `Draw()` override).
2. **Delete the goat asset.** `Data/goat.png` and the `<Content Include="Data\goat.png">` block in `<PluginName>.csproj`.
3. **Adjust services.** `src/Plugin.cs` injects `ITextureProvider`, `IClientState`, `IPlayerState`, `IDataManager` — only because the goat demo uses them. Drop the ones you don't need.
4. **Rename `/pmycommand`.** In `src/Plugin.cs`, change `private const string CommandName = "/pmycommand"` to your plugin's actual command and update the `HelpMessage`.
---
## Project structure
```
.
├── .editorconfig Code style
├── .editorconfig Code style
├── .gitea/
│ ├── ISSUE_TEMPLATE/ Standard issue forms
│ ├── ISSUE_TEMPLATE/ Standard issue forms
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── forge-posts/v0.1.0.md Discord-announcement payload (Forge-Auto-Announce)
│ └── workflows/
│ ├── ci.yml Build verification on push/PR
│ └── forge-auto-announce.yml Discord announcement on tag
├── docs/CHANGELOG.md Long-form changelog (slim main copy)
├── images/icon.png Plugin icon (replace before shipping)
│ ├── ci.yml Build verification on push/PR
│ └── forge-auto-announce.yml Discord announcement on tag
├── Data/
│ └── goat.png Sample asset for the working demo (replace or delete)
├── docs/CHANGELOG.md Long-form changelog
├── images/.gitkeep Plugin icon goes here (icon.png)
├── src/
│ ├── Plugin.cs IDalamudPlugin entry point
│ ├── PluginConfiguration.cs IPluginConfiguration
│ ├── Plugin.cs IDalamudPlugin entry, WindowSystem, command handler
│ ├── Configuration.cs IPluginConfiguration
│ ├── packages.lock.json NuGet lockfile (Dalamud SDK manages this)
│ └── Windows/
── ConfigWindow.cs Skeleton config window
├── PluginNameTemplate.csproj Dalamud-SDK csproj
├── PluginNameTemplate.yaml Dalamud manifest
├── repo.json Custom-repo manifest for testers
├── CHANGELOG.md Slim changelog (latest 2-4 versions)
├── CODEOWNERS Default reviewer
├── LICENSE MIT
── README.md This file (replace before shipping)
── ConfigWindow.cs Movable-toggle demo
│ └── MainWindow.cs Goat + PlayerState/Lumina demo
├── PluginNameTemplate.csproj Dalamud-SDK 15.0.0 csproj
├── PluginNameTemplate.sln Solution file
├── PluginNameTemplate.yaml Dalamud manifest
├── repo.json Custom-repo manifest for testers
├── CHANGELOG.md Slim changelog (latest 2-4 versions)
── CODEOWNERS Default reviewer
├── LICENSE MIT
└── README.md This file (replace before shipping)
```
---
@@ -70,7 +91,7 @@ DalamudPackager produces the `.zip` artifact under `bin/Release/<PluginName>/lat
Synchronized version fields (bump all at once):
- `<PluginName>.csproj` → `AssemblyVersion`
- `<PluginName>.csproj` → `Version`, `AssemblyVersion`, `FileVersion`
- `<PluginName>.yaml` → `assembly_version` + `changelog`
- `repo.json` → `AssemblyVersion`, `TestingAssemblyVersion`, all 3 `DownloadLink*` URLs, `Description`, `Changelog`
- `CHANGELOG.md` (slim) and `docs/CHANGELOG.md` (full) — keep the latest 2-4 versions in the slim copy
@@ -86,6 +107,17 @@ The Forge-Auto-Announce workflow reads from the **tagged tree**, not main. If a
---
## Dalamud SDK version
This template targets `Dalamud.NET.Sdk/15.0.0` and `dalamud_api_level: 15` in the manifest. When the SDK bumps:
1. Update `<Project Sdk="Dalamud.NET.Sdk/X.Y.Z">` in the csproj
2. Update `dalamud_api_level: X` in the yaml
3. Check the SDK release notes for breaking API changes
4. Run a clean build (`dotnet clean && dotnet build`) and validate the goat demo still works
---
## Testing
Service classes coupled to Dalamud (`IPluginInterface`, `IDataManager`, etc.) cannot be instantiated directly in xUnit because the Dalamud assembly isn't on the test AppDomain. Patterns that work:
@@ -100,4 +132,4 @@ The default csproj has no test project. Add one when there's something to test.
## License
MIT — see `LICENSE`.
MIT — see `LICENSE`. The base sample-code is from goatcorp's Dalamud SamplePlugin (AGPL-3.0 in upstream); the working translation here is shipped under MIT alongside the Hellion-specific scaffolding. If you ship a plugin that's a near-1:1 fork of the upstream sample, check the upstream license for your distribution.
+19
View File
@@ -0,0 +1,19 @@
using Dalamud.Configuration;
using System;
namespace PluginNameTemplate;
[Serializable]
public class Configuration : IPluginConfiguration
{
public int Version { get; set; } = 0;
public bool IsConfigWindowMovable { get; set; } = true;
public bool SomePropertyToBeSavedAndWithADefault { get; set; } = true;
// The below exists just to make saving less cumbersome
public void Save()
{
Plugin.PluginInterface.SavePluginConfig(this);
}
}
+66 -9
View File
@@ -1,28 +1,85 @@
using Dalamud.Game.Command;
using Dalamud.IoC;
using Dalamud.Plugin;
using System.IO;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using PluginNameTemplate.Windows;
namespace PluginNameTemplate;
public sealed class Plugin : IDalamudPlugin
{
[PluginService] public static IDalamudPluginInterface Pi { get; private set; } = null!;
[PluginService] public static IPluginLog Log { get; private set; } = null!;
[PluginService] public static ICommandManager Commands { get; private set; } = null!;
[PluginService] internal static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService] internal static ITextureProvider TextureProvider { get; private set; } = null!;
[PluginService] internal static ICommandManager CommandManager { get; private set; } = null!;
[PluginService] internal static IClientState ClientState { get; private set; } = null!;
[PluginService] internal static IPlayerState PlayerState { get; private set; } = null!;
[PluginService] internal static IDataManager DataManager { get; private set; } = null!;
[PluginService] internal static IPluginLog Log { get; private set; } = null!;
private readonly PluginConfiguration config;
private const string CommandName = "/pmycommand";
public Configuration Configuration { get; init; }
public readonly WindowSystem WindowSystem = new("PluginNameTemplate");
private ConfigWindow ConfigWindow { get; init; }
private MainWindow MainWindow { get; init; }
public Plugin()
{
this.config = PluginConfiguration.Load();
Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
// Register your commands, hooks, windows, etc. here.
Log.Information("PluginNameTemplate loaded.");
// You might normally want to embed resources and load them from the manifest stream
var goatImagePath = Path.Combine(PluginInterface.AssemblyLocation.Directory?.FullName!, "goat.png");
ConfigWindow = new ConfigWindow(this);
MainWindow = new MainWindow(this, goatImagePath);
WindowSystem.AddWindow(ConfigWindow);
WindowSystem.AddWindow(MainWindow);
CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand)
{
HelpMessage = "A useful message to display in /xlhelp"
});
// Tell the UI system that we want our windows to be drawn through the window system
PluginInterface.UiBuilder.Draw += WindowSystem.Draw;
// This adds a button to the plugin installer entry of this plugin which allows
// toggling the display status of the configuration ui
PluginInterface.UiBuilder.OpenConfigUi += ToggleConfigUi;
// Adds another button doing the same but for the main ui of the plugin
PluginInterface.UiBuilder.OpenMainUi += ToggleMainUi;
// Add a simple message to the log with level set to information
// Use /xllog to open the log window in-game
Log.Information($"===A cool log message from {PluginInterface.Manifest.Name}===");
}
public void Dispose()
{
// Unregister anything that was registered above. Order matters —
// dispose UI before hooks, hooks before services.
// Unregister all actions to not leak anything during disposal of plugin
PluginInterface.UiBuilder.Draw -= WindowSystem.Draw;
PluginInterface.UiBuilder.OpenConfigUi -= ToggleConfigUi;
PluginInterface.UiBuilder.OpenMainUi -= ToggleMainUi;
WindowSystem.RemoveAllWindows();
ConfigWindow.Dispose();
MainWindow.Dispose();
CommandManager.RemoveHandler(CommandName);
}
private void OnCommand(string command, string args)
{
// In response to the slash command, toggle the display status of our main ui
MainWindow.Toggle();
}
public void ToggleConfigUi() => ConfigWindow.Toggle();
public void ToggleMainUi() => MainWindow.Toggle();
}
-23
View File
@@ -1,23 +0,0 @@
using Dalamud.Configuration;
namespace PluginNameTemplate;
public sealed class PluginConfiguration : IPluginConfiguration
{
public int Version { get; set; } = 1;
// Add your config fields below. Plain types serialize cleanly; complex
// types need [JsonConverter] or a manual migration step.
public bool ExampleToggle { get; set; } = false;
public static PluginConfiguration Load()
{
return Plugin.Pi.GetPluginConfig() as PluginConfiguration ?? new PluginConfiguration();
}
public void Save()
{
Plugin.Pi.SavePluginConfig(this);
}
}
+43 -12
View File
@@ -1,28 +1,59 @@
using System;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Windowing;
using ImGuiNET;
namespace PluginNameTemplate.Windows;
public sealed class ConfigWindow : Window
public class ConfigWindow : Window, IDisposable
{
private readonly PluginConfiguration config;
private readonly Configuration configuration;
public ConfigWindow(PluginConfiguration config)
: base("PluginNameTemplate Settings###PluginNameTemplate-config")
// We give this window a constant ID using ###.
// This allows for labels to be dynamic, like "{FPS Counter}fps###XYZ counter window",
// and the window ID will always be "###XYZ counter window" for ImGui
public ConfigWindow(Plugin plugin) : base("A Wonderful Configuration Window###With a constant ID")
{
this.config = config;
this.Size = new Vector2(420, 320);
this.SizeCondition = ImGuiCond.FirstUseEver;
Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoScrollWithMouse;
Size = new Vector2(232, 90);
SizeCondition = ImGuiCond.Always;
configuration = plugin.Configuration;
}
public void Dispose() { }
public override void PreDraw()
{
// Flags must be added or removed before Draw() is being called, or they won't apply
if (configuration.IsConfigWindowMovable)
{
Flags &= ~ImGuiWindowFlags.NoMove;
}
else
{
Flags |= ImGuiWindowFlags.NoMove;
}
}
public override void Draw()
{
var toggle = this.config.ExampleToggle;
if (ImGui.Checkbox("Example toggle", ref toggle))
// Can't ref a property, so use a local copy
var configValue = configuration.SomePropertyToBeSavedAndWithADefault;
if (ImGui.Checkbox("Random Config Bool", ref configValue))
{
this.config.ExampleToggle = toggle;
this.config.Save();
configuration.SomePropertyToBeSavedAndWithADefault = configValue;
// Can save immediately on change if you don't want to provide a "Save and Close" button
configuration.Save();
}
var movable = configuration.IsConfigWindowMovable;
if (ImGui.Checkbox("Movable Config Window", ref movable))
{
configuration.IsConfigWindowMovable = movable;
configuration.Save();
}
}
}
+122
View File
@@ -0,0 +1,122 @@
using System;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Lumina.Excel.Sheets;
namespace PluginNameTemplate.Windows;
public class MainWindow : Window, IDisposable
{
private readonly string goatImagePath;
private readonly Plugin plugin;
// We give this window a hidden ID using ##.
// The user will see "My Amazing Window" as window title,
// but for ImGui the ID is "My Amazing Window##With a hidden ID"
public MainWindow(Plugin plugin, string goatImagePath)
: base("My Amazing Window##With a hidden ID", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
{
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(375, 330),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
};
this.goatImagePath = goatImagePath;
this.plugin = plugin;
}
public void Dispose() { }
public override void Draw()
{
ImGui.Text($"The random config bool is {plugin.Configuration.SomePropertyToBeSavedAndWithADefault}");
if (ImGui.Button("Show Settings"))
{
plugin.ToggleConfigUi();
}
ImGui.Spacing();
// Normally a BeginChild() would have to be followed by an unconditional EndChild(),
// ImRaii takes care of this after the scope ends.
// This works for all ImGui functions that require specific handling, examples are BeginTable() or Indent().
using (var child = ImRaii.Child("SomeChildWithAScrollbar", Vector2.Zero, true))
{
// Check if this child is drawing
if (child.Success)
{
ImGui.Text("Have a goat:");
var goatImage = Plugin.TextureProvider.GetFromFile(goatImagePath).GetWrapOrDefault();
if (goatImage != null)
{
using (ImRaii.PushIndent(55f))
{
ImGui.Image(goatImage.Handle, goatImage.Size);
}
}
else
{
ImGui.Text("Image not found.");
}
ImGuiHelpers.ScaledDummy(20.0f);
// Example for other services that Dalamud provides.
// PlayerState provides a wrapper filled with information about the player character.
var playerState = Plugin.PlayerState;
if (!playerState.IsLoaded)
{
ImGui.Text("Our local player is currently not logged in.");
return;
}
if (!playerState.ClassJob.IsValid)
{
ImGui.Text("Our current job is currently not valid.");
return;
}
ImGui.AlignTextToFramePadding();
ImGui.Text($"Current job:");
// Scaling hardcoded pixel values is important, as otherwise users with HUD scales above or below 100%
// won't be able to see everything.
ImGui.SameLine(120 * ImGuiHelpers.GlobalScale);
// Get the icon id from a known offset + the class jobs id
var jobIconId = 62100 + playerState.ClassJob.RowId;
var iconTexture = Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(jobIconId)).GetWrapOrEmpty();
ImGui.Image(iconTexture.Handle, new Vector2(28, 28) * ImGuiHelpers.GlobalScale);
ImGui.SameLine();
// If you want to see the Macro representation of this SeString use `.ToMacroString()`
// More info about SeStrings: https://dalamud.dev/plugin-development/sestring/
ImGui.Text(playerState.ClassJob.Value.Abbreviation.ToString());
ImGui.SameLine();
ImGui.Text($" [Level {playerState.Level}]");
// Example for querying Lumina, getting the name of our current area.
var territoryId = Plugin.ClientState.TerritoryType;
if (Plugin.DataManager.GetExcelSheet<TerritoryType>().TryGetRow(territoryId, out var territoryRow))
{
ImGui.Text($"Current location:");
ImGui.SameLine(120 * ImGuiHelpers.GlobalScale);
ImGui.Text(territoryRow.PlaceName.Value.Name.ToString());
}
else
{
ImGui.Text("Invalid territory.");
}
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"version": 1,
"dependencies": {
"net10.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[15.0.0, )",
"resolved": "15.0.0",
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
},
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.2.39, )",
"resolved": "1.2.39",
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
}
}
}
}