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,277 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
dnfpvp "longheng.io/server/internal/modules/dnf/pvp"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
type currentPVPTotalTeam struct {
|
||||
snapshot dnfpvp.TotalTeamSnapshot
|
||||
actors [3][]byte
|
||||
rankPoint uint32
|
||||
ratingInitialized bool
|
||||
}
|
||||
|
||||
// sub_2003570 uses the durable UI slot, not the position in the skill list.
|
||||
// Slot 255 learns an unassigned skill without assigning a shortcut (268FA30).
|
||||
func buildCurrentPVPTotalSkills(record dnfrepo.SkillRecord, active byte) ([]byte, error) {
|
||||
state, found := dnfrepo.SkillTree(record, int(active))
|
||||
if !found || active >= currentSkillInfoTreeCount {
|
||||
return nil, fmt.Errorf("total-match active skill tree missing")
|
||||
}
|
||||
type row struct {
|
||||
slot byte
|
||||
id uint16
|
||||
level byte
|
||||
}
|
||||
rows := []row{}
|
||||
assigned := map[uint16]bool{}
|
||||
for slot, id := range record.Layouts[int(active)] {
|
||||
if slot < 0 || slot >= currentSkillSlotCount {
|
||||
return nil, fmt.Errorf("invalid total-match skill slot %d", slot)
|
||||
}
|
||||
level := state.Skills[int64(id)].Level
|
||||
if level == 0 {
|
||||
continue
|
||||
}
|
||||
if level < 0 || level > 255 || id >= 512 {
|
||||
return nil, fmt.Errorf("invalid total-match skill %d", id)
|
||||
}
|
||||
rows = append(rows, row{byte(slot), id, byte(level)})
|
||||
assigned[id] = true
|
||||
}
|
||||
for id, skill := range state.Skills {
|
||||
if id < 0 || id >= 512 || skill.Level < 0 || skill.Level > 255 {
|
||||
return nil, fmt.Errorf("invalid total-match learned skill %d", id)
|
||||
}
|
||||
if skill.Level > 0 && !assigned[uint16(id)] {
|
||||
rows = append(rows, row{255, uint16(id), byte(skill.Level)})
|
||||
}
|
||||
}
|
||||
if len(rows) > 255 {
|
||||
return nil, fmt.Errorf("too many total-match skills")
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].slot != rows[j].slot {
|
||||
return rows[i].slot < rows[j].slot
|
||||
}
|
||||
return rows[i].id < rows[j].id
|
||||
})
|
||||
var w packetWriter
|
||||
w.writeByte(byte(len(rows)))
|
||||
w.writeByte(active)
|
||||
for _, row := range rows {
|
||||
w.writeByte(row.slot)
|
||||
w.writeUint16(row.id)
|
||||
w.writeByte(row.level)
|
||||
}
|
||||
return w.bytes(), nil
|
||||
}
|
||||
|
||||
func (s *Service) loadCurrentPVPTotalTeam(ctx context.Context, session *gameSession) (*currentPVPTotalTeam, error) {
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Skill == nil || repos.Equipment == nil {
|
||||
return nil, fmt.Errorf("total-match actor repositories missing")
|
||||
}
|
||||
snapshot, err := dnfpvp.ReadTotalTeam(ctx, repos, session.accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.initialEquipmentMu.Lock()
|
||||
archive, err := s.initialEquipmentArchiveLocked()
|
||||
s.initialEquipmentMu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshot, err = dnfpvp.ResolveTotalTeam(session.accountID, snapshot.Team.Name, snapshot.Slots, snapshot.Characters[:], archive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
team := ¤tPVPTotalTeam{snapshot: snapshot}
|
||||
account, found, err := repos.Account.Load(ctx, session.accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("total-match account missing")
|
||||
}
|
||||
progress, err := dnfpvp.ReadTotalProgress(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
team.rankPoint = uint32(progress.Stats[dnfpvp.RankPointKey])
|
||||
team.ratingInitialized = progress.Stats["pvp_rating_initialized"] != 0
|
||||
selectedFound := false
|
||||
for _, c := range snapshot.Characters {
|
||||
selectedFound = selectedFound || c.CharacterID == strconv.Itoa(int(session.selectedCharacterID))
|
||||
}
|
||||
if !selectedFound {
|
||||
return nil, fmt.Errorf("selected character outside total-match roster")
|
||||
}
|
||||
for i, c := range snapshot.Characters {
|
||||
skills, found, err := repos.Skill.Load(ctx, c.CharacterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("total-match character %s skills missing", c.CharacterID)
|
||||
}
|
||||
active, err := currentSceneActiveSkillTree(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
skillBody, err := buildCurrentPVPTotalSkills(skills, active)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats, found := s.characterPVFStatsForUserInfo(ctx, session, c, true)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("total-match character %s PVF stats missing", c.CharacterID)
|
||||
}
|
||||
// Preflight equipment errors instead of treating a failed load as naked.
|
||||
if _, _, err := repos.Equipment.Load(ctx, c.CharacterID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := csharpLegacyUserInfoReader{ctx: ctx, repo: repos.LegacyUserInfo, characterID: c.CharacterID, service: s, session: session, pvfStats: stats, hasPVFStats: true}
|
||||
team.actors[i], err = r.buildCurrentPVPTotalActor(c, skillBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
// Exact special=1 payload of current EXE 2237EB0, called by NOTI 800.
|
||||
// The enclosing packet owns the name and index; this is not a mode1 record.
|
||||
func (r *csharpLegacyUserInfoReader) buildCurrentPVPTotalActor(c dnfrepo.CharacterRecord, skills []byte) ([]byte, error) {
|
||||
job, err := strconv.ParseUint(c.Job, 10, 8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var w packetWriter
|
||||
w.writeByte(rosterLevel(c))
|
||||
w.writeByte(byte(job))
|
||||
w.writeByte(byte(statU32(c, "grow_type", 0) & 127))
|
||||
w.writeByte(boolByte(dnfpvp.IsPrivateCharacter(c)))
|
||||
stats := r.buildCurrentUserInfoMode1StatBlob(r.one("legacy_character_subtype1_fields", csharpUserInfoSubtype1Columns()), c)
|
||||
w.writeUint32(uint32(len(stats)))
|
||||
w.writeBytes(stats)
|
||||
w.writeByte(currentExtraEquipmentSlotState(c))
|
||||
writeCurrentSceneObjectEquipSummary(&w, currentSceneObjectMode0EquipSummary(c, true))
|
||||
w.writeUint32(currentCloneTitleItemID(c, true))
|
||||
w.writeZero(4) // 2002850, 2002880, 1B1D610, reserved u8
|
||||
rows := r.currentMode1EquipmentObjectRows()
|
||||
if len(currentMode1EquipmentDeferredUpdateRows(rows)) != 0 {
|
||||
return nil, fmt.Errorf("total-match equipment has no verified create representation")
|
||||
}
|
||||
create := currentMode1EquipmentCreateRows(rows)
|
||||
w.writeByte(byte(len(create)))
|
||||
for _, row := range create {
|
||||
writeCurrentMode1EquipmentCreateRow(&w, row)
|
||||
}
|
||||
w.writeUint32(currentCloneTitleItemID(c, true))
|
||||
// Skill-object +0 has native constructor default zero (268F852). There is
|
||||
// no persisted counterpart in this server; do not invent a character key.
|
||||
w.writeUint16(0)
|
||||
w.writeBytes(skills)
|
||||
w.writeUint32(0) // same unpopulated auxiliary actor list as normal mode1
|
||||
// Current PVF excludes Creator (job 10) from matching. Its five auxiliary
|
||||
// values are still present on the wire and ignored by other actor types.
|
||||
w.writeZero(20)
|
||||
creature := r.currentEquippedCreature()
|
||||
w.writeUint32(creature.itemID)
|
||||
w.writeUint32(creature.serialOrHandle)
|
||||
return w.bytes(), nil
|
||||
}
|
||||
|
||||
func writeCurrentPVPTotalSummary(w *packetWriter, team *currentPVPTotalTeam) {
|
||||
w.writeRawDstr(rosterNameBytes(team.snapshot.Team.Name))
|
||||
for _, c := range team.snapshot.Characters {
|
||||
w.writeRawDstr(rosterNameBytes(c.Name))
|
||||
w.writeByte(byte(numericCharacterStat(c.Job)))
|
||||
w.writeByte(byte(statU32(c, "grow_type", 0) & 127))
|
||||
w.writeByte(boolByte(dnfpvp.IsPrivateCharacter(c)))
|
||||
}
|
||||
w.writeUint32(team.rankPoint)
|
||||
}
|
||||
|
||||
func buildCurrentPVPTotalActors(members []*currentPVPMember) []byte {
|
||||
var w packetWriter
|
||||
w.writeByte(byte(len(members)))
|
||||
for _, m := range members {
|
||||
w.writeUint16(m.characterID)
|
||||
w.writeByte(3)
|
||||
for i, c := range m.total.snapshot.Characters {
|
||||
w.writeByte(byte(i))
|
||||
w.writeRawDstr(rosterNameBytes(c.Name))
|
||||
w.writeBytes(m.total.actors[i])
|
||||
}
|
||||
}
|
||||
return w.bytes()
|
||||
}
|
||||
|
||||
func buildCurrentPVPTotalSummaries(members []*currentPVPMember) []byte {
|
||||
var w packetWriter
|
||||
w.writeByte(byte(len(members)))
|
||||
for _, m := range members {
|
||||
w.writeUint32(uint32(m.characterID))
|
||||
writeCurrentPVPTotalSummary(&w, m.total)
|
||||
}
|
||||
return w.bytes()
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentPVPTotalActors(target *gameSession, members []*currentPVPMember) error {
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, packet := range []struct {
|
||||
id uint16
|
||||
body []byte
|
||||
}{{802, buildCurrentPVPTotalSummaries(members)}, {800, buildCurrentPVPTotalActors(members)}} {
|
||||
if err := s.sendGameUpperRawClass(target, packet.id, packet.body, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) refreshCurrentPVPTotalRoomLocked(room *currentPVPRoom) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
members := []*currentPVPMember{}
|
||||
loaded := map[*currentPVPMember]*currentPVPTotalTeam{}
|
||||
for seat, m := range room.members {
|
||||
if !room.combatant(seat) {
|
||||
continue
|
||||
}
|
||||
team, err := s.loadCurrentPVPTotalTeam(ctx, m.session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
loaded[m] = team
|
||||
members = append(members, m)
|
||||
}
|
||||
// Publish only once the complete room roster has loaded successfully.
|
||||
for m, team := range loaded {
|
||||
m.total = team
|
||||
}
|
||||
for _, m := range room.members {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
if m.total != nil {
|
||||
if err := s.sendGameUpperRawClass(m.session, 801, buildPVPTotalTeamSelection(m.total.snapshot), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.sendCurrentPVPTotalActors(m.session, members); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user