火焰声音循环

This commit is contained in:
wenhe committed 2026-09-30 17:01:25 +08:00
1 parent 0eead82924
commit e328d36b46
3 files changed
+77 -35

No files matched your search

@@ -8,15 +8,18 @@ from ...BlueFoxTools.EventFilter import BlueFoxFilterEnum as ef
# ---- 循环火焰音效(任务六) ---- # ---- 循环火焰音效(任务六) ----
# 点燃的壁炉/炉灶/烧烤架/烤炉 在客户端常驻循环播放 fire.fire: # 点燃的壁炉/炉灶/烧烤架/烤炉 在客户端常驻循环播放 fire.fire:
# 开播 -> ModBlockEntityTickClientEvent(方块实体 client_tick) 首次收到该方块时 # 开播 -> ModBlockEntityTickClientEvent(方块实体 client_tick) 首次收到该方块时;
# 若引擎返回错误 id(如 -6 距本地玩家>16格跳过)则按 FIRE_TICK_INTERVAL 节流重试
# 续期 -> 之后每次 tick 刷新时间戳 # 续期 -> 之后每次 tick 刷新时间戳
# 停止 -> 超过 FIRE_TICK_TIMEOUT 秒没收到 tick(方块被破坏/替换/区块卸载)由清理定时器停掉 # 停止 -> 超过 FIRE_TICK_TIMEOUT 秒没收到 tick(方块被破坏/替换/区块卸载)由清理逻辑停掉
# 清理 -> OnScriptTickClient(客户端每帧) 节流驱动, 不用 AddTimer(退出存档会被卸载且无法恢复)
# 复位 -> OnLocalPlayerStopLoading(进入存档) 清空上一存档残留记录, 保证重进后能重新开播
FIRE_LOOP_BLOCKS = ("rtdd:b_90", "rtdd:stove_2", "rtdd:barbecuegrill_1", "rtdd:oven") FIRE_LOOP_BLOCKS = ("rtdd:b_90", "rtdd:stove_2", "rtdd:barbecuegrill_1", "rtdd:oven")
FIRE_SOUND_NAME = "fire.fire" FIRE_SOUND_NAME = "fire.fire"
FIRE_SOUND_VOLUME = 0.7 # 音量(多火源同时燃时避免太吵) FIRE_SOUND_VOLUME = 0.7 # 音量(多火源同时燃时避免太吵)
FIRE_TICK_INTERVAL = 0.4 # 同一方块最快每 0.4 秒刷新一次时间戳(避免每 tick 都写数据) FIRE_TICK_INTERVAL = 0.4 # 同一方块最快每 0.4 秒刷新一次时间戳(避免每 tick 都写数据)
FIRE_TICK_TIMEOUT = 1.5 # 超过该秒数未收到 tick 即判定方块已不在 FIRE_TICK_TIMEOUT = 1.5 # 超过该秒数未收到 tick 即判定方块已不在
FIRE_SWEEP_INTERVAL = 1.0 # 清理定时器周期(秒) FIRE_SWEEP_INTERVAL = 1.0 # 清理节流周期(秒), 由 OnScriptTickClient 每帧检查
class MainClient(BlueFoxClientSystem): class MainClient(BlueFoxClientSystem):
@@ -33,8 +36,10 @@ class MainClient(BlueFoxClientSystem):
self.systemTools.Binding(self) self.systemTools.Binding(self)
self.usingCd = False self.usingCd = False
# 循环火焰音效记录: {(dim, x, y, z): {"id": musicId, "time": 最近一次tick时间}} # 循环火焰音效记录: {(dim, x, y, z): {"id": musicId, "time": 最近一次tick时间}}
# id 为 None 表示上次开播失败(引擎返回错误 id), 等下一 tick 重试
self.fireLoops = {} self.fireLoops = {}
self.fireSweeping = False # 上次清理时间戳: OnScriptTickClient 每帧触发, 用它把清理节流到 FIRE_SWEEP_INTERVAL 一次
self.fireLastSweep = 0.0
@blueFoxListener @blueFoxListener
def PlaySound(self, args): def PlaySound(self, args):
@@ -53,9 +58,10 @@ class MainClient(BlueFoxClientSystem):
def ModBlockEntityTickClientEvent(self, args): def ModBlockEntityTickClientEvent(self, args):
"""点燃方块(壁炉/炉灶/烧烤架/烤炉)的方块实体每 tick 触发。 """点燃方块(壁炉/炉灶/烧烤架/烤炉)的方块实体每 tick 触发。
首次收到 -> 在该方块位置开启循环播放 fire.fire 并记下 musicId; 首次收到该位置 -> 开启循环播放 fire.fire 并记下 musicId;
之后每次 tick 刷新时间戳, 由 _sweepFireLoops 把超时(方块被破坏/ 若上次开播失败(引擎返回错误 id, 如距本地玩家>16格跳过) -> 按 FIRE_TICK_INTERVAL 节流重试;
被替换/走远卸载)的音效停掉。 之后每次 tick 刷新时间戳, 由 _sweepFireLoops 把超时(方块被破坏/被替换/
走远卸载)的音效停掉。
""" """
if args.get("blockName") not in FIRE_LOOP_BLOCKS: if args.get("blockName") not in FIRE_LOOP_BLOCKS:
return return
@@ -65,45 +71,81 @@ class MainClient(BlueFoxClientSystem):
if rec is None: if rec is None:
self._startFireLoop(key, now) self._startFireLoop(key, now)
elif now - rec["time"] >= FIRE_TICK_INTERVAL: elif now - rec["time"] >= FIRE_TICK_INTERVAL:
if rec["id"] is None:
# 上次开播失败(如玩家距离>16格被引擎跳过), 这里重试
self._startFireLoop(key, now)
else:
rec["time"] = now rec["time"] = now
@mcListener
def OnScriptTickClient(self, args):
"""客户端每帧事件(1秒约30次): 节流驱动火焰音效清理。
不用 AddTimer 递归 —— 退出存档时 AddTimer 会被卸载且无法恢复; 本事件只在
实际处于存档时触发, 跨存档天然可靠。
"""
if not self.fireLoops:
return
now = time.time()
if now - self.fireLastSweep < FIRE_SWEEP_INTERVAL:
return
self.fireLastSweep = now
self._sweepFireLoops(now)
@mcListener
def OnLocalPlayerStopLoading(self, args):
"""玩家进入存档(出生点地形加载完成)时触发: 复位循环火焰音效状态。
退出存档时音效会被引擎停掉、AddTimer 会被卸载, 但 self.fireLoops 里旧的
musicId 仍在, 导致重进后 tick 命中旧记录(rec 非空)而永不重播。这里统一停掉
残留、清空记录并复位清理节流, 让 tick 重新走"首次开播"分支。
"""
self._resetFireLoops()
def _resetFireLoops(self):
"""停掉并清空上一存档残留的循环火焰音效记录"""
if not self.fireLoops:
return
audioComp = clientCompFactory.CreateCustomAudio(LevelId)
for rec in self.fireLoops.values():
if rec["id"] is not None:
audioComp.StopCustomMusicById(rec["id"], 0)
self.fireLoops.clear()
self.fireLastSweep = 0.0
@staticmethod
def _isPlayFailed(musicId):
"""PlayCustomMusic 失败会返回负数错误 id(-1~-6, 其中 -6=距本地玩家大于16格跳过)。
这些返回值是真值, 不能当成播放成功, 需单独识别。"""
if isinstance(musicId, basestring):
return musicId.startswith('-')
if isinstance(musicId, int):
return musicId < 0
return not musicId
def _startFireLoop(self, key, now): def _startFireLoop(self, key, now):
"""在指定方块位置开启循环火焰音效, 并确保清理定时器已启动""" """在指定方块位置开启循环火焰音效; 失败时记 id=None 等下一 tick 重试"""
dim, x, y, z = key dim, x, y, z = key
musicId = clientCompFactory.CreateCustomAudio(LevelId).PlayCustomMusic( musicId = clientCompFactory.CreateCustomAudio(LevelId).PlayCustomMusic(
FIRE_SOUND_NAME, (x + 0.5, y + 0.5, z + 0.5), FIRE_SOUND_NAME, (x + 0.5, y + 0.5, z + 0.5),
volume=FIRE_SOUND_VOLUME, pitch=1.0, loop=True volume=FIRE_SOUND_VOLUME, pitch=1.0, loop=True
) )
if not musicId: if self._isPlayFailed(musicId):
# 开播失败(例如玩家距离>16格被引擎跳过): 不当作已播放, 等玩家走近后自动重试
self.fireLoops[key] = {"id": None, "time": now}
return return
self.fireLoops[key] = {"id": musicId, "time": now} self.fireLoops[key] = {"id": musicId, "time": now}
self._ensureFireSweeper()
def _ensureFireSweeper(self): def _sweepFireLoops(self, now):
"""启动音效清理定时器(首个音效开启时启动, 队列清空后自动停)"""
if self.fireSweeping:
return
self.fireSweeping = True
self._addFireSweepTimer()
def _addFireSweepTimer(self):
"""挂一次清理定时器(客户端 AddTimer 递归实现轮询)"""
clientCompFactory.CreateGame(LevelId).AddTimer(FIRE_SWEEP_INTERVAL, self._sweepFireLoops)
def _sweepFireLoops(self):
"""清理超时未收到 tick 的音效(方块已破坏/被替换/区块卸载)""" """清理超时未收到 tick 的音效(方块已破坏/被替换/区块卸载)"""
now = time.time()
audioComp = clientCompFactory.CreateCustomAudio(LevelId) audioComp = clientCompFactory.CreateCustomAudio(LevelId)
for key in list(self.fireLoops.keys()): for key in list(self.fireLoops.keys()):
rec = self.fireLoops[key] rec = self.fireLoops[key]
if now - rec["time"] <= FIRE_TICK_TIMEOUT: if now - rec["time"] <= FIRE_TICK_TIMEOUT:
continue continue
if rec["id"] is not None:
audioComp.StopCustomMusicById(rec["id"], 0.5) audioComp.StopCustomMusicById(rec["id"], 0.5)
del self.fireLoops[key] del self.fireLoops[key]
if self.fireLoops:
self._addFireSweepTimer()
else:
self.fireSweeping = False
@blockUseListener(ef.ClientEvent.NormalEvent.NotInCd('rtdd:ItemUseOnEvent', 0.2)) @blockUseListener(ef.ClientEvent.NormalEvent.NotInCd('rtdd:ItemUseOnEvent', 0.2))
def ClientItemUseOnEventNotInCd02(self, args): def ClientItemUseOnEventNotInCd02(self, args):
@@ -11,7 +11,7 @@
"collision": { "collision": {
"min": [ "min": [
0, 0,
-0.2, -0.25,
0.1875 0.1875
], ],
"max": [ "max": [
@@ -23,7 +23,7 @@
"clip": { "clip": {
"min": [ "min": [
0, 0,
-0.2, -0.25,
0.1875 0.1875
], ],
"max": [ "max": [
@@ -12,24 +12,24 @@
"min": [ "min": [
0.0, 0.0,
0.0, 0.0,
-0.375 0
], ],
"max": [ "max": [
1.1243, 1,
1.0, 1.0,
1.125 1
] ]
}, },
"clip": { "clip": {
"min": [ "min": [
0.0, 0.0,
0.0, 0.0,
-0.375 0
], ],
"max": [ "max": [
1.1243, 1,
1.0, 1.0,
1.125 1
] ]
} }
}, },