1072 lines
36 KiB
Go
1072 lines
36 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"slices"
|
||
"strings"
|
||
"time"
|
||
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
roomv1 "hyapp.local/api/proto/room/v1"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/idgen"
|
||
"hyapp/pkg/roomid"
|
||
"hyapp/pkg/xerr"
|
||
"hyapp/services/room-service/internal/room/command"
|
||
"hyapp/services/room-service/internal/room/outbox"
|
||
"hyapp/services/room-service/internal/room/rank"
|
||
"hyapp/services/room-service/internal/room/state"
|
||
)
|
||
|
||
const (
|
||
roomRocketLevelCount = 5
|
||
|
||
roomRocketDefaultFuelSource = "heat_value"
|
||
roomRocketDefaultLaunchDelayMS = int64(30_000)
|
||
roomRocketDefaultBroadcastScope = "region"
|
||
roomRocketDefaultRewardStackPolicy = "allow_stack"
|
||
|
||
roomRocketFuelHeatValue = "heat_value"
|
||
|
||
roomRocketBroadcastNone = "none"
|
||
roomRocketBroadcastRegion = "region"
|
||
roomRocketBroadcastGlobal = "global"
|
||
|
||
roomRocketRewardStackAllow = "allow_stack"
|
||
roomRocketRewardStackPriority = "priority_only"
|
||
|
||
roomRocketRewardInRoom = "in_room"
|
||
roomRocketRewardTop1 = "top1"
|
||
roomRocketRewardIgniter = "igniter"
|
||
roomRocketGrantSource = "room_rocket"
|
||
roomRocketGrantStatus = "succeeded"
|
||
)
|
||
|
||
var roomRocketDefaultThresholds = []int64{1_000, 3_000, 6_000, 10_000, 15_000}
|
||
|
||
// GetRoomRocket 返回房间火箭 UI 初始化数据;它只读 Room Cell,不刷新 presence。
|
||
func (s *Service) GetRoomRocket(ctx context.Context, req *roomv1.GetRoomRocketRequest) (*roomv1.GetRoomRocketResponse, error) {
|
||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||
roomID := strings.TrimSpace(req.GetRoomId())
|
||
viewerUserID := req.GetViewerUserId()
|
||
if !roomid.ValidStringID(roomID) {
|
||
return nil, xerr.New(xerr.InvalidArgument, "room_id is invalid")
|
||
}
|
||
if viewerUserID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
|
||
}
|
||
|
||
snapshot, err := s.currentSnapshot(ctx, roomID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if snapshot == nil || snapshot.GetRoomId() == "" {
|
||
return nil, xerr.New(xerr.NotFound, "room not found")
|
||
}
|
||
if snapshot.GetStatus() != state.RoomStatusActive {
|
||
return nil, xerr.New(xerr.RoomClosed, "room closed")
|
||
}
|
||
if snapshotUserBanned(snapshot, viewerUserID, s.clock.Now().UnixMilli()) || findProtoUser(snapshot, viewerUserID) == nil {
|
||
// 火箭状态属于房间内活动信息,只允许仍在业务 presence 内的用户读取。
|
||
return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room")
|
||
}
|
||
|
||
config, err := s.roomRocketConfig(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
now := s.clock.Now().UTC()
|
||
info := roomRocketInfoFromConfig(config, snapshot.GetRocket(), now)
|
||
return &roomv1.GetRoomRocketResponse{
|
||
Rocket: info,
|
||
ServerTimeMs: now.UnixMilli(),
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) roomRocketConfig(ctx context.Context) (RoomRocketConfig, error) {
|
||
config, exists, err := s.repository.GetRoomRocketConfig(ctx)
|
||
if err != nil {
|
||
return RoomRocketConfig{}, err
|
||
}
|
||
if !exists {
|
||
config = defaultRoomRocketConfig(appcode.FromContext(ctx))
|
||
}
|
||
return normalizeRoomRocketConfig(config)
|
||
}
|
||
|
||
func defaultRoomRocketConfig(appCode string) RoomRocketConfig {
|
||
levels := make([]RoomRocketLevelConfig, 0, roomRocketLevelCount)
|
||
for index, threshold := range roomRocketDefaultThresholds {
|
||
levels = append(levels, RoomRocketLevelConfig{
|
||
Level: int32(index + 1),
|
||
FuelThreshold: threshold,
|
||
})
|
||
}
|
||
return RoomRocketConfig{
|
||
AppCode: appcode.Normalize(appCode),
|
||
FuelSource: roomRocketDefaultFuelSource,
|
||
LaunchDelayMS: roomRocketDefaultLaunchDelayMS,
|
||
BroadcastEnabled: true,
|
||
BroadcastScope: roomRocketDefaultBroadcastScope,
|
||
RewardStackPolicy: roomRocketDefaultRewardStackPolicy,
|
||
Levels: levels,
|
||
}
|
||
}
|
||
|
||
func normalizeRoomRocketConfig(config RoomRocketConfig) (RoomRocketConfig, error) {
|
||
config.AppCode = appcode.Normalize(config.AppCode)
|
||
if config.AppCode == "" {
|
||
config.AppCode = appcode.Default
|
||
}
|
||
if config.ConfigVersion < 0 {
|
||
return RoomRocketConfig{}, xerr.New(xerr.InvalidArgument, "room rocket config_version is invalid")
|
||
}
|
||
config.FuelSource = defaultRoomRocketString(config.FuelSource, roomRocketDefaultFuelSource)
|
||
if config.FuelSource != roomRocketFuelHeatValue {
|
||
return RoomRocketConfig{}, xerr.New(xerr.InvalidArgument, "room rocket fuel_source is invalid")
|
||
}
|
||
if config.LaunchDelayMS < 0 || config.BroadcastDelayMS < 0 {
|
||
return RoomRocketConfig{}, xerr.New(xerr.InvalidArgument, "room rocket delay is invalid")
|
||
}
|
||
config.BroadcastScope = defaultRoomRocketString(config.BroadcastScope, roomRocketDefaultBroadcastScope)
|
||
if config.BroadcastScope != roomRocketBroadcastNone && config.BroadcastScope != roomRocketBroadcastRegion && config.BroadcastScope != roomRocketBroadcastGlobal {
|
||
return RoomRocketConfig{}, xerr.New(xerr.InvalidArgument, "room rocket broadcast_scope is invalid")
|
||
}
|
||
if !config.BroadcastEnabled {
|
||
config.BroadcastScope = roomRocketBroadcastNone
|
||
}
|
||
config.RewardStackPolicy = defaultRoomRocketString(config.RewardStackPolicy, roomRocketDefaultRewardStackPolicy)
|
||
if config.RewardStackPolicy != roomRocketRewardStackAllow && config.RewardStackPolicy != roomRocketRewardStackPriority {
|
||
return RoomRocketConfig{}, xerr.New(xerr.InvalidArgument, "room rocket reward_stack_policy is invalid")
|
||
}
|
||
|
||
levels, err := normalizeRoomRocketLevels(config.Levels)
|
||
if err != nil {
|
||
return RoomRocketConfig{}, err
|
||
}
|
||
config.Levels = levels
|
||
config.GiftFuelRules = normalizeRoomRocketFuelRules(config.GiftFuelRules)
|
||
return config, nil
|
||
}
|
||
|
||
func normalizeRoomRocketLevels(levels []RoomRocketLevelConfig) ([]RoomRocketLevelConfig, error) {
|
||
if len(levels) == 0 {
|
||
return defaultRoomRocketConfig(appcode.Default).Levels, nil
|
||
}
|
||
if len(levels) != roomRocketLevelCount {
|
||
return nil, xerr.New(xerr.InvalidArgument, "room rocket levels must be 5")
|
||
}
|
||
seen := make(map[int32]bool, roomRocketLevelCount)
|
||
normalized := make([]RoomRocketLevelConfig, 0, roomRocketLevelCount)
|
||
for _, level := range levels {
|
||
if level.Level < 1 || level.Level > roomRocketLevelCount || seen[level.Level] {
|
||
return nil, xerr.New(xerr.InvalidArgument, "room rocket level is invalid")
|
||
}
|
||
if level.FuelThreshold <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "room rocket fuel_threshold is invalid")
|
||
}
|
||
seen[level.Level] = true
|
||
level.CoverURL = strings.TrimSpace(level.CoverURL)
|
||
level.AnimationURL = strings.TrimSpace(level.AnimationURL)
|
||
level.LaunchAnimationURL = strings.TrimSpace(level.LaunchAnimationURL)
|
||
level.LaunchedImageURL = strings.TrimSpace(level.LaunchedImageURL)
|
||
level.InRoomRewards = normalizeRoomRocketRewardPool(level.InRoomRewards)
|
||
level.Top1Rewards = normalizeRoomRocketRewardPool(level.Top1Rewards)
|
||
level.IgniterRewards = normalizeRoomRocketRewardPool(level.IgniterRewards)
|
||
normalized = append(normalized, level)
|
||
}
|
||
slices.SortFunc(normalized, func(left, right RoomRocketLevelConfig) int {
|
||
return int(left.Level - right.Level)
|
||
})
|
||
return normalized, nil
|
||
}
|
||
|
||
func normalizeRoomRocketRewardPool(input []RoomRocketRewardItem) []RoomRocketRewardItem {
|
||
out := make([]RoomRocketRewardItem, 0, len(input))
|
||
for _, reward := range input {
|
||
reward.RewardItemID = strings.TrimSpace(reward.RewardItemID)
|
||
reward.DisplayName = strings.TrimSpace(reward.DisplayName)
|
||
reward.IconURL = strings.TrimSpace(reward.IconURL)
|
||
if reward.ResourceGroupID <= 0 || reward.Weight <= 0 {
|
||
// 后台保存层会拒绝非法奖励;运行时跳过历史脏项,避免一个奖励池配置污染送礼主链路。
|
||
continue
|
||
}
|
||
out = append(out, reward)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func normalizeRoomRocketFuelRules(input []GiftFuelRuleConfig) []GiftFuelRuleConfig {
|
||
out := make([]GiftFuelRuleConfig, 0, len(input))
|
||
for _, rule := range input {
|
||
rule.RuleID = strings.TrimSpace(rule.RuleID)
|
||
rule.GiftID = strings.TrimSpace(rule.GiftID)
|
||
rule.GiftTypeCode = strings.TrimSpace(rule.GiftTypeCode)
|
||
if rule.GiftID == "" && rule.GiftTypeCode == "" {
|
||
continue
|
||
}
|
||
if rule.MultiplierPPM < 0 || rule.FixedFuel < 0 {
|
||
continue
|
||
}
|
||
out = append(out, rule)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func defaultRoomRocketString(value string, fallback string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return fallback
|
||
}
|
||
return value
|
||
}
|
||
|
||
func roomRocketInfoFromConfig(config RoomRocketConfig, current *roomv1.RoomRocketState, now time.Time) *roomv1.RoomRocketInfo {
|
||
stateView := roomRocketProtoStateForView(current, config, now)
|
||
return &roomv1.RoomRocketInfo{
|
||
Enabled: config.Enabled,
|
||
Levels: roomRocketLevelsToProto(config.Levels),
|
||
State: stateView,
|
||
ServerTimeMs: now.UnixMilli(),
|
||
BroadcastScope: config.BroadcastScope,
|
||
LaunchDelayMs: config.LaunchDelayMS,
|
||
BroadcastDelayMs: config.BroadcastDelayMS,
|
||
RewardStackPolicy: config.RewardStackPolicy,
|
||
}
|
||
}
|
||
|
||
func roomRocketProtoStateForView(current *roomv1.RoomRocketState, config RoomRocketConfig, now time.Time) *roomv1.RoomRocketState {
|
||
internal := state.RocketState{}
|
||
if current != nil {
|
||
internal = state.RocketState{
|
||
CurrentLevel: current.GetCurrentLevel(),
|
||
CurrentFuel: current.GetCurrentFuel(),
|
||
FuelThreshold: current.GetFuelThreshold(),
|
||
Status: current.GetStatus(),
|
||
IgnitedAtMS: current.GetIgnitedAtMs(),
|
||
LaunchAtMS: current.GetLaunchAtMs(),
|
||
LaunchedAtMS: current.GetLaunchedAtMs(),
|
||
ResetAtMS: current.GetResetAtMs(),
|
||
Top1UserID: current.GetTop1UserId(),
|
||
IgniterUserID: current.GetIgniterUserId(),
|
||
RocketID: current.GetRocketId(),
|
||
ConfigVersion: current.GetConfigVersion(),
|
||
PendingLaunches: statePendingLaunchesFromProto(current.GetPendingLaunches()),
|
||
LastRewards: stateRewardsFromProto(current.GetLastRewards()),
|
||
}
|
||
}
|
||
normalized := rocketStateForNow(&internal, config, now, false)
|
||
return rocketStateToRoomProto(normalized)
|
||
}
|
||
|
||
func stateRewardsFromProto(input []*roomv1.RoomRocketRewardGrant) []state.RocketRewardGrant {
|
||
out := make([]state.RocketRewardGrant, 0, len(input))
|
||
for _, reward := range input {
|
||
out = append(out, state.RocketRewardGrant{
|
||
RewardRole: reward.GetRewardRole(),
|
||
UserID: reward.GetUserId(),
|
||
RewardItemID: reward.GetRewardItemId(),
|
||
ResourceGroupID: reward.GetResourceGroupId(),
|
||
DisplayName: reward.GetDisplayName(),
|
||
IconURL: reward.GetIconUrl(),
|
||
GrantID: reward.GetGrantId(),
|
||
Status: reward.GetStatus(),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func statePendingLaunchesFromProto(input []*roomv1.RoomRocketPendingLaunch) []state.RocketPendingLaunch {
|
||
out := make([]state.RocketPendingLaunch, 0, len(input))
|
||
for _, launch := range input {
|
||
out = append(out, state.RocketPendingLaunch{
|
||
RocketID: launch.GetRocketId(),
|
||
Level: launch.GetLevel(),
|
||
FuelThreshold: launch.GetFuelThreshold(),
|
||
IgnitedAtMS: launch.GetIgnitedAtMs(),
|
||
LaunchAtMS: launch.GetLaunchAtMs(),
|
||
ResetAtMS: launch.GetResetAtMs(),
|
||
Top1UserID: launch.GetTop1UserId(),
|
||
IgniterUserID: launch.GetIgniterUserId(),
|
||
ConfigVersion: launch.GetConfigVersion(),
|
||
CoverURL: launch.GetCoverUrl(),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func roomRocketLevelsToProto(levels []RoomRocketLevelConfig) []*roomv1.RoomRocketLevel {
|
||
out := make([]*roomv1.RoomRocketLevel, 0, len(levels))
|
||
for _, level := range levels {
|
||
out = append(out, &roomv1.RoomRocketLevel{
|
||
Level: level.Level,
|
||
FuelThreshold: level.FuelThreshold,
|
||
CoverUrl: level.CoverURL,
|
||
AnimationUrl: level.AnimationURL,
|
||
LaunchAnimationUrl: level.LaunchAnimationURL,
|
||
LaunchedImageUrl: level.LaunchedImageURL,
|
||
InRoomRewards: roomRocketRewardsToProto(level.InRoomRewards),
|
||
Top1Rewards: roomRocketRewardsToProto(level.Top1Rewards),
|
||
IgniterRewards: roomRocketRewardsToProto(level.IgniterRewards),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func roomRocketRewardsToProto(input []RoomRocketRewardItem) []*roomv1.RoomRocketRewardItem {
|
||
out := make([]*roomv1.RoomRocketRewardItem, 0, len(input))
|
||
for _, reward := range input {
|
||
out = append(out, &roomv1.RoomRocketRewardItem{
|
||
RewardItemId: reward.RewardItemID,
|
||
ResourceGroupId: reward.ResourceGroupID,
|
||
Weight: reward.Weight,
|
||
DisplayName: reward.DisplayName,
|
||
IconUrl: reward.IconURL,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
type rocketGiftApplyResult struct {
|
||
touched bool
|
||
progressEvent *roomeventsv1.RoomRocketFuelChanged
|
||
ignited *roomeventsv1.RoomRocketIgnited
|
||
}
|
||
|
||
func (s *Service) applyRoomRocketGift(now time.Time, current *state.RoomState, cfg RoomRocketConfig, cmd command.SendGift, settled *command.SendGift, billing *walletv1.DebitGiftResponse, roomMeta RoomMeta) (rocketGiftApplyResult, error) {
|
||
if !cfg.Enabled {
|
||
return rocketGiftApplyResult{}, nil
|
||
}
|
||
now = now.UTC()
|
||
nowMS := now.UnixMilli()
|
||
rocket := rocketStateForNow(current.Rocket, cfg, now, true)
|
||
result := rocketGiftApplyResult{touched: current.Rocket == nil || rocket.ResetAtMS != current.Rocket.ResetAtMS}
|
||
|
||
addedFuel := roomRocketFuel(cfg, cmd.GiftID, billing.GetGiftTypeCode(), billing.GetHeatValue())
|
||
effectiveFuel := int64(0)
|
||
overflowFuel := int64(0)
|
||
progressRocketID := rocket.RocketID
|
||
progressLevel := rocket.CurrentLevel
|
||
progressFuel := rocket.CurrentFuel
|
||
progressThreshold := rocket.FuelThreshold
|
||
progressStatus := rocket.Status
|
||
|
||
if rocket.Status == state.RocketStatusCompleted {
|
||
// 当天最高 5 级已满后,继续送礼不再产生新火箭;整笔燃料按产品规则直接作废。
|
||
overflowFuel = addedFuel
|
||
} else if addedFuel > 0 {
|
||
remaining := rocket.FuelThreshold - rocket.CurrentFuel
|
||
if remaining < 0 {
|
||
remaining = 0
|
||
}
|
||
effectiveFuel = minInt64(addedFuel, remaining)
|
||
overflowFuel = addedFuel - effectiveFuel
|
||
if effectiveFuel > 0 {
|
||
rocket.CurrentFuel += effectiveFuel
|
||
progressFuel = rocket.CurrentFuel
|
||
result.touched = true
|
||
}
|
||
if rocket.CurrentFuel >= rocket.FuelThreshold {
|
||
levelCfg := roomRocketLevelByNumber(cfg, rocket.CurrentLevel)
|
||
pending := state.RocketPendingLaunch{
|
||
RocketID: rocket.RocketID,
|
||
Level: rocket.CurrentLevel,
|
||
FuelThreshold: rocket.FuelThreshold,
|
||
IgnitedAtMS: nowMS,
|
||
LaunchAtMS: nowMS + cfg.LaunchDelayMS,
|
||
ResetAtMS: rocket.ResetAtMS,
|
||
Top1UserID: firstRankUserID(current.GiftRank),
|
||
IgniterUserID: cmd.ActorUserID(),
|
||
ConfigVersion: cfg.ConfigVersion,
|
||
CoverURL: levelCfg.CoverURL,
|
||
}
|
||
rocket.CurrentFuel = rocket.FuelThreshold
|
||
rocket.IgnitedAtMS = pending.IgnitedAtMS
|
||
rocket.LaunchAtMS = pending.LaunchAtMS
|
||
rocket.Top1UserID = pending.Top1UserID
|
||
rocket.IgniterUserID = pending.IgniterUserID
|
||
rocket.ConfigVersion = cfg.ConfigVersion
|
||
rocket.PendingLaunches = append(rocket.PendingLaunches, pending)
|
||
progressRocketID = pending.RocketID
|
||
progressLevel = pending.Level
|
||
progressFuel = pending.FuelThreshold
|
||
progressThreshold = pending.FuelThreshold
|
||
progressStatus = state.RocketStatusIgnited
|
||
result.ignited = &roomeventsv1.RoomRocketIgnited{
|
||
RocketId: pending.RocketID,
|
||
Level: pending.Level,
|
||
CurrentFuel: pending.FuelThreshold,
|
||
FuelThreshold: pending.FuelThreshold,
|
||
IgnitedAtMs: pending.IgnitedAtMS,
|
||
LaunchAtMs: pending.LaunchAtMS,
|
||
BroadcastScope: cfg.BroadcastScope,
|
||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||
Top1UserId: pending.Top1UserID,
|
||
IgniterUserId: pending.IgniterUserID,
|
||
CommandId: cmd.ID(),
|
||
RoomShortId: roomMeta.RoomShortID,
|
||
RocketCoverUrl: pending.CoverURL,
|
||
ResetAtMs: pending.ResetAtMS,
|
||
}
|
||
if rocket.CurrentLevel < roomRocketLevelCount {
|
||
nextLevel := rocket.CurrentLevel + 1
|
||
nextLevelCfg := roomRocketLevelByNumber(cfg, nextLevel)
|
||
rocket.CurrentLevel = nextLevel
|
||
rocket.CurrentFuel = 0
|
||
rocket.FuelThreshold = nextLevelCfg.FuelThreshold
|
||
rocket.Status = state.RocketStatusCharging
|
||
rocket.RocketID = idgen.New("room_rocket")
|
||
} else {
|
||
// 5 级是当天最高等级;点火后不再生成第 6 级,UTC 日界统一重置回 1 级。
|
||
rocket.Status = state.RocketStatusCompleted
|
||
rocket.RocketID = ""
|
||
}
|
||
}
|
||
}
|
||
|
||
if result.touched {
|
||
current.Rocket = rocket
|
||
}
|
||
if effectiveFuel > 0 {
|
||
result.progressEvent = &roomeventsv1.RoomRocketFuelChanged{
|
||
RocketId: progressRocketID,
|
||
Level: progressLevel,
|
||
AddedFuel: addedFuel,
|
||
EffectiveAddedFuel: effectiveFuel,
|
||
OverflowFuel: overflowFuel,
|
||
CurrentFuel: progressFuel,
|
||
FuelThreshold: progressThreshold,
|
||
Status: progressStatus,
|
||
ResetAtMs: rocket.ResetAtMS,
|
||
SenderUserId: cmd.ActorUserID(),
|
||
GiftId: cmd.GiftID,
|
||
GiftCount: cmd.GiftCount,
|
||
CommandId: cmd.ID(),
|
||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||
}
|
||
}
|
||
|
||
if result.touched {
|
||
settled.GiftTypeCode = billing.GetGiftTypeCode()
|
||
settled.RocketTouched = true
|
||
settled.RocketAddedFuel = addedFuel
|
||
settled.RocketEffectiveFuel = effectiveFuel
|
||
settled.RocketOverflowFuel = overflowFuel
|
||
settled.RocketLevel = rocket.CurrentLevel
|
||
settled.RocketFuel = rocket.CurrentFuel
|
||
settled.RocketStatus = rocket.Status
|
||
settled.RocketFuelThreshold = rocket.FuelThreshold
|
||
settled.RocketIgnitedAtMS = rocket.IgnitedAtMS
|
||
settled.RocketLaunchAtMS = rocket.LaunchAtMS
|
||
settled.RocketResetAtMS = rocket.ResetAtMS
|
||
settled.RocketTop1UserID = rocket.Top1UserID
|
||
settled.RocketIgniterUserID = rocket.IgniterUserID
|
||
settled.RocketID = rocket.RocketID
|
||
settled.RocketConfigVersion = rocket.ConfigVersion
|
||
settled.RocketPendingLaunches = rocketPendingLaunchesToCommand(rocket.PendingLaunches)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func rocketStateForNow(input *state.RocketState, cfg RoomRocketConfig, now time.Time, allocateRocketID bool) *state.RocketState {
|
||
now = now.UTC()
|
||
nowMS := now.UnixMilli()
|
||
resetAtMS := nextUTCResetMS(now)
|
||
level := int32(1)
|
||
if input != nil && input.CurrentLevel >= 1 && input.CurrentLevel <= roomRocketLevelCount {
|
||
level = input.CurrentLevel
|
||
}
|
||
if input == nil || input.ResetAtMS <= 0 || nowMS >= input.ResetAtMS {
|
||
return newRocketState(levelForUTCReset(), roomRocketLevelByNumber(cfg, levelForUTCReset()), resetAtMS, cfg.ConfigVersion, allocateRocketID)
|
||
}
|
||
|
||
rocket := input.Clone()
|
||
levelConfig := roomRocketLevelByNumber(cfg, level)
|
||
rocket.CurrentLevel = level
|
||
rocket.FuelThreshold = levelConfig.FuelThreshold
|
||
rocket.ResetAtMS = input.ResetAtMS
|
||
if rocket.CurrentFuel < 0 {
|
||
rocket.CurrentFuel = 0
|
||
}
|
||
if rocket.CurrentFuel > rocket.FuelThreshold {
|
||
rocket.CurrentFuel = rocket.FuelThreshold
|
||
}
|
||
rocket.PendingLaunches = validRocketPendingLaunches(rocket.PendingLaunches, rocket.ResetAtMS)
|
||
if rocket.Status == state.RocketStatusCompleted {
|
||
rocket.CurrentLevel = roomRocketLevelCount
|
||
rocket.FuelThreshold = roomRocketLevelByNumber(cfg, roomRocketLevelCount).FuelThreshold
|
||
rocket.CurrentFuel = rocket.FuelThreshold
|
||
rocket.RocketID = ""
|
||
} else {
|
||
rocket.Status = state.RocketStatusCharging
|
||
}
|
||
if rocket.RocketID == "" && allocateRocketID && rocket.Status != state.RocketStatusCompleted {
|
||
rocket.RocketID = idgen.New("room_rocket")
|
||
}
|
||
return rocket
|
||
}
|
||
|
||
func validRocketPendingLaunches(input []state.RocketPendingLaunch, resetAtMS int64) []state.RocketPendingLaunch {
|
||
out := make([]state.RocketPendingLaunch, 0, len(input))
|
||
seen := make(map[string]bool, len(input))
|
||
for _, launch := range input {
|
||
if launch.RocketID == "" || launch.Level <= 0 || launch.LaunchAtMS <= 0 {
|
||
continue
|
||
}
|
||
if resetAtMS > 0 && launch.ResetAtMS > 0 && launch.ResetAtMS != resetAtMS {
|
||
continue
|
||
}
|
||
key := fmt.Sprintf("%s:%d", launch.RocketID, launch.Level)
|
||
if seen[key] {
|
||
continue
|
||
}
|
||
seen[key] = true
|
||
out = append(out, launch)
|
||
}
|
||
slices.SortFunc(out, func(left, right state.RocketPendingLaunch) int {
|
||
if left.LaunchAtMS == right.LaunchAtMS {
|
||
return int(left.Level - right.Level)
|
||
}
|
||
if left.LaunchAtMS < right.LaunchAtMS {
|
||
return -1
|
||
}
|
||
return 1
|
||
})
|
||
return out
|
||
}
|
||
|
||
func newRocketState(level int32, levelConfig RoomRocketLevelConfig, resetAtMS int64, configVersion int64, allocateRocketID bool) *state.RocketState {
|
||
rocketID := ""
|
||
if allocateRocketID {
|
||
rocketID = idgen.New("room_rocket")
|
||
}
|
||
return &state.RocketState{
|
||
CurrentLevel: level,
|
||
FuelThreshold: levelConfig.FuelThreshold,
|
||
Status: state.RocketStatusCharging,
|
||
ResetAtMS: resetAtMS,
|
||
RocketID: rocketID,
|
||
ConfigVersion: configVersion,
|
||
}
|
||
}
|
||
|
||
func levelForUTCReset() int32 {
|
||
return 1
|
||
}
|
||
|
||
func nextUTCResetMS(now time.Time) int64 {
|
||
utc := now.UTC()
|
||
next := time.Date(utc.Year(), utc.Month(), utc.Day()+1, 0, 0, 0, 0, time.UTC)
|
||
return next.UnixMilli()
|
||
}
|
||
|
||
func roomRocketLevelByNumber(cfg RoomRocketConfig, level int32) RoomRocketLevelConfig {
|
||
for _, item := range cfg.Levels {
|
||
if item.Level == level {
|
||
return item
|
||
}
|
||
}
|
||
return defaultRoomRocketConfig(cfg.AppCode).Levels[0]
|
||
}
|
||
|
||
func roomRocketFuel(cfg RoomRocketConfig, giftID string, giftTypeCode string, heatValue int64) int64 {
|
||
// 火箭燃料只读取 wallet-service 按真实扣费和贡献比例计算出的 heat_value,避免历史 GIFT_POINT 字段污染活动进度。
|
||
base := heatValue
|
||
if base < 0 {
|
||
base = 0
|
||
}
|
||
|
||
for _, rule := range cfg.GiftFuelRules {
|
||
if rule.GiftID != "" && rule.GiftID == giftID {
|
||
return roomRocketFuelByRule(base, rule)
|
||
}
|
||
}
|
||
for _, rule := range cfg.GiftFuelRules {
|
||
if rule.GiftID == "" && rule.GiftTypeCode != "" && rule.GiftTypeCode == giftTypeCode {
|
||
return roomRocketFuelByRule(base, rule)
|
||
}
|
||
}
|
||
return base
|
||
}
|
||
|
||
func roomRocketFuelByRule(base int64, rule GiftFuelRuleConfig) int64 {
|
||
if rule.Excluded {
|
||
return 0
|
||
}
|
||
fuel := int64(0)
|
||
if rule.MultiplierPPM > 0 {
|
||
fuel += base * rule.MultiplierPPM / 1_000_000
|
||
}
|
||
fuel += rule.FixedFuel
|
||
if fuel < 0 {
|
||
return 0
|
||
}
|
||
return fuel
|
||
}
|
||
|
||
func firstRankUserID(items []state.RankItem) int64 {
|
||
if len(items) == 0 {
|
||
return 0
|
||
}
|
||
return items[0].UserID
|
||
}
|
||
|
||
func minInt64(left int64, right int64) int64 {
|
||
if left < right {
|
||
return left
|
||
}
|
||
return right
|
||
}
|
||
|
||
func rocketStateToRoomProto(input *state.RocketState) *roomv1.RoomRocketState {
|
||
if input == nil {
|
||
return nil
|
||
}
|
||
rewards := make([]*roomv1.RoomRocketRewardGrant, 0, len(input.LastRewards))
|
||
for _, reward := range input.LastRewards {
|
||
rewards = append(rewards, &roomv1.RoomRocketRewardGrant{
|
||
RewardRole: reward.RewardRole,
|
||
UserId: reward.UserID,
|
||
RewardItemId: reward.RewardItemID,
|
||
ResourceGroupId: reward.ResourceGroupID,
|
||
DisplayName: reward.DisplayName,
|
||
IconUrl: reward.IconURL,
|
||
GrantId: reward.GrantID,
|
||
Status: reward.Status,
|
||
})
|
||
}
|
||
pending := make([]*roomv1.RoomRocketPendingLaunch, 0, len(input.PendingLaunches))
|
||
for _, launch := range input.PendingLaunches {
|
||
pending = append(pending, &roomv1.RoomRocketPendingLaunch{
|
||
RocketId: launch.RocketID,
|
||
Level: launch.Level,
|
||
FuelThreshold: launch.FuelThreshold,
|
||
IgnitedAtMs: launch.IgnitedAtMS,
|
||
LaunchAtMs: launch.LaunchAtMS,
|
||
ResetAtMs: launch.ResetAtMS,
|
||
Top1UserId: launch.Top1UserID,
|
||
IgniterUserId: launch.IgniterUserID,
|
||
ConfigVersion: launch.ConfigVersion,
|
||
CoverUrl: launch.CoverURL,
|
||
})
|
||
}
|
||
return &roomv1.RoomRocketState{
|
||
CurrentLevel: input.CurrentLevel,
|
||
CurrentFuel: input.CurrentFuel,
|
||
FuelThreshold: input.FuelThreshold,
|
||
Status: input.Status,
|
||
IgnitedAtMs: input.IgnitedAtMS,
|
||
LaunchAtMs: input.LaunchAtMS,
|
||
LaunchedAtMs: input.LaunchedAtMS,
|
||
ResetAtMs: input.ResetAtMS,
|
||
Top1UserId: input.Top1UserID,
|
||
IgniterUserId: input.IgniterUserID,
|
||
RocketId: input.RocketID,
|
||
ConfigVersion: input.ConfigVersion,
|
||
LastRewards: rewards,
|
||
PendingLaunches: pending,
|
||
}
|
||
}
|
||
|
||
func rocketStateToEventRewards(input []state.RocketRewardGrant) []*roomeventsv1.RoomRocketRewardGrant {
|
||
out := make([]*roomeventsv1.RoomRocketRewardGrant, 0, len(input))
|
||
for _, reward := range input {
|
||
out = append(out, &roomeventsv1.RoomRocketRewardGrant{
|
||
RewardRole: reward.RewardRole,
|
||
UserId: reward.UserID,
|
||
RewardItemId: reward.RewardItemID,
|
||
ResourceGroupId: reward.ResourceGroupID,
|
||
DisplayName: reward.DisplayName,
|
||
IconUrl: reward.IconURL,
|
||
GrantId: reward.GrantID,
|
||
Status: reward.Status,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// RunRoomRocketLaunchWorker 周期性扫描已装载房间,把到点的待发射火箭走命令链路发射。
|
||
func (s *Service) RunRoomRocketLaunchWorker(ctx context.Context, interval time.Duration) {
|
||
if interval <= 0 {
|
||
return
|
||
}
|
||
ticker := time.NewTicker(interval)
|
||
defer ticker.Stop()
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
_ = s.SweepRoomRocketLaunchings(ctx)
|
||
}
|
||
}
|
||
}
|
||
|
||
// SweepRoomRocketLaunchings 只处理本节点仍持有 lease 的已装载 Room Cell。
|
||
func (s *Service) SweepRoomRocketLaunchings(ctx context.Context) error {
|
||
now := s.clock.Now().UTC()
|
||
nowMS := now.UnixMilli()
|
||
for _, roomRef := range s.loadedRoomRefs() {
|
||
roomCtx := appcode.WithContext(ctx, roomRef.AppCode)
|
||
lease, owned, err := s.loadedRoomLease(roomCtx, roomRef, now)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !owned {
|
||
continue
|
||
}
|
||
roomCell := s.loadCell(roomCtx, roomRef.RoomID)
|
||
if roomCell == nil {
|
||
continue
|
||
}
|
||
currentState, _, err := roomCell.Snapshot(roomCtx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
owned, err = s.verifyLoadedRoomLease(roomCtx, roomRef, lease, s.clock.Now())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !owned || currentState == nil || currentState.Rocket == nil {
|
||
continue
|
||
}
|
||
rocket := currentState.Rocket
|
||
if rocket.ResetAtMS > 0 && nowMS >= rocket.ResetAtMS {
|
||
// UTC 切日优先级高于延迟发射;跨日倒计时火箭不再结算,下一次查询或送礼会按新 UTC 日投影/持久化一级新进度。
|
||
continue
|
||
}
|
||
for _, pending := range append([]state.RocketPendingLaunch(nil), rocket.PendingLaunches...) {
|
||
if pending.LaunchAtMS <= 0 || pending.LaunchAtMS > nowMS {
|
||
continue
|
||
}
|
||
actorID := pending.IgniterUserID
|
||
if actorID <= 0 {
|
||
actorID = roomRocketSystemActor(currentState)
|
||
}
|
||
cmd := command.LaunchRoomRocket{
|
||
Base: command.Base{
|
||
AppCode: roomRef.AppCode,
|
||
RequestID: idgen.New("req_room_rocket_launch"),
|
||
CommandID: roomRocketLaunchCommandID(pending.RocketID),
|
||
ActorID: actorID,
|
||
Room: roomRef.RoomID,
|
||
GatewayNodeID: s.nodeID,
|
||
SessionID: "room-rocket-launch",
|
||
SentAtMS: nowMS,
|
||
},
|
||
RocketID: pending.RocketID,
|
||
Level: pending.Level,
|
||
}
|
||
if _, err := s.launchRoomRocket(roomCtx, cmd); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func roomRocketLaunchCommandID(rocketID string) string {
|
||
if strings.TrimSpace(rocketID) == "" {
|
||
return idgen.New("cmd_room_rocket_launch")
|
||
}
|
||
return "cmd_room_rocket_launch_" + rocketID
|
||
}
|
||
|
||
func roomRocketSystemActor(current *state.RoomState) int64 {
|
||
if current == nil {
|
||
return 0
|
||
}
|
||
if current.OwnerUserID > 0 {
|
||
return current.OwnerUserID
|
||
}
|
||
if current.Rocket != nil && current.Rocket.IgniterUserID > 0 {
|
||
return current.Rocket.IgniterUserID
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func (s *Service) launchRoomRocket(ctx context.Context, cmd command.LaunchRoomRocket) (*roomv1.RoomRocketState, error) {
|
||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||
now = now.UTC()
|
||
nowMS := now.UnixMilli()
|
||
if current.Rocket == nil {
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
pending, pendingIndex, ok := findRocketPendingLaunch(current.Rocket.PendingLaunches, cmd.RocketID, cmd.Level)
|
||
if !ok {
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
if pending.ResetAtMS > 0 && nowMS >= pending.ResetAtMS {
|
||
// 手动或延迟到达的发射命令也不能越过 UTC 日界结算昨天的火箭。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
if pending.LaunchAtMS <= 0 || pending.LaunchAtMS > nowMS {
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
|
||
cfg, err := s.roomRocketConfig(ctx)
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
levelCfg := roomRocketLevelByNumber(cfg, pending.Level)
|
||
inRoomUserIDs := sortedRocketOnlineUserIDs(current.OnlineUsers)
|
||
rewards, err := s.settleRoomRocketRewards(ctx, now, current, pending, cfg, levelCfg, inRoomUserIDs)
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
pendingAfter := removeRocketPendingLaunch(current.Rocket.PendingLaunches, pendingIndex)
|
||
current.Rocket.PendingLaunches = pendingAfter
|
||
current.Rocket.LaunchedAtMS = nowMS
|
||
current.Rocket.LastRewards = rewards
|
||
|
||
settledCommand := cmd
|
||
settledCommand.CurrentLevel = current.Rocket.CurrentLevel
|
||
settledCommand.CurrentFuel = current.Rocket.CurrentFuel
|
||
settledCommand.CurrentFuelThreshold = current.Rocket.FuelThreshold
|
||
settledCommand.CurrentStatus = current.Rocket.Status
|
||
settledCommand.CurrentRocketID = current.Rocket.RocketID
|
||
settledCommand.ConfigVersion = current.Rocket.ConfigVersion
|
||
settledCommand.ResetAtMS = current.Rocket.ResetAtMS
|
||
settledCommand.Top1UserID = pending.Top1UserID
|
||
settledCommand.IgniterUserID = pending.IgniterUserID
|
||
settledCommand.LaunchedAtMS = nowMS
|
||
settledCommand.InRoomUserIDs = inRoomUserIDs
|
||
settledCommand.Rewards = rocketRewardsToCommand(rewards)
|
||
settledCommand.PendingLaunches = rocketPendingLaunchesToCommand(pendingAfter)
|
||
commandPayload, err := command.Serialize(settledCommand)
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
current.Version++
|
||
|
||
launchedEvent, err := outbox.Build(current.RoomID, "RoomRocketLaunched", current.Version, now, &roomeventsv1.RoomRocketLaunched{
|
||
RocketId: pending.RocketID,
|
||
Level: pending.Level,
|
||
NextLevel: current.Rocket.CurrentLevel,
|
||
LaunchedAtMs: nowMS,
|
||
ResetAtMs: pending.ResetAtMS,
|
||
Top1UserId: pending.Top1UserID,
|
||
IgniterUserId: pending.IgniterUserID,
|
||
InRoomUserIds: inRoomUserIDs,
|
||
Rewards: rocketStateToEventRewards(rewards),
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
records := []outbox.Record{launchedEvent}
|
||
if len(rewards) > 0 {
|
||
rewardEvent, err := outbox.Build(current.RoomID, "RoomRocketRewardGranted", current.Version, now, &roomeventsv1.RoomRocketRewardGranted{
|
||
RocketId: pending.RocketID,
|
||
Level: pending.Level,
|
||
Rewards: rocketStateToEventRewards(rewards),
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
records = append(records, rewardEvent)
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
commandPayload: commandPayload,
|
||
}, records, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return result.snapshot.GetRocket(), nil
|
||
}
|
||
|
||
func findRocketPendingLaunch(input []state.RocketPendingLaunch, rocketID string, level int32) (state.RocketPendingLaunch, int, bool) {
|
||
for index, launch := range input {
|
||
if launch.RocketID == rocketID && launch.Level == level {
|
||
return launch, index, true
|
||
}
|
||
}
|
||
return state.RocketPendingLaunch{}, -1, false
|
||
}
|
||
|
||
func removeRocketPendingLaunch(input []state.RocketPendingLaunch, index int) []state.RocketPendingLaunch {
|
||
if index < 0 || index >= len(input) {
|
||
return append([]state.RocketPendingLaunch(nil), input...)
|
||
}
|
||
out := make([]state.RocketPendingLaunch, 0, len(input)-1)
|
||
out = append(out, input[:index]...)
|
||
out = append(out, input[index+1:]...)
|
||
return out
|
||
}
|
||
|
||
func sortedRocketOnlineUserIDs(users map[int64]*state.RoomUserState) []int64 {
|
||
userIDs := make([]int64, 0, len(users))
|
||
for userID := range users {
|
||
if userID > 0 {
|
||
userIDs = append(userIDs, userID)
|
||
}
|
||
}
|
||
slices.Sort(userIDs)
|
||
return userIDs
|
||
}
|
||
|
||
func (s *Service) settleRoomRocketRewards(ctx context.Context, now time.Time, current *state.RoomState, launch state.RocketPendingLaunch, cfg RoomRocketConfig, levelCfg RoomRocketLevelConfig, inRoomUserIDs []int64) ([]state.RocketRewardGrant, error) {
|
||
if current == nil {
|
||
return nil, nil
|
||
}
|
||
rewards := make([]state.RocketRewardGrant, 0, len(inRoomUserIDs)+2)
|
||
claimed := make(map[int64]bool, len(inRoomUserIDs)+2)
|
||
addReward := func(role string, userID int64, pool []RoomRocketRewardItem) error {
|
||
if userID <= 0 {
|
||
return nil
|
||
}
|
||
if cfg.RewardStackPolicy == roomRocketRewardStackPriority && claimed[userID] {
|
||
return nil
|
||
}
|
||
item, ok := selectRoomRocketReward(launch.RocketID, role, userID, pool)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
grantID, err := s.grantRoomRocketReward(ctx, now, current, launch.RocketID, role, userID, item)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
rewards = append(rewards, state.RocketRewardGrant{
|
||
RewardRole: role,
|
||
UserID: userID,
|
||
RewardItemID: item.RewardItemID,
|
||
ResourceGroupID: item.ResourceGroupID,
|
||
DisplayName: item.DisplayName,
|
||
IconURL: item.IconURL,
|
||
GrantID: grantID,
|
||
Status: roomRocketGrantStatus,
|
||
})
|
||
claimed[userID] = true
|
||
return nil
|
||
}
|
||
|
||
if cfg.RewardStackPolicy == roomRocketRewardStackPriority {
|
||
// 去重策略下先发特殊身份奖励,再发普通在房奖励,避免 top1/点火人被普通池占位。
|
||
if err := addReward(roomRocketRewardIgniter, launch.IgniterUserID, levelCfg.IgniterRewards); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := addReward(roomRocketRewardTop1, launch.Top1UserID, levelCfg.Top1Rewards); err != nil {
|
||
return nil, err
|
||
}
|
||
for _, userID := range inRoomUserIDs {
|
||
if err := addReward(roomRocketRewardInRoom, userID, levelCfg.InRoomRewards); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return rewards, nil
|
||
}
|
||
|
||
for _, userID := range inRoomUserIDs {
|
||
if err := addReward(roomRocketRewardInRoom, userID, levelCfg.InRoomRewards); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
if err := addReward(roomRocketRewardTop1, launch.Top1UserID, levelCfg.Top1Rewards); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := addReward(roomRocketRewardIgniter, launch.IgniterUserID, levelCfg.IgniterRewards); err != nil {
|
||
return nil, err
|
||
}
|
||
return rewards, nil
|
||
}
|
||
|
||
func (s *Service) grantRoomRocketReward(ctx context.Context, _ time.Time, current *state.RoomState, rocketID string, role string, userID int64, item RoomRocketRewardItem) (string, error) {
|
||
if s.wallet == nil {
|
||
return "", xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||
}
|
||
operatorUserID := current.OwnerUserID
|
||
if operatorUserID <= 0 {
|
||
operatorUserID = userID
|
||
}
|
||
resp, err := s.wallet.GrantResourceGroup(ctx, &walletv1.GrantResourceGroupRequest{
|
||
CommandId: roomRocketGrantCommandID(rocketID, role, userID, item.RewardItemID),
|
||
AppCode: appcode.FromContext(ctx),
|
||
TargetUserId: userID,
|
||
GroupId: item.ResourceGroupID,
|
||
Reason: fmt.Sprintf("room rocket %s reward", role),
|
||
OperatorUserId: operatorUserID,
|
||
GrantSource: roomRocketGrantSource,
|
||
})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if resp == nil || resp.GetGrant() == nil {
|
||
return "", xerr.New(xerr.Unavailable, "wallet grant response is empty")
|
||
}
|
||
return resp.GetGrant().GetGrantId(), nil
|
||
}
|
||
|
||
func selectRoomRocketReward(rocketID string, role string, userID int64, pool []RoomRocketRewardItem) (RoomRocketRewardItem, bool) {
|
||
total := int64(0)
|
||
for _, item := range pool {
|
||
if item.Weight > 0 && item.ResourceGroupID > 0 {
|
||
total += item.Weight
|
||
}
|
||
}
|
||
if total <= 0 {
|
||
return RoomRocketRewardItem{}, false
|
||
}
|
||
hash := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%d", rocketID, role, userID)))
|
||
pick := int64(binary.BigEndian.Uint64(hash[:8]) % uint64(total))
|
||
cursor := int64(0)
|
||
for _, item := range pool {
|
||
if item.Weight <= 0 || item.ResourceGroupID <= 0 {
|
||
continue
|
||
}
|
||
cursor += item.Weight
|
||
if pick < cursor {
|
||
return item, true
|
||
}
|
||
}
|
||
return pool[len(pool)-1], true
|
||
}
|
||
|
||
func roomRocketGrantCommandID(rocketID string, role string, userID int64, rewardItemID string) string {
|
||
hash := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%d|%s", rocketID, role, userID, rewardItemID)))
|
||
return fmt.Sprintf("cmd_room_rocket_grant_%x", hash[:8])
|
||
}
|
||
|
||
func rocketRewardsToCommand(input []state.RocketRewardGrant) []command.RocketRewardGrant {
|
||
out := make([]command.RocketRewardGrant, 0, len(input))
|
||
for _, reward := range input {
|
||
out = append(out, command.RocketRewardGrant{
|
||
RewardRole: reward.RewardRole,
|
||
UserID: reward.UserID,
|
||
RewardItemID: reward.RewardItemID,
|
||
ResourceGroupID: reward.ResourceGroupID,
|
||
DisplayName: reward.DisplayName,
|
||
IconURL: reward.IconURL,
|
||
GrantID: reward.GrantID,
|
||
Status: reward.Status,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func rocketPendingLaunchesToCommand(input []state.RocketPendingLaunch) []command.RocketPendingLaunch {
|
||
out := make([]command.RocketPendingLaunch, 0, len(input))
|
||
for _, launch := range input {
|
||
out = append(out, command.RocketPendingLaunch{
|
||
RocketID: launch.RocketID,
|
||
Level: launch.Level,
|
||
FuelThreshold: launch.FuelThreshold,
|
||
IgnitedAtMS: launch.IgnitedAtMS,
|
||
LaunchAtMS: launch.LaunchAtMS,
|
||
ResetAtMS: launch.ResetAtMS,
|
||
Top1UserID: launch.Top1UserID,
|
||
IgniterUserID: launch.IgniterUserID,
|
||
ConfigVersion: launch.ConfigVersion,
|
||
CoverURL: launch.CoverURL,
|
||
})
|
||
}
|
||
return out
|
||
}
|