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,330 @@
|
||||
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"
|
||||
}
|
||||
Reference in new issue
Block a user