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:
Houmgaor
2026-03-20 11:22:25 +01:00
parent 97ef09be64
commit 77e7969579
10 changed files with 5528 additions and 120 deletions

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