DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,120 @@
package dnfbridge
import (
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnftown "longheng.io/server/internal/modules/dnf/town"
)
// currentWeddingRoomTownID 是婚房城镇号(PVF:town/coupleroom.twn)。
const currentWeddingRoomTownID = 101
// 本文件在工作区的等价实现说明
// ---------------------------------------------------------------------------
// 上游自己遍历 dnftown.MovableArea 矩形、靠 Params 认出"通往 101/0 的门"再算落点。
// 本工作区的 MovableArea 只有 X/Y/Width/Height,没有 Params,所以照搬不了。
// 工作区的既有做法(SeriaRoomReturn)是**在装载期**把门边安全落点推导好,
// 本文件沿用同一套路:town/wedding_room_return.go 在装载期算出
// Area.WeddingRoomReturn,运行时直接取用。
//
// 为什么必须落在赛丽亚房间(38/1)而不是城镇居民区(38/0):婚房是从赛丽亚房间那扇
// 门进去的,原生就是回到赛丽亚房间;而且 WeddingRoomReturn 正是挂在 38/1 上
// (那扇 `49 224 34 52 101 0` 的门所在的地图)。
//
// 落点语义与上游一致:必须落在传送门触发矩形之外,否则客户端的重叠 latch 保持
// 武装,会把角色立刻送回婚房。townPortalReturnPoint 还额外要求落点位于
// [virtual movable area] 内,比上游更严。
// normalizeCurrentWeddingRoomExit 把"从婚房(101/0)走左门出来"的原生 op36
// 归一化为"回到赛丽亚房间(38/1)那扇婚房门旁边的安全落点"。
//
// 上游注释记录的 EXE 行为:0x3410D33 的 -101 分支用 0x223CC20 记住的入口房间,
// 或在该记忆为空时回退 Underfoot 2/5;两条路径都以 (300,200)、方向 5 调用
// 0x3406950。所以这里只认这个精确形状;其它传送/驿站选择继续走原有路线。
func (s *Service) normalizeCurrentWeddingRoomExit(
session *gameSession, character dnfrepo.CharacterRecord, request currentTownSetUserAreaRequest,
) (currentTownSetUserAreaRequest, bool, error) {
if character.Stats["town_id"] != currentWeddingRoomTownID || character.Stats["area_id"] != 0 ||
!request.LooksLikeTownCrossTownPortalSelection(currentWeddingRoomTownID) ||
request.PositionX != 300 || request.PositionY != 200 || request.Direction != 5 {
return request, false, nil
}
// 场景快照对不上时**放行原始请求**,不报错。
//
// 教训(2026-10-04 真机):这里早先返回 error,调用方 handleTownSetUserArea
// 会直接中止整个移动请求 —— 一旦装载期没能推导出落点(例如地图只有 `(r)`
// 修订版、原图缺失),玩家就会被**永久困在婚房里出不去**。
// 把玩家困住远比落点不精确糟糕:客户端自己请求的就是赛丽亚房间的
// (300,200),那本身就是一个合法的安全点。
if snapshot := session.townPositionSnapshot; snapshot.CharacterID != 0 &&
(snapshot.CharacterID != session.selectedCharacterID || snapshot.TownID != currentWeddingRoomTownID || snapshot.AreaID != 0) {
return request, false, nil
}
area, found := s.townArea(newCharacterInitialTownID, newCharacterInitialAreaID)
if !found {
return request, false, nil
}
point, ok := currentWeddingRoomExitLandingPoint(area)
if !ok {
return request, false, nil
}
request.TownID = newCharacterInitialTownID
request.AreaID = byte(area.ID)
request.PositionX = int16(point.X)
request.PositionY = int16(point.Y)
request.Direction = newCharacterInitialDirection
return request, true, nil
}
// currentWeddingRoomExitLandingPoint 挑一个"门外的安全落点"。
//
// 优先级:装载期推导出的 WeddingRoomReturn(婚房门旁,原生口径)→
// 同区域的 SeriaRoomReturn → 该地图自己的 [gate] 锚点。
// 三者都没有时返回 false,调用方会放行客户端的原始请求(宁可落点一般,
// 也绝不把玩家困在婚房里)。
func currentWeddingRoomExitLandingPoint(area dnftown.Area) (dnftown.Gate, bool) {
if area.ID < 0 || area.ID > int64(^byte(0)) {
return dnftown.Gate{}, false
}
for _, candidate := range []*dnftown.Gate{area.WeddingRoomReturn, area.SeriaRoomReturn, area.Gate} {
if candidate == nil {
continue
}
if candidate.X < 0 || candidate.X > int64(^uint16(0)) ||
candidate.Y < 0 || candidate.Y > int64(^uint16(0)) {
continue
}
return *candidate, true
}
return dnftown.Gate{}, false
}
// currentWeddingPublicReturnOrigin 只修复"被婚房污染过的回城点"。
//
// 注意它与上面那个函数取的是**不同**的锚点,别合并:
// - normalizeCurrentWeddingRoomExit 要送玩家回赛丽亚房间(38/1),所以用 38/1 的
// WeddingRoomReturn(婚房门旁)。
// - 本函数服务的是 op1425"回到上个城镇"。上游从当前城镇那扇"指向赛丽亚房间的
// 零尺寸反向传送门"取公共到达锚点,也就是**城镇居民区那一侧**(38/0),所以用
// townSeriaRoomReturnArea(38) 的 SeriaRoomReturn。用错一侧会把玩家塞进赛丽亚
// 房间而不是他要回的城镇。
func (s *Service) currentWeddingPublicReturnOrigin(characterID uint16) (currentTownPositionSnapshot, bool) {
if s == nil || characterID == 0 {
return currentTownPositionSnapshot{}, false
}
area, found := s.townSeriaRoomReturnArea(newCharacterInitialTownID)
if !found || area.SeriaRoomReturn == nil ||
area.ID < 0 || area.ID > int64(^byte(0)) ||
area.SeriaRoomReturn.X < 0 || area.SeriaRoomReturn.X > int64(^uint16(0)) ||
area.SeriaRoomReturn.Y < 0 || area.SeriaRoomReturn.Y > int64(^uint16(0)) {
return currentTownPositionSnapshot{}, false
}
return currentTownPositionSnapshot{
CharacterID: characterID,
TownID: byte(newCharacterInitialTownID),
AreaID: byte(area.ID),
PositionX: uint16(area.SeriaRoomReturn.X),
PositionY: uint16(area.SeriaRoomReturn.Y),
MovementCode: newCharacterInitialDirection,
PositionValid: true,
}, true
}