Merge branch 'xivdev:master' into master

This commit is contained in:
Theo 2025-01-20 07:47:52 -08:00 committed by GitHub
commit 1726d14ff7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1698 additions and 167 deletions

@ -1 +1 @@
Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37
Subproject commit 5bac66e5ad73e57919aff7f8b046606b75e191a2

View file

@ -24,6 +24,34 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}"
ProjectSection(SolutionItems) = preProject
schemas\default_mod.json = schemas\default_mod.json
schemas\group.json = schemas\group.json
schemas\local_mod_data-v3.json = schemas\local_mod_data-v3.json
schemas\mod_meta-v3.json = schemas\mod_meta-v3.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}"
ProjectSection(SolutionItems) = preProject
schemas\structs\container.json = schemas\structs\container.json
schemas\structs\group_combining.json = schemas\structs\group_combining.json
schemas\structs\group_imc.json = schemas\structs\group_imc.json
schemas\structs\group_multi.json = schemas\structs\group_multi.json
schemas\structs\group_single.json = schemas\structs\group_single.json
schemas\structs\manipulation.json = schemas\structs\manipulation.json
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
schemas\structs\meta_est.json = schemas\structs\meta_est.json
schemas\structs\meta_geqp.json = schemas\structs\meta_geqp.json
schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
schemas\structs\option.json = schemas\structs\option.json
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -58,6 +86,10 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BFEA7504-1210-4F79-A7FE-BF03B6567E33} = {F89C9EAE-25C8-43BE-8108-5921E5A93502}
{B03F276A-0572-4F62-AF86-EF62F6B80463} = {BFEA7504-1210-4F79-A7FE-BF03B6567E33}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
EndGlobalSection

View file

@ -194,7 +194,8 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx)
{
saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
if (type is not ModSettingChange.TemporarySetting)
saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection));
communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false);
if (type is not ModSettingChange.TemporarySetting)
RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx);

View file

@ -11,7 +11,8 @@ public class GameState : IService
{
#region Last Game Object
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
public readonly ThreadLocal<bool> CharacterAssociated = new(() => false);
public nint LastGameObject
=> _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero;

View file

@ -76,6 +76,8 @@ public class HookOverrides
public bool CreateCharacterBase;
public bool EnableDraw;
public bool WeaponReload;
public bool SetupPlayerNpc;
public bool ConstructCutsceneCharacter;
}
public struct PostProcessingHooks

View file

@ -0,0 +1,70 @@
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
namespace Penumbra.Interop.Hooks.Objects;
public sealed unsafe class ConstructCutsceneCharacter : EventWrapperPtr<Character, ConstructCutsceneCharacter.Priority>, IHookService
{
private readonly GameState _gameState;
private readonly ObjectManager _objects;
public enum Priority
{
/// <seealso cref="PathResolving.CutsceneService.OnSetupPlayerNpc"/>
CutsceneService = 0,
}
public ConstructCutsceneCharacter(GameState gameState, HookManager hooks, ObjectManager objects)
: base("ConstructCutsceneCharacter")
{
_gameState = gameState;
_objects = objects;
_task = hooks.CreateHook<Delegate>(Name, Sigs.ConstructCutsceneCharacter, Detour, !HookOverrides.Instance.Objects.ConstructCutsceneCharacter);
}
private readonly Task<Hook<Delegate>> _task;
public delegate int Delegate(SetupPlayerNpc.SchedulerStruct* scheduler);
public int Detour(SetupPlayerNpc.SchedulerStruct* scheduler)
{
// This is the function that actually creates the new game object
// and fills it into the object table at a free index etc.
var ret = _task.Result.Original(scheduler);
// Check for the copy state from SetupPlayerNpc.
if (_gameState.CharacterAssociated.Value)
{
// If the newly created character exists, invoke the event.
var character = _objects[ret + (int)ScreenActor.CutsceneStart].AsCharacter;
if (character != null)
{
Invoke(character);
Penumbra.Log.Verbose(
$"[{Name}] Created indirect copy of player character at 0x{(nint)character}, index {character->ObjectIndex}.");
}
_gameState.CharacterAssociated.Value = false;
}
return ret;
}
public IntPtr Address
=> _task.Result.Address;
public void Enable()
=> _task.Result.Enable();
public void Disable()
=> _task.Result.Disable();
public Task Awaiter
=> _task;
public bool Finished
=> _task.IsCompletedSuccessfully;
}

View file

@ -26,7 +26,7 @@ public sealed unsafe class EnableDraw : IHookService
private void Detour(GameObject* gameObject)
{
_state.QueueGameObject(gameObject);
Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X}.");
Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X} at {gameObject->ObjectIndex}.");
_task.Result.Original.Invoke(gameObject);
_state.DequeueGameObject();
}

View file

