567 lines
18 KiB
Go
567 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/rand"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
roomv1 "hyapp.local/api/proto/room/v1"
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/pkg/logx"
|
|
"hyapp/pkg/roomid"
|
|
"hyapp/pkg/xerr"
|
|
)
|
|
|
|
const (
|
|
robotRoomStatusActive = "active"
|
|
robotRoomStatusStopped = "stopped"
|
|
robotRoomMode = "voice"
|
|
robotRoomLuckyPoolID = "robot_lucky_display"
|
|
)
|
|
|
|
func (s *Service) AdminListRobotRooms(ctx context.Context, req *roomv1.AdminListRobotRoomsRequest) (*roomv1.AdminListRobotRoomsResponse, error) {
|
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
|
items, total, err := s.repository.ListRobotRooms(ctx, RobotRoomListQuery{
|
|
AppCode: appcode.FromContext(ctx),
|
|
Status: strings.TrimSpace(req.GetStatus()),
|
|
Page: int(req.GetPage()),
|
|
PageSize: int(req.GetPageSize()),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &roomv1.AdminListRobotRoomsResponse{Total: total, ServerTimeMs: s.clock.Now().UnixMilli()}
|
|
for _, item := range items {
|
|
resp.Rooms = append(resp.Rooms, robotRoomConfigToProto(item))
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Service) AdminFilterAvailableRoomRobots(ctx context.Context, req *roomv1.AdminFilterAvailableRoomRobotsRequest) (*roomv1.AdminFilterAvailableRoomRobotsResponse, error) {
|
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
|
ids := normalizeRobotUserIDs(req.GetUserIds())
|
|
occupied, err := s.repository.OccupiedRobotUserIDs(ctx, ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &roomv1.AdminFilterAvailableRoomRobotsResponse{ServerTimeMs: s.clock.Now().UnixMilli()}
|
|
for _, userID := range ids {
|
|
if occupied[userID] {
|
|
resp.OccupiedUserIds = append(resp.OccupiedUserIds, userID)
|
|
continue
|
|
}
|
|
resp.AvailableUserIds = append(resp.AvailableUserIds, userID)
|
|
}
|
|
slices.Sort(resp.AvailableUserIds)
|
|
slices.Sort(resp.OccupiedUserIds)
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Service) AdminCreateRobotRoom(ctx context.Context, req *roomv1.AdminCreateRobotRoomRequest) (*roomv1.AdminCreateRobotRoomResponse, error) {
|
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
|
now := s.clock.Now()
|
|
config, err := s.buildRobotRoomConfig(ctx, req, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.bootstrapRobotRoom(ctx, config); err != nil {
|
|
logx.Error(ctx, "admin_robot_room_bootstrap_failed", err,
|
|
slog.String("room_id", config.RoomID),
|
|
slog.Int64("owner_robot_user_id", config.OwnerRobotUserID),
|
|
slog.Int("robot_count", len(config.RobotUserIDs)),
|
|
)
|
|
return nil, err
|
|
}
|
|
saved, err := s.repository.CreateRobotRoomConfig(ctx, CreateRobotRoomConfigInput{Config: config, NowMS: now.UnixMilli()})
|
|
if err != nil {
|
|
logx.Error(ctx, "admin_robot_room_config_save_failed", err,
|
|
slog.String("room_id", config.RoomID),
|
|
slog.Int64("owner_robot_user_id", config.OwnerRobotUserID),
|
|
slog.Int("robot_count", len(config.RobotUserIDs)),
|
|
)
|
|
return nil, err
|
|
}
|
|
s.startRobotRoomRuntime(ctx, saved)
|
|
return &roomv1.AdminCreateRobotRoomResponse{Room: robotRoomConfigToProto(saved), ServerTimeMs: now.UnixMilli()}, nil
|
|
}
|
|
|
|
func (s *Service) AdminSetRobotRoomStatus(ctx context.Context, req *roomv1.AdminSetRobotRoomStatusRequest) (*roomv1.AdminSetRobotRoomStatusResponse, error) {
|
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
|
roomID := strings.TrimSpace(req.GetMeta().GetRoomId())
|
|
status := normalizeRobotRoomStatus(req.GetStatus())
|
|
if roomID == "" {
|
|
return nil, xerr.New(xerr.InvalidArgument, "room_id is required")
|
|
}
|
|
if status == "" {
|
|
return nil, xerr.New(xerr.InvalidArgument, "robot room status is invalid")
|
|
}
|
|
config, exists, err := s.repository.UpdateRobotRoomStatus(ctx, roomID, status, s.clock.Now().UnixMilli())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, xerr.New(xerr.NotFound, "robot room not found")
|
|
}
|
|
if status == robotRoomStatusActive {
|
|
s.startRobotRoomRuntime(ctx, config)
|
|
} else {
|
|
s.stopRobotRoomRuntime(roomID)
|
|
}
|
|
return &roomv1.AdminSetRobotRoomStatusResponse{Room: robotRoomConfigToProto(config), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
|
|
}
|
|
|
|
func (s *Service) buildRobotRoomConfig(ctx context.Context, req *roomv1.AdminCreateRobotRoomRequest, now time.Time) (RobotRoomConfig, error) {
|
|
ownerID := req.GetOwnerRobotUserId()
|
|
if ownerID <= 0 {
|
|
return RobotRoomConfig{}, xerr.New(xerr.InvalidArgument, "owner_robot_user_id is required")
|
|
}
|
|
candidates := normalizeRobotUserIDs(req.GetCandidateRobotUserIds())
|
|
containsOwner := false
|
|
for _, userID := range candidates {
|
|
if userID == ownerID {
|
|
containsOwner = true
|
|
break
|
|
}
|
|
}
|
|
if !containsOwner {
|
|
candidates = append(candidates, ownerID)
|
|
}
|
|
occupied, err := s.repository.OccupiedRobotUserIDs(ctx, candidates)
|
|
if err != nil {
|
|
return RobotRoomConfig{}, err
|
|
}
|
|
if occupied[ownerID] {
|
|
return RobotRoomConfig{}, xerr.New(xerr.Conflict, "owner robot already assigned to active robot room")
|
|
}
|
|
available := make([]int64, 0, len(candidates))
|
|
for _, userID := range candidates {
|
|
if !occupied[userID] {
|
|
available = append(available, userID)
|
|
}
|
|
}
|
|
minCount, maxCount := normalizeRobotCountRange(req.GetMinRobotCount(), req.GetMaxRobotCount())
|
|
if len(available) < minCount {
|
|
return RobotRoomConfig{}, xerr.New(xerr.Conflict, "available robot count is not enough")
|
|
}
|
|
rng := rand.New(rand.NewSource(now.UnixNano()))
|
|
selectedCount := randomIntRange(rng, minCount, maxCount)
|
|
if selectedCount > len(available) {
|
|
selectedCount = len(available)
|
|
}
|
|
selected := selectRobotUsers(rng, ownerID, available, selectedCount)
|
|
rule, err := normalizeRobotRoomGiftRule(req.GetGiftRule())
|
|
if err != nil {
|
|
return RobotRoomConfig{}, err
|
|
}
|
|
roomID := fmt.Sprintf("robot_%d_%d", now.UnixMilli(), rng.Intn(900000)+100000)
|
|
if !roomid.ValidStringID(roomID) {
|
|
return RobotRoomConfig{}, xerr.New(xerr.Internal, "generated robot room id is invalid")
|
|
}
|
|
title := strings.TrimSpace(req.GetRoomName())
|
|
if title == "" {
|
|
title = fmt.Sprintf("Robot Room %d", ownerID)
|
|
}
|
|
coverURL := strings.TrimSpace(req.GetRoomAvatar())
|
|
if coverURL == "" {
|
|
coverURL = defaultRoomAvatar
|
|
}
|
|
return RobotRoomConfig{
|
|
AppCode: appcode.FromContext(ctx),
|
|
RoomID: roomID,
|
|
RoomShortID: fmt.Sprintf("%d", ownerID),
|
|
Title: title,
|
|
CoverURL: coverURL,
|
|
VisibleRegionID: normalizeVisibleRegionID(req.GetVisibleRegionId()),
|
|
Status: robotRoomStatusActive,
|
|
OwnerRobotUserID: ownerID,
|
|
RobotUserIDs: selected,
|
|
GiftRule: rule,
|
|
CreatedByAdminID: req.GetAdminId(),
|
|
CreatedAtMS: now.UnixMilli(),
|
|
UpdatedAtMS: now.UnixMilli(),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) bootstrapRobotRoom(ctx context.Context, config RobotRoomConfig) error {
|
|
_, err := s.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
|
Meta: &roomv1.RequestMeta{
|
|
RequestId: fmt.Sprintf("admin-robot-room-create-%s", config.RoomID),
|
|
CommandId: fmt.Sprintf("admin-robot-room:%s:create", config.RoomID),
|
|
ActorUserId: config.OwnerRobotUserID,
|
|
RoomId: config.RoomID,
|
|
AppCode: config.AppCode,
|
|
SentAtMs: s.clock.Now().UnixMilli(),
|
|
},
|
|
SeatCount: int32(max(10, len(config.RobotUserIDs))),
|
|
Mode: robotRoomMode,
|
|
VisibleRegionId: config.VisibleRegionID,
|
|
RoomName: config.Title,
|
|
RoomAvatar: config.CoverURL,
|
|
RoomShortId: config.RoomShortID,
|
|
RobotRoom: true,
|
|
RobotUserIds: config.RobotUserIDs,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, userID := range config.RobotUserIDs {
|
|
if userID == config.OwnerRobotUserID {
|
|
continue
|
|
}
|
|
if _, err := s.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
|
Meta: robotRoomCommandMeta(config, userID, fmt.Sprintf("join:%d", userID)),
|
|
Role: "audience",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for index, userID := range config.RobotUserIDs {
|
|
if _, err := s.RobotVirtualMicUp(ctx, RobotMicUpInput{
|
|
Meta: robotRoomCommandMeta(config, config.OwnerRobotUserID, fmt.Sprintf("mic:%d", userID)),
|
|
TargetUserID: userID,
|
|
SeatNo: int32(index + 1),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunRobotRoomRuntimeManager 恢复 active 机器人房间的进程内送礼循环;配置事实仍以 MySQL 为准。
|
|
func (s *Service) RunRobotRoomRuntimeManager(ctx context.Context, interval time.Duration) {
|
|
if interval <= 0 {
|
|
interval = 10 * time.Second
|
|
}
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
s.startActiveRobotRooms(ctx)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
s.stopAllRobotRoomRuntimes()
|
|
return
|
|
case <-ticker.C:
|
|
s.startActiveRobotRooms(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) startActiveRobotRooms(ctx context.Context) {
|
|
configs, err := s.repository.ListActiveRobotRooms(ctx)
|
|
if err != nil {
|
|
logx.Warn(ctx, "robot_room_runtime_scan_failed", slog.String("error", err.Error()))
|
|
return
|
|
}
|
|
for _, config := range configs {
|
|
s.startRobotRoomRuntime(ctx, config)
|
|
}
|
|
}
|
|
|
|
func (s *Service) startRobotRoomRuntime(parent context.Context, config RobotRoomConfig) {
|
|
if config.Status != robotRoomStatusActive || len(config.RobotUserIDs) < 2 {
|
|
return
|
|
}
|
|
s.robotRuntimeMu.Lock()
|
|
if s.robotRuntimes == nil {
|
|
s.robotRuntimes = make(map[string]context.CancelFunc)
|
|
}
|
|
if _, exists := s.robotRuntimes[config.RoomID]; exists {
|
|
s.robotRuntimeMu.Unlock()
|
|
return
|
|
}
|
|
ctx, cancel := context.WithCancel(appcode.WithContext(parent, config.AppCode))
|
|
s.robotRuntimes[config.RoomID] = cancel
|
|
s.robotRuntimeMu.Unlock()
|
|
go s.runRobotRoomRuntime(ctx, config)
|
|
}
|
|
|
|
func (s *Service) stopRobotRoomRuntime(roomID string) {
|
|
s.robotRuntimeMu.Lock()
|
|
cancel := s.robotRuntimes[roomID]
|
|
delete(s.robotRuntimes, roomID)
|
|
s.robotRuntimeMu.Unlock()
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
func (s *Service) stopAllRobotRoomRuntimes() {
|
|
s.robotRuntimeMu.Lock()
|
|
cancels := make([]context.CancelFunc, 0, len(s.robotRuntimes))
|
|
for roomID, cancel := range s.robotRuntimes {
|
|
cancels = append(cancels, cancel)
|
|
delete(s.robotRuntimes, roomID)
|
|
}
|
|
s.robotRuntimeMu.Unlock()
|
|
for _, cancel := range cancels {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
func (s *Service) runRobotRoomRuntime(ctx context.Context, config RobotRoomConfig) {
|
|
for _, senderID := range config.RobotUserIDs {
|
|
senderID := senderID
|
|
go s.runRobotNormalGiftLoop(ctx, config, senderID)
|
|
go s.runRobotLuckyGiftLoop(ctx, config, senderID)
|
|
}
|
|
<-ctx.Done()
|
|
}
|
|
|
|
func (s *Service) runRobotNormalGiftLoop(ctx context.Context, config RobotRoomConfig, senderID int64) {
|
|
interval := time.Duration(config.GiftRule.NormalGiftIntervalMS) * time.Millisecond
|
|
if interval <= 0 || len(config.GiftRule.GiftIDs) == 0 {
|
|
return
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID))
|
|
timer := time.NewTimer(randomJitter(interval, rng))
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-timer.C:
|
|
targetID := randomRobotTarget(rng, config.RobotUserIDs, senderID)
|
|
giftID := randomString(rng, config.GiftRule.GiftIDs)
|
|
if targetID > 0 && giftID != "" {
|
|
s.sendRobotGiftBestEffort(ctx, config, senderID, targetID, giftID, "", "normal")
|
|
}
|
|
timer.Reset(interval)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) runRobotLuckyGiftLoop(ctx context.Context, config RobotRoomConfig, senderID int64) {
|
|
if len(config.GiftRule.LuckyGiftIDs) == 0 || config.GiftRule.LuckyComboMax <= 0 {
|
|
return
|
|
}
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID*17))
|
|
for {
|
|
combo := randomInt64Range(rng, config.GiftRule.LuckyComboMin, config.GiftRule.LuckyComboMax)
|
|
for i := int64(0); i < combo; i++ {
|
|
giftID := randomString(rng, config.GiftRule.LuckyGiftIDs)
|
|
for _, targetID := range config.RobotUserIDs {
|
|
if targetID == senderID {
|
|
continue
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return
|
|
}
|
|
s.sendRobotGiftBestEffort(ctx, config, senderID, targetID, giftID, robotRoomLuckyPoolID, "lucky")
|
|
}
|
|
}
|
|
pause := time.Duration(randomInt64Range(rng, config.GiftRule.LuckyPauseMinMS, config.GiftRule.LuckyPauseMaxMS)) * time.Millisecond
|
|
if pause <= 0 {
|
|
pause = 5 * time.Second
|
|
}
|
|
timer := time.NewTimer(pause)
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) sendRobotGiftBestEffort(ctx context.Context, config RobotRoomConfig, senderID int64, targetID int64, giftID string, poolID string, kind string) {
|
|
_, err := s.RobotSendGift(ctx, RobotSendGiftInput{
|
|
Meta: robotRoomCommandMeta(config, senderID, fmt.Sprintf("gift:%s:%d:%d", kind, targetID, time.Now().UTC().UnixNano())),
|
|
TargetUserID: targetID,
|
|
GiftID: giftID,
|
|
GiftCount: 1,
|
|
PoolID: poolID,
|
|
RobotUserIDs: config.RobotUserIDs,
|
|
SyntheticRewardCoins: 0,
|
|
SyntheticMultiplierPPM: 0,
|
|
})
|
|
if err != nil {
|
|
logx.Warn(ctx, "robot_room_send_gift_failed", slog.String("room_id", config.RoomID), slog.String("kind", kind), slog.Int64("sender_user_id", senderID), slog.Int64("target_user_id", targetID), slog.String("gift_id", giftID), slog.String("error", err.Error()))
|
|
}
|
|
}
|
|
|
|
func robotRoomCommandMeta(config RobotRoomConfig, actorUserID int64, suffix string) *roomv1.RequestMeta {
|
|
commandID := fmt.Sprintf("robot-room:%s:%s", config.RoomID, strings.TrimSpace(suffix))
|
|
if len(commandID) > 128 {
|
|
commandID = commandID[:128]
|
|
}
|
|
return &roomv1.RequestMeta{
|
|
RequestId: commandID,
|
|
CommandId: commandID,
|
|
ActorUserId: actorUserID,
|
|
RoomId: config.RoomID,
|
|
AppCode: config.AppCode,
|
|
SentAtMs: time.Now().UTC().UnixMilli(),
|
|
}
|
|
}
|
|
|
|
func normalizeRobotRoomGiftRule(input *roomv1.AdminRobotRoomGiftRule) (RobotRoomGiftRule, error) {
|
|
if input == nil {
|
|
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "gift_rule is required")
|
|
}
|
|
rule := RobotRoomGiftRule{
|
|
GiftIDs: normalizeStringSet(input.GetGiftIds()),
|
|
LuckyGiftIDs: normalizeStringSet(input.GetLuckyGiftIds()),
|
|
NormalGiftIntervalMS: input.GetNormalGiftIntervalMs(),
|
|
LuckyComboMin: input.GetLuckyComboMin(),
|
|
LuckyComboMax: input.GetLuckyComboMax(),
|
|
LuckyPauseMinMS: input.GetLuckyPauseMinMs(),
|
|
LuckyPauseMaxMS: input.GetLuckyPauseMaxMs(),
|
|
}
|
|
if len(rule.GiftIDs) == 0 {
|
|
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "gift_ids is required")
|
|
}
|
|
if len(rule.LuckyGiftIDs) == 0 {
|
|
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "lucky_gift_ids is required")
|
|
}
|
|
if rule.NormalGiftIntervalMS <= 0 {
|
|
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "normal_gift_interval_ms is required")
|
|
}
|
|
if rule.LuckyComboMin <= 0 || rule.LuckyComboMax < rule.LuckyComboMin {
|
|
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "lucky combo range is invalid")
|
|
}
|
|
if rule.LuckyPauseMinMS <= 0 || rule.LuckyPauseMaxMS < rule.LuckyPauseMinMS {
|
|
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "lucky pause range is invalid")
|
|
}
|
|
return rule, nil
|
|
}
|
|
|
|
func robotRoomConfigToProto(config RobotRoomConfig) *roomv1.AdminRobotRoom {
|
|
return &roomv1.AdminRobotRoom{
|
|
AppCode: config.AppCode,
|
|
RoomId: config.RoomID,
|
|
RoomShortId: config.RoomShortID,
|
|
Title: config.Title,
|
|
CoverUrl: config.CoverURL,
|
|
VisibleRegionId: config.VisibleRegionID,
|
|
Status: config.Status,
|
|
OwnerRobotUserId: config.OwnerRobotUserID,
|
|
RobotUserIds: append([]int64(nil), config.RobotUserIDs...),
|
|
GiftRule: &roomv1.AdminRobotRoomGiftRule{
|
|
GiftIds: append([]string(nil), config.GiftRule.GiftIDs...),
|
|
LuckyGiftIds: append([]string(nil), config.GiftRule.LuckyGiftIDs...),
|
|
NormalGiftIntervalMs: config.GiftRule.NormalGiftIntervalMS,
|
|
LuckyComboMin: config.GiftRule.LuckyComboMin,
|
|
LuckyComboMax: config.GiftRule.LuckyComboMax,
|
|
LuckyPauseMinMs: config.GiftRule.LuckyPauseMinMS,
|
|
LuckyPauseMaxMs: config.GiftRule.LuckyPauseMaxMS,
|
|
},
|
|
CreatedByAdminId: config.CreatedByAdminID,
|
|
CreatedAtMs: config.CreatedAtMS,
|
|
UpdatedAtMs: config.UpdatedAtMS,
|
|
}
|
|
}
|
|
|
|
func normalizeRobotCountRange(minCount int32, maxCount int32) (int, int) {
|
|
minValue := int(minCount)
|
|
maxValue := int(maxCount)
|
|
if minValue <= 0 {
|
|
minValue = 6
|
|
}
|
|
if maxValue <= 0 {
|
|
maxValue = 8
|
|
}
|
|
if maxValue < minValue {
|
|
maxValue = minValue
|
|
}
|
|
return minValue, maxValue
|
|
}
|
|
|
|
func selectRobotUsers(rng *rand.Rand, ownerID int64, available []int64, count int) []int64 {
|
|
others := make([]int64, 0, len(available))
|
|
for _, userID := range available {
|
|
if userID != ownerID {
|
|
others = append(others, userID)
|
|
}
|
|
}
|
|
rng.Shuffle(len(others), func(i, j int) { others[i], others[j] = others[j], others[i] })
|
|
selected := []int64{ownerID}
|
|
for _, userID := range others {
|
|
if len(selected) >= count {
|
|
break
|
|
}
|
|
selected = append(selected, userID)
|
|
}
|
|
slices.Sort(selected)
|
|
return selected
|
|
}
|
|
|
|
func normalizeRobotRoomStatus(status string) string {
|
|
switch strings.TrimSpace(status) {
|
|
case robotRoomStatusActive:
|
|
return robotRoomStatusActive
|
|
case robotRoomStatusStopped:
|
|
return robotRoomStatusStopped
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func normalizeStringSet(values []string) []string {
|
|
seen := make(map[string]bool, len(values))
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || seen[value] {
|
|
continue
|
|
}
|
|
seen[value] = true
|
|
out = append(out, value)
|
|
}
|
|
slices.Sort(out)
|
|
return out
|
|
}
|
|
|
|
func int64SetKeys(values map[int64]bool) []int64 {
|
|
out := make([]int64, 0, len(values))
|
|
for value := range values {
|
|
out = append(out, value)
|
|
}
|
|
slices.Sort(out)
|
|
return out
|
|
}
|
|
|
|
func randomString(rng *rand.Rand, values []string) string {
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
return values[rng.Intn(len(values))]
|
|
}
|
|
|
|
func randomRobotTarget(rng *rand.Rand, robots []int64, senderID int64) int64 {
|
|
targets := make([]int64, 0, len(robots))
|
|
for _, userID := range robots {
|
|
if userID != senderID {
|
|
targets = append(targets, userID)
|
|
}
|
|
}
|
|
if len(targets) == 0 {
|
|
return 0
|
|
}
|
|
return targets[rng.Intn(len(targets))]
|
|
}
|
|
|
|
func randomIntRange(rng *rand.Rand, minValue int, maxValue int) int {
|
|
if maxValue <= minValue {
|
|
return minValue
|
|
}
|
|
return minValue + rng.Intn(maxValue-minValue+1)
|
|
}
|
|
|
|
func randomInt64Range(rng *rand.Rand, minValue int64, maxValue int64) int64 {
|
|
if maxValue <= minValue {
|
|
return minValue
|
|
}
|
|
return minValue + rng.Int63n(maxValue-minValue+1)
|
|
}
|
|
|
|
func randomJitter(interval time.Duration, rng *rand.Rand) time.Duration {
|
|
if interval <= time.Second {
|
|
return interval
|
|
}
|
|
return time.Duration(rng.Int63n(int64(interval))) + time.Second
|
|
}
|