DAT 里的 <INTEGER>VALUE: 不是商人窗口里那个数。引擎拿它乘一条按物品等级走的百分比曲线,再 ceil 成整数 ——
买入、卖出、未鉴定买、未鉴定卖各走各的曲线,六个定价档各有各的一套。这里按 Torchlight2.exe 1.25.9.5 逐指令复算,也能反过来从目标价推回该写多少 VALUE。
左边填 DAT 里的键和现场条件,右边是引擎会算出来的金币数。所有数字实时联动。
MEDIA/GLOBALS.DAT 与商人 DAT 的值(挑战者大陆没改)。
stat 83 是角色侧的隐藏统计:卖出时加成、买入时打折,原版里没有任何词缀会动它,留 0 即可。SOCKETED_SELL_PCT;
买价完全不看已镶嵌的宝石。插槽数本身两边都算。定价时真正要回答的问题是「我要玩家花多少金」。填目标金额,这里给出该写的 VALUE。
等级、定价档、堆叠、商人与角色修正全部沿用上面换算台的设置。
一次算一批,方便把一整条消耗品线 / 一套装备的定价摆在一起看。
名称, VALUE, 等级, 定价档,后三项可省略(省略就沿用换算台的设置)。
第一段不是数字就当成名称。定价档写 NORMAL / GREEN / MAGIC / UNIQUE / MAP / GAMBLE,
也认 普通 / 词缀 / 魔法 / 唯一 / 地图 / 赌博。行首 # 是注释。下面每一行都能在 Torchlight2.exe 1.25.9.5(基址 0x400000)里指到具体地址。
没确认的部分单列在最后,没有猜的成分。
第一步 —— CEquipment::computePrices @0x581AF0。物品创建 / 读档时跑一次,
把四个整数缓存进对象:+0x2A8 买入、+0x2AC 卖出、+0x2B0 未鉴定买、+0x2B4 未鉴定卖。
V = getInt(dat, "VALUE", 100) @0x581B3D 键不存在时默认 100,不是 0
if (V == 0) { 买=1; 卖=1; return; } @0x581B6C cmp ebx,edi(edi=0) —— 整数比较
L = max(itemLevel, 1) @0x581B74 itemLevel = CBaseUnit+0x110,DAT 的 LEVEL 或刷怪时 setLevel
档位按这个顺序判,先命中先算:
0. isGamblerStock @0x57BFC0 → PRICE_PLAYERGAMBLE_MAGIC( L + max(0, rand(-3..3)) ),卖价保持 1
1. UNITTYPE ⊃ Unique (0x36) @0x511FA0 → PRICE_PLAYERBUY_UNIQUE / PRICE_PLAYERSELL_UNIQUE
2. UNITTYPE ⊃ Magic (0x37) → PRICE_PLAYERBUY_MAGIC / PRICE_PLAYERSELL_MAGIC
3. vtable+0x1C0 为真 @0x57EB60 → PRICE_PLAYERBUY_GREEN / PRICE_PLAYERSELL_GREEN
4. UNITTYPE ⊃ Map (0xAC) → MAGIC 两条曲线,且先 V = (int)(*(float*)(this+0x4A8) × V)
5. 其它 → PRICE_PLAYERBUY_NORMAL / PRICE_PLAYERSELL_NORMAL
买 = (int)ceil( V × 曲线(L) × 0.01f ) 0.01f = flt_2142654 = 0.00999999977648258…
卖 = (int)ceil( V × 曲线(L) × 0.01f )
无论走哪一档,最后都会再跑一遍 NORMAL 两条曲线,填未鉴定价: @0x582254
未鉴定买 = (int)ceil( f32(V) × PRICE_PLAYERBUY_NORMAL(L) × 0.01f )
未鉴定卖 = (int)ceil( f32(V) × PRICE_PLAYERSELL_NORMAL(L) × 0.01f )
第二步 —— 商人窗口现算,每次都跑:
CItem::getPlayerBuyPrice @0x57E070 与 CItem::getPlayerSellValue @0x577C10。
买入价 if (!已鉴定 +0x3BC==0) return max(堆叠,1) × 未鉴定买
p = max(堆叠,1) × 买 堆叠 = +0x278
if (在赌博容器里) return 卖出价(this) @0x57E0EA 赌博窗口用卖价当买价
p = (int)( getFloat(商人dat, "MERCHANT_PRICE_MULT", 1.0) × p )
if (插槽数) p = (int)( (插槽数 × SOCKET_VALUE_BONUS_PCT × 0.01 + 1) × p )
if (stat83) p = max( p − (int)(stat83 × 0.01 × p), 1 ) ← 减
卖出价 if (!已鉴定) return max(堆叠,1) × 未鉴定卖
p = max(堆叠,1) × 卖
if (stat83) p += (int)(stat83 × 0.01 × p) ← 加
if (插槽数) p = (int)( (插槽数 × SOCKET_VALUE_BONUS_PCT × 0.01 + 1) × p )
for each 已镶嵌且 UNITTYPE ⊃ Socketable(0x78):
p = (int)( 该宝石的卖出价 × SOCKETED_SELL_PCT × 0.01 + p ) 递归调用自己
曲线 —— MEDIA/GRAPHS/STATS/PRICE_*.DAT,
每条 DATATYPE:LINE、CURVED:false、105 个控制点(X = 物品等级 1…105)。
十条全都是精确的直线,所以可以写成闭式:
| 曲线 | Y(L) = 百分比 | L=1 | L=50 | L=105 | L>105 |
|---|---|---|---|---|---|
| PRICE_PLAYERBUY_NORMAL | 49 + 11 L | 60 | 599 | 1204 | 继续外推 |
| PRICE_PLAYERSELL_NORMAL | 14 + 1 L | 15 | 64 | 119 | 继续外推 |
| PRICE_PLAYERBUY_GREEN | 220.5 + 49.5 L | 270 | 2695.5 | 5418 | 封顶 |
| PRICE_PLAYERSELL_GREEN | 35 + 2.5 L | 37.5 | 160 | 297.5 | 封顶 |
| PRICE_PLAYERBUY_MAGIC | 392 + 88 L | 480 | 4792 | 9632 | 继续外推 |
| PRICE_PLAYERSELL_MAGIC | 56 + 4 L | 60 | 256 | 476 | 继续外推 |
| PRICE_PLAYERBUY_UNIQUE | 2352 + 528 L | 2880 | 28752 | 57792 | 继续外推 |
| PRICE_PLAYERSELL_UNIQUE | 168 + 12 L | 180 | 768 | 1428 | 继续外推 |
| PRICE_PLAYERGAMBLE_MAGIC | 400 + 100 L | 500 | 5400 | 10900 | 继续外推 |
| PRICE_ENCHANT (附魔费,本工具不算) | 245 + 55 L | 300 | 2995 | 6020 | 继续外推 |
L 超过 105 时的行为不是统一的,取决于该 DAT 里有没有 INFER_PASSED_END 这个键
(CGraph::evaluate @0x688E00 读 +0x3D,缺省 false —— @0x689599 push 0):
+0x40 / +0x48 / +0x50)。DynamicAttributeCurved::getValue @0x7F1090 —— 那里对超出末点的 x 直接返回末点的 Y。
结果:等级 106 以上的「带词缀普通装」按 105 级定价,价格封顶;同一件东西如果是 Magic / Unique 就会继续涨。两条 ceil 路径不一样,这不是笔误:
NORMAL / GREEN / MAP 档 @0x58221A fstp dword → ceil(float) → 先舍到 float32 再 ceil
UNIQUE / MAGIC / GAMBLE @0x581D3E fstp qword → ceil(double) → 保持 double 再 ceil
未鉴定的那一对 @0x58232F fstp qword,且 V 是从 float32 槽 fld 回来的
因为 0.01f 比 1/100 略小,乘出来永远比真值低一点点。走 float32 的那条路,
这点误差会被 float32 的舍入吃掉;走 double 的那条不会。真值不是整数时两条路会差 1 金。
在 VALUE 1…399999 的范围内扫 GREEN 买价(L=105,百分比 5418 带小数),有 148373 个 VALUE 两条路结果不同 ——
本工具把两条路分开实现了,没有统一成一种。
| 地址 | 函数 | 作用 |
|---|---|---|
0x581AF0 | CEquipment::computePrices | VALUE × 等级曲线 → 四个缓存价 |
0x57E070 | CItem::getPlayerBuyPrice | 买入价:堆叠 / 商人乘数 / 插槽 / stat83 折扣 |
0x577C10 | CItem::getPlayerSellValue | 卖出价:堆叠 / stat83 加成 / 插槽 / 逐颗宝石 |
0x688E00 | CGraph::evaluate | +0x3D = INFER_PASSED_END,真则外推 |
0x7F1090 | DynamicAttributeCurved::getValue | 线性插值;超出两端一律夹到端点 Y |
0x689490 | CGraph::loadFromDat | 读 CURVED / INFER_PASSED_END(默认 false) |
0x57EB60 | CEquipment vtable+0x1C0 | GREEN 判据:非 Magic、非 Socketable,且有已烘焙附加伤害或任何非隐藏 effect / 词缀 |
0x511FA0 | CUnit::hasUnitType | id 来自 MEDIA/UNITTYPES.HIE:54=Unique 55=Magic 172=Map 120=Socketable 41=Merchant |
0x641792 / 0x6417D8 | CGlobals::loadFromDat | +0xF0=SOCKETED_SELL_PCT(75)、+0xF4=SOCKET_VALUE_BONUS_PCT(10) |
0x516A74 | CBaseUnit::loadCommonFromDat | DAT 的 LEVEL → +0x110,就是曲线的 X |
0x41C8C0 | (内联包装) | ceil(float) —— NORMAL/GREEN/MAP 那条路的舍入点 |
数据出处:E:/Torchlight 2/MEDIA/GRAPHS/STATS/PRICE_*.DAT(10 个文件,逐点解析后确认全为直线)、
MEDIA/GLOBALS.DAT、MEDIA/UNITTYPES.HIE。
挑战者大陆的 暗黑传奇 与 Adventure_mode 两个组件覆盖了 GLOBALS.DAT,但三个价格相关键与原版一致;
仓库里那套 PRICE_*.DAT 覆盖已经移进「已移除内容」,当前不生效 —— 所以本工具用的就是原版曲线。
CUnit::getStat(unit, 83, 7, 0) 被这两个价格函数调用,
且全镜像只有这两处调它。它是引擎内置的预定义 stat,不在 MEDIA/STATS 里,名字没查出来。
原版没有任何数据会写它,所以实战恒为 0;工具留了输入位,但那一栏填的数没有实测背书。*(float*)(this+0x4A8) 在地图物品定价前会乘一次 V,
来源是地图生成流程(@0x582105 附近一串 DUNGEON 相关调用)。这条链没往下追,
工具默认按 1.0 算 —— 地图物品的读数只在系数为 1 时可信。rand(-3, 3) 取正部后加到等级上(@0x581BC1),
每次刷新赌博商人重掷。工具默认按 +0 显示,可在等级里自己加。