Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

331 lines
13 KiB
Go

package dnfbridge
import (
"context"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"math"
"path"
"sort"
"strconv"
"strings"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const (
currentChatEmoticonCatalogPath = "chatemoticon/chatemoticon.lst"
currentChatEmoticonOrderPath = "chatemoticon/emoticonorderlist.etc"
currentChatEmoticonConfigMsgID uint16 = 411
currentChatEmoticonSaveMsgID uint16 = 472
currentChatEmoticonRecentMsgID uint16 = 1446
currentChatEmoticonRecentBodySize = 85
currentChatEmoticonMaximumEntries = 256
currentChatEmoticonMaximumTextBytes = 519
currentChatEmoticonValueKey = "configured_body_base64"
currentChatEmoticonRecentValueKey = "recent_body_base64"
)
var errCurrentChatEmoticonInvalid = errors.New("current chat emoticon state is invalid")
type currentChatEmoticonEntry struct {
ID uint16
UnlockType string
UnlockValue uint16
DisplayOrder int
}
type currentChatEmoticonConfiguredRow struct {
ID uint16
Text []byte
}
func currentChatEmoticonScope(accountID string, characterID uint16) string {
return "account:" + strings.TrimSpace(accountID) + ":character:" + strconv.Itoa(int(characterID)) + ":chat_emoticon"
}
func parseCurrentChatEmoticonCatalog(source initialEquipmentTextSource) ([]currentChatEmoticonEntry, error) {
if source == nil {
return nil, errCurrentChatEmoticonInvalid
}
listText, err := source.ReadText(currentChatEmoticonCatalogPath)
if err != nil {
return nil, fmt.Errorf("read %s: %w", currentChatEmoticonCatalogPath, err)
}
listDocument, err := dnfpvf.Parse(currentChatEmoticonCatalogPath, listText)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", currentChatEmoticonCatalogPath, err)
}
order := make(map[uint16]int)
if orderText, readErr := source.ReadText(currentChatEmoticonOrderPath); readErr == nil {
if orderDocument, parseErr := dnfpvf.Parse(currentChatEmoticonOrderPath, orderText); parseErr == nil {
for index, value := range orderDocument.Ints("order") {
if value <= 0 || value > math.MaxUint16 {
continue
}
id := uint16(value)
if _, duplicate := order[id]; !duplicate {
order[id] = index
}
}
}
}
entries := make([]currentChatEmoticonEntry, 0)
seen := make(map[uint16]struct{})
for _, ref := range dnfpvf.ParseList(listDocument) {
if ref.ID <= 0 || ref.ID > math.MaxUint16 {
continue
}
id := uint16(ref.ID)
if _, duplicate := seen[id]; duplicate {
return nil, fmt.Errorf("%w: duplicate emoticon id=%d", errCurrentChatEmoticonInvalid, id)
}
seen[id] = struct{}{}
refPath := strings.TrimSpace(strings.ReplaceAll(ref.Path, "\\", "/"))
if !strings.Contains(refPath, "/") {
refPath = path.Join(path.Dir(currentChatEmoticonCatalogPath), refPath)
}
text, err := source.ReadText(refPath)
if err != nil {
return nil, fmt.Errorf("read emoticon id=%d path=%s: %w", id, refPath, err)
}
document, err := dnfpvf.Parse(refPath, text)
if err != nil {
return nil, fmt.Errorf("parse emoticon id=%d path=%s: %w", id, refPath, err)
}
unlockType, found := document.Text("type")
if !found {
return nil, fmt.Errorf("%w: emoticon id=%d missing [type]", errCurrentChatEmoticonInvalid, id)
}
unlockType = strings.ToUpper(strings.Trim(strings.TrimSpace(unlockType), "[]"))
if unlockType != "COMMON" && unlockType != "LEVEL" {
continue
}
unlockValue := uint16(0)
if unlockType == "LEVEL" {
value, found := document.Int("type")
if !found || value <= 0 || value > math.MaxUint16 {
return nil, fmt.Errorf("%w: emoticon id=%d invalid level=%d", errCurrentChatEmoticonInvalid, id, value)
}
unlockValue = uint16(value)
}
displayOrder := int(id) + currentChatEmoticonMaximumEntries
if value, found := order[id]; found {
displayOrder = value
}
entries = append(entries, currentChatEmoticonEntry{ID: id, UnlockType: unlockType, UnlockValue: unlockValue, DisplayOrder: displayOrder})
}
if len(entries) == 0 || len(entries) > currentChatEmoticonMaximumEntries {
return nil, fmt.Errorf("%w: unlocked catalog entries=%d", errCurrentChatEmoticonInvalid, len(entries))
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].DisplayOrder != entries[j].DisplayOrder {
return entries[i].DisplayOrder < entries[j].DisplayOrder
}
return entries[i].ID < entries[j].ID
})
return entries, nil
}
func (s *Service) currentChatEmoticons() ([]currentChatEmoticonEntry, error) {
if s == nil {
return nil, errCurrentChatEmoticonInvalid
}
s.chatEmoticonCatalogMu.Lock()
defer s.chatEmoticonCatalogMu.Unlock()
if s.chatEmoticonCatalog != nil || s.chatEmoticonCatalogLoadErr != nil {
return append([]currentChatEmoticonEntry(nil), s.chatEmoticonCatalog...), s.chatEmoticonCatalogLoadErr
}
s.initialEquipmentMu.Lock()
archive, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err == nil {
s.chatEmoticonCatalog, err = parseCurrentChatEmoticonCatalog(archive)
}
s.chatEmoticonCatalogLoadErr = err
return append([]currentChatEmoticonEntry(nil), s.chatEmoticonCatalog...), err
}
func parseCurrentChatEmoticonConfiguredBody(body []byte) ([]currentChatEmoticonConfiguredRow, bool) {
if len(body) < 2 {
return nil, false
}
count := int(binary.LittleEndian.Uint16(body[:2]))
if count > currentChatEmoticonMaximumEntries {
return nil, false
}
rows := make([]currentChatEmoticonConfiguredRow, 0, count)
seen := make(map[uint16]struct{}, count)
next := 2
for index := 0; index < count; index++ {
if next > len(body)-2 {
return nil, false
}
id := binary.LittleEndian.Uint16(body[next : next+2])
next += 2
text, afterText, ok := readCurrentRawDstr(body, next)
if !ok || id == 0 || len(text) > currentChatEmoticonMaximumTextBytes || bytesContainsZero(text) {
return nil, false
}
if _, duplicate := seen[id]; duplicate {
return nil, false
}
seen[id] = struct{}{}
rows = append(rows, currentChatEmoticonConfiguredRow{ID: id, Text: text})
next = afterText
}
return rows, next == len(body)
}
func bytesContainsZero(value []byte) bool {
for _, current := range value {
if current == 0 {
return true
}
}
return false
}
func buildCurrentChatEmoticonConfigBody(entries []currentChatEmoticonEntry, level uint16, configured []currentChatEmoticonConfiguredRow) []byte {
configuredByID := make(map[uint16][]byte, len(configured))
for _, row := range configured {
configuredByID[row.ID] = append([]byte(nil), row.Text...)
}
eligible := make([]currentChatEmoticonEntry, 0, len(entries))
for _, entry := range entries {
if entry.UnlockType == "COMMON" || (entry.UnlockType == "LEVEL" && entry.UnlockValue <= level) {
eligible = append(eligible, entry)
}
}
var writer packetWriter
writer.writeUint16(uint16(len(eligible)))
for _, entry := range eligible {
writer.writeUint16(entry.UnlockValue)
writer.writeUint16(entry.ID)
writer.writeRawDstr(configuredByID[entry.ID])
}
return writer.bytes()
}
func loadCurrentChatEmoticonConfiguredRows(ctx context.Context, repository dnfrepo.SettingsRepository, scope string) ([]currentChatEmoticonConfiguredRow, error) {
if repository == nil || strings.TrimSpace(scope) == "" {
return nil, nil
}
record, found, err := repository.Load(ctx, scope)
if err != nil || !found {
return nil, err
}
encoded := strings.TrimSpace(record.Values[currentChatEmoticonValueKey])
if encoded == "" {
return nil, nil
}
body, err := base64.StdEncoding.Strict().DecodeString(encoded)
if err != nil {
return nil, errCurrentChatEmoticonInvalid
}
rows, ok := parseCurrentChatEmoticonConfiguredBody(body)
if !ok {
return nil, errCurrentChatEmoticonInvalid
}
return rows, nil
}
func (s *Service) sendSelectedChatEmoticonCatalog(session *gameSession, source string) (bool, error) {
if s == nil || session == nil || session.selectedCharacterID == 0 || session.chatEmoticonCatalogSent {
return false, nil
}
entries, err := s.currentChatEmoticons()
if err != nil {
s.logGameEvent(session, "game-chat-emoticon-catalog-skipped", "source", source, "char_id", session.selectedCharacterID, "reason", "pvf_catalog_unavailable", "error", err)
return false, nil
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Character == nil || repositories.Settings == nil {
s.logGameEvent(session, "game-chat-emoticon-catalog-skipped", "source", source, "char_id", session.selectedCharacterID, "reason", "repository_unavailable")
return false, nil
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
characterID := strconv.Itoa(int(session.selectedCharacterID))
character, found, err := repositories.Character.Load(ctx, characterID)
if err != nil || !found || strings.TrimSpace(character.CharacterID) != characterID ||
strings.TrimSpace(character.AccountID) != strings.TrimSpace(s.accountIDForSession(session)) {
s.logGameEvent(session, "game-chat-emoticon-catalog-skipped", "source", source, "char_id", session.selectedCharacterID, "reason", "character_identity_unavailable", "found", found, "error", err)
return false, nil
}
configured, err := loadCurrentChatEmoticonConfiguredRows(ctx, repositories.Settings, currentChatEmoticonScope(character.AccountID, session.selectedCharacterID))
if err != nil {
s.logGameEvent(session, "game-chat-emoticon-catalog-skipped", "source", source, "char_id", session.selectedCharacterID, "reason", "saved_configuration_unavailable", "error", err)
return false, nil
}
level := character.Level
if level < 0 {
level = 0
}
if level > math.MaxUint16 {
level = math.MaxUint16
}
body := buildCurrentChatEmoticonConfigBody(entries, uint16(level), configured)
if len(body) < 2 || binary.LittleEndian.Uint16(body[:2]) == 0 {
s.logGameEvent(session, "game-chat-emoticon-catalog-skipped", "source", source, "char_id", session.selectedCharacterID, "reason", "no_eligible_emoticons")
return false, nil
}
if err := s.sendGameUpperSuccess(session, currentChatEmoticonConfigMsgID, body); err != nil {
return false, err
}
session.chatEmoticonCatalogSent = true
s.logGameEvent(session, "game-chat-emoticon-catalog-sent", "source", source, "char_id", session.selectedCharacterID, "eligible_count", binary.LittleEndian.Uint16(body[:2]), "configured_count", len(configured), "body_source", "current_exe_class1_op411_u16_count_u16_source_u16_emoticon_dstr")
return true, nil
}
func (s *Service) handleCurrentChatEmoticonCommand(session *gameSession, typ uint16, body []byte) (bool, error) {
if typ != currentChatEmoticonSaveMsgID && typ != currentChatEmoticonRecentMsgID {
return false, nil
}
if session == nil || session.selectedCharacterID == 0 {
return true, nil
}
valid := false
switch typ {
case currentChatEmoticonSaveMsgID:
_, valid = parseCurrentChatEmoticonConfiguredBody(body)
case currentChatEmoticonRecentMsgID:
valid = len(body) == currentChatEmoticonRecentBodySize
}
if !valid {
s.logGameEvent(session, "game-chat-emoticon-save-rejected", "type", typ, "body_len", len(body), "reason", "current_exe_exact_body_required")
return true, s.sendGameUpperFailure(session, typ, currentTownChatGenericError)
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Character == nil || repositories.Settings == nil {
return true, s.sendGameUpperFailure(session, typ, currentTownChatGenericError)
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
characterID := strconv.Itoa(int(session.selectedCharacterID))
character, found, err := repositories.Character.Load(ctx, characterID)
if err != nil || !found || strings.TrimSpace(character.AccountID) != strings.TrimSpace(s.accountIDForSession(session)) {
return true, s.sendGameUpperFailure(session, typ, currentTownChatGenericError)
}
key := currentChatEmoticonValueKey
if typ == currentChatEmoticonRecentMsgID {
key = currentChatEmoticonRecentValueKey
}
if err := saveCurrentGameOptionScopedValue(ctx, repositories.Settings, currentChatEmoticonScope(character.AccountID, session.selectedCharacterID), key, body); err != nil {
return true, s.sendGameUpperFailure(session, typ, currentTownChatGenericError)
}
s.logGameEvent(session, "game-chat-emoticon-save-committed", "type", typ, "char_id", session.selectedCharacterID, "body_len", len(body), "body_source", currentChatEmoticonSaveBodySource(typ))
return true, s.sendGameUpperSuccess(session, typ, nil)
}
func currentChatEmoticonSaveBodySource(typ uint16) string {
if typ == uint16(dnfenum.CmdPacketSaveRecentEmoticonList) {
return "current_exe_op1446_fixed_85_byte_recent_list"
}
return "current_exe_op472_u16_count_rows_u16_emoticon_dstr"
}