pymud 0.20.4__py3-none-any.whl → 0.21.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pymud/logger.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os, re, datetime, threading, pathlib
2
2
  from queue import SimpleQueue, Empty
3
3
  from pathlib import Path
4
-
4
+ from .settings import Settings
5
5
  class Logger:
6
6
  """
7
7
  PyMUD 的记录器类型,可用于会话中向文件记录数据。记录文件保存在当前目录下的 log 子目录中
@@ -57,6 +57,10 @@ class Logger:
57
57
  now = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
58
58
  filename = f"{self.name}.{now}.log"
59
59
 
60
+ else:
61
+ raise ValueError(Settings.gettext("exception_logmode_error", self._mode))
62
+
63
+
60
64
  logdir = Path.cwd().joinpath('log')
61
65
  if not logdir.exists() or not logdir.is_dir():
62
66
  logdir.mkdir()
@@ -70,8 +74,9 @@ class Logger:
70
74
 
71
75
  else:
72
76
  self._queue.put_nowait(None)
73
- self._thread.join()
74
- self._thread = None
77
+ if self._thread:
78
+ self._thread.join()
79
+ self._thread = None
75
80
  self._closeFile()
76
81
 
77
82
  self._enabled = enabled
@@ -130,7 +135,7 @@ class Logger:
130
135
  The thread will terminate if it sees a sentinel object in the queue.
131
136
  """
132
137
  newline = True
133
- while True:
138
+ while self._stream:
134
139
  try:
135
140
  data = self._queue.get(block = True)
136
141
  if data:
pymud/main.py CHANGED
@@ -1,26 +1,32 @@
1
- import os, sys, json, platform, shutil, logging, argparse
1
+ import os, sys, json, platform, shutil, logging, argparse, locale
2
2
  from pathlib import Path
3
3
  from .pymud import PyMudApp
4
4
  from .settings import Settings
5
5
 
