工程参考/活动系统

来自天龙知识库
Waylee​(留言 | 贡献)2026年10月26日 (一) 16:32的版本 (新增活动系统说明:三层结构、ActivityNotice 31 列、ActivityRuler 21 列、时间编码与踩坑)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航跳到搜索
活动系统
152 条活动 · 30 条场景排期 · 32 个活动脚本

活动不是独立进程,而是场景里的一个定时实例。WorldServer 按时刻表决定「此刻该在哪个场景开哪场活动」,通知过去之后由场景内的 Lua 脚本建实例;实例自己按五个阶段往前推、按阶段播报,到点销毁。

读法:想知道某场活动几点开、在哪个场景开 → 看 `ActivityNotice.txt` 的 1 到 7 列和 29 列,以及 `ActivityRuler.txt`。想改它的公告话术 → 看 13 到 28 列。想改它干什么 → 看 `Public/Data/Script/event/huodong/` 下对应的脚本。

三层结构

层 运行在 职责 数据来源
排期 WorldServer 每个心跳算一次「当前时刻是否踩中某场活动的开始格」,踩中就选场景并发 `WGNotifyScene` `ActivityNotice.txt` 第 1 到 12 列、第 29 列,加 `ActivityRuler.txt`
实例 GameServer,每个场景一份 维护活动对象、推进五个状态、按阶段播报、响应脚本的启停 `ActivityNotice.txt` 除第 7 列、第 29 列之外的全部列
玩法 场景内的 Lua `OnDefaultEvent` 收到排期通知、建实例;`OnTimer` 收周期心跳做玩法判定 `Public/Data/Script/event/huodong/` 下 77 个脚本

同一张 `ActivityNotice.txt` 被 WorldServer 与 GameServer 各读一遍,但读的列不一样:WorldServer 只读调度所需的列,GameServer 只读运行与播报的列。`Client/Data/Config/` 下还有一份同名副本,与服务端这份内容不同,是另一个文件。

一场活动从配置到结束

  1. 启动时 WorldServer 读 `ActivityNotice.txt` 与 `ActivityRuler.txt`,GameServer 读同一张 `ActivityNotice.txt`,各自装进自己的结构。
  2. WorldServer 每次心跳遍历全部活动,按 `time_type` 算出三个整数 `current`、`start`、`end`。
  3. 判断是否到点:服务器刚起或场景刚连上时(计时器为 0),只要 `current` 落在 `[start, end]` 区间内就立刻补触发;此后必须 `current` 正好等于 `start` 那一格才触发,错过就等明天/下周/下月。
  4. 按 `ruler` 列(第 29 列)决定这次激活哪些场景,向对应 GameServer 发 `WGNotifyScene`,携带脚本号、活动号、组内序号与 `reserve2` 到 `reserve5`。
  5. GameServer 在场景线程里把活动号写进 Scene 的「预激活活动号」,再调 `x<脚本号>_OnDefaultEvent`。脚本里通常第一句就是 `StartOneActivity`。
  6. `StartOneActivity` 在场景的活动管理器里建实例并启动计时器,实例进入状态 0。
  7. 实例靠累计运行毫秒推进状态,跨过阈值就切下一阶段并开始按间隔播报。
  8. 到第 28 列(结束时间点)指定的分钟数,或脚本调 `StopOneActivity`,实例销毁、从管理器里摘掉。

活动表:ActivityNotice.txt

31 列、152 条数据、编号 0 到 250 不连续。第 1 列同时是 GameServer 的数组下标(上限 256)和 WorldServer 的活动编号,写重了后面的行覆盖前面的行,写空号不会报错。

调度段(第 1 到 12 列)

列 列名 读法 谁读
1 id 活动编号。同时是 GameServer 数组下标,必须小于 256。 两侧
2 opt_type 0 = 只在到点那一刻开一次;1 = 除到点触发外,GameServer 重连时若仍在时间窗口内还会补开一次。补开由「场景所属服务器连上来时把该活动的计时器清零」实现。 仅 WorldServer
3 time_type 时间粒度:0 单次、1 每天、2 每周、3 每月。它同时决定第 4 列怎么编码,也决定 GameServer 判有效期时用哪个时间口径。 两侧
4 time_start 激活时刻。编码随 `time_type` 变化,见「时间怎么填」。 两侧,但只有 WorldServer 拿它当时刻用
5 sceneid 激活哪个场景。填 -1 表示所有场景。GameServer 也会读进结构,但不参与运行判定。 两侧
6 scriptid 活动脚本编号。WorldServer 用它通知,GameServer 用它每 60 秒回调一次 `OnTimer`。 两侧
7 time_slice 持续多少个 15 分钟格。0 或负数表示只在第 4 列那一刻激活一次(结束时刻 = 开始时刻);大于 0 时在低两位上加,满 96 格进一位。 仅 WorldServer
8 到 12 reserve1 到 reserve5 五个预留参数。WorldServer 只把 `reserve2` 到 `reserve5` 传给脚本,`reserve1` 不传。 两侧

