refactor(channelserver): extract GachaRepository and HouseRepository

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.
This commit is contained in:
Houmgaor
2026-02-20 22:30:28 +01:00
parent ab9fd0bc9c
commit 93f28c721a
6 changed files with 496 additions and 217 deletions

View File

@@ -91,10 +91,7 @@ func spendGachaCoin(s *Session, quantity uint16) {
}
func transactGacha(s *Session, gachaID uint32, rollID uint8) (int, error) {
var itemType uint8
var itemNumber uint16
var rolls int
err := s.server.db.QueryRowx(`SELECT item_type, item_number, rolls FROM gacha_entries WHERE gacha_id = $1 AND entry_type = $2`, gachaID, rollID).Scan(&itemType, &itemNumber, &rolls)
itemType, itemNumber, rolls, err := s.server.gachaRepo.GetEntryForTransaction(gachaID, rollID)
if err != nil {
return 0, err
}
@@ -123,15 +120,7 @@ func transactGacha(s *Session, gachaID uint32, rollID uint8) (int, error) {
}
func getGuaranteedItems(s *Session, gachaID uint32, rollID uint8) []GachaItem {
var rewards []GachaItem
var reward GachaItem
items, err := s.server.db.Queryx(`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = (SELECT id FROM gacha_entries WHERE entry_type = $1 AND gacha_id = $2)`, rollID, gachaID)
if err == nil {
for items.Next() {
_ = items.StructScan(&reward)
rewards = append(rewards, reward)
}
}
rewards, _ := s.server.gachaRepo.GetGuaranteedItems(rollID, gachaID)
return rewards
}
@@ -224,41 +213,27 @@ func handleMsgMhfReceiveGachaItem(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfPlayNormalGacha(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfPlayNormalGacha)
bf := byteframe.NewByteFrame()
var entries []GachaEntry
var entry GachaEntry
var rewards []GachaItem
var reward GachaItem
rolls, err := transactGacha(s, pkt.GachaID, pkt.RollType)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
return
}
rows, err := s.server.db.Queryx(`SELECT id, weight, rarity FROM gacha_entries WHERE gacha_id = $1 AND entry_type = 100 ORDER BY weight DESC`, pkt.GachaID)
entries, err := s.server.gachaRepo.GetRewardPool(pkt.GachaID)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
return
}
for rows.Next() {
err = rows.StructScan(&entry)
if err != nil {
continue
}
entries = append(entries, entry)
}
rewardEntries, _ := getRandomEntries(entries, rolls, false)
temp := byteframe.NewByteFrame()
for i := range rewardEntries {
rows, err := s.server.db.Queryx(`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = $1`, rewardEntries[i].ID)
entryItems, err := s.server.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
if err != nil {
continue
}
for rows.Next() {
err = rows.StructScan(&reward)
if err != nil {
continue
}
for _, reward := range entryItems {
rewards = append(rewards, reward)
temp.WriteUint8(reward.ItemType)
temp.WriteUint16(reward.ItemID)
@@ -276,10 +251,7 @@ func handleMsgMhfPlayNormalGacha(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfPlayStepupGacha(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfPlayStepupGacha)
bf := byteframe.NewByteFrame()
var entries []GachaEntry
var entry GachaEntry
var rewards []GachaItem
var reward GachaItem
rolls, err := transactGacha(s, pkt.GachaID, pkt.RollType)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
@@ -288,39 +260,28 @@ func handleMsgMhfPlayStepupGacha(s *Session, p mhfpacket.MHFPacket) {
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.db.Exec(`DELETE FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`, pkt.GachaID, s.charID); err != nil {
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.db.Exec(`INSERT INTO gacha_stepup (gacha_id, step, character_id) VALUES ($1, $2, $3)`, pkt.GachaID, pkt.RollType+1, s.charID); err != nil {
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))
}
rows, err := s.server.db.Queryx(`SELECT id, weight, rarity FROM gacha_entries WHERE gacha_id = $1 AND entry_type = 100 ORDER BY weight DESC`, pkt.GachaID)
entries, err := s.server.gachaRepo.GetRewardPool(pkt.GachaID)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
return
}
for rows.Next() {
err = rows.StructScan(&entry)
if err != nil {
continue
}
entries = append(entries, entry)
}
guaranteedItems := getGuaranteedItems(s, pkt.GachaID, pkt.RollType)
rewardEntries, _ := getRandomEntries(entries, rolls, false)
temp := byteframe.NewByteFrame()
for i := range rewardEntries {
rows, err := s.server.db.Queryx(`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = $1`, rewardEntries[i].ID)
entryItems, err := s.server.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
if err != nil {
continue
}
for rows.Next() {
err = rows.StructScan(&reward)
if err != nil {
continue
}
for _, reward := range entryItems {
rewards = append(rewards, reward)
temp.WriteUint8(reward.ItemType)
temp.WriteUint16(reward.ItemID)
@@ -346,12 +307,10 @@ func handleMsgMhfPlayStepupGacha(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetStepupStatus(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetStepupStatus)
// TODO: Reset daily (noon)
var step uint8
_ = s.server.db.QueryRow(`SELECT step FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`, pkt.GachaID, s.charID).Scan(&step)
var stepCheck int
_ = s.server.db.QueryRow(`SELECT COUNT(1) FROM gacha_entries WHERE gacha_id = $1 AND entry_type = $2`, pkt.GachaID, step).Scan(&stepCheck)
if stepCheck == 0 {
if _, err := s.server.db.Exec(`DELETE FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`, pkt.GachaID, s.charID); err != nil {
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
@@ -364,17 +323,11 @@ func handleMsgMhfGetStepupStatus(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetBoxGachaInfo(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetBoxGachaInfo)
entries, err := s.server.db.Queryx(`SELECT entry_id FROM gacha_box WHERE gacha_id = $1 AND character_id = $2`, pkt.GachaID, s.charID)
entryIDs, err := s.server.gachaRepo.GetBoxEntryIDs(pkt.GachaID, s.charID)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
return
}
var entryIDs []uint32
for entries.Next() {
var entryID uint32
_ = entries.Scan(&entryID)
entryIDs = append(entryIDs, entryID)
}
bf := byteframe.NewByteFrame()
bf.WriteUint8(uint8(len(entryIDs)))
for i := range entryIDs {
@@ -387,41 +340,27 @@ func handleMsgMhfGetBoxGachaInfo(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfPlayBoxGacha(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfPlayBoxGacha)
bf := byteframe.NewByteFrame()
var entries []GachaEntry
var entry GachaEntry
var rewards []GachaItem
var reward GachaItem
rolls, err := transactGacha(s, pkt.GachaID, pkt.RollType)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
return
}
rows, err := s.server.db.Queryx(`SELECT id, weight, rarity FROM gacha_entries WHERE gacha_id = $1 AND entry_type = 100 ORDER BY weight DESC`, pkt.GachaID)
entries, err := s.server.gachaRepo.GetRewardPool(pkt.GachaID)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
return
}
for rows.Next() {
err = rows.StructScan(&entry)
if err == nil {
entries = append(entries, entry)
}
}
rewardEntries, _ := getRandomEntries(entries, rolls, true)
for i := range rewardEntries {
items, err := s.server.db.Queryx(`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = $1`, rewardEntries[i].ID)
entryItems, err := s.server.gachaRepo.GetItemsForEntry(rewardEntries[i].ID)
if err != nil {
continue
}
if _, err := s.server.db.Exec(`INSERT INTO gacha_box (gacha_id, entry_id, character_id) VALUES ($1, $2, $3)`, pkt.GachaID, rewardEntries[i].ID, s.charID); err != nil {
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))
}
for items.Next() {
err = items.StructScan(&reward)
if err == nil {
rewards = append(rewards, reward)
}
}
rewards = append(rewards, entryItems...)
}
bf.WriteUint8(uint8(len(rewards)))
for _, r := range rewards {
@@ -436,7 +375,7 @@ func handleMsgMhfPlayBoxGacha(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfResetBoxGachaInfo(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfResetBoxGachaInfo)
if _, err := s.server.db.Exec("DELETE FROM gacha_box WHERE gacha_id = $1 AND character_id = $2", pkt.GachaID, s.charID); err != nil {
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))

View File

@@ -8,37 +8,11 @@ import (
"erupe-ce/common/token"
_config "erupe-ce/config"
"erupe-ce/network/mhfpacket"
"fmt"
"go.uber.org/zap"
"io"
"time"
)
const warehouseNamesQuery = `
SELECT
COALESCE(item0name, ''),
COALESCE(item1name, ''),
COALESCE(item2name, ''),
COALESCE(item3name, ''),
COALESCE(item4name, ''),
COALESCE(item5name, ''),
COALESCE(item6name, ''),
COALESCE(item7name, ''),
COALESCE(item8name, ''),
COALESCE(item9name, ''),
COALESCE(equip0name, ''),
COALESCE(equip1name, ''),
COALESCE(equip2name, ''),
COALESCE(equip3name, ''),
COALESCE(equip4name, ''),
COALESCE(equip5name, ''),
COALESCE(equip6name, ''),
COALESCE(equip7name, ''),
COALESCE(equip8name, ''),
COALESCE(equip9name, '')
FROM warehouse
`
func handleMsgMhfUpdateInterior(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfUpdateInterior)
if len(pkt.InteriorData) > 64 {
@@ -46,7 +20,7 @@ func handleMsgMhfUpdateInterior(s *Session, p mhfpacket.MHFPacket) {
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
if _, err := s.server.db.Exec(`UPDATE user_binary SET house_furniture=$1 WHERE id=$2`, pkt.InteriorData, s.charID); err != nil {
if err := s.server.houseRepo.UpdateInterior(s.charID, pkt.InteriorData); err != nil {
s.logger.Error("Failed to update house furniture", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
@@ -67,16 +41,12 @@ func handleMsgMhfEnumerateHouse(s *Session, p mhfpacket.MHFPacket) {
bf := byteframe.NewByteFrame()
bf.WriteUint16(0)
var houses []HouseData
houseQuery := `SELECT c.id, hr, gr, name, COALESCE(ub.house_state, 2) as house_state, COALESCE(ub.house_password, '') as house_password
FROM characters c LEFT JOIN user_binary ub ON ub.id = c.id WHERE c.id=$1`
switch pkt.Method {
case 1:
friendsList, _ := s.server.charRepo.ReadString(s.charID, "friends")
cids := stringsupport.CSVElems(friendsList)
for _, cid := range cids {
house := HouseData{}
row := s.server.db.QueryRowx(houseQuery, cid)
err := row.StructScan(&house)
house, err := s.server.houseRepo.GetHouseByCharID(uint32(cid))
if err == nil {
houses = append(houses, house)
}
@@ -91,32 +61,20 @@ func handleMsgMhfEnumerateHouse(s *Session, p mhfpacket.MHFPacket) {
break
}
for _, member := range guildMembers {
house := HouseData{}
row := s.server.db.QueryRowx(houseQuery, member.CharID)
err = row.StructScan(&house)
house, err := s.server.houseRepo.GetHouseByCharID(member.CharID)
if err == nil {
houses = append(houses, house)
}
}
case 3:
houseQuery = `SELECT c.id, hr, gr, name, COALESCE(ub.house_state, 2) as house_state, COALESCE(ub.house_password, '') as house_password
FROM characters c LEFT JOIN user_binary ub ON ub.id = c.id WHERE name ILIKE $1`
house := HouseData{}
rows, err := s.server.db.Queryx(houseQuery, fmt.Sprintf(`%%%s%%`, pkt.Name))
result, err := s.server.houseRepo.SearchHousesByName(pkt.Name)
if err != nil {
s.logger.Error("Failed to query houses by name", zap.Error(err))
} else {
defer func() { _ = rows.Close() }()
for rows.Next() {
if err := rows.StructScan(&house); err == nil {
houses = append(houses, house)
}
}
houses = result
}
case 4:
house := HouseData{}
row := s.server.db.QueryRowx(houseQuery, pkt.CharID)
err := row.StructScan(&house)
house, err := s.server.houseRepo.GetHouseByCharID(pkt.CharID)
if err == nil {
houses = append(houses, house)
}
@@ -149,7 +107,7 @@ func handleMsgMhfUpdateHouse(s *Session, p mhfpacket.MHFPacket) {
// 03 = open friends
// 04 = open guild
// 05 = open friends+guild
if _, err := s.server.db.Exec(`UPDATE user_binary SET house_state=$1, house_password=$2 WHERE id=$3`, pkt.State, pkt.Password, s.charID); err != nil {
if err := s.server.houseRepo.UpdateHouseState(s.charID, pkt.State, pkt.Password); err != nil {
s.logger.Error("Failed to update house state", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
@@ -159,10 +117,8 @@ func handleMsgMhfLoadHouse(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadHouse)
bf := byteframe.NewByteFrame()
state := uint8(2) // Default to password-protected if DB fails
var password string
if err := s.server.db.QueryRow(`SELECT COALESCE(house_state, 2) as house_state, COALESCE(house_password, '') as house_password FROM user_binary WHERE id=$1
`, pkt.CharID).Scan(&state, &password); err != nil {
state, password, err := s.server.houseRepo.GetHouseAccess(pkt.CharID)
if err != nil {
s.logger.Error("Failed to read house state", zap.Error(err))
}
@@ -208,9 +164,7 @@ func handleMsgMhfLoadHouse(s *Session, p mhfpacket.MHFPacket) {
}
}
var houseTier, houseData, houseFurniture, bookshelf, gallery, tore, garden []byte
_ = s.server.db.QueryRow(`SELECT house_tier, house_data, house_furniture, bookshelf, gallery, tore, garden FROM user_binary WHERE id=$1
`, pkt.CharID).Scan(&houseTier, &houseData, &houseFurniture, &bookshelf, &gallery, &tore, &garden)
houseTier, houseData, houseFurniture, bookshelf, gallery, tore, garden, _ := s.server.houseRepo.GetHouseContents(pkt.CharID)
if houseFurniture == nil {
houseFurniture = make([]byte, 20)
}
@@ -247,8 +201,7 @@ func handleMsgMhfLoadHouse(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetMyhouseInfo(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetMyhouseInfo)
var data []byte
_ = s.server.db.QueryRow(`SELECT mission FROM user_binary WHERE id=$1`, s.charID).Scan(&data)
data, _ := s.server.houseRepo.GetMission(s.charID)
if len(data) > 0 {
doAckBufSucceed(s, pkt.AckHandle, data)
} else {
@@ -263,7 +216,7 @@ func handleMsgMhfUpdateMyhouseInfo(s *Session, p mhfpacket.MHFPacket) {
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
if _, err := s.server.db.Exec("UPDATE user_binary SET mission=$1 WHERE id=$2", pkt.Data, s.charID); err != nil {
if err := s.server.houseRepo.UpdateMission(s.charID, pkt.Data); err != nil {
s.logger.Error("Failed to update myhouse mission", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
@@ -345,45 +298,30 @@ type Title struct {
func handleMsgMhfEnumerateTitle(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateTitle)
var count uint16
bf := byteframe.NewByteFrame()
bf.WriteUint16(0)
bf.WriteUint16(0) // Unk
rows, err := s.server.db.Queryx("SELECT id, unlocked_at, updated_at FROM titles WHERE char_id=$1", s.charID)
titles, err := s.server.houseRepo.GetTitles(s.charID)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
return
}
for rows.Next() {
title := &Title{}
err = rows.StructScan(&title)
if err != nil {
continue
}
count++
for _, title := range titles {
bf.WriteUint16(title.ID)
bf.WriteUint16(0) // Unk
bf.WriteUint32(uint32(title.Acquired.Unix()))
bf.WriteUint32(uint32(title.Updated.Unix()))
}
_, _ = bf.Seek(0, io.SeekStart)
bf.WriteUint16(count)
bf.WriteUint16(uint16(len(titles)))
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfAcquireTitle(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfAcquireTitle)
for _, title := range pkt.TitleIDs {
var exists int
err := s.server.db.QueryRow(`SELECT count(*) FROM titles WHERE id=$1 AND char_id=$2`, title, s.charID).Scan(&exists)
if err != nil || exists == 0 {
if _, err := s.server.db.Exec(`INSERT INTO titles VALUES ($1, $2, now(), now())`, title, s.charID); err != nil {
s.logger.Error("Failed to insert title", zap.Error(err))
}
} else {
if _, err := s.server.db.Exec(`UPDATE titles SET updated_at=now() WHERE id=$1 AND char_id=$2`, title, s.charID); err != nil {
s.logger.Error("Failed to update title", zap.Error(err))
}
if err := s.server.houseRepo.AcquireTitle(title, s.charID); err != nil {
s.logger.Error("Failed to acquire title", zap.Error(err))
}
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
@@ -392,13 +330,7 @@ func handleMsgMhfAcquireTitle(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfResetTitle(s *Session, p mhfpacket.MHFPacket) {}
func initializeWarehouse(s *Session) {
var t int
err := s.server.db.QueryRow("SELECT character_id FROM warehouse WHERE character_id=$1", s.charID).Scan(&t)
if err != nil {
if _, err := s.server.db.Exec("INSERT INTO warehouse (character_id) VALUES ($1)", s.charID); err != nil {
s.logger.Error("Failed to initialize warehouse", zap.Error(err))
}
}
s.server.houseRepo.InitializeWarehouse(s.charID)
}
func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) {
@@ -409,11 +341,7 @@ func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) {
switch pkt.Operation {
case 0:
var count uint8
itemNames := make([]string, 10)
equipNames := make([]string, 10)
_ = s.server.db.QueryRow(fmt.Sprintf("%s WHERE character_id=$1", warehouseNamesQuery), s.charID).Scan(&itemNames[0],
&itemNames[1], &itemNames[2], &itemNames[3], &itemNames[4], &itemNames[5], &itemNames[6], &itemNames[7], &itemNames[8], &itemNames[9], &equipNames[0],
&equipNames[1], &equipNames[2], &equipNames[3], &equipNames[4], &equipNames[5], &equipNames[6], &equipNames[7], &equipNames[8], &equipNames[9])
itemNames, equipNames, _ := s.server.houseRepo.GetWarehouseNames(s.charID)
bf.WriteUint32(0)
bf.WriteUint16(10000) // Usages
temp := byteframe.NewByteFrame()
@@ -441,15 +369,8 @@ func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) {
if pkt.BoxIndex > 9 {
break
}
switch pkt.BoxType {
case 0:
if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE warehouse SET item%dname=$1 WHERE character_id=$2", pkt.BoxIndex), pkt.Name, s.charID); err != nil {
s.logger.Error("Failed to rename warehouse item box", zap.Error(err))
}
case 1:
if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE warehouse SET equip%dname=$1 WHERE character_id=$2", pkt.BoxIndex), pkt.Name, s.charID); err != nil {
s.logger.Error("Failed to rename warehouse equip box", zap.Error(err))
}
if err := s.server.houseRepo.RenameWarehouseBox(s.charID, pkt.BoxType, pkt.BoxIndex, pkt.Name); err != nil {
s.logger.Error("Failed to rename warehouse box", zap.Error(err))
}
case 3:
bf.WriteUint32(0) // Usage renewal time, >1 = disabled
@@ -472,7 +393,7 @@ func addWarehouseItem(s *Session, item mhfitem.MHFItemStack) {
giftBox := warehouseGetItems(s, 10)
item.WarehouseID = token.RNG.Uint32()
giftBox = append(giftBox, item)
if _, err := s.server.db.Exec("UPDATE warehouse SET item10=$1 WHERE character_id=$2", mhfitem.SerializeWarehouseItems(giftBox), s.charID); err != nil {
if err := s.server.houseRepo.SetWarehouseItemData(s.charID, 10, mhfitem.SerializeWarehouseItems(giftBox)); err != nil {
s.logger.Error("Failed to update warehouse gift box", zap.Error(err))
}
}
@@ -484,7 +405,7 @@ func warehouseGetItems(s *Session, index uint8) []mhfitem.MHFItemStack {
if index > 10 {
return items
}
_ = s.server.db.QueryRow(fmt.Sprintf(`SELECT item%d FROM warehouse WHERE character_id=$1`, index), s.charID).Scan(&data)
data, _ = s.server.houseRepo.GetWarehouseItemData(s.charID, index)
if len(data) > 0 {
box := byteframe.NewByteFrameFromBytes(data)
numStacks := box.ReadUint16()
@@ -502,7 +423,7 @@ func warehouseGetEquipment(s *Session, index uint8) []mhfitem.MHFEquipment {
if index > 10 {
return equipment
}
_ = s.server.db.QueryRow(fmt.Sprintf(`SELECT equip%d FROM warehouse WHERE character_id=$1`, index), s.charID).Scan(&data)
data, _ = s.server.houseRepo.GetWarehouseEquipData(s.charID, index)
if len(data) > 0 {
box := byteframe.NewByteFrameFromBytes(data)
numStacks := box.ReadUint16()
@@ -559,7 +480,7 @@ func handleMsgMhfUpdateWarehouse(s *Session, p mhfpacket.MHFPacket) {
zap.Int("data_size", dataSize),
)
_, err = s.server.db.Exec(fmt.Sprintf(`UPDATE warehouse SET item%d=$1 WHERE character_id=$2`, pkt.BoxIndex), serialized, s.charID)
err = s.server.houseRepo.SetWarehouseItemData(s.charID, pkt.BoxIndex, serialized)
if err != nil {
s.logger.Error("Failed to update warehouse items",
zap.Error(err),
@@ -605,7 +526,7 @@ func handleMsgMhfUpdateWarehouse(s *Session, p mhfpacket.MHFPacket) {
zap.Int("data_size", dataSize),
)
_, err = s.server.db.Exec(fmt.Sprintf(`UPDATE warehouse SET equip%d=$1 WHERE character_id=$2`, pkt.BoxIndex), serialized, s.charID)
err = s.server.houseRepo.SetWarehouseEquipData(s.charID, pkt.BoxIndex, serialized)
if err != nil {
s.logger.Error("Failed to update warehouse equipment",
zap.Error(err),

View File

@@ -95,20 +95,12 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
return
}
rows, err := s.server.db.Queryx("SELECT id, min_gr, min_hr, name, url_banner, url_feature, url_thumbnail, wide, recommended, gacha_type, hidden FROM gacha_shop")
gachas, err := s.server.gachaRepo.ListShop()
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
bf := byteframe.NewByteFrame()
var gacha Gacha
var gachas []Gacha
for rows.Next() {
err = rows.StructScan(&gacha)
if err == nil {
gachas = append(gachas, gacha)
}
}
bf.WriteUint16(uint16(len(gachas)))
bf.WriteUint16(uint16(len(gachas)))
for _, g := range gachas {
@@ -141,25 +133,13 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
case 2: // Actual gacha
bf := byteframe.NewByteFrame()
bf.WriteUint32(pkt.ShopID)
var gachaType int
_ = s.server.db.QueryRow(`SELECT gacha_type FROM gacha_shop WHERE id = $1`, pkt.ShopID).Scan(&gachaType)
rows, err := s.server.db.Queryx(`SELECT entry_type, id, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points, COALESCE(name, '') AS name FROM gacha_entries WHERE gacha_id = $1 ORDER BY weight DESC`, pkt.ShopID)
gachaType, _ := s.server.gachaRepo.GetShopType(pkt.ShopID)
entries, err := s.server.gachaRepo.GetAllEntries(pkt.ShopID)
if err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
var divisor float64
_ = s.server.db.QueryRow(`SELECT COALESCE(SUM(weight) / 100000.0, 0) AS chance FROM gacha_entries WHERE gacha_id = $1`, pkt.ShopID).Scan(&divisor)
var entry GachaEntry
var entries []GachaEntry
var item GachaItem
for rows.Next() {
err = rows.StructScan(&entry)
if err == nil {
entries = append(entries, entry)
}
}
divisor, _ := s.server.gachaRepo.GetWeightDivisor(pkt.ShopID)
bf.WriteUint16(uint16(len(entries)))
for _, ge := range entries {
var items []GachaItem
@@ -176,16 +156,10 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint8(ge.Rarity)
bf.WriteUint8(ge.Rolls)
rows, err = s.server.db.Queryx(`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id=$1`, ge.ID)
items, err := s.server.gachaRepo.GetItemsForEntry(ge.ID)
if err != nil {
bf.WriteUint8(0)
} else {
for rows.Next() {
err = rows.StructScan(&item)
if err == nil {
items = append(items, item)
}
}
bf.WriteUint8(uint8(len(items)))
}

View File

@@ -0,0 +1,226 @@
package channelserver
import (
"github.com/jmoiron/sqlx"
)
// GachaRepository centralizes all database access for gacha-related tables
// (gacha_shop, gacha_entries, gacha_items, gacha_stepup, gacha_box).
type GachaRepository struct {
db *sqlx.DB
}
// NewGachaRepository creates a new GachaRepository.
func NewGachaRepository(db *sqlx.DB) *GachaRepository {
return &GachaRepository{db: db}
}
// GetEntryForTransaction reads the cost type/amount and roll count for a gacha transaction.
func (r *GachaRepository) GetEntryForTransaction(gachaID uint32, rollID uint8) (itemType uint8, itemNumber uint16, rolls int, err error) {
err = r.db.QueryRowx(
`SELECT item_type, item_number, rolls FROM gacha_entries WHERE gacha_id = $1 AND entry_type = $2`,
gachaID, rollID,
).Scan(&itemType, &itemNumber, &rolls)
return
}
// GetRewardPool returns the entry_type=100 reward pool for a gacha, ordered by weight descending.
func (r *GachaRepository) GetRewardPool(gachaID uint32) ([]GachaEntry, error) {
var entries []GachaEntry
rows, err := r.db.Queryx(
`SELECT id, weight, rarity FROM gacha_entries WHERE gacha_id = $1 AND entry_type = 100 ORDER BY weight DESC`,
gachaID,
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var entry GachaEntry
if err := rows.StructScan(&entry); err == nil {
entries = append(entries, entry)
}
}
return entries, nil
}
// GetItemsForEntry returns the items associated with a gacha entry ID.
func (r *GachaRepository) GetItemsForEntry(entryID uint32) ([]GachaItem, error) {
var items []GachaItem
rows, err := r.db.Queryx(
`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = $1`,
entryID,
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item GachaItem
if err := rows.StructScan(&item); err == nil {
items = append(items, item)
}
}
return items, nil
}
// GetGuaranteedItems returns items for the entry matching a roll type and gacha ID.
func (r *GachaRepository) GetGuaranteedItems(rollType uint8, gachaID uint32) ([]GachaItem, error) {
var items []GachaItem
rows, err := r.db.Queryx(
`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = (SELECT id FROM gacha_entries WHERE entry_type = $1 AND gacha_id = $2)`,
rollType, gachaID,
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item GachaItem
if err := rows.StructScan(&item); err == nil {
items = append(items, item)
}
}
return items, nil
}
// Stepup methods
// GetStepupStep returns the current stepup step for a character on a gacha.
func (r *GachaRepository) GetStepupStep(gachaID uint32, charID uint32) (uint8, error) {
var step uint8
err := r.db.QueryRow(
`SELECT step FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`,
gachaID, charID,
).Scan(&step)
return step, err
}
// HasEntryType returns whether a gacha has any entries of the given type.
func (r *GachaRepository) HasEntryType(gachaID uint32, entryType uint8) (bool, error) {
var count int
err := r.db.QueryRow(
`SELECT COUNT(1) FROM gacha_entries WHERE gacha_id = $1 AND entry_type = $2`,
gachaID, entryType,
).Scan(&count)
return count > 0, err
}
// DeleteStepup removes the stepup state for a character on a gacha.
func (r *GachaRepository) DeleteStepup(gachaID uint32, charID uint32) error {
_, err := r.db.Exec(
`DELETE FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`,
gachaID, charID,
)
return err
}
// InsertStepup records a new stepup step for a character on a gacha.
func (r *GachaRepository) InsertStepup(gachaID uint32, step uint8, charID uint32) error {
_, err := r.db.Exec(
`INSERT INTO gacha_stepup (gacha_id, step, character_id) VALUES ($1, $2, $3)`,
gachaID, step, charID,
)
return err
}
// Box gacha methods
// GetBoxEntryIDs returns the entry IDs already drawn for a box gacha.
func (r *GachaRepository) GetBoxEntryIDs(gachaID uint32, charID uint32) ([]uint32, error) {
var ids []uint32
rows, err := r.db.Queryx(
`SELECT entry_id FROM gacha_box WHERE gacha_id = $1 AND character_id = $2`,
gachaID, charID,
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var id uint32
if err := rows.Scan(&id); err == nil {
ids = append(ids, id)
}
}
return ids, nil
}
// InsertBoxEntry records a drawn entry in a box gacha.
func (r *GachaRepository) InsertBoxEntry(gachaID uint32, entryID uint32, charID uint32) error {
_, err := r.db.Exec(
`INSERT INTO gacha_box (gacha_id, entry_id, character_id) VALUES ($1, $2, $3)`,
gachaID, entryID, charID,
)
return err
}
// DeleteBoxEntries resets all drawn entries for a box gacha.
func (r *GachaRepository) DeleteBoxEntries(gachaID uint32, charID uint32) error {
_, err := r.db.Exec(
`DELETE FROM gacha_box WHERE gacha_id = $1 AND character_id = $2`,
gachaID, charID,
)
return err
}
// Shop listing methods
// ListShop returns all gacha shop definitions.
func (r *GachaRepository) ListShop() ([]Gacha, error) {
var gachas []Gacha
rows, err := r.db.Queryx(
`SELECT id, min_gr, min_hr, name, url_banner, url_feature, url_thumbnail, wide, recommended, gacha_type, hidden FROM gacha_shop`,
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var g Gacha
if err := rows.StructScan(&g); err == nil {
gachas = append(gachas, g)
}
}
return gachas, nil
}
// GetShopType returns the gacha_type for a gacha shop ID.
func (r *GachaRepository) GetShopType(shopID uint32) (int, error) {
var gachaType int
err := r.db.QueryRow(
`SELECT gacha_type FROM gacha_shop WHERE id = $1`,
shopID,
).Scan(&gachaType)
return gachaType, err
}
// GetAllEntries returns all entries for a gacha, ordered by weight descending.
func (r *GachaRepository) GetAllEntries(gachaID uint32) ([]GachaEntry, error) {
var entries []GachaEntry
rows, err := r.db.Queryx(
`SELECT entry_type, id, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points, COALESCE(name, '') AS name FROM gacha_entries WHERE gacha_id = $1 ORDER BY weight DESC`,
gachaID,
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var entry GachaEntry
if err := rows.StructScan(&entry); err == nil {
entries = append(entries, entry)
}
}
return entries, nil
}
// GetWeightDivisor returns the total weight / 100000 for probability display.
func (r *GachaRepository) GetWeightDivisor(gachaID uint32) (float64, error) {
var divisor float64
err := r.db.QueryRow(
`SELECT COALESCE(SUM(weight) / 100000.0, 0) AS chance FROM gacha_entries WHERE gacha_id = $1`,
gachaID,
).Scan(&divisor)
return divisor, err
}

View File

@@ -0,0 +1,215 @@
package channelserver
import (
"fmt"
"github.com/jmoiron/sqlx"
)
// HouseRepository centralizes all database access for house-related tables
// (user_binary house columns, warehouse, titles).
type HouseRepository struct {
db *sqlx.DB
}
// NewHouseRepository creates a new HouseRepository.
func NewHouseRepository(db *sqlx.DB) *HouseRepository {
return &HouseRepository{db: db}
}
// user_binary house columns
// UpdateInterior saves the house furniture layout.
func (r *HouseRepository) UpdateInterior(charID uint32, data []byte) error {
_, err := r.db.Exec(`UPDATE user_binary SET house_furniture=$1 WHERE id=$2`, data, charID)
return err
}
const houseQuery = `SELECT c.id, hr, gr, name, COALESCE(ub.house_state, 2) as house_state, COALESCE(ub.house_password, '') as house_password
FROM characters c LEFT JOIN user_binary ub ON ub.id = c.id WHERE c.id=$1`
// GetHouseByCharID returns house data for a single character.
func (r *HouseRepository) GetHouseByCharID(charID uint32) (HouseData, error) {
var house HouseData
err := r.db.QueryRowx(houseQuery, charID).StructScan(&house)
return house, err
}
// SearchHousesByName returns houses matching a name pattern (case-insensitive).
func (r *HouseRepository) SearchHousesByName(name string) ([]HouseData, error) {
var houses []HouseData
rows, err := r.db.Queryx(
`SELECT c.id, hr, gr, name, COALESCE(ub.house_state, 2) as house_state, COALESCE(ub.house_password, '') as house_password
FROM characters c LEFT JOIN user_binary ub ON ub.id = c.id WHERE name ILIKE $1`,
fmt.Sprintf(`%%%s%%`, name),
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var house HouseData
if err := rows.StructScan(&house); err == nil {
houses = append(houses, house)
}
}
return houses, nil
}
// UpdateHouseState sets the house visibility state and password.
func (r *HouseRepository) UpdateHouseState(charID uint32, state uint8, password string) error {
_, err := r.db.Exec(`UPDATE user_binary SET house_state=$1, house_password=$2 WHERE id=$3`, state, password, charID)
return err
}
// GetHouseAccess returns the house state and password for access control checks.
func (r *HouseRepository) GetHouseAccess(charID uint32) (state uint8, password string, err error) {
state = 2 // default to password-protected
err = r.db.QueryRow(
`SELECT COALESCE(house_state, 2) as house_state, COALESCE(house_password, '') as house_password FROM user_binary WHERE id=$1`,
charID,
).Scan(&state, &password)
return
}
// GetHouseContents returns all house content columns for rendering a house visit.
func (r *HouseRepository) GetHouseContents(charID uint32) (houseTier, houseData, houseFurniture, bookshelf, gallery, tore, garden []byte, err error) {
err = r.db.QueryRow(
`SELECT house_tier, house_data, house_furniture, bookshelf, gallery, tore, garden FROM user_binary WHERE id=$1`,
charID,
).Scan(&houseTier, &houseData, &houseFurniture, &bookshelf, &gallery, &tore, &garden)
return
}
// GetMission returns the myhouse mission data.
func (r *HouseRepository) GetMission(charID uint32) ([]byte, error) {
var data []byte
err := r.db.QueryRow(`SELECT mission FROM user_binary WHERE id=$1`, charID).Scan(&data)
return data, err
}
// UpdateMission saves the myhouse mission data.
func (r *HouseRepository) UpdateMission(charID uint32, data []byte) error {
_, err := r.db.Exec(`UPDATE user_binary SET mission=$1 WHERE id=$2`, data, charID)
return err
}
// Warehouse methods
// InitializeWarehouse ensures a warehouse row exists for the character.
func (r *HouseRepository) InitializeWarehouse(charID uint32) {
var t int
err := r.db.QueryRow(`SELECT character_id FROM warehouse WHERE character_id=$1`, charID).Scan(&t)
if err != nil {
_, _ = r.db.Exec(`INSERT INTO warehouse (character_id) VALUES ($1)`, charID)
}
}
const warehouseNamesSQL = `
SELECT
COALESCE(item0name, ''),
COALESCE(item1name, ''),
COALESCE(item2name, ''),
COALESCE(item3name, ''),
COALESCE(item4name, ''),
COALESCE(item5name, ''),
COALESCE(item6name, ''),
COALESCE(item7name, ''),
COALESCE(item8name, ''),
COALESCE(item9name, ''),
COALESCE(equip0name, ''),
COALESCE(equip1name, ''),
COALESCE(equip2name, ''),
COALESCE(equip3name, ''),
COALESCE(equip4name, ''),
COALESCE(equip5name, ''),
COALESCE(equip6name, ''),
COALESCE(equip7name, ''),
COALESCE(equip8name, ''),
COALESCE(equip9name, '')
FROM warehouse WHERE character_id=$1`
// GetWarehouseNames returns item and equipment box names.
func (r *HouseRepository) GetWarehouseNames(charID uint32) (itemNames, equipNames [10]string, err error) {
err = r.db.QueryRow(warehouseNamesSQL, charID).Scan(
&itemNames[0], &itemNames[1], &itemNames[2], &itemNames[3], &itemNames[4],
&itemNames[5], &itemNames[6], &itemNames[7], &itemNames[8], &itemNames[9],
&equipNames[0], &equipNames[1], &equipNames[2], &equipNames[3], &equipNames[4],
&equipNames[5], &equipNames[6], &equipNames[7], &equipNames[8], &equipNames[9],
)
return
}
// RenameWarehouseBox renames an item or equipment warehouse box.
// boxType 0 = items, 1 = equipment. boxIndex must be 0-9.
func (r *HouseRepository) RenameWarehouseBox(charID uint32, boxType uint8, boxIndex uint8, name string) error {
var col string
switch boxType {
case 0:
col = fmt.Sprintf("item%dname", boxIndex)
case 1:
col = fmt.Sprintf("equip%dname", boxIndex)
default:
return fmt.Errorf("invalid box type: %d", boxType)
}
_, err := r.db.Exec(fmt.Sprintf("UPDATE warehouse SET %s=$1 WHERE character_id=$2", col), name, charID)
return err
}
// GetWarehouseItemData returns raw serialized item data for a warehouse box.
// index 0-10 (10 = gift box).
func (r *HouseRepository) GetWarehouseItemData(charID uint32, index uint8) ([]byte, error) {
var data []byte
err := r.db.QueryRow(fmt.Sprintf(`SELECT item%d FROM warehouse WHERE character_id=$1`, index), charID).Scan(&data)
return data, err
}
// SetWarehouseItemData saves raw serialized item data for a warehouse box.
func (r *HouseRepository) SetWarehouseItemData(charID uint32, index uint8, data []byte) error {
_, err := r.db.Exec(fmt.Sprintf(`UPDATE warehouse SET item%d=$1 WHERE character_id=$2`, index), data, charID)
return err
}
// GetWarehouseEquipData returns raw serialized equipment data for a warehouse box.
func (r *HouseRepository) GetWarehouseEquipData(charID uint32, index uint8) ([]byte, error) {
var data []byte
err := r.db.QueryRow(fmt.Sprintf(`SELECT equip%d FROM warehouse WHERE character_id=$1`, index), charID).Scan(&data)
return data, err
}
// SetWarehouseEquipData saves raw serialized equipment data for a warehouse box.
func (r *HouseRepository) SetWarehouseEquipData(charID uint32, index uint8, data []byte) error {
_, err := r.db.Exec(fmt.Sprintf(`UPDATE warehouse SET equip%d=$1 WHERE character_id=$2`, index), data, charID)
return err
}
// Title methods
// GetTitles returns all titles for a character.
func (r *HouseRepository) GetTitles(charID uint32) ([]Title, error) {
var titles []Title
rows, err := r.db.Queryx(`SELECT id, unlocked_at, updated_at FROM titles WHERE char_id=$1`, charID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var title Title
if err := rows.StructScan(&title); err == nil {
titles = append(titles, title)
}
}
return titles, nil
}
// AcquireTitle inserts a new title or updates its timestamp if it already exists.
func (r *HouseRepository) AcquireTitle(titleID uint16, charID uint32) error {
var exists int
err := r.db.QueryRow(`SELECT count(*) FROM titles WHERE id=$1 AND char_id=$2`, titleID, charID).Scan(&exists)
if err != nil || exists == 0 {
_, err = r.db.Exec(`INSERT INTO titles VALUES ($1, $2, now(), now())`, titleID, charID)
} else {
_, err = r.db.Exec(`UPDATE titles SET updated_at=now() WHERE id=$1 AND char_id=$2`, titleID, charID)
}
return err
}

View File

@@ -48,6 +48,8 @@ type Server struct {
charRepo *CharacterRepository
guildRepo *GuildRepository
userRepo *UserRepository
gachaRepo *GachaRepository
houseRepo *HouseRepository
erupeConfig *_config.Config
acceptConns chan net.Conn
deleteConns chan net.Conn
@@ -121,6 +123,8 @@ func NewServer(config *Config) *Server {
s.charRepo = NewCharacterRepository(config.DB)
s.guildRepo = NewGuildRepository(config.DB)
s.userRepo = NewUserRepository(config.DB)
s.gachaRepo = NewGachaRepository(config.DB)
s.houseRepo = NewHouseRepository(config.DB)
// Mezeporta
s.stages["sl1Ns200p0a0u0"] = NewStage("sl1Ns200p0a0u0")