Files
90CNServer/server/internal/services/dnfbridge/initial_town_location_notifications.go
T
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

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
}