Merge branch 'master' into luna

# Conflicts:
#	Penumbra/Api/Api/RedrawApi.cs
#	Penumbra/Api/Api/ResolveApi.cs
#	Penumbra/Api/Api/UiApi.cs
#	Penumbra/Interop/Services/TextureArraySlicer.cs
#	Penumbra/UI/CollectionTab/CollectionPanel.cs
#	Penumbra/UI/Tabs/CollectionsTab.cs
#	Penumbra/UI/Tabs/Debug/DebugTab.cs
#	Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs
#	Penumbra/UI/Tabs/ModsTab.cs
#	Penumbra/UI/Tabs/ResourceTab.cs
#	Penumbra/packages.lock.json
This commit is contained in:
Ottermandias 2025-12-19 14:52:06 +01:00
commit f035073966
26 changed files with 378 additions and 140 deletions

View file

@ -10,13 +10,15 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v5
with: with:
submodules: recursive submodules: recursive
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '9.x.x' dotnet-version: |
10.x.x
9.x.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Download Dalamud - name: Download Dalamud

View file

@ -9,13 +9,15 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v5
with: with:
submodules: recursive submodules: recursive
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '9.x.x' dotnet-version: |
10.x.x
9.x.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Download Dalamud - name: Download Dalamud

View file

@ -9,13 +9,15 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v5
with: with:
submodules: recursive submodules: recursive
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '9.x.x' dotnet-version: |
10.x.x
9.x.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Download Dalamud - name: Download Dalamud

@ -1 +1 @@
Subproject commit 1459e2b8f5e1687f659836709e23571235d4206c Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf

@ -1 +1 @@
Subproject commit 66a11d4c886d64da6ecb1a0ec4c8306b99167be1 Subproject commit 1750c41b53e1000c99a7fb9d8a0f082aef639a41

View file

@ -1,4 +1,4 @@
<Project Sdk="Dalamud.NET.Sdk/13.1.0"> <Project Sdk="Dalamud.NET.Sdk/14.0.1">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
</PropertyGroup> </PropertyGroup>

View file

@ -1,12 +1,12 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net9.0-windows7.0": { "net10.0-windows7.0": {
"DotNet.ReproducibleBuilds": { "DotNet.ReproducibleBuilds": {
"type": "Direct", "type": "Direct",
"requested": "[1.2.25, )", "requested": "[1.2.39, )",
"resolved": "1.2.25", "resolved": "1.2.39",
"contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
} }
} }
} }

@ -1 +1 @@
Subproject commit 462afac558becebbe06b4e5be9b1b3c3f5a9b6d6 Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592

View file

