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

@@ -11,12 +11,16 @@ var errNotFound = errors.New("not found")
// --- mockAchievementRepo ---
type mockAchievementRepo struct {
scores [33]int32
ensureCalled bool
ensureErr error
getScoresErr error
incrementErr error
incrementedID uint8
scores [33]int32
ensureCalled bool
ensureErr error
getScoresErr error
incrementErr error
incrementedID uint8
displayedLevels []byte
displayedErr error
savedLevels []byte
saveLevelsErr error
}
func (m *mockAchievementRepo) EnsureExists(_ uint32) error {
@@ -33,6 +37,15 @@ func (m *mockAchievementRepo) IncrementScore(_ uint32, id uint8) error {
return m.incrementErr
}
func (m *mockAchievementRepo) GetDisplayedLevels(_ uint32) ([]byte, error) {
return m.displayedLevels, m.displayedErr
}
func (m *mockAchievementRepo) SaveDisplayedLevels(_ uint32, levels []byte) error {
m.savedLevels = levels
return m.saveLevelsErr
}
// --- mockMailRepo ---
type mockMailRepo struct {