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

107 lines
3.8 KiB
Go

package dnfbridge
import (
"math"
"strconv"
"sync"
"time"
dnfguild "longheng.io/server/internal/modules/dnf/guild"
)
type currentGuildBuff struct {
index uint32
expiresAt time.Time
}
type currentGuildBuffProjection struct {
mu sync.Mutex
characterID uint16
buffs [2]currentGuildBuff
}
// The caller has committed op24 and owns townMu. Recreate the guild buffs on
// dungeon return: sub_18ADDA0 updates parameters but retains the original tick.
func (s *Service) sendCurrentGuildBuffProjection(session *gameSession, source string) error {
guild, err := s.currentGuildForSelectedCharacter(session)
if err != nil {
return err
}
return s.writeCurrentGuildBuffProjection(session, guild, true, source)
}
// Called only after the selected actor and scene transition exist.
func (s *Service) writeCurrentGuildBuffProjection(session *gameSession, guild *dnfguild.Guild, force bool, source string) error {
if session == nil || session.selectedCharacterID == 0 {
return nil
}
now := s.gameplayNow().UTC()
var desired [2]currentGuildBuff
if guild != nil {
if _, member := guild.Members[strconv.Itoa(int(session.selectedCharacterID))]; member {
// Current Script.pvf: Guild_Status / Guild_Status1, both icon 206.
// Other content values (EXP percentage, teleport) are not buff IDs.
for i, kind := range [...]byte{3, 5} {
benefit, found := guild.Benefits[kind]
if found && benefit.Value == uint32(167+i*2) && benefit.Active(now) {
desired[i] = currentGuildBuff{index: benefit.Value, expiresAt: benefit.ExpiresAt}
}
}
}
}
state := &session.guildBuffProjection
state.mu.Lock()
defer state.mu.Unlock()
force = force || state.characterID != session.selectedCharacterID
// 2026-10-08:重放时**先把两份都移除、再按登录顺序把它们加回来**。
// 原先每个槽位是"移除 A→加 A→移除 B→加 B"交替下发;两个公会属性 buff 共用同一个
// 图标(206),交替 churn 之后客户端只剩最后加进去的那一个的加成效力 —— 现场就是
// "两个公会属性(限时167/永久169,各+60四维)进图回城后只回来 60 点"。
// 拆成两趟之后,添加顺序与登录时(167 然后 169,实测 +120 正常)完全一致。
var pending [2]currentGuildBuff
for i, buff := range desired {
previous := state.buffs[i]
if !force && previous.index == buff.index && previous.expiresAt.Equal(buff.expiresAt) {
pending[i] = previous
continue
}
if previous.index != 0 {
var body packetWriter
body.writeByte(1)
body.writeUint32(previous.index)
// sub_18AE7D0 removes this exact VariousBuff and its HUD entry.
if err := s.sendGameUpperRawClass(session, 488, body.bytes(), 0); err != nil {
return err
}
state.buffs[i] = currentGuildBuff{}
}
pending[i] = buff
}
for i, buff := range pending {
if buff.index == 0 {
continue
}
seconds := uint32(0)
if !buff.expiresAt.IsZero() {
seconds = uint32(math.Min(math.Ceil(buff.expiresAt.Sub(now).Seconds()), math.MaxInt32))
}
var body packetWriter
body.writeByte(1)
body.writeUint32(buff.index)
body.writeUint32(0) // expire_time parameter 0, as defined in PVF
body.writeUint32(seconds) // parameter 1; zero means permanent
body.writeUint32(0) // auxiliary value, unused by Guild_Status groups
// sub_18AE360 -> sub_18AD260 -> sub_18A9060 -> sub_1F8B060.
if err := s.sendGameUpperRawClass(session, 487, body.bytes(), 0); err != nil {
return err
}
state.buffs[i] = buff
s.logPacketEvent("game-guild-various-buff-send",
"conn_id", session.connID, "char_id", session.selectedCharacterID,
"source", source, "buff_index", buff.index, "remaining_seconds", seconds,
"sequence", "class0/op487-various-buff")
}
state.characterID = session.selectedCharacterID
return nil
}