feat(achievement): add rank-up notifications (#165)

RE'd putDisplayed_achievement from ZZ client DLL via Ghidra: the packet
sends opcode + 1 zero byte with no achievement ID, acting as a blanket
"I saw everything" signal.

Server changes:
- Track per-character last-displayed levels in new displayed_levels
  column (migration 0008)
- GetAchievement compares current vs displayed levels per entry
- DisplayedAchievement snapshots current levels to clear notifications
- Repo, service, mock, and 3 new service tests

Protbot changes:
- New --action achievement: fetches achievements, shows rank-up markers,
  sends DISPLAYED_ACHIEVEMENT, re-fetches to verify notifications clear
- Packet builders for GET/ADD/DISPLAYED_ACHIEVEMENT
This commit is contained in:
Houmgaor
2026-03-18 11:35:31 +01:00
parent 476882e1fb
commit 61d85e749f
13 changed files with 383 additions and 13 deletions

View File

@@ -19,5 +19,8 @@ const (
MSG_SYS_RIGHTS_RELOAD uint16 = 0x005D
MSG_MHF_LOADDATA uint16 = 0x0061
MSG_MHF_ENUMERATE_QUEST uint16 = 0x009F
MSG_MHF_GET_WEEKLY_SCHED uint16 = 0x00E1
MSG_MHF_GET_ACHIEVEMENT uint16 = 0x00D4
MSG_MHF_ADD_ACHIEVEMENT uint16 = 0x00D6
MSG_MHF_DISPLAYED_ACHIEVEMENT uint16 = 0x00D8
MSG_MHF_GET_WEEKLY_SCHED uint16 = 0x00E1
)

View File

@@ -215,6 +215,56 @@ func BuildEnumerateQuestPacket(ackHandle uint32, world uint8, counter, offset ui
return bf.Data()
}
// BuildGetAchievementPacket builds a MSG_MHF_GET_ACHIEVEMENT packet.
// Layout mirrors Erupe's MsgMhfGetAchievement.Parse:
//
// uint16 opcode
// uint32 ackHandle
// uint32 charID
// uint32 zeroed
// 0x00 0x10 terminator
func BuildGetAchievementPacket(ackHandle, charID uint32) []byte {
bf := byteframe.NewByteFrame()
bf.WriteUint16(MSG_MHF_GET_ACHIEVEMENT)
bf.WriteUint32(ackHandle)
bf.WriteUint32(charID)
bf.WriteUint32(0) // Zeroed
bf.WriteBytes([]byte{0x00, 0x10})
return bf.Data()
}
// BuildAddAchievementPacket builds a MSG_MHF_ADD_ACHIEVEMENT packet (fire-and-forget, no ACK).
// Layout mirrors Erupe's MsgMhfAddAchievement.Parse:
//
// uint16 opcode
// uint8 achievementID
// uint16 unk1
// uint16 unk2
// 0x00 0x10 terminator
func BuildAddAchievementPacket(achievementID uint8) []byte {
bf := byteframe.NewByteFrame()
bf.WriteUint16(MSG_MHF_ADD_ACHIEVEMENT)
bf.WriteUint8(achievementID)
bf.WriteUint16(0) // Unk1
bf.WriteUint16(0) // Unk2
bf.WriteBytes([]byte{0x00, 0x10})
return bf.Data()
}
// BuildDisplayedAchievementPacket builds a MSG_MHF_DISPLAYED_ACHIEVEMENT packet (fire-and-forget).
// Layout mirrors Erupe's MsgMhfDisplayedAchievement.Parse:
//
// uint16 opcode
// uint8 zeroed
// 0x00 0x10 terminator
func BuildDisplayedAchievementPacket() []byte {
bf := byteframe.NewByteFrame()
bf.WriteUint16(MSG_MHF_DISPLAYED_ACHIEVEMENT)
bf.WriteUint8(0) // Zeroed
bf.WriteBytes([]byte{0x00, 0x10})
return bf.Data()
}
// BuildGetWeeklySchedulePacket builds a MSG_MHF_GET_WEEKLY_SCHEDULE packet.
//
// uint16 opcode