6
6
  CFG_TEMPLATE = {
7
+ "language" : "chs", # 语言设置,默认为简体中文
8
+
7
9
  "client": {
8
10
  "buffer_lines" : 5000, # 保留缓冲行数
9
11
 
10
12
  "interval" : 10, # 在自动执行中,两次命令输入中的间隔时间(ms)
11
13
  "auto_connect" : True, # 创建会话后,是否自动连接
12
14
  "auto_reconnect" : False, # 在会话异常断开之后,是否自动重连
15
+ "reconnect_wait" : 15, # 自动重连等待的时间(秒数)
13
16
  "var_autosave" : True, # 断开时自动保存会话变量
14
17
  "var_autoload" : True, # 初始化时自动加载会话变量
15
18
 
16
- "echo_input" : False,
17
19
  "beautify" : True, # 专门为解决控制台下PKUXKX字符画对不齐的问题
20
+ "history_records" : 500,
18
21
 
22
+ "status_divider" : False, # 是否显示状态栏的分隔线
19
23
  "status_display" : 1, # 状态窗口显示情况设置,0-不显示,1-显示在下方,2-显示在右侧
20
24
  "status_height" : 4, # 下侧状态栏的高度
21
25
  "status_width" : 30, # 右侧状态栏的宽度
26
+
22
27
 
23
28
  },
29
+
24
30
  "sessions" : {
25
31
  "pkuxkx" : {
26
32
  "host" : "mud.pkuxkx.net",
@@ -41,50 +47,123 @@ CFG_TEMPLATE = {
41
47
  }
42
48
  }
43
49
 
50
+ def detect_system_language():
51
+ """
52
+ 检测系统语言,返回中文或英文"
53
+ """
54
+ lang = "chs"
55
+ try:
56
+ value = locale.getlocale()[0]
57
+ if value and (value.lower().startswith("zh") or value.lower().startswith("chinese")): # 中文
58
+ lang = "chs"
59
+ else:
60
+ lang = "eng"
61
+ except Exception as e:
62
+ # default is chs
63
+ pass
64
+
65
+ return lang
66
+
44
67
  def init_pymud_env(args):
45
- print(f"欢迎使用PyMUD, 版本{Settings.__version__}. 使用PyMUD时, 建议建立一个新目录(任意位置),并将自己的脚本以及配置文件放到该目录下.")
46
- print("即将开始为首次运行初始化环境...")
47
-
48
- dir = args.dir
49
- if dir:
50
- print(f"你已经指定了创建脚本的目录为 {args.dir}")
51
- dir = Path(dir)
52
- else:
53
- dir = Path.home().joinpath('pkuxkx')
54
-
55
- system = platform.system().lower()
56
- dir_enter = input(f"检测到当前系统为 {system}, 请指定游戏脚本的目录(若目录不存在会自动创建),直接回车表示使用默认值 [{dir}]:")
57
- if dir_enter:
58
- dir = Path(dir_enter)
59
-
60
- if dir.exists() and dir.is_dir():
61
- print(f'检测到给定目录 {dir} 已存在,切换至此目录...')
62
- else:
63
- print(f'检测到给定目录 {dir} 不存在,正在创建并切换至目录...')
64
- dir.mkdir()
65
-
66
- os.chdir(dir)
68
+ system = "unknown"
69
+ lang = detect_system_language()
70
+ if lang == "chs":
71
+ print(f"欢迎使用PyMUD, 版本{Settings.__version__}. 使用PyMUD时, 建议建立一个新目录(任意位置),并将自己的脚本以及配置文件放到该目录下.")
72
+ print("即将开始为首次运行初始化环境...")
73
+
74
+ dir = args.dir
75
+ if dir:
76
+ if dir == ".":
77
+ dir_msg = "当前目录"
78
+ else:
79
+ dir_msg = f": {dir}"
80
+ print(f"你已经指定了创建脚本的目录为{dir_msg}")
81
+ dir = Path(dir)
82
+ else:
83
+ dir = Path.home().joinpath('pkuxkx')
84
+
85
+ system = platform.system().lower()
86
+ dir_enter = input(f"检测到当前系统为 {system}, 请指定游戏脚本的目录(若目录不存在会自动创建),直接回车表示使用默认值 [{dir}]:")
87
+ if dir_enter:
88
+ dir = Path(dir_enter)
89
+
90
+ if dir.exists() and dir.is_dir():
91
+ print(f'检测到给定目录 {dir} 已存在,切换至此目录...')
92
+ else:
93
+ print(f'检测到给定目录 {dir} 不存在,正在创建并切换至目录...')
94
+ dir.mkdir()
95
+
96
+ os.chdir(dir)
67
97
 
68
- if os.path.exists('pymud.cfg'):
69
- print(f'检测到脚本目录下已存在pymud.cfg文件,将直接使用此文件进入PyMUD...')
70
- else:
71
- print(f'检测到脚本目录下不存在pymud.cfg文件,将使用默认内容创建该配置文件...')
72
- with open('pymud.cfg', mode = 'x') as fp:
73
- fp.writelines(json.dumps(CFG_TEMPLATE, indent = 4))
74
-
75
- if not os.path.exists('examples.py'):
76
- from pymud import pkuxkx
77
- module_dir = pkuxkx.__file__
78
- shutil.copyfile(module_dir, 'examples.py')
79
- print(f'已将样例脚本拷贝至脚本目录,并加入默认配置文件')
80
-
81
- print(f"后续可自行修改 {dir} 目录下的 pymud.cfg 文件以进行配置。")
82
- if system == "windows":
83
- print(f"后续运行PyMUD, 请在 {dir} 目录下键入命令: python -m pymud")
84
- else:
85
- print(f"后续运行PyMUD, 请在 {dir} 目录下键入命令: python3 -m pymud")
98
+ if os.path.exists('pymud.cfg'):
99
+ print(f'检测到脚本目录下已存在pymud.cfg文件,将直接使用此文件进入PyMUD...')
100
+ else:
101
+ print(f'检测到脚本目录下不存在pymud.cfg文件,将使用默认内容创建该配置文件...')
102
+ with open('pymud.cfg', mode = 'x') as fp:
103
+ fp.writelines(json.dumps(CFG_TEMPLATE, indent = 4))
104
+
105
+ if not os.path.exists('examples.py'):
106
+ from pymud import pkuxkx
107
+ module_dir = pkuxkx.__file__
108
+ shutil.copyfile(module_dir, 'examples.py')
109
+ print(f'已将样例脚本拷贝至脚本目录,并加入默认配置文件')
110
+
111
+ print(f"后续可自行修改 {dir} 目录下的 pymud.cfg 文件以进行配置。")
112
+ if system == "windows":
113
+ print(f"后续运行PyMUD, 请在 {dir} 目录下键入命令: python -m pymud,或直接使用快捷命令 pymud")
114
+ else:
115
+ print(f"后续运行PyMUD, 请在 {dir} 目录下键入命令: python3 -m pymud,或直接使用快捷命令 pymud")
86
116
 
87
- input('所有内容已初始化完毕, 请按回车进入PyMUD.')
117
+ input('所有内容已初始化完毕, 请按回车进入PyMUD.')
118
+
119
+ else:
120
+ print(f"Welcome to PyMUD, version {Settings.__version__}. When using pymud, it is suggested that a new folder should be created (in any place), and the cfg configuration and all the scripts have been placed in the directory.")
121
+ print("Starting to initialize the environment for the first time...")
122
+ dir = args.dir
123
+ if dir:
124
+ if dir == ".":
125
+ dir_msg = "current directory"
126
+ else:
127
+ dir_msg = f": {dir}"
128
+ print(f"You have specified the directory to create the script as {dir_msg}")
129
+ dir = Path(dir)
130
+ else:
131
+ dir = Path.home().joinpath('pkuxkx')
132
+
133
+ system = platform.system().lower()
134
+ dir_enter = input(f"Detected the current system is {system}, please specify the directory of the game script (if the directory does not exist, it will be automatically created), press Enter to use the default value [{dir}]:")
135
+ if dir_enter:
136
+ dir = Path(dir_enter)
137
+
138
+ if dir.exists() and dir.is_dir():
139
+ print(f'Detected that the given directory {dir} already exists, switching to this directory...')
140
+ else:
141
+ print(f'Detected that the given directory {dir} does not exist, creating and switching to the directory...')
142
+ dir.mkdir()
143
+
144
+ os.chdir(dir)
145
+
146
+ if os.path.exists('pymud.cfg'):
147
+ print(f'Detected that the pymud.cfg file already exists in the script directory, entering PyMUD directly using this file...')
148
+ else:
149
+ print(f'Detected that the pymud.cfg file does not exist in the script directory, creating the configuration file using the default content...')
150
+ with open('pymud.cfg', mode = 'x') as fp:
151
+ CFG_TEMPLATE["language"] = "eng"
152
+ fp.writelines(json.dumps(CFG_TEMPLATE, indent = 4))
153
+
154
+ if not os.path.exists('examples.py'):
155
+ from pymud import pkuxkx
156
+ module_dir = pkuxkx.__file__
157
+ shutil.copyfile(module_dir, 'examples.py')
158
+ print(f'The sample script has been copied to the script directory and added to the default configuration file')
159
+
160
+ print(f"Afterwards, you can modify the pymud.cfg file in the {dir} directory for configuration.")
161
+ if system == "windows":
162
+ print(f"Afterwards, please type the command 'python -m pymud' in the {dir} directory to run PyMUD, or use the shortcut command pymud")
163
+ else:
164
+ print(f"Afterwards, please type the command 'python3 -m pymud' in the {dir} directory to run PyMUD, or use the shortcut command pymud")
165
+
166
+ input('Press Enter to enter PyMUD.')
88
167
 
89
168
  startApp(args)
90
169
 
@@ -123,7 +202,7 @@ def main():
123
202
  subparsers = parser.add_subparsers(help = 'init用于初始化运行环境')
124
203
 
125
204
  par_init = subparsers.add_parser('init', description = '初始化pymud运行环境, 包括建立脚本目录, 创建默认配置文件, 创建样例脚本等.')
126
- par_init.add_argument('-d', '--dir', dest = 'dir', metavar = 'dir', type = str, default = '', help = '指定构建脚本目录的名称, 不指定时会根据操作系统选择不同默认值')
205
+ par_init.add_argument('-d', '--dir', dest = 'dir', metavar = 'dir', type = str, default = '.', help = '指定构建脚本目录的名称, 不指定时会根据操作系统选择不同默认值')
127
206
  par_init.set_defaults(func = init_pymud_env)
128
207
 
129
208
  parser.add_argument('-d', '--debug', dest = 'debug', action = 'store_true', default = False, help = '指定以调试模式进入PyMUD。此时,系统log等级将设置为logging.NOTSET, 所有log数据均会被记录。默认不启用。')
pymud/modules.py CHANGED
@@ -1,9 +1,21 @@
1
1
 
2
- import importlib, importlib.util
3
- from abc import ABC, ABCMeta
2
+ import importlib, importlib.util, traceback
4
3
  from typing import Any
5
- from .objects import BaseObject, Command
4
+ from .settings import Settings
5
+ from .extras import DotDict
6
+ from .decorators import exception, async_exception, PymudDecorator, print_exception
6
7
 
8
+ class PymudMeta(type):
9
+ def __new__(cls, name, bases, attrs):
10
+ decorator_funcs = {}
11
+ for name, value in attrs.items():
12
+ if hasattr(value, "__pymud_decorators__"):
13
+ decorator_funcs[value.__name__] = getattr(value, "__pymud_decorators__", [])
14
+
15
+ attrs["_decorator_funcs"] = decorator_funcs
16
+
17
+ return super().__new__(cls, name, bases, attrs)
18
+
7
19
  class ModuleInfo:
8
20
  """
9
21
  模块管理类。对加载的模块文件进行管理。该类型由Session类进行管理,无需人工创建和干预。
@@ -15,7 +27,9 @@ class ModuleInfo:
15
27
 
16
28
  """
17
29
  def __init__(self, module_name: str, session):
18
- self.session = session
30
+ from .session import Session
31
+ if isinstance(session, Session):
32
+ self.session = session
19
33
  self._name = module_name
20
34
  self._ismainmodule = False
21
35
  self.load()
@@ -33,14 +47,19 @@ class ModuleInfo:
33
47
  if (attr_name == "Configuration") or issubclass(attr, IConfig):
34
48
  try:
35
49
  self._config[f"{self.name}.{attr_name}"] = attr(self.session, reload = reload)
36
- self.session.info(f"配置对象 {self.name}.{attr_name} {'重新' if reload else ''}创建成功.")
50
+ if not reload:
51
+ self.session.info(Settings.gettext("configuration_created", self.name, attr_name))
52
+ else:
53
+ self.session.info(Settings.gettext("configuration_recreated", self.name, attr_name))
54
+
37
55
  except Exception as e:
38
56
  result = False
39
- self.session.error(f"配置对象 {self.name}.{attr_name} 创建失败. 错误信息为: {e}")
57
+ self.session.error(Settings.gettext("configuration_fail", self.name, attr_name, e))
40
58
  self._ismainmodule = (self._config != {})
41
59
  return result
42
60
 
43
61
  def _unload(self):
62
+ from .objects import BaseObject, Command
44
63
  for key, config in self._config.items():
45
64
  if isinstance(config, Command):
46
65
  # Command 对象在从会话中移除时,自动调用其 unload 系列方法,因此不能产生递归
@@ -65,21 +84,21 @@ class ModuleInfo:
65
84
  def load(self):
66
85
  "加载模块内容"
67
86
  if self._load():
68
- self.session.info(f"{'' if self.ismainmodule else ''}配置模块 {self.name} 加载完成.")
87
+ self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('load_ok')}")
69
88
  else:
70
- self.session.error(f"{'' if self.ismainmodule else ''}配置模块 {self.name} 加载失败.")
89
+ self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('load_fail')}")
71
90
 
