fix(vt): D CreateNew seeds from VTank's real defaultsettings.usd

Item D (slice-1 fix round). CreateNew built ONLY a bespoke 4-column
Settings table (empty Description, hardcoded SettingType=1/Bool for
every row regardless of the setting's real declared type) and had no
representation of the other nine tables VTank ships at all (MyMonsters,
GemFoodItems, ExtraBuffSpells, AntiExtraBuffSpells, ItemUseSpecifiers,
SettingsCategories, SettingsEnumInfo, AssistItems, BuffedItems).
Separately, VitalSettings.RechargeHandlerRows defaulted to an empty
list, so a freshly-constructed VitalSettings (not loaded from a real
.usd) always fell back to VitalRechargePlanner's own hand-ported
handler-order switch instead of VTank's real 26-row table.

- VtankDefaultSettings.usd: the real uTank2.Resources.defaultsettings.usd
  (already committed as a test fixture) copied into the plugin project as
  an EmbeddedResource, following the existing VtankCraftRecipes.tsv/
  VtankAmmunitionOptions.tsv pattern.
- New VtankDefaultSettingsDatabase: loads the embedded text once (Lazy),
  exposes Parse() (a fresh, independently mutable VtankDatabase per call)
  and DefaultRechargeHandlerRows (the real RechargeHandlerSet table,
  parsed once via VtankSettingsProfileSerializer.ParseRechargeHandlerSet).
- VtankSettingsProfileSerializer.CreateNew now parses the embedded
  document and calls the existing Save(document, source) against it —
  every table, every row's real Description/SettingType survive
  untouched; only the Settings table's Value cells get overwritten from
  the live source, exactly as re-saving an existing profile would.
  DefaultCell (the hand-typed per-catalog-kind cell builder CreateNew
  used before) is deleted as dead code.
- VitalSettings.RechargeHandlerRows now defaults to
  VtankDefaultSettingsDatabase.DefaultRechargeHandlerRows instead of [].
  VitalRechargePlanner.Handlers' hardcoded switch-based defaults are kept
  (not deleted) as a documented, genuinely defensive fallback for the
  case where a caller explicitly clears the row list or an
  (vital, stance, percent) combination the real table happens not to
  cover — no longer the primary source of truth it used to be, and the
  doc comment says so.
- RenderRechargeHandlerSet (a write-back helper for the RechargeHandlerSet
  table, never called anywhere) is deleted rather than wired into
  Capture(): docs/research/vtank-kb/01-settings-and-profiles.md section 2
  row 137 confirms real VTank has NO save path for this table at all
  (tCustom has no case in the Advanced Options editor or "/vt opt set";
  it's owned by a dedicated cRechargeManager object). Wiring a write-back
  would be a MossTank invention, not a retail port, so Capture() keeps
  deliberately leaving this row untouched.

New CreateNewHasTheSameTableSetAsTheDefaultFixture test: asserts
CreateNew's table set matches defaultsettings.usd's own table set
exactly, and that Settings rows carry real (non-empty) Description and a
real (non-Bool-for-everything) SettingType. Verified failing against the
prior 4-column-only implementation (temporarily restored, ran, reverted):
"Expected: [AntiExtraBuffSpells, AssistItems, ...] / Actual: [Settings]".

Full MossTank suite: 570/570 passing (569 -> 570, one new test).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 22:33:31 +02:00
parent d0247dbb19
commit e1d0c703f9
7 changed files with 2428 additions and 49 deletions

View file

@ -24,5 +24,6 @@
</None>
<EmbeddedResource Include="VtankCraftRecipes.tsv" />
<EmbeddedResource Include="VtankAmmunitionOptions.tsv" />
<EmbeddedResource Include="VtankDefaultSettings.usd" />
</ItemGroup>
</Project>

View file

@ -48,12 +48,17 @@ public sealed class VitalSettings
public string RechargeHandlerSet { get; set; } = "RechargeHandlerSet";
/// <summary>
/// The real VTank <c>RechargeHandlerSet</c> nested table (5 columns:
/// Vital/HandlerString/MinPercent/MaxPercent/Stance), parsed from a
/// loaded <c>.usd</c> profile by <see cref="VtankSettingsProfileSerializer"/>.
/// Empty until a profile with this table is loaded; <see cref="VitalRechargePlanner"/>
/// falls back to its hand-ported default ordering when this is empty.
/// Vital/HandlerString/MinPercent/MaxPercent/Stance). Defaults to VTank's
/// own shipped 26-row table (<see cref="VtankDefaultSettingsDatabase.DefaultRechargeHandlerRows"/>),
/// not an independently hand-typed replica; loading a real <c>.usd</c>
/// profile via <see cref="VtankSettingsProfileSerializer"/> replaces it
/// with that profile's own table. <see cref="VitalRechargePlanner"/>'s
/// hand-ported switch-based ordering is retained only as a defensive
/// fallback for the case where every row happens to have been cleared
/// explicitly and none match a given (vital, stance, percent) band.
/// </summary>
public IReadOnlyList<RechargeHandlerRow> RechargeHandlerRows { get; set; } = [];
public IReadOnlyList<RechargeHandlerRow> RechargeHandlerRows { get; set; } =
VtankDefaultSettingsDatabase.DefaultRechargeHandlerRows;
public bool UseKitsInMagicMode { get; set; } = true;
public bool GoToPeaceModeToUseKits { get; set; }
public int MinimumHealKitSuccessChance { get; set; } = 95;

View file

@ -245,6 +245,18 @@ internal static class VitalRechargePlanner
return true;
}
/// <summary>
/// <paramref name="handlerRows"/> is <see cref="VitalSettings.RechargeHandlerRows"/>,
/// which defaults to VTank's own shipped 26-row table
/// (<see cref="VtankDefaultSettingsDatabase.DefaultRechargeHandlerRows"/>)
/// rather than an empty list, so <see cref="TryHandlersFromRows"/> is the
/// path every ordinary call takes. The hardcoded <c>defaults</c> switch
/// below is retained ONLY as a defensive net for a caller that
/// deliberately passes an empty/null row set or an unrecognized
/// (vital, stance, percent) band the real table happens not to cover —
/// not as the primary source of truth it used to be before the real
/// table became parseable.
/// </summary>
internal static IReadOnlyList<VitalRechargeMethod> Handlers(
VitalKind vital,
bool magicMode,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
using System.Reflection;
namespace AcDream.Plugins.MossTank;
/// <summary>
/// The complete <c>.usd</c> shipped by VTank as
/// <c>uTank2.Resources.defaultsettings.usd</c> — every table (Settings,
/// MyMonsters, GemFoodItems, ExtraBuffSpells, AntiExtraBuffSpells,
/// ItemUseSpecifiers, SettingsCategories, SettingsEnumInfo, AssistItems,
/// BuffedItems, RechargeHandlerSet, …), byte-for-byte, not a hand-typed
/// C# replica. <see cref="VtankSettingsProfileSerializer.CreateNew"/> seeds
/// a brand-new profile from this document (every table, every row's real
/// <c>Description</c>/<c>SettingType</c>) and
/// <see cref="VitalSettings"/>'s default <c>RechargeHandlerRows</c> is
/// parsed from its <c>RechargeHandlerSet</c> table — both replace a prior
/// port's independently hand-maintained defaults, which could (and did:
/// see the 8fa70c3e4 "26 rows, not 24" fix) silently drift from what VTank
/// itself ships.
/// </summary>
internal static class VtankDefaultSettingsDatabase
{
private const string ResourceSuffix = ".VtankDefaultSettings.usd";
private static readonly Lazy<string> RawText = new(LoadText);
private static readonly Lazy<RechargeHandlerRow[]> DefaultRows = new(LoadDefaultRows);
/// <summary>A fresh, independently mutable parse of the embedded document — every call gets its own object graph.</summary>
public static VtankDatabase Parse() => VtankDatabase.Parse(RawText.Value);
/// <summary>VTank's own shipped <c>RechargeHandlerSet</c> rows (26, in file order).</summary>
public static IReadOnlyList<RechargeHandlerRow> DefaultRechargeHandlerRows => DefaultRows.Value;
private static string LoadText()
{
Assembly assembly = typeof(VtankDefaultSettingsDatabase).Assembly;
string resource = assembly.GetManifestResourceNames().Single(
static name => name.EndsWith(ResourceSuffix, StringComparison.Ordinal));
using Stream stream = assembly.GetManifestResourceStream(resource)
?? throw new InvalidOperationException(
"The embedded VTank default settings (.usd) document is missing.");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
private static RechargeHandlerRow[] LoadDefaultRows()
{
VtankDatabase database = VtankDatabase.Parse(RawText.Value);
VtankTable? settings = database.Find("Settings");
if (settings is null)
return [];
int nameColumn = settings.ColumnIndex("Setting");
int valueColumn = settings.ColumnIndex("Value");
if (nameColumn < 0 || valueColumn < 0)
return [];
foreach (VtankRow row in settings.Rows)
{
if (!row.Cells[nameColumn].AsString().Equals(
"RechargeHandlerSet", StringComparison.OrdinalIgnoreCase))
{
continue;
}
VtankCell cell = row.Cells[valueColumn];
if (cell.Tag == "TABLE" && cell.Table is { } table)
return VtankSettingsProfileSerializer.ParseRechargeHandlerSet(table);
}
return [];
}
}

View file

@ -102,38 +102,28 @@ internal static class VtankSettingsProfileSerializer
return document.Render();
}
/// <summary>Builds a brand-new database (Settings table only) seeded from <paramref name="source"/>.</summary>
/// <summary>
/// Builds a brand-new database seeded from VTank's own shipped
/// <c>defaultsettings.usd</c> (<see cref="VtankDefaultSettingsDatabase"/>)
/// — every table (MyMonsters, GemFoodItems, ExtraBuffSpells,
/// AntiExtraBuffSpells, ItemUseSpecifiers, SettingsCategories,
/// SettingsEnumInfo, AssistItems, BuffedItems, RechargeHandlerSet, …),
/// not a hand-typed 4-column Settings-only replica: a prior version of
/// this method built only the Settings table, with an empty
/// Description and a hardcoded SettingType of 1 (Bool) for every row
/// regardless of the setting's real declared type. <see cref="Save"/>
/// then rewrites every Settings row's Value cell from
/// <paramref name="source"/>'s current values, exactly as it would for
/// an existing profile being re-saved untouched.
/// </summary>
public static VtankDatabase CreateNew(AllSettings source)
{
var table = new VtankTable();
table.ColumnNames.AddRange(["Setting", "Value", "Description", "SettingType"]);
table.IndexFlags.AddRange([true, false, false, false]);
foreach (string name in VtankOptionCatalog.Names)
{
VtankCell value = Capture(name, source) ?? DefaultCell(name);
var row = new VtankRow();
row.Cells.Add(VtankCell.String(name));
row.Cells.Add(value);
row.Cells.Add(VtankCell.String(string.Empty));
row.Cells.Add(VtankCell.Int(1));
table.Rows.Add(row);
}
var database = new VtankDatabase();
database.Tables.Add((SettingsTable, table));
ArgumentNullException.ThrowIfNull(source);
VtankDatabase database = VtankDefaultSettingsDatabase.Parse();
_ = Save(database, source);
return database;
}
private static VtankCell DefaultCell(string name)
{
MonsterValue value = VtankOptionCatalog.Default(name);
return value.Kind switch
{
MonsterValueKind.Boolean => VtankCell.Bool(value.Boolean),
MonsterValueKind.Text => VtankCell.String(value.Text),
_ => VtankCell.Double(value.Number),
};
}
/// <summary>
/// Converts a live numeric setting value to the exact
/// <see cref="VtankCell"/> shape its declared <c>SettingType</c>
@ -550,21 +540,14 @@ internal static class VtankSettingsProfileSerializer
return rows;
}
internal static VtankTable RenderRechargeHandlerSet(IReadOnlyList<RechargeHandlerRow> rows)
{
var table = new VtankTable();
table.ColumnNames.AddRange(["Vital", "HandlerString", "MinPercent", "MaxPercent", "Stance"]);
table.IndexFlags.AddRange([false, false, false, false, false]);
foreach (RechargeHandlerRow row in rows)
{
var vtankRow = new VtankRow();
vtankRow.Cells.Add(VtankCell.Int(row.Vital));
vtankRow.Cells.Add(VtankCell.String(row.HandlerString));
vtankRow.Cells.Add(VtankCell.Int(row.MinPercent));
vtankRow.Cells.Add(VtankCell.Int(row.MaxPercent));
vtankRow.Cells.Add(VtankCell.Int(row.Stance));
table.Rows.Add(vtankRow);
}
return table;
}
// No RenderRechargeHandlerSet/write-back helper exists here deliberately:
// real VTank has NO save path for this table at all (docs/research/
// vtank-kb/01-settings-and-profiles.md section 2, row 137 — tCustom has
// no case in the Advanced Options editor's display/edit switch or in
// "/vt opt set"; the table is owned by a dedicated cRechargeManager
// object, not any control that persists through the generic Settings
// save path). Writing it back would be a MossTank invention, not a
// retail port. A prior version of this file had an unused
// RenderRechargeHandlerSet helper for exactly that invention; it was
// deleted rather than wired in for this reason.
}