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,536 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
dnfcompanion "longheng.io/server/internal/modules/dnf/companion"
|
||||
dnfequip "longheng.io/server/internal/modules/dnf/equip"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
currentDungeonCompanionProjectionSourceEnv = "DNF_COMPANION_P0_SOURCE_CHARACTER_ID"
|
||||
currentDungeonCompanionProjectionOpcode = uint16(0x7F41)
|
||||
currentDungeonCompanionProjectionMagic = uint32(0x314A5250) // PRJ1
|
||||
currentDungeonCompanionProjectionVersion = byte(3)
|
||||
currentDungeonCompanionProjectionFlagPresent = byte(1)
|
||||
currentDungeonCompanionProjectionFlagAIPlan = byte(4)
|
||||
currentDungeonCompanionProjectionHeaderSize = 72
|
||||
currentDungeonCompanionProjectionMaxEquip = 32
|
||||
currentDungeonCompanionProjectionMaxSkills = 128
|
||||
currentDungeonCompanionProjectionMaxQuick = 16
|
||||
currentDungeonCompanionProjectionObjectKey = uint16(0xFFFF)
|
||||
currentDungeonCompanionMode0ObjectKeyOffset = 0x4C
|
||||
currentDungeonCompanionMode1ObjectKeyOffset = 0x15
|
||||
)
|
||||
|
||||
func currentDungeonCompanionProjectionSourceIDValue(value string) (uint16, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
parsed, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil || parsed == 0 || parsed >= uint64(^uint16(0)) ||
|
||||
strconv.FormatUint(parsed, 10) != value {
|
||||
return 0, false
|
||||
}
|
||||
return uint16(parsed), true
|
||||
}
|
||||
|
||||
type currentDungeonCompanionProjectionSkillPair struct {
|
||||
ID uint32
|
||||
Level uint32
|
||||
}
|
||||
|
||||
type currentDungeonCompanionProjectionAIPlan struct {
|
||||
Behavior byte
|
||||
ComboPresetHash byte
|
||||
ClassID uint16
|
||||
ComboEnabled bool
|
||||
}
|
||||
|
||||
func currentDungeonCompanionProjectionAIPlanFor(
|
||||
profile dnfcompanion.AIProfile,
|
||||
job byte,
|
||||
) currentDungeonCompanionProjectionAIPlan {
|
||||
profile = dnfcompanion.NormalizeAIProfile(profile)
|
||||
behavior := byte(1)
|
||||
switch profile.Behavior {
|
||||
case "aggressive":
|
||||
behavior = 2
|
||||
}
|
||||
classID := profile.ClassID
|
||||
if classID == 0 {
|
||||
classID = uint16(job)
|
||||
}
|
||||
// Keep the packet compact; the full preset name remains in GM settings.
|
||||
// The client needs a stable selector until the native action ABI is proved.
|
||||
var hash byte = 216
|
||||
for i := 0; i < len(profile.ComboPreset); i++ {
|
||||
hash ^= profile.ComboPreset[i]
|
||||
hash *= 167
|
||||
}
|
||||
return currentDungeonCompanionProjectionAIPlan{
|
||||
Behavior: behavior,
|
||||
ComboPresetHash: hash,
|
||||
ClassID: classID,
|
||||
ComboEnabled: profile.ComboEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func currentDungeonCompanionProjectionAIPlanWord(
|
||||
plan currentDungeonCompanionProjectionAIPlan,
|
||||
) uint32 {
|
||||
flags := plan.Behavior & 0x03
|
||||
if plan.ComboEnabled {
|
||||
flags |= 0x04
|
||||
}
|
||||
return uint32(flags) |
|
||||
uint32(plan.ComboPresetHash)<<8 |
|
||||
uint32(plan.ClassID)<<16
|
||||
}
|
||||
|
||||
func currentDungeonCompanionEquipmentIDs(
|
||||
record dnfrepo.EquipmentRecord,
|
||||
now time.Time,
|
||||
) ([]uint32, error) {
|
||||
if now.IsZero() {
|
||||
return nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
type equipmentItem struct {
|
||||
slot int16
|
||||
id uint32
|
||||
}
|
||||
equipmentRows := make([]equipmentItem, 0, len(record.Entries))
|
||||
slots := make(map[int16]struct{}, len(record.Entries))
|
||||
for _, entry := range record.Entries {
|
||||
if entry.SlotIndex < 0 || entry.SlotIndex > 255 ||
|
||||
entry.ItemID <= 0 || entry.ItemID > int64(^uint32(0)) {
|
||||
return nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
if _, exists := slots[entry.SlotIndex]; exists {
|
||||
return nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
slots[entry.SlotIndex] = struct{}{}
|
||||
if !entry.ExpireAt.IsZero() && !entry.ExpireAt.After(now) {
|
||||
continue
|
||||
}
|
||||
itemID := entry.ItemID
|
||||
if raw := strings.TrimSpace(entry.Extra[dnfequip.CloneAvatarDisplayItemIDExtraKey]); raw != "" {
|
||||
value, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || value <= 0 || value > int64(^uint32(0)) {
|
||||
return nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
itemID = value
|
||||
}
|
||||
equipmentRows = append(equipmentRows, equipmentItem{
|
||||
slot: entry.SlotIndex,
|
||||
id: uint32(itemID),
|
||||
})
|
||||
}
|
||||
sort.Slice(equipmentRows, func(i, j int) bool {
|
||||
if equipmentRows[i].slot != equipmentRows[j].slot {
|
||||
return equipmentRows[i].slot < equipmentRows[j].slot
|
||||
}
|
||||
return equipmentRows[i].id < equipmentRows[j].id
|
||||
})
|
||||
if len(equipmentRows) > currentDungeonCompanionProjectionMaxEquip {
|
||||
return nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
equipmentIDs := make([]uint32, len(equipmentRows))
|
||||
for i := range equipmentRows {
|
||||
equipmentIDs[i] = equipmentRows[i].id
|
||||
}
|
||||
return equipmentIDs, nil
|
||||
}
|
||||
|
||||
func buildCurrentDungeonCompanionProjectionInstanceState(
|
||||
source dnfrepo.CompanionSourceState,
|
||||
now time.Time,
|
||||
) ([]uint32, []currentDungeonCompanionProjectionSkillPair, []currentDungeonCompanionProjectionSkillPair, error) {
|
||||
if now.IsZero() || !source.HasEquipment || !source.HasSkill ||
|
||||
source.Equipment.CharacterID != source.Source.CharacterID ||
|
||||
source.Skill.CharacterID != source.Source.CharacterID {
|
||||
return nil, nil, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
|
||||
equipmentIDs, err := currentDungeonCompanionEquipmentIDs(
|
||||
source.Equipment, now)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
skills := make([]currentDungeonCompanionProjectionSkillPair, 0, len(source.Skill.Skills))
|
||||
for id, state := range source.Skill.Skills {
|
||||
if !state.Enabled || state.Level <= 0 {
|
||||
continue
|
||||
}
|
||||
if id <= 0 || id > int64(^uint32(0)) || state.Level > int(^uint16(0)) {
|
||||
return nil, nil, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
skills = append(skills, currentDungeonCompanionProjectionSkillPair{
|
||||
ID: uint32(id), Level: uint32(state.Level),
|
||||
})
|
||||
}
|
||||
sort.Slice(skills, func(i, j int) bool { return skills[i].ID < skills[j].ID })
|
||||
if len(skills) > currentDungeonCompanionProjectionMaxSkills {
|
||||
return nil, nil, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
|
||||
// The current EXE owns two six-slot quickbars in tree zero:
|
||||
// 0..5 => A/S/D/F/G/H
|
||||
// 198..203 => Q/W/E/R/T/Y
|
||||
// Preserve all 12 positions, including empty slots. Compressing the vector
|
||||
// would shift later keyboard bindings whenever a middle slot is empty.
|
||||
quickSlotOrder := [...]int{0, 1, 2, 3, 4, 5, 198, 199, 200, 201, 202, 203}
|
||||
quick := make([]currentDungeonCompanionProjectionSkillPair, len(quickSlotOrder))
|
||||
if layout := source.Skill.Layouts[0]; layout != nil {
|
||||
for index, slot := range quickSlotOrder {
|
||||
rawID, exists := layout[slot]
|
||||
if !exists || rawID == 0 {
|
||||
continue
|
||||
}
|
||||
id := uint32(rawID)
|
||||
state, ok := source.Skill.Skills[int64(id)]
|
||||
if !ok || !state.Enabled || state.Level <= 0 {
|
||||
continue
|
||||
}
|
||||
quick[index] = currentDungeonCompanionProjectionSkillPair{
|
||||
ID: id, Level: uint32(state.Level),
|
||||
}
|
||||
}
|
||||
}
|
||||
// An empty quickbar is authoritative. Do not fill it from the learned
|
||||
// skill vector: learned skills in the tree are not necessarily placed in a
|
||||
// quickbar, and doing so makes the companion cast skills the player did not
|
||||
// put in any of the twelve visible slots. Keep all twelve positions (and
|
||||
// their empty 0/0 pairs) so the client key-program indexes remain stable.
|
||||
if len(quick) > currentDungeonCompanionProjectionMaxQuick {
|
||||
return nil, nil, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
return equipmentIDs, skills, quick, nil
|
||||
}
|
||||
|
||||
func buildCurrentDungeonCompanionProjectionEnvelope(
|
||||
source dnfrepo.CompanionSourceState,
|
||||
selection dnfcompanion.Selection,
|
||||
now time.Time,
|
||||
options ...currentDungeonCompanionProjectionOptions,
|
||||
) (dnfcompanion.VisualProjection, []byte, error) {
|
||||
projection, err := dnfcompanion.BuildVisualProjection(source, selection, now)
|
||||
if err != nil {
|
||||
return dnfcompanion.VisualProjection{}, nil, err
|
||||
}
|
||||
var option currentDungeonCompanionProjectionOptions
|
||||
if len(options) > 0 {
|
||||
option = options[0]
|
||||
}
|
||||
profile := option.AIProfile
|
||||
if profile.Behavior == "" {
|
||||
profile = dnfcompanion.DefaultAIProfile(uint16(projection.Job))
|
||||
}
|
||||
if err := profile.Validate(); err != nil {
|
||||
return dnfcompanion.VisualProjection{}, nil, err
|
||||
}
|
||||
if option.EffectiveLevel > 0 {
|
||||
if option.EffectiveLevel > 90 {
|
||||
return dnfcompanion.VisualProjection{}, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
projection.Level = option.EffectiveLevel
|
||||
}
|
||||
character := dnfrepo.CloneCharacter(source.Source)
|
||||
if option.EffectiveLevel > 0 {
|
||||
character.Level = option.EffectiveLevel
|
||||
}
|
||||
equipment := dnfrepo.CloneEquipment(source.Equipment)
|
||||
rows, err := buildCurrentActorMode0AppearanceSummaryFromEquipment(equipment)
|
||||
if err != nil {
|
||||
return dnfcompanion.VisualProjection{}, nil, err
|
||||
}
|
||||
character.Roster.Entry.EquipSummary = cloneRosterEquipSummary(rows)
|
||||
|
||||
mode0 := buildCurrentSceneObjectListBodyInContext(
|
||||
currentDungeonCompanionProjectionObjectKey,
|
||||
character, true, character.Name, currentSceneObjectContext)
|
||||
// mode0 always owns source identity and complete current-EXE appearance.
|
||||
// Balanced mode optionally adds one mode1 stat blob after APC registration;
|
||||
// original mode keeps mode1 empty and therefore remains byte-compatible.
|
||||
mode1 := append([]byte(nil), option.Mode1...)
|
||||
if len(mode0) <= currentDungeonCompanionMode0ObjectKeyOffset+1 ||
|
||||
mode0[0] != 0 ||
|
||||
binary.LittleEndian.Uint16(
|
||||
mode0[currentDungeonCompanionMode0ObjectKeyOffset:currentDungeonCompanionMode0ObjectKeyOffset+2]) !=
|
||||
currentDungeonCompanionProjectionObjectKey {
|
||||
return dnfcompanion.VisualProjection{}, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
if len(mode1) != 0 &&
|
||||
(len(mode1) <= currentDungeonCompanionMode1ObjectKeyOffset+1 ||
|
||||
mode1[0] != 1 ||
|
||||
binary.LittleEndian.Uint16(
|
||||
mode1[currentDungeonCompanionMode1ObjectKeyOffset:currentDungeonCompanionMode1ObjectKeyOffset+2]) !=
|
||||
currentDungeonCompanionProjectionObjectKey) {
|
||||
return dnfcompanion.VisualProjection{}, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
|
||||
fingerprintHex := projection.Fingerprint()
|
||||
fingerprint, err := hex.DecodeString(fingerprintHex)
|
||||
if err != nil || len(fingerprint) != 32 {
|
||||
return dnfcompanion.VisualProjection{}, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
if option.EffectiveLevel > 0 || len(mode1) != 0 {
|
||||
hash := sha256.New()
|
||||
_, _ = hash.Write(fingerprint)
|
||||
_, _ = hash.Write([]byte{byte(option.EffectiveLevel), 1})
|
||||
_, _ = hash.Write(mode1)
|
||||
fingerprint = hash.Sum(nil)
|
||||
}
|
||||
revision := binary.LittleEndian.Uint64(fingerprint[:8])
|
||||
if revision == 0 {
|
||||
revision = 1
|
||||
}
|
||||
|
||||
nameUTF16 := utf16.Encode([]rune(projection.Name))
|
||||
if len(nameUTF16) == 0 || len(nameUTF16) > 32 {
|
||||
return dnfcompanion.VisualProjection{}, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
equipmentIDs, skills, quickSkills, err :=
|
||||
buildCurrentDungeonCompanionProjectionInstanceState(source, now)
|
||||
// These IDs populate the copied native AIC before APC construction. They
|
||||
// select the actual worn weapon and avatars, including clone appearances.
|
||||
// Use the frozen source/runtime equipment; substituting owner gear strips
|
||||
// source avatars and can give the APC a weapon for the wrong profession.
|
||||
if err != nil || projection.Level < 1 || projection.Level > 90 {
|
||||
return dnfcompanion.VisualProjection{}, nil, dnfcompanion.ErrSnapshot
|
||||
}
|
||||
dynamicBytes := len(nameUTF16)*2 +
|
||||
len(equipmentIDs)*4 +
|
||||
len(skills)*8 +
|
||||
len(quickSkills)*8
|
||||
body := make([]byte,
|
||||
currentDungeonCompanionProjectionHeaderSize+
|
||||
dynamicBytes+len(mode0)+len(mode1))
|
||||
binary.LittleEndian.PutUint32(
|
||||
body[0:4], currentDungeonCompanionProjectionMagic)
|
||||
body[4] = currentDungeonCompanionProjectionVersion
|
||||
body[5] = currentDungeonCompanionProjectionFlagPresent |
|
||||
currentDungeonCompanionProjectionFlagAIPlan
|
||||
binary.LittleEndian.PutUint16(body[6:8],
|
||||
uint16(mustProjectionCharacterID(projection.SourceCharacterID)))
|
||||
binary.LittleEndian.PutUint64(body[8:16], revision)
|
||||
binary.LittleEndian.PutUint32(body[16:20], uint32(len(mode0)))
|
||||
binary.LittleEndian.PutUint32(body[20:24], uint32(len(mode1)))
|
||||
copy(body[24:56], fingerprint)
|
||||
body[56] = projection.Job
|
||||
body[57] = projection.GrowType
|
||||
binary.LittleEndian.PutUint16(body[58:60], uint16(len(nameUTF16)))
|
||||
binary.LittleEndian.PutUint16(body[60:62], uint16(len(equipmentIDs)))
|
||||
binary.LittleEndian.PutUint16(body[62:64], uint16(projection.Level))
|
||||
binary.LittleEndian.PutUint16(body[64:66], uint16(len(skills)))
|
||||
binary.LittleEndian.PutUint16(body[66:68], uint16(len(quickSkills)))
|
||||
binary.LittleEndian.PutUint32(body[68:72],
|
||||
currentDungeonCompanionProjectionAIPlanWord(
|
||||
currentDungeonCompanionProjectionAIPlanFor(profile, projection.Job)))
|
||||
cursor := currentDungeonCompanionProjectionHeaderSize
|
||||
for _, unit := range nameUTF16 {
|
||||
binary.LittleEndian.PutUint16(body[cursor:cursor+2], unit)
|
||||
cursor += 2
|
||||
}
|
||||
for _, itemID := range equipmentIDs {
|
||||
binary.LittleEndian.PutUint32(body[cursor:cursor+4], itemID)
|
||||
cursor += 4
|
||||
}
|
||||
for _, skill := range skills {
|
||||
binary.LittleEndian.PutUint32(body[cursor:cursor+4], skill.ID)
|
||||
binary.LittleEndian.PutUint32(body[cursor+4:cursor+8], skill.Level)
|
||||
cursor += 8
|
||||
}
|
||||
for _, skill := range quickSkills {
|
||||
binary.LittleEndian.PutUint32(body[cursor:cursor+4], skill.ID)
|
||||
binary.LittleEndian.PutUint32(body[cursor+4:cursor+8], skill.Level)
|
||||
cursor += 8
|
||||
}
|
||||
copy(body[cursor:cursor+len(mode0)], mode0)
|
||||
cursor += len(mode0)
|
||||
copy(body[cursor:], mode1)
|
||||
return projection, body, nil
|
||||
}
|
||||
|
||||
func mustProjectionCharacterID(value string) uint64 {
|
||||
parsed, _ := strconv.ParseUint(value, 10, 16)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (s *Service) currentDungeonCompanionProjectionSourceID(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
repos dnfrepo.Group,
|
||||
) (uint16, string, bool, error) {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 {
|
||||
return 0, "", false, nil
|
||||
}
|
||||
ownerID := strconv.FormatUint(
|
||||
uint64(session.selectedCharacterID), 10)
|
||||
if repos.Settings != nil {
|
||||
scope, err := dnfcompanion.SettingsScope(ownerID)
|
||||
if err != nil {
|
||||
return 0, "settings", false, err
|
||||
}
|
||||
record, found, err := repos.Settings.Load(ctx, scope)
|
||||
if err != nil {
|
||||
return 0, "settings", false, err
|
||||
}
|
||||
if found {
|
||||
source, selected, err :=
|
||||
dnfcompanion.SourceCharacterIDFromSettings(
|
||||
record, ownerID)
|
||||
if err != nil {
|
||||
return 0, "settings", false, err
|
||||
}
|
||||
if !selected {
|
||||
return 0, "settings", false, nil
|
||||
}
|
||||
sourceID, ok :=
|
||||
currentDungeonCompanionProjectionSourceIDValue(source)
|
||||
if !ok {
|
||||
return 0, "settings", false,
|
||||
dnfcompanion.ErrSelection
|
||||
}
|
||||
return sourceID, "settings", true, nil
|
||||
}
|
||||
}
|
||||
|
||||
sourceID, enabled :=
|
||||
currentDungeonCompanionProjectionSourceIDValue(
|
||||
os.Getenv(
|
||||
currentDungeonCompanionProjectionSourceEnv))
|
||||
if !enabled {
|
||||
return 0, "", false, nil
|
||||
}
|
||||
return sourceID, "environment_fallback", true, nil
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentDungeonCompanionProjection(
|
||||
session *gameSession,
|
||||
source string,
|
||||
) error {
|
||||
if s == nil || session == nil ||
|
||||
currentDungeonCompanionP0CaptureBudget() == 0 ||
|
||||
session.selectedCharacterID == 0 {
|
||||
return nil
|
||||
}
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Character == nil {
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-skipped",
|
||||
"source", source,
|
||||
"reason", "repository_unavailable")
|
||||
return nil
|
||||
}
|
||||
selectionCtx, selectionCancel := context.WithTimeout(
|
||||
context.Background(), currentRepositorySnapshotTimeout)
|
||||
defer selectionCancel()
|
||||
sourceID, selectionSource, enabled, selectionErr :=
|
||||
s.currentDungeonCompanionProjectionSourceID(
|
||||
selectionCtx, session, repos)
|
||||
if selectionErr != nil {
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-skipped",
|
||||
"source", source,
|
||||
"selection_source", selectionSource,
|
||||
"reason", "source_selection_invalid",
|
||||
"error", selectionErr)
|
||||
return nil
|
||||
}
|
||||
if !enabled {
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-skipped",
|
||||
"source", source,
|
||||
"selection_source", selectionSource,
|
||||
"reason", "source_not_selected")
|
||||
return nil
|
||||
}
|
||||
if sourceID == session.selectedCharacterID {
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-skipped",
|
||||
"source", source, "source_character_id", sourceID,
|
||||
"selection_source", selectionSource,
|
||||
"reason", "source_is_owner")
|
||||
return nil
|
||||
}
|
||||
reader, ok := repos.Character.(dnfrepo.CompanionSourceReader)
|
||||
if !ok {
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-skipped",
|
||||
"source", source, "source_character_id", sourceID,
|
||||
"selection_source", selectionSource,
|
||||
"reason", "consistent_reader_unavailable")
|
||||
return nil
|
||||
}
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
if accountID == "" {
|
||||
return nil
|
||||
}
|
||||
_, sourceOnline := s.onlineGameSession(sourceID)
|
||||
ctx, cancel := context.WithTimeout(
|
||||
context.Background(), currentRepositorySnapshotTimeout)
|
||||
defer cancel()
|
||||
state, err := reader.ReadCompanionSource(
|
||||
ctx, accountID,
|
||||
strconv.FormatUint(uint64(session.selectedCharacterID), 10),
|
||||
strconv.FormatUint(uint64(sourceID), 10))
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-skipped",
|
||||
"source", source, "source_character_id", sourceID,
|
||||
"selection_source", selectionSource,
|
||||
"reason", "consistent_read_failed", "error", err)
|
||||
return nil
|
||||
}
|
||||
selection := dnfcompanion.Selection{
|
||||
AccountID: accountID,
|
||||
OwnerCharacterID: strconv.FormatUint(
|
||||
uint64(session.selectedCharacterID), 10),
|
||||
SourceCharacterID: strconv.FormatUint(
|
||||
uint64(sourceID), 10),
|
||||
OnlineStateKnown: true,
|
||||
SourceOnline: sourceOnline,
|
||||
}
|
||||
projection, body, err :=
|
||||
buildCurrentDungeonCompanionProjectionEnvelope(
|
||||
state, selection, s.gameplayNow())
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-skipped",
|
||||
"source", source, "source_character_id", sourceID,
|
||||
"selection_source", selectionSource,
|
||||
"source_online", sourceOnline,
|
||||
"reason", "projection_invalid", "error", err)
|
||||
return nil
|
||||
}
|
||||
weaponID := int64(0)
|
||||
if projection.Weapon != nil {
|
||||
weaponID = projection.Weapon.DisplayItemID
|
||||
}
|
||||
s.logGameEvent(session, "game-dungeon-companion-projection-send",
|
||||
"source", source,
|
||||
"selection_source", selectionSource,
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"source_character_id", sourceID,
|
||||
"name", projection.Name,
|
||||
"job", projection.Job, "grow_type", projection.GrowType,
|
||||
"level", projection.Level,
|
||||
"weapon_item_id", weaponID,
|
||||
"avatar_count", len(projection.Avatars),
|
||||
"aic_equipment_count", int(binary.LittleEndian.Uint16(body[60:62])),
|
||||
"aic_skill_count", int(binary.LittleEndian.Uint16(body[64:66])),
|
||||
"aic_quick_skill_count", int(binary.LittleEndian.Uint16(body[66:68])),
|
||||
"projection_hash", projection.Fingerprint(),
|
||||
"placeholder_object_key", currentDungeonCompanionProjectionObjectKey,
|
||||
"private_opcode", currentDungeonCompanionProjectionOpcode,
|
||||
"body_len", len(body),
|
||||
"template_mutation", false)
|
||||
return s.sendGameUpperRawClass(
|
||||
session, currentDungeonCompanionProjectionOpcode, body, 0)
|
||||
}
|
||||
Reference in new issue
Block a user