72
91
  def unload(self):
73
92
  "卸载模块内容"
74
93
  self._unload()
75
94
  self._loaded = False
76
- self.session.info(f"{'' if self.ismainmodule else ''}配置模块 {self.name} 卸载完成.")
95
+ self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('unload_ok')}")
77
96
 
78
97
  def reload(self):
79
98
  "模块文件更新后调用,重新加载已加载的模块内容"
80
99
  self._unload()
81
100
  self._load(reload = True)
82
- self.session.info(f"{'' if self.ismainmodule else ''}配置模块 {self.name} 重新加载完成.")
101
+ self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('reload_ok')}")
83
102
 
84
103
  @property
85
104
  def name(self):
@@ -101,22 +120,69 @@ class ModuleInfo:
101
120
  "只读属性,区分是否主模块(即包含具体config的模块)"
102
121
  return self._ismainmodule
103
122
 
104
- class IConfig(metaclass = ABCMeta):
123
+ class IConfigBase(metaclass = PymudMeta):
105
124
  """
106
- 用于提示PyMUD应用是否自动创建该配置类型的基础类(模拟接口)。
107
-
108
- 继承 IConfig 类型让应用自动管理该类型,唯一需要的是,构造函数中,仅存在一个必须指定的参数 Session
109
-
110
- 在应用自动创建 IConfig 实例时,除 session 参数外,还会传递一个 reload 参数 (bool类型),表示是首次加载还是重新加载特性。
111
- 可以从kwargs 中获取该参数,并针对性的设计相应代码。例如,重新加载相关联的其他模块等。
125
+ 用于支持对装饰器写法对象进行管理的基础类。
126
+ 该类型相当于原来的IConfig类,唯一区别时,模块加载时,不会对本类型创建实例对象。
127
+ 主要用于对插件中定义的Command提供装饰器写法支持,因为这些Command是在会话构建时创建,因此不能在模块加载时自动创建,也就不能继承自IConfig。
112
128
  """
