2026-06-04 18:31:06 +08:00

665 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package state 定义 Room Cell 内部持有的房间强关联状态和 protobuf 快照转换。
package state
import (
"maps"
"slices"
roomv1 "hyapp.local/api/proto/room/v1"
)
const (
// RoomStatusCreating 表示房间正在创建外部依赖,尚未对客户端开放。
RoomStatusCreating = "creating"
// RoomStatusActive 表示房间可进入、可管理、可发麦位和礼物命令。
RoomStatusActive = "active"
// RoomStatusClosing 表示房间正在收尾,不再接受新业务命令。
RoomStatusClosing = "closing"
// RoomStatusClosed 表示房间已经关闭guard 和进房都必须拒绝。
RoomStatusClosed = "closed"
// RoomStatusDeleted 表示后台删除后的运营终态;保留 command log/snapshot 作为恢复和审计来源。
RoomStatusDeleted = "deleted"
// TreasureStatusIdle 表示当前等级宝箱还在积攒能量。
TreasureStatusIdle = "idle"
// TreasureStatusCountdown 表示当前等级已经满能量,等待后台 worker 到点开箱。
TreasureStatusCountdown = "countdown"
// MicPublishIdle 表示麦位没有正在确认或已确认的 RTC 发流会话。
MicPublishIdle = "idle"
// MicPublishPending 表示业务上麦成功,但客户端/RTC 尚未确认音频发布成功。
MicPublishPending = "pending_publish"
// MicPublishPublishing 表示当前 mic_session 已确认在 RTC 侧发布音频。
MicPublishPublishing = "publishing"
// MicSeatStatusEmpty 表示麦位无人占用且未锁定。
MicSeatStatusEmpty = "empty"
// MicSeatStatusLocked 表示麦位锁定且不能上麦。
MicSeatStatusLocked = "locked"
// MicSeatStatusOccupied 表示用户已业务占麦,但还没有确认可听的上行音频。
MicSeatStatusOccupied = "occupied"
// MicSeatStatusPublishing 表示用户已占麦且当前 mic_session 已确认发流。
MicSeatStatusPublishing = "publishing"
// MicSeatStatusMuted 表示用户已占麦,但服务端可见麦克风处于静音态。
MicSeatStatusMuted = "muted"
)
// MicSeat 表达房间内单个麦位的占用状态。
type MicSeat struct {
// SeatNo 是房间内稳定麦位编号,不使用切片下标作为对外标识。
SeatNo int32
// UserID 为 0 表示麦位空闲,非 0 表示当前占用用户。
UserID int64
// Locked 表示麦位被锁定,锁定麦位不能上麦或换入。
Locked bool
// PublishState 区分业务占麦和 RTC 发流确认状态,避免把 MicUp 当作已发流。
PublishState string
// MicSessionID 标识单次上麦发流会话,确认和超时清理都必须匹配它。
MicSessionID string
// PublishDeadlineMS 是 pending_publish 自动释放麦位的截止时间。
PublishDeadlineMS int64
// MicSessionRoomVersion 是创建该会话的房间版本,用于丢弃旧版本 RTC 事件。
MicSessionRoomVersion int64
// LastPublishEventTimeMS 是已接受的最新 RTC/客户端发布事件时间。
LastPublishEventTimeMS int64
// MicHeartbeatAtMS 是当前 mic_session 最近一次服务端接受麦上心跳的时间;它不参与 RTC 事件新旧判断。
MicHeartbeatAtMS int64
// MicMuted 是服务端可见的麦克风静音状态,用于同步其他用户 UI。
MicMuted bool
}
// SeatStatus 返回面向客户端展示的派生麦位状态。
//
// PublishState 继续表达 RTC 会话确认阶段SeatStatus 只负责把“空/锁/占麦/发流/静音”
// 收敛成一个展示字段,避免客户端把 pending_publish 误当作可听发流。
func (seat MicSeat) SeatStatus() string {
if seat.UserID <= 0 {
if seat.Locked {
return MicSeatStatusLocked
}
return MicSeatStatusEmpty
}
if seat.MicMuted {
return MicSeatStatusMuted
}
if seat.PublishState == MicPublishPublishing {
return MicSeatStatusPublishing
}
return MicSeatStatusOccupied
}
// RoomUserState 只保存 room-service 真正需要的轻量用户态。
type RoomUserState struct {
// UserID 是房间 presence 的用户标识。
UserID int64
// Role 是房间内轻量角色,不承载完整用户资料。
Role string
// JoinedAtMS 是进入房间业务态的时间。
JoinedAtMS int64
// LastSeenAtMS 是最后一次房间业务操作时间,不等同于腾讯云 IM 心跳。
LastSeenAtMS int64
}
// UserModerationState 保存房间内 ban/mute 这类治理状态的时间边界。
type UserModerationState struct {
// UserID 是被治理用户。
UserID int64
// ActorUserID 是触发治理命令的房间内操作者;系统治理入口使用 0。
ActorUserID int64
// CreatedAtMS 是治理状态写入 Room Cell 的 UTC epoch milliseconds。
CreatedAtMS int64
// ExpiresAtMS 为 0 表示永久有效;非 0 表示该 UTC 毫秒后自动失效。
ExpiresAtMS int64
}
// ActiveAt 判断治理状态在指定 UTC 毫秒是否仍然生效。
func (m UserModerationState) ActiveAt(nowMS int64) bool {
return m.UserID > 0 && (m.ExpiresAtMS == 0 || m.ExpiresAtMS > nowMS)
}
// RankItem 表达房间内本地礼物榜的单个用户累计值。
type RankItem struct {
// UserID 是榜单归属用户。
UserID int64
// Score 是展示排序分值,当前版本等于 GiftValue。
Score int64
// GiftValue 是房间内礼物价值累计。
GiftValue int64
// UpdatedAtMS 是榜单项最后更新时间,分数相同时用于排序。
UpdatedAtMS int64
}
// TreasureRewardGrant 记录一次宝箱开箱时给单个用户结算出的奖励。
type TreasureRewardGrant struct {
// RewardRole 区分 in_room、top1、igniter 三种奖励归因,客户端据此分组展示。
RewardRole string
// UserID 是奖励归属用户top1 和 igniter 即使离房也必须用满能量时锁定的用户。
UserID int64
// RewardItemID 是后台奖励池内的稳定项 ID用于排障和客户端去重。
RewardItemID string
// ResourceGroupID 是 wallet-service 发放资源组 ID。
ResourceGroupID int64
// DisplayName/IconURL 是开箱 IM 展示快照,避免客户端为历史消息回查后台配置。
DisplayName string
IconURL string
// GrantID 是 wallet-service 资源发放回执;空值表示该奖励未实际发放。
GrantID string
// Status 表达发放结果,当前成功路径写 succeeded。
Status string
}
// TreasureState 保存 Room Cell 内当前宝箱进度和最近一次开箱结果。
type TreasureState struct {
// CurrentLevel 是当前正在积攒的宝箱等级UTC 日重置后回到 1。
CurrentLevel int32
// CurrentProgress 是当前等级已累计能量,满级后的溢出不跨等级继承。
CurrentProgress int64
// EnergyThreshold 是当前等级阈值快照,便于客户端不依赖配置二次计算。
EnergyThreshold int64
// Status 区分 idle/countdown倒计时期间送礼能量无效。
Status string
// CountdownStartedAtMS/OpenAtMS/OpenedAtMS 描述最近一次满能量到开箱的时间线。
CountdownStartedAtMS int64
OpenAtMS int64
OpenedAtMS int64
// ResetAtMS 是下一次 UTC 自然日重置毫秒时间。
ResetAtMS int64
// Top1UserID 和 IgniterUserID 在满能量瞬间锁定,用户离房或下线仍按这里发奖。
Top1UserID int64
IgniterUserID int64
// BoxID 是单次宝箱从积攒到开箱的幂等标识,奖励抽取和 wallet command_id 都依赖它。
BoxID string
// ConfigVersion 记录本轮宝箱使用的后台配置版本,开箱时不被中途配置变更影响。
ConfigVersion int64
// LastRewards 保存最近一次开箱发奖结果,用于断线重连后补 UI。
LastRewards []TreasureRewardGrant
}
// Clone 深拷贝宝箱状态,避免 Room Cell 失败命令污染当前状态。
func (t *TreasureState) Clone() *TreasureState {
if t == nil {
return nil
}
cloned := *t
cloned.LastRewards = append([]TreasureRewardGrant(nil), t.LastRewards...)
return &cloned
}
// RoomState 是 Room Cell 的核心业务状态。
// 这里刻意不放钱包余额、完整用户资料和跨房活动状态。
type RoomState struct {
// RoomID 是该状态所属房间。
RoomID string
// OwnerUserID 是房间所有者owner 权限由专门字段派生,不写入 AdminUsers。
OwnerUserID int64
// Mode 是房间模式,当前只作为状态字段透出。
Mode string
// VisibleRegionID 是房间礼物、列表和区域可见性策略使用的区域桶。
VisibleRegionID int64
// Status 是房间生命周期状态。
Status string
// ChatEnabled 控制腾讯云 IM 公屏发言守卫。
ChatEnabled bool
// RoomPasswordHash 非空表示房间开启入房密码;只保存哈希,明文只存在于单次 JoinRoom 请求。
RoomPasswordHash string
// MicSeats 是麦位占用状态,所有麦位变更必须在 Room Cell 内完成。
MicSeats []MicSeat
// OnlineUsers 是 room-service 业务 presence不保存真实 socket。
OnlineUsers map[int64]*RoomUserState
// AdminUsers 是房间管理权限集合。
AdminUsers map[int64]bool
// BanUsers 是被踢或封禁的房间用户状态,过期时间由 Room Cell 持久化和恢复。
BanUsers map[int64]UserModerationState
// MuteUsers 是被禁言用户集合。
MuteUsers map[int64]bool
// Heat 是房间热度,当前主要由送礼累计。
Heat int64
// GiftRank 是房间内本地礼物榜快照结果。
GiftRank []RankItem
// RoomExt 预留少量房间扩展字段,避免 v1 频繁改结构。
RoomExt map[string]string
// Treasure 是语音房宝箱状态,送礼能量和开箱必须跟随 Room Cell 串行提交。
Treasure *TreasureState
// Version 是房间状态版本,成功变更后递增,快照和 command log 都依赖它。
Version int64
}
// NewRoomState 初始化一个空房间状态。
func NewRoomState(roomID string, ownerUserID int64, seatCount int32, mode string) *RoomState {
// 麦位编号从 1 开始,避免对外暴露 0 号麦位和切片下标。
seats := make([]MicSeat, 0, seatCount)
for seatNo := int32(1); seatNo <= seatCount; seatNo++ {
seats = append(seats, MicSeat{SeatNo: seatNo})
}
return &RoomState{
RoomID: roomID,
OwnerUserID: ownerUserID,
Mode: mode,
Status: RoomStatusActive,
ChatEnabled: true,
MicSeats: seats,
OnlineUsers: make(map[int64]*RoomUserState),
AdminUsers: make(map[int64]bool),
BanUsers: make(map[int64]UserModerationState),
MuteUsers: make(map[int64]bool),
RoomExt: make(map[string]string),
}
}
// Clone 深拷贝当前房间状态,供命令流水线在副本上计算新状态。
func (s *RoomState) Clone() *RoomState {
if s == nil {
// nil 输入保持 nil便于防御性快照调用。
return nil
}
// 所有 map 和 slice 都重新分配,避免 mutate 失败时污染当前 Cell 状态。
cloned := &RoomState{
RoomID: s.RoomID,
OwnerUserID: s.OwnerUserID,
Mode: s.Mode,
VisibleRegionID: s.VisibleRegionID,
Status: s.Status,
ChatEnabled: s.ChatEnabled,
RoomPasswordHash: s.RoomPasswordHash,
MicSeats: append([]MicSeat(nil), s.MicSeats...),
OnlineUsers: make(map[int64]*RoomUserState, len(s.OnlineUsers)),
AdminUsers: make(map[int64]bool, len(s.AdminUsers)),
BanUsers: make(map[int64]UserModerationState, len(s.BanUsers)),
MuteUsers: make(map[int64]bool, len(s.MuteUsers)),
Heat: s.Heat,
GiftRank: append([]RankItem(nil), s.GiftRank...),
RoomExt: make(map[string]string, len(s.RoomExt)),
Treasure: cloneTreasureState(s.Treasure),
Version: s.Version,
}
for userID, userState := range s.OnlineUsers {
// 用户态用指针保存,必须逐项复制结构体。
copied := *userState
cloned.OnlineUsers[userID] = &copied
}
maps.Copy(cloned.AdminUsers, s.AdminUsers)
maps.Copy(cloned.BanUsers, s.BanUsers)
maps.Copy(cloned.MuteUsers, s.MuteUsers)
maps.Copy(cloned.RoomExt, s.RoomExt)
return cloned
}
// ReplaceWith 用新状态整体替换当前状态。
// 这里用整对象替换,是为了避免部分字段更新时残留旧引用。
func (s *RoomState) ReplaceWith(next *RoomState) {
// Clone 后再整体赋值,避免调用方把 next 的 map/slice 引用留在当前状态里。
*s = *next.Clone()
}
// SeatByUser 返回用户当前所在麦位。
func (s *RoomState) SeatByUser(userID int64) (MicSeat, bool) {
// 麦位数量较小,线性扫描比维护额外索引更简单可靠。
for _, seat := range s.MicSeats {
if seat.UserID == userID {
return seat, true
}
}
return MicSeat{}, false
}
// ClearMicSession 清空麦位占用和 RTC 发流会话字段。
func (s *RoomState) ClearMicSession(index int) {
if index < 0 || index >= len(s.MicSeats) {
return
}
seatNo := s.MicSeats[index].SeatNo
locked := s.MicSeats[index].Locked
s.MicSeats[index] = MicSeat{
SeatNo: seatNo,
Locked: locked,
}
}
// SeatIndex 返回指定麦位在切片中的下标。
func (s *RoomState) SeatIndex(seatNo int32) int {
// 返回 -1 表示外部传入的麦位编号不存在。
for index, seat := range s.MicSeats {
if seat.SeatNo == seatNo {
return index
}
}
return -1
}
// ToProto 把当前内部房间态投影成对外使用的 protobuf 快照。
func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
if s == nil {
// nil 状态没有可投影快照,调用方按房间不存在处理。
return nil
}
// OnlineUsers 是 map先排序 user_id保证快照输出稳定便于测试和排障。
users := make([]*roomv1.RoomUser, 0, len(s.OnlineUsers))
userIDs := make([]int64, 0, len(s.OnlineUsers))
for userID := range s.OnlineUsers {
userIDs = append(userIDs, userID)
}
slices.Sort(userIDs)
for _, userID := range userIDs {
// protobuf 快照只暴露轻量 room user不包含 user-service 主资料。
userState := s.OnlineUsers[userID]
users = append(users, &roomv1.RoomUser{
UserId: userState.UserID,
Role: userState.Role,
JoinedAtMs: userState.JoinedAtMS,
LastSeenAtMs: userState.LastSeenAtMS,
})
}
seats := make([]*roomv1.SeatState, 0, len(s.MicSeats))
for _, seat := range s.MicSeats {
// 麦位顺序沿用内部切片顺序,即初始化的 SeatNo 升序和后续原位修改。
seats = append(seats, &roomv1.SeatState{
SeatNo: seat.SeatNo,
UserId: seat.UserID,
Locked: seat.Locked,
PublishState: seat.PublishState,
MicSessionId: seat.MicSessionID,
PublishDeadlineMs: seat.PublishDeadlineMS,
MicSessionRoomVersion: seat.MicSessionRoomVersion,
LastPublishEventTimeMs: seat.LastPublishEventTimeMS,
MicMuted: seat.MicMuted,
SeatStatus: seat.SeatStatus(),
MicHeartbeatAtMs: seat.MicHeartbeatAtMS,
})
}
rankItems := make([]*roomv1.RankItem, 0, len(s.GiftRank))
for _, item := range s.GiftRank {
// GiftRank 已由 LocalRank 排好序,这里只做结构转换。
rankItems = append(rankItems, &roomv1.RankItem{
UserId: item.UserID,
Score: item.Score,
GiftValue: item.GiftValue,
UpdatedAtMs: item.UpdatedAtMS,
})
}
return &roomv1.RoomSnapshot{
RoomId: s.RoomID,
OwnerUserId: s.OwnerUserID,
Mode: s.Mode,
VisibleRegionId: s.VisibleRegionID,
Status: s.Status,
ChatEnabled: s.ChatEnabled,
MicSeats: seats,
OnlineUsers: users,
AdminUserIds: sortedSetIDsExcept(s.AdminUsers, s.OwnerUserID),
BanUserIds: sortedModerationIDs(s.BanUsers),
MuteUserIds: sortedSetIDs(s.MuteUsers),
GiftRank: rankItems,
RoomExt: cloneStringMap(s.RoomExt),
Heat: s.Heat,
Version: s.Version,
RoomShortId: s.RoomExt["room_short_id"],
Locked: s.RoomPasswordHash != "",
Treasure: treasureStateToProto(s.Treasure),
BanStates: sortedModerationStates(s.BanUsers),
}
}
// FromProto 把持久化快照恢复回内部房间态。
func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState {
if snapshot == nil {
// 没有快照时由调用方基于 RoomMeta 新建初始状态。
return nil
}
// 所有集合重新分配,恢复后的状态可以直接交给 Room Cell 独占使用。
restored := &RoomState{
RoomID: snapshot.GetRoomId(),
OwnerUserID: snapshot.GetOwnerUserId(),
Mode: snapshot.GetMode(),
VisibleRegionID: snapshot.GetVisibleRegionId(),
Status: snapshot.GetStatus(),
ChatEnabled: snapshot.GetChatEnabled(),
MicSeats: make([]MicSeat, 0, len(snapshot.GetMicSeats())),
OnlineUsers: make(map[int64]*RoomUserState, len(snapshot.GetOnlineUsers())),
AdminUsers: make(map[int64]bool, len(snapshot.GetAdminUserIds())),
BanUsers: make(map[int64]UserModerationState, len(snapshot.GetBanStates())+len(snapshot.GetBanUserIds())),
MuteUsers: make(map[int64]bool, len(snapshot.GetMuteUserIds())),
GiftRank: make([]RankItem, 0, len(snapshot.GetGiftRank())),
RoomExt: cloneStringMap(snapshot.GetRoomExt()),
Treasure: treasureStateFromProto(snapshot.GetTreasure()),
Heat: snapshot.GetHeat(),
Version: snapshot.GetVersion(),
}
for _, seat := range snapshot.GetMicSeats() {
// protobuf 空字段会映射成零值UserID=0 仍表示空麦位。
restored.MicSeats = append(restored.MicSeats, MicSeat{
SeatNo: seat.GetSeatNo(),
UserID: seat.GetUserId(),
Locked: seat.GetLocked(),
PublishState: seat.GetPublishState(),
MicSessionID: seat.GetMicSessionId(),
PublishDeadlineMS: seat.GetPublishDeadlineMs(),
MicSessionRoomVersion: seat.GetMicSessionRoomVersion(),
LastPublishEventTimeMS: seat.GetLastPublishEventTimeMs(),
MicHeartbeatAtMS: seat.GetMicHeartbeatAtMs(),
MicMuted: seat.GetMicMuted(),
})
}
for _, user := range snapshot.GetOnlineUsers() {
// 快照中的 online users 是业务 presence重启后腾讯云 IM 连接由客户端重新登录进群。
restored.OnlineUsers[user.GetUserId()] = &RoomUserState{
UserID: user.GetUserId(),
Role: user.GetRole(),
JoinedAtMS: user.GetJoinedAtMs(),
LastSeenAtMS: user.GetLastSeenAtMs(),
}
}
for _, userID := range snapshot.GetAdminUserIds() {
// set 在 protobuf 中用数组表达,恢复时重新转为 map。
if userID == restored.OwnerUserID {
// owner 不是管理员;旧快照里如果残留 owner恢复时过滤掉。
continue
}
restored.AdminUsers[userID] = true
}
for _, ban := range snapshot.GetBanStates() {
if ban.GetUserId() <= 0 {
continue
}
restored.BanUsers[ban.GetUserId()] = UserModerationState{
UserID: ban.GetUserId(),
ActorUserID: ban.GetActorUserId(),
CreatedAtMS: ban.GetCreatedAtMs(),
ExpiresAtMS: ban.GetExpiresAtMs(),
}
}
for _, userID := range snapshot.GetBanUserIds() {
if userID <= 0 {
continue
}
if _, exists := restored.BanUsers[userID]; !exists {
// 旧快照只有 ban_user_ids 时按永久 ban 恢复,避免恢复后放宽权限。
restored.BanUsers[userID] = UserModerationState{UserID: userID}
}
}
for _, userID := range snapshot.GetMuteUserIds() {
restored.MuteUsers[userID] = true
}
for _, item := range snapshot.GetGiftRank() {
restored.GiftRank = append(restored.GiftRank, RankItem{
UserID: item.GetUserId(),
Score: item.GetScore(),
GiftValue: item.GetGiftValue(),
UpdatedAtMS: item.GetUpdatedAtMs(),
})
}
return restored
}
func sortedSetIDs(values map[int64]bool) []int64 {
// map 转数组时只导出 value=true 的成员false 等价于不存在。
ids := make([]int64, 0, len(values))
for userID, enabled := range values {
if enabled {
ids = append(ids, userID)
}
}
slices.Sort(ids)
return ids
}
func sortedModerationIDs(values map[int64]UserModerationState) []int64 {
ids := make([]int64, 0, len(values))
for userID, moderation := range values {
if moderation.UserID > 0 {
ids = append(ids, userID)
}
}
slices.Sort(ids)
return ids
}
func sortedModerationStates(values map[int64]UserModerationState) []*roomv1.RoomBanState {
ids := sortedModerationIDs(values)
states := make([]*roomv1.RoomBanState, 0, len(ids))
for _, userID := range ids {
moderation := values[userID]
states = append(states, &roomv1.RoomBanState{
UserId: moderation.UserID,
ActorUserId: moderation.ActorUserID,
CreatedAtMs: moderation.CreatedAtMS,
ExpiresAtMs: moderation.ExpiresAtMS,
})
}
return states
}
func sortedSetIDsExcept(values map[int64]bool, excluded int64) []int64 {
ids := make([]int64, 0, len(values))
for userID, enabled := range values {
if enabled && userID != excluded {
ids = append(ids, userID)
}
}
slices.Sort(ids)
return ids
}
func cloneStringMap(input map[string]string) map[string]string {
if input == nil {
// protobuf map 缺省按空 map 处理,调用方可以安全写入。
return map[string]string{}
}
// RoomExt 是扩展字段集合,转换时必须复制,避免共享 map 引用。
cloned := make(map[string]string, len(input))
maps.Copy(cloned, input)
return cloned
}
func cloneTreasureState(input *TreasureState) *TreasureState {
if input == nil {
return nil
}
cloned := *input
cloned.LastRewards = append([]TreasureRewardGrant(nil), input.LastRewards...)
return &cloned
}
func treasureStateToProto(input *TreasureState) *roomv1.RoomTreasureState {
if input == nil {
return nil
}
rewards := make([]*roomv1.RoomTreasureRewardGrant, 0, len(input.LastRewards))
for _, reward := range input.LastRewards {
rewards = append(rewards, &roomv1.RoomTreasureRewardGrant{
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 &roomv1.RoomTreasureState{
CurrentLevel: input.CurrentLevel,
CurrentProgress: input.CurrentProgress,
EnergyThreshold: input.EnergyThreshold,
Status: input.Status,
CountdownStartedAtMs: input.CountdownStartedAtMS,
OpenAtMs: input.OpenAtMS,
OpenedAtMs: input.OpenedAtMS,
ResetAtMs: input.ResetAtMS,
Top1UserId: input.Top1UserID,
IgniterUserId: input.IgniterUserID,
BoxId: input.BoxID,
ConfigVersion: input.ConfigVersion,
LastRewards: rewards,
}
}
func treasureStateFromProto(input *roomv1.RoomTreasureState) *TreasureState {
if input == nil {
return nil
}
rewards := make([]TreasureRewardGrant, 0, len(input.GetLastRewards()))
for _, reward := range input.GetLastRewards() {
rewards = append(rewards, TreasureRewardGrant{
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 &TreasureState{
CurrentLevel: input.GetCurrentLevel(),
CurrentProgress: input.GetCurrentProgress(),
EnergyThreshold: input.GetEnergyThreshold(),
Status: input.GetStatus(),
CountdownStartedAtMS: input.GetCountdownStartedAtMs(),
OpenAtMS: input.GetOpenAtMs(),
OpenedAtMS: input.GetOpenedAtMs(),
ResetAtMS: input.GetResetAtMs(),
Top1UserID: input.GetTop1UserId(),
IgniterUserID: input.GetIgniterUserId(),
BoxID: input.GetBoxId(),
ConfigVersion: input.GetConfigVersion(),
LastRewards: rewards,
}
}