@ -2,11 +2,14 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Luna; using Luna;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Interop;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
namespace Penumbra.Api.Api; namespace Penumbra.Api.Api;
public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
{ {
public void RedrawObject(int gameObjectIndex, RedrawType setting) public void RedrawObject(int gameObjectIndex, RedrawType setting)
{ {
@ -30,7 +33,19 @@ public class RedrawApi(RedrawService redrawService, IFramework framework) : IPen
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting) public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
{ {
throw new NotImplementedException(); if (!collections.Storage.ById(collectionId, out var collection))
collection = ModCollection.Empty;
framework.RunOnFrameworkThread(() =>
{
foreach (var actor in objects.Objects)
{
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
if (collection == modCollection)
{
redrawService.RedrawObject(actor.ObjectIndex, setting);
}
}
});
} }
public event GameObjectRedrawnDelegate? GameObjectRedrawn public event GameObjectRedrawnDelegate? GameObjectRedrawn

View file

@ -1,24 +1,28 @@
using FFXIVClientStructs.FFXIV.Common.Lua;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.MainWindow; using Penumbra.UI.MainWindow;
using Penumbra.UI.Integration;
using Penumbra.UI.Tabs;
namespace Penumbra.Api.Api; namespace Penumbra.Api.Api;
public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly MainWindow _mainWindow; private readonly MainWindow _mainWindow;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly IntegrationSettingsRegistry _integrationSettings;
public UiApi(CommunicatorService communicator, MainWindow mainWindow, ModManager modManager) public UiApi(CommunicatorService communicator, MainWindow mainWindow, ModManager modManager, IntegrationSettingsRegistry integrationSettings)
{ {
_communicator = communicator; _communicator = communicator;
_mainWindow = mainWindow; _mainWindow = mainWindow;
_modManager = modManager; _modManager = modManager;
_integrationSettings = integrationSettings;
_communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default); _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
_communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default); _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
_communicator.PreSettingsTabBarDraw.Subscribe(OnPreSettingsTabBarDraw, Communication.PreSettingsTabBarDraw.Priority.Default); _communicator.PreSettingsTabBarDraw.Subscribe(OnPreSettingsTabBarDraw, Communication.PreSettingsTabBarDraw.Priority.Default);
@ -103,4 +107,12 @@ public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
var (type, id) = arguments.Data.ToApiObject(); var (type, id) = arguments.Data.ToApiObject();
ChangedItemTooltip.Invoke(type, id); ChangedItemTooltip.Invoke(type, id);
} }
public PenumbraApiEc RegisterSettingsSection(Action draw)
=> _integrationSettings.RegisterSection(draw);
public PenumbraApiEc UnregisterSettingsSection(Action draw)
=> _integrationSettings.UnregisterSection(draw)
? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged;
} }

View file

@ -89,6 +89,7 @@ public sealed class IpcProviders : IDisposable, IApiService, IRequiredService
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw), IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw), IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve), IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve), IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
@ -132,6 +133,8 @@ public sealed class IpcProviders : IDisposable, IApiService, IRequiredService
IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui),
IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui),
IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui),
IpcSubscribers.RegisterSettingsSection.Provider(pi, api.Ui),
IpcSubscribers.UnregisterSettingsSection.Provider(pi, api.Ui),
]; ];
if (_characterUtility.Ready) if (_characterUtility.Ready)
_initializedProvider.Invoke(); _initializedProvider.Invoke();

View file

@ -151,6 +151,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
Im.Popup.Open("Changed Item List"u8); Im.Popup.Open("Changed Item List"u8);
} }
} }
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
if (ImGui.Button("Redraw##ObjectCollection"))
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
} }
private void DrawChangedItemPopup() private void DrawChangedItemPopup()

View file