113
129
  def __init__(self, session, *args, **kwargs):
114
- self.session = session
130
+ from .session import Session
131
+ from .objects import Alias, Trigger, Timer, GMCPTrigger
132
+ if isinstance(session, Session):
133
+ self.session = session
134
+ self.__inline_objects__ = DotDict()
135
+
136
+ if hasattr(self, "_decorator_funcs"):
137
+ deco_funcs = getattr(self, "_decorator_funcs")
138
+ for func_name, decorators in deco_funcs.items():
139
+ func = getattr(self, func_name)
140
+ for deco in decorators:
141
+ if isinstance(deco, PymudDecorator):
142
+ if deco.type == "alias":
143
+ #patterns = deco.kwargs.pop("patterns")
144
+ ali = Alias(self.session, *deco.args, **deco.kwargs, onSuccess = func)
145
+ self.__inline_objects__[ali.id] = ali
146
+
147
+ elif deco.type == "trigger":
148
+ #patterns = deco.kwargs.pop("patterns")
149
+ tri = Trigger(self.session, *deco.args, **deco.kwargs, onSuccess = func)
150
+ self.__inline_objects__[tri.id] = tri
151
+
152
+ elif deco.type == "timer":
153
+ tim = Timer(self.session, *deco.args, **deco.kwargs, onSuccess = func)
154
+ self.__inline_objects__[tim.id] = tim
155
+
156
+ elif deco.type == "gmcp":
157
+ gmcp = GMCPTrigger(self.session, name = deco.kwargs.get("id"), *deco.args, **deco.kwargs, onSuccess = func)
158
+ self.__inline_objects__[gmcp.id] = gmcp
115
159
 