@ -0,0 +1,55 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using OtterGui.Services;
using Penumbra.GameData;
namespace Penumbra.Interop.Hooks.Objects;
public sealed unsafe class SetupPlayerNpc : FastHook<SetupPlayerNpc.Delegate>
{
private readonly GameState _gameState;
public SetupPlayerNpc(GameState gameState, HookManager hooks)
{
_gameState = gameState;
Task = hooks.CreateHook<Delegate>("SetupPlayerNPC", Sigs.SetupPlayerNpc, Detour,
!HookOverrides.Instance.Objects.SetupPlayerNpc);
}
public delegate SchedulerStruct* Delegate(byte* npcType, nint unk, NpcSetupData* setupData);
public SchedulerStruct* Detour(byte* npcType, nint unk, NpcSetupData* setupData)
{
// This function actually seems to generate all NPC.
// If an ENPC is being created, check the creation parameters.
// If CopyPlayerCustomize is true, the event NPC gets a timeline that copies its customize and glasses from the local player.
// Keep track of this, so we can associate the actor to be created for this with the player character, see ConstructCutsceneCharacter.
if (setupData->CopyPlayerCustomize && npcType != null && *npcType is 8)
_gameState.CharacterAssociated.Value = true;
var ret = Task.Result.Original.Invoke(npcType, unk, setupData);
Penumbra.Log.Excessive(
$"[Setup Player NPC] Invoked for type {*npcType} with 0x{unk:X} and Copy Player Customize: {setupData->CopyPlayerCustomize}.");
return ret;
}
[StructLayout(LayoutKind.Explicit)]
public struct NpcSetupData
{
[FieldOffset(0x0B)]
private byte _copyPlayerCustomize;
public bool CopyPlayerCustomize
{
get => _copyPlayerCustomize != 0;
set => _copyPlayerCustomize = value ? (byte)1 : (byte)0;
}
}
[StructLayout(LayoutKind.Explicit)]
public struct SchedulerStruct
{
public static Character* GetCharacter(SchedulerStruct* s)
=> ((delegate* unmanaged<SchedulerStruct*, Character*>**)s)[0][19](s);
}
}

View file

@ -15,10 +15,11 @@ public sealed class CutsceneService : IRequiredService, IDisposable
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
private readonly ObjectManager _objects;
private readonly CopyCharacter _copyCharacter;
private readonly CharacterDestructor _characterDestructor;
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
private readonly ObjectManager _objects;
private readonly CopyCharacter _copyCharacter;
private readonly CharacterDestructor _characterDestructor;
private readonly ConstructCutsceneCharacter _constructCutsceneCharacter;
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
public IEnumerable<KeyValuePair<int, IGameObject>> Actors
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
@ -26,13 +27,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!));
public unsafe CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor,
IClientState clientState)
ConstructCutsceneCharacter constructCutsceneCharacter, IClientState clientState)
{
_objects = objects;
_copyCharacter = copyCharacter;
_characterDestructor = characterDestructor;
_objects = objects;
_copyCharacter = copyCharacter;
_characterDestructor = characterDestructor;
_constructCutsceneCharacter = constructCutsceneCharacter;
_copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService);
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService);
_constructCutsceneCharacter.Subscribe(OnSetupPlayerNpc, ConstructCutsceneCharacter.Priority.CutsceneService);
if (clientState.IsGPosing)
RecoverGPoseActors();
}
@ -87,6 +90,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable
{
_copyCharacter.Unsubscribe(OnCharacterCopy);
_characterDestructor.Unsubscribe(OnCharacterDestructor);
_constructCutsceneCharacter.Unsubscribe(OnSetupPlayerNpc);
}
private unsafe void OnCharacterDestructor(Character* character)
@ -124,6 +128,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable
_copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1);
}
private unsafe void OnSetupPlayerNpc(Character* npc)
{
if (npc == null || npc->ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx)
return;
var idx = npc->GameObject.ObjectIndex - CutsceneStartIdx;
_copiedCharacters[idx] = 0;
}
/// <summary> Try to recover GPose actors on reloads into a running game. </summary>
/// <remarks> This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. </remarks>
private void RecoverGPoseActors()

View file

