diff --git a/Anvil/Anvil.csproj b/Anvil/Anvil.csproj index 52208a0..c6bd805 100644 --- a/Anvil/Anvil.csproj +++ b/Anvil/Anvil.csproj @@ -1,6 +1,6 @@ - 0.1.0 + 0.1.1 enable enable latest diff --git a/Anvil/Anvil.yaml b/Anvil/Anvil.yaml index f8f0788..1a4e84e 100644 --- a/Anvil/Anvil.yaml +++ b/Anvil/Anvil.yaml @@ -32,6 +32,37 @@ tags: - UI - Privacy changelog: |- + **v0.1.1 — Module 01 Spec-Sync Hotfix (2026-05-28)** + + Code-sync against the 01-RecipeData Rev 6 spec polish that landed + during the Module 02 (CraftingSimulator) Phase-2 review. No new + surface; the existing v0.1.0 module shape is brought in line with + the spec edits the simulator design needs. + + - `AnvilRecipe.RecipeLevelTableClassJobLevel` added. Carries the + character-level equivalent of a recipe's difficulty (e.g. 90 for + Endwalker 3-star, 100 for Dawntrail endgame). The simulator + module uses this for the mod-penalty branch in BaseProgress and + BaseQuality. Adapter resolves it from + `RecipeLevelTable.Value.ClassJobLevel`. + - `AnvilRecipe.IsCollectable` added. Marks a recipe as a + collectable submission (custom delivery, masterpiece supply, + collectables shop). The simulator module's Summarize branch + uses this discriminator. Adapter resolves it from + `recipe.IsExpert` or the output item's `IsCollectable` flag. + - `AnvilRecipe.IsSplendorCosmic` removed. The Splendorous-tool + bonus is character-equipment state, not recipe state — it lives + in the stats layer of the simulator module instead. + - `ActionMechanicsTable.Reflect` EfficiencyQuality corrected + from 100 to 300, matching the live game value. + - `ActionMechanicsTable.RefinedTouch` IqStackBonus comment + clarified: the +1 IQ stack is unconditional. The combo gating + is a UI/solver hint, not a data-layer rule. + + Inspired by Craftimizer by Asriel Camora (MIT). + + --- + **v0.1.0 — Initial Release (2026-05-27)** First Hellion Forge release of Anvil. Module 01 (RecipeData) is in diff --git a/Anvil/RecipeData/AnvilRecipe.cs b/Anvil/RecipeData/AnvilRecipe.cs index e6b6f16..696e7f2 100644 --- a/Anvil/RecipeData/AnvilRecipe.cs +++ b/Anvil/RecipeData/AnvilRecipe.cs @@ -16,6 +16,14 @@ public sealed record AnvilRecipe public required byte OutputAmount { get; init; } public required uint ClassJobId { get; init; } public required ushort RecipeLevel { get; init; } + + // Character-level equivalent of the recipe's difficulty (e.g. 90 for an + // Endwalker 3-star, 100 for a Dawntrail endgame recipe). Not the same as + // RecipeLevel above - that field is the RecipeLevelTable row id, this one + // is its ClassJobLevel column. Module 02 uses it for the mod-penalty + // branch in BaseProgress and BaseQuality. + public required byte RecipeLevelTableClassJobLevel { get; init; } + public required int Difficulty { get; init; } public required int QualityMax { get; init; } public required byte Durability { get; init; } @@ -25,22 +33,30 @@ public sealed record AnvilRecipe public required bool IsExpertRecipe { get; init; } public required bool CanHQ { get; init; } + // True when the recipe is a collectable submission (custom delivery, + // masterpiece supply, collectables shop). Module 02's Summarize branch + // uses this discriminator. Resolved as IsExpertRecipe OR the output + // item's AlwaysCollectable flag - the latter is the canonical FFXIV + // signal for collectable submissions. + public required bool IsCollectable { get; init; } + // Cosmic Exploration (Patch 7.x). v0.1.0 ships with MissionHas* always // false (SH-15 option B) - the data schema stays in place so v0.2.0+ can // light up the surface by resolving the WKS mission sheet without - // touching the type. IsCosmic / IsSplendorCosmic are still set by the - // recipe-detection path so the catalog knows which recipes belong to the - // Cosmic surface even while the action flags stay dormant. + // touching the type. IsCosmic is still set by the recipe-detection path + // so the catalog knows which recipes belong to the Cosmic surface even + // while the mission flags stay dormant. The Splendorous-tool bonus is + // character-equipment state rather than recipe state, so it lives in + // the simulator's stats layer (CraftStats.HasSplendorousTool in module + // 02) instead of carrying a flag on this record. // // Adapter invariant (validated when the catalog is built): - // IsSplendorCosmic == true implies IsCosmic == true // MissionHasMaterialMiracle == true implies IsCosmic == true // MissionHasSteadyHand == true implies IsCosmic == true // // Inconsistent recipe states (sub-flag without master flag) throw at // catalog-build time instead of silently corrupting the simulator. public required bool IsCosmic { get; init; } - public required bool IsSplendorCosmic { get; init; } public required bool MissionHasMaterialMiracle { get; init; } public required bool MissionHasSteadyHand { get; init; } public required bool IsIshgardExpert { get; init; } diff --git a/Anvil/RecipeData/Internal/LuminaRecipeAdapter.cs b/Anvil/RecipeData/Internal/LuminaRecipeAdapter.cs index e5bd358..dd5c274 100644 --- a/Anvil/RecipeData/Internal/LuminaRecipeAdapter.cs +++ b/Anvil/RecipeData/Internal/LuminaRecipeAdapter.cs @@ -557,9 +557,14 @@ internal sealed class LuminaRecipeAdapter var isCosmic = recipe.Number == 0; var ingredients = ResolveIngredients(recipe); - var displayName = itemSheet.TryGetRow(outputItemId, out var outputItem) - ? outputItem.Name.ExtractText() - : string.Empty; + // IsCollectable resolves off Item.AlwaysCollectable from the same + // sheet row we already fetch for the display name. Lumina is the + // canonical source here and folding it into the Recipe walk keeps + // the adapter single-pass (the Item walk runs later and rebuilds + // its own catalog from the same sheet). + var hasOutputItem = itemSheet.TryGetRow(outputItemId, out var outputItem); + var displayName = hasOutputItem ? outputItem.Name.ExtractText() : string.Empty; + var isCollectable = recipe.IsExpert || (hasOutputItem && outputItem.AlwaysCollectable); var anvilRecipe = new AnvilRecipe { @@ -568,6 +573,7 @@ internal sealed class LuminaRecipeAdapter OutputAmount = recipe.AmountResult, ClassJobId = classJobId, RecipeLevel = (ushort)recipe.RecipeLevelTable.RowId, + RecipeLevelTableClassJobLevel = levelTable.ClassJobLevel, Difficulty = difficulty, QualityMax = qualityMax, Durability = durability, @@ -576,8 +582,8 @@ internal sealed class LuminaRecipeAdapter QualityForHQ = recipe.CanHq ? qualityMax : 0, IsExpertRecipe = recipe.IsExpert, CanHQ = recipe.CanHq, + IsCollectable = isCollectable, IsCosmic = isCosmic, - IsSplendorCosmic = false, MissionHasMaterialMiracle = false, MissionHasSteadyHand = false, IsIshgardExpert = false, @@ -636,11 +642,6 @@ internal sealed class LuminaRecipeAdapter // time instead of producing a corrupt simulator state at runtime. private static void ValidateCosmicInvariant(AnvilRecipe recipe) { - if (recipe.IsSplendorCosmic && !recipe.IsCosmic) - throw new InvalidOperationException( - $"AnvilRecipe {recipe.RecipeId}: IsSplendorCosmic implies IsCosmic, " - + "but IsCosmic is false." - ); if (recipe.MissionHasMaterialMiracle && !recipe.IsCosmic) throw new InvalidOperationException( $"AnvilRecipe {recipe.RecipeId}: MissionHasMaterialMiracle implies IsCosmic, " diff --git a/Anvil/RecipeData/Mechanics/ActionMechanicsTable.cs b/Anvil/RecipeData/Mechanics/ActionMechanicsTable.cs index 93efb7d..90448d6 100644 --- a/Anvil/RecipeData/Mechanics/ActionMechanicsTable.cs +++ b/Anvil/RecipeData/Mechanics/ActionMechanicsTable.cs @@ -209,7 +209,9 @@ internal static class ActionMechanicsTable DurabilityCost = 10, EfficiencyProgress = 0, EfficiencyQuality = 100, - // +1 only after BasicTouch combo; simulator gates the bonus. + // +1 IQ stack unconditionally. The BasicTouch combo that the + // UI and solver hint at is a presentation layer rule, not a + // data-layer one - the catalog stays free of the gate. IqStackBonus = 1, ChargesPerCraft = 0, GrantsBuff = null, @@ -286,7 +288,7 @@ internal static class ActionMechanicsTable CpCost = 6, DurabilityCost = 10, EfficiencyProgress = 0, - EfficiencyQuality = 100, + EfficiencyQuality = 300, IqStackBonus = 1, ChargesPerCraft = 0, GrantsBuff = null, diff --git a/Anvil/RecipeData/Mechanics/ConditionMechanicsTable.cs b/Anvil/RecipeData/Mechanics/ConditionMechanicsTable.cs index ace0e77..8136cd9 100644 --- a/Anvil/RecipeData/Mechanics/ConditionMechanicsTable.cs +++ b/Anvil/RecipeData/Mechanics/ConditionMechanicsTable.cs @@ -10,9 +10,10 @@ // behaviour for Robust - that wording predates the verified mechanics table // and the values below are the source of truth.) // -// SplendorCosmic raises Good's quality multiplier from 1.5 to 1.75 on -// recipes flagged IsSplendorCosmic. The override lives in the simulator; -// the table holds the non-Splendor default. +// Splendor (the Splendorous-tool bonus) raises Good's quality multiplier +// from 1.5 to 1.75 when the character has the tool equipped. The override +// lives in the simulator's stats layer (CraftStats.HasSplendorousTool in +// module 02); the table holds the non-Splendor default. using System.Collections.Generic; diff --git a/Anvil/SelfTest/RecipeDataAdapterLoadStep.cs b/Anvil/SelfTest/RecipeDataAdapterLoadStep.cs index e2603c6..298cf84 100644 --- a/Anvil/SelfTest/RecipeDataAdapterLoadStep.cs +++ b/Anvil/SelfTest/RecipeDataAdapterLoadStep.cs @@ -144,7 +144,33 @@ internal sealed class RecipeDataAdapterLoadStep : ISelfTestStep } } - // #9: Cosmic-surface sanity. v0.1.0 ships with SH-15 option B (all + // #9: RecipeLevelTableClassJobLevel sanity. At least one recipe must + // resolve a positive value from RecipeLevelTable.ClassJobLevel - if + // every recipe shows zero the adapter never walked the second sheet, + // which would silently break the simulator's mod-penalty branch. + if (!_catalog.RecipesById.Values.Any(r => r.RecipeLevelTableClassJobLevel > 0)) + { + _logger.LogError( + "RecipeDataCatalog.RecipesById has no recipe with " + + "RecipeLevelTableClassJobLevel > 0; RecipeLevelTable walk failed." + ); + failures++; + } + + // #10: IsCollectable sanity. FFXIV ships a non-zero number of + // collectable submission recipes (custom delivery, masterpiece + // supply, collectables shop), so the catalog must surface at + // least one. A flat false would mean the resolve path is broken. + if (!_catalog.RecipesById.Values.Any(r => r.IsCollectable)) + { + _logger.LogError( + "RecipeDataCatalog.RecipesById has no recipe with " + + "IsCollectable == true; collectable detection failed." + ); + failures++; + } + + // #11: Cosmic-surface sanity. v0.1.0 ships with SH-15 option B (all // MissionHas* flags forced false), so the WARN is expected whenever // any Cosmic recipe is in the catalog. The step still returns Pass // because the catalog itself is not broken - the Cosmic sub-surface diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ae90e0d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable Anvil releases. See the Gitea release history for prior builds. + +## v0.1.1 — Module 01 Spec-Sync Hotfix (2026-05-28) + +Code-sync against the 01-RecipeData Rev 6 spec polish that landed during +the Module 02 (CraftingSimulator) Phase-2 review. No new surface; the +existing v0.1.0 module shape is brought in line with the spec edits the +simulator design needs. + +- `AnvilRecipe.RecipeLevelTableClassJobLevel` added. Carries the + character-level equivalent of a recipe's difficulty (e.g. 90 for + Endwalker 3-star, 100 for Dawntrail endgame). The simulator module + uses this for the mod-penalty branch in BaseProgress and BaseQuality. + Adapter resolves it from `RecipeLevelTable.Value.ClassJobLevel`. +- `AnvilRecipe.IsCollectable` added. Marks a recipe as a collectable + submission (custom delivery, masterpiece supply, collectables shop). + The simulator module's Summarize branch uses this discriminator. + Adapter resolves it from `recipe.IsExpert` or the output item's + `IsCollectable` flag. +- `AnvilRecipe.IsSplendorCosmic` removed. The Splendorous-tool bonus + is character-equipment state, not recipe state — it lives in the + stats layer of the simulator module instead. +- `ActionMechanicsTable.Reflect` EfficiencyQuality corrected from 100 + to 300, matching the live game value. +- `ActionMechanicsTable.RefinedTouch` IqStackBonus comment clarified: + the +1 IQ stack is unconditional. The combo gating is a UI/solver + hint, not a data-layer rule. + +Inspired by Craftimizer by Asriel Camora (MIT). + +## v0.1.0 — Initial Release (2026-05-27) + +First Hellion Forge release of Anvil. Module 01 (RecipeData) is in +place: catalogues every FFXIV crafting recipe, item, action, buff, +condition, and food/medicine from Lumina sheets, surfaces them through +a Dalamud-free read-only API, and verifies the load via the `/xlperf` +SelfTest step. + +Note: This release is the foundation layer. UI windows, simulator, +solver, macros, hooks, and the IPC provider follow in v0.2.0+. + +Inspired by Craftimizer by Asriel Camora (MIT). diff --git a/repo.json b/repo.json new file mode 100644 index 0000000..cee0858 --- /dev/null +++ b/repo.json @@ -0,0 +1,38 @@ +[ + { + "Author": "Jon Kazama (Hellion Forge)", + "Name": "Anvil", + "InternalName": "Anvil", + "AssemblyVersion": "0.1.1.0", + "Description": "A Hellion Forge plugin — privacy-focused FFXIV crafting helper.\n\nAnvil is a clean-room crafting plugin for FINAL FANTASY XIV. It provides a state-machine crafting simulator, a wrapper for the Raphael Rust solver, a recipe-note overlay, a synth-helper overlay, a macro editor, recipe bookmarks, and an opt-in Auto-Craft hook.\n\nPrivacy:\n- Zero telemetry, no network calls\n- Bookmarks and settings stay on your machine\n- Auto-Craft is OFF by default with an explicit warning modal\n\nStatus: v0.1.1 ships the spec-sync hotfix for the Module 01 (RecipeData) foundation. UI windows, simulator, solver wrapper, macro engine, and the IPC provider follow in v0.2.0+.\n\nCosmic Exploration support: planned for v0.2.0 (recipe-flag schema is already in place, mission-flag resolution is deferred).\n\nInspired by Craftimizer by Asriel Camora (MIT). Anvil is an independent re-implementation; no code was ported from Craftimizer.", + "ApplicableVersion": "any", + "RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Anvil", + "Tags": [ + "Crafting", + "Macros", + "Solver", + "UI", + "Privacy" + ], + "DalamudApiLevel": 15, + "LoadRequiredState": 0, + "LoadSync": false, + "CanUnloadAsync": false, + "LoadPriority": 0, + "Punchline": "A Hellion Forge plugin — privacy-focused FFXIV crafting helper with simulator, solver, and bookmarks.", + "Changelog": "**v0.1.1 — Module 01 Spec-Sync Hotfix (2026-05-28)**\n\nCode-sync against the 01-RecipeData Rev 6 spec polish that landed during the Module 02 (CraftingSimulator) Phase-2 review. No new surface; the existing v0.1.0 module shape is brought in line with the spec edits the simulator design needs.\n\n- `AnvilRecipe.RecipeLevelTableClassJobLevel` added. Carries the character-level equivalent of a recipe's difficulty (e.g. 90 for Endwalker 3-star, 100 for Dawntrail endgame). The simulator module uses this for the mod-penalty branch in BaseProgress and BaseQuality. Adapter resolves it from `RecipeLevelTable.Value.ClassJobLevel`.\n- `AnvilRecipe.IsCollectable` added. Marks a recipe as a collectable submission (custom delivery, masterpiece supply, collectables shop). The simulator module's Summarize branch uses this discriminator. Adapter resolves it from `recipe.IsExpert` or the output item's `IsCollectable` flag.\n- `AnvilRecipe.IsSplendorCosmic` removed. The Splendorous-tool bonus is character-equipment state, not recipe state — it lives in the stats layer of the simulator module instead.\n- `ActionMechanicsTable.Reflect` EfficiencyQuality corrected from 100 to 300, matching the live game value.\n- `ActionMechanicsTable.RefinedTouch` IqStackBonus comment clarified: the +1 IQ stack is unconditional. The combo gating is a UI/solver hint, not a data-layer rule.\n\nInspired by Craftimizer by Asriel Camora (MIT).\n\n---\n\n**v0.1.0 — Initial Release (2026-05-27)**\n\nFirst Hellion Forge release of Anvil. Module 01 (RecipeData) is in place: catalogues every FFXIV crafting recipe, item, action, buff, condition, and food/medicine from Lumina sheets, surfaces them through a Dalamud-free read-only API, and verifies the load via the `/xlperf` SelfTest step.\n\nNote: This release is the foundation layer. UI windows, simulator, solver, macros, hooks, and the IPC provider follow in v0.2.0+.\n\nInspired by Craftimizer by Asriel Camora (MIT).\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/Anvil/releases", + "AcceptsFeedback": true, + "DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Anvil/releases/download/v0.1.1/latest.zip", + "DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Anvil/releases/download/v0.1.1/latest.zip", + "DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Anvil/releases/download/v0.1.1/latest.zip", + "TestingAssemblyVersion": "0.1.1.0", + "IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Anvil/raw/branch/main/Anvil/images/icon.png", + "ImageUrls": [], + "DownloadCount": 0, + "IsHide": false, + "IsTestingExclusive": false, + "CategoryTags": [ + "utility" + ] + } +]