116
160
  def __unload__(self):
161
+ from .objects import BaseObject
117
162
  if self.session:
118
- self.session.delObject(self)
163
+ self.session.delObjects(self.__inline_objects__)
164
+ if isinstance(self, BaseObject):
165
+ self.session.delObject(self)
119
166
 
167
+ @property
168
+ def objs(self) -> DotDict:
169
+ "返回内联自动创建的对象字典"
170
+ return self.__inline_objects__
171
+
172
+ def obj(self, obj_id: str):
173
+ "根据对象ID返回内联自动创建的对象"
174
+ return self.__inline_objects__.get(obj_id, None) # type: ignore
175
+
176
+ class IConfig(IConfigBase):
177
+ """
178
+ 用于提示PyMUD应用是否自动创建该配置类型的基础类。
179
+
180
+ 继承 IConfig 类型让应用自动管理该类型,唯一需要的是,构造函数中,仅存在一个必须指定的参数 Session。
181
+
182
+ 在应用自动创建 IConfig 实例时,除 session 参数外,还会传递一个 reload 参数 (bool类型),表示是首次加载还是重新加载特性。
183
+ 可以从kwargs 中获取该参数,并针对性的设计相应代码。例如,重新加载相关联的其他模块等。
184
+ """
185
+
120
186
  class Plugin:
