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,248 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user