播报段(第 13 到 28 列)

只由 GameServer 读。每组三列:间隔、内容、下一个阶段的时间点。

列 列名 读法
13 广播类型 发到哪个频道,默认系统频道。填 -1 或内容填 `-1` 时整场静默。
14、15、16 慢报间隔 / 慢报内容 / 慢报结束时间点 状态 0(预告开始)阶段:每 14 列分钟播一次 15 列的内容,累计到 16 列分钟切状态 1。
17、18、19 快报间隔 / 快报内容 / 快报结束时间点 状态 1(即将开始):到 19 列切状态 2。
20、21、22 刚开报间隔 / 刚开报内容 / 刚开报结束时间点 状态 2(刚开始):到 22 列切状态 3。
23、24、25 进行中报间隔 / 进行中报内容 / 进行中报结束时间点 状态 3(进行中):到 25 列切状态 4,但只在第 28 列不为 0 时生效。
26、27、28 快结束报间隔 / 快结束报内容 / 结束时间点 状态 4(预告结束):到 28 列自动销毁。填 0 表示永不自动结束,只能靠脚本调停。

这五个「结束时间点」不是时刻,是「活动开始后第几分钟」。读取时统一乘 60000,换成毫秒参与累计。填 30 表示开跑 30 分钟后进入下一阶段,不是 30 点 30 分。

收尾段(第 29 到 31 列)

列 列名 读法 谁读
29 ruler 场景选择规则,-2 到 10 有含义,见「场景怎么选」。GameServer 不读这一列。 仅 WorldServer
30 活动名称 只用于人看表。GameServer 读进结构后没有任何消费点,改它不影响行为。 GameServer 读了不用
31 失效时间 活动本身的绝对有效期,口径随第 3 列变化。填 0 或负数表示永不过期。 仅 GameServer

场景排期表:ActivityRuler.txt

21 列、30 条。一行对应一场活动,列成「活动号 + 10 组(场景号, 权重)」。读取时遇到 -1 就停,有效组数记进结构。

列 含义
1 活动号,与 `ActivityNotice.txt` 第 1 列对应
2、3 第 1 组的场景号与权重
4、5 第 2 组的场景号与权重
… 依此类推,直到第 20、21 列(第 10 组)

只有 `ruler` 填 0、1 或大于等于 2 时才会用到这张表;填 -1 或 -2 时这张表里对应的行不起作用。全服实际用到它的活动只有 24 条(`ruler` 为 0 的 17 条、为 1 的 7 条)。

场景怎么选

第 29 列 `ruler` 有六种取值,决定一场活动这次在几个场景里开:

取值 行为 用到的条数
-2 从所有主城里随机抽一个城市场景。 2
-1 只激活第 5 列写的那个场景;第 5 列是 -1 时所有场景都开。 126
0 激活 `ActivityRuler.txt` 里该活动的全部场景组,组内第一个场景收到序号 0,其余收到 -1。 17
1 按权重随机一个场景,权重来自排期表。 7
2 到 10 从场景组里随机挑这么多个场景(洗牌后取前 N 个),上限 10。 0
其他 不激活。排期表里找不到对应行时也不激活。 —

组内序号是 `WGNotifyScene` 第 3 个参数,第一个场景是 0、其余是 -1。脚本收到的第 3 个形参就是它(形参名叫 `iNoticeType`,容易误会,见「容易踩的坑」)。

时间怎么填

这是整张表最容易配错的地方。第 3 列 `time_type` 不同,第 4 列要用完全不同的量纲,第 31 列 `失效时间` 的口径也跟着变。

底层只有一个函数在算「现在是哪一格」:把当前时间压成「年 × 1000 × 100 + 年内日 × 100 + 小时 × 4 + 刻」,其中刻 = 分钟除以 15,取值 0 到 3。所以一天固定 96 格,一格 15 分钟。

time_type 粒度 第 4 列 time_start 的编码 第 31 列失效时间的编码 举例
1 每天 0 到 95 的格号,直接把「几点几分」写成 时 × 4 + 分 ÷ 15 季度时间取模 100,即格号 16:30 → 66
2 每周 星期 × 100 + 格号。星期用 0 表示周日、1 到 6 表示周一到周六 周序号 × 100 + 格号 周六 14:30 → 658
3 每月 日 × 100 + 格号 日 × 100 + 格号 每月 1 日 20:00 → 180
0 或其他 单次 年内日 × 100 + 格号 年内日 × 100 + 格号 10 月 1 日 20:00 → 27380

样本里 152 条活动的分布是:每天 95 条、每周 25 条、单次 32 条,没有月度活动。可以拿它们验证自己的算法——例如某条每天活动的 `time_start` 写 84,就是 21:00;写 66 就是 16:30。

第 7 列 `time_slice` 也按格算,不是分钟也不是小时:填 4 表示 1 小时,填 96 表示一天。跨天时会进位到高位,例如每天活动 21:00 开、持续 16 格(4 小时),结束时刻算成「次日 01:00」。

脚本怎么写

活动脚本放在 `Public/Data/Script/event/huodong/`,目录下 77 个 lua,73 个含活动入口。脚本编号到文件名的映射不看文件名,而是由 `Public/Data/Script.dat` 这个二进制索引给出,由 `CFileDataMgr` 在启动时读入。函数名必须带 `x<脚本号>_` 前缀,否则按编号回调时找不到函数。

入口 什么时候被调 参数
x<脚本号>_OnDefaultEvent WorldServer 判定到点、按 `ruler` 选中本场景时 sceneId, actId, 组内序号, reserve2, reserve3, reserve4, reserve5
x<脚本号>_OnTimer 实例启动后按固定间隔回调,间隔由 `StartOneActivity` 第 3 个参数决定,示例脚本传 60000 毫秒 sceneId, actId, uTime

`OnDefaultEvent` 里最常见的一句就是建实例:

StartOneActivity( sceneId, actId, 60000, 组内序号 )

`OnTimer` 里通常做两件事:判活动是否还在有效期内(`CheckActiviyValidity` 返回 0 就 `StopOneActivity`),以及跑玩法判定。脚本侧能调的全部接口列在 活动相关。

容易踩的坑

表里的注释行是旧版布局

`ActivityNotice.txt` 第 3 行起有一批以 `#<序号>` 开头的注释行,写着列含义。它们描述的是旧版 14 列布局:注释里 `ruler` 排在第 13 项、`time_invalid` 排在第 14 项,而现在这两列分别是第 29 列和第 31 列,中间插进了 15 列播报配置。照着注释行配表会全错,以列名行(第 2 行)和本页为准。

`time_start` 写成「几点」永远不触发

第 4 列不是「1900 表示 19:00」,而是格号。每天活动的合法范围是 0 到 95,写成 1900 会让 `current` 永远等不到,活动静默不开,日志里也不会有报错。

`iNoticeType` 这个名字有两套含义

脚本 `OnDefaultEvent` 的第 3 个形参叫 `iNoticeType`,实际收到的是「组内序号」:主场景 0、附加场景 -1。示例脚本又把它原样传给了 `StartOneActivity` 的第 4 个参数,而这个参数的含义是播报开关——传 -1 会让整场活动的系统播报全部静默(`BroadCast_Notice` 遇到 -1 直接返回)。

合起来的效果是:多场景活动只有第一个场景会播报。这多半是有意的(避免同一活动在多个场景重复刷屏),但两处同名不同义,改脚本时要留意。

`reserve1` 传不到脚本

第 8 列 `reserve1` 被 WorldServer 读进 `m_Param1` 后没有往下传,`WGNotifyScene` 只带 `reserve2` 到 `reserve5`。要往脚本传参数,从第 9 列开始填。

客户端那份副本是独立文件

`Client/Data/Config/ActivityNotice.txt` 与服务端 `Public/Config/ActivityNotice.txt` 同名,但内容不同:151 条对 152 条,条目本身也有差异。服务端读的是 `Public/Config/` 这份,两份改一边不会同步到另一边。

和脚本接口的关系

活动系统本身只负责「什么时候开、在哪个场景开、开多久、播什么」,具体玩法全在脚本里。脚本侧可用的接口按用途分四类,逐条列在 活动相关:

用途 代表接口
启停 `StartOneActivity`、`StopOneActivity`、`CheckActiviyValidity`、`GetActivityState`
实例内状态 `SetActivityParam`、`GetActivityParam`,共 1024 个参数槽,用于在脚本回调之间传递状态
排行榜 `HitParadeRegisterTab`、`HitParadePlayerJoin`、`HitParadeSort` 等,活动内的名次统计
预激活活动号 `GetScenePreActiveActivityId`,取当前正在被激活的那个活动号

一个场景同时最多挂 32 个活动实例,超出后 `AddActivity` 直接失败并写错误日志;重复开同一个活动号也会失败。这两个上限都是硬限制,脚本里做「重复开启保护」比靠引擎报错更稳。