Feature/rust console #17

Merged
shenjackyuanjie merged 6 commits from feature/rust_console into main 2023-05-14 20:59:53 +08:00
12 changed files with 218 additions and 128 deletions

View File

@ -5,6 +5,7 @@
# ------------------------------- # -------------------------------
import sys import sys
import time
import importlib import importlib
import traceback import traceback
import contextlib import contextlib

View File

@ -210,8 +210,6 @@ class ClientWindow(Window):
def start_game(self) -> None: def start_game(self) -> None:
self.set_icon(pyglet.image.load('./textures/icon.png')) self.set_icon(pyglet.image.load('./textures/icon.png'))
self.run_input = True
self.read_input()
try: try:
pyglet.app.event_loop.run(1 / self.main_config['runtime']['fps']) pyglet.app.event_loop.run(1 / self.main_config['runtime']['fps'])
except KeyboardInterrupt: except KeyboardInterrupt:
@ -221,22 +219,6 @@ class ClientWindow(Window):
self.dispatch_event("on_close") self.dispatch_event("on_close")
sys.exit(0) sys.exit(0)
@new_thread('window read_input', daemon=True)
def read_input(self):
self.logger.debug('read_input start')
while self.run_input:
try:
get = input(">")
except (EOFError, KeyboardInterrupt):
self.run_input = False
break
if get in ('', ' ', '\n', '\r'):
continue
if get == 'stop':
self.run_input = False
self.command_list.append(get)
self.logger.debug('read_input end')
@new_thread('window save_info') @new_thread('window save_info')
def save_info(self): def save_info(self):
self.logger.info(tr().client.config.save.start()) self.logger.info(tr().client.config.save.start())
@ -266,10 +248,8 @@ class ClientWindow(Window):
@_call_screen_after @_call_screen_after
def on_draw(self, *dt): def on_draw(self, *dt):
if self.command_list: while command := self.game.console.get_command():
for command in self.command_list: self.on_command(line.CommandText(command))
self.on_command(line.CommandText(command))
self.command_list.pop(0)
pyglet.gl.glClearColor(0.1, 0, 0, 0.0) pyglet.gl.glClearColor(0.1, 0, 0, 0.0)
self.clear() self.clear()
self.draw_update(float(self.SPF)) self.draw_update(float(self.SPF))
@ -310,25 +290,26 @@ class ClientWindow(Window):
@_call_screen_after @_call_screen_after
def on_command(self, command: line.CommandText): def on_command(self, command: line.CommandText):
print(command.re_match('/')) print(command.find('/'))
self.logger.info(tr().window.command.text().format(command)) self.logger.info(tr().window.command.text().format(command))
if command.re_match('stop'): if command.find('stop'):
# self.dispatch_event('on_exit') # self.dispatch_event('on_exit')
print("command stop!")
pyglet.app.platform_event_loop.stop() pyglet.app.platform_event_loop.stop()
self.dispatch_event('on_close', 'command') # source = command self.dispatch_event('on_close', 'command') # source = command
elif command.re_match('fps'): elif command.find('fps'):
if command.re_match('log'): if command.find('log'):
self.logger.debug(self.fps_log.fps_list) self.logger.debug(self.fps_log.fps_list)
elif command.re_match('max'): elif command.find('max'):
self.logger.info(self.fps_log.max_fps) self.logger.info(self.fps_log.max_fps)
self.command.push_line(self.fps_log.max_fps, block_line=True) self.command.push_line(self.fps_log.max_fps, block_line=True)
elif command.re_match('min'): elif command.find('min'):
self.logger.info(self.fps_log.min_fps) self.logger.info(self.fps_log.min_fps)
self.command.push_line(self.fps_log.min_fps, block_line=True) self.command.push_line(self.fps_log.min_fps, block_line=True)
elif command.re_match('default'): elif command.find('default'):
self.set_size(int(self.main_config['window_default']['width']), self.set_size(int(self.main_config['window_default']['width']),
int(self.main_config['window_default']['height'])) int(self.main_config['window_default']['height']))
elif command.re_match('lang'): elif command.find('lang'):
try: try:
lang = command.text[5:] lang = command.text[5:]
tr._language = lang tr._language = lang
@ -439,10 +420,13 @@ class ClientWindow(Window):
self.game.dispatch_event('on_close', game=self.game, client=self, source=source) self.game.dispatch_event('on_close', game=self.game, client=self, source=source)
self.logger.info(tr().window.game.stop_get().format(tr().game[source]())) self.logger.info(tr().window.game.stop_get().format(tr().game[source]()))
self.logger.info(tr().window.game.stop()) self.logger.info(tr().window.game.stop())
self.fps_log.check_list = False # self.fps_log.check_list = False
DR_runtime.running = False DR_runtime.running = False
if self.run_input: if self.run_input:
self.run_input = False self.run_input = False
self.save_info() self.save_info()
super().on_close() super().on_close()
self.logger.info(tr().window.game.end()) self.logger.info(tr().window.game.end())
ClientWindow.register_event_type("on_command")

