mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-26 17:43:21 +01:00
feat(diva): implement Diva Defense (UD) system
Add full Diva Defense / United Defense system: schema, repo layer, i18n bead names, and RE-verified packet handler implementations. Schema (0011_diva.sql): diva_beads, diva_beads_assignment, diva_beads_points, diva_prizes tables; interception_maps/points columns on guilds and guild_characters. Seed (DivaDefaults.sql): 26 prize milestones for personal and guild reward tracks (item_type=26 diva coins). Repo (DivaRepo): 11 new methods covering bead assignment, point accumulation, interception point tracking, prize queries, and cleanup. Mocks wired in test_helpers_test.go. i18n: Bead struct with EN/JP names for all 18 bead types (IDs 1–25). Session tracks currentBeadIndex (-1 = none assigned). Packet handlers corrected against mhfo-hd.dll RE findings: - GetKijuInfo: u8 count, 512-byte desc, color_id+bead_type per entry - SetKiju: 1-byte ACK; persists bead assignment to DB - GetUdMyPoint: 8×18-byte entries, no count prefix - GetUdTotalPointInfo: u8 error + u64[64] + u8[64] + u64 (~585 B) - GetUdSelectedColorInfo: u8 error + u8[8] = 9 bytes - GetUdDailyPresentList: correct u16 count format (was wrong hex) - GetUdNormaPresentList: correct u16 count format (was wrong hex) - GetUdRankingRewardList: correct u16 count with u32 item_id/qty - GetRewardSong: 22-byte layout with 0xFFFFFFFF prayer_end sentinel - AddRewardSongCount: parse implemented (was NOT IMPLEMENTED stub)
This commit is contained in:
@@ -23,6 +23,9 @@ func cleanupDiva(s *Session) {
|
||||
if err := s.server.divaRepo.DeleteEvents(); err != nil {
|
||||
s.logger.Error("Failed to delete diva events", zap.Error(err))
|
||||
}
|
||||
if err := s.server.divaRepo.CleanupBeads(); err != nil {
|
||||
s.logger.Error("Failed to cleanup diva beads", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 {
|
||||
@@ -137,16 +140,50 @@ func handleMsgMhfGetUdInfo(s *Session, p mhfpacket.MHFPacket) {
|
||||
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
||||
}
|
||||
|
||||
// defaultBeadTypes are used when the database has no bead rows configured.
|
||||
var defaultBeadTypes = []int{1, 3, 4, 8}
|
||||
|
||||
func handleMsgMhfGetKijuInfo(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfGetKijuInfo)
|
||||
// Temporary canned response
|
||||
data, _ := hex.DecodeString("04965C959782CC8B468EEC00000000000000000000000000000000000000000000815C82A082E782B582DC82A982BA82CC82AB82B682E3815C0A965C959782C682CD96D282E98E7682A281420A95B782AD8ED282C997458B4382F0975E82A682E98142000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001018BAD8C8282CC8B468EEC00000000000000000000000000000000000000000000815C82AB82E582A482B082AB82CC82AB82B682E3815C0A8BAD8C8282C682CD8BAD82A290BA904681420A95B782AD8ED282CC97CD82F08CA482AC909F82DC82B78142200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003138C8B8F5782CC8B468EEC00000000000000000000000000000000000000000000815C82AF82C182B582E382A482CC82AB82B682E3815C0A8C8B8F5782C682CD8A6D8CC582BD82E9904D978A81420A8F5782DF82E982D982C782C98EEB906C82BD82BF82CC90B8905F97CD82C682C882E9814200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041189CC8CEC82CC8B468EEC00000000000000000000000000000000000000000000815C82A482BD82DC82E082E882CC82AB82B682E3815C0A89CC8CEC82C682CD89CC955082CC8CEC82E881420A8F5782DF82E982D982C782C98EEB906C82BD82BF82CC8E7882A682C682C882E9814220000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000212")
|
||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
||||
|
||||
// RE-confirmed entry layout (546 bytes each):
|
||||
// +0x000 char[32] name
|
||||
// +0x020 char[512] description
|
||||
// +0x220 u8 color_id (slot index, 1-based)
|
||||
// +0x221 u8 bead_type (effect ID)
|
||||
// Response: u8 count + count × 546 bytes.
|
||||
beadTypes, err := s.server.divaRepo.GetBeads()
|
||||
if err != nil || len(beadTypes) == 0 {
|
||||
beadTypes = defaultBeadTypes
|
||||
}
|
||||
|
||||
lang := getLangStrings(s.server)
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint8(uint8(len(beadTypes)))
|
||||
for i, bt := range beadTypes {
|
||||
name, desc := lang.beadName(bt), lang.beadDescription(bt)
|
||||
bf.WriteBytes(stringsupport.PaddedString(name, 32, true))
|
||||
bf.WriteBytes(stringsupport.PaddedString(desc, 512, true))
|
||||
bf.WriteUint8(uint8(i + 1)) // color_id: slot 1..N
|
||||
bf.WriteUint8(uint8(bt)) // bead_type: effect ID
|
||||
}
|
||||
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfSetKiju(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfSetKiju)
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
||||
beadIndex := int(pkt.Unk1)
|
||||
expiry := TimeAdjusted().Add(24 * time.Hour)
|
||||
if err := s.server.divaRepo.AssignBead(s.charID, beadIndex, expiry); err != nil {
|
||||
s.logger.Warn("Failed to assign bead",
|
||||
zap.Uint32("charID", s.charID),
|
||||
zap.Int("beadIndex", beadIndex),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
s.currentBeadIndex = beadIndex
|
||||
}
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00})
|
||||
}
|
||||
|
||||
func handleMsgMhfAddUdPoint(s *Session, p mhfpacket.MHFPacket) {
|
||||
@@ -169,6 +206,17 @@ func handleMsgMhfAddUdPoint(s *Session, p mhfpacket.MHFPacket) {
|
||||
zap.Uint32("bonusPoints", pkt.BonusPoints),
|
||||
zap.Error(err))
|
||||
}
|
||||
if s.currentBeadIndex >= 0 {
|
||||
total := int(pkt.QuestPoints) + int(pkt.BonusPoints)
|
||||
if total > 0 {
|
||||
if err := s.server.divaRepo.AddBeadPoints(s.charID, s.currentBeadIndex, total); err != nil {
|
||||
s.logger.Warn("Failed to add bead points",
|
||||
zap.Uint32("charID", s.charID),
|
||||
zap.Int("beadIndex", s.currentBeadIndex),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
||||
@@ -176,23 +224,92 @@ func handleMsgMhfAddUdPoint(s *Session, p mhfpacket.MHFPacket) {
|
||||
|
||||
func handleMsgMhfGetUdMyPoint(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfGetUdMyPoint)
|
||||
// Temporary canned response
|
||||
data, _ := hex.DecodeString("00040000013C000000FA000000000000000000040000007E0000003C02000000000000000000000000000000000000000000000000000002000004CC00000438000000000000000000000000000000000000000000000000000000020000026E00000230000000000000000000020000007D0000007D000000000000000000000000000000000000000000000000000000")
|
||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
||||
|
||||
// RE confirms: no count prefix. Client hardcodes exactly 8 loop iterations.
|
||||
// Per-entry stride is 18 bytes:
|
||||
// +0x00 u8 bead_index
|
||||
// +0x01 u32 points
|
||||
// +0x05 u32 points_dupe (same value as points)
|
||||
// +0x09 u8 unk1 (half-period: 0=first 12h, 1=second 12h)
|
||||
// +0x0A u32 unk2
|
||||
// +0x0E u32 unk3
|
||||
// Total: 8 × 18 = 144 bytes.
|
||||
beadPoints, err := s.server.divaRepo.GetCharacterBeadPoints(s.charID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get bead points", zap.Uint32("charID", s.charID), zap.Error(err))
|
||||
beadPoints = map[int]int{}
|
||||
}
|
||||
activeBead := uint8(0)
|
||||
if s.currentBeadIndex >= 0 {
|
||||
activeBead = uint8(s.currentBeadIndex)
|
||||
}
|
||||
pts := uint32(0)
|
||||
if s.currentBeadIndex >= 0 {
|
||||
if p, ok := beadPoints[s.currentBeadIndex]; ok {
|
||||
pts = uint32(p)
|
||||
}
|
||||
}
|
||||
bf := byteframe.NewByteFrame()
|
||||
for i := 0; i < 8; i++ {
|
||||
bf.WriteUint8(activeBead)
|
||||
bf.WriteUint32(pts)
|
||||
bf.WriteUint32(pts) // points_dupe
|
||||
bf.WriteUint8(uint8(i % 2)) // unk1: 0=first half, 1=second half
|
||||
bf.WriteUint32(0) // unk2
|
||||
bf.WriteUint32(0) // unk3
|
||||
}
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
// udMilestones are the global contribution milestones for Diva Defense.
|
||||
// RE confirms: 64 × u64 target_values + 64 × u8 target_types + u64 total = ~585 bytes.
|
||||
// Slots 0–12 are populated; slots 13–63 are zero.
|
||||
var udMilestones = []uint64{
|
||||
500000, 1000000, 2000000, 3000000, 5000000, 7000000, 10000000,
|
||||
15000000, 20000000, 30000000, 50000000, 70000000, 100000000,
|
||||
}
|
||||
|
||||
func handleMsgMhfGetUdTotalPointInfo(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfGetUdTotalPointInfo)
|
||||
// Temporary canned response
|
||||
data, _ := hex.DecodeString("00000000000007A12000000000000F424000000000001E848000000000002DC6C000000000003D090000000000004C4B4000000000005B8D8000000000006ACFC000000000007A1200000000000089544000000000009896800000000000E4E1C00000000001312D0000000000017D78400000000001C9C3800000000002160EC00000000002625A000000000002AEA5400000000002FAF0800000000003473BC0000000000393870000000000042C1D800000000004C4B40000000000055D4A800000000005F5E10000000000008954400000000001C9C3800000000003473BC00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001020300000000000000000000000000000000000000000000000000000000000000000000000000000000101F1420")
|
||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
||||
|
||||
total, err := s.server.divaRepo.GetTotalBeadPoints()
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get total bead points", zap.Error(err))
|
||||
}
|
||||
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint8(0) // error = success
|
||||
// 64 × u64 target_values (big-endian)
|
||||
for i := 0; i < 64; i++ {
|
||||
var v uint64
|
||||
if i < len(udMilestones) {
|
||||
v = udMilestones[i]
|
||||
}
|
||||
bf.WriteUint64(v)
|
||||
}
|
||||
// 64 × u8 target_types (0 = global)
|
||||
for i := 0; i < 64; i++ {
|
||||
bf.WriteUint8(0)
|
||||
}
|
||||
// u64 total_souls
|
||||
bf.WriteUint64(uint64(total))
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfGetUdSelectedColorInfo(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfGetUdSelectedColorInfo)
|
||||
|
||||
// Unk
|
||||
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x02, 0x00, 0x00})
|
||||
// RE confirms: exactly 9 bytes = u8 error + u8[8] winning colors.
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint8(0) // error = success
|
||||
for day := 0; day < 8; day++ {
|
||||
topBead, err := s.server.divaRepo.GetTopBeadPerDay(day)
|
||||
if err != nil {
|
||||
topBead = 0
|
||||
}
|
||||
bf.WriteUint8(uint8(topBead))
|
||||
}
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfGetUdMonsterPoint(s *Session, p mhfpacket.MHFPacket) {
|
||||
@@ -329,16 +446,25 @@ func handleMsgMhfGetUdMonsterPoint(s *Session, p mhfpacket.MHFPacket) {
|
||||
|
||||
func handleMsgMhfGetUdDailyPresentList(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfGetUdDailyPresentList)
|
||||
// Temporary canned response
|
||||
data, _ := hex.DecodeString("0100001600000A5397DF00000000000000000000000000000000")
|
||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
||||
// DailyPresentList: u16 count + count × 15-byte entries.
|
||||
// Entry: u8 rank_type, u16 rank_from, u16 rank_to, u8 item_type,
|
||||
// u16 _pad0(skip), u16 item_id, u16 _pad1(skip), u16 quantity, u8 unk.
|
||||
// Padding at +6 and +10 is NOT read by the client.
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(0) // count = 0 (no entries configured)
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfGetUdNormaPresentList(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfGetUdNormaPresentList)
|
||||
// Temporary canned response
|
||||
data, _ := hex.DecodeString("0100001600000A5397DF00000000000000000000000000000000")
|
||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
||||
// NormaPresentList: u16 count + count × 19-byte entries.
|
||||
// Same layout as DailyPresent (+0x00..+0x0D), plus:
|
||||
// +0x0E u32 points_required (norma threshold)
|
||||
// +0x12 u8 bead_type (BeadType that unlocks this tier)
|
||||
// Padding at +6 and +10 NOT read.
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(0) // count = 0 (no entries configured)
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfAcquireUdItem(s *Session, p mhfpacket.MHFPacket) {
|
||||
|
||||
Reference in New Issue
Block a user