From 09562b4409f37a977e5123475a1ff8f5e107856f Mon Sep 17 00:00:00 2001 From: Wuxie233 <121599346+Wuxie233@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:55:59 +0800 Subject: [PATCH 1/2] Update __init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 断点续导支持 - 新增 continue/ 命令 - 新增 区块加载检测 --- .../__init__.py" | 140 +++++++++++++++++- 1 file changed, 134 insertions(+), 6 deletions(-) diff --git "a/\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/__init__.py" "b/\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/__init__.py" index 8ed70bdd..3016c5e0 100644 --- "a/\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/__init__.py" +++ "b/\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/__init__.py" @@ -12,6 +12,7 @@ plugin_entry, ) from tooldelta.utils.tooldelta_thread import ToolDeltaThread +from .task_store import TaskStore class SimpleWorldImport(Plugin): @@ -24,6 +25,9 @@ class SimpleWorldImport(Plugin): flowers_for_machines: "FlowersForMachine | None" _chest_cache_requester: str + _task_store: TaskStore + _resume_task: dict | None + _current_task_id: str def __init__(self, frame: Frame): self.frame = frame @@ -39,6 +43,9 @@ def __init__(self, frame: Frame): self.ListenActive(self.on_inject) self.ListenFrameExit(self.on_close) self.make_data_path() + self._task_store = TaskStore(self.format_data_path("任务数据.json")) + self._resume_task = None + self._current_task_id = "" def on_def(self): global bwo, nbtlib, FlowersForMachine @@ -71,6 +78,12 @@ def on_inject(self): "导入存档内的建筑物", self.runner, ) + self.frame.add_console_cmd_trigger( + ["continue/"], + "[任务编号]", + "续导未完成的导入任务", + self.runner_continue, + ) def on_close(self, _: FrameExit): self.should_close = True @@ -254,16 +267,52 @@ def _do_world_import(self, cmd: list[str]): (start_pos[0], start_pos[2]), (end_pos[0], end_pos[2]), ) - - progress = 0 - for origin_chunk_pos in bot_path: + # 续导/新任务 初始化 + total_chunks = len(bot_path) + start_index = 0 + current_task_id = "" + resume_task = self._resume_task + has_task = True + if resume_task is not None: + try: + start_index = int(resume_task.get("progress_index", 0)) + current_task_id = str(resume_task.get("task_id", "")) + except Exception: + start_index = 0 + current_task_id = "" + else: + # 普通任务:创建任务 + try: + new_task = { + "task_id": "", + "world_name": filename, + "dimension": int(cmd[1]), + "source_start": [start_pos[0], start_pos[1], start_pos[2]], + "source_end": [end_pos[0], end_pos[1], end_pos[2]], + "dest_start": [result_start_pos[0], result_start_pos[1], result_start_pos[2]], + "progress_index": 0, + "total_chunks": total_chunks, + } + current_task_id = self._task_store.add_task(new_task) + except Exception: + current_task_id = "" + if current_task_id == "": + has_task = False + # 续导状态使用一次后即清空 + self._resume_task = None + + progress = start_index + for idx, origin_chunk_pos in enumerate(bot_path): + # 跳过已完成的区块(续导) + if idx < start_index: + continue # 检查用户是否重载 if self.should_close: world.close_world() return # 显示处理进度 - finish_ratio = round(progress / (len(bot_path)) * 100) + finish_ratio = round(progress / (total_chunks) * 100) fmts.print_inf(f"正在处理 {origin_chunk_pos} 处的区块 ({finish_ratio}%)") progress += 1 @@ -272,6 +321,13 @@ def _do_world_import(self, cmd: list[str]): chunk_range = chunk.range() if not chunk.is_valid(): fmts.print_war(f"位于 {origin_chunk_pos} 的区块没有找到, 跳过") + # 完成一个区块后更新任务进度(即便区块不存在也视为处理过) + if has_task: + next_index = idx + 1 + if next_index < total_chunks: + self._task_store.update_progress(current_task_id, next_index) + else: + self._task_store.remove_task(current_task_id) continue # (sub_x, sub_z) 相对于当前正在处理的区 @@ -306,7 +362,31 @@ def _do_world_import(self, cmd: list[str]): ) # 发送指令等待返回, 让机器人确保在进行下一步前租赁服已经将机器人传送到目标地点 # 对于租赁服较为卡顿的时候, 它的作用尤为明显 - self.game_ctrl.sendwscmd_with_resp("testforblock ~ ~ ~ air") + attempts = 0 + while attempts < 30: + try: + resp = self.game_ctrl.sendwscmd_with_resp( + f'execute as @a[name="{self.game_ctrl.bot_name}"] at @s run tp ~ ~ ~ true', + timeout=3, + ) + try: + resp_text = str(resp.as_dict) + except Exception: + resp_text = str(resp) + if "无法将" in resp_text: + fmts.print_inf("等待区块加载") + time.sleep(1) + attempts += 1 + continue + break + except Exception: + fmts.print_inf("等待区块加载") + attempts += 1 + continue + else: + fmts.print_err("检测区块超时过久") + world.close_world() + return except Exception: pass @@ -357,7 +437,7 @@ def _do_world_import(self, cmd: list[str]): ) continue - # 这个子区块是空的(全是空气), + # 这个子区块是空的(全是空气), # 所以完全安全地跳过 if sub_chunk.empty(): continue @@ -510,6 +590,14 @@ def _do_world_import(self, cmd: list[str]): # 0.001 = 1/1000 time.sleep(0.001) + # 当前区块的子区块导入任务全部执行完毕,更新任务进度 + if has_task: + next_index = idx + 1 + if next_index < total_chunks: + self._task_store.update_progress(current_task_id, next_index) + else: + self._task_store.remove_task(current_task_id) + # 一定要记得关掉打开的存档哟, # 千万别忘了 world.close_world() @@ -518,5 +606,45 @@ def _do_world_import(self, cmd: list[str]): def runner(self, cmd: list[str]): self.do_world_import(cmd) + def runner_continue(self, cmd: list[str]): + unfinished = self._task_store.list_unfinished() + if len(unfinished) == 0: + fmts.print_inf("无未完成任务") + return + if len(cmd) == 0: + for i, t in enumerate(unfinished): + try: + ss = t.get("source_start", [0, 0, 0]) + se = t.get("source_end", [0, 0, 0]) + ds = t.get("dest_start", [0, 0, 0]) + tc = int(t.get("total_chunks", 0)) + pi = int(t.get("progress_index", 0)) + fmts.print_inf( + f"[{i}] 地图={t.get('world_name','')} 维度={t.get('dimension',0)} 起点=({ss[0]},{ss[1]},{ss[2]}) 终点=({se[0]},{se[1]},{se[2]}) 目标=({ds[0]},{ds[1]},{ds[2]}) 进度={pi}/{tc}" + ) + except Exception: + continue + fmts.print_inf('请输入 "continue/ [编号]" 开始续导') + return + try: + selection = int(cmd[0]) + except Exception: + fmts.print_war("非法数字,已取消执行") + return + if selection < 0 or selection >= len(unfinished): + fmts.print_war("非法数字,已取消执行") + return + task = unfinished[selection] + self._resume_task = task + filename = str(task.get("world_name", "")) + dm = str(task.get("dimension", 0)) + ss = task.get("source_start", [0, 0, 0]) + se = task.get("source_end", [0, 0, 0]) + ds = task.get("dest_start", [0, 0, 0]) + start_str = f"({ss[0]},{ss[1]},{ss[2]})" + end_str = f"({se[0]},{se[1]},{se[2]})" + dest_str = f"({ds[0]},{ds[1]},{ds[2]})" + self.do_world_import([filename, dm, start_str, end_str, dest_str]) + entry = plugin_entry(SimpleWorldImport) From 33b49494bbb311aae412cea0c98afda62eca5bc0 Mon Sep 17 00:00:00 2001 From: Wuxie233 <121599346+Wuxie233@users.noreply.github.com> Date: Fri, 5 Sep 2025 12:19:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E7=AE=80=E5=8D=95=E4=B8=96=E7=95=8C?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=20=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用于断点续导 --- .../task_store.pytask_store.py" | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 "\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/task_store.pytask_store.py" diff --git "a/\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/task_store.pytask_store.py" "b/\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/task_store.pytask_store.py" new file mode 100644 index 00000000..c0f0fc94 --- /dev/null +++ "b/\347\256\200\345\215\225\344\270\226\347\225\214\345\257\274\345\205\245/task_store.pytask_store.py" @@ -0,0 +1,98 @@ +import os +import json +import time +import uuid +from typing import Any + + +class TaskStore: + """TaskStore manages unfinished import tasks persistence. + + Structure of file: + { + "tasks": [ + { + "task_id": str, + "world_name": str, + "dimension": int, + "source_start": list[int, int, int], + "source_end": list[int, int, int], + "dest_start": list[int, int, int], + "progress_index": int, + "total_chunks": int, + "updated_at": int + } + ] + } + """ + + def __init__(self, file_path: str): + self.file_path = file_path + + def _empty(self) -> dict[str, Any]: + return {"tasks": []} + + def _read(self) -> dict[str, Any]: + if not os.path.isfile(self.file_path): + return self._empty() + try: + with open(self.file_path, "r", encoding="utf-8") as fp: + data = json.load(fp) + if not isinstance(data, dict) or "tasks" not in data or not isinstance(data["tasks"], list): + return self._empty() + return data + except Exception: + return self._empty() + + def _write(self, data: dict[str, Any]) -> None: + os.makedirs(os.path.dirname(self.file_path), exist_ok=True) + with open(self.file_path, "w", encoding="utf-8") as fp: + json.dump(data, fp, ensure_ascii=False, indent=2) + + def list_unfinished(self) -> list[dict[str, Any]]: + data = self._read() + result: list[dict[str, Any]] = [] + for task in data["tasks"]: + try: + if int(task.get("progress_index", 0)) < int(task.get("total_chunks", 0)): + result.append(task) + except Exception: + continue + return result + + def add_task(self, task: dict[str, Any]) -> str: + data = self._read() + if "task_id" not in task or not task["task_id"]: + task["task_id"] = str(uuid.uuid4()) + task["updated_at"] = int(time.time()) + data["tasks"].append(task) + self._write(data) + return task["task_id"] + + def update_progress(self, task_id: str, progress_index: int) -> None: + data = self._read() + changed = False + for task in data["tasks"]: + if task.get("task_id") == task_id: + task["progress_index"] = int(progress_index) + task["updated_at"] = int(time.time()) + changed = True + break + if changed: + self._write(data) + + def remove_task(self, task_id: str) -> None: + data = self._read() + new_tasks: list[dict[str, Any]] = [] + for task in data["tasks"]: + if task.get("task_id") != task_id: + new_tasks.append(task) + data["tasks"] = new_tasks + self._write(data) + + def find_task_by_id(self, task_id: str) -> dict[str, Any] | None: + data = self._read() + for task in data["tasks"]: + if task.get("task_id") == task_id: + return task + return None