服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
173 lines
5.7 KiB
Go
173 lines
5.7 KiB
Go
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
|
|
}
|