121
187
  """
122
188
  插件管理类。对加载的插件文件进行管理。该类型由PyMudApp进行管理,无需人工创建。
@@ -137,17 +203,17 @@ class Plugin:
137
203
  "加载/重新加载插件对象"
138
204
  #del self.modspec, self.mod
139
205
  self.modspec = importlib.util.spec_from_file_location(self._plugin_file[:-3], self._plugin_loc)
140
- self.mod = importlib.util.module_from_spec(self.modspec)
141
- self.modspec.loader.exec_module(self.mod)
206
+ if self.modspec and self.modspec.loader:
207
+ self.mod = importlib.util.module_from_spec(self.modspec)
208
+ self.modspec.loader.exec_module(self.mod)
142
209
 
143
- # self._app_init = self.mod.__dict__["PLUGIN_PYMUD_START"]
144
- # self._session_create = self.mod.__dict__["PLUGIN_SESSION_CREATE"]
145
- # self._session_destroy = self.mod.__dict__["PLUGIN_SESSION_DESTROY"]
146
- # self._app_destroy = self.mod.__dict__["PLUGIN_PYMUD_DESTROY"]
147
- self._app_init = self._load_mod_function("PLUGIN_PYMUD_START")
148
- self._session_create = self._load_mod_function("PLUGIN_SESSION_CREATE")
149
- self._session_destroy = self._load_mod_function("PLUGIN_SESSION_DESTROY")
150
- self._app_destroy = self._load_mod_function("PLUGIN_PYMUD_DESTROY")
210
+ self._app_init = self._load_mod_function("PLUGIN_PYMUD_START")
211
+ self._session_create = self._load_mod_function("PLUGIN_SESSION_CREATE")
212
+ self._session_destroy = self._load_mod_function("PLUGIN_SESSION_DESTROY")
213
+ self._app_destroy = self._load_mod_function("PLUGIN_PYMUD_DESTROY")
214
+
215
+ else:
216
+ raise FileNotFoundError(Settings.gettext("exception_plugin_file_not_found", self._plugin_file))
151
217
 
152
218
  def _load_mod_function(self, func_name):
153
219
  # 定义一个默认函数,当插件文件中未定义指定名称的函数时,返回该函数
@@ -191,7 +257,11 @@ class Plugin:
191
257
 
192
258
  :param session: 新创建的会话对象实例
193
259
  """
194
- self._session_create(session)
260
+ try:
261
+ self._session_create(session)
262
+ except Exception as e:
263
+ print_exception(session, e)
264
+
195
265
 
196
266
  def onSessionDestroy(self, session):
197
267
  """
@@ -199,7 +269,10 @@ class Plugin:
199
269
 
200
270
  :param session: 所关闭的会话对象实例
201
271
  """
202
- self._session_destroy(session)
272
+ try:
273
+ self._session_destroy(session)
274
+ except Exception as e:
275
+ print_exception(session, e)
203
276
 
204
277
  def onAppDestroy(self, app):
205
278
  """