Files
WulaFallenEmpireRW/Source/WulaFallenEmpire/EventSystem/AI/Utils/ThingDefSearcher.cs
2025-12-13 14:56:38 +08:00

133 lines
4.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using RimWorld;
using Verse;
namespace WulaFallenEmpire.EventSystem.AI.Utils
{
public static class ThingDefSearcher
{
public struct SearchResult
{
public ThingDef Def;
public int Count;
public float Score;
}
/// <summary>
/// Parses a natural language request string into a list of spawnable items.
/// Example: "5 beef, 1 persona core, 30 wood"
/// </summary>
public static List<SearchResult> ParseAndSearch(string request)
{
var results = new List<SearchResult>();
if (string.IsNullOrEmpty(request)) return results;
// Split by common separators
var parts = request.Split(new[] { ',', '', ';', '、', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var result = ParseSingleItem(part.Trim());
if (result.Def != null)
{
results.Add(result);
}
}
return results;
}
private static SearchResult ParseSingleItem(string itemRequest)
{
// Extract count and name
// Regex to match "number name" or "name number" or "number个name"
// Supports Chinese and English numbers
int count = 1;
string nameQuery = itemRequest;
// Try to find digits
var match = Regex.Match(itemRequest, @"(\d+)");
if (match.Success)
{
if (int.TryParse(match.Value, out int parsedCount))
{
count = parsedCount;
// Remove the number from the query string to get the name
nameQuery = itemRequest.Replace(match.Value, "").Trim();
// Remove common quantifiers
nameQuery = Regex.Replace(nameQuery, @"[个只把张条xX×]", "").Trim();
}
}
if (string.IsNullOrWhiteSpace(nameQuery))
{
return new SearchResult { Def = null, Count = 0, Score = 0 };
}
// Search for the Def
var bestMatch = FindBestThingDef(nameQuery);
return new SearchResult
{
Def = bestMatch.Def,
Count = count,
Score = bestMatch.Score
};
}
private static (ThingDef Def, float Score) FindBestThingDef(string query)
{
ThingDef bestDef = null;
float bestScore = 0f;
string lowerQuery = query.ToLower();
foreach (var def in DefDatabase<ThingDef>.AllDefs)
{
// Filter out non-items or abstract defs
if (def.category != ThingCategory.Item && def.category != ThingCategory.Building) continue;
if (def.label == null) continue;
float score = 0f;
string label = def.label.ToLower();
string defName = def.defName.ToLower();
// Exact match
if (label == lowerQuery) score = 1.0f;
else if (defName == lowerQuery) score = 0.9f;
// Starts with match (High priority for defNames like "WoodLog" when searching "Wood")
else if (defName.StartsWith(lowerQuery))
{
score = 0.8f + (0.1f * ((float)lowerQuery.Length / defName.Length));
}
// Contains match
else if (label.Contains(lowerQuery))
{
// Shorter labels that contain the query are better matches
score = 0.6f + (0.2f * ((float)lowerQuery.Length / label.Length));
}
else if (defName.Contains(lowerQuery))
{
score = 0.5f + (0.2f * ((float)lowerQuery.Length / defName.Length));
}
// Bonus for tradeability (more likely to be what player wants)
if (def.tradeability != Tradeability.None) score += 0.05f;
if (score > bestScore)
{
bestScore = score;
bestDef = def;
}
}
// Threshold
if (bestScore < 0.4f) return (null, 0f);
return (bestDef, bestScore);
}
}
}