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

@@ -114,7 +114,7 @@ func handleMsgMhfGetAchievement(s *Session, p mhfpacket.MHFPacket) {
resp.WriteUint8(ach.Level)
resp.WriteUint16(ach.NextValue)
resp.WriteUint32(ach.Required)
resp.WriteBool(false) // TODO: Notify on rank increase since last checked, see MhfDisplayedAchievement
resp.WriteBool(summary.Notify[id])
resp.WriteUint8(ach.Trophy)
/* Trophy bitfield
0000 0000
@@ -152,7 +152,9 @@ func handleMsgMhfAddAchievement(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfPaymentAchievement(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfDisplayedAchievement(s *Session, p mhfpacket.MHFPacket) {
// This is how you would figure out if the rank-up notification needs to occur
if err := s.server.achievementService.MarkDisplayed(s.charID); err != nil {
s.logger.Warn("Failed to mark achievements as displayed", zap.Error(err))
}
}
func handleMsgMhfGetCaAchievementHist(s *Session, p mhfpacket.MHFPacket) {}