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,326 @@
package dnfbridge
import (
"context"
"encoding/binary"
"fmt"
"strconv"
"strings"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// currentEquippedCreatureSnapshot is the one durable creature projection used
// by all current-EXE class0/op2 actor modes. Equipment slot 26 owns whether a
// creature is worn; PetRecord supplies the hatched creature's name and level.
//
// Current EXE sub_20077D0 consumes {itemID, serialOrHandle}. The second dword
// is the same value stored at raw offset 24 in the slot-26 equipment entry and
// passed as constructor field a10 by sub_1D77560.
type currentEquippedCreatureSnapshot struct {
itemID uint32
serialOrHandle uint32
name string
level byte
aliveState byte
townDisplay bool
source string
}
func (snapshot currentEquippedCreatureSnapshot) valid() bool {
return snapshot.itemID != 0 && snapshot.serialOrHandle != 0
}
func (s *Service) currentEquippedCreatureForCharacter(ctx context.Context, characterID string) currentEquippedCreatureSnapshot {
snapshot, _ := s.currentEquippedCreatureForCharacterWithError(ctx, characterID)
return snapshot
}
// currentEquippedCreatureForCharacterWithError distinguishes an authoritative
// "nothing worn" result from a transient repository failure. The staged
// post-op24 chain must not permanently skip op102 merely because one database
// read failed; ordinary snapshot readers retain the compatibility wrapper
// above when they can safely render an empty optional creature.
func (s *Service) currentEquippedCreatureForCharacterWithError(
ctx context.Context,
characterID string,
) (currentEquippedCreatureSnapshot, error) {
if s == nil || strings.TrimSpace(characterID) == "" {
return currentEquippedCreatureSnapshot{}, fmt.Errorf("equipped creature owner is unavailable")
}
repos, ok := s.projectionRepositories(ctx)
if !ok {
return currentEquippedCreatureSnapshot{}, fmt.Errorf("equipped creature repositories are unavailable")
}
if ctx == nil {
ctx = context.Background()
}
var petRecord dnfrepo.PetRecord
petFound := false
var petLoadErr error
if repos.Pet != nil {
if loaded, found, err := repos.Pet.Load(ctx, characterID); err != nil {
petLoadErr = fmt.Errorf("load equipped creature metadata for character %s: %w", characterID, err)
} else if found {
petRecord = dnfrepo.ClonePet(loaded)
petFound = true
}
}
if repos.Equipment != nil {
equipment, found, err := repos.Equipment.Load(ctx, characterID)
if err != nil {
return currentEquippedCreatureSnapshot{}, fmt.Errorf(
"load equipped creature slot for character %s: %w",
characterID,
err,
)
}
if found {
entry, worn := currentEquippedCreatureEquipmentEntry(equipment)
if !worn {
// A present equipment record is authoritative. Do not resurrect
// a stale PetRecord.EquippedKey after slot 26 was removed.
return currentEquippedCreatureSnapshot{source: "equipment_slot26_absent"}, nil
}
snapshot := currentEquippedCreatureFromEquipment(entry)
if !snapshot.valid() {
return currentEquippedCreatureSnapshot{source: "equipment_slot26_invalid_item_or_serial"}, nil
}
if petLoadErr != nil {
return currentEquippedCreatureSnapshot{}, petLoadErr
}
snapshot.source = "equipment_slot26_raw"
if petFound {
currentEquippedCreatureEnrichFromPetRecord(&snapshot, petRecord)
}
s.currentEquippedCreatureEnrichNameFromPVF(&snapshot)
return snapshot, nil
}
}
// Compatibility for imported records which predate the equipment owner.
if petLoadErr != nil {
return currentEquippedCreatureSnapshot{}, petLoadErr
}
// This fallback is used only when no EquipmentRecord exists at all.
if petFound {
if entry, found := petRecord.Entries[strings.TrimSpace(petRecord.EquippedKey)]; found {
snapshot := currentEquippedCreatureFromPetEntry(entry)
if snapshot.valid() {
snapshot.townDisplay = petRecord.TownDisplay
snapshot.source = "legacy_pet_equipped_key"
s.currentEquippedCreatureEnrichNameFromPVF(&snapshot)
return snapshot, nil
}
}
}
return currentEquippedCreatureSnapshot{}, nil
}
func currentEquippedCreatureEquipmentEntry(record dnfrepo.EquipmentRecord) (dnfrepo.EquipmentEntry, bool) {
if entry, ok := record.Entries["26"]; ok && entry.SlotIndex == 26 && entry.ItemID > 0 {
return entry, true
}
for _, entry := range record.Entries {
if entry.SlotIndex == 26 && entry.ItemID > 0 {
return entry, true
}
}
return dnfrepo.EquipmentEntry{}, false
}
func currentEquippedCreatureFromEquipment(entry dnfrepo.EquipmentEntry) currentEquippedCreatureSnapshot {
itemID, itemOK := currentCreatureWireUint32(entry.ItemID)
serial := currentEquippedCreatureSerialFromRaw(entry.RawEntry)
if serial == 0 {
serial = currentCreatureExtraUint32(entry.Extra,
"creature_serial_or_handle", "creature_serial", "pet_serial")
}
if serial == 0 && currentCreatureEquipmentSourceIsPet(entry.Extra) {
serial = currentCreatureExtraUint32(entry.Extra, "serial", "handle", "instance_value", "item_uid")
}
if !itemOK || serial == 0 {
return currentEquippedCreatureSnapshot{}
}
return currentEquippedCreatureSnapshot{
itemID: itemID,
serialOrHandle: serial,
name: currentCreatureExtraString(entry.Extra, "creature_name", "pet_name"),
level: currentCreatureExtraByte(entry.Extra, "creature_level", "pet_level"),
aliveState: 1,
}
}
func currentEquippedCreatureFromPetEntry(entry dnfrepo.PetEntry) currentEquippedCreatureSnapshot {
itemID, itemOK := currentCreatureWireUint32(entry.ItemID)
serial := currentCreatureExtraUint32(entry.Extra,
"creature_serial_or_handle", "creature_serial", "pet_serial",
"serial", "handle", "instance_value", "item_uid")
if serial == 0 {
if parsed, err := strconv.ParseUint(strings.TrimSpace(entry.PetKey), 0, 32); err == nil {
serial = uint32(parsed)
}
}
if !itemOK || serial == 0 {
return currentEquippedCreatureSnapshot{}
}
return currentEquippedCreatureSnapshot{
itemID: itemID,
serialOrHandle: serial,
name: strings.TrimSpace(entry.Name),
level: currentCreatureLevelByte(entry.Level),
aliveState: 1,
}
}
func currentEquippedCreatureEnrichFromPetRecord(snapshot *currentEquippedCreatureSnapshot, record dnfrepo.PetRecord) {
if snapshot == nil || !snapshot.valid() {
return
}
entry, found := currentEquippedCreatureMetadataEntry(*snapshot, record)
if found {
if strings.TrimSpace(entry.Name) != "" {
snapshot.name = strings.TrimSpace(entry.Name)
}
if entry.Level > 0 {
snapshot.level = currentCreatureLevelByte(entry.Level)
}
}
snapshot.townDisplay = record.TownDisplay
}
func currentEquippedCreatureMetadataEntry(snapshot currentEquippedCreatureSnapshot, record dnfrepo.PetRecord) (dnfrepo.PetEntry, bool) {
if key := strings.TrimSpace(record.EquippedKey); key != "" {
if entry, ok := record.Entries[key]; ok && currentCreatureItemMatches(entry.ItemID, snapshot.itemID) {
entrySerial := currentCreaturePetEntrySerial(entry)
if entrySerial == 0 || entrySerial == snapshot.serialOrHandle {
return entry, true
}
}
}
serialKey := strconv.FormatUint(uint64(snapshot.serialOrHandle), 10)
if entry, ok := record.Entries[serialKey]; ok && currentCreatureItemMatches(entry.ItemID, snapshot.itemID) {
return entry, true
}
var candidate dnfrepo.PetEntry
matchCount := 0
for _, entry := range record.Entries {
if !currentCreatureItemMatches(entry.ItemID, snapshot.itemID) {
continue
}
entrySerial := currentCreaturePetEntrySerial(entry)
if entrySerial == snapshot.serialOrHandle {
return entry, true
}
candidate = entry
matchCount++
}
return candidate, matchCount == 1
}
// currentEquippedCreatureEnrichNameFromPVF repairs only an absent custom name
// at the scene projection boundary. The final mode0 actor packet creates the
// live town creature object and must carry a non-empty DSTR there; op105 alone
// only populates the creature-state table. Durable renamed values remain
// authoritative, and the PVF fallback is never persisted.
func (s *Service) currentEquippedCreatureEnrichNameFromPVF(snapshot *currentEquippedCreatureSnapshot) {
if s == nil || snapshot == nil || !snapshot.valid() || strings.TrimSpace(snapshot.name) != "" {
return
}
catalog, err := s.currentPetPVFCatalog()
if err != nil || catalog == nil {
return
}
definition, err := catalog.ResolveCreature(int64(snapshot.itemID))
if err != nil {
return
}
snapshot.name = strings.TrimSpace(definition.Name)
}
func currentEquippedCreatureSerialFromRaw(raw []byte) uint32 {
// op2/sub_20077D0 consumes the constructor-a10 field repeated at +24 in a
// current pet equipment row. Do not fall back to the ordinary +5 equipment
// instance: ordinary equipment instance values are not creature serials.
if len(raw) >= 28 {
return binary.LittleEndian.Uint32(raw[24:28])
}
return 0
}
func currentCreatureEquipmentSourceIsPet(extra map[string]string) bool {
source := strings.ToLower(currentCreatureExtraString(extra, "source", "item_kind", "equipment_kind"))
return strings.Contains(source, "pet") || strings.Contains(source, "creature")
}
func currentCreaturePetEntrySerial(entry dnfrepo.PetEntry) uint32 {
return currentCreatureExtraUint32(entry.Extra,
"creature_serial_or_handle", "creature_serial", "pet_serial",
"serial", "handle", "instance_value", "item_uid")
}
func currentCreatureWireUint32(value int64) (uint32, bool) {
if value <= 0 || value > int64(^uint32(0)) {
return 0, false
}
return uint32(value), true
}
func currentCreatureItemMatches(value int64, wire uint32) bool {
itemID, ok := currentCreatureWireUint32(value)
return ok && itemID == wire
}
func currentCreatureExtraUint32(extra map[string]string, keys ...string) uint32 {
for _, key := range keys {
raw := strings.TrimSpace(extra[key])
if raw == "" {
continue
}
if value, err := strconv.ParseUint(raw, 0, 32); err == nil && value != 0 {
return uint32(value)
}
}
return 0
}
func currentCreatureExtraString(extra map[string]string, keys ...string) string {
for _, key := range keys {
if value := strings.TrimSpace(extra[key]); value != "" {
return value
}
}
return ""
}
func currentCreatureExtraByte(extra map[string]string, keys ...string) byte {
for _, key := range keys {
raw := strings.TrimSpace(extra[key])
if raw == "" {
continue
}
if value, err := strconv.ParseInt(raw, 0, 64); err == nil && value > 0 {
return currentCreatureLevelByte(value)
}
}
return 0
}
func currentCreatureLevelByte(value int64) byte {
if value <= 0 {
return 0
}
if value > 255 {
return 255
}
return byte(value)
}
func (r *csharpLegacyUserInfoReader) currentEquippedCreature() currentEquippedCreatureSnapshot {
if r == nil || r.service == nil {
return currentEquippedCreatureSnapshot{}
}
return r.service.currentEquippedCreatureForCharacter(r.ctx, r.characterID)
}