Merge pull request #182 from Mezeporta/feature/event-tent-clean

feat(campaign): implement Event Tent campaign system
This commit is contained in:
Houmgaor
2026-03-20 11:35:56 +01:00
committed by GitHub
13 changed files with 5546 additions and 143 deletions

View File

@@ -420,7 +420,7 @@ func TestBatchParseMultiField(t *testing.T) {
if err := pkt.Parse(bf, ctx); err != nil {
t.Fatal(err)
}
if pkt.Unk0 != 2 || pkt.Unk1 != 3 || pkt.Unk2 != 4 {
if pkt.QuestID != 2 || pkt.ItemType != 3 || pkt.Quantity != 4 {
t.Error("field mismatch")
}
})

View File

@@ -515,8 +515,8 @@ func TestBuildParseStateCampaign(t *testing.T) {
if parsed.CampaignID != tt.campaignID {
t.Errorf("CampaignID = %d, want %d", parsed.CampaignID, tt.campaignID)
}
if parsed.Unk1 != tt.unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, tt.unk1)
if parsed.NullPadding != tt.unk1 {
t.Errorf("NullPadding = %d, want %d", parsed.NullPadding, tt.unk1)
}
})
}
@@ -526,15 +526,14 @@ func TestBuildParseStateCampaign(t *testing.T) {
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
func TestBuildParseApplyCampaign(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint32
unk1 uint16
unk2 []byte
name string
ackHandle uint32
campaignID uint32
code string
}{
{"typical", 0x55667788, 5, 10, make([]byte, 16)},
{"zero", 0, 0, 0, make([]byte, 16)},
{"max", 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF, make([]byte, 16)},
{"typical", 0x55667788, 5, "TESTCODE"},
{"zero", 0, 0, ""},
{"max", 0xFFFFFFFF, 0xFFFFFFFF, "MAXCODE"},
}
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
@@ -542,9 +541,11 @@ func TestBuildParseApplyCampaign(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ackHandle)
bf.WriteUint32(tt.unk0)
bf.WriteUint16(tt.unk1)
bf.WriteBytes(tt.unk2)
bf.WriteUint32(tt.campaignID)
bf.WriteUint16(0) // zeroed
codeBytes := make([]byte, 16)
copy(codeBytes, []byte(tt.code))
bf.WriteBytes(codeBytes)
_, _ = bf.Seek(0, io.SeekStart)
parsed := &MsgMhfApplyCampaign{}
@@ -555,14 +556,11 @@ func TestBuildParseApplyCampaign(t *testing.T) {
if parsed.AckHandle != tt.ackHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
}
if parsed.Unk0 != tt.unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, tt.unk0)
if parsed.CampaignID != tt.campaignID {
t.Errorf("CampaignID = %d, want %d", parsed.CampaignID, tt.campaignID)
}
if parsed.Unk1 != tt.unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, tt.unk1)
}
if len(parsed.Unk2) != len(tt.unk2) {
t.Errorf("Unk2 len = %d, want %d", len(parsed.Unk2), len(tt.unk2))
if parsed.Code != tt.code {
t.Errorf("Code = %q, want %q", parsed.Code, tt.code)
}
})
}

View File

