diff --git a/README.md b/README.md
index c63f543..be87bab 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,13 @@

# Polaris_TKDataModEditor
wip....
+
+## Recovering from malformed customization-item mods
+
+If a malformed `customize_item_*.bin` mod item was equipped in-game, the game may keep referencing that item from save data even after uninstalling the mod.
+
+Recommended recovery flow:
+1. Re-enable the mod that contains the equipped item.
+2. Enter customization and switch every equipped modded part back to a valid vanilla item.
+3. Save, close the game, then remove the mod again.
+4. If the error still persists, restore a backup save from before the malformed item was equipped (or delete the affected save/profile data and start from a clean save).
diff --git a/src/fbsdata/io/TkmodIO.cpp b/src/fbsdata/io/TkmodIO.cpp
index aaf00b2..4da17d2 100644
--- a/src/fbsdata/io/TkmodIO.cpp
+++ b/src/fbsdata/io/TkmodIO.cpp
@@ -385,6 +385,30 @@ static bool ParseFloat(const char* p, const char* end, float& out)
return ep != p;
}
+static bool IsNonEmptyText(const char* s)
+{
+ return s && s[0] != '\0';
+}
+
+static bool IsValidCustomizeItemCommonEntry(const CustomizeItemCommonEntry& e)
+{
+ return e.item_id != 0
+ && IsNonEmptyText(e.item_code)
+ && IsNonEmptyText(e.text_key)
+ && IsNonEmptyText(e.package_id)
+ && IsNonEmptyText(e.package_sub_id);
+}
+
+static bool IsValidCustomizeItemUniqueEntry(const CustomizeItemUniqueEntry& e)
+{
+ return e.char_item_id != 0 && IsNonEmptyText(e.asset_name);
+}
+
+static bool IsValidCustomizeItemUniqueBodyEntry(const CustomizeItemUniqueBodyEntry& e)
+{
+ return e.char_item_id != 0 && IsNonEmptyText(e.asset_name);
+}
+
static bool ParseCustomizeItemCommonEntry(const char* start, const char* end,
CustomizeItemCommonEntry& e)
{
@@ -433,7 +457,8 @@ static bool ParseCustomizeItemCommonJson(const std::string& json, ContentsBinDat
CustomizeItemCommonEntry entry;
ParseCustomizeItemCommonEntry(arr, objEnd, entry);
- bin.commonEntries.push_back(entry);
+ if (IsValidCustomizeItemCommonEntry(entry))
+ bin.commonEntries.push_back(entry);
arr = objEnd;
}
// Skip commas and whitespace between entries
@@ -637,8 +662,8 @@ static void ParseExclusiveArray(const char* json, const char* arrayKey,
const char* objEnd = SkipObject(arr);
if (!objEnd) break;
TEntry entry{};
- parseFn(arr, objEnd, entry);
- out.push_back(entry);
+ if (parseFn(arr, objEnd, entry))
+ out.push_back(entry);
arr = objEnd;
}
while (*arr == ',' || *arr == ' ' || *arr == '\n' || *arr == '\r' || *arr == '\t')
@@ -1390,15 +1415,15 @@ static bool ParseCustomizeItemUniqueListJson(const std::string& json, ContentsBi
{ auto* fp = FindField(s, e, "id_19"); ParseUInt32(fp, e, entry.unk_19); }
{ auto* fp = FindField(s, e, "id_20"); ParseUInt32(fp, e, entry.unk_20); }
{ auto* fp = FindField(s, e, "id_21"); ParseUInt32(fp, e, entry.unk_21); }
- return true;
+ return IsValidCustomizeItemUniqueEntry(entry);
});
ParseExclusiveArray(p, "\"id_1\"", bin.customizeItemUniqueBodyEntries,
[](const char* s, const char* e, CustomizeItemUniqueBodyEntry& entry) -> bool {
{ auto* fp = FindField(s, e, "id_0"); ParseString(fp, e, entry.asset_name, sizeof(entry.asset_name)); }
{ auto* fp = FindField(s, e, "id_1"); ParseUInt32(fp, e, entry.char_item_id); }
- return true;
+ return IsValidCustomizeItemUniqueBodyEntry(entry);
});
- return true;
+ return !bin.customizeItemUniqueEntries.empty() || !bin.customizeItemUniqueBodyEntries.empty();
}
// -----------------------------------------------------------------------------
@@ -2545,9 +2570,11 @@ namespace TkmodIO
ContentsBinData bin;
bin.type = BinType::CustomizeItemUniqueList;
bin.name = binName;
- ParseCustomizeItemUniqueListJson(jsonStr, bin);
- loaded.selectedIndex = static_cast(loaded.contents.size());
- loaded.contents.push_back(std::move(bin));
+ if (ParseCustomizeItemUniqueListJson(jsonStr, bin))
+ {
+ loaded.selectedIndex = static_cast(loaded.contents.size());
+ loaded.contents.push_back(std::move(bin));
+ }
}
else if (binName == "character_select_list.bin")
{