mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Centralizes all gacha_shop/gacha_entries/gacha_items/gacha_stepup/gacha_box table access into GachaRepository (15 methods) and all user_binary house columns, warehouse, and titles table access into HouseRepository (17 methods). Eliminates all direct DB calls from handlers_gacha.go and handlers_house.go.
390 lines
12 KiB
Go
390 lines
12 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"erupe-ce/common/byteframe"
|
|
"erupe-ce/network/mhfpacket"
|
|
"math/rand"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Gacha represents a gacha lottery definition.
|
|
type Gacha struct {
|
|
ID uint32 `db:"id"`
|
|
MinGR uint32 `db:"min_gr"`
|
|
MinHR uint32 `db:"min_hr"`
|
|
Name string `db:"name"`
|
|
URLBanner string `db:"url_banner"`
|
|
URLFeature string `db:"url_feature"`
|
|
URLThumbnail string `db:"url_thumbnail"`
|
|
Wide bool `db:"wide"`
|
|
Recommended bool `db:"recommended"`
|
|
GachaType uint8 `db:"gacha_type"`
|
|
Hidden bool `db:"hidden"`
|
|
}
|
|
|
|
// GachaEntry represents a gacha entry (step/box).
|
|
type GachaEntry struct {
|
|
EntryType uint8 `db:"entry_type"`
|
|
ID uint32 `db:"id"`
|
|
ItemType uint8 `db:"item_type"`
|
|
ItemNumber uint32 `db:"item_number"`
|
|
ItemQuantity uint16 `db:"item_quantity"`
|
|
Weight float64 `db:"weight"`
|
|
Rarity uint8 `db:"rarity"`
|
|
Rolls uint8 `db:"rolls"`
|
|
FrontierPoints uint16 `db:"frontier_points"`
|
|
DailyLimit uint8 `db:"daily_limit"`
|
|
Name string `db:"name"`
|
|
}
|
|
|
|
// GachaItem represents a single item in a gacha pool.
|
|
type GachaItem struct {
|
|
ItemType uint8 `db:"item_type"`
|
|
ItemID uint16 `db:"item_id"`
|
|
Quantity uint16 `db:"quantity"`
|
|
}
|
|
|
|
func handleMsgMhfGetGachaPlayHistory(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetGachaPlayHistory)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(1)
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfGetGachaPoint(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetGachaPoint)
|
|
fp, gp, gt, _ := s.server.userRepo.GetGachaPoints(s.userID)
|
|
resp := byteframe.NewByteFrame()
|
|
resp.WriteUint32(gp)
|
|
resp.WriteUint32(gt)
|
|
resp.WriteUint32(fp)
|
|
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
|
}
|
|
|
|
func handleMsgMhfUseGachaPoint(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfUseGachaPoint)
|
|
if pkt.TrialCoins > 0 {
|
|
if err := s.server.userRepo.DeductTrialCoins(s.userID, pkt.TrialCoins); err != nil {
|
|
s.logger.Error("Failed to deduct gacha trial coins", zap.Error(err))
|
|
}
|
|
}
|
|
if pkt.PremiumCoins > 0 {
|
|
if err := s.server.userRepo.DeductPremiumCoins(s.userID, pkt.PremiumCoins); err != nil {
|
|
s.logger.Error("Failed to deduct gacha premium coins", zap.Error(err))
|
|
}
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func spendGachaCoin(s *Session, quantity uint16) {
|
|
gt, _ := s.server.userRepo.GetTrialCoins(s.userID)
|
|
if quantity <= gt {
|
|
if err := s.server.userRepo.DeductTrialCoins(s.userID, uint32(quantity)); err != nil {
|
|
s.logger.Error("Failed to deduct gacha trial coins", zap.Error(err))
|
|
}
|
|
} else {
|
|
if err := s.server.userRepo.DeductPremiumCoins(s.userID, uint32(quantity)); err != nil {
|
|
s.logger.Error("Failed to deduct gacha premium coins", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func transactGacha(s *Session, gachaID uint32, rollID uint8) (int, error) {
|
|
itemType, itemNumber, rolls, err := s.server.gachaRepo.GetEntryForTransaction(gachaID, rollID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
switch itemType {
|
|
/*
|
|
valid types that need manual savedata manipulation:
|
|
- Ryoudan Points
|
|
- Bond Points
|
|
- Image Change Points
|
|
valid types that work (no additional code needed):
|
|
- Tore Points
|
|
- Festa Points
|
|
*/
|
|
case 17:
|
|
_ = addPointNetcafe(s, int(itemNumber)*-1)
|
|
case 19:
|
|
fallthrough
|
|
case 20:
|
|
spendGachaCoin(s, itemNumber)
|
|
case 21:
|
|
if err := s.server.userRepo.DeductFrontierPoints(s.userID, uint32(itemNumber)); err != nil {
|
|
s.logger.Error("Failed to deduct frontier points for gacha", zap.Error(err))
|
|
}
|
|
}
|
|
return rolls, nil
|
|
}
|
|
|
|
func getGuaranteedItems(s *Session, gachaID uint32, rollID uint8) []GachaItem {
|
|
rewards, _ := s.server.gachaRepo.GetGuaranteedItems(rollID, gachaID)
|
|
return rewards
|
|
}
|
|
|
|
func addGachaItem(s *Session, items []GachaItem) {
|
|
data, _ := s.server.charRepo.LoadColumn(s.charID, "gacha_items")
|
|
if len(data) > 0 {
|
|
numItems := int(data[0])
|
|
data = data[1:]
|
|
oldItem := byteframe.NewByteFrameFromBytes(data)
|
|
for i := 0; i < numItems; i++ {
|
|
items = append(items, GachaItem{
|
|
ItemType: oldItem.ReadUint8(),
|
|
ItemID: oldItem.ReadUint16(),
|
|
Quantity: oldItem.ReadUint16(),
|
|
})
|
|
}
|
|
}
|
|
newItem := byteframe.NewByteFrame()
|
|
newItem.WriteUint8(uint8(len(items)))
|
|
for i := range items {
|
|
newItem.WriteUint8(items[i].ItemType)
|
|
newItem.WriteUint16(items[i].ItemID)
|
|
newItem.WriteUint16(items[i].Quantity)
|
|
}
|
|
if err := s.server.charRepo.SaveColumn(s.charID, "gacha_items", newItem.Data()); err != nil {
|
|
s.logger.Error("Failed to update gacha items", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
func getRandomEntries(entries []GachaEntry, rolls int, isBox bool) ([]GachaEntry, error) {
|
|
var chosen []GachaEntry
|
|
var totalWeight float64
|
|
for i := range entries {
|
|
totalWeight += entries[i].Weight
|
|
}
|
|
for rolls != len(chosen) {
|
|
|
|
if !isBox {
|
|
result := rand.Float64() * totalWeight
|
|
for _, entry := range entries {
|
|
result -= entry.Weight
|
|
if result < 0 {
|
|
chosen = append(chosen, entry)
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
result := rand.Intn(len(entries))
|
|
chosen = append(chosen, entries[result])
|
|
entries[result] = entries[len(entries)-1]
|
|
entries = entries[:len(entries)-1]
|
|
}
|
|
}
|
|
return chosen, nil
|
|
}
|
|
|
|
func handleMsgMhfReceiveGachaItem(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfReceiveGachaItem)
|
|
data, err := s.server.charRepo.LoadColumnWithDefault(s.charID, "gacha_items", []byte{0x00})
|
|
if err != nil {
|
|
data = []byte{0x00}
|
|
}
|
|
|
|
// I think there are still some edge cases where rewards can be nulled via overflow
|
|
if data[0] > 36 || len(data) > 181 {
|
|
resp := byteframe.NewByteFrame()
|
|
resp.WriteUint8(36)
|
|
resp.WriteBytes(data[1:181])
|
|
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
|
} else {
|
|
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
}
|
|
|
|
if !pkt.Freeze {
|
|
if data[0] > 36 || len(data) > 181 {
|
|
update := byteframe.NewByteFrame()
|
|
update.WriteUint8(uint8(len(data[181:]) / 5))
|
|
update.WriteBytes(data[181:])
|
|
if err := s.server.charRepo.SaveColumn(s.charID, "gacha_items", update.Data()); err != nil {
|
|
s.logger.Error("Failed to update gacha items overflow", zap.Error(err))
|
|
}
|
|
} else {
|
|
if err := s.server.charRepo.SaveColumn(s.charID, "gacha_items", nil); err != nil {
|
|
s.logger.Error("Failed to clear gacha items", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleMsgMhfPlayNormalGacha(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPlayNormalGacha)
|
|
bf := byteframe.NewByteFrame()
|
|
var rewards []GachaItem
|
|
rolls, err := transactGacha(s, pkt.GachaID, pkt.RollType)
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
|
return
|
|
}
|
|
|
|
entries, err := s.server.gachaRepo.GetRewardPool(pkt.GachaID)
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
|
return
|
|
}
|
|
|
|
rewardEntries, _ := getRandomEntries(entries, rolls, false)
|
|
temp := byteframe.NewByteFrame()
|
|
for i := range rewardEntries {
|
|
entryItems, err := s.server.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, reward := range entryItems {
|
|
rewards = append(rewards, reward)
|
|
temp.WriteUint8(reward.ItemType)
|
|
temp.WriteUint16(reward.ItemID)
|
|
temp.WriteUint16(reward.Quantity)
|
|
temp.WriteUint8(rewardEntries[i].Rarity)
|
|
}
|
|
}
|
|
|
|
bf.WriteUint8(uint8(len(rewards)))
|
|
bf.WriteBytes(temp.Data())
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
addGachaItem(s, rewards)
|
|
}
|
|
|
|
func handleMsgMhfPlayStepupGacha(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPlayStepupGacha)
|
|
bf := byteframe.NewByteFrame()
|
|
var rewards []GachaItem
|
|
rolls, err := transactGacha(s, pkt.GachaID, pkt.RollType)
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
|
return
|
|
}
|
|
if err := s.server.userRepo.AddFrontierPointsFromGacha(s.userID, pkt.GachaID, pkt.RollType); err != nil {
|
|
s.logger.Error("Failed to award stepup gacha frontier points", zap.Error(err))
|
|
}
|
|
if err := s.server.gachaRepo.DeleteStepup(pkt.GachaID, s.charID); err != nil {
|
|
s.logger.Error("Failed to delete gacha stepup state", zap.Error(err))
|
|
}
|
|
if err := s.server.gachaRepo.InsertStepup(pkt.GachaID, pkt.RollType+1, s.charID); err != nil {
|
|
s.logger.Error("Failed to insert gacha stepup state", zap.Error(err))
|
|
}
|
|
|
|
entries, err := s.server.gachaRepo.GetRewardPool(pkt.GachaID)
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
|
return
|
|
}
|
|
|
|
guaranteedItems := getGuaranteedItems(s, pkt.GachaID, pkt.RollType)
|
|
rewardEntries, _ := getRandomEntries(entries, rolls, false)
|
|
temp := byteframe.NewByteFrame()
|
|
for i := range rewardEntries {
|
|
entryItems, err := s.server.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, reward := range entryItems {
|
|
rewards = append(rewards, reward)
|
|
temp.WriteUint8(reward.ItemType)
|
|
temp.WriteUint16(reward.ItemID)
|
|
temp.WriteUint16(reward.Quantity)
|
|
temp.WriteUint8(rewardEntries[i].Rarity)
|
|
}
|
|
}
|
|
|
|
bf.WriteUint8(uint8(len(rewards) + len(guaranteedItems)))
|
|
bf.WriteUint8(uint8(len(rewards)))
|
|
for _, item := range guaranteedItems {
|
|
bf.WriteUint8(item.ItemType)
|
|
bf.WriteUint16(item.ItemID)
|
|
bf.WriteUint16(item.Quantity)
|
|
bf.WriteUint8(0)
|
|
}
|
|
bf.WriteBytes(temp.Data())
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
addGachaItem(s, rewards)
|
|
addGachaItem(s, guaranteedItems)
|
|
}
|
|
|
|
func handleMsgMhfGetStepupStatus(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetStepupStatus)
|
|
// TODO: Reset daily (noon)
|
|
step, _ := s.server.gachaRepo.GetStepupStep(pkt.GachaID, s.charID)
|
|
hasEntry, _ := s.server.gachaRepo.HasEntryType(pkt.GachaID, step)
|
|
if !hasEntry {
|
|
if err := s.server.gachaRepo.DeleteStepup(pkt.GachaID, s.charID); err != nil {
|
|
s.logger.Error("Failed to reset gacha stepup state", zap.Error(err))
|
|
}
|
|
step = 0
|
|
}
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(step)
|
|
bf.WriteUint32(uint32(TimeAdjusted().Unix()))
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfGetBoxGachaInfo(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetBoxGachaInfo)
|
|
entryIDs, err := s.server.gachaRepo.GetBoxEntryIDs(pkt.GachaID, s.charID)
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
|
return
|
|
}
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(uint8(len(entryIDs)))
|
|
for i := range entryIDs {
|
|
bf.WriteUint32(entryIDs[i])
|
|
bf.WriteBool(true)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfPlayBoxGacha(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPlayBoxGacha)
|
|
bf := byteframe.NewByteFrame()
|
|
var rewards []GachaItem
|
|
rolls, err := transactGacha(s, pkt.GachaID, pkt.RollType)
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
|
return
|
|
}
|
|
entries, err := s.server.gachaRepo.GetRewardPool(pkt.GachaID)
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
|
return
|
|
}
|
|
rewardEntries, _ := getRandomEntries(entries, rolls, true)
|
|
for i := range rewardEntries {
|
|
entryItems, err := s.server.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if err := s.server.gachaRepo.InsertBoxEntry(pkt.GachaID, rewardEntries[i].ID, s.charID); err != nil {
|
|
s.logger.Error("Failed to insert gacha box entry", zap.Error(err))
|
|
}
|
|
rewards = append(rewards, entryItems...)
|
|
}
|
|
bf.WriteUint8(uint8(len(rewards)))
|
|
for _, r := range rewards {
|
|
bf.WriteUint8(r.ItemType)
|
|
bf.WriteUint16(r.ItemID)
|
|
bf.WriteUint16(r.Quantity)
|
|
bf.WriteUint8(0)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
addGachaItem(s, rewards)
|
|
}
|
|
|
|
func handleMsgMhfResetBoxGachaInfo(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfResetBoxGachaInfo)
|
|
if err := s.server.gachaRepo.DeleteBoxEntries(pkt.GachaID, s.charID); err != nil {
|
|
s.logger.Error("Failed to reset gacha box", zap.Error(err))
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func handleMsgMhfPlayFreeGacha(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPlayFreeGacha)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(1)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|