ItemRT(Item Rare Table)是 PSO 的「稀有掉落表」。它决定了每种怪物、每种箱子在掉出稀有物品时, 会掉哪一件、概率多少。本页掉落表里看到的「Dragon Slayer 1/456」「Tyrell's Parasol 1/3660」这些具体数字,就是来自 ItemRT。 配合 ItemPT(掉不掉 / 掉什么大类) 和 ItemPMT(物品本身的属性)三者一起,才构成 PSO 完整的掉落系统。
在 PSO 的「掉落判定」流程里,稀有判定是独立于普通掉落的。一次怪物死亡或箱子破坏时,游戏会先做一次稀有判定:
这意味着同一只怪,「掉什么普通货」和「掉什么稀有货」是两条独立的路径。稀有掉率再低,也不会因为普通货多而降低。
ItemRT 按「难度 × Section ID」为单位存放多份子表,一共 4 × 10 = 40 份(EP4 不参与,因为 EP4 的 Episode 独立一份)。
每一份子表又分成两段:
文件层面上,PSOBB 用两个独立文件:ItemRT.gsl(Ep1 & Ep2)和 ItemRT_Ep4.gsl。
GSL 是 Sega 自己的小型归档格式(GStreamLib),里面每个 entry 对应一份子表。
(物品 ID, 概率值)。PSOBB 的 ItemRT 文件里,每个稀有物品的概率不是直接存一个分数,而是用一个 uint8(1 字节)压缩存储。
原因是 Sega 想在一个很小的文件里塞下几百条稀有记录。这个 1 字节按 SSSSS VVV 的格式解读:
(2 << shift) × (value + 7),得到一个 32 位概率值0xFFFFFFFF(即 4,294,967,295)举两个例子便于理解:
0x40000000:最终掉率 = 25%(0x40000000 / 0xFFFFFFFF ≈ 1/4)0x00FFFFFF:最终掉率 ≈ 1/256
因为压缩格式只有 256 个可选值,ItemRT 的概率不是任意精确的 — 所以社区常见的数值如 1/456、1/3660、1/11702 等看起来「不太圆整」。
它们都是压缩格式能精确表示的「合法值」中最接近的那一个。
每个 PSOBB 角色都有一个固定的 Section ID(10 种颜色 ID),这个 ID 在角色创建时由名字的 hash 决定,一旦确定不可更改。Section ID 的作用是:
这就是为什么 PSO 有「刷 ID」的文化:特定稀有物品必须用特定 ID 刷。可以用 Section ID 计算器 反推能掉某件稀有的名字。
(物品 ID, 压缩概率),固定长度ItemRT 的原始概率是 Sega 设定的「基准值」。实际服务器通常会再乘一个全局掉率倍数:
1x(基准)。周年活动里程碑会开更高的倍率。ItemRT 的原始二进制文件使用固定长度的「压缩概率 + 物品代码」作为最小存储单元。一份子表(对应一个「难度 × ID」组合)的顶部是下面这张偏移头。
Offsets(0x10 字节)| 偏移 | 字段 | 类型 | 说明 |
|---|---|---|---|
| 0x00 | monster_rares_offset | u32 | 怪物稀有池指针 → PackedDrop[0x65](v1 只有 0x33 项)。按 rt_index 索引。 |
| 0x04 | box_count | u32 | 箱子稀有条目总数,通常固定为 30(0x1E)。 |
| 0x08 | box_areas_offset | u32 | 箱子所属区域数组 → u8[box_count]。第 i 项告诉你第 i 个稀有条目位于哪个区域。 |
| 0x0C | box_rares_offset | u32 | 箱子稀有池指针 → PackedDrop[box_count],和 box_areas 一一对应。 |
PackedDrop(4 字节)每一条稀有物品记录都是 4 字节,这是 ItemRT 的最小单元。怪物稀有池和箱子稀有池都用同一种结构。
| 偏移 | 字段 | 类型 | 说明 |
|---|---|---|---|
| 0x00 | probability | u8 | 压缩概率值(SSSSS VVV 位格式)。 |
| 0x01 | item_code | u8×3 | 物品代码 data1[0..2],例如 00 01 00 表示 Saber。 |
1 字节的 probability 被当成 SSSSS VVV 解读:
S 决定 shift:shift = S − 4,范围 [−4, 27],负值按 0 算V 决定 value:value = V + 7,范围 [7, 14](2 << shift) × value,这个值作为「出该稀有物品」的分子,分母是 0xFFFFFFFF(全 32 位)0xFFFFFFFF举例:
| packed(16 进制) | shift | value | expanded | 约等于 |
|---|---|---|---|---|
| 0xFF | 27 | 14 | 0xE0000000 | 87.5% |
| 0xC0 | 20 | 7 | 0x01C00000 | 约 1/152 |
| 0x80 | 12 | 7 | 0x0001C000 | 约 1/38836 |
| 0x00 | 0 | 7 | 0x0000000E | 极小 (< 1/3 亿) |
因为只有 256 个可选值,掉率不是任意精确的 —— 社区看到的 1/456、1/3660、1/11702 这些「不圆整」的数字都是压缩表能精确表达的有效值中最接近的那一个。不可能写出例如恰好 1/500 这种精确掉率。
ExpandedDropnewserv 在内存里把每条 PackedDrop 展开成一个便于计算的结构体,交互和序列化都用它:
| 字段 | 类型 | 说明 |
|---|---|---|
| probability | u32 | 32 位展开概率(分子,分母固定 0xFFFFFFFF) |
| data | ItemData | 完整的物品数据(含 data1/data2,不止 3 字节 ID) |
BoxRare因为箱子稀有记录不按区域索引,而是把区域编号嵌在记录本身里:
| 字段 | 类型 | 说明 |
|---|---|---|
| area_norm_plus_1 | u8 | 区域编号 +1(0 保留做未使用标记) |
| drop | ExpandedDrop | 概率 + 物品数据 |
SpecCollection顶层的每一份子表对应一个「难度 × Section ID」键,展开后包含:
| 字段 | 类型 | 说明 |
|---|---|---|
| enemy_specs | map⟨Enemy, ExpandedDrop[]⟩ | 每种怪物对应的稀有池。 |
| box_specs | ExpandedDrop[][] | 按 area_norm 索引的外层数组,每个区域内是一个稀有候选列表(内层数组里每一项都是独立判定的)。 |
一次掉落事件的入口是「怪物死亡」或「箱子破坏」,两条路径独立,都先做稀有判定、失败后再走 ItemPT 的普通流程。
// ① 前置判定:这只怪到底会不会掉东西
type_drop_prob = pt.enemy_type_drop_probs[enemy_type] // 0–100
if rand(0..99) >= type_drop_prob:
return nothing // 什么都不掉
// ② 稀有判定(Stacking 算法 — 一次随机判定所有 spec)
specs = rare_item_set.get_enemy_specs(mode, episode, difficulty, section_id, enemy_type)
det = rand(0, 0x100000000) // 32 位 + 1
for spec in specs:
det -= spec.probability // 按顺序减
if det < 0:
return create_rare_item(spec.data, area) // 命中稀有,流程结束
// ③ 走 ItemPT 的普通流程,先决定大类
determinant = allow_meseta ? rand(0..2) : rand(0..1) + 1
switch determinant:
case 0: item_class = 5 (梅塞塔)
case 1: item_class = 4 (消耗品)
case 2: item_class = pt.enemy_type_item_classes[enemy_type] // 0..3 即 武器/铠甲/盾/插件
// ④ 按大类生成具体物品(权重表、磨石、bonus、special…)
generate_common_item_variances(item, area)
// ① 稀有判定(和怪物同一套 stacking 算法)
table_index = table_index_for_area(area)
specs = rare_item_set.get_box_specs(mode, episode, difficulty, section_id, table_index)
det = rand(0, 0x100000000)
for spec in specs:
det -= spec.probability
if det < 0:
return create_rare_item(spec.data, area)
// ② 没中稀有 → 从 box_item_class_prob_table 按权重抽一个大类
item_class = weighted_pick(pt.box_item_class_prob_table, table_index)
// 0=武器 1=铠甲 2=盾 3=插件 4=消耗品 5=梅塞塔 6=空
// ③ 若 item_class < 6 则生成具体物品
if item_class < 6:
generate_common_item_variances(item, area)
关于 stacking 的一个重要提醒:
stacking 的写法是「初始 det,逐个 spec 从 det 里减去概率,减到负数时命中」,等价于一次随机决定所有 spec 的命中,spec 的顺序不影响最终分布。
而 Sega 原版客户端对每个 spec 独立掷一次 rand(0, 0xFFFFFFFF) 然后和 spec.probability 比较 —— 这会让「后面的 spec 实际稀有度更高」(因为前一个 spec 命中后流程就结束了)。对只有一个 spec 的怪物没影响,但对候选多达十几项的箱子列表,原版的计算结果和 stacking 不完全一致。newserv 作者在注释里说明了这点。