- 服务端新增活动开关运行时配置与 GM 接口(GET/POST /dnf/gm/activity-switches), 持久化到 Settings,接入月签到/周签到/PC房↔累计在线的实时下发 - 服务端新增频道开关 GM 接口(channel-switches),目录生成时过滤禁用频道 - channelcatalog/channelinfo 支持按禁用频道 ID 过滤目录与客户端脚本 - 启动器新增「游戏设置」页面:频道开关 Tab + 活动开关 Tab - native.py/server.py 新增对应转发 API - 窗口标题/页面 title/侧边栏「90轮回」统一改为「DNF90 灵狐小竹整合单机版」
284 lines
9.8 KiB
Go
284 lines
9.8 KiB
Go
package dnfbridge
|
||
|
||
// gm_activity_switches.go 提供「活动开关」的运行时配置:服主在启动器的
|
||
// 「游戏设置 → 活动开关」页里切换某个活动是否下发,这里负责持久化 + 缓存,
|
||
// 并让活动目录下发(buildCurrentOnlineEventInfoBodyAt)与各活动投影读取它。
|
||
//
|
||
// 为什么单独一份 scope 而不是塞进 gm-gameplay:
|
||
//
|
||
// gameplay 那一份 scope 管的是"数值旋钮"(掉落率、倍率、疲劳、燃烧),
|
||
// 活动开关管的是"这个活动要不要在客户端显示/可参与",语义与读写频率都不同。
|
||
// 分开后,启动器页面可以各自独立刷新,也不用担心一份 scope 里键越堆越多。
|
||
//
|
||
// 持久化与 gameplay 设置同模式:repositories.Settings + scope + mutex 缓存。
|
||
// 缓存未就绪时首次读取落一次仓储,之后走内存;写入即时更新缓存并落盘。
|
||
//
|
||
// 开关的默认值(defaults)来自当前部署的真实状态,不是"全开":
|
||
//
|
||
// spend_time 累计在线得好礼(2347) 默认关:本部署走 PC 房泡点(228)分支
|
||
// level_aiming 等级预约(222) 默认关:按运营要求已关闭
|
||
// joust 骑士马战(2365) 默认关:活动已取消
|
||
// monthly 月度签到(2370) 默认开
|
||
// weekly 周周签到(2208) 默认开
|
||
// pcroom PC 房时间点(228) 默认开:当前生效的泡点活动
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
)
|
||
|
||
const gmActivitySwitchesScope = "global:dnf90:gm-activity-switches"
|
||
|
||
// 活动开关键名。值与 true/false 对应("1"/"true" 为开)。
|
||
const (
|
||
gmActivitySpendTimeKey = "spend_time"
|
||
gmActivityLevelAimingKey = "level_aiming"
|
||
gmActivityJoustKey = "joust"
|
||
gmActivityMonthlyKey = "monthly_attendance"
|
||
gmActivityWeeklyKey = "weekly_attendance"
|
||
gmActivityPCRoomKey = "pcroom_timepoint"
|
||
)
|
||
|
||
// gmActivitySwitchDefinition 描述一个可切换的活动。
|
||
type gmActivitySwitchDefinition struct {
|
||
Key string // 持久化键名
|
||
Title string // 中文名
|
||
Description string // 中文说明
|
||
Default bool // 默认开关状态
|
||
}
|
||
|
||
// gmActivitySwitchDefinitions 全部可切换活动(顺序即启动器展示顺序)。
|
||
var gmActivitySwitchDefinitions = []gmActivitySwitchDefinition{
|
||
{
|
||
Key: gmActivityPCRoomKey,
|
||
Title: "PC房时间点奖励",
|
||
Description: "泡点/挂机得奖励(事件 228/561)。当前部署默认开启。",
|
||
Default: true,
|
||
},
|
||
{
|
||
Key: gmActivitySpendTimeKey,
|
||
Title: "累计在线得好礼",
|
||
Description: "按累计在线时长领取奖励(事件 2347)。与 PC 房泡点互斥,默认关闭。",
|
||
Default: false,
|
||
},
|
||
{
|
||
Key: gmActivityMonthlyKey,
|
||
Title: "月度签到",
|
||
Description: "每月推荐副本通关计数领奖(事件 2370)。默认开启。",
|
||
Default: true,
|
||
},
|
||
{
|
||
Key: gmActivityWeeklyKey,
|
||
Title: "周周签到",
|
||
Description: "每周签到领奖(事件 2208)。默认开启。",
|
||
Default: true,
|
||
},
|
||
{
|
||
Key: gmActivityLevelAimingKey,
|
||
Title: "等级预约(命运/决策)",
|
||
Description: "全职LV预约得好礼(事件 222)。按运营要求默认关闭。",
|
||
Default: false,
|
||
},
|
||
{
|
||
Key: gmActivityJoustKey,
|
||
Title: "骑士马战",
|
||
Description: "骑士马战竞猜活动(事件 2365)。已取消,默认关闭。",
|
||
Default: false,
|
||
},
|
||
}
|
||
|
||
// GMActivitySwitch 单个活动的当前开关状态。
|
||
type GMActivitySwitch struct {
|
||
Key string `json:"key"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
Enabled bool `json:"enabled"`
|
||
}
|
||
|
||
// GMActivitySwitches 活动开关总览(供 GM 接口 / 启动器展示)。
|
||
type GMActivitySwitches struct {
|
||
Switches []GMActivitySwitch `json:"switches"`
|
||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||
}
|
||
|
||
// gmActivitySwitchesFromRecord 从设置记录解析全部活动开关。
|
||
// 缺失的键回落对应默认值;非法值回落默认值(不做硬失败,保持可恢复)。
|
||
func gmActivitySwitchesFromRecord(record dnfrepo.SettingsRecord, found bool) map[string]bool {
|
||
out := make(map[string]bool, len(gmActivitySwitchDefinitions))
|
||
for _, def := range gmActivitySwitchDefinitions {
|
||
out[def.Key] = def.Default
|
||
}
|
||
if !found {
|
||
return out
|
||
}
|
||
for _, def := range gmActivitySwitchDefinitions {
|
||
raw := strings.TrimSpace(record.Values[def.Key])
|
||
if raw == "" {
|
||
continue
|
||
}
|
||
out[def.Key] = raw == "1" || strings.EqualFold(raw, "true")
|
||
}
|
||
return out
|
||
}
|
||
|
||
// GMActivitySwitches 返回当前活动开关总览(带缓存)。
|
||
func (s *Service) GMActivitySwitches(ctx context.Context) (GMActivitySwitches, error) {
|
||
if s == nil {
|
||
return GMActivitySwitches{}, ErrGMUnavailable
|
||
}
|
||
s.gmActivitySwitchesMu.Lock()
|
||
defer s.gmActivitySwitchesMu.Unlock()
|
||
if s.gmActivitySwitchesReady {
|
||
return s.gmActivitySwitchesSnapshotLocked(), nil
|
||
}
|
||
repositories, ok := s.repositoryGroup()
|
||
if !ok || repositories.Settings == nil {
|
||
return GMActivitySwitches{}, ErrGMUnavailable
|
||
}
|
||
record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope)
|
||
if err != nil {
|
||
return GMActivitySwitches{}, err
|
||
}
|
||
s.gmActivitySwitches = gmActivitySwitchesFromRecord(record, found)
|
||
s.gmActivitySwitchesAt = record.UpdatedAt
|
||
s.gmActivitySwitchesReady = true
|
||
return s.gmActivitySwitchesSnapshotLocked(), nil
|
||
}
|
||
|
||
// GMSetActivitySwitch 切换某个活动的开关状态,返回更新后的总览。
|
||
func (s *Service) GMSetActivitySwitch(ctx context.Context, key string, enabled bool) (GMActivitySwitches, error) {
|
||
if s == nil {
|
||
return GMActivitySwitches{}, ErrGMUnavailable
|
||
}
|
||
key = strings.TrimSpace(strings.ToLower(key))
|
||
if !validGMActivitySwitchKey(key) {
|
||
return GMActivitySwitches{}, fmt.Errorf("%w: 未知的活动开关 %q", ErrGMInvalidRequest, key)
|
||
}
|
||
s.gmActivitySwitchesMu.Lock()
|
||
defer s.gmActivitySwitchesMu.Unlock()
|
||
repositories, ok := s.repositoryGroup()
|
||
if !ok || repositories.Settings == nil {
|
||
return GMActivitySwitches{}, ErrGMUnavailable
|
||
}
|
||
// 先确保缓存已就绪(读当前值),再改。
|
||
if !s.gmActivitySwitchesReady {
|
||
record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope)
|
||
if err != nil {
|
||
return GMActivitySwitches{}, err
|
||
}
|
||
s.gmActivitySwitches = gmActivitySwitchesFromRecord(record, found)
|
||
s.gmActivitySwitchesAt = record.UpdatedAt
|
||
s.gmActivitySwitchesReady = true
|
||
}
|
||
// 读最新记录用于落盘(避免覆盖其它并发写入)。
|
||
record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope)
|
||
if err != nil {
|
||
return GMActivitySwitches{}, err
|
||
}
|
||
if !found {
|
||
record = dnfrepo.SettingsRecord{Scope: gmActivitySwitchesScope}
|
||
} else {
|
||
record = dnfrepo.CloneSettings(record)
|
||
}
|
||
if record.Values == nil {
|
||
record.Values = make(map[string]string)
|
||
}
|
||
record.Values[key] = strconv.FormatBool(enabled)
|
||
record.UpdatedAt = time.Now().UTC()
|
||
if err := dnfrepo.SaveSettingsFields(ctx, repositories.Settings, record, dnfrepo.SettingsFieldValues); err != nil {
|
||
return GMActivitySwitches{}, err
|
||
}
|
||
s.gmActivitySwitches[key] = enabled
|
||
s.gmActivitySwitchesAt = record.UpdatedAt
|
||
return s.gmActivitySwitchesSnapshotLocked(), nil
|
||
}
|
||
|
||
// activitySwitchEnabled 读取某个活动的当前开关(内部无锁读取,调用方自行加锁)。
|
||
func (s *Service) activitySwitchEnabledLocked(key string) bool {
|
||
if s == nil || s.gmActivitySwitches == nil {
|
||
return false
|
||
}
|
||
enabled, ok := s.gmActivitySwitches[key]
|
||
return ok && enabled
|
||
}
|
||
|
||
// ActivitySwitchEnabled 读取某个活动的当前开关(公开、带锁)。
|
||
//
|
||
// 语义:开关「开启」意味着该活动照常下发/可参与;「关闭」意味着不下发、
|
||
// 客户端不显示该活动。未持久化过时回落定义里的默认值。
|
||
//
|
||
// 注意默认值的含义:本方法在缓存未就绪时会尽力加载一次(懒加载),
|
||
// 加载失败或仓储不可用时,回落"默认值"——即该活动在定义里的 Default。
|
||
// 这样即使 GM 接口从未被调用,玩法侧也能按部署默认(而非全关)工作。
|
||
func (s *Service) ActivitySwitchEnabled(ctx context.Context, key string) bool {
|
||
if s == nil {
|
||
return false
|
||
}
|
||
key = strings.TrimSpace(strings.ToLower(key))
|
||
s.gmActivitySwitchesMu.Lock()
|
||
defer s.gmActivitySwitchesMu.Unlock()
|
||
if !s.gmActivitySwitchesReady {
|
||
if repositories, ok := s.repositoryGroup(); ok && repositories.Settings != nil {
|
||
if record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope); err == nil {
|
||
s.gmActivitySwitches = gmActivitySwitchesFromRecord(record, found)
|
||
s.gmActivitySwitchesAt = record.UpdatedAt
|
||
s.gmActivitySwitchesReady = true
|
||
}
|
||
}
|
||
}
|
||
if s.gmActivitySwitches == nil {
|
||
for _, def := range gmActivitySwitchDefinitions {
|
||
if def.Key == key {
|
||
return def.Default
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
if enabled, ok := s.gmActivitySwitches[key]; ok {
|
||
return enabled
|
||
}
|
||
for _, def := range gmActivitySwitchDefinitions {
|
||
if def.Key == key {
|
||
return def.Default
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// gmActivitySwitchesSnapshotLocked 组装总览(调用方须持 gmActivitySwitchesMu)。
|
||
func (s *Service) gmActivitySwitchesSnapshotLocked() GMActivitySwitches {
|
||
switches := make([]GMActivitySwitch, 0, len(gmActivitySwitchDefinitions))
|
||
for _, def := range gmActivitySwitchDefinitions {
|
||
enabled := def.Default
|
||
if s.gmActivitySwitches != nil {
|
||
if value, ok := s.gmActivitySwitches[def.Key]; ok {
|
||
enabled = value
|
||
}
|
||
}
|
||
switches = append(switches, GMActivitySwitch{
|
||
Key: def.Key,
|
||
Title: def.Title,
|
||
Description: def.Description,
|
||
Enabled: enabled,
|
||
})
|
||
}
|
||
return GMActivitySwitches{
|
||
Switches: switches,
|
||
UpdatedAt: s.gmActivitySwitchesAt,
|
||
}
|
||
}
|
||
|
||
func validGMActivitySwitchKey(key string) bool {
|
||
for _, def := range gmActivitySwitchDefinitions {
|
||
if def.Key == key {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|