diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/NaturalData.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/NaturalData.py new file mode 100644 index 0000000..45b5f60 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/NaturalData.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" +================================================================================ +乡野自然(xy_natural)转化规则数据 +================================================================================ +统一管理"乡野自然"(xy_natural) 分类下的转化规则常量。只被 +NaturalService / NaturalTimerService 读取,FeatureDispatcher 通过方块名/物品名 +集合路由到对应服务。 + +【本次实现的功能】 + 1) 果树结果 (计时): 苹果/桃/橘 树叶 -> 果实形态 逐级结果 + 2) 葡萄藤生长 (计时): grapebushes_1 -> grapebushes_2 + 3) 葡萄藤取葡萄 (右键): 成熟葡萄藤 -> 取3葡萄, 变回葡萄藤_1 + 4) 柴火堆叠/取用 (右键): firewood <-> firewood_2 + 5) 柴火分解 (工作台配方): firewood -> 9个木条 + 6) 水稻种植 (手持稻谷右键耕地): farmland -> 水稻刚种下 + 7) 水稻生长 (计时): riceseedlingsb -> c -> d + 8) 水稻收获 (右键): 成熟水稻 -> 取16大米, 变回刚种下 + +【不实现(后续实现)】 + - 果树"树苗成长"机制(树叶对应树苗, 像原版一样长成树): 后续实现, 本次跳过。 + +【计时说明】 + 果树结果/藤蔓生长/水稻生长均 autoStart=True(放置/种下后自动开始), + 用绝对时间戳写方块实体数据, 退出重进继续计时。 +================================================================================ +""" + +# ============================================================================ +# 一、柴火堆叠/取用(右键) +# firewood(柴火_1) + 手持柴火 -> firewood_2(柴火_2 堆放) +# firewood_2 空手右键 -> firewood + 给1个柴火 +# ============================================================================ +FIREWOOD_RULES = [ + {"source": "rtdd:firewood", "item": "rtdd:firewood", + "target": "rtdd:firewood_2", "consume": 1, "tip": "§a堆放柴火"}, + {"source": "rtdd:firewood_2", "item": None, + "target": "rtdd:firewood", "giveItem": "rtdd:firewood", "tip": "§a取走柴火"}, +] + +# ============================================================================ +# 二、葡萄藤取葡萄(右键) +# grapebushes_2(成熟) 空手右键 -> grapebushes_1 + 掉落3个葡萄 +# ============================================================================ +GRAPE_HARVEST = { + "source": "rtdd:grapebushes_2", "item": None, + "target": "rtdd:grapebushes_1", + "dropItem": [{"name": "rtdd:grape", "count": 3}], + "tip": "§a取出了3个葡萄", +} + +# ============================================================================ +# 三、水稻收获(右键) +# riceseedlingsd(成熟) 空手右键 -> riceseedlingsb + 掉落16个大米 +# ============================================================================ +RICE_HARVEST = { + "source": "rtdd:riceseedlingsd", "item": None, + "target": "rtdd:riceseedlingsb", + "dropItem": [{"name": "rtdd:brownrice", "count": 16}], + "tip": "§a收获了16个大米", +} + +# 汇总: 乡野自然右键切换规则 +NATURAL_SWITCHES = FIREWOOD_RULES + [GRAPE_HARVEST, RICE_HARVEST] + +# ============================================================================ +# 四、计时转换 (果树结果 / 葡萄藤生长 / 水稻生长, 均 autoStart 自动生长) +# ============================================================================ +NATURAL_TIMERS = [ + # 苹果树: 树叶 -> 苹果形态1 -> 苹果形态2 + {"source": "rtdd:apple_leaves", "target": "rtdd:apple_fruit_1", + "time": 60, "autoStart": True, "doneTip": "§e苹果树结果了"}, + {"source": "rtdd:apple_fruit_1", "target": "rtdd:apple_fruit_3", + "time": 60, "autoStart": True, "doneTip": "§e苹果熟了"}, + # 桃树: 树叶 -> 桃子一个 -> 桃子两个 + {"source": "rtdd:peach_leaves", "target": "rtdd:b_173", + "time": 60, "autoStart": True, "doneTip": "§e桃树结果了"}, + {"source": "rtdd:b_173", "target": "rtdd:b_176", + "time": 60, "autoStart": True, "doneTip": "§e桃子熟了"}, + # 橘树: 树叶 -> 橘子形态一 -> 形态二 -> 形态三 + {"source": "rtdd:orange_leaves", "target": "rtdd:tangerine_0", + "time": 60, "autoStart": True, "doneTip": "§e橘树结果了"}, + {"source": "rtdd:tangerine_0", "target": "rtdd:tangerine_1", + "time": 60, "autoStart": True}, + {"source": "rtdd:tangerine_1", "target": "rtdd:tangerine_2", + "time": 60, "autoStart": True, "doneTip": "§e橘子熟了"}, + # 葡萄藤: 藤_1 -> 藤_2 + {"source": "rtdd:grapebushes_1", "target": "rtdd:grapebushes_2", + "time": 180, "autoStart": True, "doneTip": "§e葡萄熟了"}, + # 水稻: 刚种下 -> 生长中 -> 成熟 + {"source": "rtdd:riceseedlingsb", "target": "rtdd:riceseedlingsc", + "time": 120, "autoStart": True}, + {"source": "rtdd:riceseedlingsc", "target": "rtdd:riceseedlingsd", + "time": 120, "autoStart": True, "doneTip": "§e水稻成熟了"}, +] + +# ============================================================================ +# 五、水稻种植(手持稻谷右键耕地, 在耕地上方放置水稻刚种下) +# ============================================================================ +RICE_PLANT = { + "item": "rtdd:rice_seeds", # 稻谷(手持) + "surface": "minecraft:farmland", # 必须右键原版耕地 + "target": "rtdd:riceseedlingsb", # 放置水稻刚种下 + "consume": 1, + "tip": "§a种下水稻", +} \ No newline at end of file diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/__init__.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/__init__.py new file mode 100644 index 0000000..7c68785 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/NaturalService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/NaturalService.py new file mode 100644 index 0000000..40eefe6 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/NaturalService.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +""" +================================================================================ +乡野自然·右键转换引擎(服务端) +================================================================================ +处理"乡野自然"(xy_natural) 分类下的右键转换 + 水稻种植: + 柴火堆叠/取用、葡萄藤取葡萄、水稻收获(右键);稻谷右键耕地种植水稻(ItemUseOn)。 + +【与计时转换解耦】 + 果树结果/葡萄藤生长/水稻生长的计时拆到 NaturalTimerService(本模块不碰计时)。 + +【事件监听在哪?】 + 本类【不】监听任何事件! + 所有事件由 FeatureDispatcher 统一监听, 按 blockOwner/itemUseOnOwner 路由到本类。 + +【本类怎么做】 + handleBlockUse ← 入口:分发器转发右键方块事件(柴火/葡萄收获/水稻收获) + handleItemUseOn ← 入口:分发器转发"手持稻谷右键方块表面"(种植水稻) + (通用工具) getCarriedItem / consumeItem / giveItem / replaceBlock / + dropItems / checkCooldown +================================================================================ +""" +import time +import mod.server.extraServerApi as serverApi + +# ---- 网易API基础对象 ---- +ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类 +comp = serverApi.GetEngineCompFactory() # 组件工厂 + +# ---- 继承 BlueFox 框架的系统基类(提供 CreateEngineItemEntity 等方法) ---- +from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem +from ....BlueFoxTools.ToolsConfig import LevelId + +# ---- 只导入乡野自然自己的数据库 ---- +from ..NaturalData import NATURAL_SWITCHES, RICE_PLANT + +# ---- 统一分发器 ---- +from ...service.FeatureDispatcher import FeatureDispatcher + + +class NaturalService(BlueFoxServerSystem): + """ + 乡野自然·右键转换引擎 + """ + + def __init__(self, namespace, systemName): + super(NaturalService, self).__init__(namespace, systemName) + print "===== NaturalService init =====" + + # ---- 构建右键规则字典 (方块, 物品) -> 规则 ---- + self.switchRecipes = {} + self.featureBlocks = set() + for r in NATURAL_SWITCHES: + self.switchRecipes[(r["source"], r.get("item"))] = r + self.featureBlocks.add(r["source"]) + + # ---- 右键冷却表 ---- + self.cooldown = {} + + # ---- 注册到统一分发器 ---- + FeatureDispatcher.register(self) + + # ============================================================================ + # 一、给 FeatureDispatcher 的接口 + # ============================================================================ + + def getFeatureBlocks(self): + """返回本功能管辖的所有右键方块名集合(右键路由用)""" + return self.featureBlocks + + def getItemUseOnRules(self): + """返回"手持物品右键方块表面"的规则: 稻谷(rice_seeds) → 种植水稻""" + return {RICE_PLANT["item"]: True} + + def handleBlockUse(self, args): + """由 FeatureDispatcher 转发的右键方块入口(柴火/葡萄收获/水稻收获)""" + blockName = args.get("blockName") + playerId = args.get("playerId") + x, y, z = args.get("x"), args.get("y"), args.get("z") + pos = (x, y, z) + dim = comp.CreateDimension(playerId).GetEntityDimensionId() + key = (dim, x, y, z) + + itemDict = self.getCarriedItem(playerId) + itemName = itemDict.get("newItemName") if itemDict else None + + recipe = self.findRecipe(blockName, itemDict) + if not recipe: + return False + if not self.checkCooldown(key, 0.5): + return True + self.handleSwitch(recipe, playerId, pos, dim, itemDict, key) + return True + + def handleItemUseOn(self, args): + """由 FeatureDispatcher 转发的"手持物品右键方块表面"入口(稻谷种水稻)""" + itemName = args.get("itemName") + playerId = args.get("playerId") or args.get("entityId") + x, y, z = args.get("x"), args.get("y"), args.get("z") + dim = comp.CreateDimension(playerId).GetEntityDimensionId() + + if itemName != RICE_PLANT["item"]: + return False + + # 被点方块必须是原版耕地 + try: + cur = comp.CreateBlockInfo(LevelId).GetBlockNew((x, y, z), dim) + if not cur or cur.get("name") != RICE_PLANT["surface"]: + return False + except Exception: + return False + + # 目标格 = 耕地上方一格 + targetPos = (x, y + 1, z) + try: + cur = comp.CreateBlockInfo(LevelId).GetBlockNew(targetPos, dim) + if cur and cur.get("name") != "minecraft:air": + return False + except Exception: + return False + + # 连点冷却 + key = (dim, targetPos[0], targetPos[1], targetPos[2]) + if not self.checkCooldown(key, 0.5): + return True + + # 消耗稻谷 + itemDict = self.getCarriedItem(playerId) + if not itemDict: + return False + self.consumeItem(playerId, itemDict, RICE_PLANT.get("consume", 1)) + + # 放置水稻刚种下 + self.replaceBlock(targetPos, dim, RICE_PLANT["target"], playerId) + if RICE_PLANT.get("tip"): + comp.CreateMsg(playerId).NotifyOneMessage(playerId, RICE_PLANT["tip"]) + return True + + # ============================================================================ + # 二、规则匹配(精确 + 空手 fallback) + # ============================================================================ + + def findRecipe(self, blockName, itemDict): + itemName = itemDict.get("newItemName") if itemDict else None + recipe = self.switchRecipes.get((blockName, itemName)) + if recipe: + return recipe + if not itemName or itemName == "minecraft:air": + return self.switchRecipes.get((blockName, None)) + return None + + # ============================================================================ + # 三、右键规则执行 + # ============================================================================ + + def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key): + # 消耗物品 + if recipe.get("item") is not None and recipe.get("consume", 0) > 0: + self.consumeItem(playerId, itemDict, recipe.get("consume", 1)) + # 替换方块 + if recipe.get("target"): + self.replaceBlock(pos, dim, recipe["target"], playerId) + # 掉落物 + if recipe.get("dropItem"): + self.dropItems(dim, pos, recipe["dropItem"]) + # 给背包物品 + if recipe.get("giveItem"): + self.giveItem(playerId, recipe["giveItem"]) + # 提示 + if recipe.get("tip"): + comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["tip"]) + + # ============================================================================ + # 四、工具方法:获取玩家主手物品 + # ============================================================================ + + def getCarriedItem(self, playerId): + itemComp = comp.CreateItem(playerId) + return itemComp.GetPlayerItem( + serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0, True) + + # ============================================================================ + # 五、工具方法:给玩家物品 + # ============================================================================ + + def giveItem(self, playerId, itemName, count=1, aux=0): + try: + itemDict = {"itemName": itemName, "count": count, "auxValue": aux} + comp.CreateItem(playerId).SpawnItemToPlayerInv(itemDict, playerId) + except Exception: + pass + + # ============================================================================ + # 六、工具方法:消耗物品 + # ============================================================================ + + def consumeItem(self, playerId, itemDict, amount=1): + if itemDict is None: + return + count = itemDict.get("count", 0) + if count <= amount: + comp.CreateItem(playerId).SetEntityItem( + serverApi.GetMinecraftEnum().ItemPosType.CARRIED, + {"itemName": "minecraft:air", "count": 0, "auxValue": 0}) + else: + itemDict["count"] = count - amount + comp.CreateItem(playerId).SetEntityItem( + serverApi.GetMinecraftEnum().ItemPosType.CARRIED, + {"itemName": itemDict["newItemName"], + "count": itemDict["count"], + "auxValue": itemDict.get("newAuxValue", 0), + "userData": itemDict.get("userData", {})}) + + # ============================================================================ + # 七、工具方法:替换方块 + # ============================================================================ + + def replaceBlock(self, pos, dim, newBlockName, playerId=None): + creator = playerId if playerId is not None else LevelId + aux = 0 + try: + oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim) + aux = oldBlock.get("aux", 0) + except Exception: + aux = 0 + comp.CreateBlockInfo(creator).SetBlockNew( + pos, blockDict={"name": newBlockName, "aux": aux}, + oldBlockHandling=0, dimensionId=dim) + + # ============================================================================ + # 八、工具方法:以掉落物形式生成物品 + # ============================================================================ + + def dropItems(self, dim, pos, dropList): + for d in dropList: + itemDict = { + "itemName": d["name"], + "count": d.get("count", 1), + "auxValue": d.get("aux", 0), + } + self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2])) + + # ============================================================================ + # 九、工具方法:冷却检查 + # ============================================================================ + + def checkCooldown(self, key, cd=0.5): + now = time.time() + last = self.cooldown.get(key, 0) + if now - last < cd: + return False + self.cooldown[key] = now + return True \ No newline at end of file diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/NaturalTimerService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/NaturalTimerService.py new file mode 100644 index 0000000..c38d0b2 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/NaturalTimerService.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +""" +================================================================================ +乡野自然·计时转换引擎(服务端) +================================================================================ +只处理"乡野自然"(xy_natural) 分类下的【计时自动转换】: + 果树结果(苹果/桃/橘 树叶->果实)、葡萄藤生长、水稻生长。 +全部 autoStart=True:方块放置/种下后自动开始计时(自动生长)。 + +【与右键转换解耦】 + 柴火堆叠/取用、葡萄收获、水稻收获、种植 在 NaturalService。 + 本类只负责计时,由 FeatureDispatcher 按 timerOwner 路由。 + +【计时怎么做到"退出游戏继续计时"】 + 不做倒计时,只记"开始时刻": + - 启动时把 开始时间戳(rtdd_tstart)+总时长(rtdd_tdur) 写进方块实体数据 + - 每帧 tick 判断 当前时间-开始时刻 >= 总时长 -> 到点转换 + - 时间戳是绝对时间(time.time()), 重启/离线真实时间仍在流逝, 剩余时间自然准确 + +【事件监听在哪?】 + 本类【不】监听任何事件! + ServerBlockEntityTickEvent 由 FeatureDispatcher 统一监听, 再按 timerOwner 路由。 +================================================================================ +""" +import time +import mod.server.extraServerApi as serverApi + +# ---- 网易API基础对象 ---- +ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类 +comp = serverApi.GetEngineCompFactory() # 组件工厂 + +# ---- 继承 BlueFox 框架的系统基类 ---- +from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem +from ....BlueFoxTools.ToolsConfig import LevelId + +# ---- 方块实体数据组件(计时持久化) ---- +blockEntityDataComp = comp.CreateBlockEntityData(LevelId) + +# ---- 只导入乡野自然自己的数据库 ---- +from ..NaturalData import NATURAL_TIMERS + +# ---- 统一分发器 ---- +from ...service.FeatureDispatcher import FeatureDispatcher + + +class NaturalTimerService(BlueFoxServerSystem): + """ + 乡野自然·计时转换引擎 + """ + + def __init__(self, namespace, systemName): + super(NaturalTimerService, self).__init__(namespace, systemName) + print "===== NaturalTimerService init =====" + + # ---- 收集计时型规则(source -> 规则) ---- + self.naturalTimers = {t["source"]: t for t in NATURAL_TIMERS} + + # ---- 注册到分发器的计时路由表 ---- + FeatureDispatcher.registerTimer(self) + + # ============================================================================ + # 一、给 FeatureDispatcher 的接口 + # ============================================================================ + + def getTimerBlocks(self): + """返回本功能的所有"计时源方块"集合(tick 路由用)""" + return set(self.naturalTimers.keys()) + + def handleBlockEntityTick(self, args): + """由 FeatureDispatcher 转发的计时方块 tick 入口""" + dim = args.get("dimension") + pos = (args.get("posX"), args.get("posY"), args.get("posZ")) + blockName = args.get("blockName", "") + self._tickProcess(dim, pos, blockName) + + # ============================================================================ + # 二、启动计时(果树/藤蔓/水稻 autoStart, 保留接口) + # ============================================================================ + + def startTimer(self, dim, pos, blockName, playerId=None): + recipe = self.naturalTimers.get(blockName) + if not recipe: + return False + try: + data = blockEntityDataComp.GetBlockEntityData(dim, pos) + if data and data["rtdd_tid"] == blockName: + return False + except Exception: + pass + self._writeTimerData(dim, pos, recipe, playerId) + return True + + # ============================================================================ + # 三、tick 检查与到点转换 + # ============================================================================ + + def _tickProcess(self, dim, pos, blockName): + try: + recipe = self.naturalTimers.get(blockName) + if not recipe: + return + + blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) + if blockData is None: + return + + try: + tid = blockData["rtdd_tid"] + except Exception: + tid = "" + if tid != blockName: + # autoStart=True: 计时源方块无计时数据时自动启动(自动生长) + if recipe.get("autoStart", True): + self._writeTimerData(dim, pos, recipe, None) + return + + tstart = blockData["rtdd_tstart"] + tdur = blockData["rtdd_tdur"] + + if time.time() - tstart >= tdur: + self._finishTimer(dim, pos, recipe, blockData) + except Exception: + pass + + def _finishTimer(self, dim, pos, recipe, data): + try: + cur = comp.CreateBlockInfo(LevelId).GetBlockNew(pos, dim) + if cur and cur.get("name") != recipe["source"]: + self._clearTimerData(dim, pos) + return + self.replaceBlock(pos, dim, recipe["target"], None) + self._clearTimerData(dim, pos) + if recipe.get("doneTip"): + self._notifyIfOnline(data["rtdd_tp"], recipe["doneTip"]) + except Exception: + pass + + # ============================================================================ + # 四、计时数据读写(存在方块实体数据里,重启不丢) + # ============================================================================ + + def _writeTimerData(self, dim, pos, recipe, playerId=None): + try: + blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) + if blockData is None: + return + now = time.time() + blockData["rtdd_tid"] = recipe["source"] + blockData["rtdd_tstart"] = now + blockData["rtdd_tdur"] = recipe.get("time", 60) + blockData["rtdd_tp"] = playerId if playerId else "" + except Exception: + pass + + def _clearTimerData(self, dim, pos): + try: + blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) + if blockData is None: + return + blockData["rtdd_tid"] = "" + blockData["rtdd_tstart"] = 0 + blockData["rtdd_tdur"] = 0 + blockData["rtdd_tp"] = "" + except Exception: + pass + + # ============================================================================ + # 五、工具方法:完成提示 + # ============================================================================ + + def _notifyIfOnline(self, playerId, tip): + if not tip or not playerId: + return + try: + ids = comp.CreateGame(LevelId).GetAllPlayerIds() + if playerId in ids: + comp.CreateMsg(playerId).NotifyOneMessage(playerId, tip) + except Exception: + pass + + # ============================================================================ + # 六、工具方法:替换方块 + # ============================================================================ + + def replaceBlock(self, pos, dim, newBlockName, playerId=None): + creator = playerId if playerId is not None else LevelId + aux = 0 + try: + oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim) + aux = oldBlock.get("aux", 0) + except Exception: + aux = 0 + comp.CreateBlockInfo(creator).SetBlockNew( + pos, blockDict={"name": newBlockName, "aux": aux}, + oldBlockHandling=0, dimensionId=dim) \ No newline at end of file diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/__init__.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/__init__.py new file mode 100644 index 0000000..7c68785 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/ziran/service/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/behavior_pack_v8jz0zSR/netease_recipes/xy_firewood_plank.json b/behavior_pack_v8jz0zSR/netease_recipes/xy_firewood_plank.json new file mode 100644 index 0000000..0ba2604 --- /dev/null +++ b/behavior_pack_v8jz0zSR/netease_recipes/xy_firewood_plank.json @@ -0,0 +1,22 @@ +{ + "format_version": "1.12", + "minecraft:recipe_shapeless": { + "ingredients": [ + { + "item": "rtdd:firewood", + "data": 0 + } + ], + "description": { + "identifier": "rtdd:xy_firewood_plank" + }, + "result": { + "data": 0, + "item": "rtdd:b_15", + "count": 9 + }, + "tags": [ + "crafting_table" + ] + } +} \ No newline at end of file