服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
type currentActorSkillResource struct {
|
|
id uint16
|
|
rank byte
|
|
}
|
|
|
|
func (r *csharpLegacyUserInfoReader) currentActorSkillResources() ([]currentActorSkillResource, error) {
|
|
if r == nil || r.service == nil || r.characterID == "" {
|
|
return nil, nil
|
|
}
|
|
repos, ready := r.service.repositoryGroup()
|
|
if !ready || repos.Skill == nil {
|
|
return nil, nil
|
|
}
|
|
ctx := r.ctx
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
record, found, err := repos.Skill.Load(ctx, r.characterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, nil
|
|
}
|
|
rows := make([]currentActorSkillResource, 0, len(record.Skills))
|
|
for id, state := range record.Skills {
|
|
if !state.Enabled || state.Level <= 0 {
|
|
continue
|
|
}
|
|
if id < 0 || id > 0xffff || state.Level > 0xff {
|
|
return nil, fmt.Errorf("actor skill resource exceeds native u16/u8 range: skill=%d rank=%d", id, state.Level)
|
|
}
|
|
rows = append(rows, currentActorSkillResource{id: uint16(id), rank: byte(state.Level)})
|
|
}
|
|
if len(rows) > 0xff {
|
|
return nil, fmt.Errorf("actor skill resource count exceeds native u8 range: %d", len(rows))
|
|
}
|
|
sort.Slice(rows, func(i, j int) bool { return rows[i].id < rows[j].id })
|
|
return rows, nil
|
|
}
|
|
|
|
func (r *csharpLegacyUserInfoReader) writeCurrentActorSkillResources(w *packetWriter) {
|
|
if r != nil && r.suppressActorSkillResources {
|
|
w.writeByte(0xff)
|
|
for tree := 0; tree < currentSkillInfoTreeCount; tree++ {
|
|
w.writeByte(0)
|
|
}
|
|
if r.service != nil {
|
|
r.service.logGameEvent(r.session,
|
|
"game-actor-skill-resources-suppressed",
|
|
"character_id", r.characterID,
|
|
"tree_count", currentSkillInfoTreeCount,
|
|
"reason", "existing_actor_context_rebind_must_not_replay_learned_skill_resources")
|
|
}
|
|
return
|
|
}
|
|
rows, err := r.currentActorSkillResources()
|
|
if err != nil {
|
|
if r.service != nil {
|
|
r.service.logGameEvent(r.session, "game-actor-skill-resources-unavailable", "character_id", r.characterID, "error", err)
|
|
}
|
|
rows = nil
|
|
}
|
|
if len(rows) > 0 && r.service != nil {
|
|
r.service.logGameEvent(r.session, "game-actor-skill-resources-built", "character_id", r.characterID, "tree_count", currentSkillInfoTreeCount, "skill_ranks", fmt.Sprint(rows), "body_source", "mode1_native_u16_skill_u8_persisted_rank")
|
|
}
|
|
w.writeByte(0xff)
|
|
// Current EXE sub_2003420 consumes both groups even for the selected
|
|
// actor, but applies the u16 ID/u8 rank rows only to remote actors. As in
|
|
// op19, both current skill pages share this persisted learned-skill set.
|
|
for tree := 0; tree < currentSkillInfoTreeCount; tree++ {
|
|
w.writeByte(byte(len(rows)))
|
|
for _, row := range rows {
|
|
w.writeUint16(row.id)
|
|
w.writeByte(row.rank)
|
|
}
|
|
}
|
|
}
|