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,387 @@
|
||||
// 本文件负责 PVF 教堂预约、入场和持久化仪式阶段的计时恢复。
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/wedding"
|
||||
)
|
||||
|
||||
func (s *Service) weddingRoom() (byte, error) {
|
||||
for room := byte(8); room <= 38; room++ {
|
||||
if area, found := s.townArea(39, int64(room)); found && strings.EqualFold(strings.ReplaceAll(area.MapPath, "\\", "/"), "Church/Church.map") {
|
||||
return room, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("wedding church missing from runtime PVF")
|
||||
}
|
||||
|
||||
func weddingTownInstance(c dnfrepo.CharacterRecord, room byte, channel uint16) (uint32, bool) {
|
||||
state, err := wedding.Read(c)
|
||||
if err != nil || (state.Status != 2 && state.Status != 3) || state.Room != room || state.ChannelID != channel {
|
||||
return 0, false
|
||||
}
|
||||
return state.PairID, true
|
||||
}
|
||||
|
||||
// currentWeddingGuildDungeon 对应上游的 runtime.guildDungeonID != 0。
|
||||
//
|
||||
// 工作区的 runtimeDungeonState 不记"这是不是公会地下城"这个标志:公会净化是通关后
|
||||
// 由 creditCurrentGuildDungeonClear 用同一份目录反查入账的。所以这里复用同一个
|
||||
// 判据(目录里能查到该副本 id 即为公会地下城),保持"公会本不算夫妻通关"的语义。
|
||||
// 目录加载失败时返回 false —— 宁可漏排除,也不要因为读目录失败而吞掉正常通关。
|
||||
//
|
||||
// 判据本体已提到 currentGuildDungeonRun(公会贡献度那条规则也要用同一个判据),
|
||||
// 这里只是保留夫妻系统原有的函数名与语义,行为逐字节不变。
|
||||
func (s *Service) currentWeddingGuildDungeon(runtime *runtimeDungeonState) bool {
|
||||
return s.currentGuildDungeonRun(runtime)
|
||||
}
|
||||
|
||||
// The dungeon owner supplies the actual completion roster, not a town party list.
|
||||
func (s *Service) currentWeddingDungeonCouple(ctx context.Context, runtime *runtimeDungeonState) (bool, error) {
|
||||
if runtime == nil || runtime.ProtocolMode != currentDungeonProtocolModeNormal ||
|
||||
runtime.raidDungeon.RaidKey != 0 || s.currentWeddingGuildDungeon(runtime) || runtime.altar != nil ||
|
||||
runtime.tournament != nil || runtime.Dungeon.Metadata.NoFatigue ||
|
||||
runtime.Dungeon.Metadata.TutorialDungeon.Value != 0 || runtime.Dungeon.Metadata.InfernoDungeon {
|
||||
return false, nil
|
||||
}
|
||||
kind := normalizeDungeonPVFSymbol(runtime.Dungeon.Metadata.DungeonType)
|
||||
if kind != "" && kind != "normal" {
|
||||
return false, nil
|
||||
}
|
||||
owner := runtime.partyDungeonOwner
|
||||
if owner == nil {
|
||||
return false, nil
|
||||
}
|
||||
owner.mu.Lock()
|
||||
defer owner.mu.Unlock()
|
||||
// Freeze both recipients' eligibility at the first authoritative completion,
|
||||
// so the first member returning to town does not invalidate the other's result.
|
||||
if owner.weddingClearKnown {
|
||||
return owner.weddingClearCouple, nil
|
||||
}
|
||||
if owner.retired || owner.entryMemberCount != 2 {
|
||||
return false, nil
|
||||
}
|
||||
members := owner.members
|
||||
if len(members) != 2 {
|
||||
return false, nil
|
||||
}
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Character == nil {
|
||||
return false, dnfrepo.ErrRepoMissing
|
||||
}
|
||||
var states [2]wedding.State
|
||||
containsActor := false
|
||||
for i, member := range members {
|
||||
identity, online := s.onlineGameSessionCharacterSnapshot(member.userID)
|
||||
if !online || identity.generation != member.generation {
|
||||
return false, nil
|
||||
}
|
||||
id := strconv.Itoa(int(member.userID))
|
||||
containsActor = containsActor || id == runtime.Character.CharacterID
|
||||
character, found, err := repos.Character.Load(ctx, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !found || character.AccountID != s.accountIDForSession(identity.session) || character.Stats["delete_flag"] != 0 {
|
||||
return false, nil
|
||||
}
|
||||
states[i], err = wedding.Read(character)
|
||||
if err != nil || states[i].Status != 1 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
owner.weddingClearCouple = containsActor && states[0].PairID == states[1].PairID &&
|
||||
states[0].PartnerID == members[1].userID && states[1].PartnerID == members[0].userID
|
||||
owner.weddingClearKnown = true
|
||||
return owner.weddingClearCouple, nil
|
||||
}
|
||||
|
||||
func (s *Service) weddingGiftAllocator(cat *wedding.Catalog, kind byte) (func(*dnfrepo.InventoryRecord, wedding.Item) error, error) {
|
||||
if kind > 2 {
|
||||
return nil, wedding.Invalid
|
||||
}
|
||||
items, err := s.currentPVFItemCatalog()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gift := cat.Ceremonies[kind].Gift
|
||||
definition, err := items.ResolveItem(uint32(gift.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(bag *dnfrepo.InventoryRecord, item wedding.Item) error {
|
||||
if item != gift {
|
||||
return wedding.Invalid
|
||||
}
|
||||
_, err := s.grantCurrentWeddingMainProduct(bag, definition, uint32(item.Count))
|
||||
return err
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) restoreWeddingCeremony(session *gameSession, c dnfrepo.CharacterRecord) error {
|
||||
state, err := wedding.Read(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.Status != 2 && state.Status != 3 {
|
||||
return nil
|
||||
}
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Character == nil {
|
||||
return dnfrepo.ErrRepoMissing
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
partner, found, err := repos.Character.Load(ctx, strconv.Itoa(int(state.PartnerID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return wedding.Missing
|
||||
}
|
||||
var publishErr error
|
||||
if state.Status == 2 && s.gameplayNow().Before(time.Unix(state.StartAt, 0)) {
|
||||
publishErr = s.publishWeddingReservation([2]dnfrepo.CharacterRecord{c, partner})
|
||||
}
|
||||
return errors.Join(publishErr, s.scheduleWeddingCeremony(c))
|
||||
}
|
||||
|
||||
func weddingReservationBody(chars [2]dnfrepo.CharacterRecord, state wedding.State, channel uint32, invitations uint16) []byte {
|
||||
var w packetWriter
|
||||
w.writeUint32(state.PairID)
|
||||
w.writeUint32(uint32(state.Room))
|
||||
w.writeUint32(uint32(state.StartAt))
|
||||
w.writeUint32(channel)
|
||||
w.writeByte(state.Ceremony)
|
||||
for _, c := range chars {
|
||||
name := rosterNameBytes(c.Name)
|
||||
w.writeBytes(name)
|
||||
w.writeZero(30 - len(name))
|
||||
}
|
||||
w.writeUint16(invitations)
|
||||
return w.bytes()
|
||||
}
|
||||
|
||||
func (s *Service) publishWeddingReservation(chars [2]dnfrepo.CharacterRecord) error {
|
||||
state, err := wedding.Read(chars[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cat, err := s.currentWeddingCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range chars {
|
||||
if len(rosterNameBytes(c.Name)) > 29 {
|
||||
return wedding.Invalid
|
||||
}
|
||||
}
|
||||
for _, c := range chars {
|
||||
id, _ := strconv.ParseUint(c.CharacterID, 10, 16)
|
||||
identity, online := s.onlineGameSessionCharacterSnapshot(uint16(id))
|
||||
if online {
|
||||
err = errors.Join(err, s.sendGameUpperRawClass(identity.session, currentWeddingReservationMsgID, weddingReservationBody(chars, state, uint32(state.ChannelID), uint16(cat.Ceremonies[state.Ceremony].Invitations)), 0))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) sendWeddingInventorySnapshot(session *gameSession, bag dnfrepo.InventoryRecord) error {
|
||||
if bag.CharacterID == "" {
|
||||
return nil
|
||||
}
|
||||
// Only main-bag contents changed; retain all other native containers and equipped objects.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
body, _, _, ok := s.buildCurrentItemListBodyForSession(ctx, session, 0)
|
||||
if !ok {
|
||||
return fmt.Errorf("wedding inventory snapshot missing")
|
||||
}
|
||||
return s.sendGameUpperRawClass(session, 13, body, 0)
|
||||
}
|
||||
|
||||
func (s *Service) scheduleWeddingCeremony(c dnfrepo.CharacterRecord) error {
|
||||
state, err := wedding.Read(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.Status != 2 && state.Status != 3 {
|
||||
return nil
|
||||
}
|
||||
cat, err := s.currentWeddingCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
at := state.StartAt
|
||||
if state.Status == 3 {
|
||||
at += cat.EndSeconds
|
||||
}
|
||||
key := fmt.Sprintf("dnf-wedding:%d:%d", state.PairID, state.StartAt)
|
||||
return s.ensureGameplayTimeQueue().ScheduleAfter(key, max(time.Duration(0), time.Unix(at, 0).Sub(s.gameplayNow())), func(time.Time) {
|
||||
if err := s.advanceWeddingCeremony(c.CharacterID, state.PairID, state.StartAt); err != nil {
|
||||
s.logWarn("wedding ceremony deferred", "character_id", c.CharacterID, "error", err)
|
||||
// A full bag or transient DB failure leaves the stage intact; retry without charging again.
|
||||
_ = s.ensureGameplayTimeQueue().ScheduleAfter(key, 30*time.Second, func(time.Time) {
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Character == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
fresh, found, e := repos.Character.Load(ctx, c.CharacterID)
|
||||
if e == nil && found {
|
||||
_ = s.scheduleWeddingCeremony(fresh)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) advanceWeddingCeremony(characterID string, pair uint32, start int64) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
s.weddingMu.Lock()
|
||||
defer s.weddingMu.Unlock()
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Character == nil {
|
||||
return dnfrepo.ErrRepoMissing
|
||||
}
|
||||
c, found, err := repos.Character.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return wedding.Missing
|
||||
}
|
||||
state, err := wedding.Read(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.PairID != pair || state.StartAt != start || (state.Status != 2 && state.Status != 3) {
|
||||
return nil
|
||||
}
|
||||
cat, err := s.currentWeddingCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := wedding.Command{ActorID: characterID, PartnerID: strconv.Itoa(int(state.PartnerID)), AccountID: c.AccountID, Operation: 3, Now: s.gameplayNow()}
|
||||
if state.Status == 3 {
|
||||
cmd.Operation = 4
|
||||
cmd.Grant, err = s.weddingGiftAllocator(cat, state.Ceremony)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result, err := wedding.Commit(ctx, repos, cat, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var publishErr error
|
||||
if cmd.Operation == 3 {
|
||||
publishErr = s.publishWeddingCommit(result, 3)
|
||||
for _, peer := range s.weddingCeremonyPeers(state) {
|
||||
var w packetWriter
|
||||
w.writeUint32(pair)
|
||||
w.writeUint32(uint32(state.Room))
|
||||
for _, character := range result.Characters {
|
||||
name := rosterNameBytes(character.Name)
|
||||
w.writeBytes(name)
|
||||
w.writeZero(30 - len(name))
|
||||
}
|
||||
publishErr = errors.Join(publishErr, s.sendGameUpperRawClass(peer, currentWeddingStartMsgID, w.bytes(), 0))
|
||||
}
|
||||
return errors.Join(publishErr, s.scheduleWeddingCeremony(result.Characters[0]))
|
||||
}
|
||||
publishErr = s.publishWeddingCommit(result, 4)
|
||||
for i, character := range result.Characters {
|
||||
id, _ := strconv.ParseUint(character.CharacterID, 10, 16)
|
||||
if identity, online := s.onlineGameSessionCharacterSnapshot(uint16(id)); online {
|
||||
publishErr = errors.Join(publishErr, s.sendWeddingInventorySnapshot(identity.session, result.Inventories[i]))
|
||||
}
|
||||
}
|
||||
// sub_1837CC0 end notification removes the ceremony object and invokes the native exit route.
|
||||
for _, peer := range s.weddingCeremonyPeers(state) {
|
||||
var w packetWriter
|
||||
w.writeByte(1)
|
||||
w.writeByte(state.Ceremony)
|
||||
for _, character := range result.Characters {
|
||||
name := rosterNameBytes(character.Name)
|
||||
w.writeBytes(name)
|
||||
w.writeZero(30 - len(name))
|
||||
}
|
||||
w.writeUint32(uint32(state.Room))
|
||||
w.writeByte(state.Ceremony)
|
||||
w.writeUint32(uint32(peer.channel.ID))
|
||||
publishErr = errors.Join(publishErr, s.sendGameUpperRawClass(peer, currentWeddingEndMsgID, w.bytes(), 0))
|
||||
}
|
||||
return publishErr
|
||||
}
|
||||
|
||||
func (s *Service) weddingCeremonyPeers(state wedding.State) []*gameSession {
|
||||
if s.onlinePlayers == nil {
|
||||
return nil
|
||||
}
|
||||
var peers []*gameSession
|
||||
for _, p := range s.onlinePlayers.GetAreaInstancePlayers(state.ChannelID, 39, state.Room, state.PairID) {
|
||||
if p.Session != nil {
|
||||
peers = append(peers, p.Session)
|
||||
}
|
||||
}
|
||||
return peers
|
||||
}
|
||||
|
||||
func (s *Service) handleWeddingEnter(session *gameSession, body []byte) error {
|
||||
if len(body) != 22 {
|
||||
return nil
|
||||
}
|
||||
identity, current := s.boundGameSessionCharacterSnapshot(session)
|
||||
if !current {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
c, err := s.weddingCharacter(ctx, identity)
|
||||
state, stateErr := wedding.Read(c)
|
||||
if err == nil {
|
||||
err = stateErr
|
||||
}
|
||||
if err == nil && (body[21] != 0 || state.Status != 2 || uint16(session.channel.ID) != state.ChannelID || binary.LittleEndian.Uint32(body[13:17]) != state.PairID || binary.LittleEndian.Uint32(body[17:21]) != uint32(state.Room) || !s.gameplayNow().Before(time.Unix(state.StartAt, 0))) {
|
||||
err = wedding.Error(-6)
|
||||
}
|
||||
cat, catErr := s.currentWeddingCatalog()
|
||||
if err == nil {
|
||||
err = catErr
|
||||
}
|
||||
if err == nil {
|
||||
index := 0
|
||||
if c.CharacterID > strconv.Itoa(int(state.PartnerID)) {
|
||||
index = 1
|
||||
}
|
||||
pos := cat.Positions[index]
|
||||
err = s.handleTownSetUserArea(session, buildCurrentSoloTeleportMoveBody(currentSoloTeleportRequest{TownID: 39, AreaID: state.Room, PositionX: int16(pos[0]), PositionY: int16(pos[1]), Direction: 5}, uint16(c.Stats["town_id"])))
|
||||
if err == nil {
|
||||
session.townMu.Lock()
|
||||
p := session.townPositionSnapshot
|
||||
session.townMu.Unlock()
|
||||
if p.TownID != 39 || p.AreaID != state.Room {
|
||||
err = wedding.Error(-6)
|
||||
}
|
||||
}
|
||||
}
|
||||
var code wedding.Error
|
||||
if err != nil && !errors.As(err, &code) {
|
||||
code = wedding.Error(-6)
|
||||
}
|
||||
var w packetWriter
|
||||
w.writeInt32(int(code))
|
||||
return s.sendGameUpperSuccess(session, 1039, w.bytes())
|
||||
}
|
||||
Reference in new issue
Block a user