mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
feat(channelserver): implement monthly guild item claim tracking
Players could never claim monthly guild items because the handler always returned 0x01 (claimed). Now tracks per-character per-type (standard/HLC/EXC) claim timestamps in the stamps table, comparing against the current month boundary to determine claim eligibility. Adds MonthStart() to gametime, extends StampRepo with GetMonthlyClaimed/SetMonthlyClaimed, and includes schema migration 31-monthly-items.sql.
This commit is contained in:
@@ -52,6 +52,7 @@ func TestHandleMsgMhfGenerateUdGuildMap(t *testing.T) {
|
||||
|
||||
func TestHandleMsgMhfCheckMonthlyItem(t *testing.T) {
|
||||
server := createMockServer()
|
||||
server.stampRepo = &mockStampRepoForItems{monthlyClaimedErr: errNotFound}
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfCheckMonthlyItem{
|
||||
@@ -73,6 +74,7 @@ func TestHandleMsgMhfCheckMonthlyItem(t *testing.T) {
|
||||
|
||||
func TestHandleMsgMhfAcquireMonthlyItem(t *testing.T) {
|
||||
server := createMockServer()
|
||||
server.stampRepo = &mockStampRepoForItems{}
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfAcquireMonthlyItem{
|
||||
|
||||
@@ -383,15 +383,48 @@ func handleMsgMhfSetGuildManageRight(s *Session, p mhfpacket.MHFPacket) {
|
||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
}
|
||||
|
||||
// monthlyTypeString maps the packet's Type field to the DB column prefix.
|
||||
func monthlyTypeString(t uint8) string {
|
||||
switch t {
|
||||
case 0:
|
||||
return "monthly"
|
||||
case 1:
|
||||
return "monthly_hl"
|
||||
case 2:
|
||||
return "monthly_ex"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func handleMsgMhfCheckMonthlyItem(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfCheckMonthlyItem)
|
||||
|
||||
typeStr := monthlyTypeString(pkt.Type)
|
||||
if typeStr == "" {
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
||||
return
|
||||
}
|
||||
|
||||
claimed, err := s.server.stampRepo.GetMonthlyClaimed(s.charID, typeStr)
|
||||
if err != nil || claimed.Before(TimeMonthStart()) {
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
||||
return
|
||||
}
|
||||
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01})
|
||||
// TODO: Implement month-by-month tracker, 0 = Not claimed, 1 = Claimed
|
||||
// Also handles HLC and EXC items, IDs = 064D, 076B
|
||||
}
|
||||
|
||||
func handleMsgMhfAcquireMonthlyItem(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfAcquireMonthlyItem)
|
||||
|
||||
typeStr := monthlyTypeString(pkt.Unk0)
|
||||
if typeStr != "" {
|
||||
if err := s.server.stampRepo.SetMonthlyClaimed(s.charID, typeStr, TimeAdjusted()); err != nil {
|
||||
s.logger.Error("Failed to set monthly item claimed", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
cfg "erupe-ce/config"
|
||||
"erupe-ce/network/mhfpacket"
|
||||
)
|
||||
|
||||
// TestGuildCreation tests basic guild creation
|
||||
@@ -822,3 +823,104 @@ func TestGuildAllianceRelationship(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- handleMsgMhfCheckMonthlyItem tests ---
|
||||
|
||||
func TestCheckMonthlyItem_NotClaimed(t *testing.T) {
|
||||
server := createMockServer()
|
||||
stampMock := &mockStampRepoForItems{
|
||||
monthlyClaimedErr: errNotFound,
|
||||
}
|
||||
server.stampRepo = stampMock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfCheckMonthlyItem{AckHandle: 100, Type: 0}
|
||||
handleMsgMhfCheckMonthlyItem(session, pkt)
|
||||
|
||||
select {
|
||||
case p := <-session.sendPackets:
|
||||
if len(p.data) < 4 {
|
||||
t.Fatalf("Response too short: %d bytes", len(p.data))
|
||||
}
|
||||
default:
|
||||
t.Error("No response packet queued")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMonthlyItem_ClaimedThisMonth(t *testing.T) {
|
||||
server := createMockServer()
|
||||
stampMock := &mockStampRepoForItems{
|
||||
monthlyClaimed: TimeAdjusted(), // claimed right now (within this month)
|
||||
}
|
||||
server.stampRepo = stampMock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfCheckMonthlyItem{AckHandle: 100, Type: 0}
|
||||
handleMsgMhfCheckMonthlyItem(session, pkt)
|
||||
|
||||
select {
|
||||
case <-session.sendPackets:
|
||||
// Response received — claimed this month should return 1
|
||||
default:
|
||||
t.Error("No response packet queued")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMonthlyItem_ClaimedLastMonth(t *testing.T) {
|
||||
server := createMockServer()
|
||||
stampMock := &mockStampRepoForItems{
|
||||
monthlyClaimed: TimeMonthStart().Add(-24 * time.Hour), // before this month
|
||||
}
|
||||
server.stampRepo = stampMock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfCheckMonthlyItem{AckHandle: 100, Type: 1}
|
||||
handleMsgMhfCheckMonthlyItem(session, pkt)
|
||||
|
||||
select {
|
||||
case <-session.sendPackets:
|
||||
// Response received — last month claim should return 0 (unclaimed)
|
||||
default:
|
||||
t.Error("No response packet queued")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMonthlyItem_UnknownType(t *testing.T) {
|
||||
server := createMockServer()
|
||||
stampMock := &mockStampRepoForItems{}
|
||||
server.stampRepo = stampMock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfCheckMonthlyItem{AckHandle: 100, Type: 99}
|
||||
handleMsgMhfCheckMonthlyItem(session, pkt)
|
||||
|
||||
select {
|
||||
case <-session.sendPackets:
|
||||
// Unknown type returns 0 (unclaimed) without DB call
|
||||
default:
|
||||
t.Error("No response packet queued")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireMonthlyItem_MarksAsClaimed(t *testing.T) {
|
||||
server := createMockServer()
|
||||
stampMock := &mockStampRepoForItems{}
|
||||
server.stampRepo = stampMock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfAcquireMonthlyItem{AckHandle: 100, Unk0: 2}
|
||||
handleMsgMhfAcquireMonthlyItem(session, pkt)
|
||||
|
||||
if !stampMock.monthlySetCalled {
|
||||
t.Error("SetMonthlyClaimed should be called")
|
||||
}
|
||||
if stampMock.monthlySetType != "monthly_ex" {
|
||||
t.Errorf("SetMonthlyClaimed type = %q, want %q", stampMock.monthlySetType, "monthly_ex")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-session.sendPackets:
|
||||
default:
|
||||
t.Error("No response packet queued")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +245,8 @@ type StampRepo interface {
|
||||
GetTotals(charID uint32, stampType string) (total, redeemed uint16, err error)
|
||||
ExchangeYearly(charID uint32) (total, redeemed uint16, err error)
|
||||
Exchange(charID uint32, stampType string) (total, redeemed uint16, err error)
|
||||
GetMonthlyClaimed(charID uint32, monthlyType string) (time.Time, error)
|
||||
SetMonthlyClaimed(charID uint32, monthlyType string, now time.Time) error
|
||||
}
|
||||
|
||||
// DistributionRepo defines the contract for distribution/event item data access.
|
||||
|
||||
@@ -663,6 +663,12 @@ type mockStampRepoForItems struct {
|
||||
exchangeErr error
|
||||
yearlyResult [2]uint16
|
||||
yearlyErr error
|
||||
|
||||
// Monthly item fields
|
||||
monthlyClaimed time.Time
|
||||
monthlyClaimedErr error
|
||||
monthlySetCalled bool
|
||||
monthlySetType string
|
||||
}
|
||||
|
||||
func (m *mockStampRepoForItems) GetChecked(_ uint32, _ string) (time.Time, error) {
|
||||
@@ -696,6 +702,16 @@ func (m *mockStampRepoForItems) Exchange(_ uint32, _ string) (uint16, uint16, er
|
||||
return m.exchangeResult[0], m.exchangeResult[1], m.exchangeErr
|
||||
}
|
||||
|
||||
func (m *mockStampRepoForItems) GetMonthlyClaimed(_ uint32, _ string) (time.Time, error) {
|
||||
return m.monthlyClaimed, m.monthlyClaimedErr
|
||||
}
|
||||
|
||||
func (m *mockStampRepoForItems) SetMonthlyClaimed(_ uint32, monthlyType string, _ time.Time) error {
|
||||
m.monthlySetCalled = true
|
||||
m.monthlySetType = monthlyType
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- mockHouseRepoForItems ---
|
||||
|
||||
type mockHouseRepoForItems struct {
|
||||
|
||||
@@ -59,3 +59,20 @@ func (r *StampRepository) Exchange(charID uint32, stampType string) (total, rede
|
||||
err = r.db.QueryRow(fmt.Sprintf("UPDATE stamps SET %s_redeemed=%s_redeemed+8 WHERE character_id=$1 RETURNING %s_total, %s_redeemed", stampType, stampType, stampType, stampType), charID).Scan(&total, &redeemed)
|
||||
return
|
||||
}
|
||||
|
||||
// GetMonthlyClaimed returns the last monthly item claim time for the given type.
|
||||
func (r *StampRepository) GetMonthlyClaimed(charID uint32, monthlyType string) (time.Time, error) {
|
||||
var claimed time.Time
|
||||
err := r.db.QueryRow(
|
||||
fmt.Sprintf("SELECT %s_claimed FROM stamps WHERE character_id=$1", monthlyType), charID,
|
||||
).Scan(&claimed)
|
||||
return claimed, err
|
||||
}
|
||||
|
||||
// SetMonthlyClaimed updates the monthly item claim time for the given type.
|
||||
func (r *StampRepository) SetMonthlyClaimed(charID uint32, monthlyType string, now time.Time) error {
|
||||
_, err := r.db.Exec(
|
||||
fmt.Sprintf("UPDATE stamps SET %s_claimed=$1 WHERE character_id=$2", monthlyType), now, charID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,4 +14,5 @@ func TimeAdjusted() time.Time { return gametime.Adjusted() }
|
||||
func TimeMidnight() time.Time { return gametime.Midnight() }
|
||||
func TimeWeekStart() time.Time { return gametime.WeekStart() }
|
||||
func TimeWeekNext() time.Time { return gametime.WeekNext() }
|
||||
func TimeMonthStart() time.Time { return gametime.MonthStart() }
|
||||
func TimeGameAbsolute() uint32 { return gametime.GameAbsolute() }
|
||||
|
||||
Reference in New Issue
Block a user