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,132 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const currentSoloTeleportRequestWireSize = 15
|
||||
|
||||
// currentSoloTeleportRequest is written by current NoPack.exe
|
||||
// SendSoloTeleportRequest470. The first two values are stable -1 in the live
|
||||
// personal-teleport samples, but their meaning is not required for routing.
|
||||
type currentSoloTeleportRequest struct {
|
||||
OpaqueI32A int32
|
||||
OpaqueI32B int32
|
||||
TownID byte
|
||||
AreaID byte
|
||||
PositionX int16
|
||||
PositionY int16
|
||||
Direction byte
|
||||
}
|
||||
|
||||
func parseCurrentSoloTeleportRequest(body []byte) (currentSoloTeleportRequest, error) {
|
||||
if len(body) != currentSoloTeleportRequestWireSize {
|
||||
return currentSoloTeleportRequest{}, fmt.Errorf(
|
||||
"current solo teleport op470 body length %d, want %d",
|
||||
len(body),
|
||||
currentSoloTeleportRequestWireSize,
|
||||
)
|
||||
}
|
||||
return currentSoloTeleportRequest{
|
||||
OpaqueI32A: int32(binary.LittleEndian.Uint32(body[0:4])),
|
||||
OpaqueI32B: int32(binary.LittleEndian.Uint32(body[4:8])),
|
||||
TownID: body[8],
|
||||
AreaID: body[9],
|
||||
PositionX: int16(binary.LittleEndian.Uint16(body[10:12])),
|
||||
PositionY: int16(binary.LittleEndian.Uint16(body[12:14])),
|
||||
Direction: body[14],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentSoloTeleport(session *gameSession, body []byte) error {
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
request, err := parseCurrentSoloTeleportRequest(body)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-town-solo-teleport-blocked",
|
||||
"body_len", len(body),
|
||||
"reason", "current_exe_op470_writer_boundary_mismatch",
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// op470 has a client-side failure callback, so a successful request must
|
||||
// advance through the established op24 scene-transition owner without an
|
||||
// op470 ACK. That owner also validates the target against the runtime PVF,
|
||||
// character level/quest state, active-dungeon state, and current town.
|
||||
s.logGameEvent(session, "game-town-solo-teleport-request",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"town_id", request.TownID,
|
||||
"area_id", request.AreaID,
|
||||
"position_x", request.PositionX,
|
||||
"position_y", request.PositionY,
|
||||
"direction", request.Direction,
|
||||
"opaque_i32_a", request.OpaqueI32A,
|
||||
"opaque_i32_b", request.OpaqueI32B)
|
||||
|
||||
// The personal teleport is a five-minute lock. The client refuses on its own
|
||||
// timer before writing op470; this gate is what makes the same rule hold for
|
||||
// a request that arrives anyway.
|
||||
if remaining, cooling := s.currentSoloTeleportCooldownRemaining(
|
||||
session.selectedCharacterID); cooling {
|
||||
s.refuseCurrentSoloTeleportDuringCooldown(session, request, remaining)
|
||||
return nil
|
||||
}
|
||||
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Character == nil || session.selectedCharacterID == 0 {
|
||||
s.logGameEvent(session, "game-town-solo-teleport-blocked",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"reason", "selected_character_location_unavailable")
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
characterID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
character, found, err := repositories.Character.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentTown, townFound := character.Stats["town_id"]
|
||||
if !found || character.Stats == nil || !townFound ||
|
||||
currentTown <= 0 || currentTown > int64(^uint16(0)) {
|
||||
s.logGameEvent(session, "game-town-solo-teleport-blocked",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"reason", "selected_character_town_id_missing_or_invalid")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Personal teleport is an explicit transport command, not an ordinary
|
||||
// op36 portal click. Preserve the persisted current town as the transport
|
||||
// source so the PVF-validated route can admit a cross-town destination
|
||||
// without weakening normal op36 movement checks.
|
||||
if err := s.handleTownSetUserArea(
|
||||
session,
|
||||
buildCurrentSoloTeleportMoveBody(request, uint16(currentTown)),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
// Arm the five-minute lock and publish its countdown, but only once the
|
||||
// destination is actually committed.
|
||||
s.armCurrentSoloTeleportCooldownAfterMove(session, request)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCurrentSoloTeleportMoveBody(
|
||||
request currentSoloTeleportRequest,
|
||||
sourceTownID uint16,
|
||||
) []byte {
|
||||
body := make([]byte, currentTownSetUserAreaBodySize)
|
||||
body[0] = request.TownID
|
||||
body[1] = request.AreaID
|
||||
binary.LittleEndian.PutUint16(body[2:4], uint16(request.PositionX))
|
||||
binary.LittleEndian.PutUint16(body[4:6], uint16(request.PositionY))
|
||||
body[6] = request.Direction
|
||||
binary.LittleEndian.PutUint16(body[7:9], sourceTownID)
|
||||
body[15] = 5
|
||||
return body
|
||||
}
|
||||
Reference in new issue
Block a user