package dnfbridge import ( "fmt" dnfcompanion "longheng.io/server/internal/modules/dnf/companion" "longheng.io/server/internal/modules/dnf/worldmap" ) // ⚠️⚠️ 实测禁用的路径 —— 不要再尝试用它"免客户端补丁"地召唤支援兵 ⚠️⚠️ // // 本文件是 Server-90 的「服务端自建 APC」通道(AICharacter code 5405, // 阵营 "[character]"),用来把支援兵作为一条普通 extended-actor 行塞进 // 副本的 START_MAP 列表。把它接进本部署后做过一次实机验证,结论如下: // // 1. 服务端侧**完全正常**:配置冻结 → run ready → 注入条件成立 → // op29 的 actor 数量确实 +1(实测 5 → 6,包长 144 → 165 字节)。 // 本文件的 actor 行构造与 Server-90 逐行一致。 // // 2. 客户端侧**必然崩溃**:零售 23.5.13 DNF.exe(指纹 8A7AF614…, // profile 90cn-decode-bypass-v1)在解析这条含自建 APC 的 op29 时 // 抛出 0xC0000005 空指针访问违例并闪退: // exception code=0xC0000005 address=0236400D access_address=0x00000000 // esi=0x00000000 edx=0x00000000 // // 3. 因此这条路**只对官方配套客户端有效**:Server-90 的 90CN 客户端补丁 // (NewNativeCompanionBindings,锁定 DB4 可执行文件)负责让客户端认得 // 这条行并走原生 APC 召唤。零售客户端没有这套适配,硬塞就会崩。 // // 结论:支援兵在零售客户端上**必须**走 `0x7F41` 投影 + 客户端补丁那条路 // (见 companion_dungeon_control.go 的 sendFrozenCurrentDungeonCompanionProjection)。 // 本通道默认关闭(环境变量与标记文件都不设即关闭),保留代码仅供 // 配套客户端环境使用,勿在零售客户端上打开。 // // ⚠️ 另有一个**门控缺陷**,启用前必须先修: // `planRuntimeDungeonExtendedActors` 的 4 个换房调用点 // (dungeon_move_runtime.go 的 363 / 659 / 779 / 927:普通走图、Boss 门口、 // 剧情阶段、分层地图)**没有**传 `companionEnabled` 变参,于是它们退回默认 // 分支,只看 `currentDungeonCompanionP0StartMapEnabled()`(环境变量/标记 // 文件),**完全不看 run.Ready**。 // // 后果:开关开着但配置未就绪时,进副本不注入(runtime_state 那处有 run gate) // 而**第一次换房就注入** → 表现为"进图正常、一换房就闪退",极难排查。 // 要启用本通道,先把这 4 处改成传入与 dungeon_runtime_state.go:961 相同的 // run gate 值。 const currentDungeonCompanionP0APCCode = int64(5405) func appendCurrentDungeonCompanionP0StartMapActor( plan *runtimeDungeonExtendedActorPlan, aiCharacterCatalog *pvfDungeonAICharacterCatalog, ) error { return appendCurrentDungeonCompanionP0StartMapActorForRun( plan, aiCharacterCatalog, currentDungeonCompanionP0StartMapEnabled(), ) } func appendCurrentDungeonCompanionP0StartMapActorForRun( plan *runtimeDungeonExtendedActorPlan, aiCharacterCatalog *pvfDungeonAICharacterCatalog, enabled bool, ) error { selector := dnfcompanion.ResolveAITemplate(dnfcompanion.DefaultAIProfile(1), 1) return appendCurrentDungeonCompanionP0StartMapActorWithTemplate(plan, aiCharacterCatalog, enabled, selector) } func appendCurrentDungeonCompanionP0StartMapActorWithTemplate( plan *runtimeDungeonExtendedActorPlan, aiCharacterCatalog *pvfDungeonAICharacterCatalog, enabled bool, selector dnfcompanion.AITemplateSelector, ) error { if !enabled { return nil } if plan == nil { return errDungeonWorldMapUnavailable } if aiCharacterCatalog == nil { return errDungeonAICharacterUnavailable } if plan.NextObjectKey == 0 || plan.NextObjectKey > uint32(^uint16(0)) { return fmt.Errorf("%w: object_key=%d", errDungeonExtendedActorObjectKeyRange, plan.NextObjectKey) } if selector.APCCode <= 0 { selector.APCCode = currentDungeonCompanionP0APCCode } definition, found, err := aiCharacterCatalog.Find(selector.APCCode) if err != nil { return fmt.Errorf("resolve companion P0 AI character code=%d: %w", selector.APCCode, err) } if !found { return fmt.Errorf("%w: companion P0 AI character code=%d", errDungeonAICharacterDefinitionMiss, selector.APCCode) } apcPacketIndex := uint32(0) for _, actor := range plan.Actors { if actor.Packet.Type >= 5 && actor.Packet.Type <= 8 { apcPacketIndex++ } } actorValue := worldmap.AICharacter{ Code: selector.APCCode, Faction: "[character]", AIType: "[normal]", } actor, err := newRuntimeDungeonExtendedActor( runtimeDungeonActorAICharacter, plan.NextObjectKey, selector.APCCode, currentDungeonStartMapActor{ PacketIndex: apcPacketIndex, Level: definition.Level, Type: 5, Blocking: 0, }, ) if err != nil { return err } actor.AICharacter = cloneRuntimeDungeonAICharacter(actorValue) definitionCopy := cloneDungeonAICharacterDefinition(definition) actor.AICharacterMetadata = &definitionCopy plan.Actors = append(plan.Actors, actor) plan.NextObjectKey++ plan.Diagnostics = append(plan.Diagnostics, fmt.Sprintf( "companion P0 START_MAP APC code=%d object_key=%d packet_index=%d level=%d faction=character", selector.APCCode, actor.ObjectKey, actor.Packet.PacketIndex, actor.Packet.Level, )) return nil } func currentDungeonCompanionP0ActorFromPlan( plan runtimeDungeonExtendedActorPlan, ) (runtimeDungeonExtendedActor, bool) { for _, actor := range plan.Actors { if actor.Kind == runtimeDungeonActorAICharacter && actor.Packet.Code == uint32(currentDungeonCompanionP0APCCode) && actor.HostileReference == nil { return actor, true } } return runtimeDungeonExtendedActor{}, false }