From 9453743964e66e37b7131572ed4ce8a5ce6bb0ea Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Thu, 16 Apr 2026 17:22:57 +0300 Subject: [PATCH] Add consumable item sell functionality --- server/Makefile | 2 +- server/cmd/lunar-tear/grpc.go | 4 ++ server/cmd/lunar-tear/main.go | 7 ++ server/internal/masterdata/consumableitem.go | 31 ++++++++ server/internal/service/consumableitem.go | 75 ++++++++++++++++++++ server/proto/consumableitem.proto | 20 +++--- 6 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 server/internal/masterdata/consumableitem.go create mode 100644 server/internal/service/consumableitem.go diff --git a/server/Makefile b/server/Makefile index 60ea468..b7918e1 100644 --- a/server/Makefile +++ b/server/Makefile @@ -1,7 +1,7 @@ # Proto generation: outputs to gen/proto/ (module=lunar-tear/server). # All proto files have go_package. Only protos used by the server are generated here # (generating all would put them in one package and cause name clashes). -PROTO_USED = proto/banner.proto proto/battle.proto proto/bighunt.proto proto/cageornament.proto proto/character.proto proto/characterboard.proto proto/characterviewer.proto proto/companion.proto proto/config.proto proto/contentsstory.proto proto/costume.proto proto/data.proto proto/deck.proto proto/dokan.proto proto/explore.proto proto/friend.proto proto/gacha.proto proto/gameplay.proto proto/gift.proto proto/gimmick.proto proto/labyrinth.proto proto/loginbonus.proto proto/material.proto proto/mission.proto proto/movie.proto proto/navicutin.proto proto/omikuji.proto proto/notification.proto proto/parts.proto proto/portalcage.proto proto/pvp.proto proto/quest.proto proto/reward.proto proto/shop.proto proto/sidestoryquest.proto proto/tutorial.proto proto/user.proto proto/weapon.proto +PROTO_USED = proto/banner.proto proto/battle.proto proto/bighunt.proto proto/cageornament.proto proto/character.proto proto/characterboard.proto proto/characterviewer.proto proto/companion.proto proto/config.proto proto/consumableitem.proto proto/contentsstory.proto proto/costume.proto proto/data.proto proto/deck.proto proto/dokan.proto proto/explore.proto proto/friend.proto proto/gacha.proto proto/gameplay.proto proto/gift.proto proto/gimmick.proto proto/labyrinth.proto proto/loginbonus.proto proto/material.proto proto/mission.proto proto/movie.proto proto/navicutin.proto proto/omikuji.proto proto/notification.proto proto/parts.proto proto/portalcage.proto proto/pvp.proto proto/quest.proto proto/reward.proto proto/shop.proto proto/sidestoryquest.proto proto/tutorial.proto proto/user.proto proto/weapon.proto proto: protoc -I . $(PROTO_USED) --go_out=. --go_opt=module=lunar-tear/server --go-grpc_out=. --go-grpc_opt=module=lunar-tear/server diff --git a/server/cmd/lunar-tear/grpc.go b/server/cmd/lunar-tear/grpc.go index 7cbf95b..dc94045 100644 --- a/server/cmd/lunar-tear/grpc.go +++ b/server/cmd/lunar-tear/grpc.go @@ -55,6 +55,7 @@ func startGRPC( characterRebirthCatalog *masterdata.CharacterRebirthCatalog, companionCatalog *masterdata.CompanionCatalog, materialCatalog *masterdata.MaterialCatalog, + consumableItemCatalog *masterdata.ConsumableItemCatalog, gameConfig *masterdata.GameConfig, sideStoryCatalog *masterdata.SideStoryCatalog, bigHuntCatalog *masterdata.BigHuntCatalog, @@ -90,6 +91,7 @@ func startGRPC( characterRebirthCatalog, companionCatalog, materialCatalog, + consumableItemCatalog, gameConfig, sideStoryCatalog, bigHuntCatalog, @@ -126,6 +128,7 @@ func registerServices( characterRebirthCatalog *masterdata.CharacterRebirthCatalog, companionCatalog *masterdata.CompanionCatalog, materialCatalog *masterdata.MaterialCatalog, + consumableItemCatalog *masterdata.ConsumableItemCatalog, gameConfig *masterdata.GameConfig, sideStoryCatalog *masterdata.SideStoryCatalog, bigHuntCatalog *masterdata.BigHuntCatalog, @@ -163,6 +166,7 @@ func registerServices( pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, characterRebirthCatalog, gameConfig)) pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, companionCatalog, gameConfig)) pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, materialCatalog, gameConfig)) + pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, consumableItemCatalog, gameConfig)) pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, sideStoryCatalog)) pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine)) pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter)) diff --git a/server/cmd/lunar-tear/main.go b/server/cmd/lunar-tear/main.go index 4584c9d..cb65e1d 100644 --- a/server/cmd/lunar-tear/main.go +++ b/server/cmd/lunar-tear/main.go @@ -120,6 +120,12 @@ func main() { } log.Printf("material catalog loaded: %d materials", len(materialCatalog.All)) + consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog() + if err != nil { + log.Fatalf("load consumable item catalog: %v", err) + } + log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All)) + costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog) if err != nil { log.Fatalf("load costume catalog: %v", err) @@ -184,6 +190,7 @@ func main() { characterRebirthCatalog, companionCatalog, materialCatalog, + consumableItemCatalog, gameConfig, sideStoryCatalog, bigHuntCatalog, diff --git a/server/internal/masterdata/consumableitem.go b/server/internal/masterdata/consumableitem.go new file mode 100644 index 0000000..2e18691 --- /dev/null +++ b/server/internal/masterdata/consumableitem.go @@ -0,0 +1,31 @@ +package masterdata + +import ( + "fmt" + + "lunar-tear/server/internal/utils" +) + +type ConsumableItemRow struct { + ConsumableItemId int32 `json:"ConsumableItemId"` + SellPrice int32 `json:"SellPrice"` +} + +type ConsumableItemCatalog struct { + All map[int32]ConsumableItemRow +} + +func LoadConsumableItemCatalog() (*ConsumableItemCatalog, error) { + rows, err := utils.ReadJSON[ConsumableItemRow]("EntityMConsumableItemTable.json") + if err != nil { + return nil, fmt.Errorf("load consumable item table: %w", err) + } + + catalog := &ConsumableItemCatalog{ + All: make(map[int32]ConsumableItemRow, len(rows)), + } + for _, row := range rows { + catalog.All[row.ConsumableItemId] = row + } + return catalog, nil +} diff --git a/server/internal/service/consumableitem.go b/server/internal/service/consumableitem.go new file mode 100644 index 0000000..98d7e3a --- /dev/null +++ b/server/internal/service/consumableitem.go @@ -0,0 +1,75 @@ +package service + +import ( + "context" + "fmt" + "log" + + pb "lunar-tear/server/gen/proto" + "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/store" + "lunar-tear/server/internal/userdata" +) + +type ConsumableItemServiceServer struct { + pb.UnimplementedConsumableItemServiceServer + users store.UserRepository + sessions store.SessionRepository + catalog *masterdata.ConsumableItemCatalog + config *masterdata.GameConfig +} + +func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ConsumableItemCatalog, config *masterdata.GameConfig) *ConsumableItemServiceServer { + return &ConsumableItemServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +} + +func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) { + log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession)) + + userId := currentUserId(ctx, s.users, s.sessions) + + oldUser, _ := s.users.SnapshotUser(userId) + tracker := userdata.NewDeleteTracker(). + Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"}) + + snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + totalGold := int32(0) + for _, item := range req.ConsumableItemPossession { + row, ok := s.catalog.All[item.ConsumableItemId] + if !ok { + log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId) + continue + } + + cur := user.ConsumableItems[item.ConsumableItemId] + if cur < item.Count { + log.Printf("[ConsumableItemService] Sell: insufficient consumableItemId=%d have=%d need=%d", item.ConsumableItemId, cur, item.Count) + continue + } + + user.ConsumableItems[item.ConsumableItemId] -= item.Count + if user.ConsumableItems[item.ConsumableItemId] <= 0 { + delete(user.ConsumableItems, item.ConsumableItemId) + } + + gold := row.SellPrice * item.Count + totalGold += gold + log.Printf("[ConsumableItemService] Sell: consumableItemId=%d x%d -> %d gold", item.ConsumableItemId, item.Count, gold) + } + + if totalGold > 0 { + user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold + log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold) + } + }) + if err != nil { + return nil, fmt.Errorf("consumable item sell: %w", err) + } + + tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), []string{"IUserConsumableItem"}) + diff := tracker.Apply(snapshot, tables) + + return &pb.ConsumableItemSellResponse{ + DiffUserData: diff, + }, nil +} diff --git a/server/proto/consumableitem.proto b/server/proto/consumableitem.proto index 9defa42..502b54f 100644 --- a/server/proto/consumableitem.proto +++ b/server/proto/consumableitem.proto @@ -6,29 +6,29 @@ import "proto/data.proto"; package apb.api.consumableitem; -service ConsumableitemService { - rpc UseEffectItem (UseEffectItemRequest) returns (UseEffectItemResponse); - rpc Sell (SellRequest) returns (SellResponse); +service ConsumableItemService { + rpc UseEffectItem (ConsumableItemUseEffectItemRequest) returns (ConsumableItemUseEffectItemResponse); + rpc Sell (ConsumableItemSellRequest) returns (ConsumableItemSellResponse); } -message UseEffectItemRequest { +message ConsumableItemUseEffectItemRequest { int32 consumableItemId = 1; int32 count = 2; } -message UseEffectItemResponse { +message ConsumableItemUseEffectItemResponse { map diffUserData = 99; } -message SellRequest { - repeated SellPossession consumableItemPossession = 1; +message ConsumableItemSellRequest { + repeated ConsumableItemSellPossession consumableItemPossession = 1; } -message SellPossession { +message ConsumableItemSellPossession { int32 consumableItemId = 1; int32 count = 2; } -message SellResponse { +message ConsumableItemSellResponse { map diffUserData = 99; -} \ No newline at end of file +}