@ -27,6 +27,9 @@ public enum ModDataChangeType : ushort
public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService
{
public SaveService SaveService
=> saveService;
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
string? website)
@ -40,148 +43,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
saveService.ImmediateSaveSync(new ModMeta(mod));
}
public ModDataChangeType LoadLocalData(Mod mod)
{
var dataFile = saveService.FileNames.LocalDataFile(mod);
var importDate = 0L;
var localTags = Enumerable.Empty<string>();
var favorite = false;
var note = string.Empty;
var save = true;
if (File.Exists(dataFile))
try
{
var text = File.ReadAllText(dataFile);
var json = JObject.Parse(text);
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
save = false;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load local mod data:\n{e}");
}
if (importDate == 0)
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
ModDataChangeType changes = 0;
if (mod.ImportDate != importDate)
{
mod.ImportDate = importDate;
changes |= ModDataChangeType.ImportDate;
}
changes |= ModLocalData.UpdateTags(mod, null, localTags);
if (mod.Favorite != favorite)
{
mod.Favorite = favorite;
changes |= ModDataChangeType.Favorite;
}
if (mod.Note != note)
{
mod.Note = note;
changes |= ModDataChangeType.Note;
}
if (save)
saveService.QueueSave(new ModLocalData(mod));
return changes;
}
public ModDataChangeType LoadMeta(ModCreator creator, Mod mod)
{
var metaFile = saveService.FileNames.ModMetaPath(mod);
if (!File.Exists(metaFile))
{
Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}.");
return ModDataChangeType.Deletion;
}
try
{
var text = File.ReadAllText(metaFile);
var json = JObject.Parse(text);
var newName = json[nameof(Mod.Name)]?.Value<string>() ?? string.Empty;
var newAuthor = json[nameof(Mod.Author)]?.Value<string>() ?? string.Empty;
var newDescription = json[nameof(Mod.Description)]?.Value<string>() ?? string.Empty;
var newImage = json[nameof(Mod.Image)]?.Value<string>() ?? string.Empty;
var newVersion = json[nameof(Mod.Version)]?.Value<string>() ?? string.Empty;
var newWebsite = json[nameof(Mod.Website)]?.Value<string>() ?? string.Empty;
var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value<uint>() ?? 0;
var importDate = json[nameof(Mod.ImportDate)]?.Value<long>();
var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values<string>().OfType<string>();
ModDataChangeType changes = 0;
if (mod.Name != newName)
{
changes |= ModDataChangeType.Name;
mod.Name = newName;
}
if (mod.Author != newAuthor)
{
changes |= ModDataChangeType.Author;
mod.Author = newAuthor;
}
if (mod.Description != newDescription)
{
changes |= ModDataChangeType.Description;
mod.Description = newDescription;
}
if (mod.Image != newImage)
{
changes |= ModDataChangeType.Image;
mod.Image = newImage;
}
if (mod.Version != newVersion)
{
changes |= ModDataChangeType.Version;
mod.Version = newVersion;
}
if (mod.Website != newWebsite)
{
changes |= ModDataChangeType.Website;
mod.Website = newWebsite;
}
if (newFileVersion != ModMeta.FileVersion)
if (ModMigration.Migrate(creator, saveService, mod, json, ref newFileVersion))
{
changes |= ModDataChangeType.Migration;
saveService.ImmediateSave(new ModMeta(mod));
}
if (importDate != null && mod.ImportDate != importDate.Value)
{
mod.ImportDate = importDate.Value;
changes |= ModDataChangeType.ImportDate;
}
changes |= ModLocalData.UpdateTags(mod, modTags, null);
return changes;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load mod meta for {metaFile}:\n{e}");
return ModDataChangeType.Deletion;
}
}
public void ChangeModName(Mod mod, string newName)
{
if (mod.Name.Text == newName)

View file

@ -72,11 +72,11 @@ public partial class ModCreator(
if (!Directory.Exists(mod.ModPath.FullName))
return false;
modDataChange = dataEditor.LoadMeta(this, mod);
modDataChange = ModMeta.Load(dataEditor, this, mod);
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
return false;
modDataChange |= dataEditor.LoadLocalData(mod);
modDataChange |= ModLocalData.Load(dataEditor, mod);
LoadDefaultOption(mod);
LoadAllGroups(mod);
if (incorporateMetaChanges)

View file

@ -27,6 +27,63 @@ public readonly struct ModLocalData(Mod mod) : ISavable
jObject.WriteTo(jWriter);
}
public static ModDataChangeType Load(ModDataEditor editor, Mod mod)
{
var dataFile = editor.SaveService.FileNames.LocalDataFile(mod);
var importDate = 0L;
var localTags = Enumerable.Empty<string>();
var favorite = false;
var note = string.Empty;
var save = true;
if (File.Exists(dataFile))
try
{
var text = File.ReadAllText(dataFile);
var json = JObject.Parse(text);
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
save = false;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load local mod data:\n{e}");
}
if (importDate == 0)
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
ModDataChangeType changes = 0;
if (mod.ImportDate != importDate)
{
mod.ImportDate = importDate;
changes |= ModDataChangeType.ImportDate;
}
changes |= ModLocalData.UpdateTags(mod, null, localTags);
if (mod.Favorite != favorite)
{
mod.Favorite = favorite;
changes |= ModDataChangeType.Favorite;
}
if (mod.Note != note)
{
mod.Note = note;
changes |= ModDataChangeType.Note;
}
if (save)
editor.SaveService.QueueSave(new ModLocalData(mod));
return changes;
}
internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
{
if (newModTags == null && newLocalTags == null)

View file

@ -1,5 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Mods;
@ -28,4 +30,85 @@ public readonly struct ModMeta(Mod mod) : ISavable
jWriter.Formatting = Formatting.Indented;
jObject.WriteTo(jWriter);
}
public static ModDataChangeType Load(ModDataEditor editor, ModCreator creator, Mod mod)
{
var metaFile = editor.SaveService.FileNames.ModMetaPath(mod);
if (!File.Exists(metaFile))
{
Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}.");
return ModDataChangeType.Deletion;
}
try
{
var text = File.ReadAllText(metaFile);
var json = JObject.Parse(text);
var newFileVersion = json[nameof(FileVersion)]?.Value<uint>() ?? 0;
// Empty name gets checked after loading and is not allowed.
var newName = json[nameof(Mod.Name)]?.Value<string>() ?? string.Empty;
var newAuthor = json[nameof(Mod.Author)]?.Value<string>() ?? string.Empty;
var newDescription = json[nameof(Mod.Description)]?.Value<string>() ?? string.Empty;
var newImage = json[nameof(Mod.Image)]?.Value<string>() ?? string.Empty;
var newVersion = json[nameof(Mod.Version)]?.Value<string>() ?? string.Empty;
var newWebsite = json[nameof(Mod.Website)]?.Value<string>() ?? string.Empty;
var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values<string>().OfType<string>();
ModDataChangeType changes = 0;
if (mod.Name != newName)
{
changes |= ModDataChangeType.Name;
mod.Name = newName;
}
if (mod.Author != newAuthor)
{
changes |= ModDataChangeType.Author;
mod.Author = newAuthor;
}
if (mod.Description != newDescription)
{
changes |= ModDataChangeType.Description;
mod.Description = newDescription;
}
if (mod.Image != newImage)
{
changes |= ModDataChangeType.Image;
mod.Image = newImage;
}
if (mod.Version != newVersion)
{
changes |= ModDataChangeType.Version;
mod.Version = newVersion;
}
if (mod.Website != newWebsite)
{
changes |= ModDataChangeType.Website;
mod.Website = newWebsite;
}
if (newFileVersion != FileVersion)
if (ModMigration.Migrate(creator, editor.SaveService, mod, json, ref newFileVersion))
{
changes |= ModDataChangeType.Migration;
editor.SaveService.ImmediateSave(new ModMeta(mod));
}
changes |= ModLocalData.UpdateTags(mod, modTags, null);
return changes;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load mod meta for {metaFile}:\n{e}");
return ModDataChangeType.Deletion;
}
}
}

