| name | sdv-data-validation |
| description | This skill should be used when the user asks to "validate data structures", "check save format", "update config options", or discusses data persistence. Also use when modifying FavoritesModel.cs, ModConfig.cs, modData conventions, JSON save files, or when preparing migrations for breaking changes. |
| version | 1.0.0 |
SDV Data Validation
Validate data structures and prevent regressions in save formats, config, and modData conventions.
When to Use This Skill
- Modifying
Models/FavoritesModel.cs(FavoritesData, FavoriteOutfit) - Modifying
ModConfig.cs - Changing modData key conventions
- Adding new save data fields
- Before commits affecting data persistence
Data Structure Validation
FavoritesData Schema
public class FavoritesData
{
public List<FavoriteOutfit> Favorites { get; set; }
}
public class FavoriteOutfit
{
// Metadata
public bool isFavorite { get; set; } = false;
public string Name { get; set; } = "";
public string Category { get; set; } = ""; // Spring/Summer/Fall/Winter/Special
public int LastWorn { get; set; } = 0;
// Item references (modData tags)
public Dictionary<string, string> Items { get; set; }
// Keys: "Hat", "Shirt", "Pants", "Shoes", "LeftRing", "RightRing"
// Values: modData tag like "Hat_abc123"
// Item IDs (for recreation)
public Dictionary<string, string> ItemIds { get; set; }
// Keys: "Hat", "Shirt", "Pants", "Shoes", "LeftRing", "RightRing"
// Values: QualifiedItemId like "(H)6", "(S)1001"
// Dye colors
public Dictionary<string, string> ItemColors { get; set; }
// Keys: "Shirt", "Pants" (only dyeable items)
// Values: "R,G,B,A" format
// Appearance
public string HairIndex { get; set; } = "";
public int Hair { get; set; } = 0;
public string AccessoryIndex { get; set; } = "";
public int Accessory { get; set; } = 0;
}
Validation Checks
Required Fields:
-
Nameis non-null (can be empty) -
Categoryis valid: "Spring", "Summer", "Fall", "Winter", or "Special" -
Itemsdictionary is non-null -
ItemIdsdictionary is non-null -
ItemColorsdictionary is non-null
Item References:
- Item keys are valid slot names: "Hat", "Shirt", "Pants", "Shoes", "LeftRing", "RightRing"
- ItemIds values are valid QualifiedItemIds (start with type prefix)
- ItemColors values match "R,G,B,A" format
Data Integrity:
- If Items has a slot, ItemIds should have the same slot
- ItemColors only contains keys for dyeable items ("Shirt", "Pants")
ModConfig Validation
Current Schema
public sealed class ModConfig
{
public bool StartingDresser { get; set; } = true;
public bool RobinSellsDressers { get; set; } = true;
public bool TravelingMerchantSellsDressers { get; set; } = true;
public bool IncludeRingsInOutfits { get; set; } = true;
public bool IncludeFacialHair { get; set; } = false;
public bool IncludeModdedHairstyles { get; set; } = true;
public bool IncludeModdedAccessories { get; set; } = true;
public DresserSharingMode DresserInventorySharing { get; set; } = DresserSharingMode.Touching;
}
public enum DresserSharingMode { Individual, Touching, SameBuilding }
Validation Checks
When adding new config options:
- Added to
ModConfig.cswith proper default value - Added to GMCM registration in
StardewOutfitManager.cs - Default value matches intended behavior
- XML documentation added
GMCM Registration Pattern:
api.AddBoolOption(
mod: ModManifest,
name: () => "Option Name",
tooltip: () => "Description of what this does",
getValue: () => Config.OptionName,
setValue: v => Config.OptionName = v
);
ModData Key Conventions
Correct Format
// Prefix with mod's UniqueID
item.modData[$"{ModId}/FavoriteItem"] = "value";
// This mod's convention (from manifest.json: LiminalWarmth.StardewOutfitManager)
item.modData["LiminalWarmth.StardewOutfitManager/FavoriteItem"] = $"{item.Name}_{Guid.NewGuid()}";
Validation Checks
- All modData keys start with
LiminalWarmth.StardewOutfitManager/ - No legacy keys without prefix (migration needed if found)
- Keys are consistent across codebase
Migration for Legacy Keys
If old keys exist without prefix:
// Check for legacy key and migrate
if (item.modData.TryGetValue("StardewOutfitManagerFavoriteItem", out string legacyValue))
{
// Migrate to new key
item.modData[$"{ModId}/FavoriteItem"] = legacyValue;
item.modData.Remove("StardewOutfitManagerFavoriteItem");
}
Save File Paths
Correct Pattern
// Unique per player, per save, per multiplayer session
string path = $"data/favoritesData/{Game1.player.Name}_{Constants.SaveFolderName}_{Game1.player.UniqueMultiplayerID}.json";
// Use Helper.Data API
Helper.Data.WriteJsonFile<FavoritesData>(path, data);
var loaded = Helper.Data.ReadJsonFile<FavoritesData>(path);
Validation Checks
- Path includes player name
- Path includes save folder
- Path includes multiplayer ID
- Using
Helper.DataAPI (not raw file I/O)
Breaking Change Migration
When making breaking changes to data structures:
1. Identify Breaking Changes
Examples of breaking changes:
- Renaming a property
- Changing a property type
- Removing a required field
- Changing dictionary key format
2. Create Migration Code
private FavoritesData MigrateData(FavoritesData data, int fromVersion)
{
// Version 1 -> 2: Renamed "Outfit" to "Items"
if (fromVersion < 2)
{
foreach (var outfit in data.Favorites)
{
// Migration logic
}
}
// Version 2 -> 3: Added ItemColors
if (fromVersion < 3)
{
foreach (var outfit in data.Favorites)
{
outfit.ItemColors ??= new Dictionary<string, string>();
}
}
return data;
}
3. Handle Null/Missing Fields
Always use null-safe access:
// WRONG: Assumes Items exists
var hatTag = outfit.Items["Hat"];
// CORRECT: Null-safe
if (outfit.Items?.TryGetValue("Hat", out string hatTag) == true)
{
// Use hatTag
}
// Also initialize missing dictionaries on load
outfit.Items ??= new Dictionary<string, string>();
outfit.ItemIds ??= new Dictionary<string, string>();
outfit.ItemColors ??= new Dictionary<string, string>();
Audit Output Format
## Data Validation Results
### Schema Issues
- [ ] FavoriteOutfit.Category allows invalid values (no validation)
- [ ] Missing null check for Items dictionary in LoadFavorites
### Config Issues
- [ ] New option `DebugMode` not registered in GMCM
### ModData Key Issues
- [ ] Legacy key "StardewOutfitManagerFavoriteItem" found in FavoritesMethods.cs:45
- Fix: Migrate to "LiminalWarmth.StardewOutfitManager/FavoriteItem"
### Breaking Changes
- [ ] Property renamed from `Outfit` to `Items` - migration required
### Passed Checks
- [x] All config options have defaults
- [x] Save paths include all required components
- [x] ItemIds use valid QualifiedItemId format
Quick Reference
Valid QualifiedItemId Prefixes
(O) Object
(BC) BigCraftable
(F) Furniture
(W) Weapon
(B) Boots
(H) Hat
(P) Pants
(S) Shirt
(T) Tool
(TR) Trinket
(R) Ring
Valid Category Values
"Spring"
"Summer"
"Fall"
"Winter"
"Special"
Valid Item Slot Keys
"Hat"
"Shirt"
"Pants"
"Shoes"
"LeftRing"
"RightRing"