fix: 修复多个组件的空引用、调试日志和编码问题

### HediffComp_GestaltNode
- 添加 pawn 空引用检查,防止 NullReferenceException
- 将 Log.Message 改为 ArachnaeLog.Debug 统一日志管理
- 新增 Notify_PawnDied 方法处理 Pawn 死亡时的过渡状态清理
- 修复 UpdateTransitionState 中重复声明 pawn 变量的问题

### CompAbilityEffect_LaunchMultiProjectile
- 添加目标有效性检查,处理目标死亡或消失的情况
- 实现动态目标追踪,更新目标位置
- 移除未使用的 parametersInitialized 字段
- 新增 ForceReinitialize 方法支持状态变化时重新初始化

### CompHediffGiver
- 改进异常处理,记录警告日志而非静默吞掉异常
- 重构 IsSymmetricalPart 方法,使用翻译键和 BodyPartTagDef 支持本地化

### HediffComp_Spawner
- 将 DebugSettings.debugLogging 改为 Prefs.DevMode
- 修复所有 UTF-8 编码乱码注释(约30处)

### Comp_PawnResearchBlueprintReader
- 修复灵能科研点消耗时机,确保先检查→再消耗→最后添加进度
- 提高研究进度添加的原子性
This commit is contained in:
2026-02-15 00:24:08 +08:00
parent ae7a72fd27
commit f9624818f5
6 changed files with 148 additions and 65 deletions

Binary file not shown.

View File

@@ -13,7 +13,6 @@ namespace ArachnaeSwarm
private int nextProjectileTick;
private int projectilesFired;
private IntVec3? targetCell;
private bool parametersInitialized;
// 缓存当前状态的参数
private int currentNumProjectiles;
@@ -139,17 +138,20 @@ namespace ArachnaeSwarm
/// </summary>
private void InitializeParameters()
{
if (parametersInitialized)
return;
var state = GetCurrentState();
currentNumProjectiles = state?.numProjectiles ?? Props.numProjectiles;
currentProjectileDef = state?.projectileDef ?? Props.projectileDef;
currentOffsetRadius = state?.offsetRadius ?? Props.offsetRadius;
currentShotIntervalTicks = state?.shotIntervalTicks ?? Props.shotIntervalTicks;
parametersInitialized = true;
}
/// <summary>
/// 强制重新初始化参数(当状态变化时调用)
/// </summary>
public void ForceReinitialize()
{
InitializeParameters();
}
/// <summary>
@@ -182,7 +184,31 @@ namespace ArachnaeSwarm
return;
Pawn pawn = parent.pawn;
// 检查目标有效性(目标可能已移动或消失)
LocalTargetInfo finalTarget = target;
if (Props.useSustainedJob && target.Pawn != null)
{
// 如果是持续模式且有目标Pawn尝试追踪目标
if (target.Pawn.Dead || !target.Pawn.Spawned)
{
// 目标已死亡或消失,使用最后已知位置
if (targetCell.HasValue)
{
finalTarget = new LocalTargetInfo(targetCell.Value);
}
else
{
return; // 无法发射
}
}
else
{
// 更新目标位置
targetCell = target.Pawn.Position;
finalTarget = target;
}
}
// 如果有偏移范围,计算偏移后的目标
if (Props.useRandomOffset && currentOffsetRadius > 0)
@@ -205,7 +231,6 @@ namespace ArachnaeSwarm
nextProjectileTick = 0;
projectilesFired = 0;
targetCell = null;
parametersInitialized = false;
}
/// <summary>

View File

