mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
feat(campaign): implement Event Tent campaign system
Adds the full campaign/event-tent feature: Packet layer: - MsgMhfApplyCampaign: Unk0/Unk1/Unk2 → CampaignID/Code (null-terminated 16-byte string) - MsgMhfAcquireItem: Unk0/Length/Unk1 → RewardIDs []uint32 - MsgMhfEnumerateItem: remove Unk0/Unk1 (RE'd: zeroed + always-2, ignored) - MsgMhfStateCampaign: Unk1 → NullPadding (RE'd: always zero) - MsgMhfTransferItem: Unk0/Unk1/Unk2 → QuestID/ItemType/Quantity (RE'd) Handler layer (handlers_campaign.go): - handleMsgMhfEnumerateCampaign: reads campaigns, categories, links from DB; prefix moved into pascal string slot 3 of each event entry (RE confirmed 3-section response format — removes spurious intermediate section) - handleMsgMhfStateCampaign: returns stamp count and redeemable flag - handleMsgMhfApplyCampaign: validates and records code redemption - handleMsgMhfEnumerateItem: lists rewards gated by stamp count - handleMsgMhfAcquireItem: marks rewards as claimed - handleMsgMhfTransferItem: records campaign quest completion (item_type=9) Quest gating (handlers_quest.go): - makeEventQuest: for QuestTypeSpecialTool, check campaign stamp count and deadline before allowing the quest (WriteBool true/false) Database: - 0010_campaign.sql: 8-table schema (campaigns, categories, links, rewards, claimed, state, codes, quest) - CampaignDemo.sql: community-researched live game campaign data
This commit is contained in:
@@ -9,55 +9,83 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CampaignEvent represents a promotional campaign event.
|
||||
type CampaignEvent struct {
|
||||
ID uint32
|
||||
Unk0 uint32
|
||||
MinHR int16
|
||||
MaxHR int16
|
||||
MinSR int16
|
||||
MaxSR int16
|
||||
MinGR int16
|
||||
MaxGR int16
|
||||
Unk1 uint16
|
||||
Unk2 uint8
|
||||
Unk3 uint8
|
||||
Unk4 uint16
|
||||
Unk5 uint16
|
||||
Start time.Time
|
||||
End time.Time
|
||||
Unk6 uint8
|
||||
String0 string
|
||||
String1 string
|
||||
String2 string
|
||||
String3 string
|
||||
Link string
|
||||
Prefix string
|
||||
Categories []uint16
|
||||
ID uint32 `db:"id"`
|
||||
MinHR int16 `db:"min_hr"`
|
||||
MaxHR int16 `db:"max_hr"`
|
||||
MinSR int16 `db:"min_sr"`
|
||||
MaxSR int16 `db:"max_sr"`
|
||||
MinGR int16 `db:"min_gr"`
|
||||
MaxGR int16 `db:"max_gr"`
|
||||
RewardType uint16 `db:"reward_type"`
|
||||
Stamps uint8 `db:"stamps"`
|
||||
ReceiveType uint8 `db:"receive_type"`
|
||||
BackgroundID uint16 `db:"background_id"`
|
||||
Start time.Time `db:"start_time"`
|
||||
End time.Time `db:"end_time"`
|
||||
Title string `db:"title"`
|
||||
Reward string `db:"reward"`
|
||||
Link string `db:"link"`
|
||||
Prefix string `db:"code_prefix"`
|
||||
}
|
||||
|
||||
// CampaignCategory represents a category grouping for campaign events.
|
||||
type CampaignCategory struct {
|
||||
ID uint16
|
||||
Type uint8
|
||||
Title string
|
||||
Description string
|
||||
ID uint16 `db:"id"`
|
||||
Type uint8 `db:"type"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
}
|
||||
|
||||
// CampaignLink links a campaign event to its items/rewards.
|
||||
type CampaignLink struct {
|
||||
CategoryID uint16
|
||||
CampaignID uint32
|
||||
CategoryID uint16 `db:"category_id"`
|
||||
CampaignID uint32 `db:"campaign_id"`
|
||||
}
|
||||
|
||||
type CampaignReward struct {
|
||||
ID uint32 `db:"id"`
|
||||
ItemType uint16 `db:"item_type"`
|
||||
Quantity uint16 `db:"quantity"`
|
||||
ItemID uint16 `db:"item_id"`
|
||||
Deadline time.Time `db:"deadline"`
|
||||
}
|
||||
|
||||
// campaignRequiredStamps returns the stamp requirement for a campaign,
|
||||
// clamping to a minimum of 1. Campaigns with 0 stamps in the DB are
|
||||
// treated as requiring a single stamp (code redemption) to unlock.
|
||||
func campaignRequiredStamps(stamps int) int {
|
||||
if stamps < 1 {
|
||||
return 1
|
||||
}
|
||||
return stamps
|
||||
}
|
||||
|
||||
func handleMsgMhfEnumerateCampaign(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfEnumerateCampaign)
|
||||
if s.server.db == nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
bf := byteframe.NewByteFrame()
|
||||
|
||||
events := []CampaignEvent{}
|
||||
categories := []CampaignCategory{}
|
||||
var events []CampaignEvent
|
||||
var categories []CampaignCategory
|
||||
var campaignLinks []CampaignLink
|
||||
|
||||
err := s.server.db.Select(&events, "SELECT id,min_hr,max_hr,min_sr,max_sr,min_gr,max_gr,reward_type,stamps,receive_type,background_id,start_time,end_time,title,reward,link,code_prefix FROM campaigns")
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
err = s.server.db.Select(&categories, "SELECT id, type, title, description FROM campaign_categories")
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
err = s.server.db.Select(&campaignLinks, "SELECT campaign_id, category_id FROM campaign_category_links")
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
if len(events) > 255 {
|
||||
bf.WriteUint8(255)
|
||||
bf.WriteUint16(uint16(len(events)))
|
||||
@@ -66,7 +94,7 @@ func handleMsgMhfEnumerateCampaign(s *Session, p mhfpacket.MHFPacket) {
|
||||
}
|
||||
for _, event := range events {
|
||||
bf.WriteUint32(event.ID)
|
||||
bf.WriteUint32(event.Unk0)
|
||||
bf.WriteUint32(0)
|
||||
bf.WriteInt16(event.MinHR)
|
||||
bf.WriteInt16(event.MaxHR)
|
||||
bf.WriteInt16(event.MinSR)
|
||||
@@ -75,34 +103,19 @@ func handleMsgMhfEnumerateCampaign(s *Session, p mhfpacket.MHFPacket) {
|
||||
bf.WriteInt16(event.MinGR)
|
||||
bf.WriteInt16(event.MaxGR)
|
||||
}
|
||||
bf.WriteUint16(event.Unk1)
|
||||
bf.WriteUint8(event.Unk2)
|
||||
bf.WriteUint8(event.Unk3)
|
||||
bf.WriteUint16(event.Unk4)
|
||||
bf.WriteUint16(event.Unk5)
|
||||
bf.WriteUint16(event.RewardType)
|
||||
bf.WriteUint8(event.Stamps)
|
||||
bf.WriteUint8(event.ReceiveType)
|
||||
bf.WriteUint16(event.BackgroundID)
|
||||
bf.WriteUint16(0)
|
||||
bf.WriteUint32(uint32(event.Start.Unix()))
|
||||
bf.WriteUint32(uint32(event.End.Unix()))
|
||||
bf.WriteUint8(event.Unk6)
|
||||
ps.Uint8(bf, event.String0, true)
|
||||
ps.Uint8(bf, event.String1, true)
|
||||
ps.Uint8(bf, event.String2, true)
|
||||
ps.Uint8(bf, event.String3, true)
|
||||
bf.WriteBool(event.End.Before(time.Now()))
|
||||
ps.Uint8(bf, event.Title, true)
|
||||
ps.Uint8(bf, event.Reward, true)
|
||||
ps.Uint8(bf, event.Prefix, true)
|
||||
ps.Uint8(bf, "", false)
|
||||
ps.Uint8(bf, event.Link, true)
|
||||
for i := range event.Categories {
|
||||
campaignLinks = append(campaignLinks, CampaignLink{event.Categories[i], event.ID})
|
||||
}
|
||||
}
|
||||
|
||||
if len(events) > 255 {
|
||||
bf.WriteUint8(255)
|
||||
bf.WriteUint16(uint16(len(events)))
|
||||
} else {
|
||||
bf.WriteUint8(uint8(len(events)))
|
||||
}
|
||||
for _, event := range events {
|
||||
bf.WriteUint32(event.ID)
|
||||
bf.WriteUint8(1) // Always 1?
|
||||
bf.WriteBytes([]byte(event.Prefix))
|
||||
}
|
||||
|
||||
if len(categories) > 255 {
|
||||
@@ -137,43 +150,185 @@ func handleMsgMhfEnumerateCampaign(s *Session, p mhfpacket.MHFPacket) {
|
||||
|
||||
func handleMsgMhfStateCampaign(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfStateCampaign)
|
||||
if s.server.db == nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(1)
|
||||
bf.WriteUint16(0)
|
||||
var required int
|
||||
var deadline time.Time
|
||||
var stamps []uint32
|
||||
|
||||
err := s.server.db.Select(&stamps, "SELECT id FROM campaign_state WHERE campaign_id = $1 AND character_id = $2", pkt.CampaignID, s.charID)
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
err = s.server.db.QueryRow(`SELECT stamps, end_time FROM campaigns WHERE id = $1`, pkt.CampaignID).Scan(&required, &deadline)
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
bf.WriteUint16(uint16(len(stamps)))
|
||||
required = campaignRequiredStamps(required)
|
||||
|
||||
if len(stamps) >= required && deadline.After(time.Now()) {
|
||||
bf.WriteUint16(2)
|
||||
} else {
|
||||
bf.WriteUint16(0)
|
||||
}
|
||||
|
||||
for _, v := range stamps {
|
||||
bf.WriteUint32(v)
|
||||
}
|
||||
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfApplyCampaign(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfApplyCampaign)
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint32(1)
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
|
||||
if s.server.db == nil {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
// Check if the code exists, belongs to this campaign, and check if it's a multi-code
|
||||
var multi bool
|
||||
err := s.server.db.QueryRow(`SELECT multi FROM public.campaign_codes WHERE code = $1 AND campaign_id = $2`, pkt.Code, pkt.CampaignID).Scan(&multi)
|
||||
if err != nil {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the code is already used
|
||||
var exists bool
|
||||
if multi {
|
||||
err = s.server.db.QueryRow(`SELECT COUNT(*) > 0 FROM public.campaign_state WHERE code = $1 AND character_id = $2`, pkt.Code, s.charID).Scan(&exists)
|
||||
} else {
|
||||
err = s.server.db.QueryRow(`SELECT COUNT(*) > 0 FROM public.campaign_state WHERE code = $1`, pkt.Code).Scan(&exists)
|
||||
}
|
||||
if err != nil || exists {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.server.db.Exec(`INSERT INTO public.campaign_state (code, campaign_id, character_id) VALUES ($1, $2, $3)`, pkt.Code, pkt.CampaignID, s.charID)
|
||||
if err != nil {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
}
|
||||
|
||||
func handleMsgMhfEnumerateItem(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfEnumerateItem)
|
||||
items := []struct {
|
||||
Unk0 uint32
|
||||
Unk1 uint16
|
||||
Unk2 uint16
|
||||
Unk3 uint16
|
||||
Unk4 uint32
|
||||
Unk5 uint32
|
||||
}{}
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint16(uint16(len(items)))
|
||||
for _, item := range items {
|
||||
bf.WriteUint32(item.Unk0)
|
||||
bf.WriteUint16(item.Unk1)
|
||||
bf.WriteUint16(item.Unk2)
|
||||
bf.WriteUint16(item.Unk3)
|
||||
bf.WriteUint32(item.Unk4)
|
||||
bf.WriteUint32(item.Unk5)
|
||||
if s.server.db == nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
bf := byteframe.NewByteFrame()
|
||||
|
||||
var stamps, required, rewardType uint16
|
||||
err := s.server.db.QueryRow(`SELECT COUNT(*) FROM campaign_state WHERE campaign_id = $1 AND character_id = $2`, pkt.CampaignID, s.charID).Scan(&stamps)
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
err = s.server.db.QueryRow(`SELECT stamps, reward_type FROM campaigns WHERE id = $1`, pkt.CampaignID).Scan(&required, &rewardType)
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
required = uint16(campaignRequiredStamps(int(required)))
|
||||
|
||||
if stamps >= required {
|
||||
var items []CampaignReward
|
||||
if rewardType == 2 {
|
||||
var exists int
|
||||
err = s.server.db.QueryRow(`SELECT COUNT(*) FROM campaign_quest WHERE campaign_id = $1 AND character_id = $2`, pkt.CampaignID, s.charID).Scan(&exists)
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
if exists > 0 {
|
||||
err = s.server.db.Select(&items, `
|
||||
SELECT id, item_type, quantity, item_id, TO_TIMESTAMP(0) AS deadline FROM campaign_rewards
|
||||
WHERE campaign_id = $1 AND item_type != 9
|
||||
AND NOT EXISTS (SELECT 1 FROM campaign_rewards_claimed WHERE reward_id = campaign_rewards.id AND character_id = $2)
|
||||
`, pkt.CampaignID, s.charID)
|
||||
} else {
|
||||
err = s.server.db.Select(&items, `
|
||||
SELECT cr.id, cr.item_type, cr.quantity, cr.item_id, COALESCE(c.end_time, TO_TIMESTAMP(0)) AS deadline FROM campaign_rewards cr
|
||||
JOIN campaigns c ON cr.campaign_id = c.id
|
||||
WHERE campaign_id = $1 AND item_type = 9`, pkt.CampaignID)
|
||||
}
|
||||
} else {
|
||||
err = s.server.db.Select(&items, `
|
||||
SELECT id, item_type, quantity, item_id, TO_TIMESTAMP(0) AS deadline FROM campaign_rewards
|
||||
WHERE campaign_id = $1
|
||||
AND NOT EXISTS (SELECT 1 FROM campaign_rewards_claimed WHERE reward_id = campaign_rewards.id AND character_id = $2)
|
||||
`, pkt.CampaignID, s.charID)
|
||||
}
|
||||
if err != nil {
|
||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
|
||||
bf.WriteUint16(uint16(len(items)))
|
||||
for _, item := range items {
|
||||
bf.WriteUint32(item.ID)
|
||||
bf.WriteUint16(item.ItemType)
|
||||
bf.WriteUint16(item.Quantity)
|
||||
bf.WriteUint16(item.ItemID) //HACK:placed quest id in this field to fit with Item No pattern. however it could be another field... possibly the other unks.
|
||||
bf.WriteUint16(0) //Unk4, gets cast to uint8
|
||||
bf.WriteUint32(0) //Unk5
|
||||
bf.WriteUint32(uint32(item.Deadline.Unix()))
|
||||
}
|
||||
if len(items) == 0 {
|
||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
} else {
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
} else {
|
||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
}
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfAcquireItem(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfAcquireItem)
|
||||
if s.server.db == nil {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
for _, id := range pkt.RewardIDs {
|
||||
_, err := s.server.db.Exec(`INSERT INTO campaign_rewards_claimed (reward_id, character_id) VALUES ($1, $2)`, id, s.charID)
|
||||
if err != nil {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
}
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
}
|
||||
|
||||
func handleMsgMhfTransferItem(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfTransferItem)
|
||||
if s.server.db == nil {
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
if pkt.ItemType == 9 {
|
||||
var campaignID uint32
|
||||
err := s.server.db.QueryRow(`
|
||||
SELECT ce.campaign_id FROM campaign_rewards ce
|
||||
JOIN event_quests eq ON ce.item_id = eq.quest_id
|
||||
WHERE eq.id = $1
|
||||
`, pkt.QuestID).Scan(&campaignID)
|
||||
if err == nil {
|
||||
_, err = s.server.db.Exec(`INSERT INTO campaign_quest (campaign_id, character_id) VALUES ($1, $2)`, campaignID, s.charID)
|
||||
if err != nil {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
}
|
||||
|
||||
@@ -9,11 +9,6 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func handleMsgMhfTransferItem(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfTransferItem)
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
||||
}
|
||||
|
||||
func handleMsgMhfEnumeratePrice(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfEnumeratePrice)
|
||||
bf := byteframe.NewByteFrame()
|
||||
|
||||
@@ -313,7 +313,34 @@ func makeEventQuest(s *Session, eq EventQuest) ([]byte, error) {
|
||||
}
|
||||
bf.WriteUint8(eq.QuestType)
|
||||
if eq.QuestType == QuestTypeSpecialTool {
|
||||
bf.WriteBool(false)
|
||||
var stamps, required int
|
||||
var deadline time.Time
|
||||
err := s.server.db.QueryRow(`SELECT COUNT(*) FROM campaign_state WHERE campaign_id = (
|
||||
SELECT campaign_id
|
||||
FROM campaign_rewards
|
||||
WHERE item_type = 9
|
||||
AND item_id = $1
|
||||
LIMIT 1
|
||||
) AND character_id = $2`, eq.QuestID, s.charID).Scan(&stamps)
|
||||
if err != nil {
|
||||
bf.WriteBool(false)
|
||||
} else {
|
||||
err = s.server.db.QueryRow(`SELECT stamps, end_time
|
||||
FROM campaigns
|
||||
WHERE id = (
|
||||
SELECT campaign_id
|
||||
FROM campaign_rewards
|
||||
WHERE item_type = 9
|
||||
AND item_id = $1
|
||||
LIMIT 1
|
||||
)`, eq.QuestID).Scan(&required, &deadline)
|
||||
required = campaignRequiredStamps(required)
|
||||
if err == nil && stamps >= required && deadline.After(time.Now()) {
|
||||
bf.WriteBool(true)
|
||||
} else {
|
||||
bf.WriteBool(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bf.WriteBool(true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user