结论摘要
当前服务端的普通角色摆摊规则主要由 configs/Stall_Info.txt 生效控制:
- 摆摊场景:按
Stall_Info.txt 的 摆摊场景 字段匹配当前 scene id。
- 摆摊坐标:按
摊位定位点X_1/X_2/Z_1/Z_2 组成的矩形区域判断。
- 摊位类型:
摊位类型(0:money 1:yuanbao) 决定成交货币是金币还是元宝。
- 摊位费:
摊位费 在正式开摊时一次性从金币 money 扣除。
- 交易税率:
交易税率% 在成交时从卖家到账金额里扣除。
configs/scene/*.scn 里的 stallinfodata=xxx_StallInfo.stall 当前只被识别到入口,但没有真正读取对应 .stall 文件内容。代码里存在 lualib/stallinforeader.lua,但当前场景初始化没有调用它。因此在现有代码路径下,.stall 文件基本属于历史遗留/半废弃数据,不参与普通摆摊坐标、税率、地图限制的最终判断。
silindao.scn 当前没有 stallinfodata= 配置项,configs/scene/ 下也没有 silindao_StallInfo.stall。所以私林岛/会所这张图不是“配置了 .stall 但没加载”,而是场景配置本身没有挂摆摊格子文件。
配置数据来源
Stall_Info.txt
路径:
configs/Stall_Info.txt
读取函数:
lualib/cfghelper/shenbing_dfeng_tail.lua:114
读取逻辑:
function cfghelper:read_stall_info()
local transferd = {}
local read = require "txtreader".new()
local configs = read:load("Stall_Info.txt")
for _, conf in pairs(configs) do
local id = conf["序号"]
if id then
local t = {}
id = conf["摆摊场景"]
t.ntype = conf["摊位类型(0:money 1:yuanbao)"]
t.pos_tax = conf["摊位费"]
t.trade_tax = conf["交易税率%"]
t.pos_left = conf["摊位定位点X_1"]
t.pos_right = conf["摊位定位点X_2"]
t.pos_top = conf["摊位定位点Z_1"]
t.pos_bottom = conf["摊位定位点Z_2"]
if transferd[id] then
table.insert(transferd[id], t)
else
transferd[id] = { t }
end
end
end
self.stall_info = transferd
end
它把配置转换成:
stall_info[scene_id] = {
{
ntype = 0 or 1,
pos_tax = 摊位费,
trade_tax = 交易税率,
pos_left = X1,
pos_right = X2,
pos_top = Z1,
pos_bottom = Z2,
},
...
}
因此同一个场景可以配置多个摆摊矩形区。例如 摆摊场景 = 0 可以出现多行,表示同一张地图多个摆摊范围。
ConfigInfo.ini 的 PlayerShop
路径:
configs/ConfigInfo.ini
其中:
[PlayerShop]
MaxCount=256
PaymentPerHour=0
这块属于固定玩家商店系统 PlayerShop,不是普通角色原地摆摊的坐标/税率来源。普通摆摊走的是 scenecore/stall.lua,不要和 scenecore/player_shop.lua 混在一起判断。
普通摆摊协议入口
普通摆摊相关包定义在:
services/game/packet_parts/social_trade/stall_bbs.lua
关键包:
CGStallApply:申请摆摊,客户端到服务端,包体为空。
GCStallApply:服务端返回是否允许摆摊、摊位费、交易税率、是否元宝摊位。
CGStallEstablish:正式开摊,客户端到服务端,包体为空。
GCStallEstablish:正式开摊成功,包体为空。
CGStallBuy/GCStallBuy:摆摊购买,带物品/珍兽 GUID、serial、成交金额等结果字段。
请求入口在:
services/msgagent.lua:4371
services/msgagent.lua:4395
services/msgagent.lua:4444
调用链:
CGStallApply
-> msgagent.request:CGStallApply()
-> scene.char_stall()
CGStallEstablish
-> msgagent.request:CGStallEstablish()
-> scene.char_stall_establish()
CGStallBuy
-> msgagent.request:CGStallBuy()
-> scene.char_stall_buy()
CGStallApply 会检查:
- 小密码是否解锁。
- 账号是否交易限制。
- 当前场景类型是否为普通场景
scene_type == 0。
CGStallEstablish 会额外检查角色数据版本号,避免数据保存版本异常时开摊。
申请摆摊时如何解析地图、坐标、税率
核心函数:
services/scene/scenecore/stall.lua:86
核心逻辑:
local sceneId = self:get_id()
local stall_info = configenginer:get_config("stall_info")
stall_info = stall_info[sceneId]
if not stall_info then
me:notify_tips("#{GCStallApplyHandler_Info_Stall_Err}")
return
end
local pos = me:get_world_pos()
local me_posx = pos.x
local me_posz = pos.y
for _, info in ipairs(stall_info) do
if me_posx >= info.pos_left
and me_posx <= info.pos_right
and me_posz >= info.pos_top
and me_posz <= info.pos_bottom
then
pos_tax = info.pos_tax
trade_tax = info.trade_tax
is_yuanbao_stall = info.ntype == 1
break
end
end
判断顺序:
- 用当前场景 id 查
stall_info[sceneId]。
- 如果当前场景在
Stall_Info.txt 没有任何配置,直接失败。
- 取玩家当前世界坐标
x 和 y,这里代码把 pos.y 当作地图 Z 坐标使用。
- 遍历该场景下所有矩形区域。
- 命中第一个矩形后取得摊位费、交易税率、摊位类型。
- 如果没有命中任何矩形,返回摆摊错误。
申请成功后返回:
msg.is_can_stall = can_stall and 1 or 0
msg.pos_tax = pos_tax
msg.trade_tax = trade_tax
msg.is_yuanbao_stall = is_yuanbao_stall and 1 or 0
正式开摊时如何扣摊位费
核心函数:
services/scene/scenecore/stall.lua:158
正式开摊会重新计算一次场景和坐标,不信任申请阶段的客户端状态:
local sceneId = self:get_id()
local stall_info = configenginer:get_config("stall_info")
stall_info = stall_info[sceneId]
...
for _, info in ipairs(stall_info) do
if me_posx >= info.pos_left
and me_posx <= info.pos_right
and me_posz >= info.pos_top
and me_posz <= info.pos_bottom
then
pos_tax = info.pos_tax
trade_tax = info.trade_tax
is_yuanbao_stall = info.ntype == 1
break
end
end
正式开摊前要求状态必须是 STALL_READY:
if stall_box:get_stall_status() ~= stall_box.STALL_STATUS.STALL_READY then
msg.result = msg.STALL_MSG.ERR_ILLEGAL
self:send2client(me, msg)
stall_box:clean_up()
return
end
扣摊位费:
if me:get_money() < pos_tax then
msg.result = msg.STALL_MSG.ERR_NOT_ENOUGH_MONEY_TO_OPEN
self:send2client(me, msg)
return
end
me:set_money(me:get_money() - pos_tax, "摆摊交易-税费")
注意:这里无论 ntype 是金币摊还是元宝摊,开摊费都扣 money。ntype == 1 只被保存到 stall_box:set_is_yuanbao_stall(is_yuanbao_stall),后续交易才决定买卖货币。
开摊成功后写入摊位状态:
stall_box:set_stall_status(stall_box.STALL_STATUS.STALL_OPEN)
stall_box:set_stall_is_open(true)
stall_box:set_pos_tax(pos_tax)
stall_box:set_trade_tax(trade_tax)
stall_box:set_is_yuanbao_stall(is_yuanbao_stall)
并占用当前位置:
local world_pos = me:get_world_pos()
self:set_pos_can_stall(world_pos.x, world_pos.y, false)
stall_box:set_stall_pos(world_pos.x, world_pos.y)
me:get_ai():change_state("stall")
成交时如何使用摊位类型和交易税
核心函数:
services/scene/scenecore/stall.lua:551
物品和珍兽购买逻辑基本一致。
先从摊位取价格:
local need_money = stall_box:get_price_by_index(index)
按摊位类型选择买家余额:
if stall_box:get_is_yuanbao_stall() then
my_money = obj:get_yuanbao()
else
my_money = obj:get_money()
end
成交后按摊位类型扣买家货币:
if stall_box:get_is_yuanbao_stall() then
obj:set_yuanbao(obj:get_yuanbao() - need_money, "元宝摊位-购买消费", ...)
else
obj:set_money(obj:get_money() - need_money, "金币摊位-购买消费", ...)
end
卖家到账金额按交易税率计算:
local trade_tax = stall_box:get_trade_tax()
trade_tax = trade_tax > 100 and 100 or trade_tax
local profit = need_money * (1 - trade_tax / 100)
profit = math.floor(profit)
再按摊位类型给卖家加货币:
if stall_box:get_is_yuanbao_stall() then
stall_human:set_yuanbao(stall_human:get_yuanbao() + profit, "元宝摊位-卖出道具", ...)
else
stall_human:set_money(stall_human:get_money() + profit, "金币摊位-卖出道具", ...)
end
因此税率实际效果是:
卖家到账 = floor(售价 * (1 - 交易税率 / 100))
系统税收 = 售价 - 卖家到账
地图限制的实际规则
普通摆摊能否开始,至少需要同时满足:
- 当前场景类型是普通场景,
scene_type == 0。
- 当前场景 id 在
Stall_Info.txt 中存在配置。
- 当前坐标落在该场景某条配置的矩形范围内。
- 角色等级大于等于 30。
- 不在双人骑乘等禁止状态。
- 账号没有交易限制。
- 小密码已解锁。
- 道具限制交易检查通过。
- 申请摆摊后正式开摊时,状态仍为
STALL_READY。
- 金币足够支付
pos_tax。
其中第 2、3 点是地图和坐标限制的核心。
.stall 文件当前是否作废
代码中确实存在 .stall 文件读取器:
lualib/stallinforeader.lua
设计上的二进制格式:
stall_info.ver = is:readint()
stall_info.width = is:readint()
stall_info.height = is:readint()
for y = 1, stall_info.height do
for x = 1, stall_info.width do
inf.can_stall = is:readuchar() == 1
inf.trade_tax = is:readuchar()
inf.pos_tax = is:readint()
inf.ext = is:readuchar()
stall_info.map[x][y] = inf
end
end
如果这条链路完整生效,它应该提供逐格的:
- 是否可摆摊
can_stall
- 交易税率
trade_tax
- 摊位费
pos_tax
- 扩展字段
ext
但当前场景初始化不是这样写的。
场景 .scn 解析到 stallinfodata 后,调用:
services/scene/scenecore.lua:417
if self.scn.System.stallinfodata then
self:init_stall_info(self.scn.System.stallinfodata)
end
而实际初始化函数:
services/scene/scenecore/scene_state.lua:172
function scenecore:init_stall_info(stallinfofile)
self.stallinfo = { map = {} }
end
这里没有使用传入的 stallinfofile,也没有:
require "stallinforeader"
更没有:
stallinforeader:load(stallinfofile)
所以结论是:
当前普通摆摊运行链路下,*.stall 文件没有被加载。
它不是文件不存在意义上的废弃,而是代码路径中保留了接口和读取器,但当前生效初始化把它绕过去了。实际效果等同于废弃。
为什么有些 .scn 仍然写 stallinfodata
例如:
configs/scene/luoyang.scn
有:
stallinfodata=luoyang_StallInfo.stall
这会触发:
self:init_stall_info("luoyang_StallInfo.stall")
但因为 init_stall_info 只做:
self.stallinfo = { map = {} }
所以文件名只起到了“让该场景拥有 self.stallinfo 空表”的效果,没有读取文件内容。
随后 char_stall 中有一段兜底:
local m = self.stallinfo.map
local world_pos = me:get_world_pos()
local x = math.ceil(world_pos.x)
local y = math.ceil(world_pos.y)
m[x] = m[x] or {}
m[x][y] = m[x][y] or { can_stall = true, trade_tax = trade_tax, pos_tax = pos_tax }
local d = m[x][y]
if d.can_stall then
can_stall = true
trade_tax = d.trade_tax
pos_tax = d.pos_tax
end
由于 self.stallinfo.map 是空表,第一次申请某坐标时会自动补:
can_stall = true
trade_tax = Stall_Info.txt 命中的 trade_tax
pos_tax = Stall_Info.txt 命中的 pos_tax
也就是说,当前真正限制坐标的是 Stall_Info.txt 的矩形。.stall 逐格限制没有生效。
silindao.scn / 私林岛 / 会所的情况
当前文件:
configs/scene/silindao.scn
内容:
[System]
navmapname=silindao.nav
monsterfile=silindao_monster.ini
patrolpoint=silindao_patrolpoint.ini
growpointdata=silindao_growpoint.txt
growpointsetup=silindao_growpointsetup.txt
eventfile=silindao_area.ini
没有:
stallinfodata=...
同时 configs/scene/ 下也没有发现:
silindao_StallInfo.stall
所以 silindao.scn 当前不会调用 init_stall_info,场景对象上不会因为 .scn 配置而初始化 self.stallinfo。
再结合普通摆摊申请逻辑:
local stall_info = configenginer:get_config("stall_info")
stall_info = stall_info[sceneId]
if not stall_info then
me:notify_tips("#{GCStallApplyHandler_Info_Stall_Err}")
return
end
如果 Stall_Info.txt 中没有私林岛对应 scene id,那么私林岛无法摆摊。
如果 Stall_Info.txt 中有私林岛对应 scene id,但 silindao.scn 没有 stallinfodata,当前代码在命中矩形后仍然会进入:
if self.stallinfo then
...
stall_box:set_stall_status(STALL_READY)
end
由于 self.stallinfo 不存在,can_stall 不会被置为 true,STALL_READY 也不会设置。申请返回会带 is_can_stall = 0,正式开摊时也会因为状态不是 STALL_READY 报非法。
因此私林岛/会所想允许普通摆摊,当前至少需要:
Stall_Info.txt 增加对应 scene id 的矩形区域。
silindao.scn 增加任意 stallinfodata=...,让 self.stallinfo 被初始化。
- 如果保持当前代码不改,
.stall 文件内容本身仍不会被读取;文件是否真实存在,对当前 init_stall_info 不产生影响。
如果要恢复 .stall 逐格限制,则需要修改 init_stall_info,真正调用 stallinforeader 并处理文件缺失、旧数据兼容和税率/摊位费来源优先级。
风险点
1. .scn 有 stallinfodata 但文件内容不生效
运维或策划可能以为修改 xxx_StallInfo.stall 会改变摆摊格子、摊位费、税率,但当前服务端不会读取这个文件。
2. Stall_Info.txt 是真正权威
当前实际权威是 Stall_Info.txt。如果只改 .stall,线上行为不会变。
3. stallinfodata 目前仍影响是否有 self.stallinfo
虽然 .stall 内容不生效,但 .scn 里有没有 stallinfodata 会影响 self.stallinfo 是否初始化。没有 self.stallinfo 时,即使 Stall_Info.txt 命中矩形,也无法把角色置为 STALL_READY。
所以不能简单说 .scn 的 stallinfodata 完全没用。更精确的说法是:
stallinfodata 这个配置项仍影响摆摊流程开关;
stallinfodata 指向的 *.stall 文件内容当前不生效。
4. 元宝摊位的开摊费仍扣金币
ntype == 1 只控制成交货币。正式开摊时 pos_tax 统一扣 money。如果设计期望元宝摊位开摊也扣元宝,需要另行调整。
排查或修改建议
只想调整某地图能否摆摊
优先查:
configs/Stall_Info.txt
configs/scene/目标地图.scn
确认:
Stall_Info.txt 是否有对应 scene id。
- 玩家坐标是否落入矩形范围。
.scn 是否存在 stallinfodata=,用来初始化 self.stallinfo。
只想调整税率或摊位费
改:
configs/Stall_Info.txt
当前不要改 .stall,因为不生效。