Fix data export formatting and implement robust modifier detection
Major improvements to data cleaning, modifier extraction, and role name handling: ## Data Cleaning & Normalization - Enhanced StripColorTags() to remove ALL HTML/Unity tags using universal regex - Added removal of modifier symbols (♥, #, ★, etc.) from player names - Fixed winningTeam containing HTML tags by applying StripColorTags() - Result: Clean player names without tags or symbols in exported JSON ## Modifier Detection System - Implemented three-tier modifier detection: 1. Symbol extraction from <size=60%>SYMBOL</size> tags (primary) 2. Reflection-based GetModifiers<GameModifier>() (secondary) 3. RoleString parsing fallback (tertiary) - Fixed false positive Underdog detection from friend codes (username#1234) - Symbols now extracted ONLY from size tags, not entire player name - Fixed Dictionary duplicate key error (removed \u2665 duplicate of ♥) - Supported symbols: ♥❤♡ (Lovers), # (Underdog), ★☆ (VIP), @ (Sleuth), etc. ## Role Name Handling - Added fallback for roles without GetRoleName() method - Extracts role names from type names: "GrenadierRole" → "Grenadier" - Removes "Tou" suffix: "EngineerTou" → "Engineer" - Skips generic "RoleBehaviour" type (not a real role) - Result: Full role history now populated correctly ## Code Quality - Added comprehensive logging for debugging modifier/role extraction - Enhanced error handling with per-player try-catch blocks - Improved code documentation with inline comments ## Documentation - Added CLAUDE.md with complete architecture guide - Documented modifier detection system - Added data cleaning & normalization section - Included troubleshooting guide and development workflows Fixes: #1 (HTML tags in export), #2 (missing role history), #3 (false modifier detection) 🎮 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -92,17 +92,27 @@ public static class DataTransformer
|
||||
var player = PlayerControl.AllPlayerControls.ToArray()
|
||||
.FirstOrDefault(p => p.PlayerId == record.PlayerId);
|
||||
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($"Transforming player {record.PlayerId} ({record.PlayerName})");
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" RoleString: '{record.RoleString}'");
|
||||
|
||||
// STEP 1: Extract modifiers from player name BEFORE cleaning it
|
||||
// Symbols like ♥ indicate Lovers modifier
|
||||
var nameModifiers = ExtractModifiersFromPlayerName(record.PlayerName);
|
||||
|
||||
// Get role history for this player
|
||||
var roles = roleHistory.GetValueOrDefault(record.PlayerId, new List<string>());
|
||||
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" RoleHistory entries: {roles.Count}");
|
||||
|
||||
// If roleHistory is empty, try parsing from RoleString as fallback
|
||||
if (roles.Count == 0 && !string.IsNullOrEmpty(record.RoleString))
|
||||
{
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($"RoleHistory empty for player {record.PlayerId}, parsing from RoleString: {record.RoleString}");
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Parsing roles from RoleString...");
|
||||
roles = ParseRolesFromRoleString(record.RoleString);
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Parsed {roles.Count} role(s): {string.Join(", ", roles)}");
|
||||
}
|
||||
|
||||
|
||||
var lastRole = roles.LastOrDefault() ?? "Unknown";
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Final role: {lastRole}");
|
||||
|
||||
// Get stats
|
||||
var stats = playerStats.GetValueOrDefault(record.PlayerId, new PlayerStatsData());
|
||||
@@ -110,20 +120,35 @@ public static class DataTransformer
|
||||
// Count kills
|
||||
var kills = killedPlayers.Count(k => k.KillerId == record.PlayerId && k.VictimId != record.PlayerId);
|
||||
|
||||
// Get modifiers
|
||||
// STEP 2: Get modifiers from multiple sources
|
||||
var bridge = ReflectionBridgeProvider.GetBridge();
|
||||
var modifiers = bridge.GetPlayerModifiers(record.PlayerId);
|
||||
|
||||
// If no modifiers found via reflection, try parsing from RoleString
|
||||
if (modifiers.Count == 0 && !string.IsNullOrEmpty(record.RoleString))
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Reflection modifiers: {modifiers.Count}");
|
||||
|
||||
// Add modifiers from player name symbols
|
||||
if (nameModifiers.Count > 0)
|
||||
{
|
||||
modifiers = ParseModifiersFromRoleString(record.RoleString);
|
||||
if (modifiers.Count > 0)
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Name symbol modifiers: {nameModifiers.Count} ({string.Join(", ", nameModifiers)})");
|
||||
foreach (var mod in nameModifiers)
|
||||
{
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($"Parsed {modifiers.Count} modifier(s) from RoleString for player {record.PlayerId}");
|
||||
if (!modifiers.Contains(mod))
|
||||
{
|
||||
modifiers.Add(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still no modifiers, try parsing from RoleString
|
||||
if (modifiers.Count == 0 && !string.IsNullOrEmpty(record.RoleString))
|
||||
{
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Parsing modifiers from RoleString...");
|
||||
var roleStringMods = ParseModifiersFromRoleString(record.RoleString);
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Parsed {roleStringMods.Count} modifier(s): {string.Join(", ", roleStringMods)}");
|
||||
modifiers.AddRange(roleStringMods);
|
||||
}
|
||||
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Total modifiers: {modifiers.Count} ({string.Join(", ", modifiers)})");
|
||||
|
||||
// Get task info
|
||||
int totalTasks = 0;
|
||||
int completedTasks = 0;
|
||||
@@ -173,7 +198,8 @@ public static class DataTransformer
|
||||
// Use WinningFaction from GameHistory if available
|
||||
if (!string.IsNullOrEmpty(winningFaction))
|
||||
{
|
||||
return winningFaction;
|
||||
// Clean HTML tags from winning faction
|
||||
return StripColorTags(winningFaction);
|
||||
}
|
||||
|
||||
// Fallback: Check first winner's team
|
||||
@@ -198,6 +224,74 @@ public static class DataTransformer
|
||||
return MapNames.TryGetValue(mapId, out var name) ? name : $"Unknown Map ({mapId})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts modifier information from special symbols in player names.
|
||||
/// Symbols like ♥ (Lovers) appear inside HTML tags like: <b><size=60%>♥</size></b>
|
||||
/// </summary>
|
||||
/// <param name="playerName">Raw player name with HTML tags</param>
|
||||
/// <returns>List of modifier names found in the name</returns>
|
||||
private static List<string> ExtractModifiersFromPlayerName(string playerName)
|
||||
{
|
||||
var modifiers = new List<string>();
|
||||
|
||||
if (string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
// Map of symbols to modifier names
|
||||
// Note: \u2665 IS the same as ♥, so we only include one
|
||||
var symbolToModifier = new Dictionary<string, string>
|
||||
{
|
||||
{ "♥", "Lovers" }, // \u2665 in Unicode
|
||||
{ "❤", "Lovers" }, // \u2764
|
||||
{ "♡", "Lovers" }, // \u2661
|
||||
{ "#", "Underdog" },
|
||||
{ "★", "VIP" }, // \u2605
|
||||
{ "☆", "VIP" }, // \u2606
|
||||
{ "@", "Sleuth" },
|
||||
{ "†", "Bait" }, // \u2020
|
||||
{ "‡", "Bait" }, // \u2021
|
||||
{ "§", "Torch" },
|
||||
{ "⚡", "Flash" },
|
||||
{ "⚔", "Assassin" },
|
||||
{ "🗡", "Assassin" },
|
||||
};
|
||||
|
||||
// Extract content from size tags: <size=60%>SYMBOL</size>
|
||||
// This is where modifier symbols are located
|
||||
var sizeTagPattern = @"<size=[^>]*>([^<]+)</size>";
|
||||
var matches = Regex.Matches(playerName, sizeTagPattern);
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
if (match.Groups.Count > 1)
|
||||
{
|
||||
var symbolText = match.Groups[1].Value;
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Found symbol in size tag: '{symbolText}'");
|
||||
|
||||
// Check if this symbol matches any known modifier
|
||||
foreach (var kvp in symbolToModifier)
|
||||
{
|
||||
if (symbolText.Contains(kvp.Key))
|
||||
{
|
||||
if (!modifiers.Contains(kvp.Value))
|
||||
{
|
||||
modifiers.Add(kvp.Value);
|
||||
TownOfUsStatsPlugin.Logger.LogInfo($" Matched modifier '{kvp.Value}' from symbol '{kvp.Key}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips all HTML/Unity tags AND modifier symbols from text to get clean player name.
|
||||
/// Should be called AFTER extracting modifiers from symbols.
|
||||
/// </summary>
|
||||
private static string StripColorTags(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
@@ -205,8 +299,18 @@ public static class DataTransformer
|
||||
return text;
|
||||
}
|
||||
|
||||
text = Regex.Replace(text, @"<color=#[A-Fa-f0-9]+>", string.Empty);
|
||||
text = text.Replace("</color>", string.Empty);
|
||||
// Step 1: Remove all Unity/HTML tags
|
||||
// Pattern matches: <tag>, <tag=value>, <tag=#value>, </tag>
|
||||
text = Regex.Replace(text, @"<[^>]+>", string.Empty);
|
||||
|
||||
// Step 2: Remove all modifier symbols that were extracted
|
||||
// These symbols should not appear in the final clean name
|
||||
var symbolsToRemove = new[] { "♥", "❤", "♡", "#", "★", "☆", "@", "†", "‡", "§", "⚡", "⚔", "🗡" };
|
||||
foreach (var symbol in symbolsToRemove)
|
||||
{
|
||||
text = text.Replace(symbol, string.Empty);
|
||||
}
|
||||
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user