@@ -11,9 +11,7 @@ import (
// MsgMhfAcquireItem represents the MSG_MHF_ACQUIRE_ITEM
type MsgMhfAcquireItem struct {
AckHandle uint32
Unk0 uint16
Length uint16
Unk1 []uint32
RewardIDs []uint32
}
// Opcode returns the ID associated with this packet type.
@@ -24,10 +22,10 @@ func (m *MsgMhfAcquireItem) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfAcquireItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint16()
m.Length = bf.ReadUint16()
for i := 0; i < int(m.Length); i++ {
m.Unk1 = append(m.Unk1, bf.ReadUint32())
bf.ReadUint16() // Zeroed
ids := bf.ReadUint16()
for i := uint16(0); i < ids; i++ {
m.RewardIDs = append(m.RewardIDs, bf.ReadUint32())
}
return nil
}

View File

@@ -2,6 +2,7 @@ package mhfpacket
import (
"errors"
"erupe-ce/common/bfutil"
"erupe-ce/common/byteframe"
"erupe-ce/network"
"erupe-ce/network/clientctx"
@@ -9,10 +10,9 @@ import (
// MsgMhfApplyCampaign represents the MSG_MHF_APPLY_CAMPAIGN
type MsgMhfApplyCampaign struct {
AckHandle uint32
Unk0 uint32
Unk1 uint16
Unk2 []byte
AckHandle uint32
CampaignID uint32
Code string
}
// Opcode returns the ID associated with this packet type.
@@ -23,9 +23,9 @@ func (m *MsgMhfApplyCampaign) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfApplyCampaign) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint32()
m.Unk1 = bf.ReadUint16()
m.Unk2 = bf.ReadBytes(16)
m.CampaignID = bf.ReadUint32()
bf.ReadUint16() // Zeroed
m.Code = string(bfutil.UpToNull(bf.ReadBytes(16)))
return nil
}

View File

@@ -11,8 +11,6 @@ import (
// MsgMhfEnumerateItem represents the MSG_MHF_ENUMERATE_ITEM
type MsgMhfEnumerateItem struct {
AckHandle uint32
Unk0 uint16
Unk1 uint16
CampaignID uint32
}
@@ -24,8 +22,8 @@ func (m *MsgMhfEnumerateItem) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfEnumerateItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint16()
m.Unk1 = bf.ReadUint16()
bf.ReadUint16() // Zeroed
bf.ReadUint16() // Always 2
m.CampaignID = bf.ReadUint32()
return nil
}

View File

@@ -10,9 +10,9 @@ import (
// MsgMhfStateCampaign represents the MSG_MHF_STATE_CAMPAIGN
type MsgMhfStateCampaign struct {
AckHandle uint32
CampaignID uint32
Unk1 uint16
AckHandle uint32
CampaignID uint32
NullPadding uint16
}
// Opcode returns the ID associated with this packet type.
@@ -24,7 +24,7 @@ func (m *MsgMhfStateCampaign) Opcode() network.PacketID {
func (m *MsgMhfStateCampaign) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32()
m.CampaignID = bf.ReadUint32()
m.Unk1 = bf.ReadUint16()
m.NullPadding = bf.ReadUint16() //0 in Z2
return nil
}

View File

@@ -11,12 +11,9 @@ import (
// MsgMhfTransferItem represents the MSG_MHF_TRANSFER_ITEM
type MsgMhfTransferItem struct {
AckHandle uint32
// looking at packets, these were static across sessions and did not actually
// correlate with any item IDs that would make sense to get after quests so
// I have no idea what this actually does
Unk0 uint32
Unk1 uint8
Unk2 uint16
QuestID uint32
ItemType uint8
Quantity uint16
}
// Opcode returns the ID associated with this packet type.
@@ -27,10 +24,10 @@ func (m *MsgMhfTransferItem) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfTransferItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint32()
m.Unk1 = bf.ReadUint8()
m.QuestID = bf.ReadUint32()
m.ItemType = bf.ReadUint8()
bf.ReadUint8() // Zeroed
m.Unk2 = bf.ReadUint16()
m.Quantity = bf.ReadUint16()
return nil
}

View File

@@ -102,8 +102,8 @@ func TestParseCoverage_VariableLength(t *testing.T) {
if err := pkt.Parse(parsed, ctx); err != nil {
t.Errorf("Parse() error: %v", err)
}
if len(pkt.Unk1) != 2 {
t.Errorf("expected 2 items, got %d", len(pkt.Unk1))
if len(pkt.RewardIDs) != 2 {
t.Errorf("expected 2 items, got %d", len(pkt.RewardIDs))
}
})

View File

@@ -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))
}

View File

@@ -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()

View File

@@ -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)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
-- Campaign / Event Tent system tables.
CREATE TABLE IF NOT EXISTS public.campaigns (
id INTEGER PRIMARY KEY,
min_hr INTEGER,
max_hr INTEGER,
min_sr INTEGER,
max_sr INTEGER,
min_gr INTEGER,
max_gr INTEGER,
reward_type INTEGER,
stamps INTEGER,
receive_type INTEGER,
background_id INTEGER,
start_time TIMESTAMP WITH TIME ZONE,
end_time TIMESTAMP WITH TIME ZONE,
title TEXT,
reward TEXT,
link TEXT,
code_prefix TEXT
);
CREATE TABLE IF NOT EXISTS public.campaign_categories (
id SERIAL PRIMARY KEY,
type INTEGER,
title TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS public.campaign_category_links (
id SERIAL PRIMARY KEY,
campaign_id INTEGER REFERENCES public.campaigns(id) ON DELETE CASCADE,
category_id INTEGER REFERENCES public.campaign_categories(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS public.campaign_rewards (
id SERIAL PRIMARY KEY,
campaign_id INTEGER REFERENCES public.campaigns(id) ON DELETE CASCADE,
item_type INTEGER,
quantity INTEGER,
item_id INTEGER
);
CREATE TABLE IF NOT EXISTS public.campaign_rewards_claimed (
character_id INTEGER REFERENCES public.characters(id) ON DELETE CASCADE,
reward_id INTEGER REFERENCES public.campaign_rewards(id) ON DELETE CASCADE,
PRIMARY KEY (character_id, reward_id)
);
CREATE TABLE IF NOT EXISTS public.campaign_state (
id SERIAL PRIMARY KEY,
campaign_id INTEGER REFERENCES public.campaigns(id) ON DELETE CASCADE,
character_id INTEGER REFERENCES public.characters(id) ON DELETE CASCADE,
code TEXT
);
CREATE TABLE IF NOT EXISTS public.campaign_codes (
code TEXT PRIMARY KEY,
campaign_id INTEGER REFERENCES public.campaigns(id) ON DELETE CASCADE,
multi BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS public.campaign_quest (
campaign_id INTEGER REFERENCES public.campaigns(id) ON DELETE CASCADE,
character_id INTEGER REFERENCES public.characters(id) ON DELETE CASCADE,
PRIMARY KEY (campaign_id, character_id)
);