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,412 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GMAvatarSetQuery is deliberately job-scoped. A package that contains
|
||||
// several profession variants must never project another profession's avatar
|
||||
// items into the dressing-room selection.
|
||||
type GMAvatarSetQuery struct {
|
||||
Query string
|
||||
Job string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type GMAvatarSetItem struct {
|
||||
Slot string `json:"slot"`
|
||||
Item GMItemSummary `json:"item"`
|
||||
}
|
||||
|
||||
type GMAvatarSetSummary struct {
|
||||
Package GMItemSummary `json:"package"`
|
||||
Items []GMAvatarSetItem `json:"items"`
|
||||
}
|
||||
|
||||
type GMAvatarSetPage struct {
|
||||
Sets []GMAvatarSetSummary `json:"sets"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
// GMSearchAvatarSets resolves real package, booster-selection and deterministic
|
||||
// booster relationships from the active PVF. Only definitions that resolve at
|
||||
// least three distinct avatar slots for the requested profession are returned.
|
||||
// Ambiguous alternatives for the same slot remain excluded because an
|
||||
// automatic "whole set" choice would otherwise invent the player's style.
|
||||
func (s *Service) GMSearchAvatarSets(ctx context.Context, query GMAvatarSetQuery) (GMAvatarSetPage, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if query.Limit <= 0 {
|
||||
query.Limit = 24
|
||||
}
|
||||
if query.Limit > 100 || query.Offset < 0 {
|
||||
return GMAvatarSetPage{}, ErrGMInvalidRequest
|
||||
}
|
||||
job := gmItemTypeTag(query.Job)
|
||||
if job == "" || job == "all" {
|
||||
return GMAvatarSetPage{}, ErrGMInvalidRequest
|
||||
}
|
||||
catalog, err := s.currentPVFItemCatalog()
|
||||
if err != nil {
|
||||
return GMAvatarSetPage{}, err
|
||||
}
|
||||
sets, err := gmAvatarSetsForJob(ctx, catalog, job)
|
||||
if err != nil {
|
||||
return GMAvatarSetPage{}, err
|
||||
}
|
||||
needle := strings.ToLower(strings.TrimSpace(query.Query))
|
||||
matches := make([]GMAvatarSetSummary, 0, query.Limit)
|
||||
total := 0
|
||||
for _, summary := range sets {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return GMAvatarSetPage{}, err
|
||||
}
|
||||
packageItem := summary.Package
|
||||
if needle != "" && !strings.Contains(strings.ToLower(packageItem.Name), needle) &&
|
||||
!strings.Contains(strings.ToLower(packageItem.PVFPath), needle) &&
|
||||
strconv.FormatUint(uint64(packageItem.ItemID), 10) != needle {
|
||||
continue
|
||||
}
|
||||
if total >= query.Offset && len(matches) < query.Limit {
|
||||
matches = append(matches, summary)
|
||||
}
|
||||
total++
|
||||
}
|
||||
return GMAvatarSetPage{Sets: matches, Total: total, Limit: query.Limit, Offset: query.Offset}, nil
|
||||
}
|
||||
|
||||
func gmAvatarSetsForJob(ctx context.Context, catalog *pvfDungeonDropCatalog, job string) ([]GMAvatarSetSummary, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
catalog.gmAvatarIndexMu.Lock()
|
||||
defer catalog.gmAvatarIndexMu.Unlock()
|
||||
if catalog.gmAvatarSetsByJob == nil {
|
||||
catalog.gmAvatarSetsByJob = make(map[string][]GMAvatarSetSummary)
|
||||
}
|
||||
if cached, ok := catalog.gmAvatarSetsByJob[job]; ok {
|
||||
return cached, nil
|
||||
}
|
||||
packageIDs := gmAvatarSetPackageIDs(ctx, catalog)
|
||||
sets := make([]GMAvatarSetSummary, 0, 512)
|
||||
for _, packageID := range packageIDs {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definition, resolveErr := catalog.ResolveItem(packageID)
|
||||
if resolveErr != nil {
|
||||
continue
|
||||
}
|
||||
effectiveJobs := gmItemEffectiveJobs(catalog, definition)
|
||||
if len(effectiveJobs) > 0 && !gmJobTagsContain(effectiveJobs, job) {
|
||||
continue
|
||||
}
|
||||
sets = append(sets, gmAvatarSetSummaries(catalog, definition, job, gmItemSummary(definition))...)
|
||||
}
|
||||
catalog.gmAvatarSetsByJob[job] = sets
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
func gmAvatarSetSummaries(
|
||||
catalog *pvfDungeonDropCatalog,
|
||||
definition dungeonDropItemDefinition,
|
||||
job string,
|
||||
packageItem GMItemSummary,
|
||||
) []GMAvatarSetSummary {
|
||||
items, deterministic := gmAvatarSetItems(catalog, definition, job)
|
||||
if deterministic && len(items) >= 3 {
|
||||
return []GMAvatarSetSummary{{Package: packageItem, Items: items}}
|
||||
}
|
||||
selection := definition
|
||||
if normalizeDungeonDropStackableType(definition.StackableType) == "[cera booster]" {
|
||||
resolution, err := resolveCurrentMagicBox(catalog, catalog.source, int64(definition.ItemID))
|
||||
if err != nil || resolution.Kind != "random" || len(resolution.Groups) != 1 || len(resolution.Groups[0].Entries) != 1 {
|
||||
return nil
|
||||
}
|
||||
rewardID := resolution.Groups[0].Entries[0].ItemID
|
||||
if rewardID <= 0 || rewardID > int64(^uint32(0)) {
|
||||
return nil
|
||||
}
|
||||
selection, err = catalog.ResolveItem(uint32(rewardID))
|
||||
if err != nil || normalizeDungeonDropStackableType(selection.StackableType) != "[booster selection]" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if normalizeDungeonDropStackableType(selection.StackableType) != "[booster selection]" {
|
||||
return nil
|
||||
}
|
||||
variants := gmAvatarSetChoiceVariants(catalog, selection, job)
|
||||
summaries := make([]GMAvatarSetSummary, 0, len(variants))
|
||||
for index, variant := range variants {
|
||||
variantPackage := packageItem
|
||||
variantPackage.ItemID = variant.source.ItemID
|
||||
variantPackage.PVFPath = variant.source.PVFPath
|
||||
variantPackage.Name = packageItem.Name + " · 款式" + strconv.Itoa(index+1)
|
||||
summaries = append(summaries, GMAvatarSetSummary{Package: variantPackage, Items: variant.items})
|
||||
}
|
||||
return summaries
|
||||
}
|
||||
|
||||
type gmAvatarSetVariant struct {
|
||||
source dungeonDropItemDefinition
|
||||
items []GMAvatarSetItem
|
||||
}
|
||||
|
||||
func gmAvatarSetChoiceVariants(catalog *pvfDungeonDropCatalog, selection dungeonDropItemDefinition, job string) []gmAvatarSetVariant {
|
||||
booster, err := resolveCurrentBoosterDefinition(catalog, selection.ItemID, currentBoosterRequestSelection)
|
||||
if err != nil || booster.SelectionRequired != 1 {
|
||||
return nil
|
||||
}
|
||||
categoryIDs := make([]int, 0, len(booster.SelectionCategory))
|
||||
for categoryID := range booster.SelectionCategory {
|
||||
categoryIDs = append(categoryIDs, int(categoryID))
|
||||
}
|
||||
sort.Ints(categoryIDs)
|
||||
variants := make([]gmAvatarSetVariant, 0, 2)
|
||||
seen := make(map[string]struct{})
|
||||
for _, rawCategoryID := range categoryIDs {
|
||||
for _, candidate := range booster.SelectionCategory[uint16(rawCategoryID)] {
|
||||
reward, resolveErr := catalog.ResolveItem(candidate.ItemID)
|
||||
if resolveErr != nil {
|
||||
continue
|
||||
}
|
||||
items, deterministic := gmAvatarSetItems(catalog, reward, job)
|
||||
if !deterministic || len(items) < 3 {
|
||||
continue
|
||||
}
|
||||
signature := gmAvatarSetSignature(items)
|
||||
if _, duplicate := seen[signature]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[signature] = struct{}{}
|
||||
variants = append(variants, gmAvatarSetVariant{source: reward, items: items})
|
||||
}
|
||||
}
|
||||
return variants
|
||||
}
|
||||
|
||||
func gmAvatarSetPackageIDs(ctx context.Context, catalog *pvfDungeonDropCatalog) []uint32 {
|
||||
catalog.gmAvatarSetMu.Lock()
|
||||
defer catalog.gmAvatarSetMu.Unlock()
|
||||
if catalog.gmAvatarSetReady {
|
||||
return append([]uint32(nil), catalog.gmAvatarSetIDs...)
|
||||
}
|
||||
ids := make([]uint32, 0, 1024)
|
||||
for _, itemID := range catalog.stackableIDs {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
definition, err := catalog.ResolveItem(itemID)
|
||||
stackableType := normalizeDungeonDropStackableType(definition.StackableType)
|
||||
if err == nil && (len(definition.PackageRewardIDs) > 0 ||
|
||||
stackableType == "[booster selection]" || stackableType == "[cera booster]") {
|
||||
ids = append(ids, itemID)
|
||||
}
|
||||
}
|
||||
sort.Slice(ids, func(left, right int) bool { return ids[left] < ids[right] })
|
||||
catalog.gmAvatarSetIDs = append([]uint32(nil), ids...)
|
||||
catalog.gmAvatarSetReady = ctx.Err() == nil
|
||||
return ids
|
||||
}
|
||||
|
||||
func gmAvatarSetItems(catalog *pvfDungeonDropCatalog, root dungeonDropItemDefinition, job string) ([]GMAvatarSetItem, bool) {
|
||||
visited := make(map[uint32]struct{})
|
||||
items, deterministic := gmResolveAvatarSetItems(catalog, root, job, visited, 0)
|
||||
if !deterministic {
|
||||
return nil, false
|
||||
}
|
||||
return gmNormalizeAvatarSetItems(items)
|
||||
}
|
||||
|
||||
func gmNormalizeAvatarSetItems(rows []GMAvatarSetItem) ([]GMAvatarSetItem, bool) {
|
||||
bySlot := make(map[string]GMAvatarSetItem)
|
||||
for _, row := range rows {
|
||||
if row.Item.UsablePeriodDays > 0 || !row.Item.ExpirationDate.IsZero() {
|
||||
return nil, false
|
||||
}
|
||||
if previous, duplicate := bySlot[row.Slot]; duplicate && previous.Item.ItemID != row.Item.ItemID {
|
||||
return nil, false
|
||||
}
|
||||
bySlot[row.Slot] = row
|
||||
}
|
||||
items := make([]GMAvatarSetItem, 0, len(bySlot))
|
||||
for _, row := range bySlot {
|
||||
items = append(items, row)
|
||||
}
|
||||
sort.Slice(items, func(left, right int) bool {
|
||||
leftOrder, _ := gmAvatarSlot(items[left].Item.EquipmentType)
|
||||
rightOrder, _ := gmAvatarSlot(items[right].Item.EquipmentType)
|
||||
if leftOrder != rightOrder {
|
||||
return leftOrder < rightOrder
|
||||
}
|
||||
return items[left].Item.ItemID < items[right].Item.ItemID
|
||||
})
|
||||
return items, true
|
||||
}
|
||||
|
||||
func gmResolveAvatarSetItems(
|
||||
catalog *pvfDungeonDropCatalog,
|
||||
definition dungeonDropItemDefinition,
|
||||
job string,
|
||||
visited map[uint32]struct{},
|
||||
depth int,
|
||||
) ([]GMAvatarSetItem, bool) {
|
||||
if depth >= 8 {
|
||||
return nil, false
|
||||
}
|
||||
if definition.Kind == dungeonDropItemEquipment {
|
||||
item := gmItemSummary(definition)
|
||||
_, slot := gmAvatarSlot(item.EquipmentType)
|
||||
if slot != "" && gmItemMatchesJob(item, job) {
|
||||
return []GMAvatarSetItem{{Slot: slot, Item: item}}, true
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
if definition.Kind != dungeonDropItemStackable {
|
||||
return nil, true
|
||||
}
|
||||
if _, exists := visited[definition.ItemID]; exists {
|
||||
return nil, false
|
||||
}
|
||||
visited[definition.ItemID] = struct{}{}
|
||||
defer delete(visited, definition.ItemID)
|
||||
|
||||
if len(definition.PackageRewardIDs) > 0 {
|
||||
items := make([]GMAvatarSetItem, 0, len(definition.PackageRewardIDs))
|
||||
for _, rewardID := range definition.PackageRewardIDs {
|
||||
reward, err := catalog.ResolveItem(rewardID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rows, deterministic := gmResolveAvatarSetItems(catalog, reward, job, visited, depth+1)
|
||||
if !deterministic {
|
||||
return nil, false
|
||||
}
|
||||
items = append(items, rows...)
|
||||
}
|
||||
return items, true
|
||||
}
|
||||
|
||||
switch normalizeDungeonDropStackableType(definition.StackableType) {
|
||||
case "[booster selection]":
|
||||
booster, err := resolveCurrentBoosterDefinition(catalog, definition.ItemID, currentBoosterRequestSelection)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
categoryIDs := make([]int, 0, len(booster.SelectionCategory))
|
||||
for categoryID := range booster.SelectionCategory {
|
||||
categoryIDs = append(categoryIDs, int(categoryID))
|
||||
}
|
||||
sort.Ints(categoryIDs)
|
||||
var selected []GMAvatarSetItem
|
||||
selectedSignature := ""
|
||||
for _, rawCategoryID := range categoryIDs {
|
||||
category := booster.SelectionCategory[uint16(rawCategoryID)]
|
||||
if booster.SelectionRequired > 0 && booster.SelectionRequired != len(category) {
|
||||
continue
|
||||
}
|
||||
rows := make([]GMAvatarSetItem, 0, len(category))
|
||||
deterministic := true
|
||||
for _, candidate := range category {
|
||||
reward, resolveErr := catalog.ResolveItem(candidate.ItemID)
|
||||
if resolveErr != nil {
|
||||
continue
|
||||
}
|
||||
candidateRows, candidateDeterministic := gmResolveAvatarSetItems(catalog, reward, job, visited, depth+1)
|
||||
if !candidateDeterministic {
|
||||
deterministic = false
|
||||
break
|
||||
}
|
||||
rows = append(rows, candidateRows...)
|
||||
}
|
||||
rows, deterministic = gmNormalizeAvatarSetItems(rows)
|
||||
if !deterministic || len(rows) < 3 {
|
||||
continue
|
||||
}
|
||||
signature := gmAvatarSetSignature(rows)
|
||||
if selectedSignature == "" {
|
||||
selected, selectedSignature = rows, signature
|
||||
continue
|
||||
}
|
||||
if signature != selectedSignature {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if selectedSignature == "" {
|
||||
return nil, true
|
||||
}
|
||||
return selected, true
|
||||
|
||||
case "[cera booster]":
|
||||
resolution, err := resolveCurrentMagicBox(catalog, catalog.source, int64(definition.ItemID))
|
||||
if err != nil || resolution.Kind != "random" {
|
||||
return nil, false
|
||||
}
|
||||
items := make([]GMAvatarSetItem, 0, len(resolution.Groups))
|
||||
for _, group := range resolution.Groups {
|
||||
if len(group.Entries) != 1 {
|
||||
return nil, false
|
||||
}
|
||||
rewardID := group.Entries[0].ItemID
|
||||
if rewardID <= 0 || rewardID > int64(^uint32(0)) {
|
||||
return nil, false
|
||||
}
|
||||
reward, resolveErr := catalog.ResolveItem(uint32(rewardID))
|
||||
if resolveErr != nil {
|
||||
return nil, false
|
||||
}
|
||||
rows, deterministic := gmResolveAvatarSetItems(catalog, reward, job, visited, depth+1)
|
||||
if !deterministic {
|
||||
return nil, false
|
||||
}
|
||||
items = append(items, rows...)
|
||||
}
|
||||
return items, true
|
||||
default:
|
||||
return nil, true
|
||||
}
|
||||
}
|
||||
|
||||
func gmAvatarSetSignature(items []GMAvatarSetItem) string {
|
||||
parts := make([]string, 0, len(items))
|
||||
for _, row := range items {
|
||||
parts = append(parts, row.Slot+":"+strconv.FormatUint(uint64(row.Item.ItemID), 10))
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
func gmAvatarSlot(equipmentType string) (int, string) {
|
||||
switch gmItemTypeTag(equipmentType) {
|
||||
case "hair avatar":
|
||||
return 0, "hair"
|
||||
case "hat avatar":
|
||||
return 1, "hat"
|
||||
case "face avatar":
|
||||
return 2, "face"
|
||||
case "breast avatar":
|
||||
return 3, "breast"
|
||||
case "coat avatar":
|
||||
return 4, "coat"
|
||||
case "waist avatar":
|
||||
return 5, "waist"
|
||||
case "pants avatar":
|
||||
return 6, "pants"
|
||||
case "shoes avatar":
|
||||
return 7, "shoes"
|
||||
case "skin avatar":
|
||||
return 8, "skin"
|
||||
case "aurora avatar":
|
||||
return 9, "aurora"
|
||||
case "weapon avatar":
|
||||
return 10, "weapon"
|
||||
default:
|
||||
return 100, ""
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user