View File

@ -43,7 +43,14 @@ class CommandText:
break break
i += 1 i += 1
def find(self, text: str) -> Union[str, bool]: def find(self, text: str) -> bool:
find = self.text.find(text)
if find != -1:
self.text = self.text[find + len(text):]
return True
return False
def re_find(self, text: str) -> Union[str, bool]:
return finding.group() if (finding := re.match(text, self.text)) else False return finding.group() if (finding := re.match(text, self.text)) else False
def re_match(self, text: str) -> bool: def re_match(self, text: str) -> bool:

View File

@ -11,10 +11,10 @@ github: @shenjackyuanjie
gitee: @shenjackyuanjie gitee: @shenjackyuanjie
""" """
import os
import sys import sys
import time import time
import logging import logging
import traceback
import importlib import importlib
import importlib.util import importlib.util
import logging.config import logging.config
@ -22,59 +22,83 @@ import multiprocessing
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, List, Optional, Dict, TypeVar
if __name__ == '__main__': # been start will not run this if __name__ == '__main__': # been start will not run this
sys.path.append('/bin/libs') sys.path.append('/bin/libs')
sys.path.append('/bin') sys.path.append('/bin')
from Difficult_Rocket import client, server, DR_option, DR_runtime
if TYPE_CHECKING: if TYPE_CHECKING:
from Difficult_Rocket.api.mod import ModInfo from Difficult_Rocket.api.mod import ModInfo
from Difficult_Rocket.crash import write_info_to_cache else:
ModInfo = TypeVar('ModInfo')
from Difficult_Rocket.utils import tools from Difficult_Rocket.utils import tools
from Difficult_Rocket.api.types import Options
from Difficult_Rocket.utils.translate import tr from Difficult_Rocket.utils.translate import tr
from Difficult_Rocket.utils.thread import new_thread
from Difficult_Rocket.crash import write_info_to_cache
from Difficult_Rocket import client, server, DR_option, DR_runtime
class Game: class Console(Options):
def __init__(self): name = 'python stdin console'
# basic config
self.on_python_v_info = sys.version_info running: bool = False
self.on_python_v = sys.version.split(' ')[0]
self.start_time = time.strftime('%Y-%m-%d %H-%M-%S', time.gmtime(time.time())) @new_thread('python console', daemon=True, log_thread=True)
# lang_config def main(self):
self.language = tools.load_file('configs/main.toml', 'runtime')['language'] while self.running:
DR_option.language = self.language try:
# logging config get_str = input('>>>')
log_config = tools.load_file('configs/logger.toml') except (EOFError, KeyboardInterrupt):
file_name = log_config['handlers']['file']['filename'] get_str = 'stop'
del log_config['handlers']['file']['datefmt'] self.caches.append(get_str)
log_config['handlers']['file']['filename'] = f'logs/{file_name.format(self.start_time)}' if get_str == 'stop':
try: self.running = False
logging.config.dictConfig(log_config) break
self.logger = logging.getLogger('main')
except ValueError: # it should be no 'logs/' folder def start(self):
os.mkdir('logs') self.running = True
logging.config.dictConfig(log_config) self.caches: List[str] = []
self.logger = logging.getLogger('main') self.main()
def stop(self):
self.running = False
def get_command(self) -> str:
return self.caches.pop(0)
class Game(Options):
name = 'MainGame'
client: client.Client
server: server.Server
console: Console
console_class: Console = Console
main_config: Dict
logging_config: Dict
logger: logging.Logger
mod_module: List["ModInfo"]
def init_logger(self) -> None:
log_path = self.logging_config['handlers']['file']['filename']
log_path = Path(f"logs/{log_path.format(time.strftime('%Y-%m-%d %H-%M-%S' , time.gmtime(DR_runtime.start_time_ns / 1000_000_000)))}")
mkdir = False
if not Path("logs/").exists():
log_path.mkdir(parents=True)
mkdir = True
self.logging_config['handlers']['file']['filename'] = str(log_path.absolute())
logging.config.dictConfig(self.logging_config)
self.logger = logging.getLogger('main')
if mkdir:
self.logger.info(tr().main.logger.mkdir()) self.logger.info(tr().main.logger.mkdir())
self.logger.info(tr().language_set_to())
self.logger.info(tr().main.logger.created())
# version check
self.log_env()
self.python_version_check()
self.loaded_mods = []
# self.client = client.Client
# self.server = server.Server
self.setup()
def log_env(self) -> None: def init_mods(self) -> None:
cache_steam = StringIO() """验证/加载 mod"""
write_info_to_cache(cache_steam) print(self)
text = cache_steam.getvalue()
self.logger.info(text)
def load_mods(self) -> None:
mods = [] mods = []
mod_path = Path(DR_runtime.mod_path) mod_path = Path(DR_runtime.mod_path)
if not mod_path.exists(): if not mod_path.exists():
@ -114,9 +138,9 @@ class Game:
module.append(mod_class) module.append(mod_class)
self.logger.info(tr().main.mod.load.info().format(mod_class.mod_id, mod_class.version)) self.logger.info(tr().main.mod.load.info().format(mod_class.mod_id, mod_class.version))
except ImportError as e: except ImportError as e:
self.logger.warning(tr().main.mod.load.faild().format(mod, e)) self.logger.warning(tr().main.mod.load.faild.info().format(mod, e))
self.logger.info(tr().main.mod.load.done()) self.logger.info(tr().main.mod.load.done())
self.loaded_mods = module self.mod_module = module
mod_list = [] mod_list = []
for mod in module: for mod in module:
mod_list.append((mod.mod_id, mod.version)) mod_list.append((mod.mod_id, mod.version))
@ -124,30 +148,11 @@ class Game:
self.dispatch_event('on_load', game=self) self.dispatch_event('on_load', game=self)
DR_runtime.DR_Mod_List = mod_list DR_runtime.DR_Mod_List = mod_list
def dispatch_event(self, event_name: str, *args, **kwargs) -> None: def init_console(self) -> None:
for mod in self.loaded_mods: self.console = self.console_class()
if hasattr(mod, event_name): self.console.start()
try:
getattr(mod, event_name)(*args, **kwargs)
except Exception as e:
self.logger.error(tr().main.mod.event.error().format(event_name, e, mod.mod_id))
def setup(self) -> None: def start(self):
self.load_mods()
self.client = client.Client(game=self, net_mode='local')
self.server = server.Server(net_mode='local')
def python_version_check(self) -> None: # best 3.8+ and write at 3.8.10
self.logger.info(f"{tr().main.version.now_on()} {self.on_python_v}")
if self.on_python_v_info[0] == 2:
self.logger.critical(tr().main.version.need3p())
raise SystemError(tr().main.version.need3p())
elif self.on_python_v_info[1] < 8:
warning = tools.name_handler(tr.main.version.best38p())
self.logger.warning(warning)
# @new_thread('main')
def _start(self):
self.server.run() self.server.run()
if DR_option.use_multiprocess: if DR_option.use_multiprocess:
try: try:
@ -161,5 +166,42 @@ class Game:
else: else:
self.client.start() self.client.start()
def start(self) -> None: def dispatch_event(self, event_name: str, *args, **kwargs) -> None:
self._start() """向 mod 分发事件"""
for mod in self.mod_module:
if hasattr(mod, event_name):
try:
getattr(mod, event_name)(*args, **kwargs)
except Exception:
error = traceback.format_exc()
self.logger.error(tr().main.mod.event.error().format(event_name, error, mod.mod_id))
def log_env(self) -> None:
cache_steam = StringIO()
write_info_to_cache(cache_steam)
text = cache_steam.getvalue()
self.logger.info(text)
self.flush_option()
config_cache = self.logging_config.copy()
self.logging_config = {"logging_config": "too long to show"}
self.logger.info(f"\n{self.as_markdown()}")
self.logging_config = config_cache
def setup(self) -> None:
self.client = client.Client(game=self, net_mode='local')
self.server = server.Server(net_mode='local')
def init(self, **kwargs) -> bool:
self.load_file()
self.setup()
self.log_env()
return True
def load_file(self) -> bool:
"""加载文件"""
self.logging_config = tools.load_file('configs/logger.toml')
self.init_logger()
self.init_mods()
self.init_console()
return True

