DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,483 @@
package dnfbridge
import (
"fmt"
"hash/fnv"
"math"
"sort"
"longheng.io/server/internal/modules/dnf/raid"
)
// 卢克攻坚战一阶段的塔罗牌显示包:opcode 1181 (0x49D),
// LUKE_RAID_CARD_DISPLAY。客户端侧没有对应的上行请求包(名字表里含 LUKE 的
// 包全是 ENUM_NOTIPACKET_*),1181 只能由服务端推;逐字段逆向见
// tools\_rev_卢克塔罗牌_1181规格_20260929.md。
//
// 下发时机(官方系统说明,2017-06-22 国服开放卢克团队模式的更新公告):
//
// 「塔罗卡牌在团队频道入场时可以看见1次,之后可以在攻坚战进行中的状况表上确认。」
// 「塔罗卡牌以卢克团队模式进行的当日6点为基准进行变更,所有频道的塔罗卡牌统一相同。」
//
// ⇒ 推送点 = 成员走进卢克攻坚队待机室的那一次,每个成员每次进入恰好一包,
// openWindowFlag = 1(自动弹窗);牌面随后留在客户端本地,战况表自己重开窗口,
// 所以服务端**不重连补发、不周期重发**——这一点是有意的:那会变成刷屏源,
// 而且客户端没有任何字段能区分「补发」与「今天的牌变了」。
//
// 牌面本身官方未公开选牌规则(客户端不校验 P、不去重、没有「未翻开」哨兵,
// 见规格 §8.3),下面这套选法是**本实现的约定**,不是逆向结论。
// lukeCardSlotsPerPattern 是一个图案的卡位数 = `[CARD POS]` 的 (x,y) 对数,也就是
// 包体里每条记录的 K。出货 PVF 的 8 个图案全部是 7(规格 §5.2.1;客户端侧的上界
// 就是 `sar edx,3` 数出来的 `[CARD POS]` 对数,规格 §7.4)。它不是发出去的常量:
// K 实际从解析到的 `[CARD POS]` 数来,这里只做一致性守卫——形状与已逆向的包体
// 不一致时宁可不发(fail closed)。客户端按该对数遍历、并与包内 P 列表同步推进,
// 所以两者长度必须一致。
const lukeCardSlotsPerPattern = 7
// runtimeRaidLukeTarotSanctums 是一阶段三个圣所的发包顺序。顺序本身客户端不校验,
// 用 luke.etc [DEFAULT OBJECT COUNT] 的书写顺序(诞生/湮灭/扩散)。
var runtimeRaidLukeTarotSanctums = []uint32{
lukeBirthSanctumMapID,
lukeExtinctionSanctumMapID,
lukeSpreadSanctumMapID,
}
// runtimeRaidLukeCardSetRecordCount 是 luke.etc `[CARD SET MAP INFO]` 的记录条数。
// 该段是扁平列表(出货 15 个数:0 0 0 3 3 3 6 6 6 9 9 9 12 12 12),客户端按每 3 个
// 数字 push 进 3 字节元素的向量,1181 的字段 1 正是拿这个条数做上界(规格 §1.1/§8.1.2)。
// 定义点与迷宫侧共用(runtimeRaidLukeTarotCardSetRecords)。
func runtimeRaidLukeCardSetRecordCount(cardSetMapInfo []int) int {
return runtimeRaidLukeTarotCardSetRecords(cardSetMapInfo)
}
// runtimeRaidLukeTarotCardSetIndex 是「当天那一套牌」的下标(0..记录条数-1)。
//
// 它只依赖 tarot day:官方文案写明全频道统一,且客户端只把它当合法性字节用
// (越界则整包丢弃,之后再无引用,规格 §8.1.1),所以它既不带圣所也不带攻坚队。
// **它与 raid_luke_sanctum_maze.go 用的是同一个抽取器**:1181 里这个字节代表的那条
// [CARD SET MAP INFO] 记录,正是当天三个圣所走的迷宫组所在的那条 —— 一个概念只留
// 一个数字,免得日后有人以为"显示用的套"和"实际走的组"是两回事。
func runtimeRaidLukeTarotCardSetIndex(raidDay int, cardSetMapInfo []int) (int, int) {
records := runtimeRaidLukeCardSetRecordCount(cardSetMapInfo)
if records <= 0 {
return 0, 0
}
return runtimeRaidLukeTarotCardSetRecord(raidDay, records), records
}
// runtimeRaidLukeTarotPatternIndex 选一个圣所今天启动哪一套防御体系(包体字段 B,
// 即 `[PATTERN] <地牢ID> <图案序号>` 的序号)。
//
// 官方文案:「卢克手下的怪物们以塔罗牌的形式展现,且分配在源生宫殿(1阶段)的各个
// 地下城中。随着塔罗牌分配的形态不同,各个地下城的防御体系启动。」⇒ B 就是
// 「该圣所今天启动哪套防御体系」,其 `[TOOLTIP TEXT]` 即那套系统的名字。
// 出货 PVF 里:5054 → 属性值弱化系统 / 防御体系;5055 → 元素防御系统(冰/光/火/暗);
// 5056 → 超合金装甲系统 / 反对魔法系统。
//
// 本实现的约定:沿用既有的每日抽取器 runtimeRaidLukeTarotGroup(day, 地牢ID, n),
// 即同一天同一圣所全服取同一套(与「所有频道统一」一致),换日即变。
// **本次不动它的选法**:「每天一套牌」怎么映射到各圣所各自的图案序号没有证据
// (各圣所 [PATTERN] 段数是 2/4/2,不是同一张表的三个分量),所以 B 保持按
// (day, 地牢ID) 抽;改的只是"走哪组迷宫"。
func runtimeRaidLukeTarotPatternIndex(raidDay int, dungeonID uint32, patterns []int) (int, bool) {
if len(patterns) == 0 {
return 0, false
}
return patterns[runtimeRaidLukeTarotGroup(raidDay, int(dungeonID), len(patterns))], true
}
// runtimeRaidLukeTarotCards 从一个圣所那份 22 张大阿卡纳里选 7 张互不重复的牌。
//
// **本实现的约定**(官方选牌规则未知,客户端也不校验、不去重):
// 按 (tarot day, 圣所地牢 ID, 图案序号) 对每张大阿卡纳做一次确定性哈希,按键升序取前
// want 张。因此同一场攻坚队、同一天、同一圣所每次算出同一批牌(可复现、可对账),
// 换一天就换一批;不同圣所、不同图案也各自不同。哈希键里带卡号本身,所以天然不会重复。
func runtimeRaidLukeTarotCards(raidDay int, dungeonID uint32, pattern int, cardIDs []uint32, want int) []uint32 {
if want <= 0 || len(cardIDs) < want {
return nil
}
type rankedCard struct {
score uint32
cardID uint32
}
ranked := make([]rankedCard, 0, len(cardIDs))
for _, cardID := range cardIDs {
hash := fnv.New32a()
_, _ = fmt.Fprintf(hash, "luke-tarot-card:%d:%d:%d:%d", raidDay, dungeonID, pattern, cardID)
ranked = append(ranked, rankedCard{score: hash.Sum32(), cardID: cardID})
}
sort.Slice(ranked, func(left, right int) bool {
if ranked[left].score != ranked[right].score {
return ranked[left].score < ranked[right].score
}
// 同分时按卡号定序,保证结果与卡表顺序无关(可复现)。
return ranked[left].cardID < ranked[right].cardID
})
cards := make([]uint32, 0, want)
for _, entry := range ranked[:want] {
cards = append(cards, entry.cardID)
}
return cards
}
// lukeCardEmptySlot is what a slot carries when the day's mazes do not fill it.
//
// The client draws one arcanum per slot by looking the value up in its own
// `[CARD]` table and **silently draws nothing for a value that misses** (see
// raid.LukeCardSetConfig.HasCard). All 22 shipped keys are five-digit monster
// ids, so the inactive-id sentinel can never collide with one, and a short day
// therefore leaves a quietly empty slot instead of a wrong card. Zero would be
// the other natural marker, but BuildLukeCardDisplayBody rejects it as a shape
// error, so the empty slot has to be a real u32 that simply is not a card.
const lukeCardEmptySlot uint32 = math.MaxUint32
// runtimeRaidLukeTarotCardsFromMazeMonsters derives a sanctum's card slots from
// the monsters that sanctum's three mazes really spawn on the day's group.
//
// The tarot faces are not decorative: every `[CARD] <monsterID>` block in
// LukeCardSet.etc is one major arcanum whose tooltip names the Luke monster it
// belongs to (00 愚者_红色小丑 59171, 02 女祭司_量产型贝奇 59873, 10 命运之轮
// 56673, 16 塔_全金属机甲斗神 56063 ...), and the value the notification carries
// is that monster id. The client owns the mapping, so naming the monsters the
// run will actually fight is exactly what makes the board show the day's real
// faces.
//
// Order: the sanctum's mazes in maze order (groupStart, +1, +2), and inside one
// maze the monster order the document lists ([NAMED] then [BOSS], see
// LukeRaidMonsterListMaze). Monsters that are not [CARD] keys are skipped, and a
// monster already used keeps its first slot, so the seven slots stay distinct.
//
// ⚠ 未证实约定:「第 n 只怪填第 n 格」没有任何客户端证据 —— 客户端只是把 7 个
// 值逐个查表画出来,槽位与怪的对应关系官方没有公开。这里按最自然的顺序填充
// (迷宫顺序 → 迷宫内出场顺序),它的可验证后果只有一条:**牌面集合**必须是当天
// 真会刷的那几只。出货 PVF 里每个组只能推出 5 只带 [CARD] 的怪
// (3 个迷宫 = 1 组中 boss + 1 只 named + 1 个圣所 boss,第三迷宫与第一迷宫共用
// 中 boss),所以 7 格里固定有 2 格是 lukeCardEmptySlot。
//
// nil when the day yields no arcanum at all: 调用方必须**不发包**(失败关闭),
// 不能发一包 7 个空槽的牌面。A non-nil result always has exactly want entries.
func runtimeRaidLukeTarotCardsFromMazeMonsters(
dungeonID uint32,
groupStart int,
monsters *raid.LukeRaidMonsterListConfig,
cardSet *raid.LukeCardSetConfig,
want int,
) []uint32 {
if want <= 0 || monsters == nil || cardSet == nil || groupStart < 0 {
return nil
}
if _, ok := monsters.Dungeon(dungeonID); !ok {
return nil
}
cards := make([]uint32, 0, want)
used := make(map[uint32]bool, want)
for maze := groupStart; maze < groupStart+lukeSanctumClearsPerGroup && len(cards) < want; maze++ {
monsterIDs, ok := monsters.MazeMonsters(dungeonID, maze)
if !ok {
// This sanctum does not declare that maze; the group is only ever
// accepted when all three fit, so this is a table that changed
// shape. Stop rather than read a neighbour's monsters.
break
}
for _, monsterID := range monsterIDs {
if len(cards) >= want {
break
}
if used[monsterID] || !cardSet.HasCard(monsterID) {
continue
}
used[monsterID] = true
cards = append(cards, monsterID)
}
}
if len(cards) == 0 {
return nil
}
for len(cards) < want {
cards = append(cards, lukeCardEmptySlot)
}
return cards
}
// runtimeRaidLukeTarotGroupStart is the group of mazes a sanctum walks on one
// tarot day: the day's shared [CARD SET MAP INFO] record's own component, with
// the old candidate list as the fallback for a table whose shape does not
// address a whole group (same relationship runtimeRaidLukeSanctumMazeDrawnGroup
// has at dungeon entry). The fallback index is the tarot-day hash rather than a
// dice roll, so the card display stays deterministic within a day.
func runtimeRaidLukeTarotGroupStart(
cardSetMapInfo []int,
record int,
dungeonID int,
mazeCount int,
raidDay int,
) (int, bool) {
if mazeCount >= lukeSanctumClearsPerGroup {
if start, ok := runtimeRaidLukeSanctumMazeRecordGroup(cardSetMapInfo, record, dungeonID, mazeCount); ok {
return start, true
}
}
groups := runtimeRaidLukeSanctumMazeGroups(cardSetMapInfo, mazeCount)
if len(groups) == 0 {
return 0, false
}
return groups[runtimeRaidLukeTarotCardSetRecord(raidDay, len(groups))], true
}
// runtimeRaidLukeTarotCardsForSanctum is one sanctum's seven slots for one tarot
// day: resolve the day's group for this sanctum, then read the monsters that
// group really spawns. nil means "this sanctum's face cannot be described
// today", which the caller turns into "do not send the packet at all".
func runtimeRaidLukeTarotCardsForSanctum(
dungeonID uint32,
cardSetMapInfo []int,
record int,
raidDay int,
monsters *raid.LukeRaidMonsterListConfig,
cardSet *raid.LukeCardSetConfig,
want int,
) []uint32 {
if want <= 0 || cardSet == nil || monsters == nil {
return nil
}
mazeCount, ok := monsters.MazeCount(dungeonID)
if !ok {
return nil
}
groupStart, ok := runtimeRaidLukeTarotGroupStart(cardSetMapInfo, record, int(dungeonID), mazeCount, raidDay)
if !ok {
return nil
}
return runtimeRaidLukeTarotCardsFromMazeMonsters(dungeonID, groupStart, monsters, cardSet, want)
}
// runtimeRaidLukeCardDisplayBody 组出今天这一包 1181 的包体。
//
// ok=false 表示「今天这套牌描述不出来」,调用方必须**不发包**:配置缺失、
// 图案缺失、卡位对数不是 7、大阿卡纳不足 7 张,任何一种都不发半截包。
//
// cardSetMapInfo 是 luke.etc 的 `[CARD SET MAP INFO]`(它同时决定字段 1 的上界和
// 三个圣所今天走的迷宫组所在的那条记录,见 raid_luke_sanctum_maze.go)。
// monsters 是 luke.etc.buff 的 `[RAID MONSTER LIST]`,牌面由**当天这三个圣所
// 真会刷的怪**推出(runtimeRaidLukeTarotCardsFromMazeMonsters)。
// openWindow 直接写进包体末字节:只有"本 session 第一次落到团队频道"那一包为 true。
func runtimeRaidLukeCardDisplayBody(
cardSetMapInfo []int,
cardSet *raid.LukeCardSetConfig,
monsters *raid.LukeRaidMonsterListConfig,
raidDay int,
openWindow bool,
) ([]byte, bool, error) {
if cardSet == nil {
return nil, false, nil
}
cardSetIndex, records := runtimeRaidLukeTarotCardSetIndex(raidDay, cardSetMapInfo)
if records <= 0 {
return nil, false, nil
}
display := raid.LukeCardDisplay{
CardSetIndex: byte(cardSetIndex),
CardSetCount: records,
Sanctums: make([]raid.LukeCardDisplaySanctum, 0, len(runtimeRaidLukeTarotSanctums)),
OpenWindow: openWindow,
}
for _, dungeonID := range runtimeRaidLukeTarotSanctums {
patterns := cardSet.PatternIndices(dungeonID)
patternIndex, ok := runtimeRaidLukeTarotPatternIndex(raidDay, dungeonID, patterns)
if !ok {
return nil, false, nil
}
pattern, found := cardSet.Pattern(dungeonID, patternIndex)
if !found || len(pattern.CardPositions) != lukeCardSlotsPerPattern {
return nil, false, nil
}
cards := runtimeRaidLukeTarotCardsForSanctum(
dungeonID,
cardSetMapInfo,
cardSetIndex,
raidDay,
monsters,
cardSet,
len(pattern.CardPositions),
)
if cards == nil {
// 牌面表在、却一张都推不出来 = 今天这组怪没有一张是 [CARD],这属于
// 「描述不出来」:失败关闭,不发半截包。
return nil, false, nil
}
display.Sanctums = append(display.Sanctums, raid.LukeCardDisplaySanctum{
DungeonID: dungeonID,
Pattern: patternIndex,
Cards: cards,
})
}
body, err := raid.BuildLukeCardDisplayBody(display)
if err != nil {
return nil, false, err
}
return body, true, nil
}
// runtimeRaidLukeCardDisplayPopupOwed reports whether this login session still
// owes the one pop-up the official behaviour grants
// ("每次登陆后,第一次进入卢克 RAID 频道时…自动展示一次塔罗牌"). The ledger is
// keyed by tarot day, because the packet itself does not depend on a raid.
// Read-only: the ledger is only spent by markRuntimeRaidLukeCardDisplayPopupSpent,
// and only after the packet actually went out with the flag set.
func runtimeRaidLukeCardDisplayPopupOwed(session *gameSession, tarotDay int) bool {
if session == nil {
return false
}
session.lukeCardDisplayMu.Lock()
defer session.lukeCardDisplayMu.Unlock()
return !session.lukeCardDisplayPoppedDays[tarotDay]
}
// markRuntimeRaidLukeCardDisplayPopupSpent spends this session's one pop-up for
// this tarot day. The lock is a leaf and is released before returning.
func markRuntimeRaidLukeCardDisplayPopupSpent(session *gameSession, tarotDay int) {
if session == nil {
return
}
session.lukeCardDisplayMu.Lock()
if session.lukeCardDisplayPoppedDays == nil {
session.lukeCardDisplayPoppedDays = make(map[int]bool, 2)
}
session.lukeCardDisplayPoppedDays[tarotDay] = true
session.lukeCardDisplayMu.Unlock()
}
// publishRuntimeRaidLukeCardDisplay 给一名玩家发一包 1181(今天的塔罗牌面)。
// 它自己吞掉全部错误并只记日志:频道入场不能被一张牌面拖垮。
//
// 官方行为(服主 2026-09-29 口径):
//
// 「玩家在每次登陆后,第一次进入卢克 RAID 频道时,离开赛利亚旅馆会自动展示
// 一次塔罗牌。」
//
// 所以这个包**不依赖攻坚队**。它的数据全部来自 PVF 的**全局配置**,与任何
// runtimeRaidState 都无关:
//
// 当天那套牌 = runtimeRaidLukeTarotCardSetRecord(tarotDay, 记录条数)
// (全服统一,只按日期)
// 牌面 = 当天那条 [CARD SET MAP INFO] 记录 + LukeCardSet.etc 的 22 张大
// 阿卡纳 + luke.etc.buff [RAID MONSTER LIST] 里当天那三个迷宫会刷的怪
//
// 前者来自 luke.etc(服务级缓存 runtimeRaidLukeConfig),后两者来自各自的
// PVF(runtimeRaidLukeCardSetConfig / runtimeRaidLukeRaidMonsterListConfig)。
// 这里**不再**读 state.LukeConfig —— 那份配置只有在团里才有,而官方规则说的是
// 进频道,不是进团(线上 2026-09-29T12:50:46Z 那条
// `..._skipped reason=luke_team_channel_entry_without_a_raid` 就是"没团"被跳过)。
//
// 账本是**每个登录会话、每个 tarot day 一次**(不是每个团一次):
// - 本次登录第一次进入卢克频道城镇 → open_window=1(弹窗);
// - 同一次登录里之后再进(换城镇回来、重连走同一条路、op36 进待机室、通关返回
// 待机室)→ 仍然发,但 open_window=0:数据保持新鲜,不再打扰;
// - 重新登录 = 新 session = 空账本 → 再弹一次。这正是"每次登陆后…一次"。
//
// raidKey 的两种取值(三条真实入口共用同一份账本):
// - 非 0:调用点已知这个团 —— op36 走进攻坚队待机室(handleTownSetUserArea)、
// 通关后落回待机室(restoreCompletedRaidWaitingPresence)。必须确实是本人的
// 卢克团,安徒恩一个字都不改(单人卢克也算:见下)。
// - 0:与团无关的入口 —— currentRaidTownTransition 落进卢克频道城镇。
// **没团也发**:官方规则只要求"人在卢克频道"。
//
// 单人模式(TailFlag == 1)不再特殊对待:官方规则的门槛是"进卢克 RAID 频道",
// 与团的编制无关;这条守卫生来是因为牌面过去取自团配置,现在不取了。
func (s *Service) publishRuntimeRaidLukeCardDisplay(session *gameSession, raidKey uint32, source string) {
if s == nil || session == nil || session.selectedCharacterID == 0 {
return
}
characterID := session.selectedCharacterID
if raidKey != 0 {
state, member := s.runtimeRaidForMember(characterID)
if !member || state.RaidKey == 0 || state.RaidKey != raidKey {
return
}
// 安徒恩不进这里:塔罗牌面只属于卢克。
if state.RaidMode != runtimeRaidOwnerLuke {
return
}
} else if !isCurrentLukeRaidChannel(session) {
// 与团无关的入口必须**确实是卢克频道**才发,安徒恩频道一个字都不改。
return
}
// luke.etc 的 [CARD SET MAP INFO]:全局配置,不是某个团的配置。
lukeConfig, err := s.runtimeRaidLukeConfig()
if err != nil {
s.logPacketEvent("game-raid-luke-card-display-skipped",
"char_id", characterID,
"raid_key", raidKey,
"msg_id", int(raid.LukeCardDisplayMsgID),
"source", source,
"reason", "luke_config_pvf_unavailable",
"error", err)
return
}
cardSet, err := s.runtimeRaidLukeCardSetConfig()
if err != nil {
s.logPacketEvent("game-raid-luke-card-display-skipped",
"char_id", characterID,
"raid_key", raidKey,
"msg_id", int(raid.LukeCardDisplayMsgID),
"source", source,
"reason", "card_set_pvf_unavailable",
"error", err)
return
}
monsters, err := s.runtimeRaidLukeRaidMonsterListConfig()
if err != nil {
s.logPacketEvent("game-raid-luke-card-display-skipped",
"char_id", characterID,
"raid_key", raidKey,
"msg_id", int(raid.LukeCardDisplayMsgID),
"source", source,
"reason", "raid_monster_list_pvf_unavailable",
"error", err)
return
}
raidDay := runtimeRaidLukeTarotDay(s.gameplayNow())
openWindow := runtimeRaidLukeCardDisplayPopupOwed(session, raidDay)
body, ok, err := runtimeRaidLukeCardDisplayBody(lukeConfig.CardSetMapInfo, cardSet, monsters, raidDay, openWindow)
if err != nil || !ok || len(body) == 0 {
s.logPacketEvent("game-raid-luke-card-display-skipped",
"char_id", characterID,
"raid_key", raidKey,
"msg_id", int(raid.LukeCardDisplayMsgID),
"tarot_day", raidDay,
"source", source,
"reason", "card_display_not_describable",
"error", err)
return
}
if err := s.sendGameUpperRawClassCodec(session, raid.LukeCardDisplayMsgID, body, 0, true); err != nil {
s.logPacketEvent("game-raid-luke-card-display-send-failed",
"char_id", characterID,
"raid_key", raidKey,
"msg_id", int(raid.LukeCardDisplayMsgID),
"tarot_day", raidDay,
"body_len", len(body),
"source", source,
"error", err)
return
}
// 只有真正出去了才花掉这一天这一次的弹窗额度:发失败的那一包不算"看见过"。
if openWindow {
markRuntimeRaidLukeCardDisplayPopupSpent(session, raidDay)
}
s.logPacketEvent("game-raid-luke-card-display-send",
"char_id", characterID,
"raid_key", raidKey,
"msg_id", int(raid.LukeCardDisplayMsgID),
"tarot_day", raidDay,
// card_set_index 是包体字段 1;card_set_records 是它被校验的上界,也就是
// luke.etc [CARD SET MAP INFO] 的记录条数(出货数据 = 15/3 = 5);packet_records
// 是包体字段 N(本包几条圣所记录,出货数据 = 3)。三个名字以前混用过,别再合并。
"card_set_index", int(body[0]),
"card_set_records", runtimeRaidLukeCardSetRecordCount(lukeConfig.CardSetMapInfo),
"packet_records", int(body[1]),
"open_window", int(body[len(body)-1]),
"body_len", len(body),
"source", source)
}