package mapExchange import ( actorRemote "f1-game/internal/actor_remote" "f1-game/internal/code" "f1-game/internal/data" "f1-game/internal/enum" nameActor "f1-game/internal/name/actor" nameRemote "f1-game/internal/name/remote" "f1-game/internal/pb" "f1-game/internal/types" ao "f1-game/nodes/game/player/asset/origin" mapCall "f1-game/nodes/map/internal/call" "f1-game/nodes/map/internal/db" dbExchange "f1-game/nodes/map/internal/db/data/exchange" "time" cherryNUID "github.com/cherry-game/cherry/extend/nuid" ctime "github.com/cherry-game/cherry/extend/time" cfacade "github.com/cherry-game/cherry/facade" clog "github.com/cherry-game/cherry/logger" cactor "github.com/cherry-game/cherry/net/actor" ) // 交易所管理actor type actor struct { cactor.Base tables *db.MapExchangeTables } func Init(app cfacade.IApplication, tables *db.MapExchangeTables, exchangeDeliverTables map[int64]*db.MapExchangeDeliverTable) { actor := &actor{ tables: tables, } app.ActorSystem().CreateActor(actor.AliasID(), actor) // 创建发货的 子actor actor.Child().Create(nameActor.Map_ExchangeDeliver, newActorDeliver(exchangeDeliverTables)) } func (p *actor) AliasID() string { return nameActor.Map_Exchange } func (p *actor) OnInit() { p.Remote().Register(nameRemote.MapExchange_HistoryPrice, p.historyPrice) p.Remote().Register(nameRemote.MapExchange_Item, p.exchangeItem) p.Remote().Register(nameRemote.MapExchange_BuyItem, p.exchangeBuyItem) p.Remote().Register(nameRemote.MapExchange_QueryFilter, p.queryFilter) p.Remote().Register(nameRemote.MapExchange_MyItems, p.myItems) p.Remote().Register(nameRemote.MapExchange_ItemUpdate, p.itemUpdate) p.Timer().Add(3*time.Second, p.every3STimer) p.Timer().Add(15*time.Second, p.every15STimer) } func (p *actor) OnFindChild(m *cfacade.Message) (cfacade.IActor, bool) { childID := m.TargetPath().ChildID if childActor, found := p.Child().Get(childID); found { return childActor, true } clog.Warnf("child actor not found: %s", childID) return nil, false } // 3秒定时器 func (p *actor) every3STimer() { // 处理待上架的列表 p.onPending() // 处理求购商品状态更新 p.onWishItemUpState() // 处理出售商品状态更新 p.onSellItemUpState() } // 15秒定时器 func (p *actor) every15STimer() { // 处理求购匹配的列表 p.onWishMatch() } // 处理待上架的列表 func (p *actor) onPending() { start := ctime.Now() for _, item := range p.tables.PendingListed { if item.ItemDetail.Status != enum.ExchangeStatus_Pending { continue } // 更新为上架状态 item.ItemDetail.Status = enum.ExchangeStatus_Listed item.Save2Queue() if item.ItemDetail.ActType == enum.ExchangeActType_Sell { // 添加到出售查询列表 p.tables.SellQueryListed.Add(item) if p.tables.SellQueryListed.Count() > data.Const.ExchangeCatLimit { if sellItem, found := p.tables.SellQueryListed.RemoveAt(0); found { // 将超出限制的物品标记为隐藏 sellItem.ItemDetail.IsFlag = true } } } } // 清空待上架的列表 p.tables.PendingListed.Clear() cost := time.Since(start.Time) if cost > 20*time.Millisecond { clog.Warnf("onPending cost too much. cost: %v", cost) } } // 处理求购匹配的列表 func (p *actor) onWishMatch() { start := ctime.Now() for _, wishItem := range p.tables.WishMap { // 跳过非上架状态的求购项 if wishItem.ItemDetail.Status != enum.ExchangeStatus_Listed { continue } // 跳过已达上限的求购项 if wishItem.ItemDetail.IsWishLimit { continue } p.handleWishMatch(wishItem) } cost := time.Since(start.Time) if cost > 20*time.Millisecond { clog.Warnf("onWishMatch cost too much. cost: %v", cost) } } // 单个求购匹配逻辑 func (p *actor) handleWishMatch(wishItem *db.MapExchangeTable) { var ( pageSize = data.Const.ExchangePageSize wishPlayerID = wishItem.ItemDetail.PlayerID goodsID = wishItem.ItemDetail.Asset.ID wishCount = wishItem.ItemDetail.Asset.Num wishPrice = wishItem.ItemDetail.Price ) filter := db.GetWishMatchFilter(wishPlayerID, goodsID, wishPrice) comparator := db.GetWishMatchComparator() _, filterList := p.tables.SellQueryListed.Query(1, pageSize, filter, comparator) if len(filterList) == 0 { return } // 匹配的逻辑处理 sellItem := filterList[0] // 交易数量 count := min(sellItem.ItemDetail.Asset.Num, wishCount) // 数量扣除 sellItem.ItemDetail.Asset.Num -= count //修改求购商品的状态 wishItem.ItemDetail.Asset.Num -= count sellItem.Save2Queue() wishItem.Save2Queue() clog.Infof("onWishMatch match success. wish:%s, sell:%s, count:%d", wishItem.TransID, sellItem.TransID, count) errCode := mapCall.Exchange.ItemDeliver(wishPlayerID, sellItem.ToDetailProto(), count, wishPrice) if code.IsFail(errCode) { clog.Warnf("ItemDeliver failed. errCode: %d, wishPlayerID: %d, sellItem: %s, count: %d, wishPrice: %d", errCode, wishPlayerID, sellItem.TransID, count, wishPrice) } } // 处理出售商品状态更新 func (p *actor) onSellItemUpState() { start := ctime.Now() tempFlagList := types.NewList[*db.MapExchangeTable]() for transID, sellItem := range p.tables.SellMap { // 判断商品是否已经售罄 if sellItem.ItemDetail.Asset.Num < 1 { sellItem.ItemDetail.Status = enum.ExchangeStatus_Filled sellItem.ItemDetail.FilledTime = start.ToMillisecond() sellItem.IsDelete = true sellItem.Save2Queue() // 移除已经完成交易的商品 p.tables.SellQueryListed.Remove(sellItem) p.tables.SellMap.Remove(transID) // 出售方同步删除 mapCall.Player.ExchangeItemRemove(sellItem.ItemDetail.PlayerID, enum.ExchangeActType_Sell, sellItem.TransID) clog.Infof("onSellItemUpState remove sold item: %s", sellItem.TransID) continue } // 时间到了,处理下架 if sellItem.ItemDetail.Status == enum.ExchangeStatus_Listed && sellItem.ItemDetail.CanceledTime <= start.ToMillisecond() { sellItem.ItemDetail.Status = enum.ExchangeStatus_Canceled sellItem.Save2Queue() // 通知玩家下架 mapCall.Player.ExchangeItemCanceled(sellItem.ItemDetail.PlayerID, sellItem.ToDetailProto()) } // 移除已下架的商品 if sellItem.ItemDetail.Status >= enum.ExchangeStatus_Canceled { // 移除可补充标记 if sellItem.ItemDetail.IsFlag { sellItem.ItemDetail.IsFlag = false } p.tables.SellQueryListed.Remove(sellItem) if sellItem.ItemDetail.Status > enum.ExchangeStatus_Canceled { // 如果是取回了就标记删除 if sellItem.ItemDetail.Status == enum.ExchangeStatus_Retrieve { sellItem.IsDelete = true sellItem.Save2Queue() mapCall.Player.ExchangeItemRetrieve(sellItem.ItemDetail.PlayerID, sellItem.ToDetailProto()) } p.tables.SellMap.Remove(transID) clog.Infof("onSellItemUpState remove item from SellMap: %s", transID) } continue } if sellItem.ItemDetail.IsFlag { tempFlagList.Add(sellItem) } } // 补充的数量 addCount := data.Const.ExchangeCatLimit - p.tables.SellQueryListed.Count() if !tempFlagList.IsEmpty() && addCount > 0 { // 排序,按时间倒序补充 tempFlagList.Sort(db.GetListedTimeComparator(true)) // 往查询列表添加addCount个数量 count := min(tempFlagList.Size(), int(addCount)) for i := range count { if item, found := tempFlagList.Get(i); found { // 移除可补充标记 item.ItemDetail.IsFlag = false p.tables.SellQueryListed.Add(item) } } clog.Debugf("onSellItemUpState add sell item. count: %d", count) // 重新排序一下 p.tables.SellQueryListed.Sort(db.GetListedTimeComparator(false)) } cost := time.Since(start.Time) if cost > 20*time.Millisecond { clog.Warnf("onItemCanceled cost too much. cost: %v", cost) } } // 处理求购商品状态更新 func (p *actor) onWishItemUpState() { start := ctime.Now() for index, wishItem := range p.tables.WishMap { if wishItem.ItemDetail.Asset.Num < 1 { wishItem.ItemDetail.Status = enum.ExchangeStatus_Filled wishItem.ItemDetail.FilledTime = start.ToMillisecond() wishItem.IsDelete = true wishItem.Save2Queue() // 移除求购列表 p.tables.WishMap.Remove(index) // 删除玩家关联求购商品 mapCall.Player.ExchangeItemRemove(wishItem.ItemDetail.PlayerID, enum.ExchangeActType_Wish, wishItem.TransID) clog.Infof("onWishItemUpState remove wish item: %s", wishItem.TransID) continue } // 时间到了,下架 if wishItem.ItemDetail.Status == enum.ExchangeStatus_Listed && wishItem.ItemDetail.CanceledTime <= start.ToMillisecond() { wishItem.ItemDetail.Status = enum.ExchangeStatus_Canceled wishItem.Save2Queue() // 通知玩家下架 mapCall.Player.ExchangeItemCanceled(wishItem.ItemDetail.PlayerID, wishItem.ToDetailProto()) } // 如果下架了 if wishItem.ItemDetail.Status >= enum.ExchangeStatus_Canceled { // 如果是取回了就标记删除 if wishItem.ItemDetail.Status == enum.ExchangeStatus_Retrieve { wishItem.IsDelete = true wishItem.Save2Queue() mapCall.Player.ExchangeItemRetrieve(wishItem.ItemDetail.PlayerID, wishItem.ToDetailProto()) } // 只有交易成功和取回才移除 if wishItem.ItemDetail.Status > enum.ExchangeStatus_Canceled { p.tables.WishMap.Remove(index) clog.Infof("onWishItemUpState remove item from WishMap: %s", wishItem.TransID) } } } cost := time.Since(start.Time) if cost > 20*time.Millisecond { clog.Warnf("onItemCanceled cost too much. cost: %v", cost) } } // 交易商品 func (p *actor) exchangeItem(req *pb.PlayerExchangeItem) (*pb.ExchangeDetail, int32) { var ( itemInfo = req.ItemInfo // 商品信息 actType = enum.ExchangeActType(itemInfo.ActType) // 类型:1-出售,2-求购 playerID = req.PlayerID // 玩家ID ) // 上架最大毫秒数 maxListedMilliSec := db.GetMaxListedMilliSec(actType) newItemDetail := dbExchange.NewItemDetail(actType, playerID, itemInfo.Asset.Id, itemInfo.Asset.Num, itemInfo.Price, maxListedMilliSec) isOk := newItemDetail.LoadConfig() if !isOk { return nil, code.ExchangeItemConfigError } newMapExchangeTable := db.NewMapExchangeTable(cherryNUID.Next(), newItemDetail) newMapExchangeTable.Save2Queue() p.tables.PendingListed.Add(newMapExchangeTable) switch actType { case enum.ExchangeActType_Sell: // 添加到出售集合 p.tables.SellMap.Put(newMapExchangeTable.TransID, newMapExchangeTable) case enum.ExchangeActType_Wish: // 添加到求购集合 p.tables.WishMap.Put(newMapExchangeTable.TransID, newMapExchangeTable) // 更新商品求购价格 mapExchangeStatsTable, found := db.GetMapExchangeStatsTable(itemInfo.Asset.Id) if !found { mapExchangeStatsTable = db.NewMapExchangeStatsTable(itemInfo.Asset.Id) } mapExchangeStatsTable.UpdateWishPrice(itemInfo.Price) } rsp := newMapExchangeTable.ToDetailProto() clog.Infof("exchangeItem success. newMapExchangeTable: %+v", newMapExchangeTable) return rsp, code.OK } // 获取资源历史求购价格 func (p *actor) historyPrice(req *pb.I32) (*pb.I32I32, int32) { var ( itemID = req.Value // 商品ID rsp = &pb.I32I32{} // 响应数据 ) mapExchangeStatsTable, found := db.GetMapExchangeStatsTable(itemID) if found { rsp.Key = mapExchangeStatsTable.MinWishPrice rsp.Value = mapExchangeStatsTable.MaxWishPrice } return rsp, code.OK } // 购买商品 func (p *actor) exchangeBuyItem(req *pb.PlayerExchangeBuyItem) (rsp *pb.ExchangeBuyResult, errCode int32) { var ( playerID = req.PlayerID // 玩家ID gameNodeID = req.GameNodeID // 玩家游戏节点ID transID = req.TransID // 商品流水号ID count = req.Count // 购买数量 ) errCode = code.OK // 不管错误码都要返回 mapExchangeTable, found := p.tables.GetExchangeTable(enum.ExchangeActType_Sell, transID) if !found { rsp = &pb.ExchangeBuyResult{ Code: code.ExchangeItemNotFound, Detail: &pb.ExchangeDetail{ TransactionID: transID, }, } return } // 判断商品状态 if mapExchangeTable.ItemDetail.Status > enum.ExchangeStatus_Listed { rsp = p.buildBuyResult(mapExchangeTable, code.ExchangeItemSoldOut) return } // 商品数量不足 if count > mapExchangeTable.ItemDetail.Asset.Num { rsp = p.buildBuyResult(mapExchangeTable, code.ExchangeCountNotEnough) return } // 扣除资源 exchangeGoodsRow, found := data.ExchangeGoods.GetByID(mapExchangeTable.ItemDetail.Asset.ID) if !found { rsp = p.buildBuyResult(mapExchangeTable, code.ConfigNotFound_ExchangeGoods) return } var ( // 交易道具ID(金币) itemID = exchangeGoodsRow.ItemID // 单价 price = mapExchangeTable.ItemDetail.Price // 总价 sumPrice = int64(price) * count asset = types.NewAsset(itemID, sumPrice) assets = types.Assets{&asset} ) // 扣除购买的物品 if _, errcode := actorRemote.GamePlayer.AssetSub(gameNodeID, playerID, assets, ao.ExchangeBuyItemCost); code.IsFail(errcode) { rsp = p.buildBuyResult(mapExchangeTable, errcode) return } // 扣除数量 mapExchangeTable.ItemDetail.Asset.Num -= count mapExchangeTable.Save2Queue() if errcode := mapCall.Exchange.ItemDeliver(playerID, mapExchangeTable.ToDetailProto(), count, 0); code.IsFail(errcode) { clog.Warnf("exchangeBuyItem ItemDeliver failed. errcode: %d, playerID: %d, count: %d, mapExchangeTable: %+v", errcode, playerID, count, mapExchangeTable) } rsp = p.buildBuyResult(mapExchangeTable, code.OK) clog.Infof("exchangeBuyItem success. mapExchangeTable: [%+v] buy count: [%d]", mapExchangeTable, count) return } func (p *actor) buildBuyResult(mapExchangeTable *db.MapExchangeTable, code int32) *pb.ExchangeBuyResult { return &pb.ExchangeBuyResult{ Code: code, Detail: mapExchangeTable.ToDetailProto(), } } // 根据条件查询地图交易所的商品列表 func (p *actor) queryFilter(req *pb.ExchangeQueryFilter) (*pb.ExchangeDetailList, int32) { var ( pageNum = req.PageNum // 页码 itemIDList = req.ItemIDList // 物品ID列表 subTypeList = req.SubTypeList // 类型列表(道具表二级类型) qualityList = req.QualityList // 品质列表 priceSort = enum.ExchangeSortType(req.PriceSort) // 按单价排序 countSort = enum.ExchangeSortType(req.CountSort) // 按数量排序 pageSize = data.Const.ExchangePageSize // 每页数量 rsp = &pb.ExchangeDetailList{PageNum: pageNum} ) filter := db.GetFilterByParams(itemIDList, subTypeList, qualityList) comparator := db.GetComparatorByParams(priceSort, countSort) total, list := p.tables.SellQueryListed.Query(pageNum, pageSize, filter, comparator) rsp.Total = total for _, mapExchangeTable := range list { rsp.List = append(rsp.List, mapExchangeTable.ToDetailProto()) } clog.Debugf("queryFilter success. total: %d, list count: %d, pageNum: %d", total, len(list), pageNum) return rsp, code.OK } // 获取玩家的交易商品列表 func (p *actor) myItems(req *pb.PlayerExchangeMyItem) (*pb.ExchangeDetailList, int32) { var ( playerID = req.PlayerID actType = enum.ExchangeActType(req.ActType) transactionIDs = req.TransIDs ) rsp := &pb.ExchangeDetailList{} if transactionIDs == nil || len(transactionIDs.List) == 0 { return rsp, code.OK } for _, transID := range transactionIDs.List { mapExchangeTable, found := p.tables.GetExchangeTable(actType, transID) if !found { clog.Warnf("myItems not found. playerID: %d, transID: %s", playerID, transID) continue } rsp.List = append(rsp.List, mapExchangeTable.ToDetailProto()) } clog.Debugf("myItems success. playerID: %d, list count: %d", playerID, len(rsp.List)) return rsp, code.OK } // 更新商品: 下架,重新上架,取回(如果没有下架就先下架再取回) func (p *actor) itemUpdate(req *pb.PlayerExchangeItemUpdate) int32 { var ( playerID = req.PlayerID // 玩家ID transID = req.TransID // 商品流水号ID updateType = enum.ExchangeUpdateType(req.UpdateType) // 更新类型:1-下架,2-重新上架,3-取回 actType = enum.ExchangeActType(req.ActType) nowMilliSec = ctime.Now().ToMillisecond() ) mapExchangeTable, found := p.tables.GetExchangeTable(actType, transID) if !found { return code.ExchangeItemNotFound } switch updateType { case enum.ExchangeUpdateType_Cancel: { if mapExchangeTable.ItemDetail.Status > enum.ExchangeStatus_Listed { return code.ExchangeItemSoldOut } mapExchangeTable.ItemDetail.Status = enum.ExchangeStatus_Canceled mapExchangeTable.ItemDetail.CanceledTime = nowMilliSec mapExchangeTable.Save2Queue() // 下架商品 p.tables.SellQueryListed.Remove(mapExchangeTable) clog.Infof("itemUpdate SellQueryListed remove. playerID: %d, transID: %s", playerID, transID) } case enum.ExchangeUpdateType_ReList: { if mapExchangeTable.ItemDetail.Status != enum.ExchangeStatus_Canceled { return code.ExchangeItemNotSoldOut } // 重新设置上架商品状态 mapExchangeTable.Relist(nowMilliSec, req.IsWishLimit) // 重新上架商品 p.tables.PendingListed.Add(mapExchangeTable) clog.Infof("itemUpdate PendingListed add. playerID: %d, transID: %s", playerID, transID) } case enum.ExchangeUpdateType_Retrieve: { // 取回之前如果商品没有下架,则先下架再取回 if mapExchangeTable.ItemDetail.Status < enum.ExchangeStatus_Canceled { mapExchangeTable.ItemDetail.CanceledTime = nowMilliSec p.tables.SellQueryListed.Remove(mapExchangeTable) clog.Infof("itemUpdate Retrieve SellQueryListed remove. playerID: %d, transID: %s", playerID, transID) } // 标记取回状态 mapExchangeTable.ItemDetail.Status = enum.ExchangeStatus_Retrieve mapExchangeTable.ItemDetail.RetrieveTime = nowMilliSec mapExchangeTable.Save2Queue() clog.Infof("itemUpdate Retrieve ok. playerID: %d, transID: %s", playerID, transID) } } return code.OK }