DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,137 @@
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
}