View File

@ -66,9 +66,11 @@ class Options:
if option not in self.cached_options: if option not in self.cached_options:
raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined") raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined")
setattr(self, option, value) setattr(self, option, value)
run_load_file = True
if hasattr(self, 'init'): if hasattr(self, 'init'):
self.init(**kwargs) run_load_file = self.init(**kwargs) # 默认 False/None
if hasattr(self, 'load_file'): run_load_file = not run_load_file
if hasattr(self, 'load_file') and run_load_file:
try: try:
self.load_file() self.load_file()
except Exception: except Exception:
@ -78,8 +80,10 @@ class Options:
if TYPE_CHECKING: if TYPE_CHECKING:
_options: Dict[str, Union[Callable, object]] = {} _options: Dict[str, Union[Callable, object]] = {}
def init(self, **kwargs) -> None: def init(self, **kwargs) -> bool:
""" 如果子类定义了这个函数,则会在 __init__ 之后调用这个函数 """ """ 如果子类定义了这个函数,则会在 __init__ 之后调用这个函数
返回值为 True 则不会调用 load_file 函数
"""
def load_file(self) -> bool: def load_file(self) -> bool:
"""如果子类定义了这个函数,则会在 __init__ 和 init 之后再调用这个函数 """如果子类定义了这个函数,则会在 __init__ 和 init 之后再调用这个函数

