mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 05:04:15 +01:00
commit
086f90171e
37 changed files with 2600 additions and 124 deletions
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -2,8 +2,8 @@ name: Create Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags-ignore:
|
||||||
- '*'
|
- t*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -75,8 +75,10 @@ jobs:
|
||||||
git config --global user.name "Actions User"
|
git config --global user.name "Actions User"
|
||||||
git config --global user.email "actions@github.com"
|
git config --global user.email "actions@github.com"
|
||||||
|
|
||||||
git fetch origin master && git checkout master
|
git fetch origin master && git fetch origin test && git branch -f test origin/master && git checkout master
|
||||||
git add repo.json
|
git add repo.json
|
||||||
git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true
|
git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true
|
||||||
|
|
||||||
git push origin master || true
|
git push origin master || true
|
||||||
|
git checkout test
|
||||||
|
git push origin test -f || true
|
||||||
|
|
|
||||||
84
.github/workflows/test_release.yml
vendored
Normal file
84
.github/workflows/test_release.yml
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
name: Create Test Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- t*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: 6.0.100
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
- name: Download Dalamud
|
||||||
|
run: |
|
||||||
|
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip
|
||||||
|
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
$ver = '${{ github.ref }}' -replace 'refs/tags/t',''
|
||||||
|
invoke-expression 'dotnet build --no-restore --configuration Debug --nologo -p:Version=$ver -p:FileVersion=$ver -p:AssemblyVersion=$ver'
|
||||||
|
- name: write version into json
|
||||||
|
run: |
|
||||||
|
$ver = '${{ github.ref }}' -replace 'refs/tags/t',''
|
||||||
|
$path = './Penumbra/bin/Debug/net5.0-windows/Penumbra.json'
|
||||||
|
$content = get-content -path $path
|
||||||
|
$content = $content -replace '1.0.0.0',$ver
|
||||||
|
set-content -Path $path -Value $content
|
||||||
|
- name: Archive
|
||||||
|
run: Compress-Archive -Path Penumbra/bin/Debug/net5.0-windows/* -DestinationPath Penumbra.zip
|
||||||
|
- name: Upload a Build Artifact
|
||||||
|
uses: actions/upload-artifact@v2.2.1
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./Penumbra/bin/Debug/net5.0-windows/*
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: Penumbra ${{ github.ref }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
- name: Upload Release Asset
|
||||||
|
id: upload-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||||
|
asset_path: ./Penumbra.zip
|
||||||
|
asset_name: Penumbra.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Write out repo.json
|
||||||
|
run: |
|
||||||
|
$ver = '${{ github.ref }}' -replace 'refs/tags/t',''
|
||||||
|
$ver2 = '${{ github.ref }}' -replace 'refs/tags/',''
|
||||||
|
$path = './base_repo.json'
|
||||||
|
$new_path = './repo.json'
|
||||||
|
$content = get-content -path $path
|
||||||
|
$content = $content -replace '/1.0.0.0/',"/$ver2/"
|
||||||
|
$content = $content -replace '1.0.0.0',$ver
|
||||||
|
set-content -Path $new_path -Value $content
|
||||||
|
|
||||||
|
- name: Commit repo.json
|
||||||
|
run: |
|
||||||
|
git config --global user.name "Actions User"
|
||||||
|
git config --global user.email "actions@github.com"
|
||||||
|
|
||||||
|
git fetch origin master && git fetch origin test && git branch -f test origin/master && git checkout test
|
||||||
|
git add repo.json
|
||||||
|
git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true
|
||||||
|
|
||||||
|
git push origin test -f || true
|
||||||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit a9a5b2a4bbf061d9cfed5234ca731bd2d94bcb96
|
Subproject commit d97a26923981db2a27d0172367c9e2841767f9b1
|
||||||
|
|
@ -44,6 +44,7 @@ internal class GamePathParser : IGamePathParser
|
||||||
, { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture/(?'minus'(--)?)(v(?'variant'\d{2})_)?c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?(_[a-z])?_[a-z]\.tex")
|
, { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture/(?'minus'(--)?)(v(?'variant'\d{2})_)?c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?(_[a-z])?_[a-z]\.tex")
|
||||||
, new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")
|
, new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")
|
||||||
, new(@"chara/common/texture/skin(?'skin'.*)\.tex")
|
, new(@"chara/common/texture/skin(?'skin'.*)\.tex")
|
||||||
|
, new(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")
|
||||||
, new(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex") } } } }
|
, new(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex") } } } }
|
||||||
, { FileType.Model, new Dictionary< ObjectType, Regex[] >()
|
, { FileType.Model, new Dictionary< ObjectType, Regex[] >()
|
||||||
{ { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl") } }
|
{ { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl") } }
|
||||||
|
|
@ -223,6 +224,11 @@ internal class GamePathParser : IGamePathParser
|
||||||
|
|
||||||
private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups )
|
private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups )
|
||||||
{
|
{
|
||||||
|
if( groups[ "catchlight" ].Success )
|
||||||
|
{
|
||||||
|
return GameObjectInfo.Customization( fileType, CustomizationType.Iris );
|
||||||
|
}
|
||||||
|
|
||||||
if( groups[ "skin" ].Success )
|
if( groups[ "skin" ].Success )
|
||||||
{
|
{
|
||||||
return GameObjectInfo.Customization( fileType, CustomizationType.Skin );
|
return GameObjectInfo.Customization( fileType, CustomizationType.Skin );
|
||||||
|
|
|
||||||
|
|
@ -290,10 +290,16 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
case CustomizationType.DecalFace:
|
case CustomizationType.DecalFace:
|
||||||
set[ $"Customization: Face Decal {info.PrimaryId}" ] = null;
|
set[ $"Customization: Face Decal {info.PrimaryId}" ] = null;
|
||||||
break;
|
break;
|
||||||
|
case CustomizationType.Iris when race == ModelRace.Unknown:
|
||||||
|
set[ $"Customization: All Eyes (Catchlight)" ] = null;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
var customizationString =
|
var customizationString = race == ModelRace.Unknown
|
||||||
$"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
|
|| info.BodySlot == BodySlot.Unknown
|
||||||
|
|| info.CustomizationType == CustomizationType.Unknown
|
||||||
|
? "Customization: Unknown"
|
||||||
|
: $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
|
||||||
set[ customizationString ] = null;
|
set[ customizationString ] = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.Design;
|
||||||
using Dalamud.Configuration;
|
using Dalamud.Configuration;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Lumina.Data;
|
using Lumina.Data;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
|
||||||
namespace Penumbra.Api;
|
namespace Penumbra.Api;
|
||||||
|
|
||||||
|
|
@ -16,6 +21,22 @@ public interface IPenumbraApiBase
|
||||||
public delegate void ChangedItemHover( object? item );
|
public delegate void ChangedItemHover( object? item );
|
||||||
public delegate void ChangedItemClick( MouseButton button, object? item );
|
public delegate void ChangedItemClick( MouseButton button, object? item );
|
||||||
|
|
||||||
|
public enum PenumbraApiEc
|
||||||
|
{
|
||||||
|
Okay = 0,
|
||||||
|
NothingChanged = 1,
|
||||||
|
CollectionMissing = 2,
|
||||||
|
ModMissing = 3,
|
||||||
|
OptionGroupMissing = 4,
|
||||||
|
SettingMissing = 5,
|
||||||
|
|
||||||
|
CharacterCollectionExists = 6,
|
||||||
|
LowerPriority = 7,
|
||||||
|
InvalidGamePath = 8,
|
||||||
|
FileMissing = 9,
|
||||||
|
InvalidManipulation = 10,
|
||||||
|
}
|
||||||
|
|
||||||
public interface IPenumbraApi : IPenumbraApiBase
|
public interface IPenumbraApi : IPenumbraApiBase
|
||||||
{
|
{
|
||||||
// Obtain the currently set mod directory from the configuration.
|
// Obtain the currently set mod directory from the configuration.
|
||||||
|
|
@ -34,6 +55,9 @@ public interface IPenumbraApi : IPenumbraApiBase
|
||||||
// Queue redrawing of all actors of the given name with the given RedrawType.
|
// Queue redrawing of all actors of the given name with the given RedrawType.
|
||||||
public void RedrawObject( string name, RedrawType setting );
|
public void RedrawObject( string name, RedrawType setting );
|
||||||
|
|
||||||
|
// Queue redrawing of the actor with the given object table index, if it exists, with the given RedrawType.
|
||||||
|
public void RedrawObject( int tableIndex, RedrawType setting );
|
||||||
|
|
||||||
// Queue redrawing of the specific actor with the given RedrawType. Should only be used when the actor is sure to be valid.
|
// Queue redrawing of the specific actor with the given RedrawType. Should only be used when the actor is sure to be valid.
|
||||||
public void RedrawObject( GameObject gameObject, RedrawType setting );
|
public void RedrawObject( GameObject gameObject, RedrawType setting );
|
||||||
|
|
||||||
|
|
@ -74,4 +98,64 @@ public interface IPenumbraApi : IPenumbraApiBase
|
||||||
|
|
||||||
// Obtain a list of all installed mods. The first string is their directory name, the second string is their mod name.
|
// Obtain a list of all installed mods. The first string is their directory name, the second string is their mod name.
|
||||||
public IList< (string, string) > GetModList();
|
public IList< (string, string) > GetModList();
|
||||||
|
|
||||||
|
|
||||||
|
// ############## Mod Settings #################
|
||||||
|
|
||||||
|
// Obtain the potential settings of a mod specified by its directory name first or mod name second.
|
||||||
|
// Returns null if the mod could not be found.
|
||||||
|
public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName );
|
||||||
|
|
||||||
|
// Obtain the enabled state, the priority, the settings of a mod specified by its directory name first or mod name second,
|
||||||
|
// and whether these settings are inherited, or null if the collection does not set them at all.
|
||||||
|
// If allowInheritance is false, only the collection itself will be checked.
|
||||||
|
public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName,
|
||||||
|
string modDirectory, string modName, bool allowInheritance );
|
||||||
|
|
||||||
|
// Try to set the inheritance state in the given collection of a mod specified by its directory name first or mod name second.
|
||||||
|
// Returns Okay, NothingChanged, CollectionMissing or ModMissing.
|
||||||
|
public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit );
|
||||||
|
|
||||||
|
// Try to set the enabled state in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance.
|
||||||
|
// Returns Okay, NothingChanged, CollectionMissing or ModMissing.
|
||||||
|
public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled );
|
||||||
|
|
||||||
|
// Try to set the priority in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance.
|
||||||
|
// Returns Okay, NothingChanged, CollectionMissing or ModMissing.
|
||||||
|
public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority );
|
||||||
|
|
||||||
|
// Try to set a specific option group in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance.
|
||||||
|
// If the group is a Single Selection group, options should be a single string, otherwise the array of enabled options.
|
||||||
|
// Returns Okay, NothingChanged, CollectionMissing or ModMissing, OptionGroupMissing or SettingMissing.
|
||||||
|
// If any setting can not be found, it will not change anything.
|
||||||
|
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option );
|
||||||
|
|
||||||
|
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName,
|
||||||
|
string[] options );
|
||||||
|
|
||||||
|
|
||||||
|
// Create a temporary collection without actual settings but with a cache.
|
||||||
|
// If character is non-zero and either no character collection for this character exists or forceOverwriteCharacter is true,
|
||||||
|
// associate this collection to a specific character.
|
||||||
|
// Can return Okay, CharacterCollectionExists or NothingChanged.
|
||||||
|
public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter );
|
||||||
|
|
||||||
|
// Remove a temporary collection if it exists.
|
||||||
|
// Can return Okay or NothingChanged.
|
||||||
|
public PenumbraApiEc RemoveTemporaryCollection( string collectionName );
|
||||||
|
|
||||||
|
|
||||||
|
// Set or remove a specific file redirection or meta manipulation under the name of Tag and with a given priority
|
||||||
|
// for a given collection, which may be temporary.
|
||||||
|
// Can return Okay, CollectionMissing, InvalidPath, FileMissing, LowerPriority, or NothingChanged.
|
||||||
|
public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority );
|
||||||
|
|
||||||
|
// Can return Okay, CollectionMissing, InvalidManipulation, LowerPriority, or NothingChanged.
|
||||||
|
public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority );
|
||||||
|
|
||||||
|
// Can return Okay, CollectionMissing, InvalidPath, or NothingChanged.
|
||||||
|
public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath );
|
||||||
|
|
||||||
|
// Can return Okay, CollectionMissing, InvalidManipulation, or NothingChanged.
|
||||||
|
public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 );
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ using Newtonsoft.Json;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
|
||||||
namespace Penumbra.Api;
|
namespace Penumbra.Api;
|
||||||
|
|
||||||
|
|
@ -71,6 +72,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RedrawObject( int tableIndex, RedrawType setting )
|
||||||
|
{
|
||||||
|
CheckInitialized();
|
||||||
|
_penumbra!.ObjectReloader.RedrawObject( tableIndex, setting );
|
||||||
|
}
|
||||||
|
|
||||||
public void RedrawObject( string name, RedrawType setting )
|
public void RedrawObject( string name, RedrawType setting )
|
||||||
{
|
{
|
||||||
CheckInitialized();
|
CheckInitialized();
|
||||||
|
|
@ -202,4 +209,44 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
CheckInitialized();
|
CheckInitialized();
|
||||||
return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray();
|
return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName,
|
||||||
|
bool allowInheritance )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string[] options )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc RemoveTemporaryCollection( string collectionName )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 )
|
||||||
|
=> throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
@ -112,10 +112,12 @@ public partial class PenumbraIpc
|
||||||
public partial class PenumbraIpc
|
public partial class PenumbraIpc
|
||||||
{
|
{
|
||||||
public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName";
|
public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName";
|
||||||
|
public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex";
|
||||||
public const string LabelProviderRedrawObject = "Penumbra.RedrawObject";
|
public const string LabelProviderRedrawObject = "Penumbra.RedrawObject";
|
||||||
public const string LabelProviderRedrawAll = "Penumbra.RedrawAll";
|
public const string LabelProviderRedrawAll = "Penumbra.RedrawAll";
|
||||||
|
|
||||||
internal ICallGateProvider< string, int, object >? ProviderRedrawName;
|
internal ICallGateProvider< string, int, object >? ProviderRedrawName;
|
||||||
|
internal ICallGateProvider< int, int, object >? ProviderRedrawIndex;
|
||||||
internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject;
|
internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject;
|
||||||
internal ICallGateProvider< int, object >? ProviderRedrawAll;
|
internal ICallGateProvider< int, object >? ProviderRedrawAll;
|
||||||
|
|
||||||
|
|
@ -142,6 +144,16 @@ public partial class PenumbraIpc
|
||||||
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" );
|
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProviderRedrawIndex = pi.GetIpcProvider<int, int, object>( LabelProviderRedrawIndex );
|
||||||
|
ProviderRedrawIndex.RegisterAction( ( idx, i ) => Api.RedrawObject( idx, CheckRedrawType( i ) ) );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" );
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject );
|
ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject );
|
||||||
|
|
@ -166,6 +178,7 @@ public partial class PenumbraIpc
|
||||||
private void DisposeRedrawProviders()
|
private void DisposeRedrawProviders()
|
||||||
{
|
{
|
||||||
ProviderRedrawName?.UnregisterAction();
|
ProviderRedrawName?.UnregisterAction();
|
||||||
|
ProviderRedrawIndex?.UnregisterAction();
|
||||||
ProviderRedrawObject?.UnregisterAction();
|
ProviderRedrawObject?.UnregisterAction();
|
||||||
ProviderRedrawAll?.UnregisterAction();
|
ProviderRedrawAll?.UnregisterAction();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,18 @@ public class RedrawController : WebApiController
|
||||||
public async Task Redraw()
|
public async Task Redraw()
|
||||||
{
|
{
|
||||||
var data = await HttpContext.GetRequestDataAsync< RedrawData >();
|
var data = await HttpContext.GetRequestDataAsync< RedrawData >();
|
||||||
_penumbra.Api.RedrawObject( data.Name, data.Type );
|
if( data.ObjectTableIndex >= 0 )
|
||||||
|
{
|
||||||
|
_penumbra.Api.RedrawObject( data.ObjectTableIndex, data.Type );
|
||||||
|
}
|
||||||
|
else if( data.Name.Length > 0 )
|
||||||
|
{
|
||||||
|
_penumbra.Api.RedrawObject( data.Name, data.Type );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_penumbra.Api.RedrawAll( data.Type );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route( HttpVerbs.Post, "/redrawAll" )]
|
[Route( HttpVerbs.Post, "/redrawAll" )]
|
||||||
|
|
@ -30,5 +41,6 @@ public class RedrawController : WebApiController
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public RedrawType Type { get; set; } = RedrawType.Redraw;
|
public RedrawType Type { get; set; } = RedrawType.Redraw;
|
||||||
|
public int ObjectTableIndex { get; set; } = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,10 +87,18 @@ public partial class ModCollection
|
||||||
ReloadMod( Penumbra.ModManager[ modIdx ], true );
|
ReloadMod( Penumbra.ModManager[ modIdx ], true );
|
||||||
break;
|
break;
|
||||||
case ModSettingChange.EnableState:
|
case ModSettingChange.EnableState:
|
||||||
if( oldValue != 1 )
|
if( oldValue == 0 )
|
||||||
{
|
{
|
||||||
AddMod( Penumbra.ModManager[ modIdx ], true );
|
AddMod( Penumbra.ModManager[ modIdx ], true );
|
||||||
}
|
}
|
||||||
|
else if( oldValue == 1 )
|
||||||
|
{
|
||||||
|
RemoveMod( Penumbra.ModManager[ modIdx ], true );
|
||||||
|
}
|
||||||
|
else if( _collection[ modIdx ].Settings?.Enabled == true )
|
||||||
|
{
|
||||||
|
ReloadMod( Penumbra.ModManager[ modIdx ], true );
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
RemoveMod( Penumbra.ModManager[ modIdx ], true );
|
RemoveMod( Penumbra.ModManager[ modIdx ], true );
|
||||||
|
|
@ -257,6 +265,7 @@ public partial class ModCollection
|
||||||
{
|
{
|
||||||
AddMetaFiles();
|
AddMetaFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
if( _collection == Penumbra.CollectionManager.Default )
|
if( _collection == Penumbra.CollectionManager.Default )
|
||||||
{
|
{
|
||||||
Penumbra.ResidentResources.Reload();
|
Penumbra.ResidentResources.Reload();
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ public partial class Configuration : IPluginConfiguration
|
||||||
|
|
||||||
public bool FixMainWindow { get; set; } = false;
|
public bool FixMainWindow { get; set; } = false;
|
||||||
public bool ShowAdvanced { get; set; }
|
public bool ShowAdvanced { get; set; }
|
||||||
|
public bool AutoDeduplicateOnImport { get; set; } = false;
|
||||||
public bool DisableSoundStreaming { get; set; } = true;
|
public bool DisableSoundStreaming { get; set; } = true;
|
||||||
public bool EnableHttpApi { get; set; }
|
public bool EnableHttpApi { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
322
Penumbra/Import/Dds/DXT10Header.cs
Normal file
322
Penumbra/Import/Dds/DXT10Header.cs
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Dds;
|
||||||
|
|
||||||
|
[StructLayout( LayoutKind.Sequential )]
|
||||||
|
public struct DXT10Header
|
||||||
|
{
|
||||||
|
public DXGIFormat Format;
|
||||||
|
public D3DResourceDimension ResourceDimension;
|
||||||
|
public D3DResourceMiscFlags MiscFlags;
|
||||||
|
public uint ArraySize;
|
||||||
|
public D3DAlphaMode AlphaMode;
|
||||||
|
|
||||||
|
public ParseType ToParseType()
|
||||||
|
{
|
||||||
|
return Format switch
|
||||||
|
{
|
||||||
|
DXGIFormat.BC1Typeless => ParseType.DXT1,
|
||||||
|
DXGIFormat.BC1UNorm => ParseType.DXT1,
|
||||||
|
DXGIFormat.BC1UNormSRGB => ParseType.DXT1,
|
||||||
|
|
||||||
|
DXGIFormat.BC2Typeless => ParseType.DXT3,
|
||||||
|
DXGIFormat.BC2UNorm => ParseType.DXT3,
|
||||||
|
DXGIFormat.BC2UNormSRGB => ParseType.DXT3,
|
||||||
|
|
||||||
|
DXGIFormat.BC3Typeless => ParseType.DXT5,
|
||||||
|
DXGIFormat.BC3UNorm => ParseType.DXT5,
|
||||||
|
DXGIFormat.BC3UNormSRGB => ParseType.DXT5,
|
||||||
|
|
||||||
|
DXGIFormat.BC4Typeless => ParseType.BC4,
|
||||||
|
DXGIFormat.BC4SNorm => ParseType.BC4,
|
||||||
|
DXGIFormat.BC4UNorm => ParseType.BC4,
|
||||||
|
|
||||||
|
DXGIFormat.BC5Typeless => ParseType.BC5,
|
||||||
|
DXGIFormat.BC5SNorm => ParseType.BC5,
|
||||||
|
DXGIFormat.BC5UNorm => ParseType.BC5,
|
||||||
|
|
||||||
|
DXGIFormat.R8G8B8A8Typeless => ParseType.R8G8B8A8,
|
||||||
|
DXGIFormat.R8G8B8A8UNorm => ParseType.R8G8B8A8,
|
||||||
|
DXGIFormat.R8G8B8A8UNormSRGB => ParseType.R8G8B8A8,
|
||||||
|
DXGIFormat.R8G8B8A8UInt => ParseType.R8G8B8A8,
|
||||||
|
DXGIFormat.R8G8B8A8SNorm => ParseType.R8G8B8A8,
|
||||||
|
DXGIFormat.R8G8B8A8SInt => ParseType.R8G8B8A8,
|
||||||
|
|
||||||
|
DXGIFormat.B8G8R8A8Typeless => ParseType.B8G8R8A8,
|
||||||
|
DXGIFormat.B8G8R8A8UNorm => ParseType.B8G8R8A8,
|
||||||
|
DXGIFormat.B8G8R8A8UNormSRGB => ParseType.B8G8R8A8,
|
||||||
|
DXGIFormat.B8G8R8X8Typeless => ParseType.B8G8R8A8,
|
||||||
|
DXGIFormat.B8G8R8X8UNorm => ParseType.B8G8R8A8,
|
||||||
|
DXGIFormat.B8G8R8X8UNormSRGB => ParseType.B8G8R8A8,
|
||||||
|
|
||||||
|
DXGIFormat.B4G4R4A4UNorm => ParseType.B4G4R4A4,
|
||||||
|
DXGIFormat.B5G5R5A1UNorm => ParseType.B5G5R5A1,
|
||||||
|
DXGIFormat.B5G6R5UNorm => ParseType.B5G6R5,
|
||||||
|
|
||||||
|
DXGIFormat.BC6HSF16 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.BC6HTypeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.BC6HUF16 => ParseType.Unsupported,
|
||||||
|
|
||||||
|
DXGIFormat.BC7Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.BC7UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.BC7UNormSRGB => ParseType.Unsupported,
|
||||||
|
|
||||||
|
DXGIFormat.Unknown => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32A32Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32A32Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32A32UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32A32SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32B32SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16B16A16Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16B16A16Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16B16A16UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16B16A16UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16B16A16SNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16B16A16SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G32SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32G8X24Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.D32FloatS8X24UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32FloatX8X24Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.X32TypelessG8X24UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R10G10B10A2Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R10G10B10A2UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R10G10B10A2UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R11G11B10Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16SNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16G16SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.D32Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R32SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R24G8Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.D24UNormS8UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R24UNormX8Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.X24TypelessG8UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8G8Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8G8UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8G8UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8G8SNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8G8SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16Float => ParseType.Unsupported,
|
||||||
|
DXGIFormat.D16UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16SNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R16SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8Typeless => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8UInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8SNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8SInt => ParseType.Unsupported,
|
||||||
|
DXGIFormat.A8UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R1UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R9G9B9E5SharedEXP => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R8G8B8G8UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.G8R8G8B8UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.R10G10B10XRBiasA2UNorm => ParseType.Unsupported,
|
||||||
|
DXGIFormat.AYUV => ParseType.Unsupported,
|
||||||
|
DXGIFormat.Y410 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.Y416 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.NV12 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.P010 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.P016 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.F420Opaque => ParseType.Unsupported,
|
||||||
|
DXGIFormat.YUY2 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.Y210 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.Y216 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.NV11 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.AI44 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.IA44 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.P8 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.A8P8 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.P208 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.V208 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.V408 => ParseType.Unsupported,
|
||||||
|
DXGIFormat.SamplerFeedbackMinMipOpaque => ParseType.Unsupported,
|
||||||
|
DXGIFormat.SamplerFeedbackMipRegionUsedOpaque => ParseType.Unsupported,
|
||||||
|
DXGIFormat.ForceUInt => ParseType.Unsupported,
|
||||||
|
_ => ParseType.Unsupported,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DXGIFormat : uint
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
R32G32B32A32Typeless = 1,
|
||||||
|
R32G32B32A32Float = 2,
|
||||||
|
R32G32B32A32UInt = 3,
|
||||||
|
R32G32B32A32SInt = 4,
|
||||||
|
R32G32B32Typeless = 5,
|
||||||
|
R32G32B32Float = 6,
|
||||||
|
R32G32B32UInt = 7,
|
||||||
|
R32G32B32SInt = 8,
|
||||||
|
R16G16B16A16Typeless = 9,
|
||||||
|
R16G16B16A16Float = 10,
|
||||||
|
R16G16B16A16UNorm = 11,
|
||||||
|
R16G16B16A16UInt = 12,
|
||||||
|
R16G16B16A16SNorm = 13,
|
||||||
|
R16G16B16A16SInt = 14,
|
||||||
|
R32G32Typeless = 15,
|
||||||
|
R32G32Float = 16,
|
||||||
|
R32G32UInt = 17,
|
||||||
|
R32G32SInt = 18,
|
||||||
|
R32G8X24Typeless = 19,
|
||||||
|
D32FloatS8X24UInt = 20,
|
||||||
|
R32FloatX8X24Typeless = 21,
|
||||||
|
X32TypelessG8X24UInt = 22,
|
||||||
|
R10G10B10A2Typeless = 23,
|
||||||
|
R10G10B10A2UNorm = 24,
|
||||||
|
R10G10B10A2UInt = 25,
|
||||||
|
R11G11B10Float = 26,
|
||||||
|
R8G8B8A8Typeless = 27,
|
||||||
|
R8G8B8A8UNorm = 28,
|
||||||
|
R8G8B8A8UNormSRGB = 29,
|
||||||
|
R8G8B8A8UInt = 30,
|
||||||
|
R8G8B8A8SNorm = 31,
|
||||||
|
R8G8B8A8SInt = 32,
|
||||||
|
R16G16Typeless = 33,
|
||||||
|
R16G16Float = 34,
|
||||||
|
R16G16UNorm = 35,
|
||||||
|
R16G16UInt = 36,
|
||||||
|
R16G16SNorm = 37,
|
||||||
|
R16G16SInt = 38,
|
||||||
|
R32Typeless = 39,
|
||||||
|
D32Float = 40,
|
||||||
|
R32Float = 41,
|
||||||
|
R32UInt = 42,
|
||||||
|
R32SInt = 43,
|
||||||
|
R24G8Typeless = 44,
|
||||||
|
D24UNormS8UInt = 45,
|
||||||
|
R24UNormX8Typeless = 46,
|
||||||
|
X24TypelessG8UInt = 47,
|
||||||
|
R8G8Typeless = 48,
|
||||||
|
R8G8UNorm = 49,
|
||||||
|
R8G8UInt = 50,
|
||||||
|
R8G8SNorm = 51,
|
||||||
|
R8G8SInt = 52,
|
||||||
|
R16Typeless = 53,
|
||||||
|
R16Float = 54,
|
||||||
|
D16UNorm = 55,
|
||||||
|
R16UNorm = 56,
|
||||||
|
R16UInt = 57,
|
||||||
|
R16SNorm = 58,
|
||||||
|
R16SInt = 59,
|
||||||
|
R8Typeless = 60,
|
||||||
|
R8UNorm = 61,
|
||||||
|
R8UInt = 62,
|
||||||
|
R8SNorm = 63,
|
||||||
|
R8SInt = 64,
|
||||||
|
A8UNorm = 65,
|
||||||
|
R1UNorm = 66,
|
||||||
|
R9G9B9E5SharedEXP = 67,
|
||||||
|
R8G8B8G8UNorm = 68,
|
||||||
|
G8R8G8B8UNorm = 69,
|
||||||
|
BC1Typeless = 70,
|
||||||
|
BC1UNorm = 71,
|
||||||
|
BC1UNormSRGB = 72,
|
||||||
|
BC2Typeless = 73,
|
||||||
|
BC2UNorm = 74,
|
||||||
|
BC2UNormSRGB = 75,
|
||||||
|
BC3Typeless = 76,
|
||||||
|
BC3UNorm = 77,
|
||||||
|
BC3UNormSRGB = 78,
|
||||||
|
BC4Typeless = 79,
|
||||||
|
BC4UNorm = 80,
|
||||||
|
BC4SNorm = 81,
|
||||||
|
BC5Typeless = 82,
|
||||||
|
BC5UNorm = 83,
|
||||||
|
BC5SNorm = 84,
|
||||||
|
B5G6R5UNorm = 85,
|
||||||
|
B5G5R5A1UNorm = 86,
|
||||||
|
B8G8R8A8UNorm = 87,
|
||||||
|
B8G8R8X8UNorm = 88,
|
||||||
|
R10G10B10XRBiasA2UNorm = 89,
|
||||||
|
B8G8R8A8Typeless = 90,
|
||||||
|
B8G8R8A8UNormSRGB = 91,
|
||||||
|
B8G8R8X8Typeless = 92,
|
||||||
|
B8G8R8X8UNormSRGB = 93,
|
||||||
|
BC6HTypeless = 94,
|
||||||
|
BC6HUF16 = 95,
|
||||||
|
BC6HSF16 = 96,
|
||||||
|
BC7Typeless = 97,
|
||||||
|
BC7UNorm = 98,
|
||||||
|
BC7UNormSRGB = 99,
|
||||||
|
AYUV = 100,
|
||||||
|
Y410 = 101,
|
||||||
|
Y416 = 102,
|
||||||
|
NV12 = 103,
|
||||||
|
P010 = 104,
|
||||||
|
P016 = 105,
|
||||||
|
F420Opaque = 106,
|
||||||
|
YUY2 = 107,
|
||||||
|
Y210 = 108,
|
||||||
|
Y216 = 109,
|
||||||
|
NV11 = 110,
|
||||||
|
AI44 = 111,
|
||||||
|
IA44 = 112,
|
||||||
|
P8 = 113,
|
||||||
|
A8P8 = 114,
|
||||||
|
B4G4R4A4UNorm = 115,
|
||||||
|
P208 = 130,
|
||||||
|
V208 = 131,
|
||||||
|
V408 = 132,
|
||||||
|
SamplerFeedbackMinMipOpaque,
|
||||||
|
SamplerFeedbackMipRegionUsedOpaque,
|
||||||
|
ForceUInt = 0xffffffff,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum D3DResourceDimension : int
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Buffer = 1,
|
||||||
|
Texture1D = 2,
|
||||||
|
Texture2D = 3,
|
||||||
|
Texture3D = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum D3DResourceMiscFlags : uint
|
||||||
|
{
|
||||||
|
GenerateMips = 0x000001,
|
||||||
|
Shared = 0x000002,
|
||||||
|
TextureCube = 0x000004,
|
||||||
|
DrawIndirectArgs = 0x000010,
|
||||||
|
BufferAllowRawViews = 0x000020,
|
||||||
|
BufferStructured = 0x000040,
|
||||||
|
ResourceClamp = 0x000080,
|
||||||
|
SharedKeyedMutex = 0x000100,
|
||||||
|
GDICompatible = 0x000200,
|
||||||
|
SharedNTHandle = 0x000800,
|
||||||
|
RestrictedContent = 0x001000,
|
||||||
|
RestrictSharedResource = 0x002000,
|
||||||
|
RestrictSharedResourceDriver = 0x004000,
|
||||||
|
Guarded = 0x008000,
|
||||||
|
TilePool = 0x020000,
|
||||||
|
Tiled = 0x040000,
|
||||||
|
HWProtected = 0x080000,
|
||||||
|
SharedDisplayable,
|
||||||
|
SharedExclusiveWriter,
|
||||||
|
};
|
||||||
|
|
||||||
|
public enum D3DAlphaMode : int
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Straight = 1,
|
||||||
|
Premultiplied = 2,
|
||||||
|
Opaque = 3,
|
||||||
|
Custom = 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
253
Penumbra/Import/Dds/DdsFile.cs
Normal file
253
Penumbra/Import/Dds/DdsFile.cs
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using Lumina.Data.Files;
|
||||||
|
using Lumina.Extensions;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Dds;
|
||||||
|
|
||||||
|
public class DdsFile
|
||||||
|
{
|
||||||
|
public const int DdsIdentifier = 0x20534444;
|
||||||
|
|
||||||
|
public readonly DdsHeader Header;
|
||||||
|
public readonly DXT10Header? DXT10Header;
|
||||||
|
private readonly byte[] _data;
|
||||||
|
|
||||||
|
public ReadOnlySpan< byte > Data
|
||||||
|
=> _data;
|
||||||
|
|
||||||
|
public ReadOnlySpan< byte > MipMap( int level )
|
||||||
|
{
|
||||||
|
var mipSize = ParseType switch
|
||||||
|
{
|
||||||
|
ParseType.Unsupported => 0,
|
||||||
|
ParseType.DXT1 => Header.Height * Header.Width / 2,
|
||||||
|
ParseType.BC4 => Header.Height * Header.Width / 2,
|
||||||
|
|
||||||
|
ParseType.DXT3 => Header.Height * Header.Width,
|
||||||
|
ParseType.DXT5 => Header.Height * Header.Width,
|
||||||
|
ParseType.BC5 => Header.Height * Header.Width,
|
||||||
|
ParseType.Greyscale => Header.Height * Header.Width,
|
||||||
|
|
||||||
|
ParseType.R4G4B4A4 => Header.Height * Header.Width * 2,
|
||||||
|
ParseType.B4G4R4A4 => Header.Height * Header.Width * 2,
|
||||||
|
ParseType.R5G5B5 => Header.Height * Header.Width * 2,
|
||||||
|
ParseType.B5G5R5 => Header.Height * Header.Width * 2,
|
||||||
|
ParseType.R5G6B5 => Header.Height * Header.Width * 2,
|
||||||
|
ParseType.B5G6R5 => Header.Height * Header.Width * 2,
|
||||||
|
ParseType.R5G5B5A1 => Header.Height * Header.Width * 2,
|
||||||
|
ParseType.B5G5R5A1 => Header.Height * Header.Width * 2,
|
||||||
|
|
||||||
|
ParseType.R8G8B8 => Header.Height * Header.Width * 3,
|
||||||
|
ParseType.B8G8R8 => Header.Height * Header.Width * 3,
|
||||||
|
|
||||||
|
ParseType.R8G8B8A8 => Header.Height * Header.Width * 4,
|
||||||
|
ParseType.B8G8R8A8 => Header.Height * Header.Width * 4,
|
||||||
|
_ => throw new ArgumentOutOfRangeException( nameof( ParseType ), ParseType, null ),
|
||||||
|
};
|
||||||
|
|
||||||
|
if( Header.MipMapCount < level )
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException( nameof( level ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = 0;
|
||||||
|
for( var i = 0; i < level; ++i )
|
||||||
|
{
|
||||||
|
sum += mipSize;
|
||||||
|
mipSize = Math.Max( 16, mipSize >> 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if( _data.Length < sum + mipSize )
|
||||||
|
{
|
||||||
|
throw new Exception( "Not enough data to encode image." );
|
||||||
|
}
|
||||||
|
|
||||||
|
return _data.AsSpan( sum, mipSize );
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[]? _rgbaData;
|
||||||
|
public readonly ParseType ParseType;
|
||||||
|
|
||||||
|
public ReadOnlySpan< byte > RgbaData
|
||||||
|
=> _rgbaData ??= ParseToRgba();
|
||||||
|
|
||||||
|
private DdsFile( ParseType type, DdsHeader header, byte[] data, DXT10Header? dxt10Header = null )
|
||||||
|
{
|
||||||
|
ParseType = type;
|
||||||
|
Header = header;
|
||||||
|
DXT10Header = dxt10Header;
|
||||||
|
_data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ParseToRgba()
|
||||||
|
{
|
||||||
|
return ParseType switch
|
||||||
|
{
|
||||||
|
ParseType.Unsupported => Array.Empty< byte >(),
|
||||||
|
ParseType.DXT1 => ImageParsing.DecodeDxt1( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.DXT3 => ImageParsing.DecodeDxt3( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.DXT5 => ImageParsing.DecodeDxt5( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.BC4 => ImageParsing.DecodeBc4( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.BC5 => ImageParsing.DecodeBc5( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.Greyscale => ImageParsing.DecodeUncompressedGreyscale( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.B4G4R4A4 => ImageParsing.DecodeUncompressedB4G4R4A4( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.R5G5B5 => ImageParsing.DecodeUncompressedR5G5B5( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.B5G5R5 => ImageParsing.DecodeUncompressedB5G5R5( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.R5G6B5 => ImageParsing.DecodeUncompressedR5G6B5( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.B5G6R5 => ImageParsing.DecodeUncompressedB5G6R5( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.B5G5R5A1 => ImageParsing.DecodeUncompressedB5G5R5A1( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.R8G8B8 => ImageParsing.DecodeUncompressedR8G8B8( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.B8G8R8 => ImageParsing.DecodeUncompressedB8G8R8( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
ParseType.R8G8B8A8 => _data.Length == Header.Width * Header.Height * 4 ? _data : _data[ ..( Header.Width * Header.Height * 4 ) ],
|
||||||
|
ParseType.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( MipMap( 0 ), Header.Height, Header.Width ),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Load( Stream data, [NotNullWhen( true )] out DdsFile? file )
|
||||||
|
{
|
||||||
|
file = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var br = new BinaryReader( data );
|
||||||
|
if( br.ReadUInt32() != DdsIdentifier )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = br.ReadStructure< DdsHeader >();
|
||||||
|
var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null;
|
||||||
|
var type = header.PixelFormat.ToParseType( dxt10 );
|
||||||
|
|
||||||
|
file = new DdsFile( type, header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), dxt10 );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not load DDS file:\n{e}" );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ConvertToTex( out byte[] texBytes )
|
||||||
|
{
|
||||||
|
using var mem = new MemoryStream( _data.Length * 2 );
|
||||||
|
using( var bw = new BinaryWriter( mem ) )
|
||||||
|
{
|
||||||
|
var (format, mipLength) = WriteTexHeader( bw );
|
||||||
|
var bytes = format == TexFile.TextureFormat.R8G8B8X8 ? RgbaData : _data;
|
||||||
|
|
||||||
|
if( bytes.Length < mipLength )
|
||||||
|
{
|
||||||
|
throw new Exception( "Broken file. Not enough data." );
|
||||||
|
}
|
||||||
|
|
||||||
|
bw.Write( _data.AsSpan( 0, mipLength ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
texBytes = mem.ToArray();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (TexFile.TextureFormat, int) WriteTexHeader( BinaryWriter bw )
|
||||||
|
{
|
||||||
|
var (format, mipLength) = ConvertFormat( ParseType, Header.Height, Header.Width );
|
||||||
|
if( mipLength == 0 )
|
||||||
|
{
|
||||||
|
throw new Exception( "Invalid format to convert to tex." );
|
||||||
|
}
|
||||||
|
|
||||||
|
var mipCount = Header.MipMapCount;
|
||||||
|
if( format == TexFile.TextureFormat.R8G8B8X8 && ParseType != ParseType.R8G8B8A8 )
|
||||||
|
{
|
||||||
|
mipCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bw.Write( ( uint )TexFile.Attribute.TextureType2D );
|
||||||
|
bw.Write( ( uint )format );
|
||||||
|
bw.Write( ( ushort )Header.Width );
|
||||||
|
bw.Write( ( ushort )Header.Height );
|
||||||
|
bw.Write( ( ushort )Header.Depth );
|
||||||
|
bw.Write( ( ushort )mipCount );
|
||||||
|
bw.Write( 0 );
|
||||||
|
bw.Write( 1 );
|
||||||
|
bw.Write( 2 );
|
||||||
|
|
||||||
|
var offset = 80;
|
||||||
|
var mipLengthSum = 0;
|
||||||
|
for( var i = 0; i < mipCount; ++i )
|
||||||
|
{
|
||||||
|
bw.Write( offset );
|
||||||
|
offset += mipLength;
|
||||||
|
mipLengthSum += mipLength;
|
||||||
|
mipLength = Math.Max( 16, mipLength >> 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
for( var i = mipCount; i < 13; ++i )
|
||||||
|
{
|
||||||
|
bw.Write( 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return ( format, mipLengthSum );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (TexFile.TextureFormat, int) ConvertFormat( ParseType type, int height, int width )
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
ParseType.Unsupported => ( TexFile.TextureFormat.Unknown, 0 ),
|
||||||
|
ParseType.DXT1 => ( TexFile.TextureFormat.DXT1, height * width / 2 ),
|
||||||
|
ParseType.DXT3 => ( TexFile.TextureFormat.DXT3, height * width * 2 ),
|
||||||
|
ParseType.DXT5 => ( TexFile.TextureFormat.DXT5, height * width * 2 ),
|
||||||
|
ParseType.BC4 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.BC5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.Greyscale => ( TexFile.TextureFormat.A8, height * width ),
|
||||||
|
ParseType.R4G4B4A4 => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ),
|
||||||
|
ParseType.B4G4R4A4 => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ),
|
||||||
|
ParseType.R5G5B5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.B5G5R5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.R5G6B5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.B5G6R5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.R5G5B5A1 => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ),
|
||||||
|
ParseType.B5G5R5A1 => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ),
|
||||||
|
ParseType.R8G8B8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.B8G8R8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.R8G8B8A8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
ParseType.B8G8R8A8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ),
|
||||||
|
_ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TmpTexFile
|
||||||
|
{
|
||||||
|
public TexFile.TexHeader Header;
|
||||||
|
public byte[] RgbaData;
|
||||||
|
|
||||||
|
public void Load( BinaryReader br )
|
||||||
|
{
|
||||||
|
Header = br.ReadStructure< TexFile.TexHeader >();
|
||||||
|
var data = br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) );
|
||||||
|
RgbaData = Header.Format switch
|
||||||
|
{
|
||||||
|
TexFile.TextureFormat.L8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.A8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.DXT1 => ImageParsing.DecodeDxt1( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.DXT3 => ImageParsing.DecodeDxt3( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.DXT5 => ImageParsing.DecodeDxt5( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.A8R8G8B8 => ImageParsing.DecodeUncompressedB8G8R8A8( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.R8G8B8X8 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.A8R8G8B82 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( data, Header.Height, Header.Width ),
|
||||||
|
TexFile.TextureFormat.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( data, Header.Height, Header.Width ),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
109
Penumbra/Import/Dds/DdsHeader.cs
Normal file
109
Penumbra/Import/Dds/DdsHeader.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Dds;
|
||||||
|
|
||||||
|
[StructLayout( LayoutKind.Sequential )]
|
||||||
|
public struct DdsHeader
|
||||||
|
{
|
||||||
|
public const int Size = 124;
|
||||||
|
public const uint MagicNumber = 'D' | ( 'D' << 8 ) | ( 'S' << 16 ) | ( ' ' << 24 );
|
||||||
|
|
||||||
|
private int _size;
|
||||||
|
public DdsFlags Flags;
|
||||||
|
public int Height;
|
||||||
|
public int Width;
|
||||||
|
public int PitchOrLinearSize;
|
||||||
|
public int Depth;
|
||||||
|
public int MipMapCount;
|
||||||
|
public int Reserved1;
|
||||||
|
public int Reserved2;
|
||||||
|
public int Reserved3;
|
||||||
|
public int Reserved4;
|
||||||
|
public int Reserved5;
|
||||||
|
public int Reserved6;
|
||||||
|
public int Reserved7;
|
||||||
|
public int Reserved8;
|
||||||
|
public int Reserved9;
|
||||||
|
public int ReservedA;
|
||||||
|
public int ReservedB;
|
||||||
|
public PixelFormat PixelFormat;
|
||||||
|
public DdsCaps1 Caps1;
|
||||||
|
public DdsCaps2 Caps2;
|
||||||
|
public uint Caps3;
|
||||||
|
public uint Caps4;
|
||||||
|
public int ReservedC;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum DdsFlags : uint
|
||||||
|
{
|
||||||
|
Caps = 0x00000001,
|
||||||
|
Height = 0x00000002,
|
||||||
|
Width = 0x00000004,
|
||||||
|
Pitch = 0x00000008,
|
||||||
|
PixelFormat = 0x00001000,
|
||||||
|
MipMapCount = 0x00020000,
|
||||||
|
LinearSize = 0x00080000,
|
||||||
|
Depth = 0x00800000,
|
||||||
|
|
||||||
|
Required = Caps | Height | Width | PixelFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum DdsCaps1 : uint
|
||||||
|
{
|
||||||
|
Complex = 0x08,
|
||||||
|
MipMap = 0x400000,
|
||||||
|
Texture = 0x1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum DdsCaps2 : uint
|
||||||
|
{
|
||||||
|
CubeMap = 0x200,
|
||||||
|
CubeMapPositiveEX = 0x400,
|
||||||
|
CubeMapNegativeEX = 0x800,
|
||||||
|
CubeMapPositiveEY = 0x1000,
|
||||||
|
CubeMapNegativeEY = 0x2000,
|
||||||
|
CubeMapPositiveEZ = 0x4000,
|
||||||
|
CubeMapNegativeEZ = 0x8000,
|
||||||
|
Volume = 0x200000,
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write( BinaryWriter bw )
|
||||||
|
{
|
||||||
|
bw.Write( MagicNumber );
|
||||||
|
bw.Write( Size );
|
||||||
|
bw.Write( ( uint )Flags );
|
||||||
|
bw.Write( Height );
|
||||||
|
bw.Write( Width );
|
||||||
|
bw.Write( PitchOrLinearSize );
|
||||||
|
bw.Write( Depth );
|
||||||
|
bw.Write( MipMapCount );
|
||||||
|
bw.Write( Reserved1 );
|
||||||
|
bw.Write( Reserved2 );
|
||||||
|
bw.Write( Reserved3 );
|
||||||
|
bw.Write( Reserved4 );
|
||||||
|
bw.Write( Reserved5 );
|
||||||
|
bw.Write( Reserved6 );
|
||||||
|
bw.Write( Reserved7 );
|
||||||
|
bw.Write( Reserved8 );
|
||||||
|
bw.Write( Reserved9 );
|
||||||
|
bw.Write( ReservedA );
|
||||||
|
bw.Write( ReservedB );
|
||||||
|
PixelFormat.Write( bw );
|
||||||
|
bw.Write( ( uint )Caps1 );
|
||||||
|
bw.Write( ( uint )Caps2 );
|
||||||
|
bw.Write( Caps3 );
|
||||||
|
bw.Write( Caps4 );
|
||||||
|
bw.Write( ReservedC );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write( byte[] bytes, int offset )
|
||||||
|
{
|
||||||
|
using var m = new MemoryStream( bytes, offset, bytes.Length - offset );
|
||||||
|
using var bw = new BinaryWriter( m );
|
||||||
|
Write( bw );
|
||||||
|
}
|
||||||
|
}
|
||||||
580
Penumbra/Import/Dds/ImageParsing.cs
Normal file
580
Penumbra/Import/Dds/ImageParsing.cs
Normal file
|
|
@ -0,0 +1,580 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Dds;
|
||||||
|
|
||||||
|
public static partial class ImageParsing
|
||||||
|
{
|
||||||
|
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||||
|
private static Rgba32 Get565Color( ushort c )
|
||||||
|
{
|
||||||
|
var ret = new Rgba32
|
||||||
|
{
|
||||||
|
R = ( byte )( c & 0x1F ),
|
||||||
|
G = ( byte )( ( c >> 5 ) & 0x3F ),
|
||||||
|
B = ( byte )( c >> 11 ),
|
||||||
|
A = 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
ret.R = ( byte )( ( ret.R << 3 ) | ( ret.R >> 2 ) );
|
||||||
|
ret.G = ( byte )( ( ret.G << 2 ) | ( ret.G >> 3 ) );
|
||||||
|
ret.B = ( byte )( ( ret.B << 3 ) | ( ret.B >> 2 ) );
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||||
|
private static (Rgba32, Rgba32) GetDxt1CombinedColors( bool c1Larger, Rgba32 c1, Rgba32 c2 )
|
||||||
|
{
|
||||||
|
if( c1Larger )
|
||||||
|
{
|
||||||
|
static byte C( byte a1, byte a2 )
|
||||||
|
=> ( byte )( ( 2 * a1 + a2 ) / 3 );
|
||||||
|
|
||||||
|
return ( new Rgba32( C( c1.R, c2.R ), C( c1.G, c2.G ), C( c1.B, c2.B ) ),
|
||||||
|
new Rgba32( C( c2.R, c1.R ), C( c2.G, c1.G ), C( c2.B, c1.B ) ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
static byte C( byte a1, byte a2 )
|
||||||
|
=> ( byte )( ( a1 + a2 ) / 2 );
|
||||||
|
|
||||||
|
return ( new Rgba32( C( c1.R, c2.R ), C( c1.G, c2.G ), C( c1.B, c2.B ) ),
|
||||||
|
new Rgba32( 0, 0, 0, 0 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||||
|
private static unsafe byte* CopyBytes( byte* ptr, Rgba32 color, byte alpha )
|
||||||
|
{
|
||||||
|
*ptr++ = color.R;
|
||||||
|
*ptr++ = color.G;
|
||||||
|
*ptr++ = color.B;
|
||||||
|
*ptr++ = alpha;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||||
|
private static unsafe byte* CopyBytes( byte* ptr, Rgba32 color )
|
||||||
|
=> CopyBytes( ptr, color, color.A );
|
||||||
|
|
||||||
|
|
||||||
|
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||||
|
private static unsafe byte* CopyBytesAlphaDown( byte* ptr, Rgba32 color, byte alpha )
|
||||||
|
=> CopyBytes( ptr, color, ( byte )( ( alpha & 0x0F ) | ( alpha << 4 ) ) );
|
||||||
|
|
||||||
|
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||||
|
private static unsafe byte* CopyBytesAlphaUp( byte* ptr, Rgba32 color, byte alpha )
|
||||||
|
=> CopyBytes( ptr, color, ( byte )( ( alpha & 0xF0 ) | ( alpha >> 4 ) ) );
|
||||||
|
|
||||||
|
private static void Verify( ReadOnlySpan< byte > data, int height, int width, int blockSize, int bytes )
|
||||||
|
{
|
||||||
|
if( data.Length % bytes != 0 )
|
||||||
|
{
|
||||||
|
throw new ArgumentException( $"Length {data.Length} not a multiple of {bytes} bytes.", nameof( data ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( height * width > data.Length * blockSize * blockSize / bytes )
|
||||||
|
{
|
||||||
|
throw new ArgumentException( $"Not enough data encoded in {data.Length} to fill image of dimensions {height} * {width}.",
|
||||||
|
nameof( data ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( height % blockSize != 0 )
|
||||||
|
{
|
||||||
|
throw new ArgumentException( $"Height must be a multiple of {blockSize}.", nameof( height ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( width % blockSize != 0 )
|
||||||
|
{
|
||||||
|
throw new ArgumentException( $"Height must be a multiple of {blockSize}.", nameof( height ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe byte* GetDxt1Colors( byte* ptr, Span< Rgba32 > colors )
|
||||||
|
{
|
||||||
|
var c1 = ( ushort )( *ptr | ( ptr[ 1 ] << 8 ) );
|
||||||
|
var c2 = ( ushort )( ptr[ 2 ] | ( ptr[ 3 ] << 8 ) );
|
||||||
|
colors[ 0 ] = Get565Color( c1 );
|
||||||
|
colors[ 1 ] = Get565Color( c2 );
|
||||||
|
( colors[ 2 ], colors[ 3 ] ) = GetDxt1CombinedColors( c1 > c2, colors[ 0 ], colors[ 1 ] );
|
||||||
|
return ptr + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeDxt1( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 4, 8 );
|
||||||
|
|
||||||
|
var ret = new byte[data.Length * 8];
|
||||||
|
Span< Rgba32 > colors = stackalloc Rgba32[4];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var inputPtr = d;
|
||||||
|
for( var y = 0; y < height; y += 4 )
|
||||||
|
{
|
||||||
|
var outputPtr = r + y * width * 4;
|
||||||
|
for( var x = 0; x < width; x += 4 )
|
||||||
|
{
|
||||||
|
inputPtr = GetDxt1Colors( inputPtr, colors );
|
||||||
|
for( var j = 0; j < 4; ++j )
|
||||||
|
{
|
||||||
|
var outputPtr2 = outputPtr + 4 * ( x + j * width );
|
||||||
|
var colorMask = *inputPtr++;
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, colors[ colorMask & 0b11 ] );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ] );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ] );
|
||||||
|
CopyBytes( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeDxt3( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 4, 16 );
|
||||||
|
var ret = new byte[data.Length * 4];
|
||||||
|
Span< Rgba32 > colors = stackalloc Rgba32[4];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var inputPtr = d;
|
||||||
|
for( var y = 0; y < height; y += 4 )
|
||||||
|
{
|
||||||
|
var outputPtr = r + y * width * 4;
|
||||||
|
for( var x = 0; x < width; x += 4 )
|
||||||
|
{
|
||||||
|
var alphaPtr = inputPtr;
|
||||||
|
inputPtr = GetDxt1Colors( inputPtr + 8, colors );
|
||||||
|
for( var j = 0; j < 4; ++j )
|
||||||
|
{
|
||||||
|
var outputPtr2 = outputPtr + 4 * ( x + j * width );
|
||||||
|
var colorMask = *inputPtr++;
|
||||||
|
outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ colorMask & 0b11 ], *alphaPtr );
|
||||||
|
outputPtr2 = CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ], *alphaPtr++ );
|
||||||
|
outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ], *alphaPtr );
|
||||||
|
CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ], *alphaPtr++ );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe byte* Dxt5AlphaTable( byte* ptr, Span< byte > alphaValues )
|
||||||
|
{
|
||||||
|
var alphaLookup = stackalloc byte[8];
|
||||||
|
alphaLookup[ 0 ] = *ptr++;
|
||||||
|
alphaLookup[ 1 ] = *ptr++;
|
||||||
|
if( alphaLookup[ 0 ] > alphaLookup[ 1 ] )
|
||||||
|
{
|
||||||
|
alphaLookup[ 2 ] = ( byte )( ( 6 * alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 7 );
|
||||||
|
alphaLookup[ 3 ] = ( byte )( ( 5 * alphaLookup[ 0 ] + 2 * alphaLookup[ 1 ] ) / 7 );
|
||||||
|
alphaLookup[ 4 ] = ( byte )( ( 4 * alphaLookup[ 0 ] + 3 * alphaLookup[ 1 ] ) / 7 );
|
||||||
|
alphaLookup[ 5 ] = ( byte )( ( 3 * alphaLookup[ 0 ] + 4 * alphaLookup[ 1 ] ) / 7 );
|
||||||
|
alphaLookup[ 6 ] = ( byte )( ( 2 * alphaLookup[ 0 ] + 5 * alphaLookup[ 1 ] ) / 7 );
|
||||||
|
alphaLookup[ 7 ] = ( byte )( ( alphaLookup[ 0 ] + 6 * alphaLookup[ 1 ] ) / 7 );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alphaLookup[ 2 ] = ( byte )( ( 4 * alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 5 );
|
||||||
|
alphaLookup[ 3 ] = ( byte )( ( 3 * alphaLookup[ 0 ] + 3 * alphaLookup[ 1 ] ) / 5 );
|
||||||
|
alphaLookup[ 4 ] = ( byte )( ( 2 * alphaLookup[ 0 ] + 2 * alphaLookup[ 1 ] ) / 5 );
|
||||||
|
alphaLookup[ 5 ] = ( byte )( ( alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 5 );
|
||||||
|
alphaLookup[ 6 ] = byte.MinValue;
|
||||||
|
alphaLookup[ 7 ] = byte.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alphaLong = ( ulong )*ptr++;
|
||||||
|
alphaLong |= ( ulong )*ptr++ << 8;
|
||||||
|
alphaLong |= ( ulong )*ptr++ << 16;
|
||||||
|
alphaLong |= ( ulong )*ptr++ << 24;
|
||||||
|
alphaLong |= ( ulong )*ptr++ << 32;
|
||||||
|
alphaLong |= ( ulong )*ptr++ << 40;
|
||||||
|
|
||||||
|
for( var i = 0; i < 16; ++i )
|
||||||
|
{
|
||||||
|
alphaValues[ i ] = alphaLookup[ ( alphaLong >> ( i * 3 ) ) & 0x07 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeDxt5( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 4, 16 );
|
||||||
|
var ret = new byte[data.Length * 4];
|
||||||
|
Span< Rgba32 > colors = stackalloc Rgba32[4];
|
||||||
|
Span< byte > alphaValues = stackalloc byte[16];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data, a = alphaValues )
|
||||||
|
{
|
||||||
|
var inputPtr = d;
|
||||||
|
for( var y = 0; y < height; y += 4 )
|
||||||
|
{
|
||||||
|
var outputPtr = r + y * width * 4;
|
||||||
|
for( var x = 0; x < width; x += 4 )
|
||||||
|
{
|
||||||
|
inputPtr = Dxt5AlphaTable( inputPtr, alphaValues );
|
||||||
|
inputPtr = GetDxt1Colors( inputPtr, colors );
|
||||||
|
var alphaPtr = a;
|
||||||
|
for( var j = 0; j < 4; ++j )
|
||||||
|
{
|
||||||
|
var outputPtr2 = outputPtr + 4 * ( x + j * width );
|
||||||
|
var colorMask = *inputPtr++;
|
||||||
|
outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ colorMask & 0b11 ], *alphaPtr++ );
|
||||||
|
outputPtr2 = CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ], *alphaPtr++ );
|
||||||
|
outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ], *alphaPtr++ );
|
||||||
|
CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ], *alphaPtr++ );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeBc4( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 4, 8 );
|
||||||
|
var ret = new byte[data.Length * 8];
|
||||||
|
Span< byte > channelValues = stackalloc byte[16];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data, a = channelValues )
|
||||||
|
{
|
||||||
|
var inputPtr = d;
|
||||||
|
for( var y = 0; y < height; y += 4 )
|
||||||
|
{
|
||||||
|
var outputPtr = r + y * width * 4;
|
||||||
|
for( var x = 0; x < width; x += 4 )
|
||||||
|
{
|
||||||
|
inputPtr = Dxt5AlphaTable( inputPtr, channelValues );
|
||||||
|
var channelPtr = a;
|
||||||
|
for( var j = 0; j < 4; ++j )
|
||||||
|
{
|
||||||
|
var outputPtr2 = outputPtr + 4 * ( x + j * width );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) );
|
||||||
|
CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeBc5( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 4, 16 );
|
||||||
|
var ret = new byte[data.Length * 4];
|
||||||
|
Span< byte > channel1 = stackalloc byte[16];
|
||||||
|
Span< byte > channel2 = stackalloc byte[16];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data, a = channel1, b = channel2 )
|
||||||
|
{
|
||||||
|
var inputPtr = d;
|
||||||
|
for( var y = 0; y < height; y += 4 )
|
||||||
|
{
|
||||||
|
var outputPtr = r + y * width * 4;
|
||||||
|
for( var x = 0; x < width; x += 4 )
|
||||||
|
{
|
||||||
|
inputPtr = Dxt5AlphaTable( inputPtr, channel1 );
|
||||||
|
inputPtr = Dxt5AlphaTable( inputPtr, channel2 );
|
||||||
|
var channel1Ptr = a;
|
||||||
|
var channel2Ptr = b;
|
||||||
|
for( var j = 0; j < 4; ++j )
|
||||||
|
{
|
||||||
|
var outputPtr2 = outputPtr + 4 * ( x + j * width );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) );
|
||||||
|
outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) );
|
||||||
|
CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedGreyscale( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 1 );
|
||||||
|
var ret = new byte[data.Length * 4];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
var end = d + data.Length;
|
||||||
|
var input = d;
|
||||||
|
while( input != end )
|
||||||
|
{
|
||||||
|
*ptr++ = *input;
|
||||||
|
*ptr++ = *input;
|
||||||
|
*ptr++ = *input++;
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedR4G4B4A4( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
var input = ( ushort* )d;
|
||||||
|
foreach( var b in data )
|
||||||
|
{
|
||||||
|
*ptr++ = ( byte )( ( b << 4 ) | ( b & 0x0F ) );
|
||||||
|
*ptr++ = ( byte )( ( b >> 4 ) | ( b & 0xF0 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedB4G4R4A4( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
foreach( var b in new Span< ushort >( d, data.Length / 2 ) )
|
||||||
|
{
|
||||||
|
*ptr++ = ( byte )( ( ( b >> 8 ) & 0x0F ) | ( ( b >> 4 ) & 0xF0 ) );
|
||||||
|
*ptr++ = ( byte )( ( b & 0xF0 ) | ( ( b >> 4 ) & 0x0F ) );
|
||||||
|
|
||||||
|
*ptr++ = ( byte )( ( b & 0x0F ) | ( b << 4 ) );
|
||||||
|
*ptr++ = ( byte )( ( ( b >> 8 ) & 0xF0 ) | ( b >> 12 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedR5G5B5( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
foreach( var b in new Span< ushort >( d, data.Length / 2 ) )
|
||||||
|
{
|
||||||
|
*ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) );
|
||||||
|
var tmp = b & 0x03E0;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) );
|
||||||
|
tmp = b & 0x7C00;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) );
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedB5G5R5( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
foreach( var b in new Span< ushort >( d, data.Length / 2 ) )
|
||||||
|
{
|
||||||
|
var tmp = b & 0x7C00;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) );
|
||||||
|
tmp = b & 0x03E0;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) );
|
||||||
|
*ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) );
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedR5G6B5( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
foreach( var b in new Span< ushort >( d, data.Length / 2 ) )
|
||||||
|
{
|
||||||
|
*ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) );
|
||||||
|
var tmp = b & 0x07E0;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 3 ) | ( tmp >> 9 ) );
|
||||||
|
tmp = b & 0xF800;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 14 ) | ( tmp >> 9 ) );
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedB5G6R5( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
foreach( var b in new Span< ushort >( d, data.Length / 2 ) )
|
||||||
|
{
|
||||||
|
var tmp = b & 0xF800;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 14 ) | ( tmp >> 9 ) );
|
||||||
|
tmp = b & 0x07E0;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 3 ) | ( tmp >> 9 ) );
|
||||||
|
*ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) );
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedR5G5B5A1( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
foreach( var b in new Span< ushort >( d, data.Length / 2 ) )
|
||||||
|
{
|
||||||
|
*ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) );
|
||||||
|
var tmp = b & 0x03E0;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) );
|
||||||
|
tmp = b & 0x7C00;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) );
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
*ptr++ = ( byte )( b > 0x7FFF ? 0xFF : 0x00 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedB5G5R5A1( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 2 );
|
||||||
|
var ret = new byte[data.Length * 2];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
foreach( var b in new Span< ushort >( d, data.Length / 2 ) )
|
||||||
|
{
|
||||||
|
var tmp = b & 0x7C00;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) );
|
||||||
|
tmp = b & 0x03E0;
|
||||||
|
*ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) );
|
||||||
|
*ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) );
|
||||||
|
*ptr++ = ( byte )( b > 0x7FFF ? 0xFF : 0x00 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedR8G8B8( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 3 );
|
||||||
|
var ret = new byte[data.Length * 4 / 3];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
var end = d + data.Length;
|
||||||
|
var input = d;
|
||||||
|
while( input != end )
|
||||||
|
{
|
||||||
|
*ptr++ = *input++;
|
||||||
|
*ptr++ = *input++;
|
||||||
|
*ptr++ = *input++;
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedB8G8R8( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 3 );
|
||||||
|
var ret = new byte[data.Length * 4 / 3];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
var end = d + data.Length;
|
||||||
|
var input = d;
|
||||||
|
while( input != end )
|
||||||
|
{
|
||||||
|
var b = *input++;
|
||||||
|
var g = *input++;
|
||||||
|
*ptr++ = *input++;
|
||||||
|
*ptr++ = g;
|
||||||
|
*ptr++ = b;
|
||||||
|
*ptr++ = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] DecodeUncompressedR8G8B8A8( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 4 );
|
||||||
|
var ret = new byte[data.Length];
|
||||||
|
data.CopyTo( ret );
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] DecodeUncompressedB8G8R8A8( ReadOnlySpan< byte > data, int height, int width )
|
||||||
|
{
|
||||||
|
Verify( data, height, width, 1, 4 );
|
||||||
|
var ret = new byte[data.Length];
|
||||||
|
|
||||||
|
fixed( byte* r = ret, d = data )
|
||||||
|
{
|
||||||
|
var ptr = r;
|
||||||
|
var end = d + data.Length;
|
||||||
|
var input = d;
|
||||||
|
while( input != end )
|
||||||
|
{
|
||||||
|
var b = *input++;
|
||||||
|
var g = *input++;
|
||||||
|
*ptr++ = *input++;
|
||||||
|
*ptr++ = g;
|
||||||
|
*ptr++ = b;
|
||||||
|
*ptr++ = *input++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
134
Penumbra/Import/Dds/PixelFormat.cs
Normal file
134
Penumbra/Import/Dds/PixelFormat.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Dds;
|
||||||
|
|
||||||
|
public enum ParseType
|
||||||
|
{
|
||||||
|
Unsupported,
|
||||||
|
DXT1,
|
||||||
|
DXT3,
|
||||||
|
DXT5,
|
||||||
|
BC4,
|
||||||
|
BC5,
|
||||||
|
|
||||||
|
Greyscale,
|
||||||
|
R4G4B4A4,
|
||||||
|
B4G4R4A4,
|
||||||
|
R5G5B5,
|
||||||
|
B5G5R5,
|
||||||
|
R5G6B5,
|
||||||
|
B5G6R5,
|
||||||
|
R5G5B5A1,
|
||||||
|
B5G5R5A1,
|
||||||
|
R8G8B8,
|
||||||
|
B8G8R8,
|
||||||
|
R8G8B8A8,
|
||||||
|
B8G8R8A8,
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout( LayoutKind.Sequential )]
|
||||||
|
public struct PixelFormat
|
||||||
|
{
|
||||||
|
public int Size;
|
||||||
|
public FormatFlags Flags;
|
||||||
|
public FourCCType FourCC;
|
||||||
|
public int RgbBitCount;
|
||||||
|
public uint RBitMask;
|
||||||
|
public uint GBitMask;
|
||||||
|
public uint BBitMask;
|
||||||
|
public uint ABitMask;
|
||||||
|
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum FormatFlags : uint
|
||||||
|
{
|
||||||
|
AlphaPixels = 0x000001,
|
||||||
|
Alpha = 0x000002,
|
||||||
|
FourCC = 0x000004,
|
||||||
|
RGB = 0x000040,
|
||||||
|
YUV = 0x000200,
|
||||||
|
Luminance = 0x020000,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FourCCType : uint
|
||||||
|
{
|
||||||
|
NoCompression = 0,
|
||||||
|
DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ),
|
||||||
|
DXT2 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '2' << 24 ),
|
||||||
|
DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ),
|
||||||
|
DXT4 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '4' << 24 ),
|
||||||
|
DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ),
|
||||||
|
DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ),
|
||||||
|
ATI1 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '1' << 24 ),
|
||||||
|
BC4U = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( 'U' << 24 ),
|
||||||
|
BC45 = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( '5' << 24 ),
|
||||||
|
ATI2 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '2' << 24 ),
|
||||||
|
BC5U = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( 'U' << 24 ),
|
||||||
|
BC55 = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( '5' << 24 ),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void Write( BinaryWriter bw )
|
||||||
|
{
|
||||||
|
bw.Write( Size );
|
||||||
|
bw.Write( ( uint )Flags );
|
||||||
|
bw.Write( ( uint )FourCC );
|
||||||
|
bw.Write( RgbBitCount );
|
||||||
|
bw.Write( RBitMask );
|
||||||
|
bw.Write( GBitMask );
|
||||||
|
bw.Write( BBitMask );
|
||||||
|
bw.Write( ABitMask );
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParseType ToParseType( DXT10Header? dxt10 )
|
||||||
|
{
|
||||||
|
return FourCC switch
|
||||||
|
{
|
||||||
|
FourCCType.NoCompression => HandleUncompressed(),
|
||||||
|
FourCCType.DXT1 => ParseType.DXT1,
|
||||||
|
FourCCType.DXT2 => ParseType.Unsupported,
|
||||||
|
FourCCType.DXT3 => ParseType.DXT3,
|
||||||
|
FourCCType.DXT4 => ParseType.Unsupported,
|
||||||
|
FourCCType.DXT5 => ParseType.DXT5,
|
||||||
|
FourCCType.DX10 => dxt10?.ToParseType() ?? ParseType.Unsupported,
|
||||||
|
FourCCType.ATI1 => ParseType.BC4,
|
||||||
|
FourCCType.BC4U => ParseType.BC4,
|
||||||
|
FourCCType.BC45 => ParseType.BC4,
|
||||||
|
FourCCType.ATI2 => ParseType.BC5,
|
||||||
|
FourCCType.BC5U => ParseType.BC5,
|
||||||
|
FourCCType.BC55 => ParseType.BC5,
|
||||||
|
_ => ParseType.Unsupported,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParseType HandleUncompressed()
|
||||||
|
{
|
||||||
|
switch( RgbBitCount )
|
||||||
|
{
|
||||||
|
case 8: return ParseType.Greyscale;
|
||||||
|
case 16:
|
||||||
|
if( ABitMask == 0xF000 )
|
||||||
|
{
|
||||||
|
return RBitMask > GBitMask ? ParseType.B4G4R4A4 : ParseType.R4G4B4A4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( Flags.HasFlag( FormatFlags.AlphaPixels ) )
|
||||||
|
{
|
||||||
|
return RBitMask > GBitMask ? ParseType.B5G5R5A1 : ParseType.R5G5B5A1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( GBitMask == 0x07E0 )
|
||||||
|
{
|
||||||
|
return RBitMask > GBitMask ? ParseType.B5G6R5 : ParseType.R5G6B5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RBitMask > GBitMask ? ParseType.B5G5R5 : ParseType.R5G5B5;
|
||||||
|
case 24: return RBitMask > GBitMask ? ParseType.B8G8R8 : ParseType.R8G8B8;
|
||||||
|
case 32: return RBitMask > GBitMask ? ParseType.B8G8R8A8 : ParseType.R8G8B8A8;
|
||||||
|
default: return ParseType.Unsupported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
Penumbra/Import/Dds/TextureImporter.cs
Normal file
103
Penumbra/Import/Dds/TextureImporter.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Lumina.Data.Files;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
|
namespace Penumbra.Import.Dds;
|
||||||
|
|
||||||
|
public class TextureImporter
|
||||||
|
{
|
||||||
|
private static void WriteHeader( byte[] target, int width, int height )
|
||||||
|
{
|
||||||
|
using var mem = new MemoryStream( target );
|
||||||
|
using var bw = new BinaryWriter( mem );
|
||||||
|
bw.Write( ( uint )TexFile.Attribute.TextureType2D );
|
||||||
|
bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 );
|
||||||
|
bw.Write( ( ushort )width );
|
||||||
|
bw.Write( ( ushort )height );
|
||||||
|
bw.Write( ( ushort )1 );
|
||||||
|
bw.Write( ( ushort )1 );
|
||||||
|
bw.Write( 0 );
|
||||||
|
bw.Write( 1 );
|
||||||
|
bw.Write( 2 );
|
||||||
|
bw.Write( 80 );
|
||||||
|
for( var i = 1; i < 13; ++i )
|
||||||
|
{
|
||||||
|
bw.Write( 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe bool RgbaBytesToDds( byte[] rgba, int width, int height, out byte[] ddsData )
|
||||||
|
{
|
||||||
|
var header = new DdsHeader()
|
||||||
|
{
|
||||||
|
Caps1 = DdsHeader.DdsCaps1.Complex | DdsHeader.DdsCaps1.Texture | DdsHeader.DdsCaps1.MipMap,
|
||||||
|
Depth = 1,
|
||||||
|
Flags = DdsHeader.DdsFlags.Required | DdsHeader.DdsFlags.Pitch | DdsHeader.DdsFlags.MipMapCount,
|
||||||
|
Height = height,
|
||||||
|
Width = width,
|
||||||
|
PixelFormat = new PixelFormat()
|
||||||
|
{
|
||||||
|
Flags = PixelFormat.FormatFlags.AlphaPixels | PixelFormat.FormatFlags.RGB,
|
||||||
|
FourCC = 0,
|
||||||
|
BBitMask = 0x000000FF,
|
||||||
|
GBitMask = 0x0000FF00,
|
||||||
|
RBitMask = 0x00FF0000,
|
||||||
|
ABitMask = 0xFF000000,
|
||||||
|
Size = 32,
|
||||||
|
RgbBitCount = 32,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ddsData = new byte[4 + DdsHeader.Size + rgba.Length];
|
||||||
|
header.Write( ddsData, 0 );
|
||||||
|
rgba.CopyTo( ddsData, DdsHeader.Size + 4 );
|
||||||
|
for( var i = 0; i < rgba.Length; i += 4 )
|
||||||
|
{
|
||||||
|
( ddsData[ DdsHeader.Size + i ], ddsData[ DdsHeader.Size + i + 2 ] )
|
||||||
|
= ( ddsData[ DdsHeader.Size + i + 2 ], ddsData[ DdsHeader.Size + i ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData )
|
||||||
|
{
|
||||||
|
texData = Array.Empty< byte >();
|
||||||
|
if( rgba.Length != width * height * 4 )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
texData = new byte[80 + width * height * 4];
|
||||||
|
WriteHeader( texData, width, height );
|
||||||
|
// RGBA to BGRA.
|
||||||
|
for( var i = 0; i < rgba.Length; i += 4 )
|
||||||
|
{
|
||||||
|
texData[ 80 + i + 0 ] = rgba[ i + 2 ];
|
||||||
|
texData[ 80 + i + 1 ] = rgba[ i + 1 ];
|
||||||
|
texData[ 80 + i + 2 ] = rgba[ i + 0 ];
|
||||||
|
texData[ 80 + i + 3 ] = rgba[ i + 3 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool PngToTex( string inputFile, out byte[] texData )
|
||||||
|
{
|
||||||
|
using var file = File.OpenRead( inputFile );
|
||||||
|
var image = Image.Load< Bgra32 >( file );
|
||||||
|
|
||||||
|
var buffer = new byte[80 + image.Height * image.Width * 4];
|
||||||
|
WriteHeader( buffer, image.Width, image.Height );
|
||||||
|
|
||||||
|
var span = new Span< byte >( buffer, 80, buffer.Length - 80 );
|
||||||
|
image.CopyPixelDataTo( span );
|
||||||
|
|
||||||
|
texData = buffer;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Import( string inputFile )
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
@ -5,5 +5,6 @@ public enum ImporterState
|
||||||
None,
|
None,
|
||||||
WritingPackToDisk,
|
WritingPackToDisk,
|
||||||
ExtractingModFiles,
|
ExtractingModFiles,
|
||||||
|
DeduplicatingFiles,
|
||||||
Done,
|
Done,
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using ICSharpCode.SharpZipLib.Zip;
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Penumbra.Util;
|
using Penumbra.Mods;
|
||||||
using FileMode = System.IO.FileMode;
|
using FileMode = System.IO.FileMode;
|
||||||
|
|
||||||
namespace Penumbra.Import;
|
namespace Penumbra.Import;
|
||||||
|
|
@ -95,6 +94,11 @@ public partial class TexToolsImporter : IDisposable
|
||||||
{
|
{
|
||||||
var directory = VerifyVersionAndImport( file );
|
var directory = VerifyVersionAndImport( file );
|
||||||
ExtractedMods.Add( ( file, directory, null ) );
|
ExtractedMods.Add( ( file, directory, null ) );
|
||||||
|
if( Penumbra.Config.AutoDeduplicateOnImport )
|
||||||
|
{
|
||||||
|
State = ImporterState.DeduplicatingFiles;
|
||||||
|
Mod.Editor.DeduplicateMod( directory );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
catch( Exception e )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,14 @@ public partial class TexToolsImporter
|
||||||
var percentage = _modPackCount / ( float )_currentModPackIdx;
|
var percentage = _modPackCount / ( float )_currentModPackIdx;
|
||||||
ImGui.ProgressBar( percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}" );
|
ImGui.ProgressBar( percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}" );
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
ImGui.TextUnformatted( $"Extracting {_currentModName}..." );
|
if( State == ImporterState.DeduplicatingFiles )
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted( $"Deduplicating {_currentModName}..." );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted( $"Extracting {_currentModName}..." );
|
||||||
|
}
|
||||||
|
|
||||||
if( _currentNumOptions > 1 )
|
if( _currentNumOptions > 1 )
|
||||||
{
|
{
|
||||||
|
|
@ -47,8 +54,11 @@ public partial class TexToolsImporter
|
||||||
percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / ( float )_currentNumOptions;
|
percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / ( float )_currentNumOptions;
|
||||||
ImGui.ProgressBar( percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}" );
|
ImGui.ProgressBar( percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}" );
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
ImGui.TextUnformatted(
|
if( State != ImporterState.DeduplicatingFiles )
|
||||||
$"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." );
|
{
|
||||||
|
ImGui.TextUnformatted(
|
||||||
|
$"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
|
|
@ -56,7 +66,10 @@ public partial class TexToolsImporter
|
||||||
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / ( float )_currentNumFiles;
|
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / ( float )_currentNumFiles;
|
||||||
ImGui.ProgressBar( percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}" );
|
ImGui.ProgressBar( percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}" );
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
ImGui.TextUnformatted( $"Extracting file {_currentFileName}..." );
|
if( State != ImporterState.DeduplicatingFiles )
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted( $"Extracting file {_currentFileName}..." );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ public unsafe class CharacterUtility : IDisposable
|
||||||
.Select( i => Array.IndexOf( RelevantIndices, i ) ).ToArray();
|
.Select( i => Array.IndexOf( RelevantIndices, i ) ).ToArray();
|
||||||
|
|
||||||
|
|
||||||
public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[RelevantIndices.Length];
|
public readonly (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[RelevantIndices.Length];
|
||||||
|
|
||||||
public (IntPtr Address, int Size) DefaultResource( int fullIdx )
|
public (IntPtr Address, int Size) DefaultResource( int fullIdx )
|
||||||
=> DefaultResources[ ReverseIndices[ fullIdx ] ];
|
=> DefaultResources[ ReverseIndices[ fullIdx ] ];
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,100 @@ using Penumbra.Interop.Structs;
|
||||||
|
|
||||||
namespace Penumbra.Interop;
|
namespace Penumbra.Interop;
|
||||||
|
|
||||||
public sealed unsafe class ObjectReloader : IDisposable
|
public unsafe partial class ObjectReloader
|
||||||
{
|
{
|
||||||
public const int GPosePlayerIdx = 201;
|
public const int GPosePlayerIdx = 201;
|
||||||
public const int GPoseSlots = 42;
|
public const int GPoseSlots = 42;
|
||||||
public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots;
|
public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots;
|
||||||
|
|
||||||
|
private readonly string?[] _gPoseNames = new string?[GPoseSlots];
|
||||||
|
private int _gPoseNameCounter = 0;
|
||||||
|
private bool _inGPose = false;
|
||||||
|
|
||||||
|
// VFuncs that disable and enable draw, used only for GPose actors.
|
||||||
|
private static void DisableDraw( GameObject actor )
|
||||||
|
=> ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]( actor.Address );
|
||||||
|
|
||||||
|
private static void EnableDraw( GameObject actor )
|
||||||
|
=> ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address );
|
||||||
|
|
||||||
|
|
||||||
|
// Check whether we currently are in GPose.
|
||||||
|
// Also clear the name list.
|
||||||
|
private void SetGPose()
|
||||||
|
{
|
||||||
|
_inGPose = Dalamud.Objects[ GPosePlayerIdx ] != null;
|
||||||
|
_gPoseNameCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGPoseActor( int idx )
|
||||||
|
=> idx is >= GPosePlayerIdx and < GPoseEndIdx;
|
||||||
|
|
||||||
|
// Return whether an object has to be replaced by a GPose object.
|
||||||
|
// If the object does not exist, is already a GPose actor
|
||||||
|
// or no actor of the same name is found in the GPose actor list,
|
||||||
|
// obj will be the object itself (or null) and false will be returned.
|
||||||
|
// If we are in GPose and a game object with the same name as the original actor is found,
|
||||||
|
// this will be in obj and true will be returned.
|
||||||
|
private bool FindCorrectActor( int idx, out GameObject? obj )
|
||||||
|
{
|
||||||
|
obj = Dalamud.Objects[ idx ];
|
||||||
|
if( !_inGPose || obj == null || IsGPoseActor( idx ) )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = obj.Name.ToString();
|
||||||
|
for( var i = 0; i < _gPoseNameCounter; ++i )
|
||||||
|
{
|
||||||
|
var gPoseName = _gPoseNames[ i ];
|
||||||
|
if( gPoseName == null )
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( name == gPoseName )
|
||||||
|
{
|
||||||
|
obj = Dalamud.Objects[ GPosePlayerIdx + i ];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for( ; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter )
|
||||||
|
{
|
||||||
|
var gPoseName = Dalamud.Objects[ GPosePlayerIdx + _gPoseNameCounter ]?.Name.ToString();
|
||||||
|
_gPoseNames[ _gPoseNameCounter ] = gPoseName;
|
||||||
|
if( gPoseName == null )
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( name == gPoseName )
|
||||||
|
{
|
||||||
|
obj = Dalamud.Objects[ GPosePlayerIdx + _gPoseNameCounter ];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not ever redraw any of the five UI Window actors.
|
||||||
|
private static bool BadRedrawIndices( GameObject? actor, out int tableIndex )
|
||||||
|
{
|
||||||
|
if( actor == null )
|
||||||
|
{
|
||||||
|
tableIndex = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableIndex = ObjectTableIndex( actor );
|
||||||
|
return tableIndex is >= 240 and < 245;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed unsafe partial class ObjectReloader : IDisposable
|
||||||
|
{
|
||||||
private readonly List< int > _queue = new(100);
|
private readonly List< int > _queue = new(100);
|
||||||
private readonly List< int > _afterGPoseQueue = new(GPoseSlots);
|
private readonly List< int > _afterGPoseQueue = new(GPoseSlots);
|
||||||
private int _target = -1;
|
private int _target = -1;
|
||||||
|
|
@ -27,27 +115,9 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
public static DrawState* ActorDrawState( GameObject actor )
|
public static DrawState* ActorDrawState( GameObject actor )
|
||||||
=> ( DrawState* )( actor.Address + 0x0104 );
|
=> ( DrawState* )( actor.Address + 0x0104 );
|
||||||
|
|
||||||
private static void DisableDraw( GameObject actor )
|
|
||||||
=> ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]( actor.Address );
|
|
||||||
|
|
||||||
private static void EnableDraw( GameObject actor )
|
|
||||||
=> ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address );
|
|
||||||
|
|
||||||
private static int ObjectTableIndex( GameObject actor )
|
private static int ObjectTableIndex( GameObject actor )
|
||||||
=> ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex;
|
=> ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex;
|
||||||
|
|
||||||
private static bool BadRedrawIndices( GameObject? actor, out int tableIndex )
|
|
||||||
{
|
|
||||||
if( actor == null )
|
|
||||||
{
|
|
||||||
tableIndex = -1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
tableIndex = ObjectTableIndex( actor );
|
|
||||||
return tableIndex is >= 240 and < 245;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteInvisible( GameObject? actor )
|
private static void WriteInvisible( GameObject? actor )
|
||||||
{
|
{
|
||||||
if( BadRedrawIndices( actor, out var tableIndex ) )
|
if( BadRedrawIndices( actor, out var tableIndex ) )
|
||||||
|
|
@ -57,7 +127,7 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
|
|
||||||
*ActorDrawState( actor! ) |= DrawState.Invisibility;
|
*ActorDrawState( actor! ) |= DrawState.Invisibility;
|
||||||
|
|
||||||
if( tableIndex is >= GPosePlayerIdx and < GPoseEndIdx )
|
if( IsGPoseActor( tableIndex ) )
|
||||||
{
|
{
|
||||||
DisableDraw( actor! );
|
DisableDraw( actor! );
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +142,7 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
|
|
||||||
*ActorDrawState( actor! ) &= ~DrawState.Invisibility;
|
*ActorDrawState( actor! ) &= ~DrawState.Invisibility;
|
||||||
|
|
||||||
if( tableIndex is >= GPosePlayerIdx and < GPoseEndIdx )
|
if( IsGPoseActor( tableIndex ) )
|
||||||
{
|
{
|
||||||
EnableDraw( actor! );
|
EnableDraw( actor! );
|
||||||
}
|
}
|
||||||
|
|
@ -136,15 +206,22 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
for( var i = 0; i < _queue.Count; ++i )
|
for( var i = 0; i < _queue.Count; ++i )
|
||||||
{
|
{
|
||||||
var idx = _queue[ i ];
|
var idx = _queue[ i ];
|
||||||
if( idx < 0 )
|
if( FindCorrectActor( idx < 0 ? ~idx : idx, out var obj ) )
|
||||||
{
|
{
|
||||||
var newIdx = ~idx;
|
_afterGPoseQueue.Add( idx < 0 ? idx : ~idx );
|
||||||
WriteInvisible( Dalamud.Objects[ newIdx ] );
|
|
||||||
_queue[ numKept++ ] = newIdx;
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if( obj != null )
|
||||||
{
|
{
|
||||||
WriteVisible( Dalamud.Objects[ idx ] );
|
if( idx < 0 )
|
||||||
|
{
|
||||||
|
WriteInvisible( obj );
|
||||||
|
_queue[ numKept++ ] = ObjectTableIndex( obj );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteVisible( obj );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,7 +230,7 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
|
|
||||||
private void HandleAfterGPose()
|
private void HandleAfterGPose()
|
||||||
{
|
{
|
||||||
if( _afterGPoseQueue.Count == 0 || Dalamud.Objects[ GPosePlayerIdx ] != null )
|
if( _afterGPoseQueue.Count == 0 || _inGPose )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +251,7 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_afterGPoseQueue.RemoveRange( numKept, _queue.Count - numKept );
|
_afterGPoseQueue.RemoveRange( numKept, _afterGPoseQueue.Count - numKept );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUpdateEvent( object framework )
|
private void OnUpdateEvent( object framework )
|
||||||
|
|
@ -186,6 +263,7 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetGPose();
|
||||||
HandleRedraw();
|
HandleRedraw();
|
||||||
HandleAfterGPose();
|
HandleAfterGPose();
|
||||||
HandleTarget();
|
HandleTarget();
|
||||||
|
|
@ -229,6 +307,14 @@ public sealed unsafe class ObjectReloader : IDisposable
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RedrawObject( int tableIndex, RedrawType settings )
|
||||||
|
{
|
||||||
|
if( tableIndex >= 0 && tableIndex < Dalamud.Objects.Length )
|
||||||
|
{
|
||||||
|
RedrawObject( Dalamud.Objects[ tableIndex ], settings );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void RedrawObject( string name, RedrawType settings )
|
public void RedrawObject( string name, RedrawType settings )
|
||||||
{
|
{
|
||||||
var lowerName = name.ToLowerInvariant();
|
var lowerName = name.ToLowerInvariant();
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,18 @@ public unsafe partial class PathResolver
|
||||||
CharacterBaseLoadAnimationHook!.Original( drawObject );
|
CharacterBaseLoadAnimationHook!.Original( drawObject );
|
||||||
_animationLoadCollection = last;
|
_animationLoadCollection = last;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2 );
|
||||||
|
|
||||||
|
[Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )]
|
||||||
|
public Hook< LoadSomeAvfx >? LoadSomeAvfxHook;
|
||||||
|
|
||||||
|
private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2 )
|
||||||
|
{
|
||||||
|
var last = _animationLoadCollection;
|
||||||
|
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
|
||||||
|
var ret = LoadSomeAvfxHook!.Original( a1, gameObject, gameObject2 );
|
||||||
|
_animationLoadCollection = last;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +92,7 @@ public unsafe partial class PathResolver
|
||||||
Penumbra.CollectionManager.CollectionChanged += CheckCollections;
|
Penumbra.CollectionManager.CollectionChanged += CheckCollections;
|
||||||
LoadTimelineResourcesHook?.Enable();
|
LoadTimelineResourcesHook?.Enable();
|
||||||
CharacterBaseLoadAnimationHook?.Enable();
|
CharacterBaseLoadAnimationHook?.Enable();
|
||||||
|
LoadSomeAvfxHook?.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisableDataHooks()
|
private void DisableDataHooks()
|
||||||
|
|
@ -103,6 +104,7 @@ public unsafe partial class PathResolver
|
||||||
CharacterBaseDestructorHook?.Disable();
|
CharacterBaseDestructorHook?.Disable();
|
||||||
LoadTimelineResourcesHook?.Disable();
|
LoadTimelineResourcesHook?.Disable();
|
||||||
CharacterBaseLoadAnimationHook?.Disable();
|
CharacterBaseLoadAnimationHook?.Disable();
|
||||||
|
LoadSomeAvfxHook?.Disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeDataHooks()
|
private void DisposeDataHooks()
|
||||||
|
|
@ -113,6 +115,7 @@ public unsafe partial class PathResolver
|
||||||
CharacterBaseDestructorHook?.Dispose();
|
CharacterBaseDestructorHook?.Dispose();
|
||||||
LoadTimelineResourcesHook?.Dispose();
|
LoadTimelineResourcesHook?.Dispose();
|
||||||
CharacterBaseLoadAnimationHook?.Dispose();
|
CharacterBaseLoadAnimationHook?.Dispose();
|
||||||
|
LoadSomeAvfxHook?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections.
|
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
|
|
@ -55,7 +56,7 @@ public unsafe partial class PathResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check specifically for shpk and tex files whether we are currently in a material load.
|
// Check specifically for shpk and tex files whether we are currently in a material load.
|
||||||
private bool HandleMaterialSubFiles( ResourceType type, out ModCollection? collection )
|
private bool HandleMaterialSubFiles( ResourceType type, [NotNullWhen(true)] out ModCollection? collection )
|
||||||
{
|
{
|
||||||
if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk )
|
if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk )
|
||||||
{
|
{
|
||||||
|
|
@ -82,12 +83,16 @@ public unsafe partial class PathResolver
|
||||||
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString();
|
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString();
|
||||||
if( Penumbra.CollectionManager.ByName( name, out var collection ) )
|
if( Penumbra.CollectionManager.ByName( name, out var collection ) )
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path );
|
PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path );
|
||||||
|
#endif
|
||||||
SetCollection( path, collection );
|
SetCollection( path, collection );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path );
|
PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path );
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force isSync = true for this call. I don't really understand why,
|
// Force isSync = true for this call. I don't really understand why,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
|
|
@ -40,16 +41,15 @@ public partial class PathResolver : IDisposable
|
||||||
// A potential next request will add the path anew.
|
// A potential next request will add the path anew.
|
||||||
var nonDefault = HandleMaterialSubFiles( type, out var collection )
|
var nonDefault = HandleMaterialSubFiles( type, out var collection )
|
||||||
|| PathCollections.TryRemove( gamePath.Path, out collection )
|
|| PathCollections.TryRemove( gamePath.Path, out collection )
|
||||||
//|| HandlePapFile( type, gamePath, out collection )
|
|
||||||
|| HandleAnimationFile( type, gamePath, out collection )
|
|| HandleAnimationFile( type, gamePath, out collection )
|
||||||
|| HandleDecalFile( type, gamePath, out collection );
|
|| HandleDecalFile( type, gamePath, out collection );
|
||||||
if( !nonDefault )
|
if( !nonDefault || collection == null)
|
||||||
{
|
{
|
||||||
collection = Penumbra.CollectionManager.Default;
|
collection = Penumbra.CollectionManager.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve using character/default collection first, otherwise forced, as usual.
|
// Resolve using character/default collection first, otherwise forced, as usual.
|
||||||
var resolved = collection!.ResolvePath( gamePath );
|
var resolved = collection.ResolvePath( gamePath );
|
||||||
|
|
||||||
// Since mtrl files load their files separately, we need to add the new, resolved path
|
// Since mtrl files load their files separately, we need to add the new, resolved path
|
||||||
// so that the functions loading tex and shpk can find that path and use its collection.
|
// so that the functions loading tex and shpk can find that path and use its collection.
|
||||||
|
|
@ -59,7 +59,7 @@ public partial class PathResolver : IDisposable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ModCollection? collection )
|
private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen(true)] out ModCollection? collection )
|
||||||
{
|
{
|
||||||
if( type == ResourceType.Tex
|
if( type == ResourceType.Tex
|
||||||
&& _lastCreatedCollection != null
|
&& _lastCreatedCollection != null
|
||||||
|
|
@ -73,7 +73,7 @@ public partial class PathResolver : IDisposable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, out ModCollection? collection )
|
private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection )
|
||||||
{
|
{
|
||||||
if( _animationLoadCollection != null )
|
if( _animationLoadCollection != null )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public partial class Mod
|
||||||
|
|
||||||
public bool DuplicatesFinished { get; private set; } = true;
|
public bool DuplicatesFinished { get; private set; } = true;
|
||||||
|
|
||||||
public void DeleteDuplicates()
|
public void DeleteDuplicates( bool useModManager = true )
|
||||||
{
|
{
|
||||||
if( !DuplicatesFinished || _duplicates.Count == 0 )
|
if( !DuplicatesFinished || _duplicates.Count == 0 )
|
||||||
{
|
{
|
||||||
|
|
@ -41,15 +41,16 @@ public partial class Mod
|
||||||
var remaining = set[ 0 ];
|
var remaining = set[ 0 ];
|
||||||
foreach( var duplicate in set.Skip( 1 ) )
|
foreach( var duplicate in set.Skip( 1 ) )
|
||||||
{
|
{
|
||||||
HandleDuplicate( duplicate, remaining );
|
HandleDuplicate( duplicate, remaining, useModManager );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_availableFiles.RemoveAll( p => !p.File.Exists );
|
_availableFiles.RemoveAll( p => !p.File.Exists );
|
||||||
_duplicates.Clear();
|
_duplicates.Clear();
|
||||||
|
DeleteEmptyDirectories( _mod.ModPath );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleDuplicate( FullPath duplicate, FullPath remaining )
|
private void HandleDuplicate( FullPath duplicate, FullPath remaining, bool useModManager )
|
||||||
{
|
{
|
||||||
void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx )
|
void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx )
|
||||||
{
|
{
|
||||||
|
|
@ -58,7 +59,23 @@ public partial class Mod
|
||||||
kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) );
|
kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) );
|
||||||
if( changes )
|
if( changes )
|
||||||
{
|
{
|
||||||
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict );
|
if( useModManager )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sub = ( SubMod )subMod;
|
||||||
|
sub.FileData = dict;
|
||||||
|
if( groupIdx == -1 )
|
||||||
|
{
|
||||||
|
_mod.SaveDefaultMod();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IModGroup.Save( _mod.Groups[ groupIdx ], _mod.ModPath, groupIdx );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +111,7 @@ public partial class Mod
|
||||||
{
|
{
|
||||||
DuplicatesFinished = false;
|
DuplicatesFinished = false;
|
||||||
UpdateFiles();
|
UpdateFiles();
|
||||||
var files = _availableFiles.OrderByDescending(f => f.FileSize).ToArray();
|
var files = _availableFiles.OrderByDescending( f => f.FileSize ).ToArray();
|
||||||
Task.Run( () => CheckDuplicates( files ) );
|
Task.Run( () => CheckDuplicates( files ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,5 +232,53 @@ public partial class Mod
|
||||||
using var stream = File.OpenRead( f.FullName );
|
using var stream = File.OpenRead( f.FullName );
|
||||||
return _hasher.ComputeHash( stream );
|
return _hasher.ComputeHash( stream );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively delete all empty directories starting from the given directory.
|
||||||
|
// Deletes inner directories first, so that a tree of empty directories is actually deleted.
|
||||||
|
private void DeleteEmptyDirectories( DirectoryInfo baseDir )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if( !baseDir.Exists )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var dir in baseDir.EnumerateDirectories( "*", SearchOption.TopDirectoryOnly ) )
|
||||||
|
{
|
||||||
|
DeleteEmptyDirectories( dir );
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir.Refresh();
|
||||||
|
if( !baseDir.EnumerateFileSystemInfos().Any() )
|
||||||
|
{
|
||||||
|
Directory.Delete( baseDir.FullName, false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not delete empty directories in {baseDir.FullName}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Deduplicate a mod simply by its directory without any confirmation or waiting time.
|
||||||
|
internal static void DeduplicateMod( DirectoryInfo modDirectory )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mod = new Mod( modDirectory );
|
||||||
|
mod.Reload( out _ );
|
||||||
|
var editor = new Editor( mod, 0, 0 );
|
||||||
|
editor.DuplicatesFinished = false;
|
||||||
|
editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() );
|
||||||
|
editor.DeleteDuplicates( false );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Warning( $"Could not deduplicate mod {modDirectory.Name}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +163,7 @@ public partial class Mod
|
||||||
// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
|
// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
|
||||||
public bool SetGamePath( int fileIdx, int pathIdx, Utf8GamePath path )
|
public bool SetGamePath( int fileIdx, int pathIdx, Utf8GamePath path )
|
||||||
{
|
{
|
||||||
if( _usedPaths.Contains( path ) || fileIdx < 0 || fileIdx > _availableFiles.Count || pathIdx < 0 )
|
if( _usedPaths.Contains( path ) || fileIdx < 0 || fileIdx > _availableFiles.Count )
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +174,7 @@ public partial class Mod
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( pathIdx == registry.SubModUsage.Count )
|
if( (pathIdx == - 1 || pathIdx == registry.SubModUsage.Count) && !path.IsEmpty )
|
||||||
{
|
{
|
||||||
registry.SubModUsage.Add( ( CurrentOption, path ) );
|
registry.SubModUsage.Add( ( CurrentOption, path ) );
|
||||||
++registry.CurrentUsage;
|
++registry.CurrentUsage;
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -154,17 +154,12 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
if( ImGui.IsItemDeactivatedAfterEdit() )
|
if( ImGui.IsItemDeactivatedAfterEdit() )
|
||||||
{
|
{
|
||||||
|
if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) )
|
||||||
|
{
|
||||||
|
_editor!.SetGamePath( _fileIdx, _pathIdx, path );
|
||||||
|
}
|
||||||
_fileIdx = -1;
|
_fileIdx = -1;
|
||||||
_pathIdx = -1;
|
_pathIdx = -1;
|
||||||
if( _gamePathEdit.Length == 0 )
|
|
||||||
{
|
|
||||||
registry.SubModUsage.RemoveAt( j-- );
|
|
||||||
--registry.CurrentUsage;
|
|
||||||
}
|
|
||||||
else if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) )
|
|
||||||
{
|
|
||||||
registry.SubModUsage[ j ] = ( subMod, path );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,13 +176,12 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
if( ImGui.IsItemDeactivatedAfterEdit() )
|
if( ImGui.IsItemDeactivatedAfterEdit() )
|
||||||
{
|
{
|
||||||
_fileIdx = -1;
|
|
||||||
_pathIdx = -1;
|
|
||||||
if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) && !path.IsEmpty )
|
if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) && !path.IsEmpty )
|
||||||
{
|
{
|
||||||
registry.SubModUsage.Add( ( subMod, path ) );
|
_editor!.SetGamePath( _fileIdx, _pathIdx, path );
|
||||||
++registry.CurrentUsage;
|
|
||||||
}
|
}
|
||||||
|
_fileIdx = -1;
|
||||||
|
_pathIdx = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,19 +194,22 @@ public partial class ModEditWindow
|
||||||
ImGui.DragInt( "##skippedFolders", ref _folderSkip, 0.01f, 0, 10 );
|
ImGui.DragInt( "##skippedFolders", ref _folderSkip, 0.01f, 0, 10 );
|
||||||
ImGuiUtil.HoverTooltip( "Skip the first N folders when automatically constructing the game path from the file path." );
|
ImGuiUtil.HoverTooltip( "Skip the first N folders when automatically constructing the game path from the file path." );
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
spacing.Pop( );
|
spacing.Pop();
|
||||||
if( ImGui.Button( "Add Paths" ) )
|
if( ImGui.Button( "Add Paths" ) )
|
||||||
{
|
{
|
||||||
_editor!.AddPathsToSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ), _folderSkip );
|
_editor!.AddPathsToSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ), _folderSkip );
|
||||||
}
|
}
|
||||||
ImGuiUtil.HoverTooltip( "Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders." );
|
|
||||||
|
ImGuiUtil.HoverTooltip(
|
||||||
|
"Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders." );
|
||||||
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if( ImGui.Button( "Remove Paths" ) )
|
if( ImGui.Button( "Remove Paths" ) )
|
||||||
{
|
{
|
||||||
_editor!.RemovePathsFromSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) );
|
_editor!.RemovePathsFromSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiUtil.HoverTooltip( "Remove all game paths associated with the selected files in the current option." );
|
ImGuiUtil.HoverTooltip( "Remove all game paths associated with the selected files in the current option." );
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -221,7 +218,9 @@ public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
_editor!.DeleteFiles( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) );
|
_editor!.DeleteFiles( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) );
|
||||||
}
|
}
|
||||||
ImGuiUtil.HoverTooltip( "Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!" );
|
|
||||||
|
ImGuiUtil.HoverTooltip(
|
||||||
|
"Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!" );
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
var changes = _editor!.FileChanges;
|
var changes = _editor!.FileChanges;
|
||||||
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
|
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
|
||||||
|
|
@ -239,6 +238,7 @@ public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
_editor!.RevertFiles();
|
_editor!.RevertFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." );
|
ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." );
|
||||||
|
|
||||||
ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale );
|
ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale );
|
||||||
|
|
|
||||||
511
Penumbra/UI/Classes/ModEditWindow.Textures.cs
Normal file
511
Penumbra/UI/Classes/ModEditWindow.Textures.cs
Normal file
|
|
@ -0,0 +1,511 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using ImGuiNET;
|
||||||
|
using ImGuiScene;
|
||||||
|
using Lumina.Data.Files;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.Import.Dds;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
|
namespace Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
public partial class ModEditWindow
|
||||||
|
{
|
||||||
|
private string _pathLeft = string.Empty;
|
||||||
|
private string _pathRight = string.Empty;
|
||||||
|
|
||||||
|
private byte[]? _imageLeft;
|
||||||
|
private byte[]? _imageRight;
|
||||||
|
private byte[]? _imageCenter;
|
||||||
|
|
||||||
|
private TextureWrap? _wrapLeft;
|
||||||
|
private TextureWrap? _wrapRight;
|
||||||
|
private TextureWrap? _wrapCenter;
|
||||||
|
|
||||||
|
private Matrix4x4 _multiplierLeft = Matrix4x4.Identity;
|
||||||
|
private Matrix4x4 _multiplierRight = Matrix4x4.Identity;
|
||||||
|
private bool _invertLeft = false;
|
||||||
|
private bool _invertRight = false;
|
||||||
|
private int _offsetX = 0;
|
||||||
|
private int _offsetY = 0;
|
||||||
|
|
||||||
|
private readonly FileDialogManager _dialogManager = new();
|
||||||
|
|
||||||
|
private static bool DragFloat( string label, float width, ref float value )
|
||||||
|
{
|
||||||
|
var tmp = value;
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.SetNextItemWidth( width );
|
||||||
|
if( ImGui.DragFloat( label, ref tmp, 0.001f, -1f, 1f ) )
|
||||||
|
{
|
||||||
|
value = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImGui.IsItemDeactivatedAfterEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DrawMatrixInput( float width, ref Matrix4x4 matrix )
|
||||||
|
{
|
||||||
|
using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit );
|
||||||
|
if( !table )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changes = false;
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtil.Center( "R" );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtil.Center( "G" );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtil.Center( "B" );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtil.Center( "A" );
|
||||||
|
|
||||||
|
var inputWidth = width / 6;
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text( "R " );
|
||||||
|
changes |= DragFloat( "##RR", inputWidth, ref matrix.M11 );
|
||||||
|
changes |= DragFloat( "##RG", inputWidth, ref matrix.M12 );
|
||||||
|
changes |= DragFloat( "##RB", inputWidth, ref matrix.M13 );
|
||||||
|
changes |= DragFloat( "##RA", inputWidth, ref matrix.M14 );
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text( "G " );
|
||||||
|
changes |= DragFloat( "##GR", inputWidth, ref matrix.M21 );
|
||||||
|
changes |= DragFloat( "##GG", inputWidth, ref matrix.M22 );
|
||||||
|
changes |= DragFloat( "##GB", inputWidth, ref matrix.M23 );
|
||||||
|
changes |= DragFloat( "##GA", inputWidth, ref matrix.M24 );
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text( "B " );
|
||||||
|
changes |= DragFloat( "##BR", inputWidth, ref matrix.M31 );
|
||||||
|
changes |= DragFloat( "##BG", inputWidth, ref matrix.M32 );
|
||||||
|
changes |= DragFloat( "##BB", inputWidth, ref matrix.M33 );
|
||||||
|
changes |= DragFloat( "##BA", inputWidth, ref matrix.M34 );
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text( "A " );
|
||||||
|
changes |= DragFloat( "##AR", inputWidth, ref matrix.M41 );
|
||||||
|
changes |= DragFloat( "##AG", inputWidth, ref matrix.M42 );
|
||||||
|
changes |= DragFloat( "##AB", inputWidth, ref matrix.M43 );
|
||||||
|
changes |= DragFloat( "##AA", inputWidth, ref matrix.M44 );
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PathInputBox( string label, string hint, string tooltip, int which )
|
||||||
|
{
|
||||||
|
var tmp = which == 0 ? _pathLeft : _pathRight;
|
||||||
|
using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) );
|
||||||
|
ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale );
|
||||||
|
ImGui.InputTextWithHint( label, hint, ref tmp, Utf8GamePath.MaxGamePathLength );
|
||||||
|
if( ImGui.IsItemDeactivatedAfterEdit() )
|
||||||
|
{
|
||||||
|
UpdateImage( tmp, which );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.HoverTooltip( tooltip );
|
||||||
|
ImGui.SameLine();
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false,
|
||||||
|
true ) )
|
||||||
|
{
|
||||||
|
var startPath = Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath : _mod?.ModPath.FullName;
|
||||||
|
|
||||||
|
void UpdatePath( bool success, List< string > paths )
|
||||||
|
{
|
||||||
|
if( success && paths.Count > 0 )
|
||||||
|
{
|
||||||
|
UpdateImage( paths[ 0 ], which );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dialogManager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (byte[]?, int, int) GetDdsRgbaData( string path )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead( path );
|
||||||
|
if( !DdsFile.Load( stream, out var f ) )
|
||||||
|
{
|
||||||
|
return ( null, 0, 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return ( f.RgbaData.ToArray(), f.Header.Width, f.Header.Height );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not parse DDS {path} to RGBA:\n{e}" );
|
||||||
|
return ( null, 0, 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ( byte[]?, int, int) GetTexRgbaData( string path, bool fromDisk )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if( fromDisk )
|
||||||
|
{
|
||||||
|
var tmp = new TmpTexFile();
|
||||||
|
using var stream = File.OpenRead( path );
|
||||||
|
using var br = new BinaryReader( stream );
|
||||||
|
tmp.Load(br);
|
||||||
|
return (tmp.RgbaData, tmp.Header.Width, tmp.Header.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var tex = fromDisk ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( path ) : Dalamud.GameData.GetFile< TexFile >( path );
|
||||||
|
if( tex == null )
|
||||||
|
{
|
||||||
|
return ( null, 0, 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
var rgba = tex.Header.Format == TexFile.TextureFormat.A8R8G8B8
|
||||||
|
? ImageParsing.DecodeUncompressedR8G8B8A8( tex.ImageData, tex.Header.Height, tex.Header.Width )
|
||||||
|
: tex.GetRgbaImageData();
|
||||||
|
return ( rgba, tex.Header.Width, tex.Header.Height );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not parse TEX {path} to RGBA:\n{e}" );
|
||||||
|
return ( null, 0, 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (byte[]?, int, int) GetPngRgbaData( string path )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead( path );
|
||||||
|
using var png = Image.Load< Rgba32 >( stream );
|
||||||
|
var bytes = new byte[png.Height * png.Width * 4];
|
||||||
|
png.CopyPixelDataTo( bytes );
|
||||||
|
return ( bytes, png.Width, png.Height );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not parse PNG {path} to RGBA:\n{e}" );
|
||||||
|
return ( null, 0, 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateImage( string newPath, int which )
|
||||||
|
{
|
||||||
|
if( which is < 0 or > 1 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var path = ref which == 0 ? ref _pathLeft : ref _pathRight;
|
||||||
|
if( path == newPath )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = newPath;
|
||||||
|
ref var data = ref which == 0 ? ref _imageLeft : ref _imageRight;
|
||||||
|
ref var wrap = ref which == 0 ? ref _wrapLeft : ref _wrapRight;
|
||||||
|
|
||||||
|
data = null;
|
||||||
|
wrap?.Dispose();
|
||||||
|
wrap = null;
|
||||||
|
var width = 0;
|
||||||
|
var height = 0;
|
||||||
|
|
||||||
|
if( Path.IsPathRooted( path ) )
|
||||||
|
{
|
||||||
|
if( File.Exists( path ) )
|
||||||
|
{
|
||||||
|
( data, width, height ) = Path.GetExtension( path ) switch
|
||||||
|
{
|
||||||
|
".dds" => GetDdsRgbaData( path ),
|
||||||
|
".png" => GetPngRgbaData( path ),
|
||||||
|
".tex" => GetTexRgbaData( path, true ),
|
||||||
|
_ => ( null, 0, 0 ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
( data, width, height ) = GetTexRgbaData( path, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( data != null )
|
||||||
|
{
|
||||||
|
wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 );
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert )
|
||||||
|
{
|
||||||
|
if( bytes == null )
|
||||||
|
{
|
||||||
|
return Vector4.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] );
|
||||||
|
var transformed = Vector4.Transform( rgba.ToVector4(), transform );
|
||||||
|
if( invert )
|
||||||
|
{
|
||||||
|
transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W );
|
||||||
|
}
|
||||||
|
|
||||||
|
transformed.X = Math.Clamp( transformed.X, 0, 1 );
|
||||||
|
transformed.Y = Math.Clamp( transformed.Y, 0, 1 );
|
||||||
|
transformed.Z = Math.Clamp( transformed.Z, 0, 1 );
|
||||||
|
transformed.W = Math.Clamp( transformed.W, 0, 1 );
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector4 DataLeft( int offset )
|
||||||
|
=> CappedVector( _imageLeft, offset, _multiplierLeft, _invertLeft );
|
||||||
|
|
||||||
|
private Vector4 DataRight( int x, int y )
|
||||||
|
{
|
||||||
|
if( _imageRight == null )
|
||||||
|
{
|
||||||
|
return Vector4.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
x -= _offsetX;
|
||||||
|
y -= _offsetY;
|
||||||
|
if( x < 0 || x >= _wrapRight!.Width || y < 0 || y >= _wrapRight!.Height )
|
||||||
|
{
|
||||||
|
return Vector4.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = ( y * _wrapRight!.Width + x ) * 4;
|
||||||
|
return CappedVector( _imageRight, offset, _multiplierRight, _invertRight );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddPixels( int width, int x, int y )
|
||||||
|
{
|
||||||
|
var offset = ( width * y + x ) * 4;
|
||||||
|
var left = DataLeft( offset );
|
||||||
|
var right = DataRight( x, y );
|
||||||
|
var alpha = right.W + left.W * ( 1 - right.W );
|
||||||
|
if( alpha == 0 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha;
|
||||||
|
var rgba = new Rgba32( sum with { W = alpha } );
|
||||||
|
_imageCenter![ offset ] = rgba.R;
|
||||||
|
_imageCenter![ offset + 1 ] = rgba.G;
|
||||||
|
_imageCenter![ offset + 2 ] = rgba.B;
|
||||||
|
_imageCenter![ offset + 3 ] = rgba.A;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCenter()
|
||||||
|
{
|
||||||
|
if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity && !_invertLeft )
|
||||||
|
{
|
||||||
|
_imageCenter = _imageLeft;
|
||||||
|
_wrapCenter = _wrapLeft;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity && !_invertRight )
|
||||||
|
{
|
||||||
|
_imageCenter = _imageRight;
|
||||||
|
_wrapCenter = _wrapRight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !ReferenceEquals( _imageCenter, _imageLeft ) && !ReferenceEquals( _imageCenter, _imageRight ) )
|
||||||
|
{
|
||||||
|
_wrapCenter?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if( _imageLeft != null || _imageRight != null )
|
||||||
|
{
|
||||||
|
var (totalWidth, totalHeight) =
|
||||||
|
_imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height );
|
||||||
|
_imageCenter = new byte[4 * totalWidth * totalHeight];
|
||||||
|
|
||||||
|
Parallel.For( 0, totalHeight - 1, ( y, _ ) =>
|
||||||
|
{
|
||||||
|
for( var x = 0; x < totalWidth; ++x )
|
||||||
|
{
|
||||||
|
AddPixels( totalWidth, x, y );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
_wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, totalWidth, totalHeight, 4 );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_imageCenter = null;
|
||||||
|
_wrapCenter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ScaledImage( string path, TextureWrap? wrap, Vector2 size )
|
||||||
|
{
|
||||||
|
if( wrap != null )
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted( $"Image Dimensions: {wrap.Width} x {wrap.Height}" );
|
||||||
|
size = size with { Y = wrap.Height * size.X / wrap.Width };
|
||||||
|
ImGui.Image( wrap.ImGuiHandle, size );
|
||||||
|
}
|
||||||
|
else if( path.Length > 0 )
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted( "Could not load file." );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Dummy( size );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveAs( bool success, string path, int type )
|
||||||
|
{
|
||||||
|
if( !success || _imageCenter == null || _wrapCenter == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch( type )
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
var img = Image.LoadPixelData< Rgba32 >( _imageCenter, _wrapCenter.Width, _wrapCenter.Height );
|
||||||
|
img.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } );
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if( TextureImporter.RgbaBytesToTex( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var tex ) )
|
||||||
|
{
|
||||||
|
File.WriteAllBytes( path, tex );
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) )
|
||||||
|
{
|
||||||
|
File.WriteAllBytes( path, dds );
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not save image to {path}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveAsPng( bool success, string path )
|
||||||
|
=> SaveAs( success, path, 0 );
|
||||||
|
|
||||||
|
private void SaveAsTex( bool success, string path )
|
||||||
|
=> SaveAs( success, path, 1 );
|
||||||
|
|
||||||
|
private void SaveAsDds( bool success, string path )
|
||||||
|
=> SaveAs( success, path, 2 );
|
||||||
|
|
||||||
|
private void DrawTextureTab()
|
||||||
|
{
|
||||||
|
_dialogManager.Draw();
|
||||||
|
|
||||||
|
using var tab = ImRaii.TabItem( "Texture Import/Export (WIP)" );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftRightWidth = new Vector2( ( ImGui.GetWindowContentRegionWidth() - ImGui.GetStyle().FramePadding.X * 4 ) / 3, -1 );
|
||||||
|
var imageSize = new Vector2( leftRightWidth.X - ImGui.GetStyle().FramePadding.X * 2 );
|
||||||
|
using( var child = ImRaii.Child( "ImageLeft", leftRightWidth, true ) )
|
||||||
|
{
|
||||||
|
if( child )
|
||||||
|
{
|
||||||
|
PathInputBox( "##ImageLeft", "Import Image...", string.Empty, 0 );
|
||||||
|
|
||||||
|
ImGui.NewLine();
|
||||||
|
if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) || ImGui.Checkbox( "Invert##Left", ref _invertLeft ) )
|
||||||
|
{
|
||||||
|
UpdateCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.NewLine();
|
||||||
|
ScaledImage( _pathLeft, _wrapLeft, imageSize );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
using( var child = ImRaii.Child( "ImageMix", leftRightWidth, true ) )
|
||||||
|
{
|
||||||
|
if( child )
|
||||||
|
{
|
||||||
|
if( _wrapCenter == null && _wrapLeft != null && _wrapRight != null )
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted( "Images have incompatible resolutions." );
|
||||||
|
}
|
||||||
|
else if( _wrapCenter != null )
|
||||||
|
{
|
||||||
|
if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) )
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension( _pathLeft.Length > 0 ? _pathLeft : _pathRight );
|
||||||
|
_dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", SaveAsTex, _mod!.ModPath.FullName );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) )
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension( _pathRight.Length > 0 ? _pathRight : _pathLeft );
|
||||||
|
_dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", SaveAsPng, _mod!.ModPath.FullName );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) )
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension( _pathRight.Length > 0 ? _pathRight : _pathLeft );
|
||||||
|
_dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", SaveAsDds, _mod!.ModPath.FullName );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.NewLine();
|
||||||
|
ScaledImage( string.Empty, _wrapCenter, imageSize );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
using( var child = ImRaii.Child( "ImageRight", leftRightWidth, true ) )
|
||||||
|
{
|
||||||
|
if( child )
|
||||||
|
{
|
||||||
|
PathInputBox( "##ImageRight", "Import Image...", string.Empty, 1 );
|
||||||
|
|
||||||
|
ImGui.NewLine();
|
||||||
|
if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) || ImGui.Checkbox( "Invert##Right", ref _invertRight ) )
|
||||||
|
{
|
||||||
|
UpdateCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.NewLine();
|
||||||
|
ScaledImage( _pathRight, _wrapRight, imageSize );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,7 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
DrawMissingFilesTab();
|
DrawMissingFilesTab();
|
||||||
DrawDuplicatesTab();
|
DrawDuplicatesTab();
|
||||||
DrawMaterialChangeTab();
|
DrawMaterialChangeTab();
|
||||||
|
DrawTextureTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
|
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ public partial class ConfigWindow
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawMainSelectors();
|
|
||||||
DrawCharacterCollectionSelectors();
|
DrawCharacterCollectionSelectors();
|
||||||
|
DrawMainSelectors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -139,52 +139,46 @@ public partial class ConfigWindow
|
||||||
|
|
||||||
private void DrawCharacterCollectionSelectors()
|
private void DrawCharacterCollectionSelectors()
|
||||||
{
|
{
|
||||||
using var child = ImRaii.Child( "##Collections", -Vector2.One, true );
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
if( !child )
|
if( ImGui.CollapsingHeader( "Active Collections", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||||
{
|
{
|
||||||
return;
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
}
|
DrawDefaultCollectionSelector();
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
DrawDefaultCollectionSelector();
|
foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() )
|
||||||
|
|
||||||
foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() )
|
|
||||||
{
|
|
||||||
using var id = ImRaii.PushId( name );
|
|
||||||
DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, ModCollection.Type.Character, true, name );
|
|
||||||
ImGui.SameLine();
|
|
||||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty,
|
|
||||||
false,
|
|
||||||
true ) )
|
|
||||||
{
|
{
|
||||||
Penumbra.CollectionManager.RemoveCharacterCollection( name );
|
using var id = ImRaii.PushId( name );
|
||||||
|
DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, ModCollection.Type.Character, true, name );
|
||||||
|
ImGui.SameLine();
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty,
|
||||||
|
false,
|
||||||
|
true ) )
|
||||||
|
{
|
||||||
|
Penumbra.CollectionManager.RemoveCharacterCollection( name );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted( name );
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
DrawNewCharacterCollection();
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.NewLine();
|
||||||
ImGui.TextUnformatted( name );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawNewCharacterCollection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMainSelectors()
|
private void DrawMainSelectors()
|
||||||
{
|
{
|
||||||
var size = new Vector2( -1,
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
ImGui.GetTextLineHeightWithSpacing() * (InheritedCollectionHeight + 1)
|
if( ImGui.CollapsingHeader( "Collection Settings", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||||
+ _window._defaultSpace.Y * 2
|
|
||||||
+ ImGui.GetFrameHeightWithSpacing() * 4
|
|
||||||
+ ImGui.GetStyle().ItemSpacing.Y * 6 );
|
|
||||||
using var main = ImRaii.Child( "##CollectionsMain", size, true );
|
|
||||||
if( !main )
|
|
||||||
{
|
{
|
||||||
return;
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
DrawCurrentCollectionSelector();
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
DrawNewCollectionInput();
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
DrawInheritanceBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawCurrentCollectionSelector();
|
|
||||||
ImGui.Dummy( _window._defaultSpace );
|
|
||||||
DrawNewCollectionInput();
|
|
||||||
ImGui.Dummy( _window._defaultSpace );
|
|
||||||
DrawInheritanceBlock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ public partial class ConfigWindow
|
||||||
private static readonly Utf8String DescriptionTabHeader = Utf8String.FromStringUnsafe( "Description", false );
|
private static readonly Utf8String DescriptionTabHeader = Utf8String.FromStringUnsafe( "Description", false );
|
||||||
private static readonly Utf8String SettingsTabHeader = Utf8String.FromStringUnsafe( "Settings", false );
|
private static readonly Utf8String SettingsTabHeader = Utf8String.FromStringUnsafe( "Settings", false );
|
||||||
private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false );
|
private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false );
|
||||||
private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod Meta", false );
|
private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false );
|
||||||
|
|
||||||
private void DrawTabBar()
|
private void DrawTabBar()
|
||||||
{
|
{
|
||||||
|
|
@ -56,7 +56,7 @@ public partial class ConfigWindow
|
||||||
DrawChangedItemsTab();
|
DrawChangedItemsTab();
|
||||||
DrawConflictsTab();
|
DrawConflictsTab();
|
||||||
DrawEditModTab();
|
DrawEditModTab();
|
||||||
if( ImGui.TabItemButton( "Open Advanced Edit Window", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) )
|
if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) )
|
||||||
{
|
{
|
||||||
_window.ModEditPopup.ChangeMod( _mod );
|
_window.ModEditPopup.ChangeMod( _mod );
|
||||||
_window.ModEditPopup.ChangeOption( -1, 0 );
|
_window.ModEditPopup.ChangeOption( -1, 0 );
|
||||||
|
|
@ -69,7 +69,8 @@ public partial class ConfigWindow
|
||||||
+ "\t\t- file swaps\n"
|
+ "\t\t- file swaps\n"
|
||||||
+ "\t\t- metadata manipulations\n"
|
+ "\t\t- metadata manipulations\n"
|
||||||
+ "\t\t- model materials\n"
|
+ "\t\t- model materials\n"
|
||||||
+ "\t\t- duplicates" );
|
+ "\t\t- duplicates\n"
|
||||||
|
+ "\t\t- textures" );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just a simple text box with the wrapped description, if it exists.
|
// Just a simple text box with the wrapped description, if it exists.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ public partial class ConfigWindow
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Checkbox( "Auto Deduplicate on Import",
|
||||||
|
"Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.",
|
||||||
|
Penumbra.Config.AutoDeduplicateOnImport, v => Penumbra.Config.AutoDeduplicateOnImport = v );
|
||||||
DrawRequestedResourceLogging();
|
DrawRequestedResourceLogging();
|
||||||
DrawDisableSoundStreamingBox();
|
DrawDisableSoundStreamingBox();
|
||||||
DrawEnableHttpApiBox();
|
DrawEnableHttpApiBox();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
|
@ -16,6 +17,7 @@ public partial class ConfigWindow
|
||||||
{
|
{
|
||||||
private partial class SettingsTab
|
private partial class SettingsTab
|
||||||
{
|
{
|
||||||
|
public const int RootDirectoryMaxLength = 64;
|
||||||
private readonly ConfigWindow _window;
|
private readonly ConfigWindow _window;
|
||||||
|
|
||||||
public SettingsTab( ConfigWindow window )
|
public SettingsTab( ConfigWindow window )
|
||||||
|
|
@ -65,11 +67,18 @@ public partial class ConfigWindow
|
||||||
|
|
||||||
// Do not change the directory without explicitly pressing enter or this button.
|
// Do not change the directory without explicitly pressing enter or this button.
|
||||||
// Shows up only if the current input does not correspond to the current directory.
|
// Shows up only if the current input does not correspond to the current directory.
|
||||||
private static bool DrawPressEnterWarning( string old, float width )
|
private static bool DrawPressEnterWarning( string newName, string old, float width, bool saved )
|
||||||
{
|
{
|
||||||
using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg );
|
using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg );
|
||||||
var w = new Vector2( width, 0 );
|
var w = new Vector2( width, 0 );
|
||||||
return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w );
|
var symbol = '\0';
|
||||||
|
var (text, valid) = newName.Length > RootDirectoryMaxLength
|
||||||
|
? ( $"Path is too long. The maximum length is {RootDirectoryMaxLength}.", false )
|
||||||
|
: newName.Any( c => ( symbol = c ) > ( char )0x7F )
|
||||||
|
? ( $"Path contains invalid symbol {symbol}. Only ASCII is allowed.", false )
|
||||||
|
: ( $"Press Enter or Click Here to Save (Current Directory: {old})", true );
|
||||||
|
|
||||||
|
return ( ImGui.Button( text, w ) || saved ) && valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a directory picker button that toggles the directory picker.
|
// Draw a directory picker button that toggles the directory picker.
|
||||||
|
|
@ -128,7 +137,7 @@ public partial class ConfigWindow
|
||||||
var spacing = 3 * ImGuiHelpers.GlobalScale;
|
var spacing = 3 * ImGuiHelpers.GlobalScale;
|
||||||
using var group = ImRaii.Group();
|
using var group = ImRaii.Group();
|
||||||
ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - _window._iconButtonSize.X );
|
ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - _window._iconButtonSize.X );
|
||||||
var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue );
|
var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 64, ImGuiInputTextFlags.EnterReturnsTrue );
|
||||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) );
|
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) );
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
DrawDirectoryPickerButton();
|
DrawDirectoryPickerButton();
|
||||||
|
|
@ -147,7 +156,7 @@ public partial class ConfigWindow
|
||||||
|
|
||||||
if( Penumbra.Config.ModDirectory != _newModDirectory
|
if( Penumbra.Config.ModDirectory != _newModDirectory
|
||||||
&& _newModDirectory.Length != 0
|
&& _newModDirectory.Length != 0
|
||||||
&& ( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) )
|
&& DrawPressEnterWarning( _newModDirectory, Penumbra.Config.ModDirectory, pos, save ) )
|
||||||
{
|
{
|
||||||
Penumbra.ModManager.DiscoverMods( _newModDirectory );
|
Penumbra.ModManager.DiscoverMods( _newModDirectory );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
repo.json
10
repo.json
|
|
@ -4,8 +4,8 @@
|
||||||
"Name": "Penumbra",
|
"Name": "Penumbra",
|
||||||
"Description": "Runtime mod loader and manager.",
|
"Description": "Runtime mod loader and manager.",
|
||||||
"InternalName": "Penumbra",
|
"InternalName": "Penumbra",
|
||||||
"AssemblyVersion": "0.5.0.5",
|
"AssemblyVersion": "0.5.1.0",
|
||||||
"TestingAssemblyVersion": "0.5.0.5",
|
"TestingAssemblyVersion": "0.5.1.0",
|
||||||
"RepoUrl": "https://github.com/xivdev/Penumbra",
|
"RepoUrl": "https://github.com/xivdev/Penumbra",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"DalamudApiLevel": 6,
|
"DalamudApiLevel": 6,
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
"DownloadCount": 0,
|
"DownloadCount": 0,
|
||||||
"LastUpdate": 0,
|
"LastUpdate": 0,
|
||||||
"LoadPriority": 69420,
|
"LoadPriority": 69420,
|
||||||
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.5/Penumbra.zip",
|
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip",
|
||||||
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.5/Penumbra.zip",
|
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip",
|
||||||
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.5/Penumbra.zip",
|
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip",
|
||||||
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
|
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue