服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
167 lines
6.1 KiB
Go
167 lines
6.1 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"golang.org/x/text/encoding/simplifiedchinese"
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
)
|
||
|
||
func (r *csharpLegacyUserInfoReader) buildCurrentUserInfoMode1StatBlob(row dnfrepo.LegacyUserInfoRow, character dnfrepo.CharacterRecord) []byte {
|
||
var w packetWriter
|
||
/*
|
||
// MCP: 女鬼剑 actor 虚函数 sub_24A5B20 读取当前 EXE 的 92 字节块:
|
||
// +0/+4 为 dword,+8..+14 为 4 个 u16,+16 起 4 个 s16,+24 起 18 个 s16,后段固定读取 +60/+64/+66/+68/+72/+74/+76/+78/+80/+84/+85。
|
||
hpMax := rowStatU32NonZero(row, character, "stat_hp_max", 11600)
|
||
*/
|
||
// Current EXE sub_3841230 expands this 92-byte actor state and sub_1D1EDE0
|
||
// maps +8..+14 to strength, vitality, intelligence, and spirit. The next
|
||
// four s16 fields are elemental resistances, followed by 18 status
|
||
// resistances and the inventory/speed/weight fields.
|
||
hpMax := r.rowStatU32Real(row, character, "stat_hp_max")
|
||
mpMax := r.rowStatU32Real(row, character, "stat_mp_max")
|
||
w.writeUint32(hpMax)
|
||
w.writeUint32(mpMax)
|
||
for _, value := range []int{
|
||
r.rowStatIntReal(row, character, "stat_strength"),
|
||
r.rowStatIntReal(row, character, "stat_vitality"),
|
||
r.rowStatIntReal(row, character, "stat_intelligence"),
|
||
r.rowStatIntReal(row, character, "stat_spirit"),
|
||
r.rowStatIntReal(row, character, "stat_fire_resistance"),
|
||
r.rowStatIntReal(row, character, "stat_water_resistance"),
|
||
r.rowStatIntReal(row, character, "stat_dark_resistance"),
|
||
r.rowStatIntReal(row, character, "stat_light_resistance"),
|
||
} {
|
||
writeLegacyInt16(&w, currentActorStatS16Wire(value))
|
||
}
|
||
for idx := 0; idx < 18; idx++ {
|
||
value := int(rowStatU16(row, character, "active_status_resistance_"+twoDigit(idx), 0))
|
||
writeLegacyInt16(&w, currentActorStatS16Wire(value))
|
||
}
|
||
w.writeUint32(r.rowStatU32Real(row, character, "stat_inventory_limit"))
|
||
w.writeUint16(r.rowStatU16Real(row, character, "stat_hp_regen_speed"))
|
||
w.writeUint16(r.rowStatU16Real(row, character, "stat_mp_regen_speed"))
|
||
w.writeUint32(r.rowStatU32Real(row, character, "stat_move_speed"))
|
||
w.writeUint16(r.rowStatU16Real(row, character, "stat_attack_speed"))
|
||
w.writeUint16(r.rowStatU16Real(row, character, "stat_cast_speed"))
|
||
w.writeUint16(r.rowStatU16Real(row, character, "stat_hit_recovery"))
|
||
w.writeUint16(r.rowStatU16Real(row, character, "stat_jump_power"))
|
||
w.writeUint32(r.rowStatU32Real(row, character, "stat_weight"))
|
||
// Current NoPack sub_3841230 copies +0x54 to actor+0x5F04. sub_24D5640
|
||
// consumes it as the base-stat percentage: four dimensions use
|
||
// percentage*wire/1000 and current HP uses percentage*maxHP/100. The
|
||
// neutral protocol value is 100; actor level comes from the separate mode0
|
||
// level byte propagated into ability context 3.
|
||
w.writeByte(currentActorBaseStatScalePercent)
|
||
// Current NoPack sub_3841230 copies source +0x55 into the actor state, and
|
||
// sub_24A5B20 consumes it as a float scaled by 0.1. It is not the durable
|
||
// ex_equip_slot_stat byte carried by the selected-character subtype1 body.
|
||
// Keep this unresolved runtime float neutral instead of leaking slot bits.
|
||
w.writeUint32(0)
|
||
|
||
out := w.bytes()
|
||
switch {
|
||
case len(out) < currentMode1StatBlobWireSize:
|
||
padded := make([]byte, currentMode1StatBlobWireSize)
|
||
copy(padded, out)
|
||
return padded
|
||
case len(out) > currentMode1StatBlobWireSize:
|
||
return append([]byte(nil), out[:currentMode1StatBlobWireSize]...)
|
||
default:
|
||
return out
|
||
}
|
||
}
|
||
|
||
func currentActorStatS16Wire(value int) int {
|
||
scaled := value * 10
|
||
switch {
|
||
case scaled > 32767:
|
||
return 32767
|
||
case scaled < -32768:
|
||
return -32768
|
||
default:
|
||
return scaled
|
||
}
|
||
}
|
||
|
||
func clampUint16ForUserInfo(value uint32) uint16 {
|
||
if value > 0xffff {
|
||
return 0xffff
|
||
}
|
||
return uint16(value)
|
||
}
|
||
|
||
func csharpUserInfoSubtype1Columns() []string {
|
||
columns := []string{
|
||
"stat_hp_max", "stat_mp_max", "stat_strength", "stat_intelligence", "stat_vitality", "stat_spirit",
|
||
"stat_physical_attack", "stat_physical_defense", "stat_magical_attack", "stat_magical_defense",
|
||
"stat_independent_attack", "stat_fire_resistance", "stat_water_resistance", "stat_dark_resistance", "stat_light_resistance",
|
||
}
|
||
for idx := 0; idx < 17; idx++ {
|
||
columns = append(columns, "active_status_resistance_"+twoDigit(idx))
|
||
}
|
||
columns = append(columns,
|
||
"stat_inventory_limit", "stat_hp_regen_speed", "stat_mp_regen_speed", "stat_move_speed",
|
||
"stat_attack_speed", "stat_cast_speed", "stat_hit_recovery", "stat_jump_power", "stat_weight",
|
||
"stat_level", "name_tag_item_id", "name_tag_expire_time", "skill_tree_index",
|
||
"equipped_creature_level", "equip_list_trailing", "manage_level", "flag_byte",
|
||
"guild_power_war", "server_timestamp", "quest_shop_count", "progress1", "progress2")
|
||
return columns
|
||
}
|
||
|
||
func writeClientNameDstr(w *packetWriter, name string) {
|
||
if encoded, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(name)); err == nil {
|
||
w.writeRawDstr(encoded)
|
||
return
|
||
}
|
||
w.writeRawDstr([]byte(name))
|
||
}
|
||
|
||
func writeFixedBytes(w *packetWriter, data []byte, size int) {
|
||
for idx := 0; idx < size; idx++ {
|
||
var value byte
|
||
if idx < len(data) {
|
||
value = data[idx]
|
||
}
|
||
w.writeByte(value)
|
||
}
|
||
}
|
||
|
||
func writeLegacyInt16(w *packetWriter, value int) {
|
||
w.writeUint16(uint16(int16(value)))
|
||
}
|
||
|
||
func (r *csharpLegacyUserInfoReader) rowStatU16Real(row dnfrepo.LegacyUserInfoRow, character dnfrepo.CharacterRecord, column string) uint16 {
|
||
value, ok := r.realStatInt64(row, character, column)
|
||
if !ok || value <= 0 {
|
||
return 0
|
||
}
|
||
if value > 0xffff {
|
||
return 0xffff
|
||
}
|
||
return uint16(value)
|
||
}
|
||
|
||
func (r *csharpLegacyUserInfoReader) rowStatU32Real(row dnfrepo.LegacyUserInfoRow, character dnfrepo.CharacterRecord, column string) uint32 {
|
||
value, ok := r.realStatInt64(row, character, column)
|
||
if !ok || value <= 0 {
|
||
return 0
|
||
}
|
||
if value > 0xffffffff {
|
||
return 0xffffffff
|
||
}
|
||
return uint32(value)
|
||
}
|
||
|
||
func (r *csharpLegacyUserInfoReader) rowStatIntReal(row dnfrepo.LegacyUserInfoRow, character dnfrepo.CharacterRecord, column string) int {
|
||
value, ok := r.realStatInt64(row, character, column)
|
||
if !ok {
|
||
return 0
|
||
}
|
||
if value > int64(^uint(0)>>1) {
|
||
return int(^uint(0) >> 1)
|
||
}
|
||
if value < -int64(^uint(0)>>1)-1 {
|
||
return -int(^uint(0)>>1) - 1
|
||
}
|
||
return int(value)
|
||
}
|