From 6e9a4abd8a2993c0d6a762cbd3b85a3ea66a4808 Mon Sep 17 00:00:00 2001 From: Florian Wathling Date: Sat, 9 May 2026 17:15:26 +0200 Subject: [PATCH] Merge official Dalamud sample plugin into template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- Data/goat.png | Bin 0 -> 19708 bytes PluginNameTemplate.csproj | 20 +++--- PluginNameTemplate.sln | 21 +++++++ PluginNameTemplate.yaml | 10 +-- README.md | 76 +++++++++++++++------- src/Configuration.cs | 19 ++++++ src/Plugin.cs | 75 +++++++++++++++++++--- src/PluginConfiguration.cs | 23 ------- src/Windows/ConfigWindow.cs | 55 ++++++++++++---- src/Windows/MainWindow.cs | 122 ++++++++++++++++++++++++++++++++++++ src/packages.lock.json | 19 ++++++ 11 files changed, 363 insertions(+), 77 deletions(-) create mode 100644 Data/goat.png create mode 100644 PluginNameTemplate.sln create mode 100644 src/Configuration.cs delete mode 100644 src/PluginConfiguration.cs create mode 100644 src/Windows/MainWindow.cs create mode 100644 src/packages.lock.json diff --git a/Data/goat.png b/Data/goat.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca06ace488f9503cff0e9e56117a7db559ae80a GIT binary patch literal 19708 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 literal 0 HcmV?d00001 diff --git a/PluginNameTemplate.csproj b/PluginNameTemplate.csproj index 3d28fdd..39e1101 100644 --- a/PluginNameTemplate.csproj +++ b/PluginNameTemplate.csproj @@ -1,8 +1,7 @@ - + @@ -20,12 +19,19 @@ + + PreserveNewest + false + - + + diff --git a/PluginNameTemplate.sln b/PluginNameTemplate.sln new file mode 100644 index 0000000..757a939 --- /dev/null +++ b/PluginNameTemplate.sln @@ -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 diff --git a/PluginNameTemplate.yaml b/PluginNameTemplate.yaml index 1d2ed45..eee6492 100644 --- a/PluginNameTemplate.yaml +++ b/PluginNameTemplate.yaml @@ -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 ). +# Don't override DalamudPackager defaults. The SDK reads icon and image_urls +# from the .csproj entry automatically. diff --git a/README.md b/README.md index 8e961ef..13a870a 100644 --- a/README.md +++ b/README.md @@ -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 `` block in `.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//lat Synchronized version fields (bump all at once): -- `.csproj` → `AssemblyVersion` +- `.csproj` → `Version`, `AssemblyVersion`, `FileVersion` - `.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 `` 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. diff --git a/src/Configuration.cs b/src/Configuration.cs new file mode 100644 index 0000000..1a9c720 --- /dev/null +++ b/src/Configuration.cs @@ -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); + } +} diff --git a/src/Plugin.cs b/src/Plugin.cs index 38c39fc..87cdd75 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -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(); } diff --git a/src/PluginConfiguration.cs b/src/PluginConfiguration.cs deleted file mode 100644 index a71c77f..0000000 --- a/src/PluginConfiguration.cs +++ /dev/null @@ -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); - } -} diff --git a/src/Windows/ConfigWindow.cs b/src/Windows/ConfigWindow.cs index 2888d07..4b84b40 100644 --- a/src/Windows/ConfigWindow.cs +++ b/src/Windows/ConfigWindow.cs @@ -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(); } } } diff --git a/src/Windows/MainWindow.cs b/src/Windows/MainWindow.cs new file mode 100644 index 0000000..6d81414 --- /dev/null +++ b/src/Windows/MainWindow.cs @@ -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().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."); + } + } + } + } +} diff --git a/src/packages.lock.json b/src/packages.lock.json new file mode 100644 index 0000000..c291724 --- /dev/null +++ b/src/packages.lock.json @@ -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==" + } + } + } +} \ No newline at end of file