View File

@ -27,7 +27,6 @@ level = "DEBUG"
[handlers.file] [handlers.file]
class = "logging.FileHandler" class = "logging.FileHandler"
filename = "{} DR.log" filename = "{} DR.log"
datefmt = "%Y-%m-%d %H:%M:%S"
encoding = "utf-8" encoding = "utf-8"
formatter = "file" formatter = "file"
level = "DEBUG" level = "DEBUG"

View File

@ -59,6 +59,9 @@
- `get_box(&self, part_type: &SR1PartType) -> (f64, f64, f64, f64)` - `get_box(&self, part_type: &SR1PartType) -> (f64, f64, f64, f64)`
- `types::SR1Ship` - `types::SR1Ship`
- `from_file` - `from_file`
- 添加了 `Console_rs`
- 用于使用 Rust 多线程读取 stdin
- Use Rust mutithread to read stdin
### Remove ### Remove
@ -114,6 +117,13 @@
- 修改 返回值 类型+类型注释 - 修改 返回值 类型+类型注释
- Modify the return value type + type annotation - Modify the return value type + type annotation
- `List[Union[List[Tuple[str, Any, Any]], int, Any]]:` -> `Tuple[List[Tuple[str, Union[Any, Type], Type]], int, int, int]` - `List[Union[List[Tuple[str, Any, Any]], int, Any]]:` -> `Tuple[List[Tuple[str, Union[Any, Type], Type]], int, int, int]`
- `Difficult_Roocket.main.Game`
- 使用 `Options` 完全重构
- 分离 `init mods` `init console` `init logger` `load_file`
- Completely refactored using `Options`
- Separate `init mods` `init console` `init logger` `load_file`
- `Difficult_Rocket.command.api.CommandText`
- `find` -> `re_find`
### Add ### Add
@ -122,6 +132,10 @@
- 用于方便的用人类可读的 Markdown 格式 直接输出一个已经实例化的 `Options` 类的所有字段 - 用于方便的用人类可读的 Markdown 格式 直接输出一个已经实例化的 `Options` 类的所有字段
- Add `as_markdown` method - Add `as_markdown` method
- Used to easily output all fields of an instantiated `Options` class in a human-readable Markdown format - Used to easily output all fields of an instantiated `Options` class in a human-readable Markdown format
- `Difficult_Rocket.command.api.CommandText`
- 添加基于 `str.find``find(text: str) -> bool` 方法
- Add method `find(text: str)` based on `str.find`
### Docs ### Docs

View File

@ -104,5 +104,6 @@ if TYPE_CHECKING:
class Console_rs: class Console_rs:
def __init__(self) -> None: ... def __init__(self) -> None: ...
def stop_console(self) -> None: ... def start(self) -> None: ...
def stop(self) -> bool: ...
def get_command(self) -> Optional[str]: ... def get_command(self) -> Optional[str]: ...

View File