@ -5,12 +5,12 @@ using Dalamud.Plugin.Services;
using Lumina.Data.Files; using Lumina.Data.Files;
using Luna; using Luna;
using OtterTex; using OtterTex;
using SharpDX.Direct3D11;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using DxgiDevice = SharpDX.DXGI.Device; using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures; namespace Penumbra.Import.Textures;
@ -123,11 +123,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
switch (_type) switch (_type)
{ {
case TextureType.Png: case TextureType.Png:
data?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel) data?.SaveAsync(_outputPath, new PngEncoder { CompressionLevel = PngCompressionLevel.NoCompression }, cancel)
.Wait(cancel); .Wait(cancel);
return; return;
case TextureType.Targa: case TextureType.Targa:
data?.SaveAsync(_outputPath, new TgaEncoder() data?.SaveAsync(_outputPath, new TgaEncoder
{ {
Compression = TgaCompression.None, Compression = TgaCompression.None,
BitsPerPixel = TgaBitsPerPixel.Pixel32, BitsPerPixel = TgaBitsPerPixel.Pixel32,
@ -202,11 +202,16 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
rgba, width, height), rgba, width, height),
CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps),
CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height),
CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba,
CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, width, height), width, height),
CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba,
CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, width, height), width, height),
CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba,
width, height),
CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba,
width, height),
CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba,
width, height),
_ => throw new Exception("Wrong save type."), _ => throw new Exception("Wrong save type."),
}; };
@ -388,7 +393,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
} }
/// <summary> Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. </summary> /// <summary> Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. </summary>
public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) public unsafe ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel)
{ {
if (input.Meta.Format == format) if (input.Meta.Format == format)
return input; return input;
@ -404,11 +409,58 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
// See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition.
if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)
{ {
var device = new Device(uiBuilder.DeviceHandle); ref var device = ref *(ID3D11Device*)uiBuilder.DeviceHandle;
var dxgiDevice = device.QueryInterface<DxgiDevice>(); IDXGIDevice* dxgiDevice;
Marshal.ThrowExceptionForHR(device.QueryInterface(TerraFX.Interop.Windows.Windows.__uuidof<IDXGIDevice>(), (void**)&dxgiDevice));
using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); try
return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel); {
IDXGIAdapter* adapter = null;
Marshal.ThrowExceptionForHR(dxgiDevice->GetAdapter(&adapter));
try
{
dxgiDevice->Release();
dxgiDevice = null;
ID3D11Device* deviceClone = null;
ID3D11DeviceContext* contextClone = null;
var featureLevel = device.GetFeatureLevel();
Marshal.ThrowExceptionForHR(DirectX.D3D11CreateDevice(
adapter,
D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN,
HMODULE.NULL,
device.GetCreationFlags(),
&featureLevel,
1,
D3D11.D3D11_SDK_VERSION,
&deviceClone,
null,
&contextClone));
try
{
adapter->Release();
adapter = null;
return input.Compress((nint)deviceClone, format, CompressFlags.Parallel);
}
finally
{
if (contextClone is not null)
contextClone->Release();
if (deviceClone is not null)
deviceClone->Release();
}
}
finally
{
if (adapter is not null)
adapter->Release();
}
}
finally
{
if (dxgiDevice is not null)
dxgiDevice->Release();
}
} }
return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel);
@ -454,7 +506,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
GC.KeepAlive(input); GC.KeepAlive(input);
} }
private readonly struct ImageInputData private readonly struct ImageInputData : IEquatable<ImageInputData>
{ {
private readonly string? _inputPath; private readonly string? _inputPath;
@ -522,5 +574,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
public override int GetHashCode() public override int GetHashCode()
=> _inputPath != null ? _inputPath.ToLowerInvariant().GetHashCode() : HashCode.Combine(_width, _height); => _inputPath != null ? _inputPath.ToLowerInvariant().GetHashCode() : HashCode.Combine(_width, _height);
public override bool Equals(object? obj)
=> obj is ImageInputData o && Equals(o);
} }
} }

View file

