mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
refactor(tower): extract tower logic into TowerService
The tower repo had business logic beyond simple CRUD: AddGem used a fetch-transform-save pattern, progress capping was inline in the handler, and RP donation orchestrated multiple repo calls with conditional page advancement. Move these into a new TowerService following the established service layer pattern.
This commit is contained in:
@@ -307,7 +307,7 @@ func handleMsgMhfGetTenrouirai(s *Session, p mhfpacket.MHFPacket) {
|
||||
data = append(data, bf)
|
||||
}
|
||||
case 4:
|
||||
progress, err := s.server.towerRepo.GetTenrouiraiProgress(pkt.GuildID)
|
||||
progress, err := s.server.towerService.GetTenrouiraiProgressCapped(pkt.GuildID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to read tower mission page", zap.Error(err))
|
||||
} else {
|
||||
@@ -317,19 +317,6 @@ func handleMsgMhfGetTenrouirai(s *Session, p mhfpacket.MHFPacket) {
|
||||
tenrouirai.Progress[0].Mission3 = progress.Mission3
|
||||
}
|
||||
|
||||
if tenrouirai.Progress[0].Page < 1 {
|
||||
tenrouirai.Progress[0].Page = 1
|
||||
}
|
||||
if tenrouirai.Progress[0].Mission1 > tenrouiraiData[(tenrouirai.Progress[0].Page*3)-3].Goal {
|
||||
tenrouirai.Progress[0].Mission1 = tenrouiraiData[(tenrouirai.Progress[0].Page*3)-3].Goal
|
||||
}
|
||||
if tenrouirai.Progress[0].Mission2 > tenrouiraiData[(tenrouirai.Progress[0].Page*3)-2].Goal {
|
||||
tenrouirai.Progress[0].Mission2 = tenrouiraiData[(tenrouirai.Progress[0].Page*3)-2].Goal
|
||||
}
|
||||
if tenrouirai.Progress[0].Mission3 > tenrouiraiData[(tenrouirai.Progress[0].Page*3)-1].Goal {
|
||||
tenrouirai.Progress[0].Mission3 = tenrouiraiData[(tenrouirai.Progress[0].Page*3)-1].Goal
|
||||
}
|
||||
|
||||
for _, progress := range tenrouirai.Progress {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint8(progress.Page)
|
||||
@@ -384,33 +371,18 @@ func handleMsgMhfPostTenrouirai(s *Session, p mhfpacket.MHFPacket) {
|
||||
}
|
||||
|
||||
if pkt.Op == 2 {
|
||||
page, donated, err := s.server.towerRepo.GetGuildTowerPageAndRP(pkt.GuildID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to read guild tower state for donation", zap.Error(err))
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
|
||||
var requirement int
|
||||
for i := 0; i < (page*3)+1; i++ {
|
||||
requirement += int(tenrouiraiData[i].Cost)
|
||||
}
|
||||
|
||||
bf := byteframe.NewByteFrame()
|
||||
|
||||
sd, err := GetCharacterSaveData(s, s.charID)
|
||||
if err == nil && sd != nil {
|
||||
sd.RP -= pkt.DonatedRP
|
||||
sd.Save(s)
|
||||
if donated+int(pkt.DonatedRP) >= requirement {
|
||||
if err := s.server.towerRepo.AdvanceTenrouiraiPage(pkt.GuildID); err != nil {
|
||||
s.logger.Error("Failed to advance tower mission page", zap.Error(err))
|
||||
}
|
||||
pkt.DonatedRP = uint16(requirement - donated)
|
||||
}
|
||||
bf.WriteUint32(uint32(pkt.DonatedRP))
|
||||
if err := s.server.towerRepo.DonateGuildTowerRP(pkt.GuildID, pkt.DonatedRP); err != nil {
|
||||
s.logger.Error("Failed to update guild tower RP", zap.Error(err))
|
||||
result, err := s.server.towerService.DonateGuildTowerRP(pkt.GuildID, pkt.DonatedRP)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to process tower RP donation", zap.Error(err))
|
||||
bf.WriteUint32(0)
|
||||
} else {
|
||||
bf.WriteUint32(uint32(result.ActualDonated))
|
||||
}
|
||||
} else {
|
||||
bf.WriteUint32(0)
|
||||
@@ -509,7 +481,7 @@ func handleMsgMhfPostGemInfo(s *Session, p mhfpacket.MHFPacket) {
|
||||
switch pkt.Op {
|
||||
case 1: // Add gem
|
||||
i := int((pkt.Gem >> 8 * 5) + (pkt.Gem - pkt.Gem&0xFF00 - 1%5))
|
||||
if err := s.server.towerRepo.AddGem(s.charID, i, int(pkt.Quantity)); err != nil {
|
||||
if err := s.server.towerService.AddGem(s.charID, i, int(pkt.Quantity)); err != nil {
|
||||
s.logger.Error("Failed to update tower gems", zap.Error(err))
|
||||
}
|
||||
case 2: // Transfer gem
|
||||
|
||||
@@ -210,7 +210,6 @@ type TowerRepo interface {
|
||||
UpdateProgress(charID uint32, tr, trp, cost, block1 int32) error
|
||||
GetGems(charID uint32) (string, error)
|
||||
UpdateGems(charID uint32, gems string) error
|
||||
AddGem(charID uint32, gemIndex int, quantity int) error
|
||||
GetTenrouiraiProgress(guildID uint32) (TenrouiraiProgressData, error)
|
||||
GetTenrouiraiMissionScores(guildID uint32, missionIndex uint8) ([]TenrouiraiCharScore, error)
|
||||
GetGuildTowerRP(guildID uint32) (uint32, error)
|
||||
|
||||
@@ -944,6 +944,7 @@ type mockTowerRepo struct {
|
||||
skillsErr error
|
||||
gems string
|
||||
gemsErr error
|
||||
updatedGems string
|
||||
|
||||
progress TenrouiraiProgressData
|
||||
progressErr error
|
||||
@@ -951,9 +952,13 @@ type mockTowerRepo struct {
|
||||
scoresErr error
|
||||
guildRP uint32
|
||||
guildRPErr error
|
||||
page int
|
||||
donated int
|
||||
pageRPErr error
|
||||
page int
|
||||
donated int
|
||||
pageRPErr error
|
||||
advanceErr error
|
||||
advanceCalled bool
|
||||
donateErr error
|
||||
donatedRP uint16
|
||||
}
|
||||
|
||||
func (m *mockTowerRepo) GetTowerData(_ uint32) (TowerData, error) { return m.towerData, m.towerDataErr }
|
||||
@@ -961,8 +966,10 @@ func (m *mockTowerRepo) GetSkills(_ uint32) (string, error) { return m.s
|
||||
func (m *mockTowerRepo) UpdateSkills(_ uint32, _ string, _ int32) error { return nil }
|
||||
func (m *mockTowerRepo) UpdateProgress(_ uint32, _, _, _, _ int32) error { return nil }
|
||||
func (m *mockTowerRepo) GetGems(_ uint32) (string, error) { return m.gems, m.gemsErr }
|
||||
func (m *mockTowerRepo) UpdateGems(_ uint32, _ string) error { return nil }
|
||||
func (m *mockTowerRepo) AddGem(_ uint32, _ int, _ int) error { return nil }
|
||||
func (m *mockTowerRepo) UpdateGems(_ uint32, gems string) error {
|
||||
m.updatedGems = gems
|
||||
return nil
|
||||
}
|
||||
func (m *mockTowerRepo) GetTenrouiraiProgress(_ uint32) (TenrouiraiProgressData, error) {
|
||||
return m.progress, m.progressErr
|
||||
}
|
||||
@@ -973,8 +980,14 @@ func (m *mockTowerRepo) GetGuildTowerRP(_ uint32) (uint32, error) { return m.gui
|
||||
func (m *mockTowerRepo) GetGuildTowerPageAndRP(_ uint32) (int, int, error) {
|
||||
return m.page, m.donated, m.pageRPErr
|
||||
}
|
||||
func (m *mockTowerRepo) AdvanceTenrouiraiPage(_ uint32) error { return nil }
|
||||
func (m *mockTowerRepo) DonateGuildTowerRP(_ uint32, _ uint16) error { return nil }
|
||||
func (m *mockTowerRepo) AdvanceTenrouiraiPage(_ uint32) error {
|
||||
m.advanceCalled = true
|
||||
return m.advanceErr
|
||||
}
|
||||
func (m *mockTowerRepo) DonateGuildTowerRP(_ uint32, rp uint16) error {
|
||||
m.donatedRP = rp
|
||||
return m.donateErr
|
||||
}
|
||||
|
||||
// --- mockFestaRepo ---
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ package channelserver
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"erupe-ce/common/stringsupport"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
@@ -78,16 +76,6 @@ func (r *TowerRepository) UpdateGems(charID uint32, gems string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AddGem adds quantity to a specific gem index.
|
||||
func (r *TowerRepository) AddGem(charID uint32, gemIndex int, quantity int) error {
|
||||
gems, err := r.GetGems(charID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newGems := stringsupport.CSVSetIndex(gems, gemIndex, stringsupport.CSVGetIndex(gems, gemIndex)+quantity)
|
||||
return r.UpdateGems(charID, newGems)
|
||||
}
|
||||
|
||||
// TenrouiraiProgressData holds the guild's tenrouirai (sky corridor) progress.
|
||||
type TenrouiraiProgressData struct {
|
||||
Page uint8
|
||||
|
||||
102
server/channelserver/svc_tower.go
Normal file
102
server/channelserver/svc_tower.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"erupe-ce/common/stringsupport"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// DonateRPResult holds the outcome of a guild tower RP donation.
|
||||
type DonateRPResult struct {
|
||||
ActualDonated uint16
|
||||
Advanced bool
|
||||
}
|
||||
|
||||
// TowerService encapsulates tower business logic, sitting between handlers and repos.
|
||||
type TowerService struct {
|
||||
towerRepo TowerRepo
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTowerService creates a new TowerService.
|
||||
func NewTowerService(tr TowerRepo, log *zap.Logger) *TowerService {
|
||||
return &TowerService{
|
||||
towerRepo: tr,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// AddGem adds quantity to a specific gem index for a character.
|
||||
// This is a fetch-transform-save operation that reads the current gems CSV,
|
||||
// updates the value at the given index, and writes back.
|
||||
func (svc *TowerService) AddGem(charID uint32, gemIndex int, quantity int) error {
|
||||
gems, err := svc.towerRepo.GetGems(charID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newGems := stringsupport.CSVSetIndex(gems, gemIndex, stringsupport.CSVGetIndex(gems, gemIndex)+quantity)
|
||||
return svc.towerRepo.UpdateGems(charID, newGems)
|
||||
}
|
||||
|
||||
// GetTenrouiraiProgressCapped returns the guild's tenrouirai progress with
|
||||
// mission scores capped to their respective goals.
|
||||
func (svc *TowerService) GetTenrouiraiProgressCapped(guildID uint32) (TenrouiraiProgressData, error) {
|
||||
progress, err := svc.towerRepo.GetTenrouiraiProgress(guildID)
|
||||
if err != nil {
|
||||
return progress, err
|
||||
}
|
||||
|
||||
if progress.Page < 1 {
|
||||
progress.Page = 1
|
||||
}
|
||||
|
||||
idx := int(progress.Page*3) - 3
|
||||
if idx >= 0 && idx+2 < len(tenrouiraiData) {
|
||||
if progress.Mission1 > tenrouiraiData[idx].Goal {
|
||||
progress.Mission1 = tenrouiraiData[idx].Goal
|
||||
}
|
||||
if progress.Mission2 > tenrouiraiData[idx+1].Goal {
|
||||
progress.Mission2 = tenrouiraiData[idx+1].Goal
|
||||
}
|
||||
if progress.Mission3 > tenrouiraiData[idx+2].Goal {
|
||||
progress.Mission3 = tenrouiraiData[idx+2].Goal
|
||||
}
|
||||
}
|
||||
|
||||
return progress, nil
|
||||
}
|
||||
|
||||
// DonateGuildTowerRP processes a tower RP donation, advancing the mission page
|
||||
// if the cumulative donation meets the requirement. Returns the actual RP consumed
|
||||
// and whether the page was advanced.
|
||||
func (svc *TowerService) DonateGuildTowerRP(guildID uint32, donatedRP uint16) (*DonateRPResult, error) {
|
||||
page, donated, err := svc.towerRepo.GetGuildTowerPageAndRP(guildID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var requirement int
|
||||
for i := 0; i < (page*3)+1 && i < len(tenrouiraiData); i++ {
|
||||
requirement += int(tenrouiraiData[i].Cost)
|
||||
}
|
||||
|
||||
result := &DonateRPResult{
|
||||
ActualDonated: donatedRP,
|
||||
}
|
||||
|
||||
if donated+int(donatedRP) >= requirement {
|
||||
if err := svc.towerRepo.AdvanceTenrouiraiPage(guildID); err != nil {
|
||||
svc.logger.Error("Failed to advance tower mission page", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
result.ActualDonated = uint16(requirement - donated)
|
||||
result.Advanced = true
|
||||
}
|
||||
|
||||
if err := svc.towerRepo.DonateGuildTowerRP(guildID, result.ActualDonated); err != nil {
|
||||
svc.logger.Error("Failed to update guild tower RP", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
185
server/channelserver/svc_tower_test.go
Normal file
185
server/channelserver/svc_tower_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func newTestTowerService(mock *mockTowerRepo) *TowerService {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
return NewTowerService(mock, logger)
|
||||
}
|
||||
|
||||
// --- AddGem tests ---
|
||||
|
||||
func TestTowerService_AddGem_Success(t *testing.T) {
|
||||
mock := &mockTowerRepo{gems: "0,0,5,0,0"}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
err := svc.AddGem(1, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("AddGem returned error: %v", err)
|
||||
}
|
||||
// Gem at index 2 was 5, added 3, so should be 8
|
||||
if mock.updatedGems != "0,0,8,0,0" {
|
||||
t.Errorf("updatedGems = %q, want %q", mock.updatedGems, "0,0,8,0,0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTowerService_AddGem_GetGemsError(t *testing.T) {
|
||||
mock := &mockTowerRepo{gemsErr: errors.New("db error")}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
err := svc.AddGem(1, 0, 1)
|
||||
if err == nil {
|
||||
t.Fatal("AddGem should return error when GetGems fails")
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetTenrouiraiProgressCapped tests ---
|
||||
|
||||
func TestTowerService_GetTenrouiraiProgressCapped_CapsToGoals(t *testing.T) {
|
||||
// Page 1 missions have goals: 80, 16, 50 (from tenrouiraiData indices 0,1,2)
|
||||
mock := &mockTowerRepo{
|
||||
progress: TenrouiraiProgressData{
|
||||
Page: 1,
|
||||
Mission1: 9999,
|
||||
Mission2: 9999,
|
||||
Mission3: 9999,
|
||||
},
|
||||
}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
result, err := svc.GetTenrouiraiProgressCapped(10)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Mission1 != tenrouiraiData[0].Goal {
|
||||
t.Errorf("Mission1 = %d, want %d", result.Mission1, tenrouiraiData[0].Goal)
|
||||
}
|
||||
if result.Mission2 != tenrouiraiData[1].Goal {
|
||||
t.Errorf("Mission2 = %d, want %d", result.Mission2, tenrouiraiData[1].Goal)
|
||||
}
|
||||
if result.Mission3 != tenrouiraiData[2].Goal {
|
||||
t.Errorf("Mission3 = %d, want %d", result.Mission3, tenrouiraiData[2].Goal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTowerService_GetTenrouiraiProgressCapped_BelowGoals(t *testing.T) {
|
||||
mock := &mockTowerRepo{
|
||||
progress: TenrouiraiProgressData{
|
||||
Page: 1,
|
||||
Mission1: 10,
|
||||
Mission2: 5,
|
||||
Mission3: 20,
|
||||
},
|
||||
}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
result, err := svc.GetTenrouiraiProgressCapped(10)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.Mission1 != 10 {
|
||||
t.Errorf("Mission1 = %d, want 10", result.Mission1)
|
||||
}
|
||||
if result.Mission2 != 5 {
|
||||
t.Errorf("Mission2 = %d, want 5", result.Mission2)
|
||||
}
|
||||
if result.Mission3 != 20 {
|
||||
t.Errorf("Mission3 = %d, want 20", result.Mission3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTowerService_GetTenrouiraiProgressCapped_MinPage1(t *testing.T) {
|
||||
mock := &mockTowerRepo{
|
||||
progress: TenrouiraiProgressData{Page: 0},
|
||||
}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
result, err := svc.GetTenrouiraiProgressCapped(10)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.Page != 1 {
|
||||
t.Errorf("Page = %d, want 1", result.Page)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTowerService_GetTenrouiraiProgressCapped_DBError(t *testing.T) {
|
||||
mock := &mockTowerRepo{progressErr: errors.New("db error")}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
_, err := svc.GetTenrouiraiProgressCapped(10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from DB failure")
|
||||
}
|
||||
}
|
||||
|
||||
// --- DonateGuildTowerRP tests ---
|
||||
|
||||
func TestTowerService_DonateGuildTowerRP_NoAdvance(t *testing.T) {
|
||||
mock := &mockTowerRepo{
|
||||
page: 1,
|
||||
donated: 0,
|
||||
}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
result, err := svc.DonateGuildTowerRP(10, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.Advanced {
|
||||
t.Error("should not advance when donation < requirement")
|
||||
}
|
||||
if result.ActualDonated != 1 {
|
||||
t.Errorf("ActualDonated = %d, want 1", result.ActualDonated)
|
||||
}
|
||||
if mock.advanceCalled {
|
||||
t.Error("AdvanceTenrouiraiPage should not be called")
|
||||
}
|
||||
if mock.donatedRP != 1 {
|
||||
t.Errorf("donatedRP = %d, want 1", mock.donatedRP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTowerService_DonateGuildTowerRP_AdvancesPage(t *testing.T) {
|
||||
// Compute the requirement for page 1: sum of Cost for indices 0..3
|
||||
var requirement int
|
||||
for i := 0; i < 4; i++ {
|
||||
requirement += int(tenrouiraiData[i].Cost)
|
||||
}
|
||||
|
||||
mock := &mockTowerRepo{
|
||||
page: 1,
|
||||
donated: requirement - 10, // 10 short of requirement
|
||||
}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
result, err := svc.DonateGuildTowerRP(10, 100) // donating 100, but only 10 needed
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !result.Advanced {
|
||||
t.Error("should advance when donation meets requirement")
|
||||
}
|
||||
if result.ActualDonated != 10 {
|
||||
t.Errorf("ActualDonated = %d, want 10 (capped to remaining)", result.ActualDonated)
|
||||
}
|
||||
if !mock.advanceCalled {
|
||||
t.Error("AdvanceTenrouiraiPage should be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTowerService_DonateGuildTowerRP_DBError(t *testing.T) {
|
||||
mock := &mockTowerRepo{pageRPErr: errors.New("db error")}
|
||||
svc := newTestTowerService(mock)
|
||||
|
||||
_, err := svc.DonateGuildTowerRP(10, 100)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from DB failure")
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ type Server struct {
|
||||
guildService *GuildService
|
||||
achievementService *AchievementService
|
||||
gachaService *GachaService
|
||||
towerService *TowerService
|
||||
erupeConfig *cfg.Config
|
||||
acceptConns chan net.Conn
|
||||
deleteConns chan net.Conn
|
||||
@@ -161,6 +162,7 @@ func NewServer(config *Config) *Server {
|
||||
s.guildService = NewGuildService(s.guildRepo, s.mailService, s.charRepo, s.logger)
|
||||
s.achievementService = NewAchievementService(s.achievementRepo, s.logger)
|
||||
s.gachaService = NewGachaService(s.gachaRepo, s.userRepo, s.charRepo, s.logger, config.ErupeConfig.GameplayOptions.MaximumNP)
|
||||
s.towerService = NewTowerService(s.towerRepo, s.logger)
|
||||
|
||||
// Mezeporta
|
||||
s.stages.Store("sl1Ns200p0a0u0", NewStage("sl1Ns200p0a0u0"))
|
||||
|
||||
@@ -78,6 +78,11 @@ func ensureGachaService(s *Server) {
|
||||
s.gachaService = NewGachaService(s.gachaRepo, s.userRepo, s.charRepo, s.logger, 100000)
|
||||
}
|
||||
|
||||
// ensureTowerService wires the TowerService from the server's current repos.
|
||||
func ensureTowerService(s *Server) {
|
||||
s.towerService = NewTowerService(s.towerRepo, s.logger)
|
||||
}
|
||||
|
||||
// createMockSession creates a minimal Session for testing.
|
||||
// Imported from v9.2.x-stable and adapted for main.
|
||||
func createMockSession(charID uint32, server *Server) *Session {
|
||||
|
||||
Reference in New Issue
Block a user