查看“︁工程参考/活动系统”︁的源代码
来自天龙知识库
←
工程参考/活动系统
跳到导航
跳到搜索
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
__NOTOC__ <div class="lua-reference"> <div class="lua-lead"> <div role="heading" aria-level="2" class="lua-h1">活动系统</div> <div class="lua-sub">152 条活动 · 30 条场景排期 · 32 个活动脚本</div> </div> 活动不是独立进程,而是'''场景里的一个定时实例'''。WorldServer 按时刻表决定「此刻该在哪个场景开哪场活动」,通知过去之后由场景内的 Lua 脚本建实例;实例自己按五个阶段往前推、按阶段播报,到点销毁。 读法:想知道某场活动几点开、在哪个场景开 → 看 `ActivityNotice.txt` 的 1 到 7 列和 29 列,以及 `ActivityRuler.txt`。想改它的公告话术 → 看 13 到 28 列。想改它干什么 → 看 `Public/Data/Script/event/huodong/` 下对应的脚本。 == 三层结构 == {| class="wikitable" ! scope="col" style="width:12%" | 层 ! scope="col" style="width:14%" | 运行在 ! scope="col" | 职责 ! scope="col" style="width:30%" | 数据来源 |- | 排期 | 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/` 下还有一份同名副本,与服务端这份内容不同,是另一个文件。 == 一场活动从配置到结束 == # 启动时 WorldServer 读 `ActivityNotice.txt` 与 `ActivityRuler.txt`,GameServer 读同一张 `ActivityNotice.txt`,各自装进自己的结构。 # WorldServer 每次心跳遍历全部活动,按 `time_type` 算出三个整数 `current`、`start`、`end`。 # 判断是否到点:服务器刚起或场景刚连上时(计时器为 0),只要 `current` 落在 `[start, end]` 区间内就'''立刻补触发''';此后必须 `current` 正好等于 `start` 那一格才触发,错过就等明天/下周/下月。 # 按 `ruler` 列(第 29 列)决定这次激活哪些场景,向对应 GameServer 发 `WGNotifyScene`,携带脚本号、活动号、组内序号与 `reserve2` 到 `reserve5`。 # GameServer 在场景线程里把活动号写进 Scene 的「预激活活动号」,再调 `x<脚本号>_OnDefaultEvent`。脚本里通常第一句就是 `StartOneActivity`。 # `StartOneActivity` 在场景的活动管理器里建实例并启动计时器,实例进入状态 0。 # 实例靠累计运行毫秒推进状态,跨过阈值就切下一阶段并开始按间隔播报。 # 到第 28 列(结束时间点)指定的分钟数,或脚本调 `StopOneActivity`,实例销毁、从管理器里摘掉。 == 活动表:ActivityNotice.txt == 31 列、152 条数据、编号 0 到 250 不连续。第 1 列同时是 GameServer 的数组下标(上限 256)和 WorldServer 的活动编号,'''写重了后面的行覆盖前面的行,写空号不会报错'''。 === 调度段(第 1 到 12 列)=== {| class="wikitable" ! scope="col" style="width:6%" | 列 ! scope="col" style="width:16%" | 列名 ! scope="col" | 读法 ! scope="col" style="width: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 读。每组三列:间隔、内容、下一个阶段的时间点。 {| class="wikitable" ! scope="col" style="width:6%" | 列 ! scope="col" style="width:16%" | 列名 ! scope="col" | 读法 |- | 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 列)=== {| class="wikitable" ! scope="col" style="width:6%" | 列 ! scope="col" style="width:16%" | 列名 ! scope="col" | 读法 ! scope="col" style="width:12%" | 谁读 |- | 29 | ruler | 场景选择规则,-2 到 10 有含义,见「场景怎么选」。GameServer 不读这一列。 | 仅 WorldServer |- | 30 | 活动名称 | 只用于人看表。GameServer 读进结构后没有任何消费点,改它不影响行为。 | GameServer 读了不用 |- | 31 | 失效时间 | 活动本身的绝对有效期,口径随第 3 列变化。填 0 或负数表示永不过期。 | 仅 GameServer |} == 场景排期表:ActivityRuler.txt == 21 列、30 条。一行对应一场活动,列成「活动号 + 10 组(场景号, 权重)」。读取时遇到 -1 就停,有效组数记进结构。 {| class="wikitable" ! scope="col" style="width:10%" | 列 ! scope="col" | 含义 |- | 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` 有六种取值,决定一场活动这次在几个场景里开: {| class="wikitable" ! scope="col" style="width:10%" | 取值 ! scope="col" | 行为 ! scope="col" style="width:12%" | 用到的条数 |- | -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 分钟'''。 {| class="wikitable" ! scope="col" style="width:8%" | time_type ! scope="col" style="width:10%" | 粒度 ! scope="col" | 第 4 列 time_start 的编码 ! scope="col" | 第 31 列失效时间的编码 ! scope="col" style="width:14%" | 举例 |- | 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<脚本号>_` 前缀''',否则按编号回调时找不到函数。 {| class="wikitable" ! scope="col" style="width:26%" | 入口 ! scope="col" | 什么时候被调 ! scope="col" | 参数 |- | <code>x<脚本号>_OnDefaultEvent</code> | WorldServer 判定到点、按 `ruler` 选中本场景时 | <code>sceneId, actId, 组内序号, reserve2, reserve3, reserve4, reserve5</code> |- | <code>x<脚本号>_OnTimer</code> | 实例启动后按固定间隔回调,间隔由 `StartOneActivity` 第 3 个参数决定,示例脚本传 60000 毫秒 | <code>sceneId, actId, uTime</code> |} `OnDefaultEvent` 里最常见的一句就是建实例: <pre> StartOneActivity( sceneId, actId, 60000, 组内序号 ) </pre> `OnTimer` 里通常做两件事:判活动是否还在有效期内(`CheckActiviyValidity` 返回 0 就 `StopOneActivity`),以及跑玩法判定。脚本侧能调的全部接口列在 [[Lua接口/活动相关|活动相关]]。 == 容易踩的坑 == === 表里的注释行是旧版布局 === `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/` 这份,两份改一边不会同步到另一边。 == 和脚本接口的关系 == 活动系统本身只负责「什么时候开、在哪个场景开、开多久、播什么」,具体玩法全在脚本里。脚本侧可用的接口按用途分四类,逐条列在 [[Lua接口/活动相关|活动相关]]: {| class="wikitable" ! scope="col" style="width:22%" | 用途 ! scope="col" | 代表接口 |- | 启停 | `StartOneActivity`、`StopOneActivity`、`CheckActiviyValidity`、`GetActivityState` |- | 实例内状态 | `SetActivityParam`、`GetActivityParam`,共 1024 个参数槽,用于在脚本回调之间传递状态 |- | 排行榜 | `HitParadeRegisterTab`、`HitParadePlayerJoin`、`HitParadeSort` 等,活动内的名次统计 |- | 预激活活动号 | `GetScenePreActiveActivityId`,取当前正在被激活的那个活动号 |} 一个场景同时最多挂 32 个活动实例,超出后 `AddActivity` 直接失败并写错误日志;重复开同一个活动号也会失败。这两个上限都是硬限制,脚本里做「重复开启保护」比靠引擎报错更稳。 </div>
返回
工程参考/活动系统
。
导航菜单
页面操作
页面
讨论
阅读
查看源代码
历史
页面操作
页面
讨论
更多
工具
个人工具
创建账号
登录
导航
首页
最近更改
随机页面
MediaWiki帮助
特殊页面
搜索
工具
链入页面
相关更改
页面信息