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,294 @@
package dnfbridge
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
dnfavatarcompound "longheng.io/server/internal/modules/dnf/avatarcompound"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
func (s *Service) currentAvatarCompoundOwner(session *gameSession) (string, string, dnfrepo.Group, *dnfavatarcompound.Owner, error) {
if s == nil || session == nil || session.selectedCharacterID == 0 {
return "", "", dnfrepo.Group{}, nil, errCurrentAvatarCompoundUnavailable
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Character == nil || repositories.CharacterAssets == nil {
return "", "", dnfrepo.Group{}, nil, errCurrentAvatarCompoundUnavailable
}
accountID := strings.TrimSpace(s.accountIDForSession(session))
if accountID == "" {
return "", "", dnfrepo.Group{}, nil, errCurrentAvatarCompoundUnavailable
}
owner, err := dnfavatarcompound.NewOwner(repositories)
if err != nil {
return "", "", dnfrepo.Group{}, nil, errors.Join(errCurrentAvatarCompoundUnavailable, err)
}
return accountID, strconv.FormatUint(uint64(session.selectedCharacterID), 10), repositories, owner, nil
}
// currentAvatarCompoundCubeBonus resolves the rare-rate bonus of the consumed
// 装扮合成器 (the "bind cube"). Three sources, in order:
//
// 1. the canonical three-tier tail of the job file — 21 装扮合成器 (ordinary),
// 899 黄金装扮合成器 (upper), 490702265 钻石装扮合成器 (master); their bonus
// comes from etc/serverparameter.etc;
// 2. a [common bind cube] block's own [material] (升级的远古装扮合成器
// 10301984/10302329 and the other 90-level families). Those blocks carry no
// bonus field, so they price at the ordinary tier and are told apart by the
// block's own [output] pool;
// 3. the event re-issues of the same cubes (钻石装扮合成器 10007495/10007718/
// 10007901/10008013/10008512, 追忆之装扮合成器 2683630, …). These are NOT in
// the job file at all, so the only PVF evidence left is the item name: it
// must end with 装扮合成器 and must not be one of the 梦想/梦幻 absolute
// cubes (those are the `[action type] [absolute bind cube]` items and use the
// separate op1119 flow).
//
// Live 2026-10-04: only source 1 was implemented, so every 远古/追忆/钻石活动版
// cube was answered op99 {0,0x16} and the client reported the cube as unusable.
func currentAvatarCompoundCubeBonus(
config currentAvatarCompoundConfig,
catalog *pvfDungeonDropCatalog,
itemID uint32,
) (int64, bool) {
if bonus, ok := config.MaterialBonuses[itemID]; ok {
return bonus, true
}
if config.BlockMaterials[itemID] {
return 0, true
}
if catalog == nil || itemID == 0 {
return 0, false
}
definition, err := catalog.ResolveItem(itemID)
if err != nil || definition.Kind != dungeonDropItemStackable {
return 0, false
}
name := strings.TrimSpace(definition.Name)
if !strings.HasSuffix(name, "装扮合成器") || strings.Contains(name, "梦想") || strings.Contains(name, "梦幻") {
return 0, false
}
switch {
case strings.Contains(name, "钻石"):
return config.MasterBonus, true
case strings.Contains(name, "黄金"), strings.Contains(name, "追忆"):
// 追忆之装扮合成器的说明写明"出现稀有装扮的几率比普通装扮合成器更高",
// 与黄金同档;钻石再高一档(其说明:"比黄金装扮合成器更高")。
return config.UpperBonus, true
default:
return 0, true
}
}
func currentAvatarCompoundOwnerError(err error) error {
switch {
case errors.Is(err, dnfavatarcompound.ErrOwnerUnavailable),
errors.Is(err, dnfavatarcompound.ErrAccountRequired),
errors.Is(err, dnfavatarcompound.ErrCharacterRequired),
errors.Is(err, dnfavatarcompound.ErrCharacterNotFound),
errors.Is(err, dnfavatarcompound.ErrAccountMismatch),
errors.Is(err, dnfavatarcompound.ErrInventoryNotFound):
return errors.Join(errCurrentAvatarCompoundUnavailable, err)
default:
return err
}
}
func (s *Service) commitCurrentAvatarCompound(
ctx context.Context,
session *gameSession,
catalog *pvfDungeonDropCatalog,
request currentAvatarCompoundRequest,
) (currentAvatarCompoundResult, error) {
if catalog == nil || catalog.source == nil {
return currentAvatarCompoundResult{}, errCurrentAvatarCompoundUnavailable
}
accountID, characterID, repositories, owner, err := s.currentAvatarCompoundOwner(session)
if err != nil {
return currentAvatarCompoundResult{}, err
}
character, found, err := repositories.Character.Load(ctx, characterID)
if err != nil || !found || strings.TrimSpace(character.AccountID) != accountID {
return currentAvatarCompoundResult{}, errors.Join(errCurrentAvatarCompoundUnavailable, err)
}
jobValue := numericCharacterStat(character.Job)
if jobValue < 0 || jobValue > math.MaxUint8 {
return currentAvatarCompoundResult{}, fmt.Errorf("%w: character job=%q", errCurrentAvatarCompoundUnavailable, character.Job)
}
config, err := loadCurrentAvatarCompoundConfig(catalog.source, byte(jobValue))
if err != nil {
return currentAvatarCompoundResult{}, err
}
partName, found := currentAvatarCompoundRequestedPart(config, request.RequestedItemID)
if !found {
return currentAvatarCompoundResult{}, fmt.Errorf("%w: requested item=%d config=%s", errCurrentAvatarCompoundOutputInvalid, request.RequestedItemID, config.PVFPath)
}
requestedDefinition, requestedDocument, err := currentAvatarCompoundDefinition(catalog, request.RequestedItemID)
if err != nil {
return currentAvatarCompoundResult{}, err
}
requestedPart, ok := currentAvatarCompoundDocumentPart(requestedDocument)
if !ok || requestedPart != partName {
return currentAvatarCompoundResult{}, fmt.Errorf("%w: requested item=%d part=%q config_part=%q", errCurrentAvatarCompoundOutputInvalid, request.RequestedItemID, requestedPart, partName)
}
source1Definition, source1Document, err := currentAvatarCompoundDefinition(catalog, request.ExpectedItemID1)
if err != nil {
return currentAvatarCompoundResult{}, errCurrentAvatarCompoundSourceInvalid
}
source2Definition, source2Document, err := currentAvatarCompoundDefinition(catalog, request.ExpectedItemID2)
if err != nil {
return currentAvatarCompoundResult{}, errCurrentAvatarCompoundSourceInvalid
}
part1, source1PartOK := currentAvatarCompoundDocumentPart(source1Document)
part2, source2PartOK := currentAvatarCompoundDocumentPart(source2Document)
if !source1PartOK || !source2PartOK || part1 != partName || part2 != partName {
return currentAvatarCompoundResult{}, fmt.Errorf("%w: requested_part=%s source_parts=%s,%s", errCurrentAvatarCompoundSourceInvalid, partName, part1, part2)
}
var result currentAvatarCompoundResult
err = owner.Compound(ctx, dnfavatarcompound.Command{
AccountID: accountID,
CharacterID: characterID,
Project: func(assets *dnfavatarcompound.Assets) error {
if assets == nil || assets.Character == nil || assets.Inventory == nil || assets.Inventory.Slots == nil ||
numericCharacterStat(assets.Character.Job) != jobValue {
return errCurrentAvatarCompoundUnavailable
}
inventory := assets.Inventory
sourceKey1 := currentCeraShopInventorySlotKey(currentAvatarInventoryListType, request.Slot1)
sourceKey2 := currentCeraShopInventorySlotKey(currentAvatarInventoryListType, request.Slot2)
sourceStack1, sourceFound1 := inventory.Slots[sourceKey1]
sourceStack2, sourceFound2 := inventory.Slots[sourceKey2]
if !currentAvatarCompoundSourceStackValid(sourceStack1, sourceFound1, request.ExpectedItemID1) ||
!currentAvatarCompoundSourceStackValid(sourceStack2, sourceFound2, request.ExpectedItemID2) {
return errCurrentAvatarCompoundSourceInvalid
}
materialKey := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, request.ConsumeSlot)
materialStack, materialFound := inventory.Slots[materialKey]
if !materialFound || materialStack.ItemID <= 0 || materialStack.ItemID > math.MaxUint32 || materialStack.Count <= 0 || currentNPCShopItemLocked(materialStack) {
return errCurrentAvatarCompoundMaterial
}
materialItemID := uint32(materialStack.ItemID)
bonus, accepted := currentAvatarCompoundCubeBonus(config, catalog, materialItemID)
if !accepted {
return fmt.Errorf("%w: item=%d", errCurrentAvatarCompoundMaterial, materialItemID)
}
config = config.withCubeBonus(materialItemID, bonus)
materialDefinition, resolveErr := catalog.ResolveItem(materialItemID)
if resolveErr != nil || materialDefinition.Kind != dungeonDropItemStackable {
return errCurrentAvatarCompoundMaterial
}
rate, rateErr := currentAvatarCompoundRareRate(config, partName, source1Definition.Grade, source2Definition.Grade, materialItemID)
if rateErr != nil {
return rateErr
}
roll, rollErr := currentDisjointRandomUint64(10000)
if rollErr != nil {
return rollErr
}
newItemIDs := []uint32{request.RequestedItemID}
rareHit := roll < uint64(rate)
if rareHit {
rareItemID, rareErr := rollCurrentAvatarCompoundRare(config.Parts[partName].Rare)
if rareErr != nil {
return rareErr
}
_, rareDocument, rareErr := currentAvatarCompoundDefinition(catalog, rareItemID)
rarePart, rarePartOK := currentAvatarCompoundDocumentPart(rareDocument)
if rareErr != nil || !rarePartOK || rarePart != partName {
return fmt.Errorf("%w: rare item=%d part=%s", errCurrentAvatarCompoundOutputInvalid, rareItemID, rarePart)
}
newItemIDs = append(newItemIDs, rareItemID)
}
delete(inventory.Slots, sourceKey1)
delete(inventory.Slots, sourceKey2)
materialStack.Count--
materialRemain := materialStack.Count
if materialStack.Count == 0 {
delete(inventory.Slots, materialKey)
} else {
entry := currentItemListEntryFromStack(dnfrepo.MainInventoryListType, request.ConsumeSlot, materialStack)
materialStack.RawEntry = append([]byte(nil), entry.data[:]...)
inventory.Slots[materialKey] = materialStack
}
newSlots := []int16{request.Slot1, request.Slot2}
sort.Slice(newSlots, func(left, right int) bool { return newSlots[left] < newSlots[right] })
newSlots = newSlots[:len(newItemIDs)]
for index, itemID := range newItemIDs {
definition := requestedDefinition
if itemID != request.RequestedItemID {
definition, _, resolveErr = currentAvatarCompoundDefinition(catalog, itemID)
if resolveErr != nil {
return resolveErr
}
}
slot := newSlots[index]
stack := currentAvatarCompoundOutputStack(definition, request.AbilityNo)
entry := currentItemListEntryFromStack(currentAvatarInventoryListType, slot, stack)
stack.RawEntry = append([]byte(nil), entry.data[:]...)
inventory.Slots[currentCeraShopInventorySlotKey(currentAvatarInventoryListType, slot)] = stack
}
result = currentAvatarCompoundResult{
MaterialItemID: materialItemID,
MaterialRemain: materialRemain,
SourceItemIDs: [2]uint32{request.ExpectedItemID1, request.ExpectedItemID2},
NewSlots: append([]int16(nil), newSlots...),
NewItemIDs: append([]uint32(nil), newItemIDs...),
RareRate: rate,
RareRoll: roll,
RareHit: rareHit,
}
return nil
},
})
if err != nil {
return currentAvatarCompoundResult{}, currentAvatarCompoundOwnerError(err)
}
return result, nil
}
func currentAvatarCompoundSourceStackValid(stack dnfrepo.ItemStack, found bool, expectedItemID uint32) bool {
return found && stack.ItemID == int64(expectedItemID) && (stack.Count == 0 || stack.Count == 1) && !currentNPCShopItemLocked(stack)
}
func currentAvatarCompoundDefinition(catalog *pvfDungeonDropCatalog, itemID uint32) (dungeonDropItemDefinition, *dnfpvf.Document, error) {
definition, err := catalog.ResolveItem(itemID)
if err != nil || definition.Kind != dungeonDropItemEquipment ||
(!definition.ExpirationDate.IsZero() && !time.Now().UTC().Before(definition.ExpirationDate)) {
return dungeonDropItemDefinition{}, nil, errors.Join(errCurrentAvatarCompoundOutputInvalid, err)
}
document, err := parseDungeonCardPVFDocument(catalog.source, definition.PVFPath)
if err != nil || !currentDisjointDocumentIsAvatar(document) {
return dungeonDropItemDefinition{}, nil, errors.Join(errCurrentAvatarCompoundOutputInvalid, err)
}
return definition, document, nil
}
func currentAvatarCompoundOutputStack(definition dungeonDropItemDefinition, abilityNo uint16) dnfrepo.ItemStack {
extra := map[string]string{
"source": "avatar_compound",
"item_kind": "avatar",
"pvf_path": definition.PVFPath,
"equipment_type": definition.EquipmentType,
"avatar_ability_no": strconv.FormatUint(uint64(abilityNo), 10),
"durability": strconv.FormatUint(uint64(abilityNo), 10),
"amount_or_count": "0",
}
stack := dnfrepo.ItemStack{ItemID: int64(definition.ItemID), Count: 1, Extra: extra}
if !definition.ExpirationDate.IsZero() {
stack, _ = applyCurrentPVFItemExpirationAt(stack, definition, time.Now().UTC())
}
return stack
}