@ -1,7 +1,6 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using ImSharp; using ImSharp;
using SharpDX.Direct3D; using TerraFX.Interop.DirectX;
using SharpDX.Direct3D11;
namespace Penumbra.Interop.Services; namespace Penumbra.Interop.Services;
@ -21,46 +20,78 @@ public sealed unsafe class TextureArraySlicer : Luna.IUiService, IDisposable
if (texture == null) if (texture == null)
throw new ArgumentNullException(nameof(texture)); throw new ArgumentNullException(nameof(texture));
if (sliceIndex >= texture->ArraySize) if (sliceIndex >= texture->ArraySize)
throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); throw new ArgumentOutOfRangeException(nameof(sliceIndex),
$"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})");
if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state))
{ {
state.Refresh(); state.Refresh();
return new ImTextureId((nint)state.ShaderResourceView); return new ImTextureId((nint)state.ShaderResourceView);
} }
var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView;
var description = srv.Description; ref var srv = ref *(ID3D11ShaderResourceView*)(nint)texture->D3D11ShaderResourceView;
switch (description.Dimension) srv.AddRef();
try
{ {
case ShaderResourceViewDimension.Texture1D: D3D11_SHADER_RESOURCE_VIEW_DESC description;
case ShaderResourceViewDimension.Texture2D: srv.GetDesc(&description);
case ShaderResourceViewDimension.Texture2DMultisampled: switch (description.ViewDimension)
case ShaderResourceViewDimension.Texture3D: {
case ShaderResourceViewDimension.TextureCube: case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1D:
// This function treats these as single-slice arrays. case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D:
// As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DMS:
break; case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE3D:
case ShaderResourceViewDimension.Texture1DArray: case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURECUBE:
description.Texture1DArray.FirstArraySlice = sliceIndex; // This function treats these as single-slice arrays.
description.Texture2DArray.ArraySize = 1; // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do.
break; break;
case ShaderResourceViewDimension.Texture2DArray: case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1DARRAY:
description.Texture2DArray.FirstArraySlice = sliceIndex; description.Texture1DArray.FirstArraySlice = sliceIndex;
description.Texture2DArray.ArraySize = 1; description.Texture1DArray.ArraySize = 1;
break; break;
case ShaderResourceViewDimension.Texture2DMultisampledArray: case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DARRAY:
description.Texture2DMSArray.FirstArraySlice = sliceIndex; description.Texture2DArray.FirstArraySlice = sliceIndex;
description.Texture2DMSArray.ArraySize = 1; description.Texture2DArray.ArraySize = 1;
break; break;
case ShaderResourceViewDimension.TextureCubeArray: case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DMSARRAY:
description.TextureCubeArray.First2DArrayFace = sliceIndex * 6; description.Texture2DMSArray.FirstArraySlice = sliceIndex;
description.TextureCubeArray.CubeCount = 1; description.Texture2DMSArray.ArraySize = 1;
break; break;
default: case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURECUBEARRAY:
throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}"); description.TextureCubeArray.First2DArrayFace = sliceIndex * 6u;
description.TextureCubeArray.NumCubes = 1;
break;
default:
throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.ViewDimension}");
}
ID3D11Device* device = null;
srv.GetDevice(&device);
ID3D11Resource* resource = null;
srv.GetResource(&resource);
try
{
ID3D11ShaderResourceView* slicedSrv = null;
Marshal.ThrowExceptionForHR(device->CreateShaderResourceView(resource, &description, &slicedSrv));
resource->Release();
device->Release();
state = new SliceState(slicedSrv);
_activeSlices.Add(((nint)texture, sliceIndex), state);
return new ImTextureId((nint)state.ShaderResourceView);
}
finally
{
if (resource is not null)
resource->Release();
if (device is not null)
device->Release();
}
}
finally
{
srv.Release();
} }
state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description));
_activeSlices.Add(((nint)texture, sliceIndex), state);
return new ImTextureId((nint)state.ShaderResourceView);
} }
public void Tick() public void Tick()
@ -72,10 +103,9 @@ public sealed unsafe class TextureArraySlicer : Luna.IUiService, IDisposable
if (!slice.Tick()) if (!slice.Tick())
_expiredKeys.Add(key); _expiredKeys.Add(key);
} }
foreach (var key in _expiredKeys) foreach (var key in _expiredKeys)
{
_activeSlices.Remove(key); _activeSlices.Remove(key);
}
} }
finally finally
{ {
@ -86,14 +116,12 @@ public sealed unsafe class TextureArraySlicer : Luna.IUiService, IDisposable
public void Dispose() public void Dispose()
{ {
foreach (var slice in _activeSlices.Values) foreach (var slice in _activeSlices.Values)
{
slice.Dispose(); slice.Dispose();
}
} }
private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable private sealed class SliceState(ID3D11ShaderResourceView* shaderResourceView) : IDisposable
{ {
public readonly ShaderResourceView ShaderResourceView = shaderResourceView; public readonly ID3D11ShaderResourceView* ShaderResourceView = shaderResourceView;
private uint _timeToLive = InitialTimeToLive; private uint _timeToLive = InitialTimeToLive;
@ -107,13 +135,15 @@ public sealed unsafe class TextureArraySlicer : Luna.IUiService, IDisposable
if (unchecked(_timeToLive--) > 0) if (unchecked(_timeToLive--) > 0)
return true; return true;
ShaderResourceView.Dispose(); if (ShaderResourceView is not null)
ShaderResourceView->Release();
return false; return false;
} }
public void Dispose() public void Dispose()
{ {
ShaderResourceView.Dispose(); if (ShaderResourceView is not null)
ShaderResourceView->Release();
} }
} }
} }

