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'' 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 @@ + +