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,592 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const currentTownSetUserPositionBodySize = 7
|
||||
|
||||
var errCurrentDungeonTownReturnOriginUnavailable = errors.New("current dungeon town-return origin is unavailable")
|
||||
|
||||
// currentTownSetUserPositionRequest is the exact plaintext written by the
|
||||
// current EXE's sub_23E0660: coordinates, direction, then actor+0x748 scaled
|
||||
// by 100. That final u16 is the mover's current town movement rate, not an
|
||||
// opaque field: preserve it in op22 because sub_23D9DE0 applies it to peers.
|
||||
type currentTownSetUserPositionRequest struct {
|
||||
PositionX uint16
|
||||
PositionY uint16
|
||||
MovementCode byte
|
||||
OpaqueScaledU16 uint16
|
||||
}
|
||||
|
||||
// currentTownPositionSnapshot is session-owned. The repository remains the
|
||||
// durable area owner; high-frequency op35 reports are not written to the DB.
|
||||
// A snapshot can be reused only for the exact character/town/area scene that
|
||||
// established it.
|
||||
type currentTownPositionSnapshot struct {
|
||||
CharacterID uint16
|
||||
TownID byte
|
||||
AreaID byte
|
||||
PositionX uint16
|
||||
PositionY uint16
|
||||
MovementCode byte
|
||||
OpaqueScaledU16 uint16
|
||||
PositionValid bool
|
||||
}
|
||||
|
||||
func parseCurrentTownSetUserPositionRequest(body []byte) (currentTownSetUserPositionRequest, error) {
|
||||
if len(body) != currentTownSetUserPositionBodySize {
|
||||
return currentTownSetUserPositionRequest{}, fmt.Errorf(
|
||||
"current town op35 body length %d, want %d",
|
||||
len(body),
|
||||
currentTownSetUserPositionBodySize,
|
||||
)
|
||||
}
|
||||
return currentTownSetUserPositionRequest{
|
||||
PositionX: binary.LittleEndian.Uint16(body[0:2]),
|
||||
PositionY: binary.LittleEndian.Uint16(body[2:4]),
|
||||
MovementCode: body[4],
|
||||
OpaqueScaledU16: binary.LittleEndian.Uint16(body[5:7]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) handleTownSetUserPosition(session *gameSession, body []byte) error {
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
s.pvpMu.Lock()
|
||||
inAssault := session.assault != nil
|
||||
s.pvpMu.Unlock()
|
||||
if inAssault {
|
||||
return nil
|
||||
}
|
||||
request, err := parseCurrentTownSetUserPositionRequest(body)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-town-set-user-position-blocked",
|
||||
"body_len", len(body),
|
||||
"reason", "current_exe_op35_writer_boundary_mismatch",
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
characterID := session.selectedCharacterID
|
||||
if characterID == 0 {
|
||||
s.logTownSetUserPositionBlocked(session, request, "selected_character_missing")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.commitPendingDungeonReturnBeforeTownPosition(session, request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.dungeon.mu.Lock()
|
||||
runtime := session.dungeon.runtime
|
||||
if runtime != nil {
|
||||
runtime.actorPositionX = request.PositionX
|
||||
runtime.actorPositionY = request.PositionY
|
||||
runtime.actorPositionValid = true
|
||||
}
|
||||
session.dungeon.mu.Unlock()
|
||||
if runtime != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
session.townMu.Lock()
|
||||
readyCharacterID := session.townSceneReadyCharacterID
|
||||
snapshot := session.townPositionSnapshot
|
||||
if readyCharacterID == characterID && snapshot.CharacterID == characterID {
|
||||
snapshot.PositionX = request.PositionX
|
||||
snapshot.PositionY = request.PositionY
|
||||
snapshot.MovementCode = request.MovementCode
|
||||
snapshot.OpaqueScaledU16 = request.OpaqueScaledU16
|
||||
snapshot.PositionValid = true
|
||||
session.townPositionSnapshot = snapshot
|
||||
}
|
||||
session.townMu.Unlock()
|
||||
|
||||
if readyCharacterID != characterID {
|
||||
s.logTownSetUserPositionBlocked(session, request, "town_scene_player_state_not_finalized")
|
||||
return nil
|
||||
}
|
||||
if snapshot.CharacterID != characterID {
|
||||
s.logTownSetUserPositionBlocked(session, request, "town_scene_location_owner_missing")
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logGameEvent(session, "game-town-set-user-position-captured",
|
||||
"char_id", characterID,
|
||||
"town_id", snapshot.TownID,
|
||||
"area_id", snapshot.AreaID,
|
||||
"position_x", request.PositionX,
|
||||
"position_y", request.PositionY,
|
||||
"movement_code", request.MovementCode,
|
||||
"opaque_scaled_u16", request.OpaqueScaledU16,
|
||||
"persisted", false,
|
||||
"source", "current_exe_sub_23E0660_exact_plaintext")
|
||||
|
||||
// A town-side op35 with a confirmed position proves this member rebuilt its
|
||||
// town actor and resumed movement, so consume the back-to-village
|
||||
// selector-return marker here. Every party member sends this arrival op35
|
||||
// (a follower never walks to the gate and so never sends op15), which is the
|
||||
// only way currentPartyTownReturnSceneReady can pass for all of them and the
|
||||
// party co-presence can be rebuilt.
|
||||
if err := s.consumeBackToVillageEnterSelectFinalizerAfterTownPosition(
|
||||
session,
|
||||
"current_exe_op35_position_after_pending_town_return",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Broadcast position to other players in the same area.
|
||||
if s.onlinePlayers != nil {
|
||||
others := s.onlinePlayers.UpdatePosition(characterID, request.PositionX, request.PositionY)
|
||||
if len(others) > 0 {
|
||||
mover := &onlinePlayerInfo{
|
||||
CharacterID: characterID,
|
||||
ChannelID: townPresenceChannelID(session),
|
||||
TownID: snapshot.TownID,
|
||||
AreaID: snapshot.AreaID,
|
||||
PositionX: request.PositionX,
|
||||
PositionY: request.PositionY,
|
||||
Direction: request.MovementCode,
|
||||
Session: session,
|
||||
}
|
||||
s.broadcastTownPlayerMove(mover, request.OpaqueScaledU16, others)
|
||||
}
|
||||
}
|
||||
if err := s.reconcileCurrentTownNPCQuestMeetings(session, snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitPendingDungeonReturnBeforeTownPosition accepts the first town-side
|
||||
// op35 position report as proof that a previously written typed op24 return
|
||||
// was consumed by the client. Live Tower death logs show exactly this shape:
|
||||
// op40 -> op32 -> client 10s window -> server 12s watchdog -> op24, then the client sends op35 with the frozen
|
||||
// return coordinates. Blocking that op35 leaves the active dungeon runtime
|
||||
// attached forever and the corpse never reaches a usable town scene.
|
||||
func (s *Service) commitPendingDungeonReturnBeforeTownPosition(
|
||||
session *gameSession,
|
||||
request currentTownSetUserPositionRequest,
|
||||
) error {
|
||||
if session == nil || session.selectedCharacterID == 0 {
|
||||
return nil
|
||||
}
|
||||
characterID := session.selectedCharacterID
|
||||
|
||||
session.dungeon.mu.Lock()
|
||||
runtime := session.dungeon.runtime
|
||||
shouldCommit := runtime != nil && runtime.Session != nil &&
|
||||
runtime.townReturnPending && runtime.townReturnOp24Sent
|
||||
transition := currentDungeonTownTransition{}
|
||||
if shouldCommit {
|
||||
transition = cloneCurrentDungeonTownTransition(runtime.townReturnTransition)
|
||||
}
|
||||
session.dungeon.mu.Unlock()
|
||||
if !shouldCommit {
|
||||
// The runtime is detached before the staged town actor/HUD chain is
|
||||
// written. If an earlier confirmation reached that boundary but a
|
||||
// later mode0/mode1/creature/skill write failed, op35 is the next
|
||||
// current-client town proof and must resume only the unfinished suffix.
|
||||
return s.ensureCurrentConfirmedDungeonReturnPlayerState(
|
||||
session,
|
||||
"current_exe_op35_position_without_retained_runtime",
|
||||
)
|
||||
}
|
||||
|
||||
if err := s.commitPendingDungeonReturnForSceneRequest(session, "current_exe_op35_position_after_pending_town_return"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.dungeon.mu.Lock()
|
||||
stillHasRuntime := session.dungeon.runtime != nil
|
||||
session.dungeon.mu.Unlock()
|
||||
if stillHasRuntime || session.selectedCharacterID != characterID {
|
||||
s.logGameEvent(session, "game-town-set-user-position-pending-return-committed",
|
||||
"char_id", characterID,
|
||||
"selected_character", session.selectedCharacterID,
|
||||
"runtime_retained", stillHasRuntime,
|
||||
"reason", "pending_return_committed_but_scene_owner_changed")
|
||||
return nil
|
||||
}
|
||||
|
||||
session.townMu.Lock()
|
||||
clearCurrentTownSelectorOriginLocked(session)
|
||||
session.townSceneReadyCharacterID = characterID
|
||||
session.townPositionSnapshot = currentTownPositionSnapshot{
|
||||
CharacterID: characterID,
|
||||
TownID: transition.TownID,
|
||||
AreaID: transition.AreaID,
|
||||
PositionX: request.PositionX,
|
||||
PositionY: request.PositionY,
|
||||
MovementCode: request.MovementCode,
|
||||
OpaqueScaledU16: request.OpaqueScaledU16,
|
||||
PositionValid: true,
|
||||
}
|
||||
session.townMu.Unlock()
|
||||
|
||||
s.logGameEvent(session, "game-town-set-user-position-pending-return-committed",
|
||||
"char_id", characterID,
|
||||
"town_id", transition.TownID,
|
||||
"area_id", transition.AreaID,
|
||||
"position_x", request.PositionX,
|
||||
"position_y", request.PositionY,
|
||||
"movement_code", request.MovementCode,
|
||||
"opaque_scaled_u16", request.OpaqueScaledU16,
|
||||
"position_source", transition.PositionSource,
|
||||
"reason", "op35_is_first_town_side_confirmation_after_typed_op24_return")
|
||||
return nil
|
||||
}
|
||||
|
||||
// setCurrentTownPositionSceneLocked establishes the exact town scene that may
|
||||
// own later op35 reports. The caller must hold session.townMu. A scene change
|
||||
// invalidates the previous coordinate report even when the same character is
|
||||
// moving between two areas of the same town.
|
||||
func setCurrentTownPositionSceneLocked(session *gameSession, characterID uint16, townID, areaID byte) {
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
clearCurrentTownSelectorOriginLocked(session)
|
||||
session.townMeetingNearbyNPCs = nil
|
||||
session.townPositionSnapshot = currentTownPositionSnapshot{
|
||||
CharacterID: characterID,
|
||||
TownID: townID,
|
||||
AreaID: areaID,
|
||||
}
|
||||
}
|
||||
|
||||
// setCurrentTownPositionSceneFromTransitionLocked seeds the position owner
|
||||
// from the exact typed op24 actor row. Character switching can open the
|
||||
// dungeon selector before the first client op35 movement report; retaining an
|
||||
// invalid position in that window acknowledges op15 but cannot build op27,
|
||||
// leaving the selector blank until a teleport supplies a later op35. The
|
||||
// transition row is already the authoritative spawn used by the current
|
||||
// client, so it is safe to bind immediately when it belongs to this actor.
|
||||
func setCurrentTownPositionSceneFromTransitionLocked(session *gameSession, characterID uint16, townID, areaID byte, row currentSceneTransitionRow) {
|
||||
setCurrentTownPositionSceneLocked(session, characterID, townID, areaID)
|
||||
if session == nil || characterID == 0 || row.ObjectOrResourceKey != currentSceneActorObjectKey(characterID) {
|
||||
return
|
||||
}
|
||||
session.townPositionSnapshot.PositionX = row.Value1
|
||||
session.townPositionSnapshot.PositionY = row.Value2
|
||||
session.townPositionSnapshot.MovementCode = row.Value3
|
||||
session.townPositionSnapshot.PositionValid = true
|
||||
}
|
||||
|
||||
// bindCurrentTownSelectorOrigin freezes the town origin owned by the exact
|
||||
// op15/op27 selector context. A duplicate op15 keeps the first binding. This
|
||||
// prevents a later town-position report from silently changing where op132
|
||||
// returns, while the same frozen snapshot remains available after op16 has
|
||||
// committed an active dungeon runtime.
|
||||
func bindCurrentTownSelectorOrigin(session *gameSession) (currentTownPositionSnapshot, bool) {
|
||||
if session == nil || session.selectedCharacterID == 0 {
|
||||
return currentTownPositionSnapshot{}, false
|
||||
}
|
||||
characterID := session.selectedCharacterID
|
||||
session.townMu.Lock()
|
||||
defer session.townMu.Unlock()
|
||||
if session.townSelectorOriginBound {
|
||||
if session.townSelectorOriginSnapshot.CharacterID == characterID {
|
||||
return session.townSelectorOriginSnapshot, true
|
||||
}
|
||||
clearCurrentTownSelectorOriginLocked(session)
|
||||
}
|
||||
snapshot := session.townPositionSnapshot
|
||||
if session.townSceneReadyCharacterID != characterID || snapshot.CharacterID != characterID || !snapshot.PositionValid {
|
||||
return currentTownPositionSnapshot{}, false
|
||||
}
|
||||
session.townSelectorOriginSnapshot = snapshot
|
||||
session.townSelectorOriginBound = true
|
||||
return snapshot, true
|
||||
}
|
||||
|
||||
// freezeCurrentDungeonTownReturnOrigin copies the exact op35 origin into an
|
||||
// ordinary dungeon runtime before that runtime becomes visible. Tutorial
|
||||
// dungeons deliberately retain their independent persisted birth-town return
|
||||
// owner because a new character can enter them before any town op35 exists.
|
||||
func freezeCurrentDungeonTownReturnOrigin(session *gameSession, runtime *runtimeDungeonState) error {
|
||||
if session == nil || runtime == nil || runtime.Session == nil {
|
||||
return fmt.Errorf("%w: runtime owner missing", errCurrentDungeonTownReturnOriginUnavailable)
|
||||
}
|
||||
if isPVFTutorialDungeon(runtime) {
|
||||
runtime.townReturnOrigin = currentTownPositionSnapshot{}
|
||||
return nil
|
||||
}
|
||||
characterID, townID, areaID, err := currentDungeonTownReturnOriginOwner(session, runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.townMu.Lock()
|
||||
origin := session.townSelectorOriginSnapshot
|
||||
bound := session.townSelectorOriginBound
|
||||
session.townMu.Unlock()
|
||||
if !bound {
|
||||
// The current client's direct-entry buttons (training room 5000 and the
|
||||
// job-change trial experience 7142) can be re-entered immediately after
|
||||
// a town return: the op15 back-to-village finalizer consumed the
|
||||
// selector context (clearing the bound origin) before the next op16
|
||||
// SELECT_DUNGEON arrives. The post-return town position snapshot is
|
||||
// still valid and is exactly where the character stands, so freeze it
|
||||
// as the return origin instead of blocking the direct entry (which the
|
||||
// client experiences as a silent hang followed by a crash).
|
||||
if isCurrentDirectDungeonEntryRequest(runtime.Request) &&
|
||||
origin.CharacterID == characterID && origin.PositionValid {
|
||||
bound = true
|
||||
}
|
||||
}
|
||||
if !bound {
|
||||
return fmt.Errorf("%w: selector origin not bound", errCurrentDungeonTownReturnOriginUnavailable)
|
||||
}
|
||||
if err := validateCurrentDungeonTownReturnOrigin(origin, characterID, townID, areaID); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.townReturnOrigin = origin
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentDungeonTownReturnOriginOwner resolves the scene that owns the frozen
|
||||
// selector origin. Ordinary dungeons use the durable character location. An
|
||||
// Anton raid dungeon is entered from the session-scoped waiting area instead;
|
||||
// that temporary 19/2 scene is deliberately never persisted over the
|
||||
// character's ordinary town. Both freeze and commit must use this same owner,
|
||||
// otherwise commit reinterprets 19/2 as a mismatch against (for example) 38/1
|
||||
// after the request has already passed its raid-entry checks.
|
||||
func currentDungeonTownReturnOriginOwner(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) (uint16, byte, byte, error) {
|
||||
if session == nil || runtime == nil || runtime.Session == nil {
|
||||
return 0, 0, 0, fmt.Errorf("%w: runtime owner missing", errCurrentDungeonTownReturnOriginUnavailable)
|
||||
}
|
||||
characterID := session.selectedCharacterID
|
||||
if characterID == 0 || !dungeonRuntimeOwnsCharacter(runtime, characterID) {
|
||||
return 0, 0, 0, fmt.Errorf("%w: runtime character owner mismatch", errCurrentDungeonTownReturnOriginUnavailable)
|
||||
}
|
||||
if spec, ok := currentRaidTownSpecForSession(session); ok && isAntonRaidEntryDungeonID(runtime.Dungeon.ID) {
|
||||
// 超时空的返回点不是固定等待室,而是这个队伍此刻所在的作战区域。
|
||||
// 它必须与 freeze/commit 两侧校验用的 selector origin 是同一个值,否则
|
||||
// 进图会被 ordinary_dungeon_town_return_origin_unavailable 静默拒绝。
|
||||
//
|
||||
// 实测 2026-10-01: 队伍走到 area 9 后每秒重试一次开战、每次 entered=false,
|
||||
// 客户端只看到"队伍已解散"反复刷屏 —— 就是因为这里返回了等待室 98/2,而
|
||||
// 会话绑定的 selector origin 是 98/9,两边对不上。
|
||||
if isCurrentFiendWarRaidChannel(session) {
|
||||
if area, ok := currentFiendWarOperationArea(session, characterID, spec, runtime); ok {
|
||||
return characterID, byte(spec.townID), area, nil
|
||||
}
|
||||
}
|
||||
return characterID, byte(spec.townID), spec.waitingArea, nil
|
||||
}
|
||||
townID, areaID, err := currentSceneTransitionLocation(runtime.Character, true)
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("%w: %v", errCurrentDungeonTownReturnOriginUnavailable, err)
|
||||
}
|
||||
return characterID, townID, areaID, nil
|
||||
}
|
||||
|
||||
// currentFiendWarOperationArea 给出超时空这次副本尝试的返回区域。
|
||||
//
|
||||
// 取值顺序:
|
||||
// 1. 副本运行时已记下的 origin —— 这个队伍已经从这里回过城一次;
|
||||
// 2. 会话已绑定的 selector origin —— 刚走进这个区域、还没回过城;
|
||||
// 3. 都没有(还在等待室) -> false,调用方退回等待室。
|
||||
//
|
||||
// 第 2 条是关键:遭遇是从城镇移动里发起的,那一刻 session.townPositionSnapshot
|
||||
// 还没有提交到新区域(切换包还没发),但 selector origin 已经在进入该区域时就绑好了,
|
||||
// 而 freeze/commit 的校验读的正是它。
|
||||
func currentFiendWarOperationArea(
|
||||
session *gameSession,
|
||||
characterID uint16,
|
||||
spec runtimeRaidTownSpec,
|
||||
runtime *runtimeDungeonState,
|
||||
) (byte, bool) {
|
||||
if runtime != nil {
|
||||
origin := runtime.townReturnOrigin
|
||||
if origin.CharacterID == characterID && origin.TownID == byte(spec.townID) && origin.AreaID >= 2 {
|
||||
return origin.AreaID, true
|
||||
}
|
||||
}
|
||||
if session == nil {
|
||||
return 0, false
|
||||
}
|
||||
session.townMu.Lock()
|
||||
selector := session.townSelectorOriginSnapshot
|
||||
bound := session.townSelectorOriginBound
|
||||
session.townMu.Unlock()
|
||||
if bound && selector.CharacterID == characterID &&
|
||||
selector.TownID == byte(spec.townID) && selector.AreaID >= 2 {
|
||||
return selector.AreaID, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func validateCurrentDungeonRuntimeTownReturnOrigin(session *gameSession, runtime *runtimeDungeonState) error {
|
||||
if isPVFTutorialDungeon(runtime) {
|
||||
return nil
|
||||
}
|
||||
characterID, townID, areaID, err := currentDungeonTownReturnOriginOwner(session, runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validateCurrentDungeonTownReturnOrigin(runtime.townReturnOrigin, characterID, townID, areaID)
|
||||
}
|
||||
|
||||
func validateCurrentDungeonTownReturnOrigin(
|
||||
origin currentTownPositionSnapshot,
|
||||
characterID uint16,
|
||||
townID byte,
|
||||
areaID byte,
|
||||
) error {
|
||||
if !origin.PositionValid {
|
||||
return fmt.Errorf("%w: op35 position was not reported", errCurrentDungeonTownReturnOriginUnavailable)
|
||||
}
|
||||
if origin.CharacterID != characterID || origin.TownID != townID || origin.AreaID != areaID {
|
||||
return fmt.Errorf(
|
||||
"%w: origin character/town/area %d/%d/%d does not match runtime %d/%d/%d",
|
||||
errCurrentDungeonTownReturnOriginUnavailable,
|
||||
origin.CharacterID,
|
||||
origin.TownID,
|
||||
origin.AreaID,
|
||||
characterID,
|
||||
townID,
|
||||
areaID,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearCurrentTownSelectorOrigin(session *gameSession) {
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
session.townMu.Lock()
|
||||
clearCurrentTownSelectorOriginLocked(session)
|
||||
session.townMu.Unlock()
|
||||
}
|
||||
|
||||
func clearCurrentTownSelectorOriginLocked(session *gameSession) {
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
session.townSelectorOriginSnapshot = currentTownPositionSnapshot{}
|
||||
session.townSelectorOriginBound = false
|
||||
}
|
||||
|
||||
func currentTownPositionForTransition(
|
||||
session *gameSession,
|
||||
characterID uint16,
|
||||
townID byte,
|
||||
areaID byte,
|
||||
) (currentTownPositionSnapshot, string, bool, error) {
|
||||
if session == nil || characterID == 0 {
|
||||
return currentTownPositionSnapshot{}, "", false, nil
|
||||
}
|
||||
session.townMu.Lock()
|
||||
selectorSnapshot := session.townSelectorOriginSnapshot
|
||||
selectorBound := session.townSelectorOriginBound
|
||||
session.townMu.Unlock()
|
||||
if selectorBound {
|
||||
if selectorSnapshot.CharacterID != characterID ||
|
||||
selectorSnapshot.TownID != townID ||
|
||||
selectorSnapshot.AreaID != areaID {
|
||||
return currentTownPositionSnapshot{}, "", false, fmt.Errorf(
|
||||
"selector origin character/town/area %d/%d/%d does not match transition %d/%d/%d",
|
||||
selectorSnapshot.CharacterID,
|
||||
selectorSnapshot.TownID,
|
||||
selectorSnapshot.AreaID,
|
||||
characterID,
|
||||
townID,
|
||||
areaID,
|
||||
)
|
||||
}
|
||||
if !selectorSnapshot.PositionValid {
|
||||
return currentTownPositionSnapshot{}, "", false, fmt.Errorf(
|
||||
"%w: bound selector origin has no op35 position",
|
||||
errCurrentDungeonTownReturnOriginUnavailable,
|
||||
)
|
||||
}
|
||||
return selectorSnapshot, "current_exe_op35_selector_origin_snapshot", true, nil
|
||||
}
|
||||
return currentTownPositionSnapshot{}, "", false, nil
|
||||
}
|
||||
|
||||
func (s *Service) applyCurrentTownPositionToTransition(
|
||||
session *gameSession,
|
||||
characterID uint16,
|
||||
transition currentDungeonTownTransition,
|
||||
) (currentDungeonTownTransition, bool, error) {
|
||||
snapshot, positionSource, ok, err := currentTownPositionForTransition(
|
||||
session,
|
||||
characterID,
|
||||
transition.TownID,
|
||||
transition.AreaID,
|
||||
)
|
||||
if err != nil {
|
||||
return currentDungeonTownTransition{}, false, err
|
||||
}
|
||||
if !ok {
|
||||
return transition, false, nil
|
||||
}
|
||||
transition, err = applyCurrentTownPositionSnapshotToTransition(
|
||||
transition,
|
||||
snapshot,
|
||||
characterID,
|
||||
positionSource,
|
||||
)
|
||||
if err != nil {
|
||||
return currentDungeonTownTransition{}, false, err
|
||||
}
|
||||
return transition, true, nil
|
||||
}
|
||||
|
||||
func applyCurrentTownPositionSnapshotToTransition(
|
||||
transition currentDungeonTownTransition,
|
||||
snapshot currentTownPositionSnapshot,
|
||||
characterID uint16,
|
||||
positionSource string,
|
||||
) (currentDungeonTownTransition, error) {
|
||||
if err := validateCurrentDungeonTownReturnOrigin(
|
||||
snapshot,
|
||||
characterID,
|
||||
transition.TownID,
|
||||
transition.AreaID,
|
||||
); err != nil {
|
||||
return currentDungeonTownTransition{}, err
|
||||
}
|
||||
row := currentSceneTransitionRow{
|
||||
ObjectOrResourceKey: transition.ActorObjectKey,
|
||||
Value1: snapshot.PositionX,
|
||||
Value2: snapshot.PositionY,
|
||||
Value3: transition.Direction,
|
||||
Value4: transition.AreaState,
|
||||
}
|
||||
body, err := buildCurrentSceneTransitionBody(
|
||||
transition.TownID,
|
||||
transition.AreaID,
|
||||
[]currentSceneTransitionRow{row},
|
||||
)
|
||||
if err != nil {
|
||||
return currentDungeonTownTransition{}, err
|
||||
}
|
||||
transition.PositionX = int16(snapshot.PositionX)
|
||||
transition.PositionY = int16(snapshot.PositionY)
|
||||
transition.PositionSource = positionSource
|
||||
transition.Body = body
|
||||
return transition, nil
|
||||
}
|
||||
|
||||
func (s *Service) logTownSetUserPositionBlocked(
|
||||
session *gameSession,
|
||||
request currentTownSetUserPositionRequest,
|
||||
reason string,
|
||||
) {
|
||||
s.logGameEvent(session, "game-town-set-user-position-blocked",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"position_x", request.PositionX,
|
||||
"position_y", request.PositionY,
|
||||
"movement_code", request.MovementCode,
|
||||
"opaque_scaled_u16", request.OpaqueScaledU16,
|
||||
"reason", reason)
|
||||
}
|
||||
Reference in new issue
Block a user