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,840 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
dnfmonster "longheng.io/server/internal/modules/dnf/monster"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
var (
|
||||
errDungeonMonsterIDInvalid = errors.New("dnf dungeon monster id is invalid")
|
||||
errDungeonMonsterDefinitionMiss = errors.New("dnf dungeon monster definition is missing")
|
||||
errDungeonMonsterObjectKeyRange = errors.New("dnf dungeon monster object key exceeds current EXE range")
|
||||
errDungeonMonsterObjectNotFound = errors.New("dnf dungeon monster runtime object is not found")
|
||||
errDungeonMonsterAlreadyAnnounced = errors.New("dnf dungeon monster was already announced")
|
||||
errDungeonMonsterNotAnnounced = errors.New("dnf dungeon monster was not announced to the client")
|
||||
errDungeonMonsterAlreadyDefeated = errors.New("dnf dungeon monster was already defeated")
|
||||
errDungeonActorAlreadyAnnounced = errors.New("dnf dungeon actor was already announced")
|
||||
errDungeonActorOwnerConflict = errors.New("dnf dungeon actor owner conflicts with the room session")
|
||||
errDungeonActorNotHostile = errors.New("dnf dungeon actor is not a hostile room owner")
|
||||
errDungeonNonHostileRetirement = errors.New("dnf non-hostile AI actor retirement is not eligible")
|
||||
)
|
||||
|
||||
const firstDungeonMonsterObjectKey = uint32(currentSceneBootstrapObjectKey) + 1
|
||||
|
||||
type runtimeDungeonMonsterState string
|
||||
|
||||
const (
|
||||
runtimeDungeonMonsterPlanned runtimeDungeonMonsterState = "planned"
|
||||
runtimeDungeonMonsterAnnounced runtimeDungeonMonsterState = "announced"
|
||||
runtimeDungeonMonsterDefeated runtimeDungeonMonsterState = "defeated"
|
||||
)
|
||||
|
||||
type runtimeDungeonMonster struct {
|
||||
ObjectKey uint32
|
||||
Reference worldmap.HostileReference
|
||||
Spawn worldmap.MonsterSpawn
|
||||
Definition dnfmonster.Monster
|
||||
State runtimeDungeonMonsterState
|
||||
RoomClearSkipByte byte
|
||||
// Boss mutation is client-executed, but its one-shot lottery is owned by
|
||||
// the server. Keeping both bits on the announced room actor prevents a
|
||||
// client from retrying the 10% draw or replaying an approval in a later
|
||||
// room while leaving normal monster combat client-authoritative.
|
||||
BossMutationAttempted bool
|
||||
BossMutationAllowed bool
|
||||
}
|
||||
|
||||
type runtimeDungeonRoomSnapshot struct {
|
||||
Coordinate worldmap.RoomCoordinate
|
||||
MapID int64
|
||||
Monsters []runtimeDungeonMonster
|
||||
ExtendedActors []runtimeDungeonExtendedActor
|
||||
RetainedSpecialSpawns []runtimeDungeonRetainedSpecialSpawn
|
||||
Diagnostics []string
|
||||
OpaqueHostiles []worldmap.HostileReference
|
||||
SpecialObjectCount int
|
||||
AICharacterCount int
|
||||
EventMonsterConditionPublished bool
|
||||
}
|
||||
|
||||
type runtimeDungeonRoom struct {
|
||||
mu sync.RWMutex
|
||||
coordinate worldmap.RoomCoordinate
|
||||
mapID int64
|
||||
monsters []runtimeDungeonMonster
|
||||
byObjectKey map[uint32]int
|
||||
opaqueHostiles []worldmap.HostileReference
|
||||
extendedActors []runtimeDungeonExtendedActor
|
||||
extendedByObjectKey map[uint32]int
|
||||
retainedSpecialSpawns []runtimeDungeonRetainedSpecialSpawn
|
||||
diagnostics []string
|
||||
specialObjectCount int
|
||||
aiCharacterCount int
|
||||
eventMonsterConditionPublished bool
|
||||
}
|
||||
|
||||
func newRuntimeDungeonRoom(
|
||||
scene worldmap.DungeonRoomScene,
|
||||
catalog *pvfDungeonMonsterCatalog,
|
||||
firstObjectKey uint32,
|
||||
) (*runtimeDungeonRoom, uint32, error) {
|
||||
if catalog == nil {
|
||||
return nil, firstObjectKey, errDungeonMonsterCatalogUnavailable
|
||||
}
|
||||
room := &runtimeDungeonRoom{
|
||||
coordinate: scene.Coordinate,
|
||||
mapID: scene.Map.Map.ID,
|
||||
monsters: make([]runtimeDungeonMonster, 0, len(scene.Monsters)),
|
||||
byObjectKey: make(map[uint32]int, len(scene.Monsters)),
|
||||
}
|
||||
nextObjectKey := firstObjectKey
|
||||
for index, spawn := range scene.Monsters {
|
||||
if spawn.MonsterID <= 0 {
|
||||
return nil, firstObjectKey, fmt.Errorf(
|
||||
"%w: map=%d room=%s index=%d id=%d",
|
||||
errDungeonMonsterIDInvalid, room.mapID, room.coordinate, index, spawn.MonsterID,
|
||||
)
|
||||
}
|
||||
definition, ok, findErr := catalog.Find(spawn.MonsterID)
|
||||
if findErr != nil {
|
||||
return nil, firstObjectKey, fmt.Errorf(
|
||||
"resolve monster definition: map=%d room=%s index=%d id=%d: %w",
|
||||
room.mapID, room.coordinate, index, spawn.MonsterID, findErr,
|
||||
)
|
||||
}
|
||||
if !ok {
|
||||
return nil, firstObjectKey, fmt.Errorf(
|
||||
"%w: map=%d room=%s index=%d id=%d",
|
||||
errDungeonMonsterDefinitionMiss, room.mapID, room.coordinate, index, spawn.MonsterID,
|
||||
)
|
||||
}
|
||||
if nextObjectKey == 0 || nextObjectKey > uint32(^uint16(0)) {
|
||||
return nil, firstObjectKey, fmt.Errorf(
|
||||
"%w: map=%d room=%s index=%d object_key=%d",
|
||||
errDungeonMonsterObjectKeyRange, room.mapID, room.coordinate, index, nextObjectKey,
|
||||
)
|
||||
}
|
||||
reference := worldmap.HostileReference{Kind: worldmap.HostileMonster, Index: index}
|
||||
roomClearSkipByte := byte(0)
|
||||
// PVF [monster team] is positional to [monster]. Team 0 belongs to the
|
||||
// player/story side (quest 3344 Nonora in map 76329), so it must be
|
||||
// announced to the client as a non-room-clearing actor, matching the
|
||||
// server-side blocking set (worldmap monsterSpawnBlocksRoom) which already
|
||||
// excludes team 0. Missing team rows keep the historical hostile default.
|
||||
if index < len(scene.Map.Map.MonsterTeam) && scene.Map.Map.MonsterTeam[index] == 0 {
|
||||
roomClearSkipByte = 1
|
||||
}
|
||||
room.byObjectKey[nextObjectKey] = len(room.monsters)
|
||||
room.monsters = append(room.monsters, runtimeDungeonMonster{
|
||||
ObjectKey: nextObjectKey,
|
||||
Reference: reference,
|
||||
Spawn: spawn,
|
||||
Definition: definition,
|
||||
State: runtimeDungeonMonsterPlanned,
|
||||
RoomClearSkipByte: roomClearSkipByte,
|
||||
})
|
||||
nextObjectKey++
|
||||
}
|
||||
for _, reference := range scene.ExpectedHostiles {
|
||||
if reference.Kind != worldmap.HostileMonster {
|
||||
room.opaqueHostiles = append(room.opaqueHostiles, reference)
|
||||
}
|
||||
}
|
||||
return room, nextObjectKey, nil
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) MonsterCount() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.monsters)
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) OpaqueHostileCount() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.opaqueHostiles)
|
||||
}
|
||||
|
||||
// HostsRuntimeActor reports whether this room installed objectKey in either
|
||||
// runtime actor table. A code that the room actually installed is owned by the
|
||||
// monster/AI retirement paths, so the client-owned passive-object paths must
|
||||
// never claim it. A code with no runtime actor here can only be a PVF map
|
||||
// object the client spawned on its own.
|
||||
func (r *runtimeDungeonRoom) HostsRuntimeActor(objectKey uint32) bool {
|
||||
if r == nil || objectKey == 0 {
|
||||
return false
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if _, ok := r.byObjectKey[objectKey]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := r.extendedByObjectKey[objectKey]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) MarkMonsterNonBlocking(monsterIndex int) (runtimeDungeonMonster, error) {
|
||||
if r == nil {
|
||||
return runtimeDungeonMonster{}, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if monsterIndex < 0 || monsterIndex >= len(r.monsters) {
|
||||
return runtimeDungeonMonster{}, fmt.Errorf("%w: monster_index=%d", errDungeonMonsterObjectNotFound, monsterIndex)
|
||||
}
|
||||
r.monsters[monsterIndex].RoomClearSkipByte = 1
|
||||
return r.monsters[monsterIndex], nil
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) NonBlockingMonsterReferences() []worldmap.HostileReference {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]worldmap.HostileReference, 0)
|
||||
for _, monster := range r.monsters {
|
||||
if monster.RoomClearSkipByte != 0 {
|
||||
result = append(result, monster.Reference)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) Snapshot() runtimeDungeonRoomSnapshot {
|
||||
if r == nil {
|
||||
return runtimeDungeonRoomSnapshot{}
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return runtimeDungeonRoomSnapshot{
|
||||
Coordinate: r.coordinate,
|
||||
MapID: r.mapID,
|
||||
Monsters: cloneRuntimeDungeonMonsters(r.monsters),
|
||||
ExtendedActors: cloneRuntimeDungeonExtendedActors(r.extendedActors),
|
||||
RetainedSpecialSpawns: cloneRuntimeDungeonRetainedSpecialSpawns(r.retainedSpecialSpawns),
|
||||
Diagnostics: append([]string(nil), r.diagnostics...),
|
||||
OpaqueHostiles: append([]worldmap.HostileReference(nil), r.opaqueHostiles...),
|
||||
SpecialObjectCount: r.specialObjectCount,
|
||||
AICharacterCount: r.aiCharacterCount,
|
||||
EventMonsterConditionPublished: r.eventMonsterConditionPublished,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) eventMonsterConditionWasPublished() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.eventMonsterConditionPublished
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) markEventMonsterConditionPublished() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.eventMonsterConditionPublished = true
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// AnnouncedHostileAICharacterCode resolves the PVF AI-character identity
|
||||
// behind one current-room object key before its authoritative death is
|
||||
// committed. Quest [enemy reward item] rows use that AI character code with
|
||||
// enemy_type=2; the op39 object key is only a per-room runtime identity.
|
||||
func (r *runtimeDungeonRoom) AnnouncedHostileAICharacterCode(objectKey uint32) (int64, bool) {
|
||||
if r == nil || objectKey == 0 {
|
||||
return 0, false
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
index, found := r.extendedByObjectKey[objectKey]
|
||||
if !found || index < 0 || index >= len(r.extendedActors) {
|
||||
return 0, false
|
||||
}
|
||||
actor := r.extendedActors[index]
|
||||
if actor.Kind != runtimeDungeonActorAICharacter || actor.HostileReference == nil ||
|
||||
actor.State != runtimeDungeonMonsterAnnounced || actor.Packet.Code == 0 {
|
||||
return 0, false
|
||||
}
|
||||
if actor.AICharacter == nil || actor.AICharacter.Code != int64(actor.Packet.Code) {
|
||||
return 0, false
|
||||
}
|
||||
return actor.AICharacter.Code, true
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) AttachExtendedActors(plan runtimeDungeonExtendedActorPlan) error {
|
||||
if r == nil {
|
||||
return errDungeonWorldMapUnavailable
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
extendedByObjectKey := make(map[uint32]int, len(plan.Actors))
|
||||
handled := make(map[worldmap.HostileReference]struct{})
|
||||
for index, actor := range plan.Actors {
|
||||
if _, exists := r.byObjectKey[actor.ObjectKey]; exists {
|
||||
return fmt.Errorf("%w: object_key=%d normal_and_extended", errDungeonActorOwnerConflict, actor.ObjectKey)
|
||||
}
|
||||
if _, exists := extendedByObjectKey[actor.ObjectKey]; exists {
|
||||
return fmt.Errorf("%w: object_key=%d duplicate_extended", errDungeonActorOwnerConflict, actor.ObjectKey)
|
||||
}
|
||||
extendedByObjectKey[actor.ObjectKey] = index
|
||||
if actor.HostileReference != nil {
|
||||
handled[*actor.HostileReference] = struct{}{}
|
||||
}
|
||||
}
|
||||
remaining := make([]worldmap.HostileReference, 0, len(r.opaqueHostiles))
|
||||
for _, reference := range r.opaqueHostiles {
|
||||
if _, ok := handled[reference]; !ok {
|
||||
remaining = append(remaining, reference)
|
||||
}
|
||||
}
|
||||
r.extendedActors = cloneRuntimeDungeonExtendedActors(plan.Actors)
|
||||
r.extendedByObjectKey = extendedByObjectKey
|
||||
r.retainedSpecialSpawns = cloneRuntimeDungeonRetainedSpecialSpawns(plan.RetainedSpecialSpawns)
|
||||
r.diagnostics = append([]string(nil), plan.Diagnostics...)
|
||||
r.specialObjectCount = plan.SpecialObjectCount
|
||||
r.aiCharacterCount = plan.AICharacterCount
|
||||
r.opaqueHostiles = remaining
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) AnnounceAllActors(session *worldmap.DungeonSession) (int, error) {
|
||||
if r == nil {
|
||||
return 0, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
if session == nil {
|
||||
return 0, worldmap.ErrDungeonRunRequired
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
type binding struct {
|
||||
reference worldmap.HostileReference
|
||||
objectKey uint32
|
||||
}
|
||||
bindings := make([]binding, 0, len(r.monsters)+len(r.extendedActors))
|
||||
for _, monster := range r.monsters {
|
||||
if monster.State != runtimeDungeonMonsterPlanned {
|
||||
return 0, fmt.Errorf("%w: object_key=%d state=%s", errDungeonActorAlreadyAnnounced, monster.ObjectKey, monster.State)
|
||||
}
|
||||
bindings = append(bindings, binding{reference: monster.Reference, objectKey: monster.ObjectKey})
|
||||
}
|
||||
for _, actor := range r.extendedActors {
|
||||
if actor.State != runtimeDungeonMonsterPlanned {
|
||||
return 0, fmt.Errorf("%w: object_key=%d state=%s", errDungeonActorAlreadyAnnounced, actor.ObjectKey, actor.State)
|
||||
}
|
||||
if actor.HostileReference != nil {
|
||||
bindings = append(bindings, binding{reference: *actor.HostileReference, objectKey: actor.ObjectKey})
|
||||
}
|
||||
}
|
||||
|
||||
scene, ok := session.Scene()
|
||||
if !ok {
|
||||
return 0, worldmap.ErrDungeonRunRequired
|
||||
}
|
||||
seenReferences := make(map[worldmap.HostileReference]struct{}, len(bindings))
|
||||
seenObjectKeys := make(map[uint32]struct{}, len(bindings))
|
||||
for _, candidate := range bindings {
|
||||
if !runtimeSceneContainsHostile(scene.ExpectedHostiles, candidate.reference) {
|
||||
return 0, fmt.Errorf("%w: kind=%s index=%d object_key=%d reference_missing",
|
||||
errDungeonActorOwnerConflict,
|
||||
candidate.reference.Kind,
|
||||
candidate.reference.Index,
|
||||
candidate.objectKey,
|
||||
)
|
||||
}
|
||||
if _, exists := seenReferences[candidate.reference]; exists {
|
||||
return 0, fmt.Errorf("%w: kind=%s index=%d duplicate_reference",
|
||||
errDungeonActorOwnerConflict,
|
||||
candidate.reference.Kind,
|
||||
candidate.reference.Index,
|
||||
)
|
||||
}
|
||||
if _, exists := seenObjectKeys[candidate.objectKey]; exists {
|
||||
return 0, fmt.Errorf("%w: object_key=%d duplicate_key", errDungeonActorOwnerConflict, candidate.objectKey)
|
||||
}
|
||||
if previous, exists := scene.RuntimeObjects[candidate.objectKey]; exists {
|
||||
return 0, fmt.Errorf("%w: object_key=%d existing_kind=%s existing_index=%d",
|
||||
errDungeonActorOwnerConflict,
|
||||
candidate.objectKey,
|
||||
previous.Kind,
|
||||
previous.Index,
|
||||
)
|
||||
}
|
||||
for objectKey, previous := range scene.RuntimeObjects {
|
||||
if previous == candidate.reference {
|
||||
return 0, fmt.Errorf("%w: kind=%s index=%d existing_object_key=%d",
|
||||
errDungeonActorOwnerConflict,
|
||||
candidate.reference.Kind,
|
||||
candidate.reference.Index,
|
||||
objectKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
seenReferences[candidate.reference] = struct{}{}
|
||||
seenObjectKeys[candidate.objectKey] = struct{}{}
|
||||
}
|
||||
|
||||
for _, candidate := range bindings {
|
||||
if err := session.BindHostileObject(candidate.reference, candidate.objectKey); err != nil {
|
||||
return 0, fmt.Errorf("bind announced dungeon actor: %w", err)
|
||||
}
|
||||
}
|
||||
for index := range r.monsters {
|
||||
r.monsters[index].State = runtimeDungeonMonsterAnnounced
|
||||
}
|
||||
for index := range r.extendedActors {
|
||||
r.extendedActors[index].State = runtimeDungeonMonsterAnnounced
|
||||
}
|
||||
return len(r.monsters) + len(r.extendedActors), nil
|
||||
}
|
||||
|
||||
func runtimeSceneContainsHostile(values []worldmap.HostileReference, want worldmap.HostileReference) bool {
|
||||
for _, value := range values {
|
||||
if value == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) AnnounceMonster(objectKey uint32, session *worldmap.DungeonSession) error {
|
||||
if r == nil {
|
||||
return errDungeonMonsterObjectNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
index, ok := r.byObjectKey[objectKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: object_key=%d", errDungeonMonsterObjectNotFound, objectKey)
|
||||
}
|
||||
monster := &r.monsters[index]
|
||||
if monster.State != runtimeDungeonMonsterPlanned {
|
||||
return fmt.Errorf("%w: object_key=%d state=%s", errDungeonMonsterAlreadyAnnounced, objectKey, monster.State)
|
||||
}
|
||||
if session == nil {
|
||||
return worldmap.ErrDungeonRunRequired
|
||||
}
|
||||
if err := session.BindHostileObject(monster.Reference, objectKey); err != nil {
|
||||
return err
|
||||
}
|
||||
monster.State = runtimeDungeonMonsterAnnounced
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) ContainsAnnouncedActorObjectKey(objectKey uint32) bool {
|
||||
if r == nil || objectKey == 0 {
|
||||
return false
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if index, ok := r.byObjectKey[objectKey]; ok {
|
||||
return r.monsters[index].State != runtimeDungeonMonsterPlanned
|
||||
}
|
||||
if index, ok := r.extendedByObjectKey[objectKey]; ok {
|
||||
return r.extendedActors[index].State != runtimeDungeonMonsterPlanned
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) ContainsAnnouncedNonHostileAICharacterObjectKey(objectKey uint32) bool {
|
||||
if r == nil || objectKey == 0 {
|
||||
return false
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
index, ok := r.extendedByObjectKey[objectKey]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
actor := r.extendedActors[index]
|
||||
return actor.Kind == runtimeDungeonActorAICharacter &&
|
||||
actor.HostileReference == nil &&
|
||||
actor.State == runtimeDungeonMonsterAnnounced
|
||||
}
|
||||
|
||||
// AnnouncedMonster resolves only an ordinary PVF [monster] owned by this room
|
||||
// and only while it is in the announced state. Extended actors, planned
|
||||
// monsters, defeated monsters, and stale keys are intentionally excluded.
|
||||
func (r *runtimeDungeonRoom) AnnouncedMonster(objectKey uint32) (runtimeDungeonMonster, bool) {
|
||||
if r == nil || objectKey == 0 {
|
||||
return runtimeDungeonMonster{}, false
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
index, ok := r.byObjectKey[objectKey]
|
||||
if !ok || r.monsters[index].State != runtimeDungeonMonsterAnnounced {
|
||||
return runtimeDungeonMonster{}, false
|
||||
}
|
||||
return r.monsters[index], true
|
||||
}
|
||||
|
||||
// MonsterByReference resolves the PVF monster definition behind a room
|
||||
// HostileReference regardless of whether the live actor object is the ordinary
|
||||
// monster row or a current-EXE extended actor bound to the same reference. 86JP
|
||||
// performs kill reward/quest checks from the logical monster/enemy code after a
|
||||
// death event; callers still own the actor state transition before using this
|
||||
// helper for EXP, drops, or quest trigger inputs.
|
||||
func (r *runtimeDungeonRoom) MonsterByReference(
|
||||
reference worldmap.HostileReference,
|
||||
allowedStates ...runtimeDungeonMonsterState,
|
||||
) (runtimeDungeonMonster, bool) {
|
||||
if r == nil || reference.Kind != worldmap.HostileMonster || reference.Index < 0 {
|
||||
return runtimeDungeonMonster{}, false
|
||||
}
|
||||
allowed := make(map[runtimeDungeonMonsterState]struct{}, len(allowedStates))
|
||||
for _, state := range allowedStates {
|
||||
allowed[state] = struct{}{}
|
||||
}
|
||||
stateAllowed := func(state runtimeDungeonMonsterState) bool {
|
||||
if len(allowed) == 0 {
|
||||
return true
|
||||
}
|
||||
_, ok := allowed[state]
|
||||
return ok
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
for _, monster := range r.monsters {
|
||||
if monster.Reference == reference && stateAllowed(monster.State) {
|
||||
return monster, true
|
||||
}
|
||||
}
|
||||
return runtimeDungeonMonster{}, false
|
||||
}
|
||||
|
||||
type runtimeDungeonDeathTarget struct {
|
||||
ObjectKey uint32
|
||||
Reference worldmap.HostileReference
|
||||
ResponseKind currentDungeonDeathResponseKind
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) CommitActorDeathReport(
|
||||
objectKey uint32,
|
||||
session *worldmap.DungeonSession,
|
||||
) (runtimeDungeonDeathTarget, bool, error) {
|
||||
if r == nil {
|
||||
return runtimeDungeonDeathTarget{}, false, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
target, state, err := r.deathTargetLocked(objectKey)
|
||||
if err != nil {
|
||||
return runtimeDungeonDeathTarget{}, false, err
|
||||
}
|
||||
switch *state {
|
||||
case runtimeDungeonMonsterAnnounced:
|
||||
case runtimeDungeonMonsterDefeated:
|
||||
return runtimeDungeonDeathTarget{}, false, fmt.Errorf("%w: object_key=%d", errDungeonMonsterAlreadyDefeated, objectKey)
|
||||
default:
|
||||
return runtimeDungeonDeathTarget{}, false, fmt.Errorf("%w: object_key=%d state=%s", errDungeonMonsterNotAnnounced, objectKey, *state)
|
||||
}
|
||||
if session == nil {
|
||||
return runtimeDungeonDeathTarget{}, false, worldmap.ErrDungeonRunRequired
|
||||
}
|
||||
scene, ok := session.Scene()
|
||||
if !ok {
|
||||
return runtimeDungeonDeathTarget{}, false, worldmap.ErrDungeonRunRequired
|
||||
}
|
||||
reference, bound := scene.RuntimeObjects[objectKey]
|
||||
if !bound || reference != target.Reference {
|
||||
return runtimeDungeonDeathTarget{}, false, fmt.Errorf(
|
||||
"%w: object_key=%d expected=%s/%d actual=%s/%d bound=%t",
|
||||
errDungeonActorOwnerConflict,
|
||||
objectKey,
|
||||
target.Reference.Kind,
|
||||
target.Reference.Index,
|
||||
reference.Kind,
|
||||
reference.Index,
|
||||
bound,
|
||||
)
|
||||
}
|
||||
cleared, err := session.MarkHostileDefeated(objectKey)
|
||||
if err != nil {
|
||||
return runtimeDungeonDeathTarget{}, false, err
|
||||
}
|
||||
*state = runtimeDungeonMonsterDefeated
|
||||
return target, cleared, nil
|
||||
}
|
||||
|
||||
// CommitRemainingAnnouncedHostilesAfterBoss mirrors the 86JP dungeon-clear
|
||||
// domain rule used after a real boss death: the boss death is the authoritative
|
||||
// end of combat, so remaining already-announced hostile actors in the same
|
||||
// room must not keep the room/settlement chain stuck. It intentionally does
|
||||
// not fabricate a boss death; callers invoke it only after CommitActorDeathReport
|
||||
// accepted the boss actor's own op39.
|
||||
func (r *runtimeDungeonRoom) CommitRemainingAnnouncedHostilesAfterBoss(
|
||||
bossObjectKey uint32,
|
||||
session *worldmap.DungeonSession,
|
||||
) ([]runtimeDungeonDeathTarget, bool, error) {
|
||||
if r == nil {
|
||||
return nil, false, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
if session == nil {
|
||||
return nil, false, worldmap.ErrDungeonRunRequired
|
||||
}
|
||||
snapshot := r.Snapshot()
|
||||
var forced []runtimeDungeonDeathTarget
|
||||
cleared := false
|
||||
for _, monster := range snapshot.Monsters {
|
||||
if monster.ObjectKey == bossObjectKey || monster.State != runtimeDungeonMonsterAnnounced {
|
||||
continue
|
||||
}
|
||||
target, roomCleared, err := r.CommitActorDeathReport(monster.ObjectKey, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errDungeonMonsterAlreadyDefeated) || errors.Is(err, worldmap.ErrHostileAlreadyDefeated) {
|
||||
continue
|
||||
}
|
||||
return forced, cleared, err
|
||||
}
|
||||
forced = append(forced, target)
|
||||
cleared = cleared || roomCleared
|
||||
}
|
||||
for _, actor := range snapshot.ExtendedActors {
|
||||
if actor.ObjectKey == bossObjectKey || actor.State != runtimeDungeonMonsterAnnounced ||
|
||||
actor.HostileReference == nil {
|
||||
continue
|
||||
}
|
||||
target, roomCleared, err := r.CommitActorDeathReport(actor.ObjectKey, session)
|
||||
if err != nil {
|
||||
if errors.Is(err, errDungeonMonsterAlreadyDefeated) || errors.Is(err, worldmap.ErrHostileAlreadyDefeated) {
|
||||
continue
|
||||
}
|
||||
return forced, cleared, err
|
||||
}
|
||||
forced = append(forced, target)
|
||||
cleared = cleared || roomCleared
|
||||
}
|
||||
return forced, cleared, nil
|
||||
}
|
||||
|
||||
// RetireNonHostileAIActor marks an announced, non-blocking character AI as
|
||||
// retired without touching the hostile-object/session clear state. The
|
||||
// current client still reports this visual helper through DIE_MONSTER with
|
||||
// owner=0xffff, but it has no HostileReference and must not be committed via
|
||||
// MarkHostileDefeated. A repeated report for the same already-retired actor
|
||||
// returns the same target so the caller can safely replay the ACK.
|
||||
func (r *runtimeDungeonRoom) RetireNonHostileAIActor(
|
||||
objectKey uint32,
|
||||
) (runtimeDungeonDeathTarget, error) {
|
||||
if r == nil {
|
||||
return runtimeDungeonDeathTarget{}, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
index, ok := r.extendedByObjectKey[objectKey]
|
||||
if !ok {
|
||||
return runtimeDungeonDeathTarget{}, fmt.Errorf("%w: object_key=%d", errDungeonMonsterObjectNotFound, objectKey)
|
||||
}
|
||||
actor := &r.extendedActors[index]
|
||||
if actor.Kind != runtimeDungeonActorAICharacter || actor.HostileReference != nil || actor.Packet.Blocking != 0 {
|
||||
return runtimeDungeonDeathTarget{}, fmt.Errorf(
|
||||
"%w: object_key=%d kind=%s blocking=%d hostile_reference=%t",
|
||||
errDungeonNonHostileRetirement,
|
||||
objectKey,
|
||||
actor.Kind,
|
||||
actor.Packet.Blocking,
|
||||
actor.HostileReference != nil,
|
||||
)
|
||||
}
|
||||
if actor.State != runtimeDungeonMonsterAnnounced && actor.State != runtimeDungeonMonsterDefeated {
|
||||
return runtimeDungeonDeathTarget{}, fmt.Errorf(
|
||||
"%w: object_key=%d state=%s",
|
||||
errDungeonNonHostileRetirement,
|
||||
objectKey,
|
||||
actor.State,
|
||||
)
|
||||
}
|
||||
actor.State = runtimeDungeonMonsterDefeated
|
||||
return runtimeDungeonDeathTarget{
|
||||
ObjectKey: objectKey,
|
||||
ResponseKind: currentDungeonDeathResponseAICharacter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RetireSpecialMonsterActor acknowledges a PVF [special passive object] child
|
||||
// [monster] actor that the current client reports through DIE_MONSTER, but that
|
||||
// is intentionally not a hostile room owner. These actors are visual/runtime
|
||||
// children of a map object (for example enemies spawned out of a breakable
|
||||
// building), so retiring them must not call MarkHostileDefeated and must not
|
||||
// affect room-clear, EXP, drops, or quest kill accounting.
|
||||
func (r *runtimeDungeonRoom) RetireSpecialMonsterActor(
|
||||
objectKey uint32,
|
||||
) (runtimeDungeonDeathTarget, bool, error) {
|
||||
if r == nil {
|
||||
return runtimeDungeonDeathTarget{}, false, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
index, ok := r.extendedByObjectKey[objectKey]
|
||||
if !ok {
|
||||
return runtimeDungeonDeathTarget{}, false, nil
|
||||
}
|
||||
actor := &r.extendedActors[index]
|
||||
if actor.Kind != runtimeDungeonActorSpecialMonster {
|
||||
return runtimeDungeonDeathTarget{}, false, nil
|
||||
}
|
||||
if actor.HostileReference != nil {
|
||||
return runtimeDungeonDeathTarget{}, true, fmt.Errorf(
|
||||
"%w: object_key=%d kind=%s hostile_reference=%t",
|
||||
errDungeonNonHostileRetirement,
|
||||
objectKey,
|
||||
actor.Kind,
|
||||
true,
|
||||
)
|
||||
}
|
||||
if actor.State != runtimeDungeonMonsterAnnounced && actor.State != runtimeDungeonMonsterDefeated {
|
||||
return runtimeDungeonDeathTarget{}, true, fmt.Errorf(
|
||||
"%w: object_key=%d kind=%s state=%s",
|
||||
errDungeonNonHostileRetirement,
|
||||
objectKey,
|
||||
actor.Kind,
|
||||
actor.State,
|
||||
)
|
||||
}
|
||||
actor.State = runtimeDungeonMonsterDefeated
|
||||
return runtimeDungeonDeathTarget{
|
||||
ObjectKey: objectKey,
|
||||
ResponseKind: currentDungeonDeathResponseMonster,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// RetireTournamentActor mirrors an accepted tournament death in the room
|
||||
// actor table without affecting ordinary hostile-clear accounting.
|
||||
func (r *runtimeDungeonRoom) RetireTournamentActor(objectKey uint32, responseKind currentDungeonDeathResponseKind) (runtimeDungeonDeathTarget, error) {
|
||||
if r == nil {
|
||||
return runtimeDungeonDeathTarget{}, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
index, ok := r.extendedByObjectKey[objectKey]
|
||||
if !ok {
|
||||
return runtimeDungeonDeathTarget{}, fmt.Errorf("%w: object_key=%d", errDungeonMonsterObjectNotFound, objectKey)
|
||||
}
|
||||
actor := &r.extendedActors[index]
|
||||
if actor.Kind != runtimeDungeonActorTournament {
|
||||
return runtimeDungeonDeathTarget{}, fmt.Errorf("%w: object_key=%d kind=%s", errDungeonNonHostileRetirement, objectKey, actor.Kind)
|
||||
}
|
||||
if actor.State == runtimeDungeonMonsterDefeated {
|
||||
return runtimeDungeonDeathTarget{ObjectKey: objectKey, ResponseKind: responseKind}, nil
|
||||
}
|
||||
if actor.State != runtimeDungeonMonsterAnnounced {
|
||||
return runtimeDungeonDeathTarget{}, fmt.Errorf("%w: object_key=%d state=%s", errDungeonNonHostileRetirement, objectKey, actor.State)
|
||||
}
|
||||
actor.State = runtimeDungeonMonsterDefeated
|
||||
return runtimeDungeonDeathTarget{ObjectKey: objectKey, ResponseKind: responseKind}, nil
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) RetireHellPartyActor(objectKey uint32) (runtimeDungeonDeathTarget, bool, error) {
|
||||
if r == nil {
|
||||
return runtimeDungeonDeathTarget{}, false, errDungeonMonsterObjectNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
index, ok := r.extendedByObjectKey[objectKey]
|
||||
if !ok {
|
||||
return runtimeDungeonDeathTarget{}, false, nil
|
||||
}
|
||||
actor := &r.extendedActors[index]
|
||||
if actor.Kind != runtimeDungeonActorHellParty {
|
||||
return runtimeDungeonDeathTarget{}, false, nil
|
||||
}
|
||||
if actor.HostileReference != nil {
|
||||
return runtimeDungeonDeathTarget{}, true, fmt.Errorf("%w: hell actor object_key=%d", errDungeonActorOwnerConflict, objectKey)
|
||||
}
|
||||
if actor.State != runtimeDungeonMonsterAnnounced && actor.State != runtimeDungeonMonsterDefeated {
|
||||
return runtimeDungeonDeathTarget{}, true, fmt.Errorf("%w: hell actor object_key=%d state=%s", errDungeonNonHostileRetirement, objectKey, actor.State)
|
||||
}
|
||||
actor.State = runtimeDungeonMonsterDefeated
|
||||
responseKind := currentDungeonDeathResponseMonster
|
||||
if actor.Packet.Type >= 5 && actor.Packet.Type <= 8 {
|
||||
responseKind = currentDungeonDeathResponseAICharacter
|
||||
}
|
||||
return runtimeDungeonDeathTarget{ObjectKey: objectKey, ResponseKind: responseKind}, true, nil
|
||||
}
|
||||
|
||||
func (r *runtimeDungeonRoom) deathTargetLocked(
|
||||
objectKey uint32,
|
||||
) (runtimeDungeonDeathTarget, *runtimeDungeonMonsterState, error) {
|
||||
if index, ok := r.byObjectKey[objectKey]; ok {
|
||||
monster := &r.monsters[index]
|
||||
return runtimeDungeonDeathTarget{
|
||||
ObjectKey: objectKey,
|
||||
Reference: monster.Reference,
|
||||
ResponseKind: currentDungeonDeathResponseMonster,
|
||||
}, &monster.State, nil
|
||||
}
|
||||
index, ok := r.extendedByObjectKey[objectKey]
|
||||
if !ok {
|
||||
return runtimeDungeonDeathTarget{}, nil, fmt.Errorf("%w: object_key=%d", errDungeonMonsterObjectNotFound, objectKey)
|
||||
}
|
||||
actor := &r.extendedActors[index]
|
||||
if actor.HostileReference == nil {
|
||||
return runtimeDungeonDeathTarget{}, nil, fmt.Errorf("%w: object_key=%d kind=%s", errDungeonActorNotHostile, objectKey, actor.Kind)
|
||||
}
|
||||
responseKind := currentDungeonDeathResponseMonster
|
||||
if actor.Kind == runtimeDungeonActorAICharacter {
|
||||
responseKind = currentDungeonDeathResponseAICharacter
|
||||
}
|
||||
return runtimeDungeonDeathTarget{
|
||||
ObjectKey: objectKey,
|
||||
Reference: *actor.HostileReference,
|
||||
ResponseKind: responseKind,
|
||||
}, &actor.State, nil
|
||||
}
|
||||
|
||||
func cloneRuntimeDungeonMonsters(values []runtimeDungeonMonster) []runtimeDungeonMonster {
|
||||
out := make([]runtimeDungeonMonster, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = value
|
||||
out[index].Definition.Scalars = cloneFloatMap(value.Definition.Scalars)
|
||||
out[index].Definition.Sections = append([]string(nil), value.Definition.Sections...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneRuntimeDungeonExtendedActors(values []runtimeDungeonExtendedActor) []runtimeDungeonExtendedActor {
|
||||
out := make([]runtimeDungeonExtendedActor, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = value
|
||||
if value.HostileReference != nil {
|
||||
reference := *value.HostileReference
|
||||
out[index].HostileReference = &reference
|
||||
}
|
||||
if value.AICharacter != nil {
|
||||
out[index].AICharacter = cloneRuntimeDungeonAICharacter(*value.AICharacter)
|
||||
}
|
||||
if value.AICharacterMetadata != nil {
|
||||
definition := cloneDungeonAICharacterDefinition(*value.AICharacterMetadata)
|
||||
out[index].AICharacterMetadata = &definition
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneRuntimeDungeonRetainedSpecialSpawns(values []runtimeDungeonRetainedSpecialSpawn) []runtimeDungeonRetainedSpecialSpawn {
|
||||
return append([]runtimeDungeonRetainedSpecialSpawn(nil), values...)
|
||||
}
|
||||
|
||||
func cloneFloatMap(values map[string]float64) map[string]float64 {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]float64, len(values))
|
||||
for key, value := range values {
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user