package companion import ( "fmt" "strings" ) // AITemplateSelector binds one saved profile to a verified AICharacter.lst // entry. The selector is part of the offline contract; it does not imply // that the current client has loaded or executed the entry. type AITemplateSelector struct { Key string `json:"key"` Job uint8 `json:"job"` ClassID uint16 `json:"class_id"` Behavior string `json:"behavior"` ComboPreset string `json:"combo_preset"` APCCode int64 `json:"apc_code"` AICPath string `json:"aic_path"` BaseAICPath string `json:"base_aic_path"` Legacy bool `json:"legacy"` } type aiTemplateVariant struct { Code int64 Path string } type aiTemplatePair struct { Profession string Assist aiTemplateVariant Aggressive aiTemplateVariant } // These are the stable APC/AIC pairs selected by the checked-in client native // projection contract (90CN_companion_p0.h) and verified against the current // AICharacter.lst. The server must patch the AIC that the client actually // spawns. The old friendly-doll entries (for example christian_ronaldo and // yugo) are valid archive assets, but they are not the active baseline APCs // used by the native companion projection and therefore cannot change live // behaviour. // // Assist and aggressive profiles intentionally share the active baseline AIC // for a job. The profile-specific action/destination/move programs are // compiled into that AIC; the caller must build/select one profile for a // baseline at a time because a single PVF cannot contain two different // programs at the same archive path. var aiTemplatePairs = map[uint8]aiTemplatePair{ 0: {"swordman", aiTemplateVariant{900, "aicharacter/_bizarre/swordman/mirror_swordman/mirror_swordman.aic"}, aiTemplateVariant{900, "aicharacter/_bizarre/swordman/mirror_swordman/mirror_swordman.aic"}}, 1: {"fighter", aiTemplateVariant{5405, "aicharacter/fighter/arad_fighter/arad_fighter_doll.aic"}, aiTemplateVariant{5405, "aicharacter/fighter/arad_fighter/arad_fighter_doll.aic"}}, 2: {"gunner", aiTemplateVariant{10903, "aicharacter/_bizarre/gunner/mirror_gunner/mirror_gunner.aic"}, aiTemplateVariant{10903, "aicharacter/_bizarre/gunner/mirror_gunner/mirror_gunner.aic"}}, 3: {"mage", aiTemplateVariant{15902, "aicharacter/_bizarre/magician/mirror_magician/mirror_magician.aic"}, aiTemplateVariant{15902, "aicharacter/_bizarre/magician/mirror_magician/mirror_magician.aic"}}, 4: {"priest", aiTemplateVariant{20902, "aicharacter/_bizarre/priest/mirror_priest/mirror_priest.aic"}, aiTemplateVariant{20902, "aicharacter/_bizarre/priest/mirror_priest/mirror_priest.aic"}}, 5: {"atgunner", aiTemplateVariant{25900, "aicharacter/_bizarre/atgunner/mirror_atgunner/mirror_atgunner.aic"}, aiTemplateVariant{25900, "aicharacter/_bizarre/atgunner/mirror_atgunner/mirror_atgunner.aic"}}, 6: {"thief", aiTemplateVariant{30901, "aicharacter/_bizarre/Thief/mirror_Thief/mirror_Thief.aic"}, aiTemplateVariant{30901, "aicharacter/_bizarre/Thief/mirror_Thief/mirror_Thief.aic"}}, 7: {"atfighter", aiTemplateVariant{35903, "aicharacter/_bizarre/atfighter/mirror_atfighter/mirror_atfighter.aic"}, aiTemplateVariant{35903, "aicharacter/_bizarre/atfighter/mirror_atfighter/mirror_atfighter.aic"}}, 8: {"atmage", aiTemplateVariant{40800, "aicharacter/autocharacter/atmage/auto1/auto_atmage1.aic"}, aiTemplateVariant{40800, "aicharacter/autocharacter/atmage/auto1/auto_atmage1.aic"}}, 9: {"demonic_swordman", aiTemplateVariant{902, "aicharacter/_bizarre/swordman/despair_darkknight/despair_darkknight.aic"}, aiTemplateVariant{902, "aicharacter/_bizarre/swordman/despair_darkknight/despair_darkknight.aic"}}, 10: {"creator", aiTemplateVariant{15903, "aicharacter/_bizarre/magician/despair_creator/despair_creator.aic"}, aiTemplateVariant{15903, "aicharacter/_bizarre/magician/despair_creator/despair_creator.aic"}}, 11: {"atswordman", aiTemplateVariant{56901, "aicharacter/_bizarre/atswordman/despair_atswordman/despair_atswordman.aic"}, aiTemplateVariant{56901, "aicharacter/_bizarre/atswordman/despair_atswordman/despair_atswordman.aic"}}, 12: {"knight", aiTemplateVariant{60901, "aicharacter/_bizarre/knight/despair_knight/despair_knight.aic"}, aiTemplateVariant{60901, "aicharacter/_bizarre/knight/despair_knight/despair_knight.aic"}}, 13: {"demonic_lancer", aiTemplateVariant{65001, "aicharacter/demoniclancer/mirror_demonicLancer/mirror_demonicLancer.aic"}, aiTemplateVariant{65001, "aicharacter/demoniclancer/mirror_demonicLancer/mirror_demonicLancer.aic"}}, // job 14(女圣职者)必须保持 70003 `atpriest/despair_atpriest.aic`,**不要改成 70002**。 // // 曾试过改成 70002 `atpriest/mirror_demoniclancer.aic`,实测完全无效。原因是 // 选哪个 APC 由**客户端内置基线表**决定,服务端的 ai_template 只决定“补哪条 AIC”。 // // 90CN_companion_p0.h 的基线表格式是 {job, sourceGrow, apcCode, level, aicJob, aicGrow}: // // {14,0,70003,70,14,0}, // <- stable 外壳(sourceGrow=0 且 aicGrow=0) // {14,1,71200,86,14,1}, // {14,2,71203,86,14,2}, // // ProjectionBaselineContract::TrySelect 的逻辑是 // // selected = stable ? *stable : *exact; // 优先 grow=0 的稳定外壳 // // 所以奶妈 grow_type=33(firstGrow=1, awakening=2)虽然 exact 命中 {14,1,71200}, // 最终仍被 stable 抢走,**客户端永远用 APC 70003 的 AIC**。服务端把模板改成 70002 // 只会让覆盖打到一条没人用的 AIC 上,反而让 70003 丢掉 12 格快捷栏程序 —— 表现为 // “能召唤但不放技能、不攻击”。 // // 另外注意:外壳 APC 固定是 grow=0 的形态,客户端只把源角色的 grow/觉醒/技能桥接 // 到 AIC 的每实例副本上,**视觉职业仍然是那个 grow=0 外壳**。所以支援兵看起来 // “未觉醒”是这套投影设计的固有限制,不是模板选错。 14: {"atpriest", aiTemplateVariant{70003, "aicharacter/atpriest/despair_atpriest/despair_atpriest.aic"}, aiTemplateVariant{70003, "aicharacter/atpriest/despair_atpriest/despair_atpriest.aic"}}, 15: {"gunblader", aiTemplateVariant{75001, "aicharacter/gunblader/mirror_gunblader/mirror_gunblader.aic"}, aiTemplateVariant{75001, "aicharacter/gunblader/mirror_gunblader/mirror_gunblader.aic"}}, } func SelectAITemplate(profile AIProfile, job uint16) AITemplateSelector { profile = NormalizeAIProfile(profile) job8 := uint8(job) pair, ok := aiTemplatePairs[job8] if !ok { job8 = 1 pair = aiTemplatePairs[job8] } classID := profile.ClassID if classID == 0 { classID = job } variant := pair.Assist if profile.Behavior == "aggressive" { variant = pair.Aggressive } return AITemplateSelector{ Key: fmt.Sprintf("%s/%s/job-%02d/class-%d/%s", pair.Profession, profile.Behavior, job8, classID, artifactSlug(profile.ComboPreset)), Job: job8, ClassID: classID, Behavior: profile.Behavior, ComboPreset: profile.ComboPreset, APCCode: variant.Code, AICPath: variant.Path, BaseAICPath: variant.Path, Legacy: variant.Code == 5405, } } // ResolveAITemplate is the descriptive name used by server projection code; // SelectAITemplate remains the compiler-facing alias. func ResolveAITemplate(profile AIProfile, job uint16) AITemplateSelector { return SelectAITemplate(profile, job) } func ValidateAITemplateSelector(selector AITemplateSelector) error { if strings.TrimSpace(selector.Key) == "" || selector.APCCode <= 0 || selector.AICPath == "" { return ErrSelection } want := SelectAITemplate(AIProfile{Behavior: selector.Behavior, ClassID: selector.ClassID, ComboPreset: selector.ComboPreset}, uint16(selector.Job)) if selector.APCCode != want.APCCode || !strings.EqualFold(selector.AICPath, want.AICPath) { return ErrSelection } return nil } func (s AITemplateSelector) Valid() bool { return ValidateAITemplateSelector(s) == nil }