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,172 @@
|
||||
package dnfbridge
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
const (
|
||||
// These are current notification IDs, not CmdPacket enum values. The
|
||||
// generated Go enum only contains the C2S SET_USER_POSITION/AREA commands.
|
||||
// Current NoPack registers the matching S2C readers at sub_1D83990 and
|
||||
// sub_1D89590 respectively.
|
||||
currentTownUserPositionNotificationMsgID uint16 = 0x0016
|
||||
currentTownUserAreaNotificationMsgID uint16 = 0x0017
|
||||
|
||||
// sub_1D83990 passes this initial movement-rate field to the created town
|
||||
// actor. The same-client Python implementation sends 100 here; it is a
|
||||
// protocol initialization value, not a character movement-stat substitute.
|
||||
// It is only correct for the one-shot self position that constructs the
|
||||
// local scene actor. Any notification describing a character that is
|
||||
// actually walking must carry that character's reported op35 rate instead;
|
||||
// see buildCurrentTownUserPositionNotificationBody.
|
||||
currentTownInitialMovementRate uint16 = 100
|
||||
)
|
||||
|
||||
// buildCurrentTownUserPositionNotificationBody matches current NoPack
|
||||
// class0/noti0x16 -> sub_1D83990:
|
||||
//
|
||||
// u16 actor_key, u16 x, u16 y, u8 direction, u16 movement_rate
|
||||
//
|
||||
// movementRate is not a constant. sub_1D83990 hands the final u16 to the peer
|
||||
// client's town-actor movement owner (sub_23D9DE0), which is the only thing
|
||||
// that tells a remote client how fast the actor is walking. The mover reports
|
||||
// its own current rate in every op35, so a broadcast that sends a fixed 100
|
||||
// animates a boosted character at ordinary speed on every other screen.
|
||||
func buildCurrentTownUserPositionNotificationBody(
|
||||
actorKey uint16,
|
||||
x uint16,
|
||||
y uint16,
|
||||
direction byte,
|
||||
movementRate uint16,
|
||||
) []byte {
|
||||
body := make([]byte, 9)
|
||||
binary.LittleEndian.PutUint16(body[0:2], actorKey)
|
||||
binary.LittleEndian.PutUint16(body[2:4], x)
|
||||
binary.LittleEndian.PutUint16(body[4:6], y)
|
||||
body[6] = direction
|
||||
binary.LittleEndian.PutUint16(body[7:9], movementRate)
|
||||
return body
|
||||
}
|
||||
|
||||
// buildCurrentTownUserAreaNotificationBody matches current NoPack
|
||||
// class0/noti0x17 -> sub_1D89590:
|
||||
//
|
||||
// u16 actor_key, u8 town, u8 area, u16 x, u16 y, u8 direction, u8 state_bits
|
||||
//
|
||||
// sub_1D89590 passes the final two bytes to sub_23DBA50 in that order:
|
||||
// offset+6 is switched as the 2/3/4/6/8/9 direction enum, while offset+7 is
|
||||
// consumed as a bit field. Swapping them corrupts the selected town actor's
|
||||
// movement state and prevents the camera from following it.
|
||||
func buildCurrentTownUserAreaNotificationBody(
|
||||
actorKey uint16,
|
||||
townID byte,
|
||||
areaID byte,
|
||||
x uint16,
|
||||
y uint16,
|
||||
direction byte,
|
||||
stateBits byte,
|
||||
) []byte {
|
||||
body := make([]byte, 10)
|
||||
binary.LittleEndian.PutUint16(body[0:2], actorKey)
|
||||
body[2] = townID
|
||||
body[3] = areaID
|
||||
binary.LittleEndian.PutUint16(body[4:6], x)
|
||||
binary.LittleEndian.PutUint16(body[6:8], y)
|
||||
body[8] = direction
|
||||
body[9] = stateBits
|
||||
return body
|
||||
}
|
||||
|
||||
// sendCurrentTownLocationNotifications publishes the current EXE's local
|
||||
// position/area pair. Op23 is not redundant with the following op24: its
|
||||
// local-actor branch stores the active town/area/coordinates and rebinds the
|
||||
// controlled scene actor before AREA_USERS applies the area roster.
|
||||
func (s *Service) sendCurrentTownLocationNotifications(
|
||||
session *gameSession,
|
||||
characterID uint16,
|
||||
townID byte,
|
||||
areaID byte,
|
||||
row currentSceneTransitionRow,
|
||||
source string,
|
||||
) error {
|
||||
actorKey := currentSceneActorObjectKey(characterID)
|
||||
positionBody := buildCurrentTownUserPositionNotificationBody(
|
||||
actorKey,
|
||||
row.Value1,
|
||||
row.Value2,
|
||||
row.Value3,
|
||||
// This is the scene-construction position of the receiving client's own
|
||||
// actor, sent before that client has reported any op35, so the protocol
|
||||
// initialization value is the only rate available here.
|
||||
currentTownInitialMovementRate,
|
||||
)
|
||||
if err := s.sendCurrentSceneFixedClass0Packet(
|
||||
session,
|
||||
currentTownUserPositionNotificationMsgID,
|
||||
positionBody,
|
||||
source+"_user_position_current_exe_sub_1D83990",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
areaBody := buildCurrentTownUserAreaNotificationBody(
|
||||
actorKey,
|
||||
townID,
|
||||
areaID,
|
||||
row.Value1,
|
||||
row.Value2,
|
||||
row.Value3,
|
||||
row.Value4,
|
||||
)
|
||||
if err := s.sendCurrentSceneFixedClass0Packet(
|
||||
session,
|
||||
currentTownUserAreaNotificationMsgID,
|
||||
areaBody,
|
||||
source+"_user_area_current_exe_sub_1D89590",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendCurrentInitialTownLocationNotificationsLocked publishes the selected
|
||||
// actor's validated final-channel location after mode1/scene binding and before
|
||||
// the large player-state bootstrap and existing AREA_USERS/op24 snapshot.
|
||||
// The fixed16 zero-tail envelope is the same current scene envelope used by
|
||||
// the working Python service and by Go's repository-backed ITEM_LIST packets.
|
||||
// The caller holds session.townMu.
|
||||
func (s *Service) sendCurrentInitialTownLocationNotificationsLocked(
|
||||
session *gameSession,
|
||||
characterID uint16,
|
||||
townID byte,
|
||||
areaID byte,
|
||||
row currentSceneTransitionRow,
|
||||
) error {
|
||||
if session == nil || characterID == 0 ||
|
||||
session.initialTownRouteCharacterID != characterID ||
|
||||
session.initialTownRouteStage < currentInitialTownRouteActorBound ||
|
||||
!session.initialTownActorSceneSnapshotSent ||
|
||||
session.initialTownLocationNotificationsSent {
|
||||
return nil
|
||||
}
|
||||
actorKey := currentSceneActorObjectKey(characterID)
|
||||
if err := s.sendCurrentTownLocationNotifications(
|
||||
session,
|
||||
characterID,
|
||||
townID,
|
||||
areaID,
|
||||
row,
|
||||
"initial_town_location",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
session.initialTownLocationNotificationsSent = true
|
||||
s.logGameEvent(session, "game-initial-town-location-notifications-sent",
|
||||
"char_id", characterID,
|
||||
"actor_object_key", actorKey,
|
||||
"town_id", townID,
|
||||
"area_id", areaID,
|
||||
"position_x", row.Value1,
|
||||
"position_y", row.Value2,
|
||||
"direction", row.Value3,
|
||||
"area_state", row.Value4,
|
||||
"sequence", "noti16_user_position_then_noti17_user_area_then_current_op24_area_users")
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user