From 93f28c721a2f03451f0529340585e89a82bdb4ec Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Fri, 20 Feb 2026 22:30:28 +0100 Subject: [PATCH] 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. --- server/channelserver/handlers_gacha.go | 101 ++------- server/channelserver/handlers_house.go | 131 +++--------- server/channelserver/handlers_shop.go | 36 +--- server/channelserver/repo_gacha.go | 226 +++++++++++++++++++++ server/channelserver/repo_house.go | 215 ++++++++++++++++++++ server/channelserver/sys_channel_server.go | 4 + 6 files changed, 496 insertions(+), 217 deletions(-) create mode 100644 server/channelserver/repo_gacha.go create mode 100644 server/channelserver/repo_house.go diff --git a/server/channelserver/handlers_gacha.go b/server/channelserver/handlers_gacha.go index 8dbf1debe..bf912f53b 100644 --- a/server/channelserver/handlers_gacha.go +++ b/server/channelserver/handlers_gacha.go @@ -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)) diff --git a/server/channelserver/handlers_house.go b/server/channelserver/handlers_house.go index 0ef772f4d..605ffb92d 100644 --- a/server/channelserver/handlers_house.go +++ b/server/channelserver/handlers_house.go @@ -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), diff --git a/server/channelserver/handlers_shop.go b/server/channelserver/handlers_shop.go index cf1dcc170..72f9e0ad7 100644 --- a/server/channelserver/handlers_shop.go +++ b/server/channelserver/handlers_shop.go @@ -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))) } diff --git a/server/channelserver/repo_gacha.go b/server/channelserver/repo_gacha.go new file mode 100644 index 000000000..9bdbef129 --- /dev/null +++ b/server/channelserver/repo_gacha.go @@ -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 +} diff --git a/server/channelserver/repo_house.go b/server/channelserver/repo_house.go new file mode 100644 index 000000000..7c905be28 --- /dev/null +++ b/server/channelserver/repo_house.go @@ -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 +} diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 6622afc5f..ef7f85040 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -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")