# coding=utf-8 import time from ...BlueFoxTools.ModConfig import MOD_NAME from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxClientSystem from ...BlueFoxTools.SystemTools import SystemTools from ...BlueFoxTools.ToolsConfig import * from ...BlueFoxTools.EventFilter import BlueFoxFilterEnum as ef # ---- 循环火焰音效(任务六) ---- # 点燃的壁炉/炉灶/烧烤架/烤炉 在客户端常驻循环播放 fire.fire: # 开播 -> ModBlockEntityTickClientEvent(方块实体 client_tick) 首次收到该方块时; # 若引擎返回错误 id(如 -6 距本地玩家>16格跳过)则按 FIRE_TICK_INTERVAL 节流重试 # 续期 -> 之后每次 tick 刷新时间戳 # 停止 -> 超过 FIRE_TICK_TIMEOUT 秒没收到 tick(方块被破坏/替换/区块卸载)由清理逻辑停掉 # 清理 -> OnScriptTickClient(客户端每帧) 节流驱动, 不用 AddTimer(退出存档会被卸载且无法恢复) # 复位 -> OnLocalPlayerStopLoading(进入存档) 清空上一存档残留记录, 保证重进后能重新开播 FIRE_LOOP_BLOCKS = ("rtdd:b_90", "rtdd:stove_2", "rtdd:barbecuegrill_1", "rtdd:oven") FIRE_SOUND_NAME = "fire.fire" FIRE_SOUND_VOLUME = 0.7 # 音量(多火源同时燃时避免太吵) FIRE_TICK_INTERVAL = 0.4 # 同一方块最快每 0.4 秒刷新一次时间戳(避免每 tick 都写数据) FIRE_TICK_TIMEOUT = 1.5 # 超过该秒数未收到 tick 即判定方块已不在 FIRE_SWEEP_INTERVAL = 1.0 # 清理节流周期(秒), 由 OnScriptTickClient 每帧检查 class MainClient(BlueFoxClientSystem): systemTools = SystemTools.GetTools('MainClient') mcListener = systemTools.ListenForEvent() blueFoxListener = systemTools.GetBlueFoxServerListener() mainServer = systemTools.ListenForEvent( namespace=MOD_NAME, systemName='MainServer' ) blockUseListener = systemTools.FilterListener(eventName='ClientItemUseOnEvent') def __init__(self, *args): super(MainClient, self).__init__(*args) self.systemTools.Binding(self) self.usingCd = False # 循环火焰音效记录: {(dim, x, y, z): {"id": musicId, "time": 最近一次tick时间}} # id 为 None 表示上次开播失败(引擎返回错误 id), 等下一 tick 重试 self.fireLoops = {} # 上次清理时间戳: OnScriptTickClient 每帧触发, 用它把清理节流到 FIRE_SWEEP_INTERVAL 一次 self.fireLastSweep = 0.0 @blueFoxListener def PlaySound(self, args): posDim = args['posDim'] pitch = args['pitch'] if posDim[3] == clientCompFactory.CreateGame(LevelId).GetCurrentDimension(): clientCompFactory.CreateCustomAudio(LevelId).PlayCustomMusic( args['soundName'], posDim[:3], pitch=pitch ) # ======================================================================== # 循环火焰音效(任务六) # ======================================================================== @mcListener def ModBlockEntityTickClientEvent(self, args): """点燃方块(壁炉/炉灶/烧烤架/烤炉)的方块实体每 tick 触发。 首次收到该位置 -> 开启循环播放 fire.fire 并记下 musicId; 若上次开播失败(引擎返回错误 id, 如距本地玩家>16格跳过) -> 按 FIRE_TICK_INTERVAL 节流重试; 之后每次 tick 刷新时间戳, 由 _sweepFireLoops 把超时(方块被破坏/被替换/ 走远卸载)的音效停掉。 """ if args.get("blockName") not in FIRE_LOOP_BLOCKS: return key = (args.get("dimensionId"), args.get("posX"), args.get("posY"), args.get("posZ")) now = time.time() rec = self.fireLoops.get(key) if rec is None: self._startFireLoop(key, now) elif now - rec["time"] >= FIRE_TICK_INTERVAL: if rec["id"] is None: # 上次开播失败(如玩家距离>16格被引擎跳过), 这里重试 self._startFireLoop(key, now) else: rec["time"] = now @mcListener def OnScriptTickClient(self): """客户端每帧事件(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): """在指定方块位置开启循环火焰音效; 失败时记 id=None 等下一 tick 重试""" dim, x, y, z = key musicId = clientCompFactory.CreateCustomAudio(LevelId).PlayCustomMusic( FIRE_SOUND_NAME, (x + 0.5, y + 0.5, z + 0.5), volume=FIRE_SOUND_VOLUME, pitch=1.0, loop=True ) if self._isPlayFailed(musicId): # 开播失败(例如玩家距离>16格被引擎跳过): 不当作已播放, 等玩家走近后自动重试 self.fireLoops[key] = {"id": None, "time": now} return self.fireLoops[key] = {"id": musicId, "time": now} def _sweepFireLoops(self, now): """清理超时未收到 tick 的音效(方块已破坏/被替换/区块卸载)""" audioComp = clientCompFactory.CreateCustomAudio(LevelId) for key in list(self.fireLoops.keys()): rec = self.fireLoops[key] if now - rec["time"] <= FIRE_TICK_TIMEOUT: continue if rec["id"] is not None: audioComp.StopCustomMusicById(rec["id"], 0.5) del self.fireLoops[key] @blockUseListener(ef.ClientEvent.NormalEvent.NotInCd('rtdd:ItemUseOnEvent', 0.2)) def ClientItemUseOnEventNotInCd02(self, args): if args['itemName'] == 'rtdd:recipe_book': clientApi.PushScreen(MOD_NAME, 'CookRecipe', {}) @blueFoxListener def PlayAttackAnim(self, args): clientCompFactory.CreatePlayer(LevelId).Swing()