View file

@ -1,4 +1,4 @@
<Project Sdk="Dalamud.NET.Sdk/13.1.0"> <Project Sdk="Dalamud.NET.Sdk/14.0.1">
<PropertyGroup> <PropertyGroup>
<AssemblyTitle>Penumbra</AssemblyTitle> <AssemblyTitle>Penumbra</AssemblyTitle>
<Company>absolute gangstas</Company> <Company>absolute gangstas</Company>
@ -39,16 +39,8 @@
<HintPath>$(DalamudLibPath)Iced.dll</HintPath> <HintPath>$(DalamudLibPath)Iced.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="SharpDX"> <Reference Include="TerraFX.Interop.Windows">
<HintPath>$(DalamudLibPath)SharpDX.dll</HintPath> <HintPath>$(DalamudLibPath)TerraFX.Interop.Windows.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SharpDX.Direct3D11">
<HintPath>$(DalamudLibPath)SharpDX.Direct3D11.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SharpDX.DXGI">
<HintPath>$(DalamudLibPath)SharpDX.DXGI.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="OtterTex.dll"> <Reference Include="OtterTex.dll">

View file

@ -8,7 +8,7 @@
"RepoUrl": "https://github.com/xivdev/Penumbra", "RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"Tags": [ "modding" ], "Tags": [ "modding" ],
"DalamudApiLevel": 13, "DalamudApiLevel": 14,
"LoadPriority": 69420, "LoadPriority": 69420,
"LoadRequiredState": 2, "LoadRequiredState": 2,
"LoadSync": true, "LoadSync": true,

View file

@ -52,6 +52,7 @@ public static class StaticServiceManager
.AddDalamudService<ICommandManager>(pi) .AddDalamudService<ICommandManager>(pi)
.AddDalamudService<IDataManager>(pi) .AddDalamudService<IDataManager>(pi)
.AddDalamudService<IClientState>(pi) .AddDalamudService<IClientState>(pi)
.AddDalamudService<IPlayerState>(pi)
.AddDalamudService<IChatGui>(pi) .AddDalamudService<IChatGui>(pi)
.AddDalamudService<IFramework>(pi) .AddDalamudService<IFramework>(pi)
.AddDalamudService<ICondition>(pi) .AddDalamudService<ICondition>(pi)

View file

@ -4,6 +4,7 @@ using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.Collections; using Penumbra.Collections;

View file

