├── requirements.txt ├── mcdreforged.plugin.json ├── mcd_task ├── exceptions.py ├── constants.py ├── utils.py ├── global_variables.py ├── __init__.py ├── config.py ├── responsible.py ├── rtext_components.py ├── task_manager.py └── command_actions.py ├── README_cn.md ├── README.md ├── .gitignore └── lang ├── zh_cn.yml └── en_us.yml /requirements.txt: -------------------------------------------------------------------------------- 1 | mcdreforged>=2.1.3 2 | parse -------------------------------------------------------------------------------- /mcdreforged.plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mcd_task", 3 | "version": "2.3.6-dev+build.68", 4 | "name": "Task", 5 | "description": { 6 | "en_us": "A plugin to show tasks of project in progress", 7 | "zh_cn": "用于展示进行中的工程任务的插件" 8 | }, 9 | "author": [ 10 | "Pandaria", 11 | "Ra1ny_Yuki" 12 | ], 13 | "link": "https://github.com/TISUnion/Task", 14 | "dependencies": { 15 | "mcdreforged": ">=2.1.3" 16 | }, 17 | "resources": [ 18 | "lang" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /mcd_task/exceptions.py: -------------------------------------------------------------------------------- 1 | from mcd_task.utils import TitleList 2 | 3 | 4 | class TaskNotFound(Exception): 5 | def __init__(self, titles: TitleList, father: TitleList = None) -> None: 6 | self.titles = str(titles) 7 | self.father = str(father) if father is not None else None 8 | 9 | def __str__(self): 10 | if self.father is not None: 11 | return f"{self.father} has no sub-task named {self.titles}" 12 | else: 13 | return f"{self.titles} not found" 14 | 15 | 16 | class DuplicatedTask(Exception): 17 | pass 18 | 19 | 20 | class IllegalTaskName(Exception): 21 | def __init__(self, titles): 22 | self.titles = str(titles) 23 | -------------------------------------------------------------------------------- /mcd_task/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # |=============================| 4 | # | Plugin global constants | 5 | # |=============================| 6 | 7 | 8 | # Command prefix 9 | PREFIX = "!!task" 10 | 11 | # Data folder 12 | DATA_FOLDER = "./config/task" 13 | if not os.path.isdir(DATA_FOLDER): 14 | os.makedirs(DATA_FOLDER) 15 | # Config file path 16 | CONFIG_PATH = os.path.join(DATA_FOLDER, "config.json") 17 | # Task file path 18 | TASK_PATH = os.path.join(DATA_FOLDER, "mc_task.json") 19 | # Task path for old version of this plugin 20 | TASK_PATH_PREV = "./plugins/task/mc_task.json" 21 | # Responsible Group file path 22 | RESG_PATH = os.path.join(DATA_FOLDER, "responsible.json") 23 | # Log file path 24 | LOG_PATH = os.path.join(DATA_FOLDER, "logs", "task.log") 25 | 26 | # If this is True, will enable some debug options 27 | DEBUG_MODE = False 28 | 29 | # Supported languages 30 | LANGUAGES = ["en_us", "zh_cn"] 31 | 32 | # Player change name info format 33 | PLAYER_RENAMED = "{new_name} (formerly known as {old_name}) joined the game" 34 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | **中文** 2 | 3 | # Task 4 | 5 | 一个用于统计服务器进行中工程任务的插件 6 | 7 | 需要 [MCDReforged](https://github.com/Fallen-Breath/MCDReforged) >= 2.1.3 8 | 9 | 不再需要 [stext](https://github.com/TISUnion/stext), 不再兼容 [MCDeamon](https://github.com/kafuuchino-desu/MCDaemon) 及 [MCDReforged](https://github.com/Fallen-Breath/MCDReforged) 1.x及以下版本 10 | 11 | ### 用法 12 | 13 | `!!task help` 显示帮助信息 14 | 15 | `!!task overview` 显示任务概览(同时也是`!!task`命令的默认行为) 16 | 17 | `!!task list` 显示任务列表 18 | 19 | `!!task detail <任务名称>` 查看任务详细信息 20 | 21 | `!!task list-all` 显示所有任务和它们的子任务 22 | 23 | `!!task add <任务名称> [任务描述]` 添加任务 24 | 25 | `!!task remove`/`rm`/`delete`/`rm <任务名称]>` 删除任务 26 | 27 | `!!task rename <旧任务名称> <新任务名称>` 重命名任务 28 | 29 | `!!task change <任务名称> <新任务描述>` 修改任务描述 30 | 31 | `!!task done <任务名称>` 标注任务为已完成 32 | 33 | `!!task undone <任务名称>` 标注任务为未完成 34 | 35 | `!!task deadline <任务名称> <工期:日数>`/`clear` 为任务设置工期或者清除工期 36 | 37 | `!!task player <任务名称>` 查阅玩家任务列表 38 | 39 | `!!task res[ponsible] <任务名称> <玩家>` 设置任务的责任人 40 | 41 | `!!task unres[ponsible] <任务名称> <玩家>`/`-all` 移除任务的责任人或者移除所有责任人 42 | 43 | 注: 上述所有 `[任务名称]` 可以用 `[任务名称].[子任务名称]` 的形式来访问子任务 44 | 45 | 例: `!!task add 女巫塔.铺地板 挂机铺黑色玻璃` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **English** | [中文](./README_cn.md) 2 | 3 | # Task 4 | 5 | A plugin shows tasks of project in progress 6 | 7 | Requires [MCDReforged](https://github.com/Fallen-Breath/MCDReforged) >= 2.1.3 8 | 9 | [stext](https://github.com/TISUnion/stext) is no longer required. [MCDeamon](https://github.com/kafuuchino-desu/MCDaemon) and [MCDReforged](https://github.com/Fallen-Breath/MCDReforged) 1.x and earlier is no longer supported 10 | 11 | ### Usage 12 | 13 | `!!task help` Show help message 14 | 15 | `!!task overview` Show task overview (also the default behavior of `!!task`) 16 | 17 | `!!task list` Show task list 18 | 19 | `!!task detail ` Show task detail 20 | 21 | `!!task list-all ` Show all tasks and sub-tasks 22 | 23 | `!!task add []` Add a task 24 | 25 | `!!task remove`/`rm`/`delete`/`del ` Remove a task 26 | 27 | `!!task rename ` Rename a task 28 | 29 | `!!task change ` Edit a task description 30 | 31 | `!!task done ` Mark task as done 32 | 33 | `!!task undone ` Mark task as undone 34 | 35 | `!!task deadline `/`clear` Set or clear deadline for the task 36 | 37 | `!!task player ` Show player task list 38 | 39 | `!!task res[ponsible] ` Set responsibles for this task 40 | 41 | `!!task unres[ponsible] `/`-all` Remove responsibles from this task 42 | 43 | PS: All the `` above can be replaced by `.` to access sub-task 44 | 45 | e.g. `!!task add Witch_Hut.Floor AFK black glass placement` 46 | -------------------------------------------------------------------------------- /mcd_task/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Optional, Union, List 3 | 4 | from mcdreforged.command.command_source import CommandSource, PlayerCommandSource 5 | 6 | from mcd_task.global_variables import GlobalVariables 7 | 8 | 9 | class TitleList: 10 | def __init__(self, titles: Optional[Union[str, 'TitleList']] = None): 11 | self.titles = list(str(titles).split('.')) if titles is not None else [] 12 | self.__removed = [] 13 | 14 | def pop_head(self) -> str: 15 | ret = self.titles.pop(0) 16 | self.__removed.append(ret) 17 | return ret 18 | 19 | def pop_tail(self) -> str: 20 | ret = self.titles.pop() 21 | self.__removed = self.removed.copy().lappend(ret).titles 22 | return ret 23 | 24 | @property 25 | def removed(self): 26 | return TitleList('.'.join(self.__removed).strip('.')) 27 | 28 | @property 29 | def head(self) -> Optional[str]: 30 | ts = self.titles 31 | return self.titles[0] if len(ts) > 0 else None 32 | 33 | @property 34 | def tail(self) -> str: 35 | return self.titles[-1] 36 | 37 | def copy(self) -> 'TitleList': 38 | r = TitleList() 39 | r.titles = self.titles[:] 40 | return r 41 | 42 | def lappend(self, title: str) -> 'TitleList': 43 | titles = self.titles.copy() # type: List[str] 44 | titles.reverse() 45 | titles.append(title) 46 | titles.reverse() 47 | self.titles = titles.copy() 48 | return self 49 | 50 | def append(self, title: str) -> 'TitleList': 51 | self.titles.append(title) 52 | return self 53 | 54 | @property 55 | def is_empty(self) -> bool: 56 | return len(self.titles) == 0 57 | 58 | # No longer support python 2.x and MCDeamon so no __unicode__ method 59 | def __str__(self) -> str: 60 | return '.'.join(self.titles) 61 | 62 | 63 | def formatted_time(timestamp: float, locale: Optional[str] = None) -> str: 64 | """ 65 | Format time text with specified locale 66 | :param timestamp: 67 | :param locale: 68 | :return: 69 | """ 70 | return time.strftime(GlobalVariables.server.tr("mcd_task.time_format", lang=locale), time.localtime(timestamp)) 71 | 72 | 73 | def source_name(source: CommandSource): 74 | if isinstance(source, PlayerCommandSource): 75 | return source.player 76 | else: 77 | return source.__class__.__name__ -------------------------------------------------------------------------------- /mcd_task/global_variables.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os.path 3 | import re 4 | import types 5 | from typing import Optional, Union, TYPE_CHECKING 6 | from mcdreforged.api.all import RTextBase, RTextList, RText, RAction, ServerInterface, MCDReforgedLogger 7 | 8 | from mcd_task.config import Config 9 | from mcd_task.constants import LOG_PATH, PREFIX, DEBUG_MODE, DATA_FOLDER 10 | 11 | 12 | if TYPE_CHECKING: 13 | from mcd_task.task_manager import TaskManager 14 | 15 | 16 | def inject_set_file_method(logger: MCDReforgedLogger): 17 | logger.set_file(LOG_PATH) 18 | return logger 19 | 20 | 21 | class GlobalVariables: 22 | task_manager: Optional["TaskManager"] = None 23 | server = ServerInterface.get_instance() 24 | logger = None 25 | if server is not None: 26 | server = server.as_plugin_server_interface() 27 | logger = inject_set_file_method(server.logger) 28 | config = None 29 | 30 | @classmethod 31 | def log(cls, msg): 32 | cls.logger.info(msg) 33 | 34 | @classmethod 35 | def debug(cls, msg): 36 | cls.logger.debug(msg, no_check=DEBUG_MODE) 37 | 38 | @classmethod 39 | def tr(cls, key: Optional[str], *args, **kwargs): 40 | if not key.startswith('mcd_task.'): 41 | key = f"mcd_task.{key}" 42 | return cls.server.rtr(key, *args, **kwargs) 43 | 44 | @classmethod 45 | def htr(cls, key: str, *args, language=None, **kwargs) -> Union[str, RTextBase]: 46 | help_message, help_msg_rtext = cls.server.tr(key, *args, language=language, **kwargs), RTextList() 47 | if not isinstance(help_message, str): 48 | cls.logger.error('Error translate text "{}"'.format(key)) 49 | return key 50 | for line in help_message.splitlines(): 51 | result = re.search(r'(?<=§7){}[\S ]*?(?=§)'.format(PREFIX), line) 52 | if result is not None: 53 | cmd = result.group().strip() + ' ' 54 | help_msg_rtext.append(RText(line).c(RAction.suggest_command, cmd).h( 55 | cls.tr("mcd_task.help_msg_suggest_hover", cmd.strip()))) 56 | else: 57 | help_msg_rtext.append(line) 58 | if line != help_message.splitlines()[-1]: 59 | help_msg_rtext.append('\n') 60 | return help_msg_rtext 61 | 62 | @classmethod 63 | def set_config(cls, cfg: 'Config'): 64 | cls.config = cfg 65 | 66 | @classmethod 67 | def setup_task_manager(cls, task_manager: 'TaskManager'): 68 | cls.task_manager = task_manager 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # IDE 132 | /.idea/ 133 | 134 | # MCDR 135 | *.mcdr 136 | 137 | # KDE Dolphin 138 | .directory 139 | 140 | # Config 141 | config.json 142 | mc_task.json 143 | responsible.json -------------------------------------------------------------------------------- /mcd_task/__init__.py: -------------------------------------------------------------------------------- 1 | from mcd_task.command_actions import * 2 | from mcd_task.global_variables import GlobalVariables 3 | from mcd_task.task_manager import * 4 | from mcd_task.constants import PLAYER_RENAMED, DATA_FOLDER 5 | from mcd_task.config import Config 6 | 7 | from parse import parse 8 | 9 | 10 | def on_info(server: PluginServerInterface, info: Info): 11 | if info.is_from_server and GlobalVariables.config["detect_player_rename"]: 12 | psd = parse(PLAYER_RENAMED, info.content) 13 | if psd is not None: 14 | inherit_responsible(info, **psd.named) 15 | 16 | if info.is_user and DEBUG_MODE: 17 | if info.content.startswith('!!task debug '): 18 | info.cancel_send_to_server() 19 | args = info.content.split(' ') 20 | if args[2] == 'base-title': 21 | info.get_command_source().reply('Manager title is {}'.format(GlobalVariables.task_manager.title)) 22 | elif args[2] == 'full-path' and len(args) == 4: 23 | info.get_command_source().reply(GlobalVariables.task_manager[args[3]].titles) 24 | elif args[2] == 'player-join': 25 | on_player_joined(server, info.player, info) 26 | elif args[2] == 'player-renamed' and len(args) == 5: 27 | inherit_responsible(info, old_name=args[3], new_name=args[4], debug=True) 28 | elif args[2] == 'taskmgr-data' and len(args) == 3: 29 | GlobalVariables.debug(str(GlobalVariables.task_manager.serialize())) 30 | elif args[2] == 'seek-no-father' and len(args) == 3: 31 | GlobalVariables.debug(str(GlobalVariables.task_manager.seek_no_father_nodes())) 32 | elif args[2] == 'detail' and len(args) == 4: 33 | GlobalVariables.debug(str(GlobalVariables.task_manager[args[3]].titles)) 34 | 35 | 36 | def on_player_joined(server: PluginServerInterface, player: str, info: Info): 37 | player_tasks = [] 38 | now_time = float(time.time()) 39 | for t in GlobalVariables.task_manager.responsible_manager[player]: # type: Task 40 | task = GlobalVariables.task_manager[t.titles] 41 | if not task.is_done and now_time > task.deadline != 0: 42 | player_tasks.append(task) 43 | if len(player_tasks) > 0: 44 | task_timed_out(server, player, player_tasks) 45 | 46 | 47 | def on_load(server: PluginServerInterface, prev_module): 48 | if prev_module is not None: 49 | pass 50 | GlobalVariables.set_config(Config.load(server)) 51 | GlobalVariables.setup_task_manager(TaskManager.load()) 52 | register_cmd_tree(server) 53 | server.register_help_message(PREFIX, server.tr("mcd_task.mcdr_help")) 54 | 55 | 56 | def on_unload(*args, **kwargs): 57 | GlobalVariables.logger.unset_file() 58 | -------------------------------------------------------------------------------- /mcd_task/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from mcdreforged.api.all import PluginServerInterface, Serializable 4 | 5 | from mcd_task.constants import CONFIG_PATH, DEBUG_MODE 6 | 7 | 8 | class Config(Serializable): 9 | permission: Dict[str, int] = { 10 | "help": 0, 11 | "list": 0, 12 | "detail": 0, 13 | "list-all": 0, 14 | "list-done": 0, 15 | "player": 2, 16 | "responsible": 2, 17 | "unresponsible": 2 18 | } 19 | detect_player_rename: bool = True 20 | default_overview_instead_of_list: bool = True 21 | overview_deadline_warning_threshold: int = 1 # days 22 | overview_maximum_task_amount: int = 10 23 | 24 | @classmethod 25 | def load(cls, server: PluginServerInterface): 26 | return server.load_config_simple( 27 | file_name=CONFIG_PATH, default_config=cls.get_default().serialize(), in_data_folder=False, target_class=cls 28 | ).set_server(server) 29 | 30 | def save(self): 31 | self.__server.save_config_simple(self, file_name=CONFIG_PATH, in_data_folder=False) 32 | 33 | def set_server(self, server: PluginServerInterface): 34 | self.__server = server 35 | return self 36 | 37 | def __init__(self, **kwargs) -> None: 38 | super(Config, self).__init__(**kwargs) 39 | self.__server: Optional[PluginServerInterface] = None 40 | 41 | @staticmethod 42 | def __get_key_from_dict(target_dict: Dict[str, Any], key: str = None) -> Any: 43 | key_list = key.split('.') 44 | ret = target_dict.copy() 45 | while True: 46 | k = key_list.pop(0) 47 | ret = ret.get(k) 48 | if len(key_list) == 0 or not isinstance(ret, dict): 49 | break 50 | if not len(key_list) == 0: 51 | ret = None 52 | return ret 53 | 54 | def get_permission(self, cmd: str): 55 | return self.permission.get(cmd, 1) 56 | 57 | @staticmethod 58 | def __set_key_to_dict(target_dict: Dict[str, Any], key: str, value: Any): 59 | key_list = key.split('.') 60 | dic = target_dict 61 | while True: 62 | k = key_list.pop(0) 63 | if not isinstance(dic.get(k), dict) and len(key_list) != 0: 64 | dic[k] = {} 65 | if len(key_list) == 0: 66 | dic[k] = value 67 | return 68 | dic = dic[k] 69 | 70 | def __getitem__(self, key: str) -> Any: 71 | ret = self.__get_key_from_dict(self.serialize(), key) 72 | if ret is None: 73 | self.__server.logger.debug("An empty value is returned from config, is it a invalid key?\n" + 74 | "Requested key: {}".format(key), no_check=DEBUG_MODE) 75 | default_value = self.__get_key_from_dict(self.get_default(), key) 76 | if default_value: 77 | self.__set_key_to_dict(vars(self), key, default_value) 78 | self.save() 79 | self.__server.logger.info("Restored default value for {}".format(key)) 80 | return ret 81 | -------------------------------------------------------------------------------- /lang/zh_cn.yml: -------------------------------------------------------------------------------- 1 | mcd_task: 2 | help_msg: | 3 | §7-----§r MCDR {name} v{ver} §7-----§r 4 | 轻松地管理服务器任务 5 | §d【指令帮助】§r 6 | §7{pre} overview§r 显示任务概览 7 | §7{pre} help§r 显示帮助信息 8 | §7{pre} list§r 显示任务列表 9 | §7{pre} reload§r 重载该插件 10 | §7{pre} detail §e<任务名称>§r 查看任务详细信息 11 | §7{pre} list-all§r 列出完整的任务表 12 | §7{pre} add §e<任务名称> §2[任务描述(可选)]§r 添加任务 13 | §7{pre} del §e<任务名称>§r 删除任务 14 | §7{pre} rename §e<旧任务名称> §e<新任务名称>§r 重命名任务 15 | §7{pre} change §e<任务名称> §2<新任务描述>§r 修改任务描述 16 | §7{pre} done §e<任务名称>§r 标注任务为已完成 17 | §7{pre} undone §e<任务名称>§r 标注任务为未完成 18 | §7{pre} deadline §e<任务名称> §c<工期(日)>§r 为任务设置工期 19 | §7{pre} player §3<玩家> §r查看玩家任务列表 20 | §7{pre} priority §e<任务名称>§6 <优先级> §r为任务设置优先级 21 | §7{pre} res§8ponsible §e<任务名称> §3<玩家>§r 设置任务的责任人 22 | §7{pre} unres§8ponsible §e<任务名称> §3<玩家> §r移除任务的责任人 23 | §d【注意事项】§r 24 | 1. 可用鼠标点击任务查看详情,或点击加号快速添加新任务 25 | 2. 可用 §e<任务名称>§r.§e<子任务名称>§r 的形式来访问子任务 26 | 3. 命名子任务时,其名称§c不可包含小数点(即.)§r 27 | §d【命令范例】§r 28 | (若已经有 §e女巫塔§r 任务, 可使用以下命令添加子任务) 29 | §7{pre} add §6女巫塔.铺地板 §2挂机铺黑色玻璃§r 30 | mcdr_help: 工程任务进度管理 31 | overview_help: | 32 | §7{0} list§r 显示完整的任务列表 33 | §7{0} help§r 显示插件帮助信息 34 | perm_denied: 权限不足, 你想桃子呢, 需要权限等级{} 35 | get_help: 点此获取指令帮助 36 | cmd_error: 指令输入有误! 点此获取帮助信息 37 | task_not_found: 任务不存在! 点此查阅任务列表 38 | task_not_found_hover: 点此§6查阅§e任务列表§r 39 | invalid_number: 无效的数字! 点此重新输入数字 40 | resuggest_cmd_hover: 点此重新补全指令 §7{}§r 41 | rename_task_hover: 点此§6重命名§e任务§r 42 | info_player_hover: 点此§6查阅§e玩家详情§r 43 | mark_task_done_hover: 点此将任务§6设为§e完成§r 44 | mark_task_undone_hover: 点此将任务§6设为§e未完成§r 45 | info_task_hover: 点此§6查阅§r任务 §e{}§r 详情§r 46 | time_format: '%Y-%m-%d %H:%M:%S' 47 | ddl_set: 已变更任务的截止日期 48 | task_renamed: '任务 §e{}§a§l 已被重命名:' 49 | player_tasks_title: '玩家 §3{} §a§l肩负了 §3{} §a§l项重任: ' 50 | info_task_title: '任务详细信息: ' 51 | list_task_title: "搬砖信息列表: " 52 | add_task_hover: 点此§6添加§e新任务§r 53 | add_sub_task_hover: 点此§6添加§e新子任务§r 54 | edit_task_hover: 点此§6变更§e任务说明§r 55 | done_task_button: '[已完成任务...]' 56 | done_task_hover: 点此§6查阅§r被标记为§e完成§r的任务 57 | task_already_exist: 任务已存在, 点此查阅任务列表 58 | new_task_created: '创建了新的任务: ' 59 | detailed_info_task_title: '搬砖详细信息列表: ' 60 | deleted_task: 任务 {} 已删除 61 | illegal_title_with_dot: '名称无效: 包含小数点' 62 | done_task_list_title: '已完成任务列表: ' 63 | on_player_joined: 您有{}项已逾期的任务, 请赶快填坑! 64 | on_player_renamed: 检测到游戏ID变更, 继承了{}项任务 65 | list_responsible_title: '此任务由 §3{}§ r名玩家承包: ' 66 | removed_responsibles_title: '移除了 §3{}§r 名责任人: ' 67 | added_responsibles_title: '添加了 §3{}§r 名责任人: ' 68 | illegal_call: 不含额外参数的该指令仅可作为玩家使用 69 | help_msg_suggest_hover: 点此以填入 §7{}§r 70 | set_ddl_hover: 点此为任务§6设置§e工期§r 71 | reloaded: "[Task] §a§l插件已重载§r" 72 | overview_headline: "当务之急: " 73 | no_priority: 暂无优先事项, 可查阅完整列表获取更多任务 74 | priority_set: 任务优先级已变更 75 | date_approaching: | 76 | 截止日期临近或已过: 77 | §c{}§r 78 | has_a_high_priority: "该任务的优先级较高: §6{}§r" 79 | detail_priority: '优先级: §6{} §r[✎]' 80 | priority_hover: '点此§6设置§e该任务的优先级§r' 81 | detail_desc: '任务描述: §2{} §r[✎]' 82 | desc_hover: 点此§6编辑§e任务描述§r 83 | detail_deadline: '截止日期: §c{} §r[✎]' 84 | detail_sub: "子任务: " 85 | not_set: "暂未设定" 86 | changed_desc_title: 已变更任务描述 87 | done_task_title: 任务被标记为已完成 88 | undone_task_title: 任务被标记为未完成 89 | detail_res: "责任人: §5§l[+]§r" 90 | add_res_hover: 点击为该任务§6添加§e责任人§r 91 | rm_res_hover: 点击自此任务§6移除§e责任人 {}§r 92 | ddl_cleared: 已移除任务的截止日期要求 93 | -------------------------------------------------------------------------------- /mcd_task/responsible.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Dict, Set, Union, TYPE_CHECKING, Iterator 4 | 5 | from parse import parse 6 | 7 | from mcd_task.constants import RESG_PATH 8 | from mcd_task.utils import TitleList 9 | from mcd_task.exceptions import DuplicatedTask, TaskNotFound 10 | 11 | 12 | if TYPE_CHECKING: 13 | from mcd_task.task_manager import TaskManager, Task 14 | 15 | 16 | class ResponsibleManager: 17 | def __init__(self, task_manager: "TaskManager"): 18 | self.path = RESG_PATH # type: str 19 | self.player_work = {} # type: Dict[str, Set[str]] 20 | self.task_manager = task_manager 21 | 22 | def rename_player(self, old_name: str, new_name: str, should_save=True): 23 | value = self.player_work.pop(old_name) 24 | self.player_work[new_name] = value 25 | if should_save: 26 | self.save() 27 | return value 28 | 29 | def rename_task(self, old_title: Union['TitleList', str], 30 | new_title: Union['TitleList', str], should_save=True) -> None: 31 | old_title, new_titles = str(old_title), TitleList(old_title) 32 | new_titles.pop_tail() 33 | new_titles.append(new_title) 34 | new_title = str(new_titles) 35 | new_data = {} 36 | for key, value in self.player_work.items(): 37 | for t in value: 38 | psd = parse(old_title + '{ext}', t) 39 | if psd is not None: 40 | new_data[key] = value.copy() 41 | new_data[key].remove(t) 42 | new_data[key].add(new_title + psd['ext']) 43 | self.player_work.update(new_data) 44 | if should_save: 45 | self.save() 46 | 47 | def remove_task(self, task_title: Union['TitleList', str], should_save=True) -> None: 48 | task_title = str(task_title) 49 | removed = self.player_work.copy() 50 | for key, value in self.player_work.items(): 51 | for t in value.copy(): 52 | if t.startswith(task_title): 53 | removed[key].remove(t) 54 | self.player_work = removed 55 | if should_save: 56 | self.save() 57 | 58 | def add_work(self, player: str, task_title: Union['TitleList', str], should_save=True) -> None: 59 | task_title = str(task_title) 60 | if player not in self.player_work.keys(): 61 | self.player_work[player] = set() 62 | if task_title not in self.player_work[player]: 63 | self.player_work[player].add(task_title) 64 | else: 65 | raise DuplicatedTask(task_title + " duplicated") 66 | if should_save: 67 | self.save() 68 | 69 | def rm_work(self, player: str, task_title: Union['TitleList', str], should_save=True) -> None: 70 | task_title = str(task_title) 71 | if player not in self.player_work.keys(): 72 | self.player_work[player] = set() 73 | if task_title not in self.player_work[player]: 74 | raise TaskNotFound(TitleList(task_title)) 75 | self.player_work[player].remove(task_title) 76 | if should_save: 77 | self.save() 78 | 79 | def save(self) -> None: 80 | to_save = {} 81 | for p, t in self.player_work.items(): 82 | to_save[p] = list(t) 83 | with open(self.path, 'w', encoding='UTF-8') as f: 84 | json.dump(to_save, f, indent=4, ensure_ascii=False) 85 | 86 | def load(self) -> None: 87 | if not os.path.isfile(self.path): 88 | self.save() 89 | with open(self.path, 'r', encoding='UTF-8') as f: 90 | to_load = json.load(f) 91 | for p, t in to_load.items(): 92 | self.player_work[p] = set(t) 93 | 94 | def get_responsibles(self, task_title: Union['TitleList', str]): 95 | task_title = str(task_title) 96 | ret = set() 97 | for key, value in self.player_work.items(): 98 | if task_title in value: 99 | ret.add(key) 100 | return list(ret) 101 | 102 | def __getitem__(self, player: str) -> Iterator["Task"]: 103 | task_titles = self.player_work.get(player, set()) 104 | for titles in task_titles: 105 | yield self.task_manager[titles] 106 | -------------------------------------------------------------------------------- /lang/en_us.yml: -------------------------------------------------------------------------------- 1 | mcd_task: 2 | help_msg: | 3 | §7-----§r MCDR {name} v{ver} §7-----§r 4 | Manage tasks in your sever easily 5 | §d【Command Help】§r 6 | §7{pre} overview§r Show task overview 7 | §7{pre} help§r Show this help message 8 | §7{pre} list§r List all tasks 9 | §7{pre} reload§r Reload this plugin 10 | §7{pre} detail §e§r Show detail of the specified task 11 | §7{pre} list-all§r List full map of the tasks 12 | §7{pre} add §e §2[desc(optional)]§r Add a new task 13 | §7{pre} remove §e§r Delete a task 14 | §7{pre} rename §e §r Rename a task 15 | §7{pre} change §e §2§r Edit a task description 16 | §7{pre} done §e§r Mark a task as done 17 | §7{pre} undone §e§r Mark a task as undone 18 | §7{pre} deadline §e §c§r Set a task deadline 19 | §7{pre} player §e §rShow tasks of a player 20 | §7{pre} priority §e §6§rSet a task priority 21 | §7{pre} res§8ponsible §e §3§r Set task responsibles 22 | §7{pre} unres§8ponsible §e §3 §rDel responsibles 23 | §d【Tips】§r 24 | 1. Click task text to show detail or plus mark to add a task 25 | 2. Use §e.§r to access the sub-tasks 26 | 3. §cDO NOT§r name the task with §cdot(.)§r 27 | §d【Command Example】§r 28 | You can add a sub-task to task §ewitch_farm§r with this command: 29 | §7{pre} add §ewitch_farm.floor §2AFK lay black glass§r 30 | mcdr_help: Manage progression of project tasks 31 | overview_help: | 32 | §7{0} list§r Show full task list 33 | §7{0} help§r Show plugin help message 34 | perm_denied: Permission denied, requires level {} 35 | get_help: Click to get command help 36 | cmd_error: Command error! Click here to get help message 37 | task_not_found: Task not found! Click here to browse task list 38 | task_not_found_hover: Click to run {} and browse task list 39 | invalid_number: Invalid number, click to reinput a number 40 | resuggest_cmd_hover: Click here to refill command {} 41 | rename_task_hover: Click here to rename this task 42 | info_player_hover: Click here to §6browse §eplayer info§r 43 | mark_task_done_hover: Click here to mark task as §edone§r 44 | mark_task_undone_hover: Click here to mark task as §eundone§r 45 | time_format: '%b.%d %Y %H:%M:%S' 46 | ddl_set: "Task deadline has been changed" 47 | task_renamed: 'Task §e{}§a§l has been renamed: ' 48 | player_tasks_title: 'Player §3{}§a§l has §3{}§a§l tasks to do: ' 49 | list_task_title: 'Task information list: ' 50 | add_task_hover: Click here to §6add§e a new task§r 51 | edit_task_hover: Click here to §6edit§e task description§r 52 | add_sub_task_hover: Click here to §6add§e a new sub-task§r 53 | info_task_title: 'Task detailed information: ' 54 | done_task_button: '[Finished tasks...]' 55 | done_task_hover: Click here to browse tasks marked as §edone§r 56 | info_task_hover: Click here to §6browse§r task §e{} §rdetail 57 | task_already_exist: Task already exist, click to browse task list 58 | new_task_created: 'New task created: ' 59 | detailed_info_task_title: 'Task detailed information list: ' 60 | deleted_task: Deleted task {} 61 | illegal_title_with_dot: "Invalid title: can't include dot" 62 | done_task_list_title: 'Finished task lists: ' 63 | on_player_joined: You have {} outdated tasks, please hurry up! 64 | on_player_renamed: Detected your nickname changed, inherited {} tasks 65 | list_responsible_title: 'This task has §3{}§a§l responsibles: ' 66 | removed_responsibles_title: 'Removed §3{}§a§l responsibles: ' 67 | added_responsibles_title: 'Added §3{}§a§l responsibles: ' 68 | illegal_call: This command without argument can only be called as player 69 | set_ddl_hover: Click here to §6set§e deadline 70 | help_msg_suggest_hover: Click here to fill §7{}§r 71 | reloaded: "[Task] §a§lPlugin reloaded§r" 72 | overview_headline: "Top priorities:" 73 | no_priority: No priorities found, check full list for more tasks 74 | date_approaching: | 75 | Deadline is approaching or already passed: 76 | {} 77 | has_a_high_priority: "This task has a high priority: {}" 78 | detail_priority: "Priority: §6{} §r[✎]" 79 | priority_hover: Click to §6set§e priority of this task 80 | priority_set: Task priority has been changed 81 | detail_desc: 'Task description: §2{} §r[✎]' 82 | desc_hover: Click here to §6edit§e description 83 | detail_deadline: "Deadline: §c{} §r[✎]" 84 | detail_sub: "Sub-tasks: " 85 | not_set: Not set yet 86 | changed_desc_title: Task description changed 87 | done_task_title: Task was marked as done 88 | undone_task_title: Task was marked as undone 89 | detail_res: "Responsibles: §5§l[+]§r" 90 | add_res_hover: Click to §6add§e responsibles§r to this task 91 | rm_res_hover: Click to §6remove§e {} as reponsible§r from this task 92 | ddl_cleared: Task deadline has been removed -------------------------------------------------------------------------------- /mcd_task/rtext_components.py: -------------------------------------------------------------------------------- 1 | from mcdreforged.api.rtext import * 2 | 3 | from mcd_task.task_manager import Task 4 | from mcd_task.global_variables import GlobalVariables 5 | from mcd_task.constants import * 6 | from collections import namedtuple 7 | from collections import namedtuple 8 | 9 | from mcdreforged.api.rtext import * 10 | 11 | from mcd_task.constants import * 12 | from mcd_task.global_variables import GlobalVariables 13 | from mcd_task.task_manager import Task 14 | 15 | TypeItems = namedtuple('TypeItems', ["name", 'hover_tr_key', 'cmd_fmt']) 16 | 17 | 18 | class EditButtonType: 19 | rename = TypeItems('name', 'rename_task_hover', PREFIX + " rename {} ") 20 | desc = TypeItems("desc", "edit_task_hover", PREFIX + " change {} ") 21 | priority = TypeItems("priority", "priority_hover", PREFIX + " priority {} ") 22 | deadline = TypeItems("deadline", "set_ddl_hover", PREFIX + " deadline {} ") 23 | 24 | 25 | def tr(key: str, *args, **kwargs): 26 | return GlobalVariables.tr(key, *args, **kwargs) 27 | 28 | 29 | def indent_text(indent: int) -> str: 30 | ret = '' 31 | for num in range(indent): 32 | ret += ' ' 33 | return ret 34 | 35 | 36 | def list_done_task_button(): 37 | return tr('done_task_button').c(RAction.run_command, f'{PREFIX} list-done').h( 38 | tr('done_task_hover')).set_color(RColor.dark_gray) 39 | 40 | 41 | def done_button(task: Task): 42 | # Done button 43 | click_event_to_do = 'undone' if task.is_done else 'done' 44 | return RText("⬛" if task.done else "⬜", RColor.dark_gray if task.done else RColor.white).h( 45 | tr(f"mark_task_{click_event_to_do}_hover")).c( 46 | RAction.run_command, "{} {} {}".format(PREFIX, click_event_to_do, task.titles) 47 | ) 48 | 49 | 50 | def add_task_button(title: str = None): 51 | # Add button 52 | title_path = '' if title in ['', None] else '{}.'.format(title) 53 | return RText('[+]', RColor.light_purple, RStyle.bold).c( 54 | RAction.suggest_command, '{} add {}'.format(PREFIX, title_path)).h( 55 | tr(f"add{'' if title is None else '_sub'}_task_hover") 56 | ) 57 | 58 | 59 | def edit_button(task: Task, button_type: TypeItems = EditButtonType.rename): 60 | return RText(" [✎]").h(tr(button_type.hover_tr_key)).c(RAction.suggest_command, button_type.cmd_fmt.format(task.titles)) 61 | 62 | 63 | def title_text(task: Task, display_full_path=False, with_edit_button=False, indent=4, display_not_empty_mark=False, 64 | include_sub=True): 65 | edit = '' 66 | if with_edit_button: 67 | edit = edit_button(task) 68 | target_title, title_text_list = task.titles.copy(), [] 69 | while True: 70 | if target_title.is_empty: 71 | break 72 | if not display_full_path and len(title_text_list) == 1: 73 | break 74 | this_title_full = str(target_title) 75 | this_title = target_title.pop_tail() 76 | title_text_list.append( 77 | RText(this_title, 78 | RColor.gray if GlobalVariables.task_manager[this_title_full].is_done else RColor.yellow).c( 79 | RAction.run_command, f'{PREFIX} detail {this_title_full}').h(tr('info_task_hover', this_title_full)) 80 | ) 81 | title_text_list.reverse() 82 | title = RText.join('§7.§r', title_text_list) 83 | if display_not_empty_mark: 84 | if task.is_not_empty or (include_sub and len(task.sub_tasks) > 0): 85 | title += ' §f[...]' 86 | return indent_text(indent) + done_button(task) + ' ' + title + edit 87 | 88 | 89 | # !!task detail components 90 | def info_elements(task: Task, button_type: TypeItems = EditButtonType.desc, indent=4): 91 | return indent_text(indent) + tr(f'detail_{button_type.name}', task.get_elements(button_type.name)).h( 92 | tr(button_type.hover_tr_key)).c( 93 | RAction.suggest_command, button_type.cmd_fmt.format(task.titles)) 94 | 95 | 96 | def info_responsibles_headline(task: Task): 97 | return tr('detail_res').c( 98 | RAction.suggest_command, '{} res {} '.format(PREFIX, task.titles)).h(tr(f"add_res_hover")) 99 | 100 | 101 | def single_responsible(task: Task, player: str, indent=8, removed=False): 102 | text = indent_text(indent) + RText( 103 | player, RColor.dark_gray if removed else RColor.dark_aqua, RStyle.strikethrough if removed else None).h( 104 | tr('info_player_hover')).c( 105 | RAction.run_command, f'{PREFIX} player {player}') + ' ' 106 | if not removed: 107 | text += RText('[-]', RColor.aqua, RStyle.bold).c( 108 | RAction.run_command, f'{PREFIX} unres {task.titles} {player}').h(tr('rm_res_hover', player)) 109 | return text 110 | 111 | 112 | def info_responsibles(task: Task, indent=4): 113 | text = indent_text(indent) + info_responsibles_headline(task) 114 | for player in task.responsibles: 115 | text += '\n' + single_responsible(task, player, indent=indent + 4) 116 | return text 117 | 118 | 119 | def sub_task_title_text(task: Task, indent=4): 120 | text = [] 121 | for sub in task.sorted_sub_tasks: 122 | text.append(indent_text(indent) + title_text(sub)) 123 | if len(sub.sub_tasks) > 0: 124 | text.append(sub_task_title_text(sub, indent + 4)) 125 | return RTextBase.join('\n', text) 126 | 127 | 128 | def info_sub_tasks(task: Task, indent=4): 129 | text = indent_text(indent) + tr('detail_sub') + ' ' + add_task_button(str(task.titles)) 130 | if len(task.sub_tasks) > 0: 131 | text += '\n' + sub_task_title_text(task, indent=indent + 4) 132 | return text 133 | -------------------------------------------------------------------------------- /mcd_task/task_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | 5 | from copy import copy 6 | from typing import List, Dict, Tuple, Any, Union, Optional, Iterable 7 | 8 | from mcdreforged.api.utils import Serializable, deserialize 9 | 10 | from mcd_task.exceptions import TaskNotFound, DuplicatedTask 11 | from mcd_task.utils import TitleList, formatted_time 12 | from mcd_task.constants import TASK_PATH, DEBUG_MODE 13 | from mcd_task.responsible import ResponsibleManager 14 | from mcd_task.global_variables import GlobalVariables 15 | 16 | 17 | SUB_TASKS = 'rue' 18 | 19 | 20 | class TaskBase(Serializable): 21 | title: str = "" 22 | done: bool = False 23 | description: str = '' 24 | sub_tasks: List["Task"] = [] 25 | deadline: float = 0 26 | permission: int = 0 27 | priority: Optional[int] = None 28 | 29 | for key, value in locals().copy().items(): 30 | if value is sub_tasks: 31 | globals()['SUB_TASKS'] = key 32 | 33 | def __init__(self, **kwargs): 34 | super().__init__(**kwargs) 35 | self._father: Optional["TaskBase"] = None 36 | 37 | @property 38 | def titles(self): 39 | return self.full_path() 40 | 41 | @property 42 | def child_map(self) -> Dict[str, 'Task']: 43 | ret = {} 44 | for item in self.sub_tasks: 45 | ret[item.title] = item 46 | return ret 47 | 48 | @property 49 | def child_titles(self) -> List[str]: 50 | return list(self.child_map.keys()) 51 | 52 | @property 53 | def is_not_empty(self): 54 | if self.deadline != 0: 55 | return True 56 | if self.description != '': 57 | return True 58 | if self.priority is not None: 59 | return True 60 | if len(self.responsibles) > 0: 61 | return True 62 | return False 63 | 64 | @property 65 | def is_done(self): 66 | return self.done or self._father.is_done 67 | 68 | def full_path(self, titles: 'TitleList' = TitleList()) -> 'TitleList': 69 | raise NotImplementedError('Not implemented method: TaskBase.full_path()') 70 | 71 | def add_task(self, titles: 'TitleList', desc: str = '') -> None: 72 | next_gen_title = titles.pop_head() 73 | next_gen_desc = desc if titles.is_empty else '' 74 | if next_gen_title not in self.child_titles: 75 | self.__add_task(dict(title=next_gen_title, description=next_gen_desc)) 76 | if not titles.is_empty: 77 | self.child_map[next_gen_title].add_task(titles, desc) 78 | 79 | def next_layer(self, titles: 'TitleList') -> Tuple[str, TitleList]: 80 | next_layer_title = titles.pop_head() 81 | if next_layer_title not in next_layer_title: 82 | raise TaskNotFound(self.titles.copy().append(next_layer_title)) 83 | return next_layer_title, titles 84 | 85 | def __add_task(self, data: dict): 86 | self.sub_tasks.append(Task.deserialize(data).set_father(self)) 87 | 88 | def seek_no_father_nodes(self): 89 | result = [] 90 | for item in self.sub_tasks: 91 | if item._father is None: 92 | result.append(item) 93 | result += item.seek_no_father_nodes() 94 | return result 95 | 96 | def create_sub_tasks_from_serialized_data(self, serialized_task_list: List[Dict[str, Any]]): 97 | self.__setattr__(SUB_TASKS, []) 98 | for item in serialized_task_list: 99 | self.__add_task(item) 100 | 101 | def set_father(self, father_node: "TaskBase"): 102 | if not isinstance(father_node, TaskBase): 103 | raise TypeError(type(father_node).__name__) 104 | self._father = father_node 105 | return self 106 | 107 | @classmethod 108 | def deserialize(cls, data: dict, **kwargs): 109 | GlobalVariables.debug(data) 110 | sub_tasks = copy(data.get(SUB_TASKS, [])) 111 | if not isinstance(sub_tasks, list): 112 | raise TypeError( 113 | 'Unsupported input type: expected class "{}" but found data with class "{}"'.format( 114 | list.__name__, type(data).__name__ 115 | )) 116 | data[SUB_TASKS] = [] 117 | this_task = deserialize(data=data, cls=cls, **kwargs) 118 | this_task.create_sub_tasks_from_serialized_data(sub_tasks) 119 | return this_task 120 | 121 | def split_sub_tasks_by_done(self): 122 | undones, dones = [], [] 123 | for t in self.sub_tasks: 124 | if t.done: 125 | dones.append(t) 126 | else: 127 | undones.append(t) 128 | return undones, dones 129 | 130 | @property 131 | def sorted_sub_tasks(self): 132 | undones, dones = self.split_sub_tasks_by_done() 133 | return sort_by_title(undones) + sort_by_title(dones) 134 | 135 | def __getitem__(self, titles: Union[TitleList, str]) -> Union['Task', 'TaskBase']: 136 | if isinstance(titles, str): 137 | titles = TitleList(titles) 138 | if titles.is_empty: 139 | return self 140 | next_layer_title, titles = self.next_layer(titles) 141 | if next_layer_title not in self.child_titles: 142 | raise TaskNotFound(titles.lappend(next_layer_title)) 143 | return self.child_map[next_layer_title][titles] 144 | 145 | def seek_for_item_with_priority(self, sort=True, with_done=False): 146 | result = [] 147 | for item in self.sub_tasks: 148 | if isinstance(item.priority, int): 149 | if with_done or not item.is_done: 150 | result.append(item) 151 | GlobalVariables.debug(f'Priority task found: {item.full_path()}') 152 | result += item.seek_for_item_with_priority(sort=False) 153 | return sorted(result, key=lambda task: task.priority, reverse=True) if sort else result 154 | 155 | def seek_for_item_with_deadline_approaching(self, sort=True, with_done=False): 156 | result = [] 157 | for item in self.sub_tasks: 158 | if item.deadline != 0 and item.deadline - \ 159 | time.time() < 3600 * 24 * GlobalVariables.config.overview_deadline_warning_threshold: 160 | if with_done or not item.is_done: 161 | result.append(item) 162 | GlobalVariables.debug(f'Deadline task found: {item.full_path()}') 163 | result += item.seek_for_item_with_deadline_approaching(sort=False) 164 | return sorted(result, key=lambda task: task.deadline, reverse=False) if sort else result 165 | 166 | @property 167 | def responsibles(self): 168 | return GlobalVariables.task_manager.responsible_manager.get_responsibles(self.titles) 169 | 170 | def __str__(self): 171 | return str(self.serialize()) 172 | 173 | 174 | class Task(TaskBase): 175 | def full_path(self, titles: TitleList = TitleList()) -> TitleList: 176 | return self._father.full_path(titles.copy().lappend(self.title)) 177 | 178 | def reinit(self) -> None: 179 | if DEBUG_MODE: 180 | self.title = '' 181 | self.done = False 182 | self.description = '' 183 | self.sub_tasks = [] # type: List[Task] 184 | 185 | def get_elements(self, element_name: str): 186 | mappings = { 187 | 'name': str(self.titles), 188 | 'desc': self.description, 189 | 'priority': '' if self.priority is None else self.priority, 190 | 'deadline': '' if self.deadline == 0 else formatted_time(self.deadline) 191 | } 192 | return mappings.get(element_name) 193 | 194 | 195 | class TaskManager(TaskBase): 196 | title: str = "TaskManager" 197 | __responsible_manager = None 198 | 199 | @property 200 | def responsible_manager(self): 201 | if self.__responsible_manager is None: 202 | self.__responsible_manager = ResponsibleManager(self) 203 | return self.__responsible_manager 204 | 205 | def save(self): 206 | data = json.dumps(self.serialize(), indent=4, ensure_ascii=False) 207 | GlobalVariables.debug(f'Saving data: {data}') 208 | with open(TASK_PATH, 'w', encoding='utf8') as fp: 209 | fp.write(data) 210 | 211 | @property 212 | def is_done(self): 213 | return False 214 | 215 | @classmethod 216 | def load(cls): 217 | if not os.path.isfile(TASK_PATH): 218 | cls.get_default().save() 219 | with open(TASK_PATH, 'r', encoding='utf8') as fp: 220 | js = json.load(fp) 221 | manager = cls.deserialize(js) 222 | GlobalVariables.debug(manager.serialize()) 223 | manager.responsible_manager.load() 224 | return manager 225 | 226 | def exists(self, titles: TitleList) -> bool: 227 | titles = titles.copy() 228 | father_titles = titles.copy() 229 | title = father_titles.pop_tail() 230 | try: 231 | father = self[father_titles] 232 | except TaskNotFound: 233 | return False 234 | if title in father.child_titles: 235 | return True 236 | else: 237 | return False 238 | 239 | def full_path(self, titles: 'TitleList' = TitleList()) -> 'TitleList': 240 | return titles 241 | 242 | # ========================= 243 | 244 | def add_task(self, titles: 'TitleList', desc: str = '', should_save=True) -> None: 245 | super(TaskManager, self).add_task(titles, desc) 246 | if should_save: 247 | self.save() 248 | 249 | def delete_task(self, titles: 'TitleList', should_save=True) -> None: 250 | titles_father = titles.copy() 251 | title_to_del = titles_father.pop_tail() 252 | father_delete = self[titles_father] 253 | try: 254 | task = father_delete.child_map[title_to_del] 255 | except KeyError: 256 | raise TaskNotFound(titles.copy(), father_delete.full_path().copy()) 257 | father_delete.sub_tasks.remove(task) 258 | self.responsible_manager.remove_task(titles) 259 | 260 | if should_save: 261 | self.save() 262 | 263 | def rename_task(self, titles: 'TitleList', new_title: str, should_save=True) -> None: 264 | self[titles.copy()].title = new_title 265 | new_titles = titles.copy() 266 | new_titles.pop_tail() 267 | new_titles.append(new_title) 268 | self.responsible_manager.rename_task(titles, new_title) 269 | if should_save: 270 | self.save() 271 | 272 | def set_deadline(self, titles: 'TitleList', deadline: float, should_save=True) -> None: 273 | self[titles].deadline = deadline 274 | if should_save: 275 | self.save() 276 | 277 | def edit_desc(self, titles: 'TitleList', new_desc: str, should_save=True) -> None: 278 | self[titles].description = new_desc 279 | if should_save: 280 | self.save() 281 | 282 | def done_task(self, titles: 'TitleList', should_save=True): 283 | self[titles].done = True 284 | if should_save: 285 | self.save() 286 | 287 | def undone_task(self, titles: 'TitleList', should_save=True): 288 | self[titles].done = False 289 | if should_save: 290 | self.save() 291 | 292 | def set_responsible(self, titles: TitleList, *res): 293 | num = 0 294 | for r in res: 295 | try: 296 | self.responsible_manager.add_work(r, titles, should_save=False) 297 | except DuplicatedTask: 298 | pass 299 | else: 300 | num += 1 301 | self.responsible_manager.save() 302 | return num 303 | 304 | def rm_responsible(self, titles: TitleList, *res): 305 | removed = set() 306 | for r in res: 307 | try: 308 | self.responsible_manager.rm_work(r, titles, should_save=False) 309 | except TaskNotFound: 310 | pass 311 | else: 312 | removed.add(r) 313 | self.responsible_manager.save() 314 | return removed 315 | 316 | def set_perm(self, titles: TitleList, perm_level: int, should_save=True) -> None: 317 | if perm_level in [0, 1, 2, 3, 4]: 318 | self[titles].permission = perm_level 319 | if should_save: 320 | self.save() 321 | 322 | def set_priority(self, titles: TitleList, priority: int, should_save=True) -> None: 323 | self[titles].priority = priority 324 | if should_save: 325 | self.save() 326 | 327 | 328 | def sort_by_title(unsorted_task_list: Iterable[Task]): 329 | return sorted(unsorted_task_list, key=lambda task: task.title) 330 | 331 | 332 | if __name__ == "__main__": 333 | print(SUB_TASKS) 334 | -------------------------------------------------------------------------------- /mcd_task/command_actions.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Optional, Union, List 3 | 4 | from mcdreforged.api.all import * 5 | 6 | from mcd_task.global_variables import GlobalVariables 7 | from mcd_task.constants import PREFIX, DEBUG_MODE 8 | from mcd_task.exceptions import TaskNotFound 9 | from mcd_task.task_manager import Task 10 | from mcd_task.utils import formatted_time, source_name, TitleList 11 | from mcd_task.rtext_components import tr, info_elements, title_text, info_responsibles, add_task_button, \ 12 | EditButtonType, info_sub_tasks, sub_task_title_text 13 | 14 | 15 | # =============================== 16 | # | Register Command Tree | 17 | # =============================== 18 | 19 | 20 | def register_cmd_tree(server: PluginServerInterface): 21 | def permed_literal(*literal: str) -> Literal: 22 | lvl = GlobalVariables.config.get_permission(literal[0]) 23 | lvl = lvl if isinstance(lvl, int) else 0 24 | return Literal(literal).requires( 25 | lambda src: src.has_permission(lvl), 26 | failure_message_getter=lambda: server.rtr('mcd_task.perm_denied', lvl) 27 | ) 28 | 29 | def ensure_task_exist_quotable_text(title: str = 'title'): 30 | return QuotableText(title).requires( 31 | lambda src, ctx: GlobalVariables.task_manager.exists(TitleList(ctx[title])), 32 | lambda: tr("mcd_task.task_not_found").h( 33 | tr("mcd_task.task_not_found_hover", PREFIX)).c( 34 | RAction.run_command, f'{PREFIX} list').set_color( 35 | RColor.red 36 | ) 37 | ) 38 | 39 | def ensure_task_not_exist_quotable_text(title: str = 'title'): 40 | def ensure_not_exist(source: CommandSource, context: CommandContext): 41 | return not GlobalVariables.task_manager.exists(TitleList(context[title])) 42 | 43 | return QuotableText(title).requires( 44 | ensure_not_exist, 45 | lambda: tr("mcd_task.task_already_exist").h( 46 | tr("mcd_task.task_not_found_hover", PREFIX)).c( 47 | RAction.run_command, f'{PREFIX} list').set_color( 48 | RColor.red 49 | ) 50 | ) 51 | 52 | root_func = task_overview if GlobalVariables.config.default_overview_instead_of_list else list_task 53 | root_node = Literal(PREFIX).runs(lambda src: root_func(src)) 54 | nodes = [ 55 | permed_literal('overview').runs(lambda src: task_overview(src)), 56 | permed_literal('list').runs(lambda src: list_task(src)), 57 | permed_literal('help').runs(lambda src: show_help(src)), 58 | permed_literal('detail').then( 59 | ensure_task_exist_quotable_text().runs(lambda src, ctx: info_task(src, title=ctx['title'])) 60 | ), 61 | permed_literal('list-all').runs(lambda src: all_tasks_detail(src)), 62 | permed_literal('add').then( 63 | ensure_task_not_exist_quotable_text().runs(lambda src, ctx: add_task(src, ctx['title'])).then( 64 | GreedyText('description').runs(lambda src, ctx: add_task(src, ctx['title'], ctx['description'])) 65 | ) 66 | ), 67 | permed_literal('remove', 'rm', 'delete', 'del').then( 68 | ensure_task_exist_quotable_text().runs(lambda src, ctx: remove_task(src, ctx['title'])) 69 | ), 70 | permed_literal('rename').then( 71 | ensure_task_exist_quotable_text('old_titles').then( 72 | QuotableText('new_title').runs(lambda src, ctx: rename_task(src, ctx['old_titles'], ctx['new_title'])) 73 | ) 74 | ), 75 | permed_literal('change').then( 76 | ensure_task_exist_quotable_text().then( 77 | GreedyText('description').runs(lambda src, ctx: edit_desc(src, ctx['title'], ctx['description'])) 78 | ) 79 | ), 80 | permed_literal('done').then( 81 | ensure_task_exist_quotable_text().runs(lambda src, ctx: set_done(src, ctx['title'])) 82 | ), 83 | permed_literal('undone').then( 84 | ensure_task_exist_quotable_text().runs(lambda src, ctx: set_undone(src, ctx['title'])) 85 | ), 86 | permed_literal('deadline').then( 87 | ensure_task_exist_quotable_text().then( 88 | Literal('clear').runs(lambda src, ctx: clear_task_deadline(src, ctx['title'])) 89 | ).then( 90 | Number('days').runs(lambda src, ctx: set_task_deadline(src, ctx['title'], ctx['days'])) 91 | ) 92 | ), 93 | permed_literal('player').then( 94 | QuotableText('name').runs(lambda src, ctx: info_player(src, ctx['name'])) 95 | ), 96 | permed_literal("responsible", "res").then( 97 | ensure_task_exist_quotable_text().runs(lambda src, ctx: set_responsible(src, ctx['title'])).then( 98 | GreedyText("players").runs(lambda src, ctx: set_responsible(src, ctx['title'], ctx['players'])) 99 | ) 100 | ), 101 | permed_literal("unresponsible", "unres").then( 102 | ensure_task_exist_quotable_text().runs(lambda src, ctx: rm_responsible(src, ctx['title'])).then( 103 | Literal('-all').runs(lambda src, ctx: rm_all_responsible(src, ctx['title'])) 104 | ).then( 105 | GreedyText("players").runs(lambda src, ctx: rm_responsible(src, ctx['title'], ctx['players'])) 106 | ) 107 | ), 108 | permed_literal('priority').then( 109 | ensure_task_exist_quotable_text().then( 110 | Literal('clear').runs(lambda src, ctx: set_task_priority(src, ctx['title'])) 111 | ).then( 112 | Integer('priority').runs(lambda src, ctx: set_task_priority(src, ctx['title'], ctx['priority'])) 113 | ) 114 | ), 115 | permed_literal('reload').runs(lambda src: reload_self(src)) 116 | ] 117 | for node in nodes: 118 | GlobalVariables.debug(f'Registered cmd "{PREFIX} {list(node.literals)[0]}"') 119 | root_node.then(node) 120 | server.register_command(root_node) 121 | 122 | 123 | # ========================= 124 | # | Command Actions | 125 | # ========================= 126 | 127 | list_cmd = f"{PREFIX} list" 128 | 129 | 130 | # Errors 131 | def cmd_error(source: CommandSource, exception: CommandError): 132 | if isinstance(exception, RequirementNotMet): 133 | if exception.has_custom_reason(): 134 | source.reply(exception.get_reason().set_color(RColor.red)) 135 | return 136 | source.reply( 137 | tr("mcd_task.cmd_error").set_color(RColor.red).h( 138 | tr("mcd_task.get_help")).c(RAction.run_command, "{} help".format(PREFIX)) 139 | ) 140 | 141 | 142 | def task_not_found(source: CommandSource): 143 | source.reply(tr("mcd_task.task_not_found").h(tr("mcd_task.task_not_found_hover", list_cmd)).set_color(RColor.red).c( 144 | RAction.run_command, list_cmd 145 | )) 146 | 147 | 148 | def task_already_exist(source: CommandSource): 149 | source.reply(tr("task_already_exist").set_color(RColor.red).c(RAction.run_command, f'{PREFIX} list').h( 150 | tr("task_not_found_hover", PREFIX) 151 | )) 152 | 153 | 154 | def illegal_call(source: CommandSource): 155 | source.reply(tr('illegal_call').set_color(RColor.red)) 156 | 157 | 158 | # Info 159 | def info_player(source: CommandSource, name: str) -> None: 160 | tasks = list(GlobalVariables.task_manager.responsible_manager[name]) 161 | text = [ 162 | tr("player_tasks_title", name, str(len(tasks))).set_color(RColor.green).set_styles(RStyle.bold), 163 | ] 164 | for task in tasks: 165 | text.append(title_text(task, display_full_path=True, display_not_empty_mark=True)) 166 | source.reply(RText.join('\n', text)) 167 | 168 | 169 | def info_task(source: CommandSource, title: str, headline_override: Union[None, str, RTextBase] = None) -> None: 170 | # Get task instance 171 | try: 172 | target_task = GlobalVariables.task_manager[title] 173 | except TaskNotFound: 174 | task_not_found(source) 175 | if DEBUG_MODE: 176 | raise 177 | return 178 | 179 | # Task detail text 180 | headline = tr("info_task_title") 181 | if isinstance(headline_override, str): 182 | headline = tr(headline_override) 183 | if isinstance(headline_override, RTextBase): 184 | headline = headline_override 185 | headline.set_color(RColor.green).set_styles(RStyle.bold) 186 | task_title_text = title_text(target_task, display_full_path=True, with_edit_button=True) 187 | info_desc = info_elements(target_task) 188 | info_ddl = info_elements(target_task, EditButtonType.deadline) 189 | info_priority = info_elements(target_task, EditButtonType.priority) 190 | info_res = info_responsibles(target_task) 191 | info_sub = info_sub_tasks(target_task) 192 | 193 | # Show task text 194 | source.reply(RText.join('\n', [headline, task_title_text, info_desc, info_ddl, info_priority, info_res, info_sub])) 195 | 196 | 197 | # Others 198 | def list_task(source: CommandSource): 199 | headline = tr('list_task_title').set_styles(RStyle.bold).set_color( 200 | RColor.green) + ' ' + add_task_button() 201 | task_list_text = [] 202 | for task in GlobalVariables.task_manager.sorted_sub_tasks: 203 | task_list_text.append(title_text(task, display_not_empty_mark=True)) 204 | task_list_text = RTextBase.join('\n', task_list_text) 205 | text = [headline, task_list_text] 206 | source.reply(RText.join('\n', text)) 207 | 208 | 209 | def set_task_deadline(source: CommandSource, titles: str, ddl: str) -> None: 210 | deadline = float(time.time()) + float(ddl) * 3600 * 24 211 | GlobalVariables.task_manager.set_deadline(TitleList(titles), deadline) 212 | info_task(source, titles, headline_override=tr("ddl_set")) 213 | GlobalVariables.log( 214 | f"{source_name(source)} set task {titles} deadline to {formatted_time(deadline, locale='en_us')}" 215 | ) 216 | 217 | 218 | def clear_task_deadline(source: CommandSource, titles: str): 219 | GlobalVariables.task_manager.set_deadline(TitleList(titles), 0) 220 | info_task(source, titles, headline_override=tr('ddl_cleared')) 221 | GlobalVariables.log( 222 | f"{source_name(source)} removed task {titles} deadline" 223 | ) 224 | 225 | 226 | def task_overview(source: CommandSource): 227 | GlobalVariables.debug('Running overview...') 228 | headline = tr('overview_headline').set_styles(RStyle.bold).set_color(RColor.green) + ' ' + add_task_button() 229 | 230 | # Get task instances 231 | deadline_approaching = GlobalVariables.task_manager.seek_for_item_with_deadline_approaching() 232 | GlobalVariables.debug(deadline_approaching) 233 | max_length = GlobalVariables.config.overview_maximum_task_amount 234 | priority_amount = max_length - len(deadline_approaching) 235 | with_priorities = [] 236 | if priority_amount > 0: 237 | with_priorities = GlobalVariables.task_manager.seek_for_item_with_priority() 238 | 239 | # Found no matched task handle 240 | if len(deadline_approaching) == 0 and len(with_priorities) == 0: 241 | task_text = tr('no_priority').set_color(RColor.yellow) 242 | # Organize task texts 243 | else: 244 | task_texts = {} 245 | for task in deadline_approaching: 246 | if len(task_texts) >= GlobalVariables.config.overview_maximum_task_amount: 247 | break 248 | task_texts[task.titles] = RText('[!] ', RColor.red).h( 249 | tr('date_approaching', formatted_time(task.deadline)) 250 | ) + title_text(task, display_full_path=True) 251 | for task in with_priorities: 252 | if len(task_texts) >= GlobalVariables.config.overview_maximum_task_amount: 253 | break 254 | if task.title not in task_texts.keys(): 255 | task_texts[task.titles] = RText('[!] ', RColor.gold).h( 256 | tr('has_a_high_priority', task.priority) 257 | ) + title_text(task, display_full_path=True) 258 | task_text = RTextBase.join('\n', task_texts.values()) 259 | 260 | help_message = tr('overview_help', PREFIX).set_translator(GlobalVariables.htr) 261 | 262 | source.reply(RTextBase.join('\n', [headline, task_text, help_message])) 263 | 264 | 265 | def set_task_priority(source: CommandSource, titles: str, priority: Optional[int] = None): 266 | GlobalVariables.task_manager.set_priority(TitleList(titles), priority) 267 | info_task(source, titles, headline_override=tr('priority_set')) 268 | GlobalVariables.log(f"{source_name(source)} set task {titles} priority to {priority}") 269 | 270 | 271 | def reload_self(source: CommandSource): 272 | server = GlobalVariables.server 273 | server.reload_plugin(server.get_self_metadata().id) 274 | source.reply(tr('reloaded')) 275 | 276 | 277 | def show_help(source: CommandSource): 278 | meta = GlobalVariables.server.get_self_metadata() 279 | ver = '.'.join(map(lambda x: str(x), meta.version.component)) 280 | if meta.version.pre is not None: 281 | ver += '-' + str(meta.version.pre) 282 | source.reply(tr('help_msg', pre=PREFIX, name=meta.name, ver=ver).set_translator(GlobalVariables.htr)) 283 | 284 | 285 | def add_task(source: CommandSource, titles: str, desc: str = ''): 286 | titles = TitleList(titles) 287 | titles_for_text = titles.copy() 288 | 289 | GlobalVariables.task_manager.add_task(titles, desc=desc) 290 | info_task(source, title=str(titles_for_text), headline_override=tr("new_task_created")) 291 | GlobalVariables.log(f"{source_name(source)} created new task named {str(titles_for_text)}") 292 | 293 | 294 | def all_tasks_detail(source: CommandSource): 295 | task_details = [tr("detailed_info_task_title").set_color(RColor.green).set_styles(RStyle.bold) + ' ' + 296 | add_task_button()] 297 | for task in GlobalVariables.task_manager.sorted_sub_tasks: 298 | task_details.append(title_text(task, include_sub=False, display_not_empty_mark=True)) 299 | if len(task.sub_tasks) > 0: 300 | task_details.append(sub_task_title_text(task, indent=8)) 301 | source.reply(RTextBase.join('\n', task_details)) 302 | 303 | 304 | def remove_task(source: CommandSource, titles: str): 305 | GlobalVariables.task_manager.delete_task(TitleList(titles)) 306 | source.reply(tr("mcd_task.deleted_task", "§e{}§r".format(titles))) 307 | GlobalVariables.log(f"{source_name(source)} deleted task {titles}") 308 | 309 | 310 | def rename_task(source: CommandSource, old_titles: str, new_title: str) -> None: 311 | if '.' in list(new_title): 312 | source.reply(tr("mcd_task.illegal_title_with_dot", new_title)) 313 | return 314 | GlobalVariables.task_manager.rename_task(TitleList(old_titles), new_title) 315 | new_titles = TitleList(old_titles) 316 | new_titles.pop_tail() 317 | new_titles.append(new_title) 318 | info_task(source, title=str(new_titles), headline_override=tr("mcd_task.task_renamed", old_titles)) 319 | GlobalVariables.log(f"{source_name(source)} renamed {old_titles} to {str(new_titles)}") 320 | 321 | 322 | def edit_desc(source: CommandSource, titles: str, desc: str) -> None: 323 | GlobalVariables.task_manager.edit_desc(TitleList(titles), desc) 324 | info_task(source, title=titles, headline_override='changed_desc_title') 325 | GlobalVariables.log(f"{source_name(source)} changed task {titles} description to {desc}") 326 | 327 | 328 | def set_done(source: CommandSource, titles: str) -> None: 329 | GlobalVariables.task_manager.done_task(TitleList(titles)) 330 | info_task(source, title=titles, headline_override='done_task_title') 331 | GlobalVariables.log(f"{source_name(source)} marked task {titles} as done") 332 | 333 | 334 | def set_undone(source: CommandSource, titles: str) -> None: 335 | GlobalVariables.task_manager.undone_task(TitleList(titles)) 336 | info_task(source, title=titles, headline_override='undone_task_title') 337 | GlobalVariables.log(f"{source_name(source)} marked task {titles} as undone") 338 | 339 | 340 | def set_responsible(source: CommandSource, titles: str, players: Optional[str] = None) -> None: 341 | if players is None: 342 | if isinstance(source, PlayerCommandSource): 343 | players = source.player 344 | else: 345 | illegal_call(source) 346 | return 347 | players = players.split(' ') 348 | num = GlobalVariables.task_manager.set_responsible(TitleList(titles), *players) 349 | info_task(source, titles, headline_override=tr("mcd_task.added_responsibles_title", num)) 350 | GlobalVariables.log(f"{source_name(source)} added responsibles for task {str(titles)}: {str(players)}") 351 | 352 | 353 | def rm_responsible(source: CommandSource, titles: str, players: Optional[str] = None) -> None: 354 | if players is None: 355 | if isinstance(source, PlayerCommandSource): 356 | players = source.player 357 | else: 358 | illegal_call(source) 359 | return 360 | players = players.split(' ') 361 | removed = GlobalVariables.task_manager.rm_responsible(TitleList(titles), *players) 362 | num = len(removed) 363 | info_task(source, titles, headline_override=tr("mcd_task.removed_responsibles_title", num)) 364 | GlobalVariables.log(f"{source_name(source)} removed responsibles for task {str(titles)}: {str(players)}") 365 | 366 | 367 | def rm_all_responsible(source: CommandSource, titles: str): 368 | players = GlobalVariables.task_manager.responsible_manager.get_responsibles(titles) 369 | rm_responsible(source, titles, players=' '.join(players)) 370 | 371 | 372 | def inherit_responsible(info: Info, old_name: str, new_name: str, debug=False): 373 | manager = GlobalVariables.task_manager.responsible_manager 374 | if old_name in manager.player_work.keys(): 375 | manager.rename_player(old_name, new_name) 376 | num = len(manager[new_name]) 377 | info.get_server().tell(new_name, tr("mcd_task.on_player_renamed", num)) 378 | GlobalVariables.logger.debug(tr("mcd_task.on_player_renamed", num), no_check=debug) 379 | GlobalVariables.log(f"Detected player rename {old_name} -> {new_name}. Inherited {num} task(s)") 380 | 381 | 382 | def task_timed_out(server: PluginServerInterface, player: str, player_tasks: List[Task]): 383 | text = [tr("on_player_joined", len(player_tasks)).set_color(RColor.red).set_styles(RStyle.bold)] 384 | for t in player_tasks: 385 | text.append(title_text(t, display_full_path=True, display_not_empty_mark=True)) 386 | server.tell(player, RTextBase.join('\n', text)) 387 | --------------------------------------------------------------------------------