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,615 @@
package dnfbridge
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
"strconv"
"strings"
)
var (
errDungeonDropObjectKeyRange = errors.New("dnf dungeon drop object key range exhausted")
errDungeonDropSceneSlotRange = errors.New("dnf dungeon drop scene slot range exhausted")
errDungeonDropObjectConflict = errors.New("dnf dungeon drop object key already exists")
errDungeonDropNotFound = errors.New("dnf dungeon drop object was not found")
errDungeonDropRoomMismatch = errors.New("dnf dungeon drop belongs to another room")
errDungeonDropActorMismatch = errors.New("dnf dungeon drop belongs to another actor")
errDungeonDropCoinBranchUnproved = errors.New("dnf dungeon coin/gold pickup branch is not proved")
)
type runtimeDungeonDropStatus byte
const (
runtimeDungeonDropAvailable runtimeDungeonDropStatus = iota + 1
runtimeDungeonDropPending
runtimeDungeonDropConsumed
)
type runtimeDungeonDrop struct {
ObjectKey uint32
SceneSlot uint16
Item dungeonDropItemDefinition
Amount uint32
QualitySeed uint32
DiscardOrigin *runtimeDungeonDiscardOrigin
OwnerActorObjectKey uint16
Room runtimeDungeonRoomKey
Status runtimeDungeonDropStatus
PickupAttempt uint64
DestinationSlot uint16
GoldAfter int64
PickupResponseBody []byte
PickupItemUpdateBody []byte
PickupEpicBookClearBody []byte
PickupEpicBookBody []byte
}
func (drop *runtimeDungeonDrop) isGold() bool {
return drop != nil && drop.Item.ItemID == 0
}
type runtimeDungeonDropOwner struct {
nextSceneSlot uint16
byObjectKey map[uint32]*runtimeDungeonDrop
}
func newRuntimeDungeonDropOwner() *runtimeDungeonDropOwner {
return &runtimeDungeonDropOwner{
nextSceneSlot: 1,
byObjectKey: make(map[uint32]*runtimeDungeonDrop),
}
}
func (owner *runtimeDungeonDropOwner) registerBatch(drops []*runtimeDungeonDrop, nextSceneSlot uint16) error {
if owner == nil {
return errDungeonDropNotFound
}
if owner.byObjectKey == nil {
owner.byObjectKey = make(map[uint32]*runtimeDungeonDrop)
}
for _, drop := range drops {
if drop == nil || drop.ObjectKey == 0 {
return errDungeonDropNotFound
}
if _, exists := owner.byObjectKey[drop.ObjectKey]; exists {
return fmt.Errorf("%w: object_key=%d", errDungeonDropObjectConflict, drop.ObjectKey)
}
}
for _, drop := range drops {
owner.byObjectKey[drop.ObjectKey] = drop
}
owner.nextSceneSlot = nextSceneSlot
return nil
}
// nextObjectKeyAtOrAbove keeps the room actor/drop namespace monotonic even
// when a room transition was planned from a stale actor high-water mark. Drop
// receipts intentionally survive room changes for pickup/idempotency checks,
// so reusing one of their keys makes the next reward batch fail atomically.
func (owner *runtimeDungeonDropOwner) nextObjectKeyAtOrAbove(next uint32) uint32 {
if owner == nil {
return next
}
for objectKey := range owner.byObjectKey {
if objectKey >= next {
if objectKey == math.MaxUint32 {
return objectKey
}
next = objectKey + 1
}
}
return next
}
func (owner *runtimeDungeonDropOwner) owned(
objectKey uint32,
room runtimeDungeonRoomKey,
actorObjectKey uint16,
) (*runtimeDungeonDrop, error) {
if owner == nil || objectKey == 0 {
return nil, errDungeonDropNotFound
}
drop := owner.byObjectKey[objectKey]
if drop == nil {
return nil, fmt.Errorf("%w: object_key=%d", errDungeonDropNotFound, objectKey)
}
if drop.Room != room {
return nil, fmt.Errorf(
"%w: object_key=%d owned=%+v current=%+v",
errDungeonDropRoomMismatch,
objectKey,
drop.Room,
room,
)
}
if drop.OwnerActorObjectKey != actorObjectKey {
return nil, fmt.Errorf(
"%w: object_key=%d owned=%d current=%d",
errDungeonDropActorMismatch,
objectKey,
drop.OwnerActorObjectKey,
actorObjectKey,
)
}
return drop, nil
}
func (s *Service) planCurrentDungeonMonsterDrops(
runtime *runtimeDungeonState,
monster runtimeDungeonMonster,
ownerActorObjectKey uint16,
) ([]currentDungeonDeathDropWire, error) {
// A raid gate drops nothing at all -- no monster drops, no regional materials,
// no gold. Its reward is the phase settlement, so a farming drop inside the gate
// would hand the raid's reward out twice. The 86JP reference reaches the same
// conclusion from its own dungeon policy: DungeonRewardPolicy.AntonRaid keeps
// monster EXP, quest progress and settlement while switching monster drops and
// quest drops off.
if currentDungeonRuntimeIsPractice(runtime) || s.currentDungeonRuntimeIsRaidGate(runtime) {
return nil, nil
}
if runtime == nil || runtime.Session == nil || runtime.Room == nil {
return nil, errDungeonWorldMapUnavailable
}
if ownerActorObjectKey == 0 {
return nil, errDungeonDropActorMismatch
}
scene, ok := runtime.Session.Scene()
if !ok {
return nil, errDungeonWorldMapUnavailable
}
roomKey := runtimeDungeonRoomKeyFromScene(scene)
monsterCatalog, err := s.dungeonMonsterCatalog()
if err != nil {
return nil, err
}
rules, err := currentDungeonMonsterDropRules(monsterCatalog)
if err != nil {
return nil, err
}
dropCatalog, err := monsterCatalog.DropCatalog()
if err != nil {
return nil, err
}
fallbacksBefore := dropCatalog.GenericFallbackMarks()
pool, err := dropCatalog.MonsterPool(monster.Spawn.MonsterID)
if err != nil {
return nil, err
}
areaMaterialEntry, areaMaterialMatched := currentDungeonAreaMaterialPoolEntry(runtime.Dungeon.ID)
if areaMaterialMatched {
// Regional materials are an extension of the monster-template weighted
// pool, not a generic creation-rate item and not an independent drop.
// Resolve the mapped item up front so a mismatched PVF fails closed.
if _, err := dropCatalog.ResolveItem(areaMaterialEntry.ItemID); err != nil {
return nil, err
}
pool = append(pool, areaMaterialEntry)
}
visit, err := runtime.currentDungeonRoomVisit(scene)
if err != nil {
return nil, err
}
monsterLevel, err := currentDungeonMonsterLevel(monster.Spawn, runtime)
if err != nil {
return nil, err
}
monsterType, err := currentDungeonMonsterType(monster.Spawn.Rank)
if err != nil {
return nil, err
}
rng := newCurrentDungeonDropLCG(visit.DropRNG)
probability, probabilityFound := rules.Probability(int64(monsterLevel))
itemReference, itemReferenceFound := rules.ItemReference(int64(monsterLevel))
difficultyRate := currentDungeonDropDifficultyRate(int(runtime.Request.Difficulty))
raw := rules.RawTables()
type plannedDrop struct {
itemID uint32
amount uint32
gold bool
}
selectedDrops := make([]plannedDrop, 0, 8)
// 86JP computes gold before the ordinary item-rate rolls, and gold is a
// normal scene drop with template id 0. Older Go intentionally consumed the
// roll but threw the object away because the coin pickup branch was still
// being isolated; that made live dungeons never show金币.
if probabilityFound {
goldReferences, goldErr := currentDungeonGoldReferences(monsterCatalog)
if goldErr != nil {
return nil, goldErr
}
goldReference, goldFound := goldReferences[int64(monsterLevel)]
if !goldFound {
return nil, fmt.Errorf("%w: monster_level=%d", errCurrentDungeonGoldReferenceInvalid, monsterLevel)
}
goldAmount := currentDungeonGoldAmount(rng, goldReference, difficultyRate)
goldRate := currentDungeonDropRate(probability.Rates[0], raw.MonsterTypeBonus[0][monsterType], difficultyRate)
goldRoll := rng.Next(currentDungeonDropDenominator)
goldSelected := goldRate > goldRoll
s.logGameEvent(nil, "game-dungeon-monster-drop-gold-roll",
"dungeon_id", runtime.Dungeon.ID,
"maze_index", runtime.MazeIndex,
"room", scene.Coordinate.String(),
"map_id", scene.Map.Map.ID,
"monster_id", monster.Spawn.MonsterID,
"monster_object_key", monster.ObjectKey,
"monster_level", monsterLevel,
"monster_type", monsterType,
"base_rate", probability.Rates[0],
"monster_type_bonus", raw.MonsterTypeBonus[0][monsterType],
"difficulty_bonus", difficultyRate,
"gold_rate", goldRate,
"gold_roll", goldRoll,
"gold_amount", goldAmount,
"selected", goldSelected,
"formula_source", "86jp_monster_gold_branch_itemdropinfo_common_and_monseter")
if goldSelected {
selectedDrops = append(selectedDrops, plannedDrop{amount: goldAmount, gold: true})
}
}
appendSelected := func(itemID uint32) error {
if itemID == 0 {
// Real gold is selected only through the gold-rate branch above,
// where the amount comes from ItemDropInfo_Common.etc.
return nil
}
if _, err := dropCatalog.ResolveItem(itemID); err != nil {
return err
}
selectedDrops = append(selectedDrops, plannedDrop{itemID: itemID, amount: 1})
return nil
}
randomDropAttempts, err := currentDungeonMapDropAttempts(monster.Spawn.RandomDropCount, "random")
if err != nil {
return nil, err
}
fixedDropAttempts, err := currentDungeonMapDropAttempts(monster.Spawn.FixedDropCount, "specified")
if err != nil {
return nil, err
}
dimensionDrops, err := currentDungeonDimensionDrops(monsterCatalog)
if err != nil {
return nil, err
}
dimensionTier, dimensionPool, dimensionMatched, err := currentDungeonDimensionDropPool(
dimensionDrops,
runtime.Dungeon.ID,
runtime.Character.Level,
)
if err != nil {
return nil, err
}
characterJob := byte(0)
characterGrowType := byte(runtime.Character.Stats["grow_type"]) & 0x0f
if dimensionMatched && len(dimensionDrops.ChronicleByCareer) > 0 {
jobValue, parseErr := strconv.ParseUint(strings.TrimSpace(runtime.Character.Job), 10, 8)
if parseErr != nil {
return nil, fmt.Errorf("%w: dimension character job=%q", errCurrentDungeonDimensionDropInvalid, runtime.Character.Job)
}
characterJob = byte(jobValue)
}
selectedDimensionChronicleItems := make([]uint32, 0, randomDropAttempts)
selectedDimensionChronicleGroups := make([]string, 0, randomDropAttempts)
if probabilityFound && itemReferenceFound {
for attempt := 0; attempt < randomDropAttempts; attempt++ {
if rate := currentDungeonDropRate(probability.Rates[1], raw.MonsterTypeBonus[1][monsterType], difficultyRate); rate > rng.Next(currentDungeonDropDenominator) {
rarity := currentDungeonDropRarity(rng, rules)
itemID, selected, err := dropCatalog.SelectGenericStackable(rng, int(monsterLevel), itemReference.ValueA, itemReference.ValueB, rarity)
if err != nil {
return nil, err
}
if selected {
if err := appendSelected(itemID); err != nil {
return nil, err
}
}
}
if rate := currentDungeonDropRate(probability.Rates[2], raw.MonsterTypeBonus[2][monsterType], difficultyRate); rate > rng.Next(currentDungeonDropDenominator) {
var itemID uint32
var selected bool
if dimensionMatched && len(dimensionDrops.ChronicleByCareer) > 0 {
var group string
_, itemID, group, _, selected, err = currentDungeonDimensionChronicleItem(
dimensionDrops,
runtime.Dungeon.ID,
runtime.Character.Level,
characterJob,
characterGrowType,
rng,
false,
)
if err != nil {
return nil, err
}
if selected {
selectedDimensionChronicleItems = append(selectedDimensionChronicleItems, itemID)
selectedDimensionChronicleGroups = append(selectedDimensionChronicleGroups, group)
}
} else {
rarity := currentDungeonDropRarity(rng, rules)
itemID, selected, err = dropCatalog.SelectGenericEquipment(rng, int(monsterLevel), itemReference.ValueA, itemReference.ValueB, rarity)
if err != nil {
return nil, err
}
}
if selected {
if err := appendSelected(itemID); err != nil {
return nil, err
}
}
}
if rate := currentDungeonDropRate(probability.Rates[3], raw.MonsterTypeBonus[3][monsterType], difficultyRate); rate > rng.Next(currentDungeonDropDenominator) {
rarity := currentDungeonDropRarity(rng, rules)
itemID, selected, err := dropCatalog.SelectGenericStackable(rng, int(monsterLevel), itemReference.ValueA, itemReference.ValueB, rarity)
if err != nil {
return nil, err
}
if selected {
if err := appendSelected(itemID); err != nil {
return nil, err
}
}
}
}
}
dimensionCandidates := make([]uint32, 0, len(dimensionPool))
for _, entry := range dimensionPool {
dimensionCandidates = append(dimensionCandidates, entry.ItemID)
}
selectedSpecialDimensionItems := make([]uint32, 0, fixedDropAttempts*len(dimensionPool))
if monsterType == 0 {
// Normal monsters expose dimension materials only as ordinary area
// candidates. They therefore retain the shared 1000/10000 outer roll
// and cannot inherit the fixed special-monster projection.
pool = append(pool, dimensionPool...)
} else {
// Champion, super-champion and boss actors keep the dimension dungeon's
// special fixed projection. This was the original fragment path before
// normal monsters were separated from it; keep the PVF fixed_drop_count
// and every tier/group row authoritative rather than inventing another
// rank probability.
for attempt := 0; attempt < fixedDropAttempts; attempt++ {
for _, entry := range dimensionPool {
if err := appendSelected(entry.ItemID); err != nil {
return nil, err
}
selectedSpecialDimensionItems = append(selectedSpecialDimensionItems, entry.ItemID)
}
}
}
selectedPoolItems := make([]uint32, 0, fixedDropAttempts)
if len(pool) > 0 {
for attempt := 0; attempt < fixedDropAttempts; attempt++ {
if currentDungeonExplicitPoolRate <= rng.Next(currentDungeonDropDenominator) {
continue
}
poolEntry, selected := selectDungeonMonsterDrop(pool, uint64(rng.NextUint32()))
if selected {
if err := appendSelected(poolEntry.ItemID); err != nil {
return nil, err
}
selectedPoolItems = append(selectedPoolItems, poolEntry.ItemID)
}
}
}
if dimensionMatched {
eventName := "game-dungeon-monster-dimension-ordinary-pool-roll"
dropMode := "ordinary_pool"
if monsterType != 0 {
eventName = "game-dungeon-monster-dimension-special-rank-projection"
dropMode = "special_rank_fixed"
}
s.logGameEvent(nil, eventName,
"dungeon_id", runtime.Dungeon.ID,
"maze_index", runtime.MazeIndex,
"room", scene.Coordinate.String(),
"map_id", scene.Map.Map.ID,
"monster_id", monster.Spawn.MonsterID,
"monster_object_key", monster.ObjectKey,
"monster_rank", monster.Spawn.Rank,
"monster_type", monsterType,
"character_level", runtime.Character.Level,
"dimension_tier", dimensionTier,
"drop_mode", dropMode,
"ordinary_pool_rate", currentDungeonExplicitPoolRate,
"ordinary_pool_denominator", currentDungeonDropDenominator,
"ordinary_pool_attempts", fixedDropAttempts,
"ordinary_pool_candidate_count", len(pool),
"dimension_candidate_items", dimensionCandidates,
"selected_pool_items", selectedPoolItems,
"selected_special_dimension_items", selectedSpecialDimensionItems,
"selected_chronicle_items", selectedDimensionChronicleItems,
"selected_chronicle_groups", selectedDimensionChronicleGroups,
"chronicle_job", characterJob,
"chronicle_grow_type", characterGrowType,
"special_probability_shared", false,
"source", currentDungeonDimensionGatePath+"+"+currentDungeonDimensionInfoPath+"+"+currentDungeonDimensionDropListPath+"+"+currentDungeonDimensionDropExcludePath+"+rank_split_monster_template_pool")
}
if areaMaterialMatched {
selectedAreaMaterialCount := 0
for _, itemID := range selectedPoolItems {
if itemID == areaMaterialEntry.ItemID {
selectedAreaMaterialCount++
}
}
s.logGameEvent(nil, "game-dungeon-monster-area-material-pool-roll",
"dungeon_id", runtime.Dungeon.ID,
"dungeon_path", runtime.Dungeon.Path,
"maze_index", runtime.MazeIndex,
"room", scene.Coordinate.String(),
"map_id", scene.Map.Map.ID,
"monster_id", monster.Spawn.MonsterID,
"monster_object_key", monster.ObjectKey,
"item_id", areaMaterialEntry.ItemID,
"item_weight", areaMaterialEntry.Weight,
"ordinary_pool_rate", currentDungeonExplicitPoolRate,
"ordinary_pool_denominator", currentDungeonDropDenominator,
"ordinary_pool_attempts", fixedDropAttempts,
"ordinary_pool_candidate_count", len(pool),
"selected_pool_items", selectedPoolItems,
"selected_area_material_count", selectedAreaMaterialCount,
"source", "active_pvf_metro_center_range+86jp_area_material_weighted_pool")
}
specialDrops, err := currentDungeonSpecialDrops(monsterCatalog)
if err != nil {
return nil, err
}
dungeonLevel := int(monsterLevel)
if basisLevel := runtime.Dungeon.Metadata.BasisLevel; basisLevel.Set && basisLevel.Value > 0 {
dungeonLevel = int(basisLevel.Value)
}
growthIndependentDropPercent := int64(0)
if growthEffect, growthActive := s.currentGrowthContractEffect(
context.Background(),
runtime.Character.AccountID,
); growthActive {
// This runtime currently admits one locally owned dungeon actor. The
// PVF's first independent-drop value is therefore authoritative.
growthIndependentDropPercent = growthEffect.independentDropRatePercent(1)
}
independentItems := currentDungeonIndependentDropItems(
specialDrops,
monster.Spawn.MonsterID,
int(runtime.Request.Difficulty),
dungeonLevel,
growthIndependentDropPercent,
rng,
)
for _, itemID := range independentItems {
if err := appendSelected(itemID); err != nil {
return nil, err
}
}
worldLevel := specialDrops.WorldByLevel[int(monsterLevel)]
worldItemID, worldSelected := currentDungeonWorldDropItem(specialDrops, int(monsterLevel), rng)
if worldSelected {
if err := appendSelected(worldItemID); err != nil {
return nil, err
}
}
if amount := currentDungeonKurunStrongBreathDropAmount(runtime.Dungeon, monsterType); amount != 0 {
if _, err := dropCatalog.ResolveItem(currentDungeonKurunStrongBreathItemID); err != nil {
return nil, err
}
selectedDrops = append(selectedDrops, plannedDrop{
itemID: currentDungeonKurunStrongBreathItemID,
amount: amount,
})
s.logGameEvent(nil, "game-dungeon-monster-kurun-strong-breath-drop",
"dungeon_id", runtime.Dungeon.ID,
"dungeon_path", runtime.Dungeon.Path,
"monster_id", monster.Spawn.MonsterID,
"monster_type", monsterType,
"item_id", currentDungeonKurunStrongBreathItemID,
"amount", amount,
"source", "pvf_dragon_kurun_nateram_rank_rule")
}
s.logGameEvent(nil, "game-dungeon-monster-special-drop-roll",
"dungeon_id", runtime.Dungeon.ID,
"maze_index", runtime.MazeIndex,
"room", scene.Coordinate.String(),
"map_id", scene.Map.Map.ID,
"monster_id", monster.Spawn.MonsterID,
"monster_level", monsterLevel,
"dungeon_basis_level", dungeonLevel,
"difficulty", runtime.Request.Difficulty,
"independent_entry_count", len(specialDrops.IndependentByMonster[monster.Spawn.MonsterID]),
"independent_selected_items", independentItems,
"growth_contract_independent_drop_percent", growthIndependentDropPercent,
"world_total_weight", worldLevel.TotalWeight,
"world_candidate_count", len(worldLevel.Items),
"world_denominator", currentDungeonWorldDropDenominator,
"world_selected", worldSelected,
"world_selected_item", worldItemID,
"source", "runtime_pvf_independent_drop_and_world_drop")
// The LCG is advanced even when no standard scene item survived the real
// PVF selection. This mirrors the reference room stream across kills.
visit.DropRNG = rng.Seed()
s.reportCurrentDungeonGenericDropFallbacks(dropCatalog, fallbacksBefore,
"game-dungeon-monster-drop-empty-rarity-pool",
"dungeon_id", runtime.Dungeon.ID,
"maze_index", runtime.MazeIndex,
"room", scene.Coordinate.String(),
"monster_id", monster.Spawn.MonsterID)
if len(selectedDrops) == 0 {
return nil, nil
}
owner := runtime.DropOwner
if owner == nil {
owner = newRuntimeDungeonDropOwner()
}
nextObjectKey := owner.nextObjectKeyAtOrAbove(runtime.NextObjectKey)
count := uint32(len(selectedDrops))
if nextObjectKey == 0 || nextObjectKey > math.MaxUint16 ||
count > math.MaxUint16-nextObjectKey {
return nil, fmt.Errorf(
"%w: next=%d count=%d",
errDungeonDropObjectKeyRange,
nextObjectKey,
count,
)
}
if owner.nextSceneSlot == 0 || uint32(owner.nextSceneSlot)+count > math.MaxUint16 {
return nil, fmt.Errorf(
"%w: next=%d count=%d",
errDungeonDropSceneSlotRange,
owner.nextSceneSlot,
count,
)
}
drops := make([]*runtimeDungeonDrop, 0, count)
wires := make([]currentDungeonDeathDropWire, 0, count)
nextSceneSlot := owner.nextSceneSlot
for _, selected := range selectedDrops {
definition := dungeonDropItemDefinition{ItemID: 0, Kind: dungeonDropItemStackable}
if !selected.gold {
var err error
definition, err = dropCatalog.ResolveItem(selected.itemID)
if err != nil {
return nil, err
}
}
itemState, err := currentDungeonDeathDropItemState(nextSceneSlot, definition, selected.amount)
if err != nil {
return nil, err
}
qualitySeed := uint32(0)
if definition.Kind == dungeonDropItemEquipment {
qualitySeed = binary.LittleEndian.Uint32(itemState.data[6:10])
}
drop := &runtimeDungeonDrop{
ObjectKey: nextObjectKey,
SceneSlot: nextSceneSlot,
Item: definition,
Amount: selected.amount,
QualitySeed: qualitySeed,
OwnerActorObjectKey: ownerActorObjectKey,
Room: roomKey,
Status: runtimeDungeonDropAvailable,
}
drops = append(drops, drop)
wires = append(wires, currentDungeonDeathDropWire{
SceneObjectKey: drop.ObjectKey,
Item: itemState,
UnknownTailSentinel: math.MaxUint16,
OwnerActorObjectKey: ownerActorObjectKey,
})
nextObjectKey++
nextSceneSlot++
}
if err := owner.registerBatch(drops, nextSceneSlot); err != nil {
return nil, err
}
runtime.DropOwner = owner
runtime.NextObjectKey = nextObjectKey
return wires, nil
}