@ -0,0 +1,115 @@
using Dalamud.Plugin;
using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Enums;
namespace Penumbra.UI.Integration;
public sealed class IntegrationSettingsRegistry : IService, IDisposable
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly List<(string InternalName, string Name, Action Draw)> _sections = [];
private bool _disposed = false;
public IntegrationSettingsRegistry(IDalamudPluginInterface pluginInterface)
{
_pluginInterface = pluginInterface;
_pluginInterface.ActivePluginsChanged += OnActivePluginsChanged;
}
public void Dispose()
{
_disposed = true;
_pluginInterface.ActivePluginsChanged -= OnActivePluginsChanged;
_sections.Clear();
}
public void Draw()
{
foreach (var (internalName, name, draw) in _sections)
{
if (!ImUtf8.CollapsingHeader($"Integration with {name}###IntegrationSettingsHeader.{internalName}"))
continue;
using var id = ImUtf8.PushId($"IntegrationSettings.{internalName}");
try
{
draw();
}
catch (Exception e)
{
Penumbra.Log.Error($"Error while drawing {internalName} integration settings: {e}");
}
}
}
public PenumbraApiEc RegisterSection(Action draw)
{
if (_disposed)
return PenumbraApiEc.SystemDisposed;
var plugin = GetPlugin(draw);
if (plugin is null)
return PenumbraApiEc.InvalidArgument;
var section = (plugin.InternalName, plugin.Name, draw);
var index = FindSectionIndex(plugin.InternalName);
if (index >= 0)
{
if (_sections[index] == section)
return PenumbraApiEc.NothingChanged;
_sections[index] = section;
}
else
_sections.Add(section);
_sections.Sort((lhs, rhs) => string.Compare(lhs.Name, rhs.Name, StringComparison.CurrentCultureIgnoreCase));
return PenumbraApiEc.Success;
}
public bool UnregisterSection(Action draw)
{
var index = FindSectionIndex(draw);
if (index < 0)
return false;
_sections.RemoveAt(index);
return true;
}
private void OnActivePluginsChanged(IActivePluginsChangedEventArgs args)
{
if (args.Kind is PluginListInvalidationKind.Loaded)
return;
foreach (var internalName in args.AffectedInternalNames)
{
var index = FindSectionIndex(internalName);
if (index >= 0 && GetPlugin(_sections[index].Draw) is null)
{
_sections.RemoveAt(index);
Penumbra.Log.Warning($"Removed stale integration setting section of {internalName} (reason: {args.Kind})");
}
}
}
private IExposedPlugin? GetPlugin(Delegate @delegate)
=> @delegate.Method.DeclaringType
switch
{
null => null,
var type => _pluginInterface.GetPlugin(type.Assembly),
};
private int FindSectionIndex(string internalName)
=> _sections.FindIndex(section => section.InternalName.Equals(internalName, StringComparison.Ordinal));
private int FindSectionIndex(Action draw)
=> _sections.FindIndex(section => section.Draw == draw);
}

View file

@ -1,4 +1,4 @@
using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin.Services;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImSharp; using ImSharp;
using Luna; using Luna;

View file

@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using ImSharp; using ImSharp;
using Luna; using Luna;

View file

