DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,536 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
pathpkg "path"
|
||||
"strings"
|
||||
|
||||
dnfcompanion "longheng.io/server/internal/modules/dnf/companion"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
const companionQuickSlotCount = 12
|
||||
const companionBasicAttackIndex = companionQuickSlotCount * 2
|
||||
|
||||
// companionQuickSlotKeys 是前段 12 格要按下的原生按键名,顺序与
|
||||
// companionFrontKeyFiles 一一对应(与 legacy 路径的 quickSlotKeys 同源)。
|
||||
var companionQuickSlotKeys = []string{
|
||||
"a", "s", "d", "f", "g", "h", "q", "w", "e", "r", "t", "y",
|
||||
}
|
||||
|
||||
var companionFrontKeyFiles = []string{
|
||||
"aicharacter/magician/biske2/key/1.key",
|
||||
"aicharacter/magician/biske2/key/2.key",
|
||||
"aicharacter/magician/biske2/key/3.key",
|
||||
"aicharacter/magician/biske2/key/4.key",
|
||||
"aicharacter/magician/biske2/key/5.key",
|
||||
"aicharacter/magician/biske2/key/6.key",
|
||||
"aicharacter/magician/biske2/key/a.key",
|
||||
"aicharacter/magician/biske2/key/c.key",
|
||||
"aicharacter/magician/biske2/key/d.key",
|
||||
"aicharacter/magician/biske2/key/f.key",
|
||||
"aicharacter/magician/biske2/key/g.key",
|
||||
"aicharacter/magician/biske2/key/h.key",
|
||||
}
|
||||
|
||||
var companionDirectKeyFiles = []string{
|
||||
"aicharacter/magician/flea_inflame_hands2/key/1.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/2.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/3.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/4.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/5.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/6.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/a.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/c.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/d.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/f.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/g.key",
|
||||
"aicharacter/magician/flea_inflame_hands2/key/h.key",
|
||||
}
|
||||
|
||||
// Use an existing, otherwise-unreferenced native key that contains a short
|
||||
// four-hit X loop. The old mirror_priest key emits one X input and then waits
|
||||
// 300 ms before the AI can choose it again, which makes a support soldier's
|
||||
// basic attack look like isolated taps. Keeping this as a donor path avoids
|
||||
// mutating the shared mirror_priest resource used by other APCs.
|
||||
const companionBasicAttackFile = "aicharacter/_nelios/priest/corps/key/x.key"
|
||||
|
||||
// companionAIRefs resolves the [ai pattern] entries using the same relative
|
||||
// layout used by the native PVF loader. Short names are relative to the AIC's
|
||||
// sibling ai directory; fully-qualified entries are already archive paths.
|
||||
func companionAIRefs(a *platformpvf.Archive, aicPath string, raw []byte) (map[string]string, error) {
|
||||
ts := toks(a, raw)
|
||||
start, end := section(ts, "[ai pattern]", "[/ai pattern]")
|
||||
if start < 0 || end <= start {
|
||||
return nil, fmt.Errorf("AIC %s has no [ai pattern] section", aicPath)
|
||||
}
|
||||
refs := make(map[string]string)
|
||||
for i := start + 1; i < end; i++ {
|
||||
t := ts[i]
|
||||
if t.typ != 6 || !strings.HasSuffix(strings.ToLower(t.txt), ".ai") {
|
||||
continue
|
||||
}
|
||||
candidate := strings.ReplaceAll(t.txt, "\\", "/")
|
||||
if !strings.Contains(candidate, "/") {
|
||||
candidate = pathpkg.Join(pathpkg.Dir(aicPath), "ai", candidate)
|
||||
}
|
||||
file, ok := a.FindFile(candidate)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("AIC %s references missing AI %q (resolved %s)", aicPath, t.txt, candidate)
|
||||
}
|
||||
canonical := strings.ReplaceAll(file.ArchivePath, "\\", "/")
|
||||
base := strings.ToLower(pathpkg.Base(canonical))
|
||||
switch {
|
||||
case strings.Contains(base, "event") && refs["event"] == "":
|
||||
refs["event"] = canonical
|
||||
case strings.Contains(base, "destination") && refs["destination"] == "":
|
||||
refs["destination"] = canonical
|
||||
case strings.Contains(base, "action") && refs["action"] == "":
|
||||
refs["action"] = canonical
|
||||
case strings.Contains(base, "move") && refs["move"] == "":
|
||||
refs["move"] = canonical
|
||||
}
|
||||
}
|
||||
if refs["action"] == "" {
|
||||
return nil, fmt.Errorf("AIC %s has no action AI reference", aicPath)
|
||||
}
|
||||
if refs["destination"] == "" {
|
||||
return nil, fmt.Errorf("AIC %s has no destination AI reference", aicPath)
|
||||
}
|
||||
if refs["move"] == "" {
|
||||
return nil, fmt.Errorf("AIC %s has no move AI reference", aicPath)
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func companionString(a *platformpvf.Archive, value string) []byte {
|
||||
off, err := a.EnsureANSIString(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return rawToken(6, off)
|
||||
}
|
||||
|
||||
func companionTag(a *platformpvf.Archive, value string) []byte {
|
||||
off, err := a.EnsureANSIString(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return rawToken(3, off)
|
||||
}
|
||||
|
||||
func companionAIThink(a *platformpvf.Archive, inner []byte) []byte {
|
||||
out := append([]byte{}, companionTag(a, "[think]")...)
|
||||
out = append(out, inner...)
|
||||
out = append(out, companionTag(a, "[/think]")...)
|
||||
return out
|
||||
}
|
||||
|
||||
// A native think node stores one predicate and one true/false pair. A second
|
||||
// predicate in the same block replaces the first node's condition/branches;
|
||||
// r9 consequently loaded only its final area predicate, with both returns -1.
|
||||
// Always nest a new decision rather than appending conditions to one block.
|
||||
func companionBranch(a *platformpvf.Archive, condition, yes, no []byte) []byte {
|
||||
body := append([]byte{}, condition...)
|
||||
body = append(body, companionTag(a, "[true]")...)
|
||||
body = append(body, yes...)
|
||||
body = append(body, companionTag(a, "[false]")...)
|
||||
body = append(body, no...)
|
||||
return companionAIThink(a, body)
|
||||
}
|
||||
|
||||
func companionYield(a *platformpvf.Archive) []byte {
|
||||
return append(companionTag(a, "[return]"), rawToken(0, -1)...)
|
||||
}
|
||||
|
||||
// The projection contract fixes quick programs at indexes 0..11. Numeric
|
||||
// returns remove construction-time name lookup from action selection.
|
||||
// The r11 parsing capture also showed that numeric returns require valid
|
||||
// enclosing branches; an index alone cannot repair malformed think blocks.
|
||||
func companionReturnSlot(a *platformpvf.Archive, slot int) []byte {
|
||||
if slot < 0 || slot >= companionQuickSlotCount {
|
||||
panic(fmt.Sprintf("companion action slot out of range: %d", slot))
|
||||
}
|
||||
return append(companionTag(a, "[return]"), rawToken(0, slot)...)
|
||||
}
|
||||
|
||||
func companionCheckReady(a *platformpvf.Archive, key string) []byte {
|
||||
out := append([]byte{}, companionTag(a, "[check key cooltime]")...)
|
||||
out = append(out, companionString(a, key)...)
|
||||
return out
|
||||
}
|
||||
|
||||
// companionReadyChain returns the first key that is not cooling down. The
|
||||
// native grammar reports true for a cooling key, so the true branch continues
|
||||
// and the false branch emits the key. This is the same pattern used by the
|
||||
// checked-in profession AIs and keeps the chain bounded.
|
||||
func companionReadyChain(a *platformpvf.Archive, slots []int) []byte {
|
||||
return companionReadyChainWithFallback(a, slots, companionYield(a))
|
||||
}
|
||||
|
||||
func companionReadyChainWithFallback(a *platformpvf.Archive, slots []int, fallback []byte) []byte {
|
||||
if len(slots) == 0 {
|
||||
// Native 0x026224e0 reads a predicate immediately after [think].
|
||||
// A return-only think exits before consuming its closing delimiter;
|
||||
// r11's live capture then placed slot N in slot N-1's false branch.
|
||||
// A completed chain is a branch leaf, not another decision node.
|
||||
return fallback
|
||||
}
|
||||
slot := slots[0]
|
||||
key := fmt.Sprintf("CompanionSlot%02d", slot)
|
||||
var body []byte
|
||||
body = append(body, companionCheckReady(a, key)...)
|
||||
body = append(body, companionTag(a, "[true]")...)
|
||||
body = append(body, companionReadyChainWithFallback(a, slots[1:], fallback)...)
|
||||
body = append(body, companionTag(a, "[false]")...)
|
||||
body = append(body, companionReturnSlot(a, slot)...)
|
||||
return companionAIThink(a, body)
|
||||
}
|
||||
|
||||
type companionBuffSpec struct {
|
||||
slot int
|
||||
family string
|
||||
skill int
|
||||
}
|
||||
|
||||
// These two nodes are the verified native support-selector placeholders.
|
||||
// Every profession shares this grammar; the owned client selects the actual
|
||||
// learned quickbar skill. These IDs are not another profession's buff IDs.
|
||||
func companionBuffSpecs(job uint8) []companionBuffSpec {
|
||||
return []companionBuffSpec{{1, "priest", 7}, {5, "priest", 53}}
|
||||
}
|
||||
|
||||
// companionPartyBuffChain returns the first configured buff whose party
|
||||
// target does not already have the corresponding native buff. The native
|
||||
// grammar reports true when the buff is present, so the true branch advances
|
||||
// and the false branch emits the matching quick-slot key.
|
||||
func companionPartyBuffChain(a *platformpvf.Archive, specs []companionBuffSpec, fallback []byte) []byte {
|
||||
if len(specs) == 0 {
|
||||
return fallback
|
||||
}
|
||||
spec := specs[0]
|
||||
key := fmt.Sprintf("CompanionSlot%02d", spec.slot)
|
||||
next := companionPartyBuffChain(a, specs[1:], fallback)
|
||||
pending := companionBranch(a, companionCheckReady(a, key), next, companionReturnSlot(a, spec.slot))
|
||||
condition := append(companionTag(a, "[check party buff on]"), companionString(a, spec.family)...)
|
||||
condition = append(condition, rawToken(0, spec.skill)...)
|
||||
return companionBranch(a, condition, next, pending)
|
||||
}
|
||||
|
||||
func companionAreaGate(a *platformpvf.Archive, x, y, halfX, halfY float32) []byte {
|
||||
var out []byte
|
||||
out = append(out, companionTag(a, "[void]")...)
|
||||
out = append(out, companionString(a, "is target in attack area()")...)
|
||||
out = append(out, rawFloat(x)...)
|
||||
out = append(out, rawFloat(y)...)
|
||||
out = append(out, rawFloat(halfX)...)
|
||||
out = append(out, rawFloat(halfY)...)
|
||||
return out
|
||||
}
|
||||
|
||||
func companionEnemyCountGate(a *platformpvf.Archive, x, y, halfX, halfY float32, minimum int) []byte {
|
||||
var out []byte
|
||||
out = append(out, companionTag(a, "[void]")...)
|
||||
out = append(out, companionString(a, "enemy number of in attack area()")...)
|
||||
out = append(out, rawFloat(x)...)
|
||||
out = append(out, rawFloat(y)...)
|
||||
out = append(out, rawFloat(halfX)...)
|
||||
out = append(out, rawFloat(halfY)...)
|
||||
out = append(out, companionTag(a, "[think]")...)
|
||||
out = append(out, companionTag(a, "[last result]")...)
|
||||
out = append(out, companionString(a, ">=")...)
|
||||
out = append(out, rawFloat(float32(minimum))...)
|
||||
return out
|
||||
}
|
||||
|
||||
func companionCountBranch(a *platformpvf.Archive, x, y float32, minimum int, yes, no []byte) []byte {
|
||||
body := companionEnemyCountGate(a, x, y, x/2, y/2, minimum)
|
||||
body = append(body, companionTag(a, "[true]")...)
|
||||
body = append(body, yes...)
|
||||
body = append(body, companionTag(a, "[false]")...)
|
||||
body = append(body, no...)
|
||||
body = append(body, companionTag(a, "[/think]")...)
|
||||
return companionAIThink(a, body)
|
||||
}
|
||||
|
||||
func companionBasicCombatAI(a *platformpvf.Archive) []byte {
|
||||
yield := companionYield(a)
|
||||
basicReturn := append(companionTag(a, "[return]"), rawToken(0, companionBasicAttackIndex)...)
|
||||
ready := companionBranch(a, companionCheckReady(a, "CompanionBasicAttack"), yield, basicReturn)
|
||||
// Native 2495960 skips movement and destination selection whenever an
|
||||
// action is accepted. The r18 zero-cooldown fallback kept accepting x
|
||||
// from the 880-wide spell area, so the APC never approached its enemy.
|
||||
// Check both the selected target's melee area and nearby enemy count:
|
||||
// a stale target alone must not keep emitting attacks in an empty room.
|
||||
near := companionBranch(a, companionAreaGate(a, 200, 120, 100, 60), ready, yield)
|
||||
return companionCountBranch(a, 200, 120, 1, near, yield)
|
||||
}
|
||||
|
||||
func companionAIHeader(a *platformpvf.Archive) []byte {
|
||||
return companionTag(a, "[ai pattern]")
|
||||
}
|
||||
func companionAIFooter(a *platformpvf.Archive) []byte {
|
||||
return companionTag(a, "[/ai pattern]")
|
||||
}
|
||||
|
||||
// The owned native selector consumes this state-0 predicate as the per-source
|
||||
// mode switch. The original state-4 action guard still protects active casts.
|
||||
func companionModeGate(a *platformpvf.Archive, assist, aggressive []byte) []byte {
|
||||
mode := append(companionTag(a, "[my state]"), rawToken(0, 0)...)
|
||||
return companionBranch(a, mode, assist, aggressive)
|
||||
}
|
||||
|
||||
func buildCompanionDestinationAI(a *platformpvf.Archive, profile dnfcompanion.AIProfile, job uint8) []byte {
|
||||
friend := append(companionTag(a, "[set friend character]"), rawToken(0, 0)...)
|
||||
follow := companionBranch(a, friend, append(companionTag(a, "[return]"), rawToken(0, 5)...), companionYield(a))
|
||||
target := append(companionTag(a, "[target state]"), rawFloat(4)...)
|
||||
fight := companionBranch(a, target, append(companionTag(a, "[return]"), rawToken(0, 1)...), append(companionTag(a, "[return]"), rawToken(0, 0)...))
|
||||
out := append(companionAIHeader(a), companionModeGate(a, follow, fight)...)
|
||||
return append(out, companionAIFooter(a)...)
|
||||
}
|
||||
|
||||
func buildCompanionMoveAI(a *platformpvf.Archive, profile dnfcompanion.AIProfile, job uint8) []byte {
|
||||
tuning := dnfcompanion.EffectiveAITuning(dnfcompanion.NormalizeAIProfile(profile).Tuning)
|
||||
// Leader or enemy: stop close to the selected actor. ChaseRange controls
|
||||
// skill engagement, never a stand-off distance that prevents melee.
|
||||
makeMove := func() []byte {
|
||||
condition := append(companionTag(a, "[target distance]"), companionString(a, ">=")...)
|
||||
condition = append(condition, rawFloat(float32(tuning.FollowDistance))...)
|
||||
return companionBranch(a, condition, append(companionTag(a, "[return]"), rawFloat(1)...), append(companionTag(a, "[return]"), rawFloat(0)...))
|
||||
}
|
||||
out := append(companionAIHeader(a), companionModeGate(a, makeMove(), makeMove())...)
|
||||
return append(out, companionAIFooter(a)...)
|
||||
}
|
||||
|
||||
// The stock event scripts refer to profession-specific key names (combo1,
|
||||
// elemental, ...). Once the AIC key stream is replaced by the profile's
|
||||
// CompanionSlot contract those names no longer exist. Returning -1 keeps
|
||||
// attack-success events from bypassing the profile action state machine.
|
||||
func buildCompanionEventAI(a *platformpvf.Archive, profiles ...dnfcompanion.AIProfile) []byte {
|
||||
profile := dnfcompanion.DefaultAIProfile(4)
|
||||
if len(profiles) != 0 {
|
||||
profile = dnfcompanion.NormalizeAIProfile(profiles[0])
|
||||
}
|
||||
tuning := dnfcompanion.EffectiveAITuning(profile.Tuning)
|
||||
// This remains a no-op event. The owned client selector also evaluates
|
||||
// this native predicate after selecting the party leader, so healing uses
|
||||
// the game's current HP percentage rather than a stale server snapshot.
|
||||
hp := append(companionTag(a, "[party member hp]"), companionString(a, "%<")...)
|
||||
hp = append(hp, rawFloat(float32(tuning.HealLeaderPercent))...)
|
||||
decision := companionBranch(a, hp, companionYield(a), companionYield(a))
|
||||
out := append(companionAIHeader(a), decision...)
|
||||
return append(out, companionAIFooter(a)...)
|
||||
}
|
||||
|
||||
func buildCompanionActionAI(a *platformpvf.Archive, profile dnfcompanion.AIProfile, job uint8) []byte {
|
||||
profile = dnfcompanion.NormalizeAIProfile(profile)
|
||||
yield := companionYield(a)
|
||||
state := append(companionTag(a, "[my state]"), rawToken(0, 4)...)
|
||||
party := append(companionTag(a, "[set party member]"), rawToken(0, 1)...)
|
||||
var decision []byte
|
||||
switch strings.ToLower(strings.TrimSpace(profile.ComboPreset)) {
|
||||
case "probe_slot1":
|
||||
decision = companionBranch(a, state, companionReturnSlot(a, 1), companionReturnSlot(a, 1))
|
||||
case "assist_cooldown_probe":
|
||||
ready := companionBranch(a, companionCheckReady(a, "CompanionSlot01"), yield,
|
||||
companionReturnSlot(a, 1))
|
||||
decision = companionBranch(a, state, yield, companionBranch(a, party, ready, yield))
|
||||
default:
|
||||
tuning := dnfcompanion.EffectiveAITuning(profile.Tuning)
|
||||
attacks := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
|
||||
// Descending chain is the serialized AoE-lane contract. The owned
|
||||
// client validates/binds it and skips skills without an area role.
|
||||
areaSlots := []int{11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
|
||||
rangeX := tuning.ChaseRange * 2
|
||||
if rangeX < 200 {
|
||||
rangeX = 200
|
||||
}
|
||||
rangeY := 200
|
||||
rotation := companionReadyChainWithFallback(a, attacks, companionBasicCombatAI(a))
|
||||
single := companionBranch(a, companionAreaGate(a, float32(rangeX), float32(rangeY), float32(rangeX/2), float32(rangeY/2)), rotation, yield)
|
||||
crowd := companionCountBranch(a, float32(rangeX), float32(rangeY), profile.AOEMinTargets,
|
||||
companionReadyChainWithFallback(a, areaSlots, single), single)
|
||||
aggressive := companionBranch(a, companionAreaGate(a, float32(rangeX), float32(rangeY), float32(rangeX/2), float32(rangeY/2)), crowd, yield)
|
||||
aggressive = companionCountBranch(a, float32(rangeX), float32(rangeY), 1, aggressive, yield)
|
||||
combat := companionModeGate(a, yield, aggressive)
|
||||
// Always include support selection. GM mode and actual learned/bar
|
||||
// state are runtime inputs; shared AI paths cannot hold per-job code.
|
||||
buffs := companionPartyBuffChain(a, companionBuffSpecs(job), combat)
|
||||
decision = companionBranch(a, state, yield, companionBranch(a, party, buffs, combat))
|
||||
}
|
||||
out := append(companionAIHeader(a), decision...)
|
||||
return append(out, companionAIFooter(a)...)
|
||||
}
|
||||
|
||||
// Reject the concrete r9 failure before repacking. Balanced delimiters alone
|
||||
// did not detect predicates and branch targets overwriting each other.
|
||||
func validateCompanionActionAI(a *platformpvf.Archive, raw []byte) error {
|
||||
type frame struct {
|
||||
predicates int
|
||||
yes, no bool
|
||||
}
|
||||
var stack []frame
|
||||
tokens := toks(a, raw)
|
||||
for i, token := range tokens {
|
||||
if token.typ != 3 {
|
||||
continue
|
||||
}
|
||||
switch token.txt {
|
||||
case "[return str]":
|
||||
return fmt.Errorf("companion action must use fixed program indexes")
|
||||
case "[return]":
|
||||
if i+1 >= len(tokens) || tokens[i+1].typ != 0 || tokens[i+1].val < -1 ||
|
||||
(tokens[i+1].val >= companionQuickSlotCount && tokens[i+1].val != companionBasicAttackIndex) {
|
||||
return fmt.Errorf("companion action return must be -1, quick-program index 0..11, or basic attack 24")
|
||||
}
|
||||
if len(stack) == 0 || stack[len(stack)-1].predicates != 1 ||
|
||||
(!stack[len(stack)-1].yes && !stack[len(stack)-1].no) {
|
||||
return fmt.Errorf("action return must belong to a predicate's true/false branch; return-only think desynchronizes native parsing")
|
||||
}
|
||||
case "[think]":
|
||||
stack = append(stack, frame{})
|
||||
case "[/think]":
|
||||
if len(stack) == 0 {
|
||||
return fmt.Errorf("action AI has unmatched think close")
|
||||
}
|
||||
if stack[len(stack)-1].predicates != 1 {
|
||||
return fmt.Errorf("action think must contain exactly one predicate")
|
||||
}
|
||||
stack = stack[:len(stack)-1]
|
||||
case "[my state]", "[party member hp]", "[set party member]", "[check party buff on]", "[check key cooltime]", "[void]", "[last result]":
|
||||
if len(stack) == 0 {
|
||||
return fmt.Errorf("action predicate outside think")
|
||||
}
|
||||
f := &stack[len(stack)-1]
|
||||
f.predicates++
|
||||
if f.predicates > 1 {
|
||||
return fmt.Errorf("action think contains multiple predicates; native branches would be overwritten")
|
||||
}
|
||||
case "[true]", "[false]":
|
||||
if len(stack) == 0 {
|
||||
return fmt.Errorf("action branch outside think")
|
||||
}
|
||||
f := &stack[len(stack)-1]
|
||||
if token.txt == "[true]" {
|
||||
if f.yes {
|
||||
return fmt.Errorf("action think repeats true branch")
|
||||
}
|
||||
f.yes = true
|
||||
} else {
|
||||
if f.no {
|
||||
return fmt.Errorf("action think repeats false branch")
|
||||
}
|
||||
f.no = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(stack) != 0 {
|
||||
return fmt.Errorf("action AI has unclosed think blocks")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func companionKeyStream(a *platformpvf.Archive) ([]byte, []byte, error) {
|
||||
if len(companionFrontKeyFiles) != companionQuickSlotCount || len(companionDirectKeyFiles) != companionQuickSlotCount {
|
||||
return nil, nil, fmt.Errorf("companion key contract has invalid slot count")
|
||||
}
|
||||
var stream, cooldown []byte
|
||||
for i := 0; i < companionQuickSlotCount; i++ {
|
||||
front, ok := a.FindFile(companionFrontKeyFiles[i])
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("missing front key %s", companionFrontKeyFiles[i])
|
||||
}
|
||||
name := fmt.Sprintf("CompanionSlot%02d", i)
|
||||
stream = append(stream, companionString(a, name)...)
|
||||
stream = append(stream, companionString(a, strings.ReplaceAll(front.ArchivePath, "\\", "/"))...)
|
||||
cooldown = append(cooldown, companionString(a, name)...)
|
||||
cooldown = append(cooldown, rawToken(0, 600)...)
|
||||
}
|
||||
// ProjectionRuntimeKeyContract addresses front programs by i and direct
|
||||
// programs by 12+i. Interleaving the pairs puts a front key at index 12,
|
||||
// causing direct-bytecode-shape rejection before any skill is bound.
|
||||
for i := 0; i < companionQuickSlotCount; i++ {
|
||||
direct, ok := a.FindFile(companionDirectKeyFiles[i])
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("missing direct key %s", companionDirectKeyFiles[i])
|
||||
}
|
||||
stream = append(stream, companionString(a, fmt.Sprintf("CompanionDirectPool%02d", i))...)
|
||||
stream = append(stream, companionString(a, strings.ReplaceAll(direct.ArchivePath, "\\", "/"))...)
|
||||
}
|
||||
basic, ok := a.FindFile(companionBasicAttackFile)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("missing basic attack key %s", companionBasicAttackFile)
|
||||
}
|
||||
stream = append(stream, companionString(a, "CompanionBasicAttack")...)
|
||||
stream = append(stream, companionString(a, strings.ReplaceAll(basic.ArchivePath, "\\", "/"))...)
|
||||
cooldown = append(cooldown, companionString(a, "CompanionBasicAttack")...)
|
||||
cooldown = append(cooldown, rawToken(0, 0)...)
|
||||
return stream, cooldown, nil
|
||||
}
|
||||
|
||||
// Check the positional contract used by ApplyDynamicRuntimeKeyPrograms, not
|
||||
// just the presence of the slot names. An archive can contain every name and
|
||||
// still make the native client reject the entire pool if pairs are interleaved.
|
||||
func validateCompanionKeyStream(a *platformpvf.Archive, raw []byte) error {
|
||||
items := toks(a, raw)
|
||||
start, end := section(items, "[key stream]", "[/key stream]")
|
||||
if start < 0 || end-start-1 != 50 {
|
||||
return fmt.Errorf("companion key stream must contain 25 name/path pairs")
|
||||
}
|
||||
for i := 0; i < 25; i++ {
|
||||
name := "CompanionBasicAttack"
|
||||
path := companionBasicAttackFile
|
||||
if i < companionQuickSlotCount {
|
||||
name = fmt.Sprintf("CompanionSlot%02d", i)
|
||||
path = companionFrontKeyFiles[i]
|
||||
} else if i < companionQuickSlotCount*2 {
|
||||
name = fmt.Sprintf("CompanionDirectPool%02d", i-companionQuickSlotCount)
|
||||
path = companionDirectKeyFiles[i-companionQuickSlotCount]
|
||||
}
|
||||
nameToken, pathToken := items[start+1+i*2], items[start+2+i*2]
|
||||
if nameToken.typ != 6 || pathToken.typ != 6 || nameToken.txt != name || !strings.EqualFold(pathToken.txt, path) {
|
||||
return fmt.Errorf("companion key program %d is %q/%q, want %q/%q", i, nameToken.txt, pathToken.txt, name, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func patchCompanionAICProfile(a *platformpvf.Archive, aicRaw []byte, artifact dnfcompanion.AICArtifact, profile dnfcompanion.AIProfile) ([]platformpvf.RawReplacement, error) {
|
||||
refs, err := companionAIRefs(a, artifact.AICPath, aicRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream, cooldown, err := companionKeyStream(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patched := append([]byte(nil), aicRaw...)
|
||||
ts := toks(a, patched)
|
||||
patched = replaceSection(patched, ts, "[key stream]", "[/key stream]", stream)
|
||||
ts = toks(a, patched)
|
||||
patched = replaceOrInsertSection(a, patched, ts, "[key cooltime]", "[/key cooltime]", "[/key stream]", cooldown)
|
||||
ts = toks(a, patched)
|
||||
values, err := dnfcompanion.ReadAICArtifact(artifact.AICText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for tag, value := range values {
|
||||
patched = replaceIntAfterTag(patched, toks(a, patched), tag, value)
|
||||
}
|
||||
replacements := []platformpvf.RawReplacement{{Path: artifact.AICPath, Data: patched}}
|
||||
action := buildCompanionActionAI(a, profile, artifact.Job)
|
||||
if err := validateCompanionActionAI(a, action); err != nil {
|
||||
return nil, fmt.Errorf("invalid generated action %s: %w", refs["action"], err)
|
||||
}
|
||||
replacements = append(replacements,
|
||||
platformpvf.RawReplacement{Path: refs["action"], Data: action},
|
||||
platformpvf.RawReplacement{Path: refs["destination"], Data: buildCompanionDestinationAI(a, profile, artifact.Job)},
|
||||
platformpvf.RawReplacement{Path: refs["move"], Data: buildCompanionMoveAI(a, profile, artifact.Job)},
|
||||
)
|
||||
if refs["event"] != "" {
|
||||
replacements = append(replacements,
|
||||
platformpvf.RawReplacement{Path: refs["event"], Data: buildCompanionEventAI(a, profile)},
|
||||
)
|
||||
}
|
||||
return replacements, nil
|
||||
}
|
||||
Reference in new issue
Block a user