@ -18,16 +18,17 @@ use pyo3::prelude::*;
// const MOD_PATH: String = String::from("mods"); // const MOD_PATH: String = String::from("mods");
#[allow(unused)]
enum LoadState { enum LoadState {
init, Init,
wait_start, WaitStart,
pre_start, PreStart,
running, Running,
clean, Clean,
} }
#[pyfunction] #[pyfunction]
fn get_version_str() -> String { "0.2.7.0".to_string() } fn get_version_str() -> String { "0.2.8.0".to_string() }
#[pyfunction] #[pyfunction]
fn test_call(py_obj: &PyAny) -> PyResult<bool> { fn test_call(py_obj: &PyAny) -> PyResult<bool> {

View File

@ -176,15 +176,21 @@ pub mod console {
#[pyo3(name = "Console_rs")] #[pyo3(name = "Console_rs")]
pub struct PyConsole { pub struct PyConsole {
/// 向子线程发送结束信号 /// 向子线程发送结束信号
pub stop_sender: std::sync::mpsc::Sender<()>, pub stop_sender: Option<std::sync::mpsc::Sender<()>>,
/// pub keyboard_input_receiver: Option<std::sync::mpsc::Receiver<String>>,
pub keyboard_input_receiver: std::sync::mpsc::Receiver<String>,
} }
#[pymethods] #[pymethods]
impl PyConsole { impl PyConsole {
#[new] #[new]
fn new() -> Self { fn new() -> Self {
Self {
stop_sender: None,
keyboard_input_receiver: None,
}
}
fn start(&mut self) {
let (stop_sender, stop_receiver) = std::sync::mpsc::channel(); let (stop_sender, stop_receiver) = std::sync::mpsc::channel();
let (keyboard_input_sender, keyboard_input_receiver) = std::sync::mpsc::channel(); let (keyboard_input_sender, keyboard_input_receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || { std::thread::spawn(move || {
@ -194,27 +200,31 @@ pub mod console {
break; break;
} }
let mut input = String::new(); let mut input = String::new();
print!(">>");
let _ = std_in.read_line(&mut input); let _ = std_in.read_line(&mut input);
if !input.is_empty() { if !input.is_empty() {
keyboard_input_sender.send(input).unwrap(); keyboard_input_sender.send(input).unwrap();
} }
print!(">>");
} }
}); });
self.stop_sender = Some(stop_sender);
Self { self.keyboard_input_receiver = Some(keyboard_input_receiver);
stop_sender,
keyboard_input_receiver,
}
} }
fn stop_console(&self) { self.stop_sender.send(()).unwrap(); } fn stop(&self) -> bool {
if let Some(sender) = &self.stop_sender {
sender.send(()).unwrap();
return true;
}
false
}
fn get_command(&self) -> Option<String> { fn get_command(&self) -> Option<String> {
// 获取输入 // 获取输入
if let Ok(string) = self.keyboard_input_receiver.try_recv() { if let Some(receiver) = &self.keyboard_input_receiver {
println!("rust recv input: {}", string); if let Ok(string) = receiver.try_recv() {
return Some(string); return Some(string);
}
} }
None None
} }

View File

@ -9,14 +9,13 @@ import traceback
from typing import Optional from typing import Optional
from libs.MCDR.version import Version from libs.MCDR.version import Version
from Difficult_Rocket.main import Game from Difficult_Rocket.main import Game, Console
from Difficult_Rocket.api.mod import ModInfo from Difficult_Rocket.api.mod import ModInfo
from Difficult_Rocket.api.types import Options from Difficult_Rocket.api.types import Options
from Difficult_Rocket.client import ClientWindow from Difficult_Rocket.client import ClientWindow
DR_rust_version = Version("0.2.7.0") # DR_mod 的 Rust 编写部分的兼容版本 DR_rust_version = Version("0.2.8.0") # DR_mod 的 Rust 编写部分的兼容版本
class _DR_mod_runtime(Options): class _DR_mod_runtime(Options):
@ -68,6 +67,10 @@ class DR_mod(ModInfo):
def on_load(self, game: Game, old_self: Optional["DR_mod"] = None) -> bool: def on_load(self, game: Game, old_self: Optional["DR_mod"] = None) -> bool:
if not DR_mod_runtime.DR_rust_available: if not DR_mod_runtime.DR_rust_available:
return False return False
from .console import RustConsole
game.console_class = RustConsole # 替换掉原来的 console 类
if old_self: if old_self:
game.client.window.add_sub_screen("SR1_ship", old_self.screen) game.client.window.add_sub_screen("SR1_ship", old_self.screen)
else: else:

24
mods/dr_game/console.py Normal file
View File

@ -0,0 +1,24 @@
from . import DR_mod_runtime
from Difficult_Rocket.main import Console
if DR_mod_runtime.use_DR_rust:
from .Difficult_Rocket_rs import Console_rs
class RustConsole(Console):
name = 'Rust stdin Console'
running: bool = False
console: Console_rs
def start(self):
self.console.start()
def stop(self):
return self.console.stop()
def init(self, **kwargs) -> None:
self.console = Console_rs()
def get_command(self) -> str:
return self.console.get_command()