This commit is contained in:
Liza 2024-03-20 19:52:54 +01:00
parent 7d5cbd8563
commit b61de1f52c
Signed by: liza
GPG Key ID: 7199F8D727D55F67
19 changed files with 1085 additions and 49 deletions

1017
.editorconfig Normal file

File diff suppressed because it is too large Load Diff

2
LLib

@ -1 +1 @@
Subproject commit 865a6080319f8ccbcd5fd5b0004404822b6e60d4 Subproject commit 3792244261a9f5426a7916f5a6dd1966238ba84a

View File

@ -0,0 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Amalj_0027aa/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ceruleum/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Workshoppa/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Yesno/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Z_0027ranmaia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -10,7 +10,7 @@ internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public CurrentItem? CurrentlyCraftedItem { get; set; } = null; public CurrentItem? CurrentlyCraftedItem { get; set; }
public List<QueuedItem> ItemQueue { get; set; } = new(); public List<QueuedItem> ItemQueue { get; set; } = new();
public bool EnableRepairKitCalculator { get; set; } = true; public bool EnableRepairKitCalculator { get; set; } = true;
public bool EnableCeruleumTankCalculator { get; set; } = true; public bool EnableCeruleumTankCalculator { get; set; } = true;
@ -27,7 +27,7 @@ internal sealed class Configuration : IPluginConfiguration
public uint WorkshopItemId { get; set; } public uint WorkshopItemId { get; set; }
public bool StartedCrafting { get; set; } public bool StartedCrafting { get; set; }
public uint PhasesComplete { get; set; } = 0; public uint PhasesComplete { get; set; }
public List<PhaseItem> ContributedItemsInCurrentPhase { get; set; } = new(); public List<PhaseItem> ContributedItemsInCurrentPhase { get; set; } = new();
public bool UpdateFromCraftState(CraftState craftState) public bool UpdateFromCraftState(CraftState craftState)

View File

@ -8,7 +8,7 @@ public sealed class CraftState
public required uint ResultItem { get; init; } public required uint ResultItem { get; init; }
public required uint StepsComplete { get; init; } public required uint StepsComplete { get; init; }
public required uint StepsTotal { get; init; } public required uint StepsTotal { get; init; }
public required List<CraftItem> Items { get; init; } public required IReadOnlyList<CraftItem> Items { get; init; }
public bool IsPhaseComplete() => Items.All(x => x.Finished || x.StepsComplete == x.StepsTotal); public bool IsPhaseComplete() => Items.All(x => x.Finished || x.StepsComplete == x.StepsTotal);

View File

@ -1,4 +1,5 @@
using System; using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LLib; using LLib;
@ -13,19 +14,19 @@ internal sealed class GameStrings
public GameStrings(IDataManager dataManager, IPluginLog pluginLog) public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{ {
PurchaseItemForGil = dataManager.GetRegex<Addon>(3406, addon => addon.Text, pluginLog) PurchaseItemForGil = dataManager.GetRegex<Addon>(3406, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(PurchaseItemForGil)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(PurchaseItemForGil)}");
PurchaseItemForCompanyCredits = dataManager.GetRegex<Addon>(3473, addon => addon.Text, pluginLog) PurchaseItemForCompanyCredits = dataManager.GetRegex<Addon>(3473, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(PurchaseItemForCompanyCredits)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(PurchaseItemForCompanyCredits)}");
ViewCraftingLog = ViewCraftingLog =
dataManager.GetString<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_MENU_CC_NOTE", dataManager.GetString<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_MENU_CC_NOTE",
pluginLog) ?? throw new Exception($"Unable to resolve {nameof(ViewCraftingLog)}"); pluginLog) ?? throw new ConstraintException($"Unable to resolve {nameof(ViewCraftingLog)}");
TurnInHighQualityItem = dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog) TurnInHighQualityItem = dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(TurnInHighQualityItem)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(TurnInHighQualityItem)}");
ContributeItems = dataManager.GetRegex<Addon>(6652, addon => addon.Text, pluginLog) ContributeItems = dataManager.GetRegex<Addon>(6652, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(ContributeItems)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(ContributeItems)}");
RetrieveFinishedItem = RetrieveFinishedItem =
dataManager.GetRegex<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_FINISH_CONF", pluginLog) dataManager.GetRegex<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_FINISH_CONF", pluginLog)
?? throw new Exception($"Unable to resolve {nameof(RetrieveFinishedItem)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(RetrieveFinishedItem)}");
} }
public Regex PurchaseItemForGil { get; } public Regex PurchaseItemForGil { get; }
@ -36,7 +37,8 @@ internal sealed class GameStrings
public Regex RetrieveFinishedItem { get; } public Regex RetrieveFinishedItem { get; }
[Sheet("custom/001/CmnDefCompanyManufactory_00150")] [Sheet("custom/001/CmnDefCompanyManufactory_00150")]
private class WorkshopDialogue : QuestDialogueText [SuppressMessage("Performance", "CA1812")]
private sealed class WorkshopDialogue : QuestDialogueText
{ {
} }
} }