@ -6,6 +6,7 @@ using ImSharp;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.Util;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
namespace Penumbra.UI.Tabs.Debug; namespace Penumbra.UI.Tabs.Debug;
@ -169,10 +170,10 @@ public unsafe class GlobalVariablesDrawer(
if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMap.Span) >= 0) if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMap.Span) >= 0)
{ {
table.DrawColumn($"[{total:D4}]"); table.DrawColumn($"[{total:D4}]");
table.DrawColumn($"{resource->Name.Unk1}"); table.DrawColumn($"{resource->Name.GetField<ushort, SchedulerResource.ResourceName>(16)}"); // Unk1
table.DrawColumn(new CiByteString(resource->Name.Buffer).Span); table.DrawColumn(new CiByteString(resource->Name.Buffer).Span);
table.DrawColumn($"{resource->Consumers}"); table.DrawColumn($"{resource->Consumers}");
table.DrawColumn($"{resource->Unk1}"); // key table.DrawColumn($"{PointerExtensions.GetField<uint, SchedulerResource>(resource, 120)}"); // key, Unk1
table.NextColumn(); table.NextColumn();
Penumbra.Dynamis.DrawPointer(resource); Penumbra.Dynamis.DrawPointer(resource);
table.NextColumn(); table.NextColumn();
@ -215,10 +216,10 @@ public unsafe class GlobalVariablesDrawer(
if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterList.Span) >= 0) if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterList.Span) >= 0)
{ {
table.DrawColumn($"[{total:D4}]"); table.DrawColumn($"[{total:D4}]");
table.DrawColumn($"{resource->Name.Unk1}"); table.DrawColumn($"{resource->Name.GetField<ushort, SchedulerResource.ResourceName>(16)}"); // Unk1
table.DrawColumn(new CiByteString(resource->Name.Buffer).Span); table.DrawColumn(new CiByteString(resource->Name.Buffer).Span);
table.DrawColumn($"{resource->Consumers}"); table.DrawColumn($"{resource->Consumers}");
table.DrawColumn($"{resource->Unk1}"); // key table.DrawColumn($"{PointerExtensions.GetField<uint, SchedulerResource>(resource, 120)}"); // key, Unk1
table.NextColumn(); table.NextColumn();
Penumbra.Dynamis.DrawPointer(resource); Penumbra.Dynamis.DrawPointer(resource);
table.NextColumn(); table.NextColumn();

View file

@ -12,6 +12,7 @@ using Penumbra.Interop.Services;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.Integration;
using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab;
using Penumbra.UI.ModsTab.Selector; using Penumbra.UI.ModsTab.Selector;
@ -51,6 +52,7 @@ public sealed class SettingsTab : ITab<TabType>
private readonly CleanupService _cleanupService; private readonly CleanupService _cleanupService;
private readonly AttributeHook _attributeHook; private readonly AttributeHook _attributeHook;
private readonly PcpService _pcpService; private readonly PcpService _pcpService;
private readonly IntegrationSettingsRegistry _integrationSettings;
private string _lastCloudSyncTestedPath = string.Empty; private string _lastCloudSyncTestedPath = string.Empty;
private bool _lastCloudSyncTestResult; private bool _lastCloudSyncTestResult;
@ -62,7 +64,7 @@ public sealed class SettingsTab : ITab<TabType>
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
AttributeHook attributeHook, PcpService pcpService) AttributeHook attributeHook, PcpService pcpService, IntegrationSettingsRegistry integrationSettings)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_config = config; _config = config;
@ -90,6 +92,7 @@ public sealed class SettingsTab : ITab<TabType>
_cleanupService = cleanupService; _cleanupService = cleanupService;
_attributeHook = attributeHook; _attributeHook = attributeHook;
_pcpService = pcpService; _pcpService = pcpService;
_integrationSettings = integrationSettings;
} }
public void PostTabButton() public void PostTabButton()
@ -120,6 +123,7 @@ public sealed class SettingsTab : ITab<TabType>
DrawColorSettings(); DrawColorSettings();
DrawPredefinedTagsSection(); DrawPredefinedTagsSection();
DrawAdvancedSettings(); DrawAdvancedSettings();
_integrationSettings.Draw();
DrawSupportButtons(); DrawSupportButtons();
} }

View file

@ -0,0 +1,20 @@
namespace Penumbra.Util;
public static class PointerExtensions
{
public static unsafe ref TField GetField<TField, TPointer>(this ref TPointer reference, int offset)
where TPointer : unmanaged
where TField : unmanaged
{
var pointer = (byte*)Unsafe.AsPointer(ref reference) + offset;
return ref *(TField*)pointer;
}
public static unsafe ref TField GetField<TField, TPointer>(TPointer* itemPointer, int offset)
where TPointer : unmanaged
where TField : unmanaged
{
var pointer = (byte*)itemPointer + offset;
return ref *(TField*)pointer;
}
}

View file