@@ -144,7 +144,7 @@ namespace ArachnaeSwarm
return comp.GestaltTracker;
}
if (tracker == null && NodeType == GestaltNodeType.OverlordNode)
if (tracker == null && NodeType == GestaltNodeType.OverlordNode && pawn != null)
{
tracker = new Pawn_GestaltTracker(pawn);
}
@@ -247,7 +247,7 @@ namespace ArachnaeSwarm
TryFindOverlord();
}
Log.Message($"[GestaltNode] {pawn.LabelShort} (HiveNode) 延迟初始化完成,当前连接状态: {IsConnectedToOverlord()}");
ArachnaeLog.Debug($"[GestaltNode] {pawn.LabelShort} (HiveNode) 延迟初始化完成,当前连接状态: {IsConnectedToOverlord()}");
}
}
@@ -256,6 +256,15 @@ namespace ArachnaeSwarm
/// </summary>
private void UpdateTransitionState()
{
// 检查Pawn是否死亡或无效
var pawn = Pawn;
if (pawn == null || pawn.Dead)
{
isTransitioning = false;
transitionStartTick = -1;
return;
}
if (!isTransitioning || transitionStartTick < 0)
return;
@@ -267,14 +276,13 @@ namespace ArachnaeSwarm
{
parent.Severity = targetSeverityAfterTransition;
// 通知Pawn状态改变
var pawn = Pawn;
// 通知Pawn状态改变使用方法开头声明的pawn变量
if (pawn?.health != null)
{
pawn.health.Notify_HediffChanged(parent);
}
Log.Message($"[GestaltNode] {Pawn?.LabelShort} 过渡完成,严重性: {targetSeverityAfterTransition}");
ArachnaeLog.Debug($"[GestaltNode] {pawn?.LabelShort} 过渡完成,严重性: {targetSeverityAfterTransition}");
}
// 重置过渡状态
@@ -316,7 +324,7 @@ namespace ArachnaeSwarm
}
}
Log.Message($"[GestaltNode] {Pawn?.LabelShort} 开始过渡,目标: {(isConnecting ? "" : "")} ({targetSeverityAfterTransition})");
ArachnaeLog.Debug($"[GestaltNode] {Pawn?.LabelShort} 开始过渡,目标: {(isConnecting ? "" : "")} ({targetSeverityAfterTransition})");
}
/// <summary>
@@ -434,7 +442,7 @@ namespace ArachnaeSwarm
}
UpdateSeverityBasedOnConnection();
Log.Message($"[GestaltNode] {pawn.LabelShort} 连接到 Overlord: {bestOverlord.LabelShort}");
ArachnaeLog.Debug($"[GestaltNode] {pawn.LabelShort} 连接到 Overlord: {bestOverlord.LabelShort}");
}
}
@@ -466,6 +474,10 @@ namespace ArachnaeSwarm
{
base.CompPostPostRemoved();
// 清理过渡状态
isTransitioning = false;
transitionStartTick = -1;
// 当Hediff被移除时如果连接到Overlord需要断开连接
if (NodeType == GestaltNodeType.HiveNode && IsConnectedToOverlord())
{
@@ -483,6 +495,20 @@ namespace ArachnaeSwarm
}
}
/// <summary>
/// Pawn 死亡时的处理
/// </summary>
public override void Notify_PawnDied(DamageInfo? dinfo, Hediff culprit = null)
{
base.Notify_PawnDied(dinfo, culprit);
// 清理过渡状态
isTransitioning = false;
transitionStartTick = -1;
ArachnaeLog.Debug($"[GestaltNode] {Pawn?.LabelShort} 死亡,清理过渡状态");
}
/// <summary>
/// 序列化
/// </summary>

View File