View File

@ -6,7 +6,7 @@ using Lumina.Excel.GeneratedSheets;
namespace Workshoppa.GameData; namespace Workshoppa.GameData;
public sealed class RecipeTree internal sealed class RecipeTree
{ {
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IReadOnlyList<uint> _shopItemsOnly; private readonly IReadOnlyList<uint> _shopItemsOnly;
@ -38,7 +38,7 @@ public sealed class RecipeTree
.AsReadOnly(); .AsReadOnly();
} }
public List<Ingredient> ResolveRecipes(List<Ingredient> materials) public IReadOnlyList<Ingredient> ResolveRecipes(IReadOnlyList<Ingredient> materials)
{ {
// look up recipes recursively // look up recipes recursively
int limit = 10; int limit = 10;
@ -72,7 +72,8 @@ public sealed class RecipeTree
//_pluginLog.Information($" → {part.Name}"); //_pluginLog.Information($" → {part.Name}");
int unmodifiedQuantity = part.TotalQuantity; int unmodifiedQuantity = part.TotalQuantity;
int roundedQuantity = (int)((unmodifiedQuantity + ingredient.AmountCrafted - 1) / ingredient.AmountCrafted); int roundedQuantity =
(int)((unmodifiedQuantity + ingredient.AmountCrafted - 1) / ingredient.AmountCrafted);
part.TotalQuantity = part.TotalQuantity - unmodifiedQuantity + roundedQuantity; part.TotalQuantity = part.TotalQuantity - unmodifiedQuantity + roundedQuantity;
} }
} }
@ -83,13 +84,13 @@ public sealed class RecipeTree
List<RecipeInfo> sortedList = new List<RecipeInfo>(); List<RecipeInfo> sortedList = new List<RecipeInfo>();
while (sortedList.Count < completeList.Count) while (sortedList.Count < completeList.Count)
{ {
var craftable = completeList.Where(x => var canBeCrafted = completeList.Where(x =>
!sortedList.Contains(x) && x.DependsOn.All(y => sortedList.Any(z => y == z.ItemId))) !sortedList.Contains(x) && x.DependsOn.All(y => sortedList.Any(z => y == z.ItemId)))
.ToList(); .ToList();
if (craftable.Count == 0) if (canBeCrafted.Count == 0)
throw new Exception("Unable to sort items"); throw new InvalidOperationException("Unable to sort items");
sortedList.AddRange(craftable.OrderBy(x => x.Name)); sortedList.AddRange(canBeCrafted.OrderBy(x => x.Name));
} }
return sortedList.Cast<Ingredient>().ToList(); return sortedList.Cast<Ingredient>().ToList();
@ -142,7 +143,7 @@ public sealed class RecipeTree
return ingredients; return ingredients;
} }
private List<RecipeInfo> ExtendWithAmountCrafted(List<Ingredient> materials) private List<RecipeInfo> ExtendWithAmountCrafted(IEnumerable<Ingredient> materials)
{ {
return materials.Select(x => new return materials.Select(x => new
{ {
@ -164,17 +165,17 @@ public sealed class RecipeTree
.ToList(); .ToList();
} }
public Recipe? GetFirstRecipeForItem(uint itemId) private Recipe? GetFirstRecipeForItem(uint itemId)
{ {
return _dataManager.GetExcelSheet<Recipe>()!.FirstOrDefault(x => x.RowId > 0 && x.ItemResult.Row == itemId); return _dataManager.GetExcelSheet<Recipe>()!.FirstOrDefault(x => x.RowId > 0 && x.ItemResult.Row == itemId);
} }
public GatheringItem? GetGatheringItem(uint itemId) private GatheringItem? GetGatheringItem(uint itemId)
{ {
return _dataManager.GetExcelSheet<GatheringItem>()!.FirstOrDefault(x => x.RowId > 0 && (uint)x.Item == itemId); return _dataManager.GetExcelSheet<GatheringItem>()!.FirstOrDefault(x => x.RowId > 0 && (uint)x.Item == itemId);
} }
public RetainerTaskNormal? GetVentureItem(uint itemId) private RetainerTaskNormal? GetVentureItem(uint itemId)
{ {
return _dataManager.GetExcelSheet<RetainerTaskNormal>()! return _dataManager.GetExcelSheet<RetainerTaskNormal>()!
.FirstOrDefault(x => x.RowId > 0 && x.Item.Row == itemId); .FirstOrDefault(x => x.RowId > 0 && x.Item.Row == itemId);

View File

@ -129,7 +129,7 @@ internal sealed class CeruleumTankWindow : ShopWindow
} }
} }
private string FormatStackCount(int ceruleumTanks) private static string FormatStackCount(int ceruleumTanks)
{ {
int fullStacks = ceruleumTanks / 999; int fullStacks = ceruleumTanks / 999;
int partials = ceruleumTanks % 999; int partials = ceruleumTanks % 999;

View File

@ -1,12 +1,11 @@
using System.Numerics; using System.Numerics;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib.ImGui;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
internal sealed class ConfigWindow : LImGui.LWindow internal sealed class ConfigWindow : LWindow
{ {
private readonly DalamudPluginInterface _pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;

View File

@ -13,12 +13,13 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib;
using LLib.ImGui;
using Workshoppa.GameData; using Workshoppa.GameData;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
// FIXME The close button doesn't work near the workshop, either hide it or make it work // FIXME The close button doesn't work near the workshop, either hide it or make it work
internal sealed class MainWindow : LImGui.LWindow internal sealed class MainWindow : LWindow
{ {
private static readonly Regex CountAndName = new(@"^(\d{1,5})x?\s+(.*)$", RegexOptions.Compiled); private static readonly Regex CountAndName = new(@"^(\d{1,5})x?\s+(.*)$", RegexOptions.Compiled);
@ -231,7 +232,7 @@ internal sealed class MainWindow : LImGui.LWindow
ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256); ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256);
foreach (var craft in _workshopCache.Crafts foreach (var craft in _workshopCache.Crafts
.Where(x => x.Name.ToLower().Contains(_searchString.ToLower())) .Where(x => x.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.WorkshopItemId)) .OrderBy(x => x.WorkshopItemId))
{ {
IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId); IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId);
@ -319,7 +320,7 @@ internal sealed class MainWindow : LImGui.LWindow
ImGui.InputTextWithHint("", "Preset Name...", ref _newPresetName, 64); ImGui.InputTextWithHint("", "Preset Name...", ref _newPresetName, 64);
ImGui.BeginDisabled(_configuration.Presets.Any(x => ImGui.BeginDisabled(_configuration.Presets.Any(x =>
x.Name.Equals(_newPresetName, StringComparison.CurrentCultureIgnoreCase))); x.Name.Equals(_newPresetName, StringComparison.OrdinalIgnoreCase)));
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save"))
{ {
_configuration.Presets.Add(new Configuration.Preset _configuration.Presets.Add(new Configuration.Preset
@ -400,7 +401,7 @@ internal sealed class MainWindow : LImGui.LWindow
continue; continue;
var craft = _workshopCache.Crafts.FirstOrDefault(x => var craft = _workshopCache.Crafts.FirstOrDefault(x =>
x.Name.Equals(match.Groups[2].Value, StringComparison.CurrentCultureIgnoreCase)); x.Name.Equals(match.Groups[2].Value, StringComparison.OrdinalIgnoreCase));
if (craft != null && int.TryParse(match.Groups[1].Value, out int quantity)) if (craft != null && int.TryParse(match.Groups[1].Value, out int quantity))
{ {
fromClipboardItems.Add(new Configuration.QueuedItem fromClipboardItems.Add(new Configuration.QueuedItem
@ -590,7 +591,7 @@ internal sealed class MainWindow : LImGui.LWindow
.ToList(); .ToList();
} }
private void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity) private static void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity)
{ {
if (completedForCurrentCraft.TryGetValue(itemId, out var existingQuantity)) if (completedForCurrentCraft.TryGetValue(itemId, out var existingQuantity))
completedForCurrentCraft[itemId] = quantity + existingQuantity; completedForCurrentCraft[itemId] = quantity + existingQuantity;

View File

@ -6,14 +6,14 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET; using ImGuiNET;
using LLib;
using LLib.GameUI; using LLib.GameUI;
using LLib.ImGui;
using Workshoppa.External; using Workshoppa.External;
using Workshoppa.GameData.Shops; using Workshoppa.GameData.Shops;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
internal abstract class ShopWindow : LImGui.LWindow, IDisposable internal abstract class ShopWindow : LWindow, IDisposable
{ {
private readonly string _addonName; private readonly string _addonName;
private readonly WorkshopPlugin _plugin; private readonly WorkshopPlugin _plugin;

View File

@ -51,12 +51,12 @@ partial class WorkshopPlugin
private void SelectCraftBranch() private void SelectCraftBranch()
{ {
if (SelectSelectString("contrib", 0, s => s.StartsWith("Contribute materials."))) if (SelectSelectString("contrib", 0, s => s.StartsWith("Contribute materials.", StringComparison.Ordinal)))
{ {
CurrentStage = Stage.ContributeMaterials; CurrentStage = Stage.ContributeMaterials;
_continueAt = DateTime.Now.AddSeconds(1); _continueAt = DateTime.Now.AddSeconds(1);
} }
else if (SelectSelectString("advance", 0, s => s.StartsWith("Advance to the next phase of production."))) else if (SelectSelectString("advance", 0, s => s.StartsWith("Advance to the next phase of production.", StringComparison.Ordinal)))
{ {
_pluginLog.Information("Phase is complete"); _pluginLog.Information("Phase is complete");
@ -67,7 +67,7 @@ partial class WorkshopPlugin
CurrentStage = Stage.TargetFabricationStation; CurrentStage = Stage.TargetFabricationStation;
_continueAt = DateTime.Now.AddSeconds(3); _continueAt = DateTime.Now.AddSeconds(3);
} }
else if (SelectSelectString("complete", 0, s => s.StartsWith("Complete the construction of"))) else if (SelectSelectString("complete", 0, s => s.StartsWith("Complete the construction of", StringComparison.Ordinal)))
{ {
_pluginLog.Information("Item is almost complete, confirming last cutscene"); _pluginLog.Information("Item is almost complete, confirming last cutscene");
CurrentStage = Stage.TargetFabricationStation; CurrentStage = Stage.TargetFabricationStation;

View File

@ -119,7 +119,7 @@ partial class WorkshopPlugin
private void ConfirmCraft() private void ConfirmCraft()
{ {
if (SelectSelectYesno(0, s => s.StartsWith("Craft "))) if (SelectSelectYesno(0, s => s.StartsWith("Craft ", StringComparison.Ordinal)))
{ {
_configuration.CurrentlyCraftedItem!.StartedCrafting = true; _configuration.CurrentlyCraftedItem!.StartedCrafting = true;
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
@ -122,7 +123,9 @@ partial class WorkshopPlugin
LAddon.IsAddonReady(&addonSelectYesno->AtkUnitBase)) LAddon.IsAddonReady(&addonSelectYesno->AtkUnitBase))
{ {
var text = MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString(); var text = MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString();
text = text.Replace("\n", "").Replace("\r", ""); text = text
.Replace("\n", "", StringComparison.Ordinal)
.Replace("\r", "", StringComparison.Ordinal);
if (predicate(text)) if (predicate(text))
{ {
_pluginLog.Information($"Selecting choice {choice} for '{text}'"); _pluginLog.Information($"Selecting choice {choice} for '{text}'");
@ -184,17 +187,22 @@ partial class WorkshopPlugin
return null; return null;
} }
private uint ParseAtkItemCountHq(AtkValue atkValue) private static uint ParseAtkItemCountHq(AtkValue atkValue)
{ {
// NQ / HQ string // NQ / HQ string
// I have no clue, but it doesn't seme like the available HQ item count is strored anywhere in the atkvalues?? // I have no clue, but it doesn't seme like the available HQ item count is strored anywhere in the atkvalues??
string? s = atkValue.ReadAtkString(); string? s = atkValue.ReadAtkString();
if (s != null) if (s != null)
{ {
var parts = s.Replace("\ue03c", "").Split('/'); var parts = s.Replace("\ue03c", "", StringComparison.Ordinal).Split('/');
if (parts.Length > 1) if (parts.Length > 1)
{ {
return uint.Parse(parts[1].Replace(",", "").Replace(".", "").Trim()); return uint.Parse(
parts[1]
.Replace(",", "", StringComparison.Ordinal)
.Replace(".", "", StringComparison.Ordinal)
.Trim(),
CultureInfo.InvariantCulture);
} }
} }

View File

@ -13,7 +13,9 @@ partial class WorkshopPlugin
_pluginLog.Verbose("SelectYesNo post-setup"); _pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon; AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").Replace("\r", ""); string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString()
.Replace("\n", "", StringComparison.Ordinal)
.Replace("\r", "", StringComparison.Ordinal);
_pluginLog.Verbose($"YesNo prompt: '{text}'"); _pluginLog.Verbose($"YesNo prompt: '{text}'");
if (_repairKitWindow.IsOpen) if (_repairKitWindow.IsOpen)

View File

@ -15,7 +15,7 @@ using Workshoppa.Windows;
namespace Workshoppa; namespace Workshoppa;
[SuppressMessage("ReSharper", "UnusedType.Global")] [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
public sealed partial class WorkshopPlugin : IDalamudPlugin public sealed partial class WorkshopPlugin : IDalamudPlugin
{ {
private readonly IReadOnlyList<uint> _fabricationStationIds = private readonly IReadOnlyList<uint> _fabricationStationIds =

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Version>4.3</Version> <Version>5.0</Version>
<LangVersion>11.0</LangVersion> <LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net7.0-windows7.0": { "net8.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.12, )", "requested": "[2.1.12, )",

View File

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "7.0.0", "version": "8.0.0",
"rollForward": "latestMinor", "rollForward": "latestMinor",
"allowPrerelease": false "allowPrerelease": false
} }