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,217 @@
|
||||
package dnfbridge
|
||||
|
||||
import dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
|
||||
func defaultNoPackCharacterRoster(objectID int) dnfrepo.CharacterRoster {
|
||||
return dnfrepo.CharacterRoster{
|
||||
Header: dnfrepo.CharacterRosterHeader{
|
||||
UnkA: latestRosterRouteNormal,
|
||||
UnkB: latestRosterContextNormal,
|
||||
TotalOrSlotLimit: defaultCharacterSlots,
|
||||
UsedOrRemain: defaultCharacterSlots,
|
||||
PageCount: rosterDefaultPageCount,
|
||||
RosterFlag: latestCharacterCreateEnabled,
|
||||
},
|
||||
Entry: dnfrepo.CharacterRosterEntry{
|
||||
ObjectID: int64(objectID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func rosterState0Value(value int64) byte {
|
||||
// 早期误把 1 当普通态写入 roster_json;最新客户端会把它显示成改名/特殊状态,老数据这里降回普通态。
|
||||
if value == 1 {
|
||||
value = 0
|
||||
}
|
||||
return rosterByteValue(value, latestCharacterStateNormal)
|
||||
}
|
||||
|
||||
func rosterObjectID(character dnfrepo.CharacterRecord) int {
|
||||
if value := numericCharacterStatValue(character, "roster_object_id"); value > 0 {
|
||||
if value > int64(^uint(0)>>1) {
|
||||
return int(^uint(0) >> 1)
|
||||
}
|
||||
return int(value)
|
||||
}
|
||||
return numericCharacterID(character)
|
||||
}
|
||||
|
||||
func rosterSlotValue(slot int, fallback int) int {
|
||||
// 32 is the default unlocked capacity, not the wire-key ceiling. Accounts
|
||||
// expanded by the supported slot item retain their real 0..59 SQL slot in
|
||||
// both the initial mode-2 batch and later mode-4 extension batches.
|
||||
if slot >= 0 && slot < maxCharacterSlots {
|
||||
return slot
|
||||
}
|
||||
if fallback >= 0 && fallback < maxCharacterSlots {
|
||||
return fallback
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func packRosterJobGrow(job, grow byte) byte {
|
||||
return (job & 0x0f) | ((grow & 0x07) << 4)
|
||||
}
|
||||
|
||||
func defaultCreatedCharacterStats(optionLen int) map[string]int64 {
|
||||
// 这些键只给 legacy USERINFO fallback 使用;创建角色的主数据以 MySQL 列为准,不再写入 stats_json。
|
||||
return map[string]int64{
|
||||
"grow_type": 0,
|
||||
"exp": 0,
|
||||
"ex_equip_slot_stat": 0,
|
||||
"bonus_sp": 0,
|
||||
"bonus_tp": 0,
|
||||
"pvp_grade": 0,
|
||||
"pvp_rating_grade": 0,
|
||||
"user_state": 0,
|
||||
"tutorial_completed": 0,
|
||||
"gold": 0,
|
||||
"coin": 0,
|
||||
"town_id": newCharacterInitialTownID,
|
||||
"area_id": newCharacterInitialAreaID,
|
||||
"pos_x": newCharacterInitialPosX,
|
||||
"pos_y": newCharacterInitialPosY,
|
||||
"direction": newCharacterInitialDirection,
|
||||
"area_state": newCharacterInitialAreaState,
|
||||
"delete_flag": 0,
|
||||
"is_event_character": 0,
|
||||
"name_tag_item_id": 0,
|
||||
"stamina": 0,
|
||||
"fatigue": newCharacterInitialFatigue,
|
||||
"fatigue_limit": newCharacterFatigueLimit,
|
||||
"fatigue_penalty": 0,
|
||||
"pc_room_id": 0x00010001,
|
||||
"is_private_store": 0,
|
||||
"is_premium_pc_room": 0,
|
||||
"server_group_id": 0,
|
||||
"black_count": 0,
|
||||
"guild_level": 0,
|
||||
"chaos_point": 0,
|
||||
"disguise_kind": 0,
|
||||
"is_disguised": 0,
|
||||
"expert_job_type": 0,
|
||||
"expert_job_exp": 0,
|
||||
"extra46": 0,
|
||||
"extra47": 0,
|
||||
"extra51": 0,
|
||||
"is_hardcore_mode": 0,
|
||||
"is_hardcore_dead": 0,
|
||||
"hardcore_death_count": 0,
|
||||
"user_state_bits": 3,
|
||||
"chat_ban_end_time": 0,
|
||||
"fatigue_update": 0,
|
||||
"return_user_flag": 1,
|
||||
"channel_display_mode": 0,
|
||||
"channel_type": 0,
|
||||
"channel_id": newCharacterInitialChannelID,
|
||||
"is_return_user": 0,
|
||||
"link_slot_enabled": 0,
|
||||
"link_type_a": 0,
|
||||
"link_type_b": 0,
|
||||
"emotion_index": 0,
|
||||
"action_byte": 0,
|
||||
"fatigue_display_update": 0,
|
||||
"costume_flag": 0,
|
||||
"aura_flag": 0,
|
||||
"pet_display_flag": 0,
|
||||
"title_display_flag": 0,
|
||||
"pvp_stat_a": 0,
|
||||
"pvp_win_streak": 0,
|
||||
"pvp_lose_streak": 0,
|
||||
"pvp_rank_point": 0,
|
||||
"trailing_byte": 0,
|
||||
"create_option_len": int64(optionLen),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultCreatedCharacterStatsFromRequest(req createCharacterRequest) map[string]int64 {
|
||||
stats := defaultCreatedCharacterStats(len(req.options))
|
||||
// 当前 Go 仓储还没有完整 subtype/init-body 写接口,USERINFO 构造保留这些只读兜底字段。
|
||||
stats["pc_room_id"] = 0x00010001
|
||||
stats["user_state_bits"] = 3
|
||||
stats["return_user_flag"] = 1
|
||||
stats["channel_id"] = 2
|
||||
// Current-EXE op1378 consumes a per-character last story-summary level.
|
||||
// Characters created by this Origin-era server start already migrated, so
|
||||
// they must not replay legacy summaries on their first scene entry.
|
||||
stats[dnfrepo.CharacterStoryDigestLastLevelStatKey] = int64(newCharacterInitialLevel)
|
||||
stats[dnfrepo.CharacterStoryDigestMigrationVersionStatKey] = int64(dnfrepo.CurrentCharacterStoryDigestMigrationVersion)
|
||||
seedCreatedSubtype1Stats(stats)
|
||||
seedCreatedRosterStats(stats)
|
||||
stats["create_option_len"] = int64(len(req.options))
|
||||
for idx := 0; idx < 64; idx++ {
|
||||
stats["create_option_byte_"+twoDigit(idx)] = 0
|
||||
}
|
||||
storeByteStats(stats, "create_option_byte", req.options, 64)
|
||||
return stats
|
||||
}
|
||||
|
||||
func seedCreatedSubtype1Stats(stats map[string]int64) {
|
||||
/*
|
||||
// C# CharacterStatComputer 在 PVF 失败时使用这组 fallback,并额外加 Premium HP/MP;
|
||||
// dnfbridge 当前没有静态数据注入,先按 C# fallback 保证新角色不是空 subtype1。
|
||||
for key, value := range map[string]int64{
|
||||
// 数值属性由 PVF 计算;这里只初始化 subtype1 的非数值状态字段。
|
||||
for key, value := range map[string]int64{
|
||||
*/
|
||||
for key, value := range map[string]int64{
|
||||
"stat_level": csharpSubtype1ProtocolStatLevel,
|
||||
"stat_block_marker": csharpSubtype1StatBlockMarker,
|
||||
"name_tag_expire_time": 0,
|
||||
"equip_list_trailing": 0,
|
||||
"skill_tree_index": 0,
|
||||
"equipped_creature_level": 0,
|
||||
"manage_level": 0,
|
||||
"flag_byte": 0,
|
||||
"guild_power_war": 0,
|
||||
"server_timestamp": 0,
|
||||
"quest_shop_count": 0,
|
||||
"progress1": 0,
|
||||
"progress2": 0,
|
||||
} {
|
||||
stats[key] = value
|
||||
}
|
||||
for idx := 0; idx < 18; idx++ {
|
||||
stats["active_status_resistance_"+twoDigit(idx)] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func seedCreatedRosterStats(stats map[string]int64) {
|
||||
for _, key := range []string{
|
||||
"roster_state0",
|
||||
"roster_time_a",
|
||||
"roster_time_b",
|
||||
"roster_value0",
|
||||
"roster_value1",
|
||||
"roster_value2",
|
||||
"roster_reserved_a",
|
||||
"roster_reserved_b",
|
||||
"roster_value3",
|
||||
"roster_object_id",
|
||||
"roster_flag0_eq1",
|
||||
"roster_card_flag",
|
||||
"roster_value5",
|
||||
"roster_display_flags",
|
||||
"roster_flag6_eq1",
|
||||
} {
|
||||
stats[key] = 0
|
||||
}
|
||||
for idx := 0; idx < rosterLinkedIDBlockSize; idx++ {
|
||||
stats["roster_linked_id_"+twoDigit(idx)] = 0
|
||||
}
|
||||
for idx := 0; idx < 12; idx++ {
|
||||
stats["roster_tail_"+twoDigit(idx)] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func storeByteStats(stats map[string]int64, prefix string, data []byte, limit int) {
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
for idx, value := range data {
|
||||
if idx >= limit {
|
||||
break
|
||||
}
|
||||
stats[prefix+"_"+twoDigit(idx)] = int64(value)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user