This commit is contained in:
2025-08-26 18:33:49 +08:00
parent c3161287d3
commit a897c2e44c
12 changed files with 11864 additions and 80 deletions

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<HediffDef ParentName="HediffWithCompsBase">
<defName>WULA_DamageShield</defName>
<label>伤害护盾</label>
<description>一种特殊的能量护盾,可以抵挡受到的伤害。每层护盾可以抵挡一次伤害。</description>
<hediffClass>WulaFallenEmpire.Hediff_DamageShield</hediffClass>
<initialSeverity>10</initialSeverity> <!-- 初始层数设置为10 -->
<maxSeverity>999</maxSeverity> <!-- 最大层数,可以根据需要调整 -->
<tendable>false</tendable>
<displayAllParts>false</displayAllParts>
<priceImpact>1</priceImpact>
<addedSimultaneously>true</addedSimultaneously>
<countsAsAddedPartOrImplant>false</countsAsAddedPartOrImplant>
<stages>
<li>
<label>活跃</label>
<minSeverity>1</minSeverity>
<!-- 这里可以添加一些统计数据偏移,例如增加防御等 -->
</li>
</stages>
<scenarioCanAdd>false</scenarioCanAdd>
</HediffDef>
</Defs>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="ResourceBase">
<defName>WULA_DamageShieldGenerator</defName>
<label>伤害护盾发生器</label>
<description>一个便携式设备,可以激活并生成一个临时的能量护盾,抵挡即将到来的伤害。</description>
<graphicData>
<texPath>Things/Item/WULA_DamageShieldGenerator</texPath> <!-- 假设有一个贴图 -->
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<stackLimit>1</stackLimit>
<useHitPoints>true</useHitPoints>
<healthAffectsPrice>false</healthAffectsPrice>
<statBases>
<MaxHitPoints>50</MaxHitPoints>
<MarketValue>500</MarketValue>
<Mass>0.5</Mass>
<WorkToMake>1000</WorkToMake>
</statBases>
<thingCategories>
<li>Items</li>
</thingCategories>
<tradeability>Sellable</tradeability>
<comps>
<li Class="CompProperties_Usable">
<useJob>UseItem</useJob>
<floatMenuCommandLabel>使用伤害护盾发生器</floatMenuCommandLabel>
</li>
<li Class="WulaFallenEmpire.CompProperties_AddDamageShieldCharges">
<hediffDef>WULA_DamageShield</hediffDef>
<chargesToAdd>10</chargesToAdd> <!-- 每次使用添加 10 层 -->
</li>
</comps>
</ThingDef>
</Defs>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
<WULA_MessageGainedDamageShieldCharges>{0} 获得了 {1} 层伤害护盾!</WULA_MessageGainedDamageShieldCharges>
<WULA_CannotUseOnDeadPawn>无法对已死亡的Pawn使用。</WULA_CannotUseOnDeadPawn>
<WULA_DamageShieldMaxChargesReached>伤害护盾已达到最大层数。</WULA_DamageShieldMaxChargesReached>
<WULA_DamageShieldChargesDescription>使用:增加 {0} 层伤害护盾</WULA_DamageShieldChargesDescription>
</LanguageData>

View File

