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

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

249 lines
7.7 KiB
Go

package dnfbridge
import (
"errors"
"fmt"
"math"
"strings"
"longheng.io/server/internal/modules/dnf/jobmap"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
const currentAvatarFittingRoomPVFPath = "etc/chn_avatar_fitting_room.etc"
var currentAvatarFittingRoomPartNames = [currentAvatarFittingRoomPartCount]string{
"hair", "hat", "face", "neck", "coat", "belt", "pants", "shoes",
}
// The PVF file and current EXE enumerate fitting-room parts in the order
// above. Durable equipment uses the ordinary avatar slot numbering below.
var currentAvatarFittingRoomEquipmentSlots = [currentAvatarFittingRoomPartCount]int16{
1, 0, 2, 6, 3, 7, 4, 5,
}
type currentAvatarFittingRoomSet struct {
Rare bool
Jobs map[byte][currentAvatarFittingRoomPartCount]uint32
}
type currentAvatarFittingRoomConfig struct {
TicketItemID uint32
GaugeThreshold int64
GaugeRewardID uint32
GaugeRewardCount int64
NormalRate int64
RareRate int64
Sets []currentAvatarFittingRoomSet
}
func loadCurrentAvatarFittingRoomConfig(source dnfpvf.Source) (currentAvatarFittingRoomConfig, error) {
if source == nil {
return currentAvatarFittingRoomConfig{}, errCurrentAvatarFittingRoomUnavailable
}
text, err := source.ReadText(currentAvatarFittingRoomPVFPath)
if err != nil {
return currentAvatarFittingRoomConfig{}, errors.Join(errCurrentAvatarFittingRoomUnavailable, err)
}
document, err := dnfpvf.Parse(currentAvatarFittingRoomPVFPath, text)
if err != nil {
return currentAvatarFittingRoomConfig{}, errors.Join(errCurrentAvatarFittingRoomUnavailable, err)
}
config := currentAvatarFittingRoomConfig{}
ticket := currentAvatarFittingRoomSectionInts(document, "ticket index")
vip := currentAvatarFittingRoomSectionInts(document, "vip gauge")
rates := currentAvatarFittingRoomSectionInts(document, "rate")
if len(ticket) < 1 || ticket[0] <= 0 || ticket[0] > math.MaxUint32 ||
len(vip) < 3 || vip[0] <= 0 || vip[1] <= 0 || vip[1] > math.MaxUint32 || vip[2] <= 0 ||
len(rates) < 2 || rates[0] < 0 || rates[1] < 0 || rates[0]+rates[1] <= 0 {
return currentAvatarFittingRoomConfig{}, fmt.Errorf("%w: incomplete %s", errCurrentAvatarFittingRoomUnavailable, currentAvatarFittingRoomPVFPath)
}
config.TicketItemID = uint32(ticket[0])
config.GaugeThreshold = vip[0]
config.GaugeRewardID = uint32(vip[1])
config.GaugeRewardCount = vip[2]
config.NormalRate = rates[0]
config.RareRate = rates[1]
for _, section := range document.Sections {
if !strings.EqualFold(strings.TrimSpace(section.Name), "fitting room avatar") {
continue
}
if section.Start < 0 || section.End > len(document.Tokens) || section.Start > section.End {
return currentAvatarFittingRoomConfig{}, errCurrentAvatarFittingRoomUnavailable
}
set, parseErr := parseCurrentAvatarFittingRoomSet(document.Tokens[section.Start:section.End])
if parseErr != nil {
return currentAvatarFittingRoomConfig{}, parseErr
}
config.Sets = append(config.Sets, set)
}
if len(config.Sets) == 0 {
return currentAvatarFittingRoomConfig{}, errCurrentAvatarFittingRoomUnavailable
}
return config, nil
}
func parseCurrentAvatarFittingRoomSet(tokens []dnfpvf.Token) (currentAvatarFittingRoomSet, error) {
set := currentAvatarFittingRoomSet{Jobs: make(map[byte][currentAvatarFittingRoomPartCount]uint32)}
markerFound := false
jobTags := make(map[string]byte)
for job := 0; job <= jobmap.MaxID; job++ {
for _, tag := range jobmap.QuestTags(job) {
jobTags[normalizeCurrentAvatarFittingRoomLabel(tag)] = byte(job)
}
}
currentJob := byte(0)
jobActive := false
sequential := 0
values := [currentAvatarFittingRoomPartCount]uint32{}
seen := [currentAvatarFittingRoomPartCount]bool{}
flush := func() error {
if !jobActive {
return nil
}
for index := range seen {
if !seen[index] || values[index] == 0 {
return fmt.Errorf("%w: job=%d missing fitting part=%s", errCurrentAvatarFittingRoomUnavailable, currentJob, currentAvatarFittingRoomPartNames[index])
}
}
set.Jobs[currentJob] = values
return nil
}
for index := 0; index < len(tokens); index++ {
token := tokens[index]
if !markerFound && token.Kind == dnfpvf.TokenInt {
set.Rare = token.Int != 0
markerFound = true
continue
}
if token.Kind != dnfpvf.TokenString && token.Kind != dnfpvf.TokenIdent {
continue
}
label := normalizeCurrentAvatarFittingRoomLabel(token.Value)
if job, ok := jobTags[label]; ok {
if err := flush(); err != nil {
return currentAvatarFittingRoomSet{}, err
}
currentJob, jobActive, sequential = job, true, 0
values = [currentAvatarFittingRoomPartCount]uint32{}
seen = [currentAvatarFittingRoomPartCount]bool{}
continue
}
if !jobActive {
continue
}
partIndex := currentAvatarFittingRoomPartIndex(label)
if partIndex < 0 {
continue
}
for lookahead := index + 1; lookahead < len(tokens); lookahead++ {
if tokens[lookahead].Kind == dnfpvf.TokenInt {
if tokens[lookahead].Int <= 0 || tokens[lookahead].Int > math.MaxUint32 {
return currentAvatarFittingRoomSet{}, errCurrentAvatarFittingRoomUnavailable
}
values[partIndex] = uint32(tokens[lookahead].Int)
seen[partIndex] = true
index = lookahead
break
}
if tokens[lookahead].Kind == dnfpvf.TokenString || tokens[lookahead].Kind == dnfpvf.TokenIdent {
break
}
}
sequential++
}
if err := flush(); err != nil {
return currentAvatarFittingRoomSet{}, err
}
if !markerFound || len(set.Jobs) == 0 {
return currentAvatarFittingRoomSet{}, errCurrentAvatarFittingRoomUnavailable
}
return set, nil
}
func currentAvatarFittingRoomSectionInts(document *dnfpvf.Document, name string) []int64 {
if document == nil {
return nil
}
for _, section := range document.Sections {
if !strings.EqualFold(strings.TrimSpace(section.Name), name) || section.Start < 0 || section.End > len(document.Tokens) || section.Start > section.End {
continue
}
values := make([]int64, 0, section.End-section.Start)
for _, token := range document.Tokens[section.Start:section.End] {
if token.Kind == dnfpvf.TokenInt {
values = append(values, token.Int)
}
}
return values
}
return nil
}
func normalizeCurrentAvatarFittingRoomLabel(value string) string {
value = strings.ToLower(strings.TrimSpace(strings.Trim(value, "`[]'\"")))
value = strings.TrimSuffix(value, " avatar")
return strings.Join(strings.Fields(value), " ")
}
func currentAvatarFittingRoomPartIndex(label string) int {
switch normalizeCurrentAvatarFittingRoomLabel(label) {
case "hair":
return 0
case "hat", "cap":
return 1
case "face":
return 2
case "neck", "breast":
return 3
case "coat", "top":
return 4
case "belt", "waist":
return 5
case "pants", "bottom":
return 6
case "shoes":
return 7
default:
return -1
}
}
func currentAvatarFittingRoomSetForJob(config currentAvatarFittingRoomConfig, job byte) (currentAvatarFittingRoomSet, error) {
var normal, rare []currentAvatarFittingRoomSet
for _, set := range config.Sets {
if _, ok := set.Jobs[job]; !ok {
continue
}
if set.Rare {
rare = append(rare, set)
} else {
normal = append(normal, set)
}
}
if len(normal) == 0 && len(rare) == 0 {
return currentAvatarFittingRoomSet{}, fmt.Errorf("%w: job=%d has no fitting pool", errCurrentAvatarFittingRoomUnavailable, job)
}
normalRate, rareRate := config.NormalRate, config.RareRate
if len(normal) == 0 {
normalRate = 0
}
if len(rare) == 0 {
rareRate = 0
}
total := normalRate + rareRate
roll, err := currentDisjointRandomUint64(uint64(total))
if err != nil {
return currentAvatarFittingRoomSet{}, err
}
pool := normal
if int64(roll) >= normalRate {
pool = rare
}
selected, err := currentDisjointRandomUint64(uint64(len(pool)))
if err != nil {
return currentAvatarFittingRoomSet{}, err
}
return pool[selected], nil
}