@@ -350,7 +350,7 @@ namespace ArachnaeSwarm
if (this.Props.animalThing)
{
// 鍔ㄧ墿鐢熸垚閫昏緫淇濇寔涓嶅彉
// 动物生成逻辑保持不变
if (this.Props.spawnMaxAdjacent > 0 && pawn.Map.mapPawns.AllPawns.Where(delegate(Pawn mP)
{
ThingDef defToCompare = this.Props.animalThing ? this.Props.animalToSpawn?.race : this.Props.thingToSpawn;
@@ -402,10 +402,10 @@ namespace ArachnaeSwarm
}
else
{
// 閲嶆柊璁捐鐗╁搧鐢熸垚閫昏緫锛氭寜浼樺厛绾ч『搴忓皾璇?
// 重新设计物品生成逻辑:按优先级顺序尝试
bool success = TrySpawnItemWithPriority(pawn);
// === 鏂板锛氬鏋滈厤缃簡閿€姣侀殢鏈洪儴浣嶏紝鍦ㄦ垚鍔熺敓鎴愮墿鍝佸悗鎵ц ===
// === 新增:如果配置了销毁随机部位,在成功生成物品后执行 ===
if (success && this.Props.destroyRandomBodyPart)
{
DestroyRandomBodyPart(pawn);
@@ -415,7 +415,7 @@ namespace ArachnaeSwarm
}
}
// 鏂板锛氶攢姣侀殢鏈鸿韩浣撻儴浣嶏紙鍙傝€?CompAbilityEffect_DestroyOwnBodyPart锛?
// 新增:销毁随机身体部位(参考 CompAbilityEffect_DestroyOwnBodyPart
private void DestroyRandomBodyPart(Pawn pawn)
{
try
@@ -426,7 +426,7 @@ namespace ArachnaeSwarm
return;
}
// 鑾峰彇鎵€鏈夊彲浠ラ攢姣佺殑韬綋閮ㄤ綅
// 获取所有可以销毁的身体部位
List<BodyPartRecord> possibleParts = GetDestroyableBodyParts(pawn);
if (possibleParts.Count == 0)
@@ -435,7 +435,7 @@ namespace ArachnaeSwarm
return;
}
// 闅忔満閫夋嫨涓€涓儴浣?
// 随机选择一个部位
BodyPartRecord partToDestroy = possibleParts.RandomElement();
if (partToDestroy == null)
@@ -444,10 +444,10 @@ namespace ArachnaeSwarm
return;
}
// 璁板綍閮ㄤ綅鍚嶇О
string partName = partToDestroy.def?.label ?? "鏈煡閮ㄤ綅";
// 记录部位名称
string partName = partToDestroy.def?.label ?? "未知部位";
// 娣诲姞缂哄け閮ㄤ綅hediff
// 添加缺失部位hediff
pawn.health.AddHediff(HediffDefOf.MissingBodyPart, partToDestroy);
Warn($"Destroyed {partName} on {pawn.LabelShort}", this.myDebug);
@@ -458,7 +458,7 @@ namespace ArachnaeSwarm
}
}
// 鏂板锛氳幏鍙栧彲浠ラ攢姣佺殑韬綋閮ㄤ綅鍒楄〃
// 新增:获取可以销毁的身体部位列表
private List<BodyPartRecord> GetDestroyableBodyParts(Pawn pawn)
{
List<BodyPartRecord> destroyableParts = new List<BodyPartRecord>();
@@ -466,20 +466,20 @@ namespace ArachnaeSwarm
if (pawn?.health?.hediffSet == null)
return destroyableParts;
// 鑾峰彇鎵€鏈夎韩浣撻儴浣?
// 获取所有身体部位
List<BodyPartRecord> allParts = pawn.RaceProps.body.AllParts;
foreach (BodyPartRecord part in allParts)
{
// 鎺掗櫎鏍稿績閮ㄤ綅锛堥伩鍏嶆浜★級
// 排除核心部位(避免死亡)
if (IsCriticalBodyPart(part))
continue;
// 鎺掗櫎宸茬粡缂哄け鐨勯儴浣?
// 排除已经缺失的部位
if (pawn.health.hediffSet.PartIsMissing(part))
continue;
// 鎺掗櫎宸茬粡鏈変弗閲嶄激瀹崇殑閮ㄤ綅锛堝彲閫夛級
// 排除已经有严重伤害的部位(可选)
if (pawn.health.hediffSet.PartOrAnyAncestorHasDirectlyAddedParts(part))
continue;
@@ -492,34 +492,34 @@ namespace ArachnaeSwarm
return destroyableParts;
}
// 鏂板锛氭鏌ユ槸鍚︽槸鍏抽敭韬綋閮ㄤ綅
// 新增:检查是否是关键身体部位
private bool IsCriticalBodyPart(BodyPartRecord part)
{
if (part == null)
return false;
// 鏍规嵁鏍囩鍒ゆ柇鏄惁涓哄叧閿儴浣?
// 根据标签判断是否为关键部位
if (part.def.tags != null)
{
// 杩欎簺鏍囩閫氬父琛ㄧず鍏抽敭閮ㄤ綅
if (part.def.tags.Contains(BodyPartTagDefOf.ConsciousnessSource) || // 鎰忚瘑婧愶紙澶ц剳锛?
part.def.tags.Contains(BodyPartTagDefOf.BloodPumpingSource) || // 琛€娑叉车婧愶紙蹇冭剰锛?
part.def.tags.Contains(BodyPartTagDefOf.BreathingSource) || // 鍛煎惛婧愶紙鑲猴級
part.def.tags.Contains(BodyPartTagDefOf.MetabolismSource) // 浠h阿婧愶紙鑲濊剰锛?
// 这些标签通常表示关键部位
if (part.def.tags.Contains(BodyPartTagDefOf.ConsciousnessSource) || // 意识源(大脑)
part.def.tags.Contains(BodyPartTagDefOf.BloodPumpingSource) || // 血液泵源(心脏)
part.def.tags.Contains(BodyPartTagDefOf.BreathingSource) || // 呼吸源(肺)
part.def.tags.Contains(BodyPartTagDefOf.MetabolismSource) // 代谢源(肝脏)
)
{
return true;
}
}
// 鏍规嵁娣卞害鍒ゆ柇锛堟繁搴﹁緝娣辩殑閫氬父鏄唴閮ㄥ櫒瀹橈級
// 根据深度判断(深度较深的通常是内部器官)
if (part.depth == BodyPartDepth.Inside && part.parent == null)
return true;
return false;
}
// 鏂板锛氭寜浼樺厛绾ч『搴忓皾璇曠敓鎴愮墿鍝?
// 新增:按优先级顺序尝试生成物品
private bool TrySpawnItemWithPriority(Pawn pawn)
{
Thing thing = ThingMaker.MakeThing(this.Props.thingToSpawn, null);
@@ -739,19 +739,19 @@ namespace ArachnaeSwarm
}
else
{
// 淇敼杩欓噷锛氬皢鍗婂緞浠?鍑忓皯鍒?锛岃鐢熸垚浣嶇疆鏇撮潬杩憄awn
// 修改这里将半径从3减少到2让生成位置更靠近Pawn
int searchRadius = 2;
// 棣栧厛灏濊瘯鍦╬awn鐨勭浉閭诲崟鍏冩牸鐢熸垚锛堝崐寰勪负1锛?
// 首先尝试在Pawn的相邻单元格生成半径为1
result = CellFinder.RandomClosewalkCellNear(this.pawn.Position, map, 1, null);
// 濡傛灉鐩搁偦鍗曞厓鏍兼壘涓嶅埌鍚堥€備綅缃紝鍐嶅皾璇曠◢杩滀竴鐐癸紙鍗婂緞涓?锛?
// 如果相邻单元格找不到合适位置再尝试稍远一点半径为2
if (!result.IsValid)
{
result = CellFinder.RandomClosewalkCellNear(this.pawn.Position, map, searchRadius, null);
}
// 濡傛灉杩樻槸鎵句笉鍒帮紝灏濊瘯pawn褰撳墠浣嶇疆锛堜綔涓烘渶鍚庢墜娈碉級
// 如果还是找不到尝试Pawn当前位置作为最后手段
if (!result.IsValid && this.pawn.Position.IsValid && this.pawn.Position.Walkable(map))
{
result = this.pawn.Position;
@@ -848,7 +848,7 @@ namespace ArachnaeSwarm
}
}
// === 鏁村悎鐨?Tools 鏂规硶 ===
// === 整合的 Tools 方法 ===
private void DestroyParentHediff(Hediff parentHediff, bool debug = false)
{
@@ -968,7 +968,7 @@ namespace ArachnaeSwarm
}
private void Warn(string warning, bool debug = false)
{
if (debug)
if (debug && Prefs.DevMode)
{
Log.Message($"[HediffComp_Spawner] {warning}");
}
@@ -990,4 +990,3 @@ namespace ArachnaeSwarm
private readonly int errorSpawnCount = 750;
}
}

View File

@@ -117,8 +117,9 @@ namespace ArachnaeSwarm
ArachnaeLog.Debug($"Added hediff {hediffDef.defName} to {pawn.Label} " +
$"{(bodyPart != null ? $"on {bodyPart.def.defName}" : "to whole body")}");
}
catch (Exception)
catch (Exception ex)
{
Log.Warning($"[CompHediffGiver] Failed to add hediff {hediffDef?.defName ?? "null"} to {pawn?.Label ?? "null"}: {ex.Message}");
}
}
@@ -252,27 +253,50 @@ namespace ArachnaeSwarm
/// </summary>
private bool IsSymmetricalPart(BodyPartRecord part1, BodyPartRecord part2)
{
// 简单的对称检测逻辑
// 优先使用 BodyPartDef 的对称标签检测
if (part1.def == part2.def)
{
// 检查是否有对称标签
if (part1.def.tags != null)
{
// 使用 BodyPartTagDef 进行对称检测
bool part1Left = part1.def.tags.Any(t => t.defName.Contains("Left"));
bool part1Right = part1.def.tags.Any(t => t.defName.Contains("Right"));
bool part2Left = part2.def.tags.Any(t => t.defName.Contains("Left"));
bool part2Right = part2.def.tags.Any(t => t.defName.Contains("Right"));
if ((part1Left && part2Right) || (part1Right && part2Left))
return true;
}
}
// 备用方案:使用自定义标签检测(支持本地化)
string label1 = part1.customLabel ?? part1.def.label;
string label2 = part2.customLabel ?? part2.def.label;
// 检查是否包含对称标识符
string[] symmetryKeywords = { "Left", "Right", "Left", "Right", "Front", "Back", "Upper", "Lower" };
// 使用翻译键检测,而非硬编码英文
string leftKey = "Left".Translate();
string rightKey = "Right".Translate();
foreach (var keyword in symmetryKeywords)
{
if (label1.Contains(keyword) && label2.Contains(keyword))
{
// 检查是否是对称的关键词对
if ((keyword == "Left" && label2.Contains("Right")) ||
(keyword == "Right" && label2.Contains("Left")) ||
(keyword == "Front" && label2.Contains("Back")) ||
(keyword == "Back" && label2.Contains("Front")))
{
return true;
}
}
}
bool label1HasLeft = label1.Contains(leftKey) || label1.Contains("Left");
bool label1HasRight = label1.Contains(rightKey) || label1.Contains("Right");
bool label2HasLeft = label2.Contains(leftKey) || label2.Contains("Left");
bool label2HasRight = label2.Contains(rightKey) || label2.Contains("Right");
if ((label1HasLeft && label2HasRight) || (label1HasRight && label2HasLeft))
return true;
// 检查前后对称
string frontKey = "Front".Translate();
string backKey = "Back".Translate();
bool label1HasFront = label1.Contains(frontKey) || label1.Contains("Front");
bool label1HasBack = label1.Contains(backKey) || label1.Contains("Back");
bool label2HasFront = label2.Contains(frontKey) || label2.Contains("Front");
bool label2HasBack = label2.Contains(backKey) || label2.Contains("Back");
if ((label1HasFront && label2HasBack) || (label1HasBack && label2HasFront))
return true;
return false;
}

View File

@@ -345,10 +345,11 @@ namespace ArachnaeSwarm
{
int pointsToAdd = Mathf.FloorToInt(accumulatedResearchPoints);
// 消耗灵能科研点
// 先检查并消耗灵能科研点,再添加进度(确保原子性)
if (Props.usePsychicResearchPoints && spellHolder != null)
{
if (!spellHolder.ConsumePsychicResearchPoints(pointsToAdd, $"研究 {currentResearch.LabelCap}"))
// 先检查是否有足够的灵能科研点
if (!spellHolder.HasEnoughPsychicResearchPoints(pointsToAdd))
{
// 灵能科研点不足,停止研究
StopResearching();
@@ -356,9 +357,17 @@ namespace ArachnaeSwarm
Pawn, MessageTypeDefOf.NegativeEvent);
return;
}
// 消耗灵能科研点
if (!spellHolder.ConsumePsychicResearchPoints(pointsToAdd, $"研究 {currentResearch.LabelCap}"))
{
// 消耗失败(理论上不应该发生,因为已经检查过了)
StopResearching();
return;
}
}
// 添加到全局研究进度
// 添加到全局研究进度(在消耗成功后)
AddResearchProgress(pointsToAdd);
// 减去已添加的点数