From bdc2da95c4ece4ce36b99fba9f1b9fea11b83251 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Jan 2025 17:22:25 +0100 Subject: [PATCH 01/10] Make mods write empty containers again for now. --- Penumbra.GameData | 2 +- Penumbra/Mods/SubMods/SubMod.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index d5f92966..78ce195c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37 +Subproject commit 78ce195c171d7bce4ad9df105f1f95cce9bf1150 diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index 7f01884d..fcb6cc0e 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -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); - } + //} } /// Write the data for a selectable mod option on a JsonWriter. From df148b556a8a8b2f90b700a01a9ac0622ace3e31 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 16 Jan 2025 16:25:18 +0000 Subject: [PATCH 02/10] [CI] Updating repo.json for testing_1.3.3.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index d9d597ba..2e3dd8bc 100644 --- a/repo.json +++ b/repo.json @@ -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" } From a1931a93fb4bfad042b1235ec62270f4c74e3fbc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 01:45:37 +0100 Subject: [PATCH 03/10] Add drafts of JSON schemas --- schemas/container.json | 486 +++++++++++++++++++++++++++++++++++++++ schemas/default_mod.json | 25 ++ schemas/group.json | 206 +++++++++++++++++ schemas/meta-v3.json | 51 ++++ 4 files changed, 768 insertions(+) create mode 100644 schemas/container.json create mode 100644 schemas/default_mod.json create mode 100644 schemas/group.json create mode 100644 schemas/meta-v3.json diff --git a/schemas/container.json b/schemas/container.json new file mode 100644 index 00000000..5798f46c --- /dev/null +++ b/schemas/container.json @@ -0,0 +1,486 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", + "type": "object", + "properties": { + "Name": { + "description": "Name of the container/option/sub-mod.", + "type": ["string", "null"] + }, + "Files": { + "description": "File redirections in this container. Keys are game paths, values are relative file paths.", + "type": "object", + "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", + "patternProperties": { + "^[^/\\\\][^\\\\]*$": { + "type": "string", + "pattern": "^[^/\\\\][^/]*$" + } + }, + "additionalProperties": false + }, + "Manipulations": { + "type": "array", + "items": { + "$ref": "#/$defs/Manipulation" + } + } + }, + "$defs": { + "Manipulation": { + "type": "object", + "properties": { + "Type": { + "enum": ["Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch"] + }, + "Manipulation": { + "type": ["object", "null"] + } + }, + "required": ["Type", "Manipulation"], + "oneOf": [ + { + "properties": { + "Type": { + "const": "Unknown" + }, + "Manipulation": { + "type": "null" + } + } + }, { + "properties": { + "Type": { + "const": "Imc" + }, + "Manipulation": { + "$ref": "#/$defs/ImcManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Eqdp" + }, + "Manipulation": { + "$ref": "#/$defs/EqdpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Eqp" + }, + "Manipulation": { + "$ref": "#/$defs/EqpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Est" + }, + "Manipulation": { + "$ref": "#/$defs/EstManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Gmp" + }, + "Manipulation": { + "$ref": "#/$defs/GmpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Rsp" + }, + "Manipulation": { + "$ref": "#/$defs/RspManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "GlobalEqp" + }, + "Manipulation": { + "$ref": "#/$defs/GlobalEqpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Atch" + }, + "Manipulation": { + "$ref": "#/$defs/AtchManipulation" + } + } + } + ], + "additionalProperties": false + }, + "ImcManipulation": { + "type": "object", + "properties": { + "Entry": { + "$ref": "#/$defs/ImcEntry" + }, + "Valid": { + "type": "boolean" + } + }, + "required": [ + "Entry" + ], + "allOf": [ + { + "$ref": "#/$defs/ImcIdentifier" + } + ], + "unevaluatedProperties": false + }, + "ImcIdentifier": { + "type": "object", + "properties": { + "PrimaryId": { + "type": "integer" + }, + "SecondaryId": { + "type": "integer" + }, + "Variant": { + "type": "integer" + }, + "ObjectType": { + "$ref": "#/$defs/ObjectType" + }, + "EquipSlot": { + "$ref": "#/$defs/EquipSlot" + }, + "BodySlot": { + "$ref": "#/$defs/BodySlot" + } + }, + "required": [ + "PrimaryId", + "SecondaryId", + "Variant", + "ObjectType", + "EquipSlot", + "BodySlot" + ] + }, + "ImcEntry": { + "type": "object", + "properties": { + "AttributeAndSound": { + "type": "integer" + }, + "MaterialId": { + "type": "integer" + }, + "DecalId": { + "type": "integer" + }, + "VfxId": { + "type": "integer" + }, + "MaterialAnimationId": { + "type": "integer" + }, + "AttributeMask": { + "type": "integer" + }, + "SoundId": { + "type": "integer" + } + }, + "required": [ + "MaterialId", + "DecalId", + "VfxId", + "MaterialAnimationId", + "AttributeMask", + "SoundId" + ], + "additionalProperties": false + }, + "EqdpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "$ref": "#/$defs/EquipSlot" + }, + "ShiftedEntry": { + "type": "integer" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "EqpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "$ref": "#/$defs/EquipSlot" + } + }, + "required": [ + "Entry", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "EstManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "enum": ["Hair", "Face", "Body", "Head"] + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "GmpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + }, + "Animated": { + "type": "boolean" + }, + "RotationA": { + "type": "number" + }, + "RotationB": { + "type": "number" + }, + "RotationC": { + "type": "number" + }, + "UnknownA": { + "type": "number" + }, + "UnknownB": { + "type": "number" + }, + "UnknownTotal": { + "type": "number" + }, + "Value": { + "type": "number" + } + }, + "required": [ + "Enabled", + "Animated", + "RotationA", + "RotationB", + "RotationC", + "UnknownA", + "UnknownB", + "UnknownTotal", + "Value" + ], + "additionalProperties": false + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + } + }, + "required": [ + "Entry", + "SetId" + ], + "additionalProperties": false + }, + "RspManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "number" + }, + "SubRace": { + "$ref": "#/$defs/SubRace" + }, + "Attribute": { + "$ref": "#/$defs/RspAttribute" + } + }, + "additionalProperties": false + }, + "GlobalEqpManipulation": { + "type": "object", + "properties": { + "Condition": { + "type": "integer" + }, + "Type": { + "enum": ["DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats"] + } + }, + "additionalProperties": false + }, + "AtchManipulation": { + "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" + ], + "additionalProperties": false + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "Type": { + "type": "string", + "minLength": 3, + "maxLength": 3 + }, + "Index": { + "type": "integer" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "Type", + "Index" + ], + "additionalProperties": false + }, + "LaxInteger": { + "oneOf": [ + { + "type": "integer" + }, { + "type": "string", + "pattern": "^\\d+$" + } + ] + }, + "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": { + "enum": ["Unknown", "Male", "Female", "MaleNpc", "FemaleNpc"] + }, + "ModelRace": { + "enum": ["Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera"] + }, + "ObjectType": { + "enum": ["Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font"] + }, + "BodySlot": { + "enum": ["Unknown", "Hair", "Face", "Tail", "Body", "Zear"] + }, + "SubRace": { + "enum": ["Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena"] + }, + "RspAttribute": { + "enum": ["MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ"] + } + } +} diff --git a/schemas/default_mod.json b/schemas/default_mod.json new file mode 100644 index 00000000..eecd74d0 --- /dev/null +++ b/schemas/default_mod.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", + "allOf": [ + { + "type": "object", + "properties": { + "Version": { + "description": "???", + "type": ["integer", "null"] + }, + "Description": { + "description": "Description of the sub-mod.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] +} diff --git a/schemas/group.json b/schemas/group.json new file mode 100644 index 00000000..0078e9f3 --- /dev/null +++ b/schemas/group.json @@ -0,0 +1,206 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/group.json", + "type": "object", + "properties": { + "Version": { + "description": "???", + "type": ["integer", "null"] + }, + "Name": { + "description": "Name of the group.", + "type": "string" + }, + "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", "null"] + }, + "Priority": { + "description": "Priority of the group. If several groups define conflicting files or manipulations, the highest priority wins.", + "type": ["integer", "null"] + }, + "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": [ + { + "properties": { + "Type": { + "const": "Single" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] + } + } + } + }, { + "properties": { + "Type": { + "const": "Multi" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "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", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] + } + } + } + }, { + "properties": { + "Type": { + "const": "Imc" + }, + "AllVariants": { + "type": "boolean" + }, + "OnlyAttributes": { + "type": "boolean" + }, + "Identifier": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcIdentifier" + }, + "DefaultEntry": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcEntry" + }, + "Options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string" + }, + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + }, + "required": [ + "Name" + ], + "oneOf": [ + { + "properties": { + "AttributeMask": { + "type": "integer" + } + }, + "required": [ + "AttributeMask" + ] + }, { + "properties": { + "IsDisableSubMod": { + "const": true + } + }, + "required": [ + "IsDisableSubMod" + ] + } + ], + "unevaluatedProperties": false + } + } + } + }, { + "properties": { + "Type": { + "const": "Combining" + }, + "Options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string" + }, + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + }, + "required": [ + "Name" + ], + "additionalProperties": false + } + }, + "Containers": { + "type": "array", + "items": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/meta-v3.json b/schemas/meta-v3.json new file mode 100644 index 00000000..1a132264 --- /dev/null +++ b/schemas/meta-v3.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/meta-v3.json", + "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" + }, + "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" + }, + "uniqueItems": true + } + }, + "required": [ + "FileVersion", + "Name" + ] +} From 3b8aac8eca94e8253b357a8b885a56bb7919e7d2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 18:50:00 +0100 Subject: [PATCH 04/10] Add schema for Material Development Kit files --- schemas/shpk_devkit.json | 500 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 schemas/shpk_devkit.json diff --git a/schemas/shpk_devkit.json b/schemas/shpk_devkit.json new file mode 100644 index 00000000..cd18ab81 --- /dev/null +++ b/schemas/shpk_devkit.json @@ -0,0 +1,500 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/shpk_devkit.json", + "type": "object", + "properties": { + "ShaderKeys": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/ShaderKey" + } + }, + "additionalProperties": false + }, + "Comment": { + "$ref": "#/$defs/MayVary" + }, + "Samplers": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/MayVary" + } + }, + "additionalProperties": false + }, + "Constants": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/MayVary" + } + }, + "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": { + "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 is defined by constraining this array's items to T" + } + }, + "required": [ + "Vary", + "Selectors", + "Items" + ], + "additionalProperties": false + }, + "MayVary": { + "oneOf": [ + { + "type": ["string", "null"] + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "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": { + "oneOf": [ + { + "$ref": "#/$defs/Sampler" + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "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": { + "oneOf": [ + { + "type": ["array", "null"], + "items": { + "$ref": "#/$defs/Constant" + } + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "type": ["array", "null"], + "items": { + "$ref": "#/$defs/Constant" + } + } + } + } + } + ] + } + ] + }, + "LaxInteger": { + "oneOf": [ + { + "type": "integer" + }, { + "type": "string", + "pattern": "^\\d+$" + } + ] + } + } +} From 5f8377acaaf1bc20944af85e525b09313285272c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jan 2025 19:54:40 +0100 Subject: [PATCH 05/10] Update mod loading structure. --- Penumbra.GameData | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 145 +------------------------ Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Mods/ModLocalData.cs | 57 ++++++++++ Penumbra/Mods/ModMeta.cs | 83 ++++++++++++++ 5 files changed, 146 insertions(+), 145 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 78ce195c..c5250722 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 78ce195c171d7bce4ad9df105f1f95cce9bf1150 +Subproject commit c525072299d5febd2bb638ab229060b0073ba6a6 diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 162f823d..7c48205a 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -27,6 +27,9 @@ public enum ModDataChangeType : ushort public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService { + public SaveService SaveService + => saveService; + /// Create the file containing the meta information about a mod from scratch. 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(); - 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() ?? importDate; - favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; - note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? 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.Empty; - var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; - var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; - var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; - var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; - var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; - var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; - var importDate = json[nameof(Mod.ImportDate)]?.Value(); - var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); - - 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) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 18d2bc09..0db83ef9 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -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) diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index beda0dc7..d3534391 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -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(); + 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() ?? importDate; + favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; + note = json[nameof(Mod.Note)]?.Value() ?? note; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? 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? newModTags, IEnumerable? newLocalTags) { if (newModTags == null && newLocalTags == null) diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 39dd20e4..0cebcf81 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -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() ?? 0; + + // Empty name gets checked after loading and is not allowed. + var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; + + var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; + var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; + var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; + var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; + var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; + var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); + + 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; + } + } } From ec3ec7db4e686318c9d7c2ee7a3ded8cc6357d4f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jan 2025 19:55:02 +0100 Subject: [PATCH 06/10] update schema organization anf change some things. --- Penumbra.sln | 32 ++ schemas/container.json | 486 --------------------- schemas/default_mod.json | 20 +- schemas/group.json | 189 +------- schemas/local_mod_data-v3.json | 32 ++ schemas/{meta-v3.json => mod_meta-v3.json} | 17 +- schemas/structs/container.json | 34 ++ schemas/structs/group_combining.json | 31 ++ schemas/structs/group_imc.json | 50 +++ schemas/structs/group_multi.json | 32 ++ schemas/structs/group_single.json | 22 + schemas/structs/manipulation.json | 95 ++++ schemas/structs/meta_atch.json | 67 +++ schemas/structs/meta_enums.json | 57 +++ schemas/structs/meta_eqdp.json | 30 ++ schemas/structs/meta_eqp.json | 20 + schemas/structs/meta_est.json | 28 ++ schemas/structs/meta_geqp.json | 40 ++ schemas/structs/meta_gmp.json | 59 +++ schemas/structs/meta_imc.json | 87 ++++ schemas/structs/meta_rsp.json | 20 + schemas/structs/option.json | 24 + 22 files changed, 796 insertions(+), 676 deletions(-) delete mode 100644 schemas/container.json create mode 100644 schemas/local_mod_data-v3.json rename schemas/{meta-v3.json => mod_meta-v3.json} (79%) create mode 100644 schemas/structs/container.json create mode 100644 schemas/structs/group_combining.json create mode 100644 schemas/structs/group_imc.json create mode 100644 schemas/structs/group_multi.json create mode 100644 schemas/structs/group_single.json create mode 100644 schemas/structs/manipulation.json create mode 100644 schemas/structs/meta_atch.json create mode 100644 schemas/structs/meta_enums.json create mode 100644 schemas/structs/meta_eqdp.json create mode 100644 schemas/structs/meta_eqp.json create mode 100644 schemas/structs/meta_est.json create mode 100644 schemas/structs/meta_geqp.json create mode 100644 schemas/structs/meta_gmp.json create mode 100644 schemas/structs/meta_imc.json create mode 100644 schemas/structs/meta_rsp.json create mode 100644 schemas/structs/option.json diff --git a/Penumbra.sln b/Penumbra.sln index 94a04ef3..c0b38118 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -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\files\default_mod.json = schemas\files\default_mod.json + schemas\files\group.json = schemas\files\group.json + schemas\files\local_mod_data-v3.json = schemas\files\local_mod_data-v3.json + schemas\files\mod_meta-v3.json = schemas\files\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 diff --git a/schemas/container.json b/schemas/container.json deleted file mode 100644 index 5798f46c..00000000 --- a/schemas/container.json +++ /dev/null @@ -1,486 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", - "type": "object", - "properties": { - "Name": { - "description": "Name of the container/option/sub-mod.", - "type": ["string", "null"] - }, - "Files": { - "description": "File redirections in this container. Keys are game paths, values are relative file paths.", - "type": "object", - "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", - "patternProperties": { - "^[^/\\\\][^\\\\]*$": { - "type": "string", - "pattern": "^[^/\\\\][^/]*$" - } - }, - "additionalProperties": false - }, - "Manipulations": { - "type": "array", - "items": { - "$ref": "#/$defs/Manipulation" - } - } - }, - "$defs": { - "Manipulation": { - "type": "object", - "properties": { - "Type": { - "enum": ["Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch"] - }, - "Manipulation": { - "type": ["object", "null"] - } - }, - "required": ["Type", "Manipulation"], - "oneOf": [ - { - "properties": { - "Type": { - "const": "Unknown" - }, - "Manipulation": { - "type": "null" - } - } - }, { - "properties": { - "Type": { - "const": "Imc" - }, - "Manipulation": { - "$ref": "#/$defs/ImcManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Eqdp" - }, - "Manipulation": { - "$ref": "#/$defs/EqdpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Eqp" - }, - "Manipulation": { - "$ref": "#/$defs/EqpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Est" - }, - "Manipulation": { - "$ref": "#/$defs/EstManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Gmp" - }, - "Manipulation": { - "$ref": "#/$defs/GmpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Rsp" - }, - "Manipulation": { - "$ref": "#/$defs/RspManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "GlobalEqp" - }, - "Manipulation": { - "$ref": "#/$defs/GlobalEqpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Atch" - }, - "Manipulation": { - "$ref": "#/$defs/AtchManipulation" - } - } - } - ], - "additionalProperties": false - }, - "ImcManipulation": { - "type": "object", - "properties": { - "Entry": { - "$ref": "#/$defs/ImcEntry" - }, - "Valid": { - "type": "boolean" - } - }, - "required": [ - "Entry" - ], - "allOf": [ - { - "$ref": "#/$defs/ImcIdentifier" - } - ], - "unevaluatedProperties": false - }, - "ImcIdentifier": { - "type": "object", - "properties": { - "PrimaryId": { - "type": "integer" - }, - "SecondaryId": { - "type": "integer" - }, - "Variant": { - "type": "integer" - }, - "ObjectType": { - "$ref": "#/$defs/ObjectType" - }, - "EquipSlot": { - "$ref": "#/$defs/EquipSlot" - }, - "BodySlot": { - "$ref": "#/$defs/BodySlot" - } - }, - "required": [ - "PrimaryId", - "SecondaryId", - "Variant", - "ObjectType", - "EquipSlot", - "BodySlot" - ] - }, - "ImcEntry": { - "type": "object", - "properties": { - "AttributeAndSound": { - "type": "integer" - }, - "MaterialId": { - "type": "integer" - }, - "DecalId": { - "type": "integer" - }, - "VfxId": { - "type": "integer" - }, - "MaterialAnimationId": { - "type": "integer" - }, - "AttributeMask": { - "type": "integer" - }, - "SoundId": { - "type": "integer" - } - }, - "required": [ - "MaterialId", - "DecalId", - "VfxId", - "MaterialAnimationId", - "AttributeMask", - "SoundId" - ], - "additionalProperties": false - }, - "EqdpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "$ref": "#/$defs/EquipSlot" - }, - "ShiftedEntry": { - "type": "integer" - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "EqpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "$ref": "#/$defs/EquipSlot" - } - }, - "required": [ - "Entry", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "EstManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "enum": ["Hair", "Face", "Body", "Head"] - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "GmpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "object", - "properties": { - "Enabled": { - "type": "boolean" - }, - "Animated": { - "type": "boolean" - }, - "RotationA": { - "type": "number" - }, - "RotationB": { - "type": "number" - }, - "RotationC": { - "type": "number" - }, - "UnknownA": { - "type": "number" - }, - "UnknownB": { - "type": "number" - }, - "UnknownTotal": { - "type": "number" - }, - "Value": { - "type": "number" - } - }, - "required": [ - "Enabled", - "Animated", - "RotationA", - "RotationB", - "RotationC", - "UnknownA", - "UnknownB", - "UnknownTotal", - "Value" - ], - "additionalProperties": false - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - } - }, - "required": [ - "Entry", - "SetId" - ], - "additionalProperties": false - }, - "RspManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "number" - }, - "SubRace": { - "$ref": "#/$defs/SubRace" - }, - "Attribute": { - "$ref": "#/$defs/RspAttribute" - } - }, - "additionalProperties": false - }, - "GlobalEqpManipulation": { - "type": "object", - "properties": { - "Condition": { - "type": "integer" - }, - "Type": { - "enum": ["DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats"] - } - }, - "additionalProperties": false - }, - "AtchManipulation": { - "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" - ], - "additionalProperties": false - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "Type": { - "type": "string", - "minLength": 3, - "maxLength": 3 - }, - "Index": { - "type": "integer" - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "Type", - "Index" - ], - "additionalProperties": false - }, - "LaxInteger": { - "oneOf": [ - { - "type": "integer" - }, { - "type": "string", - "pattern": "^\\d+$" - } - ] - }, - "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": { - "enum": ["Unknown", "Male", "Female", "MaleNpc", "FemaleNpc"] - }, - "ModelRace": { - "enum": ["Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera"] - }, - "ObjectType": { - "enum": ["Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font"] - }, - "BodySlot": { - "enum": ["Unknown", "Hair", "Face", "Tail", "Body", "Zear"] - }, - "SubRace": { - "enum": ["Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena"] - }, - "RspAttribute": { - "enum": ["MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ"] - } - } -} diff --git a/schemas/default_mod.json b/schemas/default_mod.json index eecd74d0..8f50c5db 100644 --- a/schemas/default_mod.json +++ b/schemas/default_mod.json @@ -1,25 +1,19 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", "allOf": [ { "type": "object", "properties": { "Version": { - "description": "???", - "type": ["integer", "null"] - }, - "Description": { - "description": "Description of the sub-mod.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] + "description": "Mod Container version, currently unused.", + "type": "integer", + "minimum": 0, + "maximum": 0 } } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + }, + { + "$ref": "structs/container.json" } ] } diff --git a/schemas/group.json b/schemas/group.json index 0078e9f3..4c37b631 100644 --- a/schemas/group.json +++ b/schemas/group.json @@ -1,35 +1,35 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/group.json", "type": "object", "properties": { "Version": { - "description": "???", - "type": ["integer", "null"] + "description": "Mod Container version, currently unused.", + "type": "integer" }, "Name": { "description": "Name of the group.", - "type": "string" + "type": "string", + "minLength": 1 }, "Description": { "description": "Description of the group.", - "type": ["string", "null"] + "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"] + "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", "null"] + "type": "integer" }, "Priority": { "description": "Priority of the group. If several groups define conflicting files or manipulations, the highest priority wins.", - "type": ["integer", "null"] + "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"] + "enum": [ "Single", "Multi", "Imc", "Combining" ] }, "DefaultSettings": { "description": "Default configuration for the group.", @@ -42,165 +42,16 @@ ], "oneOf": [ { - "properties": { - "Type": { - "const": "Single" - }, - "Options": { - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - ] - } - } - } - }, { - "properties": { - "Type": { - "const": "Multi" - }, - "Options": { - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "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", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - ] - } - } - } - }, { - "properties": { - "Type": { - "const": "Imc" - }, - "AllVariants": { - "type": "boolean" - }, - "OnlyAttributes": { - "type": "boolean" - }, - "Identifier": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcIdentifier" - }, - "DefaultEntry": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcEntry" - }, - "Options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "description": "Name of the option.", - "type": "string" - }, - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - }, - "required": [ - "Name" - ], - "oneOf": [ - { - "properties": { - "AttributeMask": { - "type": "integer" - } - }, - "required": [ - "AttributeMask" - ] - }, { - "properties": { - "IsDisableSubMod": { - "const": true - } - }, - "required": [ - "IsDisableSubMod" - ] - } - ], - "unevaluatedProperties": false - } - } - } - }, { - "properties": { - "Type": { - "const": "Combining" - }, - "Options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "description": "Name of the option.", - "type": "string" - }, - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - }, - "required": [ - "Name" - ], - "additionalProperties": false - } - }, - "Containers": { - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - } - } + "$ref": "structs/group_combining.json" + }, + { + "$ref": "structs/group_imc.json" + }, + { + "$ref": "structs/group_multi.json" + }, + { + "$ref": "structs/group_single.json" } - ], - "unevaluatedProperties": false + ] } diff --git a/schemas/local_mod_data-v3.json b/schemas/local_mod_data-v3.json new file mode 100644 index 00000000..bf5d1311 --- /dev/null +++ b/schemas/local_mod_data-v3.json @@ -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" ] +} diff --git a/schemas/meta-v3.json b/schemas/mod_meta-v3.json similarity index 79% rename from schemas/meta-v3.json rename to schemas/mod_meta-v3.json index 1a132264..a926b49e 100644 --- a/schemas/meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -1,6 +1,5 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/meta-v3.json", "title": "Penumbra Mod Metadata", "description": "Metadata of a Penumbra mod.", "type": "object", @@ -13,33 +12,35 @@ }, "Name": { "description": "Name of the mod.", - "type": "string" + "type": "string", + "minLength": 1 }, "Author": { "description": "Author of the mod.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Description": { "description": "Description of the mod. Can span multiple paragraphs.", - "type": ["string", "null"] + "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"] + "type": [ "string", "null" ] }, "Version": { "description": "Version of the mod. Can be an arbitrary string.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Website": { "description": "URL of the web page of the mod.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "ModTags": { "description": "Author-defined tags for the mod.", "type": "array", "items": { - "type": "string" + "type": "string", + "minLength": 1 }, "uniqueItems": true } diff --git a/schemas/structs/container.json b/schemas/structs/container.json new file mode 100644 index 00000000..74db4a23 --- /dev/null +++ b/schemas/structs/container.json @@ -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" + } + } + } +} diff --git a/schemas/structs/group_combining.json b/schemas/structs/group_combining.json new file mode 100644 index 00000000..e42edcb8 --- /dev/null +++ b/schemas/structs/group_combining.json @@ -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" ] + } + } + } + ] + } + } + } +} diff --git a/schemas/structs/group_imc.json b/schemas/structs/group_imc.json new file mode 100644 index 00000000..48a04bd9 --- /dev/null +++ b/schemas/structs/group_imc.json @@ -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" + ] + } + ] + } + } + } +} diff --git a/schemas/structs/group_multi.json b/schemas/structs/group_multi.json new file mode 100644 index 00000000..ca7d4dfa --- /dev/null +++ b/schemas/structs/group_multi.json @@ -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" + } + } + } + ] + } + } + } +} + + + diff --git a/schemas/structs/group_single.json b/schemas/structs/group_single.json new file mode 100644 index 00000000..24cda88d --- /dev/null +++ b/schemas/structs/group_single.json @@ -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" + } + ] + } + } + } +} diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json new file mode 100644 index 00000000..4a41dbe2 --- /dev/null +++ b/schemas/structs/manipulation.json @@ -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" + } + } + } + ] +} diff --git a/schemas/structs/meta_atch.json b/schemas/structs/meta_atch.json new file mode 100644 index 00000000..3c9cbef5 --- /dev/null +++ b/schemas/structs/meta_atch.json @@ -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" + ] +} diff --git a/schemas/structs/meta_enums.json b/schemas/structs/meta_enums.json new file mode 100644 index 00000000..747da849 --- /dev/null +++ b/schemas/structs/meta_enums.json @@ -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])$" + } + ] + } + } +} diff --git a/schemas/structs/meta_eqdp.json b/schemas/structs/meta_eqdp.json new file mode 100644 index 00000000..f27606b9 --- /dev/null +++ b/schemas/structs/meta_eqdp.json @@ -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" + ] +} diff --git a/schemas/structs/meta_eqp.json b/schemas/structs/meta_eqp.json new file mode 100644 index 00000000..c829d7a7 --- /dev/null +++ b/schemas/structs/meta_eqp.json @@ -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" + ] +} diff --git a/schemas/structs/meta_est.json b/schemas/structs/meta_est.json new file mode 100644 index 00000000..22bce12b --- /dev/null +++ b/schemas/structs/meta_est.json @@ -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" + ] +} diff --git a/schemas/structs/meta_geqp.json b/schemas/structs/meta_geqp.json new file mode 100644 index 00000000..3d4908f9 --- /dev/null +++ b/schemas/structs/meta_geqp.json @@ -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": {} + } + } + ] +} diff --git a/schemas/structs/meta_gmp.json b/schemas/structs/meta_gmp.json new file mode 100644 index 00000000..bf1fb1df --- /dev/null +++ b/schemas/structs/meta_gmp.json @@ -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" + ] +} diff --git a/schemas/structs/meta_imc.json b/schemas/structs/meta_imc.json new file mode 100644 index 00000000..aa9a4fca --- /dev/null +++ b/schemas/structs/meta_imc.json @@ -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" + ] + } + } +} diff --git a/schemas/structs/meta_rsp.json b/schemas/structs/meta_rsp.json new file mode 100644 index 00000000..3354281b --- /dev/null +++ b/schemas/structs/meta_rsp.json @@ -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" + ] +} diff --git a/schemas/structs/option.json b/schemas/structs/option.json new file mode 100644 index 00000000..c45ccfdb --- /dev/null +++ b/schemas/structs/option.json @@ -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" ] +} From b62563d72131c2119370b3d25677457291c15b96 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 20:06:21 +0100 Subject: [PATCH 07/10] Remove $id from shpk_devkit schema --- schemas/shpk_devkit.json | 1 - 1 file changed, 1 deletion(-) diff --git a/schemas/shpk_devkit.json b/schemas/shpk_devkit.json index cd18ab81..f03fbb05 100644 --- a/schemas/shpk_devkit.json +++ b/schemas/shpk_devkit.json @@ -1,6 +1,5 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/shpk_devkit.json", "type": "object", "properties": { "ShaderKeys": { From 4f0428832cadfe61c4c49dd7dcbfdeacde5332bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:13:09 +0100 Subject: [PATCH 08/10] Fix solution file for schemas. --- Penumbra.sln | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index c0b38118..e864fbee 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -26,10 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Pe EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}" ProjectSection(SolutionItems) = preProject - schemas\files\default_mod.json = schemas\files\default_mod.json - schemas\files\group.json = schemas\files\group.json - schemas\files\local_mod_data-v3.json = schemas\files\local_mod_data-v3.json - schemas\files\mod_meta-v3.json = schemas\files\mod_meta-v3.json + 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}" From 7b517390b6c619a27de6698ada6627b80ae21c75 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:30:03 +0100 Subject: [PATCH 09/10] Fix temporary settings causing collection saves. --- Penumbra/Collections/Manager/CollectionEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 437d4e0b..f4902fda 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -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); From 8779f4b6893b6d472c71fb63d0f31ef947140424 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:36:05 +0100 Subject: [PATCH 10/10] Add new cutscene ENPC tracking hooks. --- Penumbra.GameData | 2 +- Penumbra/Interop/GameState.cs | 3 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../Objects/ConstructCutsceneCharacter.cs | 70 +++++++++++++++++++ Penumbra/Interop/Hooks/Objects/EnableDraw.cs | 2 +- .../Interop/Hooks/Objects/SetupPlayerNpc.cs | 55 +++++++++++++++ .../Interop/PathResolving/CutsceneService.cs | 29 +++++--- 7 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs create mode 100644 Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index c5250722..5bac66e5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c525072299d5febd2bb638ab229060b0073ba6a6 +Subproject commit 5bac66e5ad73e57919aff7f8b046606b75e191a2 diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index f80ef696..497be508 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -11,7 +11,8 @@ public class GameState : IService { #region Last Game Object - private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + public readonly ThreadLocal CharacterAssociated = new(() => false); public nint LastGameObject => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index b95e5789..5a856764 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -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 diff --git a/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs b/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs new file mode 100644 index 00000000..5fa3de32 --- /dev/null +++ b/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs @@ -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, IHookService +{ + private readonly GameState _gameState; + private readonly ObjectManager _objects; + + public enum Priority + { + /// + CutsceneService = 0, + } + + public ConstructCutsceneCharacter(GameState gameState, HookManager hooks, ObjectManager objects) + : base("ConstructCutsceneCharacter") + { + _gameState = gameState; + _objects = objects; + _task = hooks.CreateHook(Name, Sigs.ConstructCutsceneCharacter, Detour, !HookOverrides.Instance.Objects.ConstructCutsceneCharacter); + } + + private readonly Task> _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; +} diff --git a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs index 68bb28af..979cb87c 100644 --- a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs +++ b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs @@ -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(); } diff --git a/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs b/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs new file mode 100644 index 00000000..8f1226c3 --- /dev/null +++ b/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs @@ -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 +{ + private readonly GameState _gameState; + + public SetupPlayerNpc(GameState gameState, HookManager hooks) + { + _gameState = gameState; + Task = hooks.CreateHook("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**)s)[0][19](s); + } +} diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 8e32dd76..6be19c46 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -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> 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; + } + /// Try to recover GPose actors on reloads into a running game. /// This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. private void RecoverGPoseActors()