diff --git a/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/Assemblies/WulaFallenEmpire.dll
index 4b985b38..01a4d656 100644
Binary files a/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/Assemblies/WulaFallenEmpire.dll differ
diff --git a/1.6/Defs/ThingDefs_Misc/Weapons/WULA_FE_Spiritualist_Weapon.xml b/1.6/Defs/ThingDefs_Misc/Weapons/WULA_FE_Spiritualist_Weapon.xml
index a7f1ca5e..ef8f9d5f 100644
--- a/1.6/Defs/ThingDefs_Misc/Weapons/WULA_FE_Spiritualist_Weapon.xml
+++ b/1.6/Defs/ThingDefs_Misc/Weapons/WULA_FE_Spiritualist_Weapon.xml
@@ -80,6 +80,15 @@
2
60
+
+
+ WULA_DamagePsychicScaling
+
+
+ 1
+ 1
+
+
None
@@ -246,6 +255,15 @@
2
60
+
+
+ WULA_DamagePsychicScaling
+
+
+ 1
+ 1
+
+
None
@@ -409,6 +427,15 @@
20
60
+
+
+ WULA_DamagePsychicScaling
+
+
+ 1
+ 1
+
+
None
diff --git a/1.6/Defs/ThingDefs_Misc/Weapons/WULA_Weapon.xml b/1.6/Defs/ThingDefs_Misc/Weapons/WULA_Weapon.xml
index 714e0e2e..102c2dad 100644
--- a/1.6/Defs/ThingDefs_Misc/Weapons/WULA_Weapon.xml
+++ b/1.6/Defs/ThingDefs_Misc/Weapons/WULA_Weapon.xml
@@ -1106,12 +1106,12 @@
- Verb_Shoot
+ WulaFallenEmpire.Verb_ShootShotgun
true
WULA_Bullet_StarDrift_Shotgun_Spear
0.2
15
- 6
+ 1
3
ChargeLance_Fire
GunTail_Heavy
@@ -1133,6 +1133,11 @@
0.65
55
+
+
+ 6
+
+
diff --git a/1.6/Defs/WeaponTraitDefs/WULA_WeaponCategoryDefs.xml b/1.6/Defs/WeaponTraitDefs/WULA_WeaponCategoryDefs.xml
new file mode 100644
index 00000000..5731dd18
--- /dev/null
+++ b/1.6/Defs/WeaponTraitDefs/WULA_WeaponCategoryDefs.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ WULA_Psychic
+
+ 与心灵能量相互作用的武器。
+
+
+
\ No newline at end of file
diff --git a/1.6/Defs/WeaponTraitDefs/WULA_WeaponTraitDefs.xml b/1.6/Defs/WeaponTraitDefs/WULA_WeaponTraitDefs.xml
new file mode 100644
index 00000000..2498b14a
--- /dev/null
+++ b/1.6/Defs/WeaponTraitDefs/WULA_WeaponTraitDefs.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ WULA_DamagePsychicScaling
+
+ 这把武器的伤害会随着使用者的心灵敏感度而变化。
+ 1
+ WULA_Psychic
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/MCP/mcpserver_stdio.py b/Source/MCP/mcpserver_stdio.py
index d78758c6..6c25ef40 100644
--- a/Source/MCP/mcpserver_stdio.py
+++ b/Source/MCP/mcpserver_stdio.py
@@ -45,28 +45,32 @@ KNOWLEDGE_BASE_PATHS = [
r"C:\Steam\steamapps\common\RimWorld\Data"
]
-# 3. --- 缓存管理 ---
-def load_cache():
- """加载缓存文件"""
- if os.path.exists(CACHE_FILE_PATH):
- try:
- with open(CACHE_FILE_PATH, 'r', encoding='utf-8') as f:
- return json.load(f)
- except (json.JSONDecodeError, IOError) as e:
- logging.error(f"读取缓存文件失败: {e}")
- return {}
- return {}
+# 3. --- 缓存管理 (分文件存储) ---
+def load_cache_for_keyword(keyword: str):
+ """为指定关键词加载缓存文件。"""
+ # 清理关键词,使其适合作为文件名
+ safe_filename = "".join(c for c in keyword if c.isalnum() or c in ('_', '-')).rstrip()
+ cache_file = os.path.join(CACHE_DIR, f"{safe_filename}.txt")
+
+ if os.path.exists(cache_file):
+ try:
+ with open(cache_file, 'r', encoding='utf-8') as f:
+ return f.read()
+ except IOError as e:
+ logging.error(f"读取缓存文件 {cache_file} 失败: {e}")
+ return None
+ return None
-def save_cache(cache_data):
- """保存缓存到文件"""
- try:
- with open(CACHE_FILE_PATH, 'w', encoding='utf-8') as f:
- json.dump(cache_data, f, ensure_ascii=False, indent=4)
- except IOError as e:
- logging.error(f"写入缓存文件失败: {e}")
-
-# 加载初始缓存
-knowledge_cache = load_cache()
+def save_cache_for_keyword(keyword: str, data: str):
+ """为指定关键词保存缓存到单独的文件。"""
+ safe_filename = "".join(c for c in keyword if c.isalnum() or c in ('_', '-')).rstrip()
+ cache_file = os.path.join(CACHE_DIR, f"{safe_filename}.txt")
+
+ try:
+ with open(cache_file, 'w', encoding='utf-8') as f:
+ f.write(data)
+ except IOError as e:
+ logging.error(f"写入缓存文件 {cache_file} 失败: {e}")
# 4. --- 向量化与相似度计算 ---
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
@@ -87,23 +91,121 @@ def get_embedding(text: str):
logging.error(f"调用向量API时出错: {e}", exc_info=True)
raise
-def find_most_similar_file(question_embedding, file_embeddings):
- """在文件向量中找到与问题向量最相似的一个"""
- if not question_embedding or not file_embeddings:
- return None
-
- # 将文件嵌入列表转换为NumPy数组
- file_vectors = np.array([emb['embedding'] for emb in file_embeddings])
- question_vector = np.array(question_embedding).reshape(1, -1)
-
- # 计算余弦相似度
- similarities = cosine_similarity(question_vector, file_vectors)[0]
-
- # 找到最相似的文件的索引
- most_similar_index = np.argmax(similarities)
-
- # 返回最相似的文件路径
- return file_embeddings[most_similar_index]['path']
+def find_most_similar_files(question_embedding, file_embeddings, top_n=3, min_similarity=0.5):
+ """在文件向量中找到与问题向量最相似的 top_n 个文件。"""
+ if not question_embedding or not file_embeddings:
+ return []
+
+ file_vectors = np.array([emb['embedding'] for emb in file_embeddings])
+ question_vector = np.array(question_embedding).reshape(1, -1)
+
+ similarities = cosine_similarity(question_vector, file_vectors)[0]
+
+ # 获取排序后的索引
+ sorted_indices = np.argsort(similarities)[::-1]
+
+ # 筛选出最相关的结果
+ results = []
+ for i in sorted_indices:
+ similarity_score = similarities[i]
+ if similarity_score >= min_similarity and len(results) < top_n:
+ results.append({
+ 'path': file_embeddings[i]['path'],
+ 'similarity': similarity_score
+ })
+ else:
+ break
+
+ return results
+
+def extract_relevant_code(file_path, keyword):
+ """从文件中智能提取包含关键词的完整代码块 (C#类 或 XML Def)。"""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ lines = content.split('\n')
+ keyword_lower = keyword.lower()
+
+ found_line_index = -1
+ for i, line in enumerate(lines):
+ if keyword_lower in line.lower():
+ found_line_index = i
+ break
+
+ if found_line_index == -1:
+ return ""
+
+ # 根据文件类型选择提取策略
+ if file_path.endswith(('.cs', '.txt')):
+ # C# 提取策略:寻找完整的类
+ return extract_csharp_class(lines, found_line_index)
+ elif file_path.endswith('.xml'):
+ # XML 提取策略:寻找完整的 Def
+ return extract_xml_def(lines, found_line_index)
+ else:
+ return "" # 不支持的文件类型
+
+ except Exception as e:
+ logging.error(f"提取代码时出错 {file_path}: {e}")
+ return f"# Error reading file: {e}"
+
+def extract_csharp_class(lines, start_index):
+ """从C#代码行中提取完整的类定义。"""
+ # 向上找到 class 声明
+ class_start_index = -1
+ brace_level_at_class_start = -1
+ for i in range(start_index, -1, -1):
+ line = lines[i]
+ if 'class ' in line:
+ class_start_index = i
+ brace_level_at_class_start = line.count('{') - line.count('}')
+ break
+
+ if class_start_index == -1: return "" # 没找到类
+
+ # 从 class 声明开始,向下找到匹配的 '}'
+ brace_count = brace_level_at_class_start
+ class_end_index = -1
+ for i in range(class_start_index + 1, len(lines)):
+ line = lines[i]
+ brace_count += line.count('{')
+ brace_count -= line.count('}')
+ if brace_count <= 0: # 找到匹配的闭合括号
+ class_end_index = i
+ break
+
+ if class_end_index != -1:
+ return "\n".join(lines[class_start_index:class_end_index+1])
+ return "" # 未找到完整的类块
+
+def extract_xml_def(lines, start_index):
+ """从XML行中提取完整的Def块。"""
+ import re
+ # 向上找到 或
+ def_start_index = -1
+ def_tag = ""
+ for i in range(start_index, -1, -1):
+ line = lines[i].strip()
+ match = re.match(r'<(\w+)\s+.*>', line) or re.match(r'<(\w+)>', line)
+ if match and ('Def' in match.group(1) or 'def' in match.group(1)):
+ # 这是一个简化的判断,实际中可能需要更复杂的逻辑
+ def_start_index = i
+ def_tag = match.group(1)
+ break
+
+ if def_start_index == -1: return ""
+
+ # 向下找到匹配的
+ def_end_index = -1
+ for i in range(def_start_index + 1, len(lines)):
+ if f'{def_tag}>' in lines[i]:
+ def_end_index = i
+ break
+
+ if def_end_index != -1:
+ return "\n".join(lines[def_start_index:def_end_index+1])
+ return ""
# 5. --- 核心功能函数 ---
def find_files_with_keyword(roots, keyword, extensions=['.xml', '.cs', '.txt']):
@@ -183,8 +285,8 @@ mcp = FastMCP(
@mcp.tool()
def get_context(question: str) -> str:
"""
- 根据问题中的关键词和向量相似度,在RimWorld知识库中搜索最相关的XML或C#文件。
- 返回最匹配的文件路径。
+ 根据问题中的关键词和向量相似度,在RimWorld知识库中搜索最相关的多个代码片段,
+ 并将其整合后返回。
"""
logging.info(f"收到问题: {question}")
keyword = find_keyword_in_question(question)
@@ -194,11 +296,11 @@ def get_context(question: str) -> str:
logging.info(f"提取到关键词: {keyword}")
- # 1. 检查缓存
- if keyword in knowledge_cache:
- cached_path = knowledge_cache[keyword]
- logging.info(f"缓存命中: 关键词 '{keyword}' -> {cached_path}")
- return f"根据知识库缓存,与 '{keyword}' 最相关的定义文件是:\n{cached_path}"
+ # 1. 检查缓存 (新逻辑)
+ cached_result = load_cache_for_keyword(keyword)
+ if cached_result:
+ logging.info(f"缓存命中: 关键词 '{keyword}'")
+ return cached_result
logging.info(f"缓存未命中,开始实时搜索: {keyword}")
@@ -221,7 +323,6 @@ def get_context(question: str) -> str:
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
- # v4模型支持更长的输入
file_embedding = get_embedding(content[:8000])
if file_embedding:
file_embeddings.append({'path': file_path, 'embedding': file_embedding})
@@ -231,18 +332,50 @@ def get_context(question: str) -> str:
if not file_embeddings:
return "无法为任何候选文件生成向量。"
- # 找到最相似的文件
- best_match_path = find_most_similar_file(question_embedding, file_embeddings)
+ # 找到最相似的多个文件
+ best_matches = find_most_similar_files(question_embedding, file_embeddings, top_n=3)
- if not best_match_path:
- return "计算向量相似度失败。"
+ if not best_matches:
+ return "计算向量相似度失败或没有找到足够相似的文件。"
- # 4. 更新缓存并返回结果
- logging.info(f"向量搜索完成。最匹配的文件是: {best_match_path}")
- knowledge_cache[keyword] = best_match_path
- save_cache(knowledge_cache)
+ # 4. 提取代码并格式化输出
+ output_parts = [f"根据向量相似度分析,与 '{keyword}' 最相关的代码定义如下:\n"]
- return f"根据向量相似度分析,与 '{keyword}' 最相关的定义文件是:\n{best_match_path}"
+ for match in best_matches:
+ file_path = match['path']
+ similarity = match['similarity']
+
+ # 智能提取代码块
+ code_block = extract_relevant_code(file_path, keyword)
+
+ # 如果提取失败,则跳过这个文件
+ if not code_block or code_block.startswith("# Error"):
+ logging.warning(f"未能从 {file_path} 提取到完整的代码块。")
+ continue
+
+ # 确定语言类型用于markdown高亮
+ lang = "csharp" if file_path.endswith(('.cs', '.txt')) else "xml"
+
+ output_parts.append(
+ f"---\n"
+ f"**文件路径:** `{file_path}`\n"
+ f"**相似度:** {similarity:.4f}\n\n"
+ f"```{lang}\n"
+ f"{code_block}\n"
+ f"```"
+ )
+
+ # 如果没有任何代码块被成功提取
+ if len(output_parts) <= 1:
+ return f"虽然找到了相似的文件,但无法在其中提取到关于 '{keyword}' 的完整代码块。"
+
+ final_output = "\n".join(output_parts)
+
+ # 5. 更新缓存并返回结果
+ logging.info(f"向量搜索完成。找到了 {len(best_matches)} 个匹配项并成功提取了代码。")
+ save_cache_for_keyword(keyword, final_output)
+
+ return final_output
except Exception as e:
logging.error(f"处理请求时发生意外错误: {e}", exc_info=True)
diff --git a/Source/WulaFallenEmpire/CompCustomUniqueWeapon.cs b/Source/WulaFallenEmpire/CompCustomUniqueWeapon.cs
new file mode 100644
index 00000000..62f94841
--- /dev/null
+++ b/Source/WulaFallenEmpire/CompCustomUniqueWeapon.cs
@@ -0,0 +1,154 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using Verse;
+using RimWorld;
+
+namespace WulaFallenEmpire
+{
+ public class CompCustomUniqueWeapon : CompUniqueWeapon
+ {
+ // 使用 'new' 关键字来明确隐藏基类成员,解决 CS0108 警告
+ public new CompProperties_CustomUniqueWeapon Props => (CompProperties_CustomUniqueWeapon)props;
+
+ private List customTraits = new List();
+
+ // 使用 'new' 关键字隐藏基类属性,解决 CS0506 错误
+ public new List TraitsListForReading => customTraits;
+
+ // PostExposeData 是 virtual 的,保留 override
+ public override void PostExposeData()
+ {
+ base.PostExposeData();
+ Scribe_Collections.Look(ref customTraits, "customTraits", LookMode.Def);
+ if (Scribe.mode == LoadSaveMode.PostLoadInit)
+ {
+ if (customTraits == null) customTraits = new List();
+ SetupCustomTraits(fromSave: true);
+ }
+ }
+
+ // PostPostMake 是 virtual 的,保留 override
+ public override void PostPostMake()
+ {
+ InitializeCustomTraits();
+ if (parent.TryGetComp(out var comp))
+ {
+ comp.SetQuality(QualityUtility.GenerateQuality(QualityGenerator.Super), ArtGenerationContext.Outsider);
+ }
+ }
+
+ private void InitializeCustomTraits()
+ {
+ if (customTraits == null) customTraits = new List();
+ customTraits.Clear();
+
+ if (Props.forcedTraits != null)
+ {
+ foreach (var traitToForce in Props.forcedTraits)
+ {
+ if (customTraits.All(t => !t.Overlaps(traitToForce)))
+ {
+ customTraits.Add(traitToForce);
+ }
+ }
+ }
+
+ IntRange traitRange = Props.numTraitsRange ?? new IntRange(1, 3);
+ int totalTraitsTarget = Mathf.Max(customTraits.Count, traitRange.RandomInRange);
+ int missingTraits = totalTraitsTarget - customTraits.Count;
+
+ if (missingTraits > 0)
+ {
+ // CanAddTrait 现在是我们自己的 'new' 方法
+ IEnumerable possibleTraits = DefDatabase.AllDefs.Where(CanAddTrait);
+ for (int i = 0; i < missingTraits; i++)
+ {
+ if (!possibleTraits.Any()) break;
+
+ var chosenTrait = possibleTraits.RandomElementByWeight(t => t.commonality);
+ customTraits.Add(chosenTrait);
+
+ possibleTraits = possibleTraits.Where(t => t != chosenTrait && !t.Overlaps(chosenTrait));
+ }
+ }
+
+ SetupCustomTraits(fromSave: false);
+ }
+
+ private void SetupCustomTraits(bool fromSave)
+ {
+ foreach (WeaponTraitDef trait in customTraits)
+ {
+ if (trait.abilityProps != null && parent.GetComp() is CompEquippableAbilityReloadable comp)
+ {
+ comp.props = trait.abilityProps;
+ if (!fromSave)
+ {
+ comp.Notify_PropsChanged();
+ }
+ }
+ }
+ }
+
+ // 使用 'new' 关键字隐藏基类方法,解决 CS0506 错误
+ public new bool CanAddTrait(WeaponTraitDef trait)
+ {
+ if (customTraits.Any(t => t == trait || t.Overlaps(t)))
+ return false;
+
+ if (Props.weaponCategories != null && Props.weaponCategories.Any() && !Props.weaponCategories.Contains(trait.weaponCategory))
+ return false;
+
+ if (customTraits.Count == 0 && !trait.canGenerateAlone)
+ return false;
+
+ return true;
+ }
+
+ // --- 下面的方法都是 virtual 的,保留 override ---
+
+ public override string TransformLabel(string label) => label;
+ public override Color? ForceColor() => null;
+
+ public override float GetStatOffset(StatDef stat) => customTraits.Sum(t => t.statOffsets.GetStatOffsetFromList(stat));
+ public override float GetStatFactor(StatDef stat) => customTraits.Aggregate(1f, (current, t) => current * t.statFactors.GetStatFactorFromList(stat));
+
+ public override string CompInspectStringExtra()
+ {
+ if (customTraits.NullOrEmpty()) return null;
+ return "WeaponTraits".Translate() + ": " + customTraits.Select(t => t.label).ToCommaList().CapitalizeFirst();
+ }
+
+ public override string CompTipStringExtra()
+ {
+ if (customTraits.NullOrEmpty()) return base.CompTipStringExtra();
+ return "WeaponTraits".Translate() + ": " + customTraits.Select(t => t.label).ToCommaList().CapitalizeFirst();
+ }
+
+ public override IEnumerable SpecialDisplayStats()
+ {
+ if (customTraits.NullOrEmpty()) yield break;
+
+ var builder = new StringBuilder();
+ builder.AppendLine("Stat_ThingUniqueWeaponTrait_Desc".Translate());
+ builder.AppendLine();
+
+ for (int i = 0; i < customTraits.Count; i++)
+ {
+ WeaponTraitDef trait = customTraits[i];
+ builder.AppendLine(trait.LabelCap.Colorize(ColorLibrary.Yellow));
+ builder.AppendLine(trait.description);
+ if (i < customTraits.Count - 1) builder.AppendLine();
+ }
+
+ yield return new StatDrawEntry(
+ parent.def.IsMeleeWeapon ? StatCategoryDefOf.Weapon_Melee : StatCategoryDefOf.Weapon_Ranged,
+ "Stat_ThingUniqueWeaponTrait_Label".Translate(),
+ customTraits.Select(t => t.label).ToCommaList().CapitalizeFirst(),
+ builder.ToString(),
+ 1104);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/CompProperties_CustomUniqueWeapon.cs b/Source/WulaFallenEmpire/CompProperties_CustomUniqueWeapon.cs
new file mode 100644
index 00000000..514a2bf3
--- /dev/null
+++ b/Source/WulaFallenEmpire/CompProperties_CustomUniqueWeapon.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using Verse;
+using RimWorld;
+
+namespace WulaFallenEmpire
+{
+ public class CompProperties_CustomUniqueWeapon : CompProperties_UniqueWeapon
+ {
+ // A list of traits that will always be added to the weapon.
+ public List forcedTraits;
+
+ // The range of traits to randomly add. If not defined in XML, a default of 1-3 will be used.
+ public IntRange? numTraitsRange;
+
+ public CompProperties_CustomUniqueWeapon()
+ {
+ // Point to the implementation of our custom logic.
+ this.compClass = typeof(CompCustomUniqueWeapon);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
index f8269d67..8771b901 100644
--- a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
+++ b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
@@ -69,6 +69,8 @@
+
+