View file

@ -81,8 +81,9 @@ public static class SubMod
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath)
{
if (data.Files.Count > 0)
{
// #TODO: remove comments when TexTools updated.
//if (data.Files.Count > 0)
//{
j.WritePropertyName(nameof(data.Files));
j.WriteStartObject();
foreach (var (gamePath, file) in data.Files)
@ -95,10 +96,10 @@ public static class SubMod
}
j.WriteEndObject();
}
//}
if (data.FileSwaps.Count > 0)
{
//if (data.FileSwaps.Count > 0)
//{
j.WritePropertyName(nameof(data.FileSwaps));
j.WriteStartObject();
foreach (var (gamePath, file) in data.FileSwaps)
@ -108,13 +109,13 @@ public static class SubMod
}
j.WriteEndObject();
}
//}
if (data.Manipulations.Count > 0)
{
//if (data.Manipulations.Count > 0)
//{
j.WritePropertyName(nameof(data.Manipulations));
serializer.Serialize(j, data.Manipulations);
}
//}
}
/// <summary> Write the data for a selectable mod option on a JsonWriter. </summary>

View file

@ -6,7 +6,7 @@
"Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra",
"AssemblyVersion": "1.3.3.1",
"TestingAssemblyVersion": "1.3.3.4",
"TestingAssemblyVersion": "1.3.3.5",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 11,
@ -19,7 +19,7 @@
"LoadRequiredState": 2,
"LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.4/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.5/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
}

19
schemas/default_mod.json Normal file
View file

@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"type": "object",
"properties": {
"Version": {
"description": "Mod Container version, currently unused.",
"type": "integer",
"minimum": 0,
"maximum": 0
}
}
},
{
"$ref": "structs/container.json"
}
]
}

57
schemas/group.json Normal file
View file

@ -0,0 +1,57 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Version": {
"description": "Mod Container version, currently unused.",
"type": "integer"
},
"Name": {
"description": "Name of the group.",
"type": "string",
"minLength": 1
},
"Description": {
"description": "Description of the group.",
"type": [ "string", "null" ]
},
"Image": {
"description": "Relative path to a preview image for the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.",
"type": ["string", "null" ]
},
"Page": {
"description": "TexTools page of the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.",
"type": "integer"
},
"Priority": {
"description": "Priority of the group. If several groups define conflicting files or manipulations, the highest priority wins.",
"type": "integer"
},
"Type": {
"description": "Group type. Single groups have one and only one of their options active at any point. Multi groups can have zero, one or many of their options active. Combining groups have n options, 2^n containers, and will have one and only one container active depending on the selected options.",
"enum": [ "Single", "Multi", "Imc", "Combining" ]
},
"DefaultSettings": {
"description": "Default configuration for the group.",
"type": "integer"
}
},
"required": [
"Name",
"Type"
],
"oneOf": [
{
"$ref": "structs/group_combining.json"
},
{
"$ref": "structs/group_imc.json"
},
{
"$ref": "structs/group_multi.json"
},
{
"$ref": "structs/group_single.json"
}
]
}

View file

@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Local Penumbra Mod Data",
"description": "The locally stored data for an installed mod in Penumbra",
"type": "object",
"properties": {
"FileVersion": {
"description": "Major version of the local data schema used.",
"type": "integer",
"minimum": 3,
"maximum": 3
},
"ImportDate": {
"description": "The date and time of the installation of the mod as a Unix Epoch millisecond timestamp.",
"type": "integer"
},
"LocalTags": {
"description": "User-defined local tags for the mod.",
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
},
"Favorite": {
"description": "Whether the mod is favourited by the user.",
"type": "boolean"
}
},
"required": [ "FileVersion" ]
}

52
schemas/mod_meta-v3.json Normal file
View file

@ -0,0 +1,52 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Penumbra Mod Metadata",
"description": "Metadata of a Penumbra mod.",
"type": "object",
"properties": {
"FileVersion": {
"description": "Major version of the metadata schema used.",
"type": "integer",
"minimum": 3,
"maximum": 3
},
"Name": {
"description": "Name of the mod.",
"type": "string",
"minLength": 1
},
"Author": {
"description": "Author of the mod.",
"type": [ "string", "null" ]
},
"Description": {
"description": "Description of the mod. Can span multiple paragraphs.",
"type": [ "string", "null" ]
},
"Image": {
"description": "Relative path to a preview image for the mod. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.",
"type": [ "string", "null" ]
},
"Version": {
"description": "Version of the mod. Can be an arbitrary string.",
"type": [ "string", "null" ]
},
"Website": {
"description": "URL of the web page of the mod.",
"type": [ "string", "null" ]
},
"ModTags": {
"description": "Author-defined tags for the mod.",
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
}
},
"required": [
"FileVersion",
"Name"
]
}

499
schemas/shpk_devkit.json Normal file
View file

@ -0,0 +1,499 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"ShaderKeys": {
"type": "object",
"patternProperties": {
"^\\d+$": {
"$ref": "#/$defs/ShaderKey"
}
},
"additionalProperties": false
},
"Comment": {
"$ref": "#/$defs/MayVary<string>"
},
"Samplers": {
"type": "object",
"patternProperties": {
"^\\d+$": {
"$ref": "#/$defs/MayVary<Sampler>"
}
},
"additionalProperties": false
},
"Constants": {
"type": "object",
"patternProperties": {
"^\\d+$": {
"$ref": "#/$defs/MayVary<Constant[]>"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"$defs": {
"ShaderKeyValue": {
"type": "object",
"properties": {
"Label": {
"type": "string"
},
"Description": {
"type": "string"
}
},
"additionalProperties": false
},
"ShaderKey": {
"type": "object",
"properties": {
"Label": {
"type": "string"
},
"Description": {
"type": "string"
},
"Values": {
"type": "object",
"patternProperties": {
"^\\d+$": {
"$ref": "#/$defs/ShaderKeyValue"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"Varying<any>": {
"type": "object",
"properties": {
"Vary": {
"type": "array",
"items": {
"$ref": "#/$defs/LaxInteger"
}
},
"Selectors": {
"description": "Keys are Σ 31^i shaderKey(Vary[i]).",
"type": "object",
"patternProperties": {
"^\\d+$": {
"type": "integer"
}
},
"additionalProperties": false
},
"Items": {
"type": "array",
"$comment": "Varying<T> is defined by constraining this array's items to T"
}
},
"required": [
"Vary",
"Selectors",
"Items"
],
"additionalProperties": false
},
"MayVary<string>": {
"oneOf": [
{
"type": ["string", "null"]
}, {
"allOf": [
{
"$ref": "#/$defs/Varying<any>"
}, {
"type": "object",
"properties": {
"Items": {
"type": "array",
"items": {
"type": ["string", "null"]
}
}
}
}
]
}
]
},
"Sampler": {
"type": ["object", "null"],
"properties": {
"Label": {
"type": "string"
},
"Description": {
"type": "string"
},
"DefaultTexture": {
"type": "string",
"pattern": "^[^/\\\\][^\\\\]*$"
}
},
"additionalProperties": false
},
"MayVary<Sampler>": {
"oneOf": [
{
"$ref": "#/$defs/Sampler"
}, {
"allOf": [
{
"$ref": "#/$defs/Varying<any>"
}, {
"type": "object",
"properties": {
"Items": {
"type": "array",
"items": {
"$ref": "#/$defs/Sampler"
}
}
}
}
]
}
]
},
"ConstantBase": {
"type": "object",
"properties": {
"Offset": {
"description": "Defaults to 0. Mutually exclusive with ByteOffset.",
"type": "integer",
"minimum": 0
},
"Length": {
"description": "Defaults to up to the end. Mutually exclusive with ByteLength.",
"type": "integer",
"minimum": 0
},
"ByteOffset": {
"description": "Defaults to 0. Mutually exclusive with Offset.",
"type": "integer",
"minimum": 0
},
"ByteLength": {
"description": "Defaults to up to the end. Mutually exclusive with Length.",
"type": "integer",
"minimum": 0
},
"Group": {
"description": "Defaults to \"Further Constants\".",
"type": "string"
},
"Label": {
"type": "string"
},
"Description": {
"description": "Defaults to empty.",
"type": "string"
},
"Type": {
"description": "Defaults to Float.",
"enum": ["Hidden", "Float", "Integer", "Color", "Enum", "Int32", "Int32Enum", "Int8", "Int8Enum", "Int16", "Int16Enum", "Int64", "Int64Enum", "Half", "Double", "TileIndex", "SphereMapIndex"]
}
},
"not": {
"anyOf": [
{
"required": ["Offset", "ByteOffset"]
}, {
"required": ["Length", "ByteLenngth"]
}
]
}
},
"HiddenConstant": {
"type": "object",
"properties": {
"Type": {
"const": "Hidden"
}
},
"required": [
"Type"
],
"allOf": [
{
"$ref": "#/$defs/ConstantBase"
}
],
"unevaluatedProperties": false
},
"FloatConstant": {
"type": "object",
"properties": {
"Type": {
"enum": ["Float", "Half", "Double"]
},
"Minimum": {
"description": "Defaults to -∞.",
"type": "number"
},
"Maximum": {
"description": "Defaults to ∞.",
"type": "number"
},
"Speed": {
"description": "Defaults to 0.1.",
"type": "number",
"minimum": 0
},
"RelativeSpeed": {
"description": "Defaults to 0.",
"type": "number",
"minimum": 0
},
"Exponent": {
"description": "Defaults to 1. Uses an odd pseudo-power function, f(x) = sgn(x) |x|^n.",
"type": "number"
},
"Factor": {
"description": "Defaults to 1.",
"type": "number"
},
"Bias": {
"description": "Defaults to 0.",
"type": "number"
},
"Precision": {
"description": "Defaults to 3.",
"type": "integer",
"minimum": 0,
"maximum": 9
},
"Slider": {
"description": "Defaults to true. Drag has priority over this.",
"type": "boolean"
},
"Drag": {
"description": "Defaults to true. Has priority over Slider.",
"type": "boolean"
},
"Unit": {
"description": "Defaults to no unit.",
"type": "string"
}
},
"required": [
"Label"
],
"allOf": [
{
"$ref": "#/$defs/ConstantBase"
}
],
"unevaluatedProperties": false
},
"IntConstant": {
"type": "object",
"properties": {
"Type": {
"enum": ["Integer", "Int32", "Int8", "Int16", "Int64"]
},
"Minimum": {
"description": "Defaults to -2^N, N being the explicit integer width specified in the type, or 32 for Int.",
"type": "number"
},
"Maximum": {
"description": "Defaults to 2^N - 1, N being the explicit integer width specified in the type, or 32 for Int.",
"type": "number"
},
"Speed": {
"description": "Defaults to 0.25.",
"type": "number",
"minimum": 0
},
"RelativeSpeed": {
"description": "Defaults to 0.",
"type": "number",
"minimum": 0
},
"Factor": {
"description": "Defaults to 1.",
"type": "number"
},
"Bias": {
"description": "Defaults to 0.",
"type": "number"
},
"Hex": {
"description": "Defaults to false. Has priority over Slider and Drag.",
"type": "boolean"
},
"Slider": {
"description": "Defaults to true. Hex and Drag have priority over this.",
"type": "boolean"
},
"Drag": {
"description": "Defaults to true. Has priority over Slider, but Hex has priority over this.",
"type": "boolean"
},
"Unit": {
"description": "Defaults to no unit.",
"type": "string"
}
},
"required": [
"Label",
"Type"
],
"allOf": [
{
"$ref": "#/$defs/ConstantBase"
}
],
"unevaluatedProperties": false
},
"ColorConstant": {
"type": "object",
"properties": {
"Type": {
"const": "Color"
},
"SquaredRgb": {
"description": "Defaults to false. Uses an odd pseudo-square function, f(x) = sgn(x) |x|².",
"type": "boolean"
},
"Clamped": {
"description": "Defaults to false.",
"type": "boolean"
}
},
"required": [
"Label",
"Type"
],
"allOf": [
{
"$ref": "#/$defs/ConstantBase"
}
],
"unevaluatedProperties": false
},
"EnumValue": {
"type": "object",
"properties": {
"Label": {
"type": "string"
},
"Description": {
"type": "string"
},
"Value": {
"type": "number"
}
},
"required": [
"Label",
"Value"
],
"additionalProperties": false
},
"EnumConstant": {
"type": "object",
"properties": {
"Type": {
"enum": ["Enum", "Int32Enum", "Int8Enum", "Int16Enum", "Int64Enum"]
},
"Values": {
"type": "array",
"items": {
"$ref": "#/$defs/EnumValue"
}
}
},
"required": [
"Label",
"Type"
],
"allOf": [
{
"$ref": "#/$defs/ConstantBase"
}
],
"unevaluatedProperties": false
},
"SpecialConstant": {
"type": "object",
"properties": {
"Type": {
"enum": ["TileIndex", "SphereMapIndex"]
}
},
"required": [
"Label",
"Type"
],
"allOf": [
{
"$ref": "#/$defs/ConstantBase"
}
],
"unevaluatedProperties": false
},
"Constant": {
"oneOf": [
{
"$ref": "#/$defs/HiddenConstant"
}, {
"$ref": "#/$defs/FloatConstant"
}, {
"$ref": "#/$defs/IntConstant"
}, {
"$ref": "#/$defs/ColorConstant"
}, {
"$ref": "#/$defs/EnumConstant"
}, {
"$ref": "#/$defs/SpecialConstant"
}
]
},
"MayVary<Constant[]>": {
"oneOf": [
{
"type": ["array", "null"],
"items": {
"$ref": "#/$defs/Constant"
}
}, {
"allOf": [
{
"$ref": "#/$defs/Varying<any>"
}, {
"type": "object",
"properties": {
"Items": {
"type": "array",
"items": {
"type": ["array", "null"],
"items": {
"$ref": "#/$defs/Constant"
}
}
}
}
}
]
}
]
},
"LaxInteger": {
"oneOf": [
{
"type": "integer"
}, {
"type": "string",
"pattern": "^\\d+$"
}
]
}
}
}

View file

@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Files": {
"description": "File redirections in this container. Keys are game paths, values are relative file paths.",
"type": [ "object", "null" ],
"patternProperties": {
"^[^/\\\\.:?][^\\\\?:]+$": {
"type": "string",
"pattern": "^[^/\\\\.:?][^?:]+$"
}
},
"additionalProperties": false
},
"FileSwaps": {
"description": "File swaps in this container. Keys are original game paths, values are actual game paths.",
"type": [ "object", "null" ],
"patternProperties": {
"^[^/\\\\.?:][^\\\\?:]+$": {
"type": "string",
"pattern": "^[^/\\\\.:?][^?:]+$"
}
},
"additionalProperties": false
},
"Manipulations": {
"type": [ "array", "null" ],
"items": {
"$ref": "manipulation.json"
}
}
}
}

View file

@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"Type": {
"const": "Combining"
},
"Options": {
"type": "array",
"items": {
"$ref": "option.json"
}
},
"Containers": {
"type": "array",
"items": {
"allOf": [
{
"$ref": "container.json"
},
{
"properties": {
"Name": {
"type": [ "string", "null" ]
}
}
}
]
}
}
}
}

View file

@ -0,0 +1,50 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"Type": {
"const": "Imc"
},
"AllVariants": {
"type": "boolean"
},
"OnlyAttributes": {
"type": "boolean"
},
"Identifier": {
"$ref": "meta_imc.json#ImcIdentifier"
},
"DefaultEntry": {
"$ref": "meta_imc.json#ImcEntry"
},
"Options": {
"type": "array",
"items": {
"$ref": "option.json",
"oneOf": [
{
"properties": {
"AttributeMask": {
"type": "integer",
"minimum": 0,
"maximum": 1023
}
},
"required": [
"AttributeMask"
]
},
{
"properties": {
"IsDisableSubMod": {
"const": true
}
},
"required": [
"IsDisableSubMod"
]
}
]
}
}
}
}

View file

@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Type": {
"const": "Multi"
},
"Options": {
"type": "array",
"items": {
"allOf": [
{
"$ref": "option.json"
},
{
"$ref": "container.json"
},
{
"properties": {
"Priority": {
"type": "integer"
}
}
}
]
}
}
}
}

View file

@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Type": {
"const": "Single"
},
"Options": {
"type": "array",
"items": {
"allOf": [
{
"$ref": "option.json"
},
{
"$ref": "container.json"
}
]
}
}
}
}

View file

@ -0,0 +1,95 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Type": {
"enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch" ]
},
"Manipulation": {
"type": "object"
}
},
"required": [ "Type", "Manipulation" ],
"oneOf": [
{
"properties": {
"Type": {
"const": "Imc"
},
"Manipulation": {
"$ref": "meta_imc.json"
}
}
},
{
"properties": {
"Type": {
"const": "Eqdp"
},
"Manipulation": {
"$ref": "meta_eqdp.json"
}
}
},
{
"properties": {
"Type": {
"const": "Eqp"
},
"Manipulation": {
"$ref": "meta_eqp.json"
}
}
},
{
"properties": {
"Type": {
"const": "Est"
},
"Manipulation": {
"$ref": "meta_est.json"
}
}
},
{
"properties": {
"Type": {
"const": "Gmp"
},
"Manipulation": {
"$ref": "meta_gmp.json"
}
}
},
{
"properties": {
"Type": {
"const": "Rsp"
},
"Manipulation": {
"$ref": "meta_rsp.json"
}
}
},
{
"properties": {
"Type": {
"const": "GlobalEqp"
},
"Manipulation": {
"$ref": "meta_geqp.json"
}
}
},
{
"properties": {
"Type": {
"const": "Atch"
},
"Manipulation": {
"$ref": "meta_atch.json"
}
}
}
]
}

View file

@ -0,0 +1,67 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Entry": {
"type": "object",
"properties": {
"Bone": {
"type": "string",
"maxLength": 34
},
"Scale": {
"type": "number"
},
"OffsetX": {
"type": "number"
},
"OffsetY": {
"type": "number"
},
"OffsetZ": {
"type": "number"
},
"RotationX": {
"type": "number"
},
"RotationY": {
"type": "number"
},
"RotationZ": {
"type": "number"
}
},
"required": [
"Bone",
"Scale",
"OffsetX",
"OffsetY",
"OffsetZ",
"RotationX",
"RotationY",
"RotationZ"
]
},
"Gender": {
"$ref": "meta_enums.json#Gender"
},
"Race": {
"$ref": "meta_enums.json#ModelRace"
},
"Type": {
"type": "string",
"minLength": 1,
"maxLength": 4
},
"Index": {
"$ref": "meta_enums.json#U16"
}
},
"required": [
"Entry",
"Gender",
"Race",
"Type",
"Index"
]
}

View file

@ -0,0 +1,57 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"EquipSlot": {
"$anchor": "EquipSlot",
"enum": [ "Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All" ]
},
"Gender": {
"$anchor": "Gender",
"enum": [ "Unknown", "Male", "Female", "MaleNpc", "FemaleNpc" ]
},
"ModelRace": {
"$anchor": "ModelRace",
"enum": [ "Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera" ]
},
"ObjectType": {
"$anchor": "ObjectType",
"enum": [ "Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font" ]
},
"BodySlot": {
"$anchor": "BodySlot",
"enum": [ "Unknown", "Hair", "Face", "Tail", "Body", "Zear" ]
},
"SubRace": {
"$anchor": "SubRace",
"enum": [ "Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena" ]
},
"U8": {
"$anchor": "U8",
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 255
},
{
"type": "string",
"pattern": "^0*(1[0-9][0-9]|[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$"
}
]
},
"U16": {
"$anchor": "U16",
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 65535
},
{
"type": "string",
"pattern": "^0*([1-5][0-9]{4}|[0-9]{0,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"
}
]
}
}
}

View file

@ -0,0 +1,30 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Entry": {
"type": "integer",
"minimum": 0,
"maximum": 1023
},
"Gender": {
"$ref": "meta_enums.json#Gender"
},
"Race": {
"$ref": "meta_enums.json#ModelRace"
},
"SetId": {
"$ref": "meta_enums.json#U16"
},
"Slot": {
"$ref": "meta_enums.json#EquipSlot"
}
},
"required": [
"Entry",
"Gender",
"Race",
"SetId",
"Slot"
]
}

View file

@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Entry": {
"type": "integer"
},
"SetId": {
"$ref": "meta_enums.json#U16"
},
"Slot": {
"$ref": "meta_enums.json#EquipSlot"
}
},
"required": [
"Entry",
"SetId",
"Slot"
]
}

View file

@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Entry": {
"$ref": "meta_enums.json#U16"
},
"Gender": {
"$ref": "meta_enums.json#Gender"
},
"Race": {
"$ref": "meta_enums.json#ModelRace"
},
"SetId": {
"$ref": "meta_enums.json#U16"
},
"Slot": {
"enum": [ "Hair", "Face", "Body", "Head" ]
}
},
"required": [
"Entry",
"Gender",
"Race",
"SetId",
"Slot"
]
}

View file

@ -0,0 +1,40 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Condition": {
"$ref": "meta_enums.json#U16"
},
"Type": {
"enum": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats" ]
}
},
"required": [ "Type" ],
"oneOf": [
{
"properties": {
"Type": {
"const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ]
},
"Condition": {
"const": 0
}
}
},
{
"properties": {
"Type": {
"const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ]
}
}
},
{
"properties": {
"Type": {
"const": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL" ]
},
"Condition": {}
}
}
]
}

View file

@ -0,0 +1,59 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Entry": {
"type": "object",
"properties": {
"Enabled": {
"type": "boolean"
},
"Animated": {
"type": "boolean"
},
"RotationA": {
"type": "integer",
"minimum": 0,
"maximum": 1023
},
"RotationB": {
"type": "integer",
"minimum": 0,
"maximum": 1023
},
"RotationC": {
"type": "integer",
"minimum": 0,
"maximum": 1023
},
"UnknownA": {
"type": "integer",
"minimum": 0,
"maximum": 15
},
"UnknownB": {
"type": "integer",
"minimum": 0,
"maximum": 15
}
},
"required": [
"Enabled",
"Animated",
"RotationA",
"RotationB",
"RotationC",
"UnknownA",
"UnknownB"
],
"additionalProperties": false
},
"SetId": {
"$ref": "meta_enums.json#U16"
}
},
"required": [
"Entry",
"SetId"
]
}

View file

@ -0,0 +1,87 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Entry": {
"$ref": "#ImcEntry"
}
},
"required": [
"Entry"
],
"allOf": [
{
"$ref": "#ImcIdentifier"
}
],
"$defs": {
"ImcIdentifier": {
"type": "object",
"properties": {
"PrimaryId": {
"$ref": "meta_enums.json#U16"
},
"SecondaryId": {
"$ref": "meta_enums.json#U16"
},
"Variant": {
"$ref": "meta_enums.json#U8"
},
"ObjectType": {
"$ref": "meta_enums.json#ObjectType"
},
"EquipSlot": {
"$ref": "meta_enums.json#EquipSlot"
},
"BodySlot": {
"$ref": "meta_enums.json#BodySlot"
}
},
"$anchor": "ImcIdentifier",
"required": [
"PrimaryId",
"SecondaryId",
"Variant",
"ObjectType",
"EquipSlot",
"BodySlot"
]
},
"ImcEntry": {
"type": "object",
"properties": {
"MaterialId": {
"$ref": "meta_enums.json#U8"
},
"DecalId": {
"$ref": "meta_enums.json#U8"
},
"VfxId": {
"$ref": "meta_enums.json#U8"
},
"MaterialAnimationId": {
"$ref": "meta_enums.json#U8"
},
"AttributeMask": {
"type": "integer",
"minimum": 0,
"maximum": 1023
},
"SoundId": {
"type": "integer",
"minimum": 0,
"maximum": 63
}
},
"$anchor": "ImcEntry",
"required": [
"MaterialId",
"DecalId",
"VfxId",
"MaterialAnimationId",
"AttributeMask",
"SoundId"
]
}
}
}

View file

@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Entry": {
"type": "number"
},
"SubRace": {
"$ref": "meta_enums.json#SubRace"
},
"Attribute": {
"enum": [ "MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ" ]
}
},
"required": [
"Entry",
"SubRace",
"Attribute"
]
}

View file

@ -0,0 +1,24 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"Name": {
"description": "Name of the option.",
"type": "string",
"minLength": 1
},
"Description": {
"description": "Description of the option.",
"type": [ "string", "null" ]
},
"Priority": {
"description": "Priority of the option. If several enabled options within the group define conflicting files or manipulations, the highest priority wins.",
"type": "integer"
},
"Image": {
"description": "Unused by Penumbra.",
"type": [ "string", "null" ]
}
},
"required": [ "Name" ]
}