@@ -388,7 +388,7 @@ def find_keywords_in_question(question: str) -> list[str]:
def analyze_question_with_llm(question: str) -> dict:
"""使用Qwen模型分析问题并提取关键词和意图"""
try:
system_prompt = """你是一个关键词提取机器人,专门用于从 RimWorld 模组开发相关问题中提取精确的搜索关键词。你的任务是识别问题中提到的核心技术术语。
system_prompt = """你是一个关键词提取机器人,专门用于从 RimWorld 模组开发相关问题中提取精确的搜索关键词。你的任务是识别问题中提到的核心技术术语,并将它们正确地拆分成独立的关键词
严格按照以下格式回复,不要添加任何额外说明:
问题类型:[问题分类]
@@ -402,7 +402,8 @@ def analyze_question_with_llm(question: str) -> dict:
3. 不要添加通用词如"RimWorld""游戏""定义""用法"
4. 不要添加缩写或扩展形式如"Def""XML"等除非问题中明确提到
5. 只提取具体的技术名词,忽略动词、形容词等
6. 关键词之间用英文逗号分隔,不要有空格
6. 当遇到用空格连接的多个技术术语时,应将它们拆分为独立的关键词
7. 关键词之间用英文逗号分隔,不要有空格
示例:
问题ThingDef的定义和用法是什么
@@ -417,6 +418,12 @@ def analyze_question_with_llm(question: str) -> dict:
关键概念API 使用
搜索关键词GenExplosion.DoExplosion,Projectile.Launch
问题RimWorld Pawn_HealthTracker PreApplyDamage
问题类型API 使用说明
关键类/方法名Pawn_HealthTracker,PreApplyDamage
关键概念:伤害处理
搜索关键词Pawn_HealthTracker,PreApplyDamage
现在请分析以下问题:"""
messages = [
@@ -512,6 +519,16 @@ def get_context(question: str) -> str:
analysis = analyze_question_with_llm(question)
keywords = analysis["search_keywords"]
# 确保关键词被正确拆分
split_keywords = []
for keyword in keywords:
# 如果关键词中包含空格,将其拆分为多个关键词
if ' ' in keyword:
split_keywords.extend(keyword.split())
else:
split_keywords.append(keyword)
keywords = split_keywords
if not keywords:
logging.warning("无法从问题中提取关键词。")
return "无法从问题中提取关键词,请提供更具体的信息。"
@@ -534,23 +551,32 @@ def get_context(question: str) -> str:
logging.info(f"缓存未命中,开始实时搜索: {cache_key}")
# 2. 关键词文件搜索 (分层智能筛选)
# 2. 对每个关键词分别执行搜索过程,然后合并结果
try:
candidate_files = find_files_with_keyword(KNOWLEDGE_BASE_PATHS, keywords)
if not candidate_files:
logging.info(f"未找到与 '{keywords}' 相关的文件。")
return f"未在知识库中找到与 '{keywords}' 相关的文件定义。"
all_results = []
processed_files = set() # 避免重复处理相同文件
logging.info(f"找到 {len(candidate_files)} 个候选文件,开始向量化处理...")
for keyword in keywords:
logging.info(f"开始搜索关键词: {keyword}")
# 为当前关键词搜索文件
candidate_files = find_files_with_keyword(KNOWLEDGE_BASE_PATHS, [keyword])
# 新增:文件名精确匹配优先
priority_results = []
remaining_files = []
for file_path in candidate_files:
filename_no_ext = os.path.splitext(os.path.basename(file_path))[0]
is_priority = False
for keyword in keywords:
if not candidate_files:
logging.info(f"未找到与 '{keyword}' 相关的文件。")
continue
logging.info(f"找到 {len(candidate_files)} 个候选文件用于关键词 '{keyword}',开始向量化处理...")
# 文件名精确匹配优先
priority_results = []
remaining_files = []
for file_path in candidate_files:
# 避免重复处理相同文件
if file_path in processed_files:
continue
filename_no_ext = os.path.splitext(os.path.basename(file_path))[0]
if filename_no_ext.lower() == keyword.lower():
logging.info(f"文件名精确匹配: {file_path}")
code_block = extract_relevant_code(file_path, keyword)
@@ -560,79 +586,92 @@ def get_context(question: str) -> str:
'similarity': 1.0, # 精确匹配给予最高分
'code': code_block
})
is_priority = True
break # 已处理该文件,跳出内层循环
if not is_priority:
remaining_files.append(file_path)
candidate_files = remaining_files # 更新候选文件列表,排除已优先处理的文件
processed_files.add(file_path)
else:
remaining_files.append(file_path)
# 更新候选文件列表,排除已优先处理的文件
candidate_files = [f for f in remaining_files if f not in processed_files]
# 限制向量化的文件数量以避免超时
MAX_FILES_TO_VECTORIZE = 5
if len(candidate_files) > MAX_FILES_TO_VECTORIZE:
logging.warning(f"候选文件过多 ({len(candidate_files)}),仅处理前 {MAX_FILES_TO_VECTORIZE} 个。")
candidate_files = candidate_files[:MAX_FILES_TO_VECTORIZE]
# 3. 向量化和相似度计算 (精准筛选)
# 增加超时保护:限制向量化的文件数量
MAX_FILES_TO_VECTORIZE = 10 # 进一步减少处理文件数量以避免超时
if len(candidate_files) > MAX_FILES_TO_VECTORIZE:
logging.warning(f"候选文件过多 ({len(candidate_files)}),仅处理前 {MAX_FILES_TO_VECTORIZE} 个。")
candidate_files = candidate_files[:MAX_FILES_TO_VECTORIZE]
# 为剩余文件生成向量
question_embedding = get_embedding(keyword) # 使用关键词而不是整个问题
if not question_embedding:
logging.warning(f"无法为关键词 '{keyword}' 生成向量。")
# 将优先结果添加到总结果中
all_results.extend(priority_results)
continue
question_embedding = get_embedding(question)
if not question_embedding:
return "无法生成问题向量请检查API连接或问题内容。"
file_embeddings = []
for i, file_path in enumerate(candidate_files):
try:
# 避免重复处理相同文件
if file_path in processed_files:
continue
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 添加处理进度日志
if i % 5 == 0: # 每5个文件记录一次进度
logging.info(f"正在处理第 {i+1}/{len(candidate_files)} 个文件: {os.path.basename(file_path)}")
file_embedding = get_embedding(content[:8000]) # 限制内容长度以提高效率
if file_embedding:
file_embeddings.append({'path': file_path, 'embedding': file_embedding})
except Exception as e:
logging.error(f"处理文件 {file_path} 时出错: {e}")
continue # 继续处理下一个文件,而不是完全失败
if not file_embeddings and not priority_results:
logging.warning(f"未能为关键词 '{keyword}' 的任何候选文件生成向量。")
continue
file_embeddings = []
for i, file_path in enumerate(candidate_files):
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 添加处理进度日志
if i % 5 == 0: # 每5个文件记录一次进度
logging.info(f"正在处理第 {i+1}/{len(candidate_files)} 个文件: {os.path.basename(file_path)}")
# 找到最相似的多个文件
best_matches = find_most_similar_files(question_embedding, file_embeddings, top_n=3)
# 重排序处理
if len(best_matches) > 1:
reranked_matches = rerank_files(keyword, best_matches, top_n=2) # 减少重排序数量
else:
reranked_matches = best_matches
# 提取代码内容
results_with_code = []
for match in reranked_matches:
# 避免重复处理相同文件
if match['path'] in processed_files:
continue
file_embedding = get_embedding(content[:8000]) # 限制内容长度以提高效率
if file_embedding:
file_embeddings.append({'path': file_path, 'embedding': file_embedding})
except Exception as e:
logging.error(f"处理文件 {file_path} 时出错: {e}")
continue # 继续处理下一个文件,而不是完全失败
code_block = extract_relevant_code(match['path'], "")
if code_block:
match['code'] = code_block
results_with_code.append(match)
processed_files.add(match['path'])
# 将优先结果和相似度结果合并
results_with_code = priority_results + results_with_code
# 将当前关键词的结果添加到总结果中
all_results.extend(results_with_code)
if not file_embeddings:
logging.warning("未能为任何候选文件生成向量。可能是由于API超时或其他错误。")
return "能为任何候选文件生成向量,请稍后重试或减少搜索范围"
# 找到最相似的多个文件
best_matches = find_most_similar_files(question_embedding, file_embeddings, top_n=5) # 进一步减少返回数量以避免超时
# 检查是否有任何结果
if len(all_results) <= 0:
return f"在知识库中找到与 '{keywords}' 相关的文件定义"
if not best_matches:
return "计算向量相似度失败或没有找到足够相似的文件。"
# 新增:重排序处理(仅在找到足够多匹配项时执行)
if len(best_matches) > 2:
reranked_matches = rerank_files(question, best_matches, top_n=3) # 减少重排序数量
else:
reranked_matches = best_matches # 如果匹配项太少,跳过重排序以节省时间
# 提取代码内容
results_with_code = []
for match in reranked_matches:
code_block = extract_relevant_code(match['path'], "")
if code_block:
match['code'] = code_block
results_with_code.append(match)
# 将优先结果添加到结果列表开头
results_with_code = priority_results + results_with_code
if len(results_with_code) <= 0:
return f"虽然找到了相似的文件,但无法在其中提取到相关代码块。"
# 直接返回原始代码结果而不是使用LLM格式化
# 整理最终输出
final_output = ""
for i, result in enumerate(results_with_code, 1):
for i, result in enumerate(all_results, 1):
final_output += f"--- 结果 {i} (相似度: {result['similarity']:.3f}) ---\n"
final_output += f"文件路径: {result['path']}\n\n"
final_output += f"{result['code']}\n\n"
# 5. 更新缓存并返回结果
logging.info(f"向量搜索完成。找到了 {len(results_with_code)} 个匹配项并成功提取了代码。")
logging.info(f"向量搜索完成。找到了 {len(all_results)} 个匹配项并成功提取了代码。")
save_cache_for_question(question, keywords, final_output)
return final_output

File diff suppressed because one or more lines are too long

100
Source/HarmonyPatch.md Normal file
View File

@@ -0,0 +1,100 @@
Patching
Concept
In order to provide your own code to Harmony, you need to define methods that run in the context of the original method. Harmony provides three types of methods that each offer different possibilities.
Types of patches
Two of them, the Prefix patch and the Postfix patch are easy to understand and you can write them as simple static methods.
Transpiler patches are not methods that are executed together with the original but instead are called in an earlier stage where the instructions of the original are fed into the transpiler so it can process and change them, to finally output the instructions that will build the new original.
A Finalizer patch is a static method that handles exceptions and can change them. It is the only patch type that is immune to exceptions thrown by the original method or by any applied patches. The other patch types are considered part of the original and may not get executed when an exception occurs.
Finally, there is the Reverse Patch. It is different from the previous types in that it patches your methods instead of foreign original methods. To use it, you define a stub that looks like the original in some way and patch the original onto your stub which you can easily call from your own code. You can even transpile the result during the process.
Patches need to be static
Patch methods need to be static because Harmony works with multiple users in different assemblies in mind. In order to guarantee the correct patching order, patches are always re-applied as soon as someone wants to change the original. Since it is hard to serialize data in a generic way across assemblies in .NET, Harmony only stores a method pointer to your patch methods so it can use and apply them at a later point again.
If you need custom state in your patches, it is recommended to use a static variable and store all your patch state in there. Keep in mind that Transpilers are only executed to generate the method so they don't "run" when the original is executed.
Commonly unsupported use cases
Harmony works only in the current AppDomain. Accessing other app domains requires xpc and serialization which is not supported.
Currently, support for generic types and methods is experimental and can give unexpected results. See Edge Cases for more information.
When a method is inlined and the code that tries to mark in for not inlining does not work, your patches are not called because there is no method to patch.
Patch Class
With manual patching, you can put your patches anywhere you like since you will refer to them yourself. Patching by annotations simplifies patching by assuming that you set up annotated classes and define your patch methods inside them.
Layout The class can be static or not, public or private, it doesn't matter. However, in order to make Harmony find it, it must have at least one [HarmonyPatch] attribute. Inside the class you define patches as static methods that either have special names like Prefix or Transpiler or use attributes to define their type. Usually they also include annotations that define their target (the original method you want to patch). It also common to have fields and other helper methods in the class.
Attribute Inheritance The attributes of the methods in the class inherit the attributes of the class.
Patch methods
Harmony identifies your patch methods and their helper methods by name. If you prefer to name your methods differently, you can use attributes to tell Harmony what your methods are.
[HarmonyPatch(...)]
class Patch
{
static void Prefix()
{
// this method uses the name "Prefix", no annotation necessary
}
[HarmonyPostfix]
static void MyOwnName()
{
// this method is a Postfix as defined by the attribute
}
}
If you prefer manual patching, you can use any method name or class structure you want. You are responsible to retrieve the MethodInfo for the different patch methods and supply them to the Patch() method by wrapping them into HarmonyMethod objects.
note Patch methods must be static but you can define them public or private. They cannot be dynamic methods but you can write static patch factory methods that return dynamic methods.
[HarmonyPatch(...)]
class Patch
{
// the return type of factory methods can be either MethodInfo or DynamicMethod
[HarmonyPrefix]
static MethodInfo PrefixFactory(MethodBase originalMethod)
{
// return an instance of MethodInfo or an instance of DynamicMethod
}
[HarmonyPostfix]
static MethodInfo PostfixFactory(MethodBase originalMethod)
{
// return an instance of MethodInfo or an instance of DynamicMethod
}
}
Method names
Manual patching knows four main patch types: Prefix, Postfix, Transpiler and Finalizer. If you use attributes for patching, you can also use the helper methods: Prepare, TargetMethod, TargetMethods and Cleanup as explained below.
Each of those names has a corresponding attribute starting with [Harmony...]. So instead of calling one of your methods "Prepare", you can call it anything and decorate it with a [HarmonyPrepare] attribute.
Patch method types
Both prefix and postfix have specific semantics that are unique to them. They do however share the ability to use a range of injected values as arguments.
Prefix
A prefix is a method that is executed before the original method. It is commonly used to:
access and edit the arguments of the original method
set the result of the original method
skip the original method
set custom state that can be recalled in the postfix
run a piece of code at the beginning that is guaranteed to be executed
Postfix
A postfix is a method that is executed after the original method. It is commonly used to:
read or change the result of the original method
access the arguments of the original method
read custom state from the prefix
Transpiler
This method defines the transpiler that modifies the code of the original method. Use this in the advanced case where you want to modify the original methods IL codes.
Finalizer
A finalizer is a method that executes after all postfixes. It wraps the original method, all prefixes, and postfixes in try/catch logic and is called either with null (no exception) or with an exception if one occurred. It is commonly used to:
run a piece of code at the end that is guaranteed to be executed
handle exceptions and suppress them
handle exceptions and alter them

View File

@@ -6,9 +6,6 @@
},
{
"path": "../../../../Data"
},
{
"path": "../../../../dll1.6"
}
],
"settings": {}

View File

@@ -0,0 +1,77 @@
using Verse;
using RimWorld;
using System.Collections.Generic;
namespace WulaFallenEmpire
{
public class CompUseEffect_AddDamageShieldCharges : CompUseEffect
{
public CompProperties_AddDamageShieldCharges Props => (CompProperties_AddDamageShieldCharges)props;
public override void DoEffect(Pawn user)
{
base.DoEffect(user);
// 获取或添加 Hediff_DamageShield
Hediff_DamageShield damageShield = user.health.hediffSet.GetFirstHediff<Hediff_DamageShield>();
if (damageShield == null)
{
// 如果没有 Hediff则添加一个
damageShield = (Hediff_DamageShield)HediffMaker.MakeHediff(Props.hediffDef, user);
user.health.AddHediff(damageShield);
damageShield.ShieldCharges = Props.chargesToAdd; // 设置初始层数
}
else
{
// 如果已有 Hediff则增加层数
damageShield.ShieldCharges += Props.chargesToAdd;
}
// 确保层数不超过最大值
if (damageShield.ShieldCharges > (int)damageShield.def.maxSeverity)
{
damageShield.ShieldCharges = (int)damageShield.def.maxSeverity;
}
// 发送消息
Messages.Message("WULA_MessageGainedDamageShieldCharges".Translate(user.LabelShort, Props.chargesToAdd), user, MessageTypeDefOf.PositiveEvent);
}
// 修正 CanBeUsedBy 方法签名
public override AcceptanceReport CanBeUsedBy(Pawn p)
{
// 确保只能对活着的 Pawn 使用
if (p.Dead)
{
return "WULA_CannotUseOnDeadPawn".Translate();
}
// 检查是否已达到最大层数
Hediff_DamageShield damageShield = p.health.hediffSet.GetFirstHediff<Hediff_DamageShield>();
if (damageShield != null && damageShield.ShieldCharges >= (int)damageShield.def.maxSeverity)
{
return "WULA_DamageShieldMaxChargesReached".Translate();
}
return true; // 可以使用
}
// 可以在这里添加 GetDescriptionPart() 来显示描述
public override string GetDescriptionPart()
{
return "WULA_DamageShieldChargesDescription".Translate(Props.chargesToAdd);
}
}
public class CompProperties_AddDamageShieldCharges : CompProperties_UseEffect
{
public HediffDef hediffDef;
public int chargesToAdd;
public CompProperties_AddDamageShieldCharges()
{
compClass = typeof(CompUseEffect_AddDamageShieldCharges);
}
}
}

View File

@@ -0,0 +1,40 @@
using HarmonyLib;
using Verse;
using System.Reflection;
using UnityEngine; // Add UnityEngine for MoteMaker and Color
namespace WulaFallenEmpire.HarmonyPatches
{
[HarmonyPatch(typeof(Pawn_HealthTracker), "PreApplyDamage")]
public static class DamageShieldPatch
{
// 使用 Harmony 的 AccessTools.Field 来获取私有的 pawn 字段
private static readonly FieldInfo PawnField = AccessTools.Field(typeof(Pawn_HealthTracker), "pawn");
public static bool Prefix(Pawn_HealthTracker __instance, ref DamageInfo dinfo, out bool absorbed)
{
// 获取 Pawn 实例
Pawn pawn = (Pawn)PawnField.GetValue(__instance);
// 查找 Pawn 身上是否有 Hediff_DamageShield
Hediff_DamageShield damageShield = pawn.health.hediffSet.GetFirstHediff<Hediff_DamageShield>();
if (damageShield != null && damageShield.ShieldCharges > 0)
{
// 如果有护盾层数,则消耗一层并抵挡伤害
damageShield.ShieldCharges--;
// MoteMaker.ThrowText(pawn.DrawPos, pawn.Map, "伤害被护盾抵挡!", Color.cyan, 1.2f); // 视觉反馈,明确指定 Verse.MoteMaker此行将被删除
// 设置 absorbed 为 true表示伤害被完全吸收
absorbed = true;
// 返回 false阻止原始方法执行即伤害不会被应用
return false;
}
// 如果没有护盾 Hediff 或者层数用尽,则正常处理伤害
absorbed = false;
return true; // 继续执行原始方法
}
}
}

View File

@@ -0,0 +1,73 @@
using Verse;
using System.Text;
using RimWorld;
using UnityEngine;
namespace WulaFallenEmpire
{
public class Hediff_DamageShield : HediffWithComps
{
// 伤害抵挡层数
// 直接将 severityInt 作为 ShieldCharges这样外部对 severity 的修改会直接影响 ShieldCharges
public int ShieldCharges
{
get => (int)severityInt;
set => severityInt = value;
}
public override string LabelInBrackets
{
get
{
if (ShieldCharges > 0)
{
return "层数: " + ShieldCharges;
}
return null;
}
}
public override string TipStringExtra
{
get
{
StringBuilder sb = new StringBuilder();
sb.Append(base.TipStringExtra);
if (ShieldCharges > 0)
{
sb.AppendLine(" - 每层抵挡一次伤害。当前层数: " + ShieldCharges);
}
else
{
sb.AppendLine(" - 没有可用的抵挡层数。");
}
return sb.ToString();
}
}
public override void ExposeData()
{
base.ExposeData();
// severityInt 会自动保存,所以不需要额外处理 ShieldCharges
}
public override void PostAdd(DamageInfo? dinfo)
{
base.PostAdd(dinfo);
// 初始层数由 XML 中的 initialSeverity 控制
// 如果需要一个固定的初始值,可以在这里设置
// 例如:如果 hediffDef.initialSeverity 设为 0这里可以强制给一个默认值
// 如果 initialSeverity 在 XML 中已经设置为 10这里就不需要额外处理
}
public override void Tick()
{
base.Tick();
// 如果层数归零,移除 Hediff
if (ShieldCharges <= 0)
{
pawn.health.RemoveHediff(this);
}
}
}
}

View File

@@ -182,6 +182,9 @@
<Compile Include="Verb\VerbProperties_WeaponStealBeam.cs" />
<Compile Include="Verb\Verb_ShootWeaponStealBeam.cs" />
<Compile Include="Verb\Verb_ShootMeltBeam.cs" />
<Compile Include="Hediff_DamageShield.cs" />
<Compile Include="HarmonyPatches\DamageShieldPatch.cs" />
<Compile Include="CompUseEffect_AddDamageShieldCharges.cs" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />