package companion import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strconv" "strings" ) // StrategyCapability is a named primitive that the offline compiler is // allowed to consume. Keeping this list explicit prevents a Lua profile from // injecting arbitrary AIC tags or silently claiming a missing client feature. type StrategyCapability string const ( CapabilityThinkTerm StrategyCapability = "think_term" CapabilityDistance StrategyCapability = "target_distance" CapabilityCooldown StrategyCapability = "key_cooldown" CapabilityActionStream StrategyCapability = "action_stream" CapabilityFriendAnchor StrategyCapability = "friend_anchor" CapabilityPartyHP StrategyCapability = "party_hp" CapabilityEnemyCount StrategyCapability = "enemy_count_in_area" CapabilityLeaderFollow StrategyCapability = "leader_follow" CapabilityLeaderBuff StrategyCapability = "leader_buff" CapabilityActionAck StrategyCapability = "action_ack" CapabilityTargetSticky StrategyCapability = "target_sticky" CapabilityStuckDetour StrategyCapability = "stuck_detour" ) // StrategyCapabilities describes the currently proven compiler/runtime // surface. The defaults reflect the AIC tokens found in the checked-in PVF; // the dynamic leader/action features remain intentionally false until a real // client implementation proves them. type StrategyCapabilities struct { ThinkTerm bool `json:"think_term"` Distance bool `json:"target_distance"` Cooldown bool `json:"key_cooldown"` ActionStream bool `json:"action_stream"` FriendAnchor bool `json:"friend_anchor"` PartyHP bool `json:"party_hp"` EnemyCount bool `json:"enemy_count_in_area"` LeaderFollow bool `json:"leader_follow"` LeaderBuff bool `json:"leader_buff"` ActionAck bool `json:"action_ack"` TargetSticky bool `json:"target_sticky"` StuckDetour bool `json:"stuck_detour"` } func DefaultStrategyCapabilities() StrategyCapabilities { return StrategyCapabilities{ ThinkTerm: true, Distance: true, Cooldown: true, ActionStream: true, FriendAnchor: true, PartyHP: true, EnemyCount: true, LeaderFollow: false, LeaderBuff: false, ActionAck: false, TargetSticky: false, StuckDetour: false, } } func (c StrategyCapabilities) Has(cap StrategyCapability) bool { switch cap { case CapabilityThinkTerm: return c.ThinkTerm case CapabilityDistance: return c.Distance case CapabilityCooldown: return c.Cooldown case CapabilityActionStream: return c.ActionStream case CapabilityFriendAnchor: return c.FriendAnchor case CapabilityPartyHP: return c.PartyHP case CapabilityEnemyCount: return c.EnemyCount case CapabilityLeaderFollow: return c.LeaderFollow case CapabilityLeaderBuff: return c.LeaderBuff case CapabilityActionAck: return c.ActionAck case CapabilityTargetSticky: return c.TargetSticky case CapabilityStuckDetour: return c.StuckDetour default: return false } } type StrategyStep struct { Name string `json:"name"` Priority int `json:"priority"` Value int `json:"value,omitempty"` } // CompiledStrategy is an offline artifact. It can be written into a // generated AIC/PVF overlay by the companionpvf tool, but it is never marked // as applied until a future client executor reports a real action receipt. type CompiledStrategy struct { Status string `json:"status"` LuaSource string `json:"lua_source"` LuaSHA256 string `json:"lua_sha256"` Steps []StrategyStep `json:"steps"` MissingCapabilities []StrategyCapability `json:"missing_capabilities,omitempty"` Warnings []string `json:"warnings,omitempty"` } func (r CompiledStrategy) ReadyForStaticAIC() bool { return r.Status == "compiled_static" || r.Status == "compiled_with_gaps" } // CompileAIProfile converts the high-level GM profile to a small, auditable // strategy IR and a Lua configuration source. The IR is deliberately limited // to known decisions; unproven dynamic features are reported as gaps instead // of being silently treated as live behaviour. func CompileAIProfile(profile AIProfile, caps StrategyCapabilities) (CompiledStrategy, error) { profile = NormalizeAIProfile(profile) if err := profile.Validate(); err != nil { return CompiledStrategy{}, err } tuning := EffectiveAITuning(profile.Tuning) steps := []StrategyStep{ {Name: "think", Priority: 100, Value: tuning.ThinkIntervalMS}, {Name: "vision", Priority: 95, Value: tuning.VisionRange}, {Name: "keep_distance", Priority: 90, Value: tuning.FollowDistance}, } missing := make(map[StrategyCapability]struct{}) warnings := []string{} need := func(cap StrategyCapability, step StrategyStep, warning string) { if caps.Has(cap) { steps = append(steps, step) return } missing[cap] = struct{}{} if warning != "" { warnings = append(warnings, warning) } } if profile.Behavior == "assist" { need(CapabilityFriendAnchor, StrategyStep{Name: "assist_anchor", Priority: 85, Value: tuning.AssistLeashX}, "辅助站位使用已验证的 friend anchor 近似;固定队长后方坐标仍需客户端能力。") need(CapabilityLeaderFollow, StrategyStep{Name: "follow_leader", Priority: 84, Value: tuning.FollowDistance}, "当前没有经过验证的队长坐标读取,未宣称实时躲在主角后方。") if profile.BuffPolicy != AIBuffPolicyOff { need(CapabilityLeaderBuff, StrategyStep{Name: "buff_leader", Priority: 120}, "BUFF 优先级已保存,但当前编译器没有真实队长施法回执。") } if tuning.HealLeaderPercent > 0 { need(CapabilityPartyHP, StrategyStep{Name: "heal_leader", Priority: 118, Value: tuning.HealLeaderPercent}, "治疗阈值可编译为 party hp 条件;技能实际生效仍需 AIC/PVF 回读。") } steps = append(steps, StrategyStep{Name: "assist_attack_gate", Priority: 40, Value: tuning.AssistAttackIntervalMS}) } else { if profile.BuffPolicy != AIBuffPolicyOff { need(CapabilityActionStream, StrategyStep{Name: "buff_first", Priority: 120}, "先 BUFF 后攻击仅能生成有序技能流,当前没有动作回执。") } steps = append(steps, StrategyStep{Name: "attack_rotation", Priority: 70, Value: tuning.ChaseRange}) } if profile.ComboEnabled { need(CapabilityCooldown, StrategyStep{Name: "combo_rotation", Priority: 80}, "职业连招使用白名单 key stream;没有冷却原语时拒绝生成。") } if profile.AOEMinTargets >= 2 { need(CapabilityEnemyCount, StrategyStep{Name: "aoe_when_many", Priority: 105, Value: profile.AOEMinTargets}, "范围技能条件依赖已验证的区域敌人数;技能键仍由职业模板提供。") } if tuning.TargetStickinessMS > 0 { need(CapabilityTargetSticky, StrategyStep{Name: "target_sticky", Priority: 60, Value: tuning.TargetStickinessMS}, "目标黏性参数已保存,当前 AIC 没有可证明的目标锁定回执。") } if tuning.StuckMS > 0 || tuning.DetourMS > 0 || tuning.GiveUpMS > 0 { need(CapabilityStuckDetour, StrategyStep{Name: "stuck_detour_give_up", Priority: 30, Value: tuning.GiveUpMS}, "卡住重试、绕行和放弃目标等待客户端位置/目标状态。") } missingList := make([]StrategyCapability, 0, len(missing)) for cap := range missing { missingList = append(missingList, cap) } sort.Slice(missingList, func(i, j int) bool { return missingList[i] < missingList[j] }) sort.Slice(steps, func(i, j int) bool { if steps[i].Priority != steps[j].Priority { return steps[i].Priority > steps[j].Priority } return steps[i].Name < steps[j].Name }) lua := renderLuaPolicy(profile, tuning, steps) hash := sha256.Sum256([]byte(lua)) status := "compiled_static" if len(missingList) > 0 { status = "compiled_with_gaps" } return CompiledStrategy{ Status: status, LuaSource: lua, LuaSHA256: hex.EncodeToString(hash[:]), Steps: steps, MissingCapabilities: missingList, Warnings: warnings, }, nil } func luaString(value string) string { var b strings.Builder b.WriteByte('"') for _, r := range value { switch r { case '\\': b.WriteString("\\\\") case '"': b.WriteString("\\\"") case '\n': b.WriteString("\\n") case '\r': b.WriteString("\\r") case '\t': b.WriteString("\\t") default: if r < 32 || r == 127 { fmt.Fprintf(&b, "\\%03d", r) } else { b.WriteRune(r) } } } b.WriteByte('"') return b.String() } func renderLuaPolicy(profile AIProfile, tuning AITuning, steps []StrategyStep) string { var b strings.Builder b.WriteString("-- generated by companion strategy compiler; data only\nreturn {\n") fmt.Fprintf(&b, " version = 1, behavior = %s, class_id = %d, buff_policy = %s, combo_preset = %s, combo_enabled = %s, aoe_min_targets = %d, protect_leader = %s,\n", luaString(profile.Behavior), profile.ClassID, luaString(profile.BuffPolicy), luaString(profile.ComboPreset), strconv.FormatBool(profile.ComboEnabled), profile.AOEMinTargets, strconv.FormatBool(profile.ProtectLeader)) fmt.Fprintf(&b, " tuning = { think_interval_ms = %d, assist_attack_interval_ms = %d, assist_leash_x = %d, assist_leash_y = %d, chase_range = %d, vision_range = %d, target_stickiness_ms = %d, heal_leader_percent = %d, heal_self_percent = %d, retry_ms = %d, support_retry_ms = %d, busy_move_ms = %d, stuck_ms = %d, detour_ms = %d, give_up_ms = %d, follow_distance = %d },\n", tuning.ThinkIntervalMS, tuning.AssistAttackIntervalMS, tuning.AssistLeashX, tuning.AssistLeashY, tuning.ChaseRange, tuning.VisionRange, tuning.TargetStickinessMS, tuning.HealLeaderPercent, tuning.HealSelfPercent, tuning.RetryMS, tuning.SupportRetryMS, tuning.BusyMoveMS, tuning.StuckMS, tuning.DetourMS, tuning.GiveUpMS, tuning.FollowDistance) b.WriteString(" steps = {\n") for _, step := range steps { fmt.Fprintf(&b, " { name = %s, priority = %d, value = %d },\n", luaString(step.Name), step.Priority, step.Value) } b.WriteString(" },\n}\n") return b.String() }