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,183 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfguild "longheng.io/server/internal/modules/dnf/guild"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
// 公会基地 (guild agit) -- class1/op227/228/229/231.
|
||||
//
|
||||
// The current client drives the whole feature from four packets and turns every
|
||||
// refusal into a toast whose text comes from a per-opcode table:
|
||||
//
|
||||
// 227 check-create empty request
|
||||
// 228 create empty request (push 0xE4 at 026AE560)
|
||||
// 229 delete empty request (push 0xE5 at 026AE580)
|
||||
// 231 upgrade u8 target stage (push 0xE7 + sub_3454FC0 at 026AE5A3)
|
||||
//
|
||||
// Every reply is {u8 success, u8 code}: zero means failure and the code then
|
||||
// selects the toast; anything else is success. The handlers (01D0C040 /
|
||||
// 01D0C150 / 01D0C2B0 / 01D0C400) read nothing else on success -- their success
|
||||
// branches end in `pop ebp; ret` -- so {1} can never walk the client off the
|
||||
// packet, which is exactly what made the op747 stub crash. The failure codes
|
||||
// below are decoded from those handlers' jump tables (D:\re_tools\agit_codes.txt):
|
||||
//
|
||||
// 36 -> 829 您不是公会会长 (228, 231)
|
||||
// 100 -> 19140 您还未加入公会 (all four)
|
||||
// 181 -> 21492 已经生成公会基地。 (227, 228)
|
||||
// 182 -> 21496 公会基地不存在。 (229, 231)
|
||||
// 183 -> 6427 公会Lv不足 (227, 228)
|
||||
// 187 -> 21495 金币不足,无法创建公会基地。 (228) / 1660 金币不足 (231)
|
||||
// 188 -> 21508 公会基地升级Lv设置错误。 (231)
|
||||
const (
|
||||
currentGuildAgitErrorNotMember = byte(100)
|
||||
currentGuildAgitErrorNotMaster = byte(36)
|
||||
currentGuildAgitErrorExists = byte(181)
|
||||
currentGuildAgitErrorMissing = byte(182)
|
||||
currentGuildAgitErrorLevel = byte(183)
|
||||
currentGuildAgitErrorGold = byte(187)
|
||||
currentGuildAgitErrorStage = byte(188)
|
||||
)
|
||||
|
||||
func currentGuildAgitErrorCode(err error) byte {
|
||||
switch {
|
||||
case errors.Is(err, dnfguild.ErrPermissionDenied):
|
||||
return currentGuildAgitErrorNotMaster
|
||||
case errors.Is(err, dnfguild.ErrNotMember), errors.Is(err, dnfguild.ErrCharacterNotFound):
|
||||
return currentGuildAgitErrorNotMember
|
||||
case errors.Is(err, dnfguild.ErrBaseExists):
|
||||
return currentGuildAgitErrorExists
|
||||
case errors.Is(err, dnfguild.ErrBaseMissing):
|
||||
return currentGuildAgitErrorMissing
|
||||
case errors.Is(err, dnfguild.ErrBaseStage):
|
||||
return currentGuildAgitErrorStage
|
||||
default:
|
||||
return currentGuildAgitErrorNotMember
|
||||
}
|
||||
}
|
||||
|
||||
// memberGuild resolves the request's guild, returning ErrNotMember when the
|
||||
// selected character belongs to none.
|
||||
func memberGuild(registry *dnfguild.Registry, characterID string) (*dnfguild.Guild, error) {
|
||||
guild, _, found := registry.GuildForCharacter(characterID)
|
||||
if !found || guild == nil {
|
||||
return nil, dnfguild.ErrNotMember
|
||||
}
|
||||
return guild, nil
|
||||
}
|
||||
|
||||
// logCurrentGuildAgitRequest records every 公会基地 request with its body and the
|
||||
// stage the server held when it arrived, from inside the guild mutation lock. The
|
||||
// client drives the whole 公会基地 window from these four opcodes, so this line is
|
||||
// what tells us which button was pressed and what the server believed the stage
|
||||
// was. See the 2026-09-12 stage-display investigation in the project notes.
|
||||
func (s *Service) logCurrentGuildAgitRequest(session *gameSession, opcode uint16, body []byte, guild *dnfguild.Guild) {
|
||||
stage := byte(0)
|
||||
if guild != nil {
|
||||
stage = guild.BaseStage
|
||||
}
|
||||
s.logGameEvent(session, "game-guild-base-request",
|
||||
"opcode", opcode, "body", hex.EncodeToString(body), "stage", stage)
|
||||
}
|
||||
|
||||
// handleCurrentGuildAgitCheck answers 228's precondition probe. The client uses
|
||||
// only its failure branch, so a member whose guild may place a base gets success.
|
||||
func (s *Service) handleCurrentGuildAgitCheck(session *gameSession, body []byte) error {
|
||||
opcode := uint16(dnfenum.CmdPacketCheckCreateGuildAgit)
|
||||
err := s.mutateCurrentGuildAgit(session, func(registry *dnfguild.Registry, characterID string) error {
|
||||
guild, err := memberGuild(registry, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logCurrentGuildAgitRequest(session, opcode, body, guild)
|
||||
if guild.BaseStage != 0 {
|
||||
return dnfguild.ErrBaseExists
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, opcode, currentGuildAgitErrorCode(err))
|
||||
}
|
||||
return s.sendGameUpperSuccess(session, opcode, nil)
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentGuildAgitCreate(session *gameSession, body []byte) error {
|
||||
opcode := uint16(dnfenum.CmdPacketCreateGuildAgit)
|
||||
err := s.mutateCurrentGuildAgit(session, func(registry *dnfguild.Registry, characterID string) error {
|
||||
guild, err := memberGuild(registry, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logCurrentGuildAgitRequest(session, opcode, body, guild)
|
||||
return registry.CreateBase(characterID, s.gameplayNow())
|
||||
})
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, opcode, currentGuildAgitErrorCode(err))
|
||||
}
|
||||
s.logGameEvent(session, "game-guild-base-created", "char_id", session.selectedCharacterID)
|
||||
return s.sendGameUpperSuccess(session, opcode, nil)
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentGuildAgitDelete(session *gameSession, body []byte) error {
|
||||
opcode := uint16(dnfenum.CmdPacketDeleteGuildAgit)
|
||||
err := s.mutateCurrentGuildAgit(session, func(registry *dnfguild.Registry, characterID string) error {
|
||||
guild, err := memberGuild(registry, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logCurrentGuildAgitRequest(session, opcode, body, guild)
|
||||
return registry.DeleteBase(characterID, s.gameplayNow())
|
||||
})
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, opcode, currentGuildAgitErrorCode(err))
|
||||
}
|
||||
s.logGameEvent(session, "game-guild-base-deleted", "char_id", session.selectedCharacterID)
|
||||
return s.sendGameUpperSuccess(session, opcode, nil)
|
||||
}
|
||||
|
||||
// handleCurrentGuildAgitUpgrade consumes the single stage byte op231 carries.
|
||||
func (s *Service) handleCurrentGuildAgitUpgrade(session *gameSession, body []byte) error {
|
||||
opcode := uint16(dnfenum.CmdPacketUpgradeGuildAgit)
|
||||
if len(body) != 1 {
|
||||
return s.sendGameUpperFailure(session, opcode, currentGuildAgitErrorStage)
|
||||
}
|
||||
stage := body[0]
|
||||
err := s.mutateCurrentGuildAgit(session, func(registry *dnfguild.Registry, characterID string) error {
|
||||
guild, err := memberGuild(registry, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logCurrentGuildAgitRequest(session, opcode, body, guild)
|
||||
return registry.UpgradeBase(characterID, stage, s.gameplayNow())
|
||||
})
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, opcode, currentGuildAgitErrorCode(err))
|
||||
}
|
||||
s.logGameEvent(session, "game-guild-base-upgraded",
|
||||
"char_id", session.selectedCharacterID, "stage", stage)
|
||||
return s.sendGameUpperSuccess(session, opcode, nil)
|
||||
}
|
||||
|
||||
// mutateCurrentGuildAgit runs one guild mutation for the request's selected
|
||||
// character. These handlers are request-driven, so taking guildMu here is the
|
||||
// same pattern the other guild mutators use -- and, unlike the online publish
|
||||
// path, this goroutine cannot already hold it.
|
||||
func (s *Service) mutateCurrentGuildAgit(
|
||||
session *gameSession,
|
||||
apply func(registry *dnfguild.Registry, characterID string) error,
|
||||
) error {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 {
|
||||
return dnfguild.ErrCharacterNotFound
|
||||
}
|
||||
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
||||
ctx, cancel := s.currentGuildContext()
|
||||
defer cancel()
|
||||
return s.mutateCurrentGuild(ctx, func(_ dnfrepo.Group, registry *dnfguild.Registry) error {
|
||||
return apply(registry, characterID)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user