@ -4,9 +4,9 @@
"net10.0-windows7.0": { "net10.0-windows7.0": {
"DotNet.ReproducibleBuilds": { "DotNet.ReproducibleBuilds": {
"type": "Direct", "type": "Direct",
"requested": "[1.2.25, )", "requested": "[1.2.39, )",
"resolved": "1.2.25", "resolved": "1.2.39",
"contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
}, },
"EmbedIO": { "EmbedIO": {
"type": "Direct", "type": "Direct",
@ -33,7 +33,6 @@
"resolved": "0.40.0", "resolved": "0.40.0",
"contentHash": "yP/aFX1jqGikVF7u2f05VEaWN4aCaKNLxSas82UgA2GGVECxq/BcqZx3STHCJ78qilo1azEOk1XpBglIuGMb7w==", "contentHash": "yP/aFX1jqGikVF7u2f05VEaWN4aCaKNLxSas82UgA2GGVECxq/BcqZx3STHCJ78qilo1azEOk1XpBglIuGMb7w==",
"dependencies": { "dependencies": {
"System.Buffers": "4.6.0",
"ZstdSharp.Port": "0.8.5" "ZstdSharp.Port": "0.8.5"
} }
}, },
@ -66,10 +65,7 @@
"FlatSharp.Runtime": { "FlatSharp.Runtime": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.9.0", "resolved": "7.9.0",
"contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==", "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw=="
"dependencies": {
"System.Memory": "4.5.5"
}
}, },
"JetBrains.Annotations": { "JetBrains.Annotations": {
"type": "Transitive", "type": "Transitive",
@ -134,38 +130,20 @@
"SharpGLTF.Core": "1.0.5" "SharpGLTF.Core": "1.0.5"
} }
}, },
"System.Buffers": {
"type": "Transitive",
"resolved": "4.6.0",
"contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
},
"System.IO.Hashing": { "System.IO.Hashing": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.8", "resolved": "9.0.8",
"contentHash": "5TJUS9EIYrp0VEcm06EPYxXmLmsVUakewFnM/CAxQfvlasI9fGkTKM9afSf2dodZcMCzFna/o7Fn+gYRt3uTiA==" "contentHash": "5TJUS9EIYrp0VEcm06EPYxXmLmsVUakewFnM/CAxQfvlasI9fGkTKM9afSf2dodZcMCzFna/o7Fn+gYRt3uTiA=="
}, },
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw=="
},
"System.Security.Cryptography.Pkcs": { "System.Security.Cryptography.Pkcs": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.1", "resolved": "8.0.1",
"contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA=="
}, },
"System.ValueTuple": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
},
"Unosquare.Swan.Lite": { "Unosquare.Swan.Lite": {
"type": "Transitive", "type": "Transitive",
"resolved": "3.1.0", "resolved": "3.1.0",
"contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==", "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ=="
"dependencies": {
"System.ValueTuple": "4.5.0"
}
}, },
"ZstdSharp.Port": { "ZstdSharp.Port": {
"type": "Transitive", "type": "Transitive",
@ -200,7 +178,7 @@
"FlatSharp.Runtime": "[7.9.0, )", "FlatSharp.Runtime": "[7.9.0, )",
"Luna": "[1.0.0, )", "Luna": "[1.0.0, )",
"Penumbra.Api": "[5.13.0, )", "Penumbra.Api": "[5.13.0, )",
"Penumbra.String": "[1.0.6, )" "Penumbra.String": "[1.0.7, )"
} }
}, },
"penumbra.string": { "penumbra.string": {

View file

@ -5,12 +5,12 @@
"Punchline": "Runtime mod loader and manager.", "Punchline": "Runtime mod loader and manager.",
"Description": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra", "InternalName": "Penumbra",
"AssemblyVersion": "1.5.1.6", "AssemblyVersion": "1.5.1.9",
"TestingAssemblyVersion": "1.5.1.7", "TestingAssemblyVersion": "1.5.1.9",
"RepoUrl": "https://github.com/xivdev/Penumbra", "RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"DalamudApiLevel": 13, "DalamudApiLevel": 14,
"TestingDalamudApiLevel": 13, "TestingDalamudApiLevel": 14,
"IsHide": "False", "IsHide": "False",
"IsTestingExclusive": "False", "IsTestingExclusive": "False",
"DownloadCount": 0, "DownloadCount": 0,
@ -18,9 +18,9 @@
"LoadPriority": 69420, "LoadPriority": 69420,
"LoadRequiredState": 2, "LoadRequiredState": 2,
"LoadSync": true, "LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.7/Penumbra.zip", "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
} }
] ]