feat(debug): 支持网易 Minecraft 调试启动
新增 debug 子命令,自动准备开发世界、注册内置调试 MOD,并将项目行为包和资源包链接到网易运行目录,方便启动游戏后直接进入调试世界。 调试 MOD 资源随仓库一起嵌入,避免依赖本机绝对路径;Windows junction 写入剥离 verbatim 前缀后的 DOS 路径,保证 Minecraft 能正确读取链接包。
This commit is contained in:
247
src/debug/addon.rs
Normal file
247
src/debug/addon.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::{CliError, Result};
|
||||
|
||||
use super::{
|
||||
config::{DebugConfig, ResolvedModDir},
|
||||
env, win,
|
||||
};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/embedded_examples.rs"));
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PackType {
|
||||
Behavior,
|
||||
Resource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PackInfo {
|
||||
pub name: String,
|
||||
pub uuid: String,
|
||||
pub version: Value,
|
||||
pub pack_type: PackType,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl PackInfo {
|
||||
fn runtime_dir_name(&self) -> String {
|
||||
self.uuid.chars().filter(|ch| *ch != '-').collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_debug_mod(config: &DebugConfig, mod_dirs: &[ResolvedModDir]) -> Result<PackInfo> {
|
||||
let manifest = EMBEDDED_DEBUG_MOD_FILES
|
||||
.iter()
|
||||
.find(|file| file.path == "manifest.json")
|
||||
.ok_or_else(|| CliError::NotFound("embedded debug MOD manifest.json".to_string()))?;
|
||||
let mut info = parse_manifest_bytes(manifest.contents)?;
|
||||
let out_root = runtime_root(info.pack_type);
|
||||
let target = out_root.join(info.runtime_dir_name());
|
||||
if target.exists() {
|
||||
fs::remove_dir_all(&target)?;
|
||||
}
|
||||
|
||||
let debug_options = python_literal_json(&config.debug_options)?;
|
||||
let target_mod_dirs = hot_reload_dirs_json(mod_dirs)?;
|
||||
|
||||
for file in EMBEDDED_DEBUG_MOD_FILES {
|
||||
let output_path = target.join(file.path.replace('/', "\\"));
|
||||
if let Some(parent) = output_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
if file.path.ends_with("Config.py") {
|
||||
let source = std::str::from_utf8(file.contents).map_err(|err| {
|
||||
CliError::InvalidData(format!("debug MOD Config.py is not UTF-8: {err}"))
|
||||
})?;
|
||||
let content = source
|
||||
.replace("\"{#debug_options}\"", &debug_options)
|
||||
.replace("\"{#target_mod_dirs}\"", &target_mod_dirs);
|
||||
fs::write(output_path, content.as_bytes())?;
|
||||
} else {
|
||||
fs::write(output_path, file.contents)?;
|
||||
}
|
||||
}
|
||||
|
||||
info.path = target;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
pub fn link_user_mod_dirs(
|
||||
mod_dirs: &[ResolvedModDir],
|
||||
linked_packs: &mut Vec<PackInfo>,
|
||||
) -> Result<()> {
|
||||
for mod_dir in mod_dirs {
|
||||
for pack in link_source_addon_to_runtime_packs(&mod_dir.path)? {
|
||||
match pack.pack_type {
|
||||
PackType::Behavior => {
|
||||
println!("[MCDK] LINK行为包: \"{}\", UUID: {}", pack.name, pack.uuid);
|
||||
if mod_dir.hot_reload {
|
||||
println!(" └── 热更新标记追踪");
|
||||
}
|
||||
}
|
||||
PackType::Resource => {
|
||||
println!("[MCDK] LINK资源包: \"{}\", UUID: {}", pack.name, pack.uuid);
|
||||
}
|
||||
}
|
||||
linked_packs.push(pack);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_pack_info(pack_path: &Path) -> Result<Option<PackInfo>> {
|
||||
if !pack_path.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
let manifest_path = manifest_path(pack_path);
|
||||
let Some(manifest_path) = manifest_path else {
|
||||
return Ok(None);
|
||||
};
|
||||
let content = fs::read(&manifest_path)?;
|
||||
let mut info = parse_manifest_bytes(&content)?;
|
||||
info.path = pack_path.to_path_buf();
|
||||
Ok(Some(info))
|
||||
}
|
||||
|
||||
fn link_source_addon_to_runtime_packs(source_dir: &Path) -> Result<Vec<PackInfo>> {
|
||||
if let Some(pack) = link_source_pack_to_runtime_pack(source_dir)? {
|
||||
return Ok(vec![pack]);
|
||||
}
|
||||
|
||||
let mut packs = Vec::new();
|
||||
if !source_dir.is_dir() {
|
||||
return Ok(packs);
|
||||
}
|
||||
for entry in fs::read_dir(source_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(pack) = link_source_pack_to_runtime_pack(&path)? {
|
||||
packs.push(pack);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(packs)
|
||||
}
|
||||
|
||||
fn link_source_pack_to_runtime_pack(source_dir: &Path) -> Result<Option<PackInfo>> {
|
||||
let Some(mut info) = parse_pack_info(source_dir)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let destination = runtime_root(info.pack_type).join(info.runtime_dir_name());
|
||||
win::create_junction(source_dir, &destination).map_err(|err| {
|
||||
CliError::Io(std::io::Error::new(
|
||||
err.kind(),
|
||||
format!(
|
||||
"failed to create junction {} -> {}: {err}",
|
||||
destination.display(),
|
||||
source_dir.display()
|
||||
),
|
||||
))
|
||||
})?;
|
||||
info.path = destination;
|
||||
Ok(Some(info))
|
||||
}
|
||||
|
||||
fn runtime_root(pack_type: PackType) -> PathBuf {
|
||||
match pack_type {
|
||||
PackType::Behavior => env::behavior_packs_path(),
|
||||
PackType::Resource => env::resource_packs_path(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_manifest_bytes(bytes: &[u8]) -> Result<PackInfo> {
|
||||
let manifest: Value = serde_json::from_slice(bytes)?;
|
||||
let header = manifest
|
||||
.get("header")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| CliError::InvalidData("manifest header is missing".to_string()))?;
|
||||
let name = header
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uuid = header
|
||||
.get("uuid")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| CliError::InvalidData("manifest header uuid is missing".to_string()))?
|
||||
.to_string();
|
||||
let version = header
|
||||
.get("version")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Array(Vec::new()));
|
||||
let modules = manifest
|
||||
.get("modules")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| CliError::InvalidData("manifest modules is missing".to_string()))?;
|
||||
let pack_type = modules
|
||||
.iter()
|
||||
.filter_map(|module| module.get("type").and_then(Value::as_str))
|
||||
.find_map(|ty| match ty {
|
||||
"data" => Some(PackType::Behavior),
|
||||
"resources" => Some(PackType::Resource),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
CliError::InvalidData("manifest module type is neither data nor resources".to_string())
|
||||
})?;
|
||||
|
||||
Ok(PackInfo {
|
||||
name,
|
||||
uuid,
|
||||
version,
|
||||
pack_type,
|
||||
path: PathBuf::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn manifest_path(pack_path: &Path) -> Option<PathBuf> {
|
||||
let manifest = pack_path.join("manifest.json");
|
||||
if manifest.is_file() {
|
||||
return Some(manifest);
|
||||
}
|
||||
let netease_manifest = pack_path.join("pack_manifest.json");
|
||||
if netease_manifest.is_file() {
|
||||
Some(netease_manifest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn hot_reload_dirs_json(mod_dirs: &[ResolvedModDir]) -> Result<String> {
|
||||
let dirs: Vec<String> = mod_dirs
|
||||
.iter()
|
||||
.filter(|dir| dir.hot_reload)
|
||||
.map(|dir| dir.path.to_string_lossy().replace('\\', "/"))
|
||||
.collect();
|
||||
Ok(serde_json::to_string(&dirs)?)
|
||||
}
|
||||
|
||||
fn python_literal_json(value: &Value) -> Result<String> {
|
||||
let mut text = serde_json::to_string(value)?;
|
||||
text = text.replace("true", "True");
|
||||
text = text.replace("false", "False");
|
||||
text = text.replace("null", "None");
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::python_literal_json;
|
||||
|
||||
#[test]
|
||||
fn converts_json_literals_to_python_literals() {
|
||||
assert_eq!(
|
||||
python_literal_json(&json!({"a": true, "b": null})).unwrap(),
|
||||
"{\"a\":True,\"b\":None}"
|
||||
);
|
||||
}
|
||||
}
|
||||
356
src/debug/config.rs
Normal file
356
src/debug/config.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::error::{CliError, Result};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DebugConfig {
|
||||
#[serde(default = "default_included_mod_dirs")]
|
||||
pub included_mod_dirs: Vec<IncludedModDir>,
|
||||
#[serde(default)]
|
||||
pub world_seed: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub reset_world: bool,
|
||||
#[serde(default = "default_world_name")]
|
||||
pub world_name: String,
|
||||
#[serde(default = "default_world_name")]
|
||||
pub world_folder_name: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_join_game: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub include_debug_mod: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_hot_reload_mods: bool,
|
||||
#[serde(default = "default_world_type")]
|
||||
pub world_type: i32,
|
||||
#[serde(default = "default_game_mode")]
|
||||
pub game_mode: i32,
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_cheats: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub keep_inventory: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub do_weather_cycle: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub do_daylight_cycle: bool,
|
||||
#[serde(default)]
|
||||
pub game_executable_path: String,
|
||||
#[serde(default)]
|
||||
pub debug_options: Value,
|
||||
#[serde(default)]
|
||||
pub netease_config: NeteaseConfig,
|
||||
#[serde(default = "default_user_name")]
|
||||
pub user_name: String,
|
||||
#[serde(default)]
|
||||
pub skin_info: Option<Value>,
|
||||
#[serde(default)]
|
||||
pub experiment_options: Option<ExperimentOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum IncludedModDir {
|
||||
Path(String),
|
||||
Object {
|
||||
path: String,
|
||||
#[serde(default = "default_true", rename = "hot_reload")]
|
||||
hot_reload: bool,
|
||||
#[serde(default = "default_true")]
|
||||
enabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ResolvedModDir {
|
||||
pub path: PathBuf,
|
||||
pub hot_reload: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct NeteaseConfig {
|
||||
#[serde(default)]
|
||||
pub chat_extension: bool,
|
||||
}
|
||||
|
||||
impl Default for NeteaseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chat_extension: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ExperimentOptions {
|
||||
#[serde(default)]
|
||||
pub data_driven_biomes: bool,
|
||||
#[serde(default)]
|
||||
pub data_driven_items: bool,
|
||||
#[serde(default)]
|
||||
pub experimental_molang_features: bool,
|
||||
}
|
||||
|
||||
impl DebugConfig {
|
||||
pub fn included_mod_dirs(&self, project_dir: &Path) -> Result<Vec<ResolvedModDir>> {
|
||||
let mut dirs = Vec::new();
|
||||
for item in &self.included_mod_dirs {
|
||||
match item {
|
||||
IncludedModDir::Path(path) => {
|
||||
dirs.push(resolve_mod_dir(project_dir, path, true, true)?)
|
||||
}
|
||||
IncludedModDir::Object {
|
||||
path,
|
||||
hot_reload,
|
||||
enabled,
|
||||
} => {
|
||||
if *enabled {
|
||||
dirs.push(resolve_mod_dir(project_dir, path, *hot_reload, true)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(dirs)
|
||||
}
|
||||
|
||||
pub fn auto_join_game_effective(&self) -> bool {
|
||||
match env::var("MCDEV_AUTO_JOIN_GAME") {
|
||||
Ok(value) if value == "0" => false,
|
||||
Ok(value) if value == "1" => true,
|
||||
_ => self.auto_join_game,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_is_subprocess_mode() -> bool {
|
||||
matches!(env::var("MCDEV_IS_SUBPROCESS_MODE"), Ok(value) if value == "1" || value.eq_ignore_ascii_case("true"))
|
||||
}
|
||||
|
||||
pub fn load_or_create(project_dir: &Path) -> Result<DebugConfig> {
|
||||
let config_path = project_dir.join(".mcdev.json");
|
||||
if config_path.is_file() {
|
||||
let content = fs::read_to_string(&config_path)?;
|
||||
let stripped = strip_json_comments(&content);
|
||||
let config = serde_json::from_str(&stripped)?;
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
let config = create_default_config();
|
||||
write_config(project_dir, &config)?;
|
||||
if config.game_executable_path.is_empty() {
|
||||
return Err(CliError::NotFound(
|
||||
"未找到 Minecraft.Windows.exe;已创建 .mcdev.json,请填写 game_executable_path"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn ensure_game_executable(project_dir: &Path, config: &mut DebugConfig) -> Result<()> {
|
||||
let configured = PathBuf::from(&config.game_executable_path);
|
||||
if !config.game_executable_path.is_empty() && configured.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(path) = crate::debug::env::auto_match_latest_game_exe_path() {
|
||||
config.game_executable_path = path.to_string_lossy().replace('\\', "/");
|
||||
write_config(project_dir, config)?;
|
||||
println!(
|
||||
"游戏路径无效,已重新搜索并更新:{}",
|
||||
config.game_executable_path
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(CliError::NotFound(
|
||||
"未找到有效的 Minecraft.Windows.exe,请在 .mcdev.json 中设置 game_executable_path"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn write_config(project_dir: &Path, config: &DebugConfig) -> Result<()> {
|
||||
let path = project_dir.join(".mcdev.json");
|
||||
let text = serde_json::to_string_pretty(config)?;
|
||||
fs::write(path, text)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_default_config() -> DebugConfig {
|
||||
DebugConfig {
|
||||
included_mod_dirs: default_included_mod_dirs(),
|
||||
world_seed: None,
|
||||
reset_world: false,
|
||||
world_name: default_world_name(),
|
||||
world_folder_name: default_world_name(),
|
||||
auto_join_game: true,
|
||||
include_debug_mod: true,
|
||||
auto_hot_reload_mods: true,
|
||||
world_type: default_world_type(),
|
||||
game_mode: default_game_mode(),
|
||||
enable_cheats: true,
|
||||
keep_inventory: true,
|
||||
do_weather_cycle: true,
|
||||
do_daylight_cycle: true,
|
||||
game_executable_path: crate::debug::env::auto_match_latest_game_exe_path()
|
||||
.map(|path| path.to_string_lossy().replace('\\', "/"))
|
||||
.unwrap_or_default(),
|
||||
debug_options: Value::Object(Map::new()),
|
||||
netease_config: NeteaseConfig::default(),
|
||||
user_name: default_user_name(),
|
||||
skin_info: None,
|
||||
experiment_options: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_mod_dir(
|
||||
project_dir: &Path,
|
||||
path: &str,
|
||||
hot_reload: bool,
|
||||
enabled: bool,
|
||||
) -> Result<ResolvedModDir> {
|
||||
if !enabled {
|
||||
return Err(CliError::InvalidInput(
|
||||
"disabled mod dir should not be resolved".to_string(),
|
||||
));
|
||||
}
|
||||
let raw = PathBuf::from(path);
|
||||
let path = if raw.is_absolute() {
|
||||
raw
|
||||
} else {
|
||||
let base = if project_dir.is_absolute() {
|
||||
project_dir.to_path_buf()
|
||||
} else {
|
||||
env::current_dir()?.join(project_dir)
|
||||
};
|
||||
base.join(raw)
|
||||
};
|
||||
Ok(ResolvedModDir {
|
||||
path: normalize_path(path),
|
||||
hot_reload,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_path(path: PathBuf) -> PathBuf {
|
||||
let mut out = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
std::path::Component::CurDir => {}
|
||||
std::path::Component::ParentDir => {
|
||||
out.pop();
|
||||
}
|
||||
other => out.push(other.as_os_str()),
|
||||
}
|
||||
}
|
||||
if out.as_os_str().is_empty() {
|
||||
PathBuf::from(".")
|
||||
} else {
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn default_included_mod_dirs() -> Vec<IncludedModDir> {
|
||||
vec![IncludedModDir::Path("./".to_string())]
|
||||
}
|
||||
|
||||
fn default_world_name() -> String {
|
||||
"MC_DEV_WORLD".to_string()
|
||||
}
|
||||
fn default_user_name() -> String {
|
||||
"developer".to_string()
|
||||
}
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_world_type() -> i32 {
|
||||
1
|
||||
}
|
||||
fn default_game_mode() -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
pub fn strip_json_comments(input: &str) -> String {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
let mut chars = input.chars().peekable();
|
||||
let mut in_string = false;
|
||||
let mut escaped = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if in_string {
|
||||
out.push(ch);
|
||||
if escaped {
|
||||
escaped = false;
|
||||
} else if ch == '\\' {
|
||||
escaped = true;
|
||||
} else if ch == '"' {
|
||||
in_string = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
in_string = true;
|
||||
out.push(ch);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ch == '/' {
|
||||
match chars.peek().copied() {
|
||||
Some('/') => {
|
||||
chars.next();
|
||||
for next in chars.by_ref() {
|
||||
if next == '\n' {
|
||||
out.push('\n');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some('*') => {
|
||||
chars.next();
|
||||
let mut prev = '\0';
|
||||
for next in chars.by_ref() {
|
||||
if next == '\n' {
|
||||
out.push('\n');
|
||||
}
|
||||
if prev == '*' && next == '/' {
|
||||
break;
|
||||
}
|
||||
prev = next;
|
||||
}
|
||||
}
|
||||
_ => out.push(ch),
|
||||
}
|
||||
} else {
|
||||
out.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_mod_dir, strip_json_comments};
|
||||
|
||||
#[test]
|
||||
fn strips_jsonc_comments_without_touching_strings() {
|
||||
let input = r#"{"url":"http://x",/*a*/"n":1//b
|
||||
}"#;
|
||||
assert_eq!(
|
||||
strip_json_comments(input),
|
||||
"{\"url\":\"http://x\",\"n\":1\n}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_current_project_resolves_to_absolute_cwd() {
|
||||
let resolved = resolve_mod_dir(std::path::Path::new("."), "./", true, true).unwrap();
|
||||
let cwd = super::normalize_path(std::env::current_dir().unwrap());
|
||||
|
||||
assert_eq!(resolved.path, cwd);
|
||||
assert!(resolved.hot_reload);
|
||||
}
|
||||
}
|
||||
99
src/debug/env.rs
Normal file
99
src/debug/env.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
pub fn app_data_path() -> PathBuf {
|
||||
env::var_os("APPDATA")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn minecraft_data_path() -> PathBuf {
|
||||
app_data_path().join("MinecraftPE_Netease")
|
||||
}
|
||||
|
||||
pub fn games_com_netease_path() -> PathBuf {
|
||||
minecraft_data_path().join("games/com.netease")
|
||||
}
|
||||
|
||||
pub fn worlds_path() -> PathBuf {
|
||||
minecraft_data_path().join("minecraftWorlds")
|
||||
}
|
||||
|
||||
pub fn behavior_packs_path() -> PathBuf {
|
||||
games_com_netease_path().join("behavior_packs")
|
||||
}
|
||||
|
||||
pub fn resource_packs_path() -> PathBuf {
|
||||
games_com_netease_path().join("resource_packs")
|
||||
}
|
||||
|
||||
pub fn clean_runtime_packs() -> Result<()> {
|
||||
for path in [behavior_packs_path(), resource_packs_path()] {
|
||||
if path.is_dir() {
|
||||
fs::remove_dir_all(&path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn auto_match_latest_game_exe_path() -> Option<PathBuf> {
|
||||
let mut best: Option<(Vec<u32>, PathBuf)> = None;
|
||||
|
||||
for drive in b'A'..=b'Z' {
|
||||
let root = format!(
|
||||
"{}:/MCStudioDownload/game/MinecraftPE_Netease",
|
||||
drive as char
|
||||
);
|
||||
let root = PathBuf::from(root);
|
||||
if !root.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(&root).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let exe = path.join("Minecraft.Windows.exe");
|
||||
if !exe.is_file() {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parse_version(name) else {
|
||||
continue;
|
||||
};
|
||||
match &best {
|
||||
Some((best_version, _)) if best_version >= &version => {}
|
||||
_ => best = Some((version, exe)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best.map(|(_, path)| path)
|
||||
}
|
||||
|
||||
fn parse_version(value: &str) -> Option<Vec<u32>> {
|
||||
let mut out = Vec::new();
|
||||
for part in value.split('.') {
|
||||
if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
out.push(part.parse().ok()?);
|
||||
}
|
||||
if out.is_empty() { None } else { Some(out) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_version;
|
||||
|
||||
#[test]
|
||||
fn parses_numeric_versions_only() {
|
||||
assert_eq!(parse_version("1.21.3"), Some(vec![1, 21, 3]));
|
||||
assert_eq!(parse_version("1.x"), None);
|
||||
}
|
||||
}
|
||||
513
src/debug/hotreload.rs
Normal file
513
src/debug/hotreload.rs
Normal file
@@ -0,0 +1,513 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
use std::{fs, time::SystemTime};
|
||||
|
||||
use serde_json::json;
|
||||
#[cfg(not(windows))]
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::{
|
||||
config::ResolvedModDir,
|
||||
ipc::DebugIpcServer,
|
||||
log::{self, ConsoleColor},
|
||||
};
|
||||
|
||||
const DEBOUNCE_MS: u64 = 100;
|
||||
|
||||
pub struct HotReloadTask {
|
||||
stop: Arc<AtomicBool>,
|
||||
|
||||
file_thread: Option<JoinHandle<()>>,
|
||||
foreground_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl HotReloadTask {
|
||||
pub fn start(
|
||||
process_id: u32,
|
||||
mod_dirs: &[ResolvedModDir],
|
||||
ipc: DebugIpcServer,
|
||||
) -> Option<Self> {
|
||||
let roots: Vec<PathBuf> = mod_dirs
|
||||
.iter()
|
||||
.filter(|dir| dir.hot_reload)
|
||||
.map(|dir| dir.path.clone())
|
||||
.collect();
|
||||
if roots.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
println!("[HotReload] 追踪目录列表:");
|
||||
for root in &roots {
|
||||
println!(" └── {}", root.display());
|
||||
}
|
||||
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let need_update = Arc::new(AtomicBool::new(false));
|
||||
let is_foreground = Arc::new(AtomicBool::new(false));
|
||||
let cached_paths = Arc::new(Mutex::new(HashSet::new()));
|
||||
|
||||
let file_thread = {
|
||||
let stop = Arc::clone(&stop);
|
||||
let need_update = Arc::clone(&need_update);
|
||||
let is_foreground = Arc::clone(&is_foreground);
|
||||
let cached_paths = Arc::clone(&cached_paths);
|
||||
let ipc = ipc.clone();
|
||||
let roots = roots.clone();
|
||||
thread::spawn(move || {
|
||||
watch_py_files(roots, stop, need_update, is_foreground, cached_paths, ipc)
|
||||
})
|
||||
};
|
||||
|
||||
let foreground_thread = {
|
||||
let stop = Arc::clone(&stop);
|
||||
let need_update = Arc::clone(&need_update);
|
||||
let is_foreground = Arc::clone(&is_foreground);
|
||||
let cached_paths = Arc::clone(&cached_paths);
|
||||
let ipc = ipc.clone();
|
||||
let roots = roots.clone();
|
||||
thread::spawn(move || {
|
||||
watch_foreground(
|
||||
process_id,
|
||||
roots,
|
||||
stop,
|
||||
need_update,
|
||||
is_foreground,
|
||||
cached_paths,
|
||||
ipc,
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
stop,
|
||||
|
||||
file_thread: Some(file_thread),
|
||||
foreground_thread: Some(foreground_thread),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn safe_exit(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
if let Some(handle) = self.file_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
if let Some(handle) = self.foreground_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HotReloadTask {
|
||||
fn drop(&mut self) {
|
||||
self.safe_exit();
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_py_files(
|
||||
roots: Vec<PathBuf>,
|
||||
stop: Arc<AtomicBool>,
|
||||
need_update: Arc<AtomicBool>,
|
||||
is_foreground: Arc<AtomicBool>,
|
||||
cached_paths: Arc<Mutex<HashSet<PathBuf>>>,
|
||||
ipc: DebugIpcServer,
|
||||
) {
|
||||
watch_py_files_native(roots, stop, need_update, is_foreground, cached_paths, ipc);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn watch_py_files_native(
|
||||
roots: Vec<PathBuf>,
|
||||
stop: Arc<AtomicBool>,
|
||||
need_update: Arc<AtomicBool>,
|
||||
is_foreground: Arc<AtomicBool>,
|
||||
cached_paths: Arc<Mutex<HashSet<PathBuf>>>,
|
||||
ipc: DebugIpcServer,
|
||||
) {
|
||||
use std::{mem, os::windows::ffi::OsStrExt, ptr, time::Instant};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{HANDLE, INVALID_HANDLE_VALUE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT},
|
||||
Storage::FileSystem::{
|
||||
CreateFileW, FILE_ACTION_MODIFIED, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED,
|
||||
FILE_LIST_DIRECTORY, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_INFORMATION,
|
||||
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
||||
ReadDirectoryChangesW,
|
||||
},
|
||||
System::{
|
||||
IO::{GetOverlappedResult, OVERLAPPED},
|
||||
Threading::{CreateEventW, WaitForMultipleObjects},
|
||||
},
|
||||
};
|
||||
|
||||
struct WatchItem {
|
||||
dir: PathBuf,
|
||||
h_dir: HANDLE,
|
||||
event: HANDLE,
|
||||
overlapped: OVERLAPPED,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Drop for WatchItem {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self.h_dir != INVALID_HANDLE_VALUE && !self.h_dir.is_null() {
|
||||
windows_sys::Win32::Foundation::CloseHandle(self.h_dir);
|
||||
}
|
||||
if !self.event.is_null() {
|
||||
windows_sys::Win32::Foundation::CloseHandle(self.event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_watch(item: &mut WatchItem) -> bool {
|
||||
item.overlapped = unsafe { mem::zeroed() };
|
||||
item.overlapped.hEvent = item.event;
|
||||
let mut bytes_returned = 0u32;
|
||||
unsafe {
|
||||
ReadDirectoryChangesW(
|
||||
item.h_dir,
|
||||
item.buffer.as_mut_ptr().cast(),
|
||||
item.buffer.len() as u32,
|
||||
1,
|
||||
FILE_NOTIFY_CHANGE_LAST_WRITE,
|
||||
&mut bytes_returned,
|
||||
&mut item.overlapped,
|
||||
None,
|
||||
) != 0
|
||||
}
|
||||
}
|
||||
|
||||
let mut items = Vec::new();
|
||||
for root in &roots {
|
||||
if !root.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let mut wide: Vec<u16> = root.as_os_str().encode_wide().collect();
|
||||
wide.push(0);
|
||||
let event = unsafe { CreateEventW(ptr::null(), 0, 0, ptr::null()) };
|
||||
if event.is_null() {
|
||||
continue;
|
||||
}
|
||||
let h_dir = unsafe {
|
||||
CreateFileW(
|
||||
wide.as_ptr(),
|
||||
FILE_LIST_DIRECTORY,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
ptr::null(),
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if h_dir == INVALID_HANDLE_VALUE {
|
||||
unsafe {
|
||||
windows_sys::Win32::Foundation::CloseHandle(event);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
items.push(WatchItem {
|
||||
dir: root.clone(),
|
||||
h_dir,
|
||||
event,
|
||||
overlapped: unsafe { mem::zeroed() },
|
||||
buffer: vec![0u8; 64 * 1024],
|
||||
});
|
||||
}
|
||||
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
if items.len() > 64 {
|
||||
log::print_colored(
|
||||
"[HotReload] 追踪目录超过 Windows WaitForMultipleObjects 限制,已忽略超出部分。",
|
||||
ConsoleColor::Yellow,
|
||||
);
|
||||
items.truncate(64);
|
||||
}
|
||||
|
||||
for item in &mut items {
|
||||
start_watch(item);
|
||||
}
|
||||
|
||||
let mut debounce = HashMap::<PathBuf, Instant>::new();
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let handles: Vec<HANDLE> = items.iter().map(|item| item.event).collect();
|
||||
let result =
|
||||
unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_ptr(), 0, 100) };
|
||||
if result == WAIT_TIMEOUT {
|
||||
continue;
|
||||
}
|
||||
if result == WAIT_FAILED {
|
||||
break;
|
||||
}
|
||||
let index = (result - WAIT_OBJECT_0) as usize;
|
||||
if index >= items.len() {
|
||||
continue;
|
||||
}
|
||||
let item = &mut items[index];
|
||||
let mut bytes = 0u32;
|
||||
let ok = unsafe { GetOverlappedResult(item.h_dir, &mut item.overlapped, &mut bytes, 0) };
|
||||
if ok == 0 || bytes == 0 {
|
||||
start_watch(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut offset = 0usize;
|
||||
while offset < bytes as usize {
|
||||
let info =
|
||||
unsafe { &*(item.buffer.as_ptr().add(offset) as *const FILE_NOTIFY_INFORMATION) };
|
||||
let name_len = (info.FileNameLength / 2) as usize;
|
||||
let name = unsafe { std::slice::from_raw_parts(info.FileName.as_ptr(), name_len) };
|
||||
let path = item.dir.join(String::from_utf16_lossy(name));
|
||||
if info.Action == FILE_ACTION_MODIFIED
|
||||
&& path.extension().and_then(|ext| ext.to_str()) == Some("py")
|
||||
{
|
||||
let now = Instant::now();
|
||||
let should_trigger = debounce
|
||||
.get(&path)
|
||||
.map(|last| now.duration_since(*last) >= Duration::from_millis(DEBOUNCE_MS))
|
||||
.unwrap_or(true);
|
||||
if should_trigger {
|
||||
debounce.insert(path.clone(), now);
|
||||
record_changed_path(
|
||||
&roots,
|
||||
&path,
|
||||
&cached_paths,
|
||||
&need_update,
|
||||
&is_foreground,
|
||||
&ipc,
|
||||
);
|
||||
}
|
||||
}
|
||||
if info.NextEntryOffset == 0 {
|
||||
break;
|
||||
}
|
||||
offset += info.NextEntryOffset as usize;
|
||||
}
|
||||
start_watch(item);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn watch_py_files_native(
|
||||
roots: Vec<PathBuf>,
|
||||
stop: Arc<AtomicBool>,
|
||||
need_update: Arc<AtomicBool>,
|
||||
is_foreground: Arc<AtomicBool>,
|
||||
cached_paths: Arc<Mutex<HashSet<PathBuf>>>,
|
||||
ipc: DebugIpcServer,
|
||||
) {
|
||||
let mut last_seen = snapshot_py_files(&roots);
|
||||
let mut debounce = HashMap::<PathBuf, SystemTime>::new();
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(DEBOUNCE_MS));
|
||||
let now = SystemTime::now();
|
||||
let current = snapshot_py_files(&roots);
|
||||
for (path, modified) in ¤t {
|
||||
let changed = match last_seen.get(path) {
|
||||
Some(previous) => modified > previous,
|
||||
None => false,
|
||||
};
|
||||
if !changed {
|
||||
continue;
|
||||
}
|
||||
if let Some(last) = debounce.get(path) {
|
||||
if now.duration_since(*last).unwrap_or_default()
|
||||
< Duration::from_millis(DEBOUNCE_MS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debounce.insert(path.clone(), now);
|
||||
record_changed_path(
|
||||
&roots,
|
||||
path,
|
||||
&cached_paths,
|
||||
&need_update,
|
||||
&is_foreground,
|
||||
&ipc,
|
||||
);
|
||||
}
|
||||
last_seen = current;
|
||||
}
|
||||
}
|
||||
|
||||
fn record_changed_path(
|
||||
roots: &[PathBuf],
|
||||
path: &Path,
|
||||
cached_paths: &Arc<Mutex<HashSet<PathBuf>>>,
|
||||
need_update: &Arc<AtomicBool>,
|
||||
is_foreground: &Arc<AtomicBool>,
|
||||
ipc: &DebugIpcServer,
|
||||
) {
|
||||
log::print_colored(
|
||||
&format!("[HotReload] Detected change in: {}", path.display()),
|
||||
ConsoleColor::Yellow,
|
||||
);
|
||||
cached_paths.lock().unwrap().insert(path.to_path_buf());
|
||||
need_update.store(true, Ordering::Relaxed);
|
||||
if is_foreground.load(Ordering::Relaxed) {
|
||||
trigger_reload(roots, cached_paths, need_update, ipc);
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_foreground(
|
||||
process_id: u32,
|
||||
roots: Vec<PathBuf>,
|
||||
stop: Arc<AtomicBool>,
|
||||
need_update: Arc<AtomicBool>,
|
||||
is_foreground: Arc<AtomicBool>,
|
||||
cached_paths: Arc<Mutex<HashSet<PathBuf>>>,
|
||||
ipc: DebugIpcServer,
|
||||
) {
|
||||
let mut last_state = false;
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let current = foreground_process_id() == Some(process_id);
|
||||
is_foreground.store(current, Ordering::Relaxed);
|
||||
if current && !last_state && need_update.load(Ordering::Relaxed) {
|
||||
trigger_reload(&roots, &cached_paths, &need_update, &ipc);
|
||||
}
|
||||
last_state = current;
|
||||
thread::sleep(Duration::from_millis(80));
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_reload(
|
||||
roots: &[PathBuf],
|
||||
cached_paths: &Arc<Mutex<HashSet<PathBuf>>>,
|
||||
need_update: &Arc<AtomicBool>,
|
||||
ipc: &DebugIpcServer,
|
||||
) {
|
||||
let paths: Vec<PathBuf> = {
|
||||
let mut guard = cached_paths.lock().unwrap();
|
||||
guard.drain().collect()
|
||||
};
|
||||
let modules: Vec<String> = paths
|
||||
.iter()
|
||||
.filter_map(|path| py_path_to_module_name(path, roots))
|
||||
.collect();
|
||||
need_update.store(false, Ordering::Relaxed);
|
||||
if modules.is_empty() {
|
||||
return;
|
||||
}
|
||||
log::print_colored(
|
||||
"[HotReload] 检测到修改,已触发热更新。",
|
||||
ConsoleColor::Yellow,
|
||||
);
|
||||
let payload = json!(modules).to_string();
|
||||
let _ = ipc.send_message(2, payload.as_bytes());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn snapshot_py_files(roots: &[PathBuf]) -> HashMap<PathBuf, SystemTime> {
|
||||
let mut files = HashMap::new();
|
||||
for root in roots {
|
||||
if !root.is_dir() {
|
||||
continue;
|
||||
}
|
||||
for entry in WalkDir::new(root).into_iter().filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
if !path.is_file() || path.extension().and_then(|ext| ext.to_str()) != Some("py") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
if let Ok(modified) = metadata.modified() {
|
||||
files.insert(path.to_path_buf(), modified);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
pub fn py_path_to_module_name(file_path: &Path, mod_roots: &[PathBuf]) -> Option<String> {
|
||||
if file_path.extension().and_then(|ext| ext.to_str()) != Some("py") {
|
||||
return None;
|
||||
}
|
||||
let mut cur = file_path.parent()?;
|
||||
loop {
|
||||
if cur.join("manifest.json").is_file() || cur.join("pack_manifest.json").is_file() {
|
||||
let rel = file_path.strip_prefix(cur).ok()?;
|
||||
let mut parts: Vec<String> = rel
|
||||
.iter()
|
||||
.map(|part| part.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
let last = parts.last_mut()?;
|
||||
if !last.ends_with(".py") {
|
||||
return None;
|
||||
}
|
||||
last.truncate(last.len() - 3);
|
||||
return Some(parts.join("."));
|
||||
}
|
||||
if mod_roots.iter().any(|root| same_path(root, cur)) {
|
||||
return None;
|
||||
}
|
||||
cur = cur.parent()?;
|
||||
}
|
||||
}
|
||||
|
||||
fn same_path(left: &Path, right: &Path) -> bool {
|
||||
left == right || (left.canonicalize().ok().as_deref() == right.canonicalize().ok().as_deref())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn foreground_process_id() -> Option<u32> {
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
GetForegroundWindow, GetWindowThreadProcessId,
|
||||
};
|
||||
unsafe {
|
||||
let hwnd = GetForegroundWindow();
|
||||
if hwnd.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut pid = 0u32;
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
if pid == 0 { None } else { Some(pid) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn foreground_process_id() -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use super::py_path_to_module_name;
|
||||
|
||||
#[test]
|
||||
fn maps_py_file_to_module_name_under_manifest() {
|
||||
let root = std::env::temp_dir().join(format!("emod-hot-{}", std::process::id()));
|
||||
let pack = root.join("behavior_pack");
|
||||
let module = pack.join("foo/bar.py");
|
||||
fs::create_dir_all(module.parent().unwrap()).unwrap();
|
||||
fs::write(pack.join("manifest.json"), "{}").unwrap();
|
||||
fs::write(&module, "").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
py_path_to_module_name(&module, std::slice::from_ref(&root)),
|
||||
Some("foo.bar".to_string())
|
||||
);
|
||||
let outside = root.join("x.py");
|
||||
fs::write(&outside, "").unwrap();
|
||||
assert_eq!(
|
||||
py_path_to_module_name(&outside, std::slice::from_ref(&root)),
|
||||
None
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
}
|
||||
123
src/debug/ipc.rs
Normal file
123
src/debug/ipc.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::error::{CliError, Result};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DebugIpcServer {
|
||||
inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
port: Mutex<u16>,
|
||||
clients: Mutex<Vec<TcpStream>>,
|
||||
stop: AtomicBool,
|
||||
thread: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl DebugIpcServer {
|
||||
pub fn start() -> Result<Self> {
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0))?;
|
||||
listener.set_nonblocking(true)?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let inner = Arc::new(Inner {
|
||||
port: Mutex::new(port),
|
||||
clients: Mutex::new(Vec::new()),
|
||||
stop: AtomicBool::new(false),
|
||||
thread: Mutex::new(None),
|
||||
});
|
||||
|
||||
let thread_inner = Arc::clone(&inner);
|
||||
let handle = thread::spawn(move || accept_loop(listener, thread_inner));
|
||||
*inner.thread.lock().unwrap() = Some(handle);
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
pub fn port(&self) -> u16 {
|
||||
*self.inner.port.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn send_message(&self, message_type: u16, payload: &[u8]) -> Result<bool> {
|
||||
let frame = encode_frame(message_type, payload)?;
|
||||
let mut clients = self.inner.clients.lock().unwrap();
|
||||
let mut any = false;
|
||||
clients.retain_mut(|stream| match stream.write(&frame) {
|
||||
Ok(_) => {
|
||||
any = true;
|
||||
true
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::WouldBlock => true,
|
||||
Err(_) => false,
|
||||
});
|
||||
Ok(any)
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
self.inner.stop.store(true, Ordering::Relaxed);
|
||||
*self.inner.port.lock().unwrap() = 0;
|
||||
self.inner.clients.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
pub fn join(&self) {
|
||||
if let Some(handle) = self.inner.thread.lock().unwrap().take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn safe_exit(&self) {
|
||||
self.stop();
|
||||
self.join();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DebugIpcServer {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_frame(message_type: u16, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let len = u32::try_from(payload.len())
|
||||
.map_err(|_| CliError::InvalidInput("IPC payload is larger than u32::MAX".to_string()))?;
|
||||
let mut frame = Vec::with_capacity(6 + payload.len());
|
||||
frame.extend_from_slice(&message_type.to_be_bytes());
|
||||
frame.extend_from_slice(&len.to_be_bytes());
|
||||
frame.extend_from_slice(payload);
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn accept_loop(listener: TcpListener, inner: Arc<Inner>) {
|
||||
while !inner.stop.load(Ordering::Relaxed) {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => {
|
||||
let _ = stream.set_nonblocking(true);
|
||||
inner.clients.lock().unwrap().push(stream);
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::encode_frame;
|
||||
|
||||
#[test]
|
||||
fn encodes_big_endian_ipc_frame() {
|
||||
let frame = encode_frame(2, b"[\"a.b\"]").unwrap();
|
||||
assert_eq!(&frame[..6], &[0, 2, 0, 0, 0, 7]);
|
||||
assert_eq!(&frame[6..], b"[\"a.b\"]");
|
||||
}
|
||||
}
|
||||
125
src/debug/level.rs
Normal file
125
src/debug/level.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
use super::{
|
||||
addon::{PackInfo, PackType},
|
||||
config::DebugConfig,
|
||||
env, nbt,
|
||||
};
|
||||
|
||||
pub fn world_dir(config: &DebugConfig) -> PathBuf {
|
||||
env::worlds_path().join(&config.world_folder_name)
|
||||
}
|
||||
|
||||
pub fn prepare_world(config: &DebugConfig, linked_packs: &[PackInfo]) -> Result<()> {
|
||||
let world_dir = world_dir(config);
|
||||
let level_path = world_dir.join("level.dat");
|
||||
|
||||
if !world_dir.is_dir() || config.reset_world {
|
||||
if world_dir.exists() {
|
||||
fs::remove_dir_all(&world_dir)?;
|
||||
}
|
||||
fs::create_dir_all(&world_dir)?;
|
||||
let data = nbt::create_level_dat(config)?;
|
||||
fs::write(&level_path, data)?;
|
||||
} else if level_path.is_file() {
|
||||
let data = fs::read(&level_path)?;
|
||||
let updated = if plugin_env_enabled() {
|
||||
nbt::update_level_dat_world_data(&data, config, false)?
|
||||
} else {
|
||||
nbt::update_level_dat_last_played(&data)?
|
||||
};
|
||||
fs::write(&level_path, updated)?;
|
||||
} else {
|
||||
let data = nbt::create_level_dat(config)?;
|
||||
fs::write(&level_path, data)?;
|
||||
}
|
||||
|
||||
write_world_pack_manifests(config, linked_packs)
|
||||
}
|
||||
|
||||
pub fn write_dev_config(config: &DebugConfig) -> Result<PathBuf> {
|
||||
let world_dir = world_dir(config);
|
||||
fs::create_dir_all(&world_dir)?;
|
||||
|
||||
let game_exe = PathBuf::from(&config.game_executable_path);
|
||||
let default_skin = game_exe
|
||||
.parent()
|
||||
.unwrap_or_else(|| game_exe.as_path())
|
||||
.join("data/skin_packs/vanilla/steve.png")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
|
||||
let mut dev_config = json!({
|
||||
"world_info": { "level_id": config.world_folder_name },
|
||||
"room_info": {},
|
||||
"player_info": {
|
||||
"urs": "",
|
||||
"user_id": 0,
|
||||
"user_name": config.user_name,
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(skin_info) = &config.skin_info {
|
||||
let mut skin_info = skin_info.clone();
|
||||
if let Value::Object(map) = &mut skin_info {
|
||||
map.entry("slim".to_string()).or_insert(Value::Bool(false));
|
||||
match map.get("skin").and_then(Value::as_str) {
|
||||
Some(path) if !path.is_empty() => {}
|
||||
_ => {
|
||||
map.insert("skin".to_string(), Value::String(default_skin));
|
||||
}
|
||||
}
|
||||
}
|
||||
dev_config["skin_info"] = skin_info;
|
||||
} else {
|
||||
dev_config["skin_info"] = json!({ "slim": false, "skin": default_skin });
|
||||
}
|
||||
|
||||
let path = world_dir.join("dev_config.cppconfig");
|
||||
fs::write(&path, serde_json::to_vec_pretty(&dev_config)?)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn write_world_pack_manifests(config: &DebugConfig, linked_packs: &[PackInfo]) -> Result<()> {
|
||||
let mut behavior = Vec::new();
|
||||
let mut resource = Vec::new();
|
||||
|
||||
for pack in linked_packs {
|
||||
let entry = json!({
|
||||
"pack_id": pack.uuid,
|
||||
"version": pack.version,
|
||||
});
|
||||
match pack.pack_type {
|
||||
PackType::Behavior => behavior.push(entry),
|
||||
PackType::Resource => resource.push(entry),
|
||||
}
|
||||
}
|
||||
|
||||
let world_dir = world_dir(config);
|
||||
let (behavior_file, resource_file) = if config.auto_join_game_effective() {
|
||||
("world_behavior_packs.json", "world_resource_packs.json")
|
||||
} else {
|
||||
(
|
||||
"netease_world_behavior_packs.json",
|
||||
"netease_world_resource_packs.json",
|
||||
)
|
||||
};
|
||||
|
||||
fs::write(
|
||||
world_dir.join(behavior_file),
|
||||
serde_json::to_vec_pretty(&behavior)?,
|
||||
)?;
|
||||
fs::write(
|
||||
world_dir.join(resource_file),
|
||||
serde_json::to_vec_pretty(&resource)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn plugin_env_enabled() -> bool {
|
||||
matches!(std::env::var("MCDEV_PLUGIN_ENV"), Ok(value) if value == "1" || value.eq_ignore_ascii_case("true"))
|
||||
}
|
||||
187
src/debug/level_template.rs
Normal file
187
src/debug/level_template.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
pub const LEVEL_DAT_TEMPLATE: &[u8] = &[
|
||||
0x0a, 0x00, 0x00, 0x00, 0x83, 0x0b, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x08, 0x0d, 0x00, 0x42, 0x69,
|
||||
0x6f, 0x6d, 0x65, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x00, 0x00, 0x01, 0x12, 0x00,
|
||||
0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x73, 0x54, 0x6f, 0x4f, 0x72, 0x69, 0x67,
|
||||
0x69, 0x6e, 0x00, 0x01, 0x1e, 0x00, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x50,
|
||||
0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x43, 0x6f, 0x6e,
|
||||
0x74, 0x65, 0x6e, 0x74, 0x00, 0x03, 0x0a, 0x00, 0x44, 0x69, 0x66, 0x66, 0x69, 0x63, 0x75, 0x6c,
|
||||
0x74, 0x79, 0x02, 0x00, 0x00, 0x00, 0x08, 0x0f, 0x00, 0x46, 0x6c, 0x61, 0x74, 0x57, 0x6f, 0x72,
|
||||
0x6c, 0x64, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x73, 0xfa, 0x00, 0x7b, 0x22, 0x62, 0x69, 0x6f, 0x6d,
|
||||
0x65, 0x5f, 0x69, 0x64, 0x22, 0x3a, 0x31, 0x2c, 0x22, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c,
|
||||
0x61, 0x79, 0x65, 0x72, 0x73, 0x22, 0x3a, 0x5b, 0x7b, 0x22, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x22, 0x6d, 0x69, 0x6e, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74,
|
||||
0x3a, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x22, 0x2c, 0x22, 0x63, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x22, 0x3a, 0x31, 0x7d, 0x2c, 0x7b, 0x22, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x22, 0x3a, 0x22, 0x6d, 0x69, 0x6e, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74, 0x3a, 0x64, 0x69,
|
||||
0x72, 0x74, 0x22, 0x2c, 0x22, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x3a, 0x32, 0x7d, 0x2c, 0x7b,
|
||||
0x22, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x22, 0x6d, 0x69,
|
||||
0x6e, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74, 0x3a, 0x67, 0x72, 0x61, 0x73, 0x73, 0x5f, 0x62, 0x6c,
|
||||
0x6f, 0x63, 0x6b, 0x22, 0x2c, 0x22, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x3a, 0x31, 0x7d, 0x5d,
|
||||
0x2c, 0x22, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x22, 0x3a, 0x36, 0x2c, 0x22, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65,
|
||||
0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0x22,
|
||||
0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x22,
|
||||
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x5f, 0x31, 0x5f, 0x31,
|
||||
0x38, 0x22, 0x7d, 0x0a, 0x01, 0x0d, 0x00, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x47, 0x61, 0x6d, 0x65,
|
||||
0x54, 0x79, 0x70, 0x65, 0x00, 0x03, 0x08, 0x00, 0x47, 0x61, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65,
|
||||
0x01, 0x00, 0x00, 0x00, 0x03, 0x09, 0x00, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72,
|
||||
0x01, 0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x06, 0x00, 0x31, 0x2e, 0x32, 0x31, 0x2e, 0x32, 0x01,
|
||||
0x0a, 0x00, 0x49, 0x73, 0x48, 0x61, 0x72, 0x64, 0x63, 0x6f, 0x72, 0x65, 0x00, 0x01, 0x0c, 0x00,
|
||||
0x4c, 0x41, 0x4e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x01, 0x01, 0x12, 0x00,
|
||||
0x4c, 0x41, 0x4e, 0x42, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65,
|
||||
0x6e, 0x74, 0x01, 0x04, 0x0a, 0x00, 0x4c, 0x61, 0x73, 0x74, 0x50, 0x6c, 0x61, 0x79, 0x65, 0x64,
|
||||
0x93, 0xcc, 0x04, 0x69, 0x00, 0x00, 0x00, 0x00, 0x08, 0x09, 0x00, 0x4c, 0x65, 0x76, 0x65, 0x6c,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x22, 0x00, 0x4b, 0x49, 0x44, 0xe5, 0x8a, 0xa8, 0xe4, 0xbd, 0x9c, 0xe4,
|
||||
0xbc, 0x98, 0xe5, 0x8c, 0x96, 0x55, 0x4c, 0x54, 0x52, 0x41, 0x5f, 0x58, 0xe5, 0xbc, 0x80, 0xe5,
|
||||
0x8f, 0x91, 0xe6, 0xb5, 0x8b, 0xe8, 0xaf, 0x95, 0x03, 0x13, 0x00, 0x4c, 0x69, 0x6d, 0x69, 0x74,
|
||||
0x65, 0x64, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x58, 0x00, 0x00,
|
||||
0x00, 0x80, 0x03, 0x13, 0x00, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x57, 0x6f, 0x72, 0x6c,
|
||||
0x64, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x59, 0x00, 0x00, 0x00, 0x80, 0x03, 0x13, 0x00, 0x4c,
|
||||
0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x4f, 0x72, 0x69, 0x67, 0x69,
|
||||
0x6e, 0x5a, 0x00, 0x00, 0x00, 0x80, 0x09, 0x1e, 0x00, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d,
|
||||
0x43, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x03, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x0f, 0x00, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x47, 0x61,
|
||||
0x6d, 0x65, 0x01, 0x01, 0x15, 0x00, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x61, 0x79, 0x65,
|
||||
0x72, 0x47, 0x61, 0x6d, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x01, 0x03, 0x0b, 0x00, 0x4e,
|
||||
0x65, 0x74, 0x68, 0x65, 0x72, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x08, 0x00, 0x00, 0x00, 0x03, 0x0e,
|
||||
0x00, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0xae,
|
||||
0x02, 0x00, 0x00, 0x03, 0x08, 0x00, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x02, 0x00,
|
||||
0x00, 0x00, 0x03, 0x17, 0x00, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x42, 0x72, 0x6f,
|
||||
0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x03, 0x00, 0x00, 0x00,
|
||||
0x04, 0x0a, 0x00, 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x53, 0x65, 0x65, 0x64, 0xda, 0x83, 0xfb,
|
||||
0x6a, 0x72, 0x36, 0x25, 0xd5, 0x01, 0x10, 0x00, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x56, 0x31, 0x56,
|
||||
0x69, 0x6c, 0x6c, 0x61, 0x67, 0x65, 0x72, 0x73, 0x00, 0x03, 0x06, 0x00, 0x53, 0x70, 0x61, 0x77,
|
||||
0x6e, 0x58, 0x00, 0x00, 0x00, 0x80, 0x03, 0x06, 0x00, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x59, 0x00,
|
||||
0x00, 0x00, 0x80, 0x03, 0x06, 0x00, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x5a, 0x00, 0x00, 0x00, 0x80,
|
||||
0x03, 0x0e, 0x00, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x0a, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x54, 0x69, 0x6d, 0x65, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x03, 0x0c, 0x00, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x56, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x01, 0x00, 0x00, 0x00, 0x03, 0x12, 0x00, 0x58, 0x42, 0x4c, 0x42, 0x72, 0x6f,
|
||||
0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x03, 0x00, 0x00, 0x00,
|
||||
0x0a, 0x09, 0x00, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x01, 0x0a, 0x00, 0x61,
|
||||
0x74, 0x74, 0x61, 0x63, 0x6b, 0x6d, 0x6f, 0x62, 0x73, 0x00, 0x01, 0x0d, 0x00, 0x61, 0x74, 0x74,
|
||||
0x61, 0x63, 0x6b, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x00, 0x01, 0x05, 0x00, 0x62, 0x75,
|
||||
0x69, 0x6c, 0x64, 0x01, 0x01, 0x10, 0x00, 0x64, 0x6f, 0x6f, 0x72, 0x73, 0x61, 0x6e, 0x64, 0x73,
|
||||
0x77, 0x69, 0x74, 0x63, 0x68, 0x65, 0x73, 0x00, 0x05, 0x08, 0x00, 0x66, 0x6c, 0x79, 0x53, 0x70,
|
||||
0x65, 0x65, 0x64, 0xcd, 0xcc, 0x4c, 0x3d, 0x01, 0x06, 0x00, 0x66, 0x6c, 0x79, 0x69, 0x6e, 0x67,
|
||||
0x00, 0x01, 0x0a, 0x00, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x00, 0x01,
|
||||
0x0c, 0x00, 0x69, 0x6e, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x00, 0x01,
|
||||
0x09, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x00, 0x01, 0x06, 0x00, 0x6d,
|
||||
0x61, 0x79, 0x66, 0x6c, 0x79, 0x00, 0x01, 0x04, 0x00, 0x6d, 0x69, 0x6e, 0x65, 0x01, 0x01, 0x02,
|
||||
0x00, 0x6f, 0x70, 0x00, 0x01, 0x0e, 0x00, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x73, 0x00, 0x01, 0x08, 0x00, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72,
|
||||
0x74, 0x00, 0x05, 0x09, 0x00, 0x77, 0x61, 0x6c, 0x6b, 0x53, 0x70, 0x65, 0x65, 0x64, 0xcd, 0xcc,
|
||||
0xcc, 0x3d, 0x00, 0x01, 0x11, 0x00, 0x62, 0x6f, 0x6e, 0x75, 0x73, 0x43, 0x68, 0x65, 0x73, 0x74,
|
||||
0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x01, 0x01, 0x11, 0x00, 0x62, 0x6f, 0x6e, 0x75, 0x73,
|
||||
0x43, 0x68, 0x65, 0x73, 0x74, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x65, 0x64, 0x00, 0x01, 0x0d, 0x00,
|
||||
0x63, 0x68, 0x65, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x00, 0x01, 0x12,
|
||||
0x00, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6f, 0x75, 0x74,
|
||||
0x70, 0x75, 0x74, 0x01, 0x01, 0x14, 0x00, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x6c,
|
||||
0x6f, 0x63, 0x6b, 0x73, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x01, 0x01, 0x0f, 0x00, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x01, 0x04,
|
||||
0x0b, 0x00, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x63, 0x6b, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0d, 0x00, 0x64, 0x61, 0x79, 0x6c, 0x69, 0x67, 0x68, 0x74,
|
||||
0x43, 0x79, 0x63, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0f, 0x00, 0x64, 0x6f, 0x64, 0x61,
|
||||
0x79, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x01, 0x01, 0x0d, 0x00, 0x64,
|
||||
0x6f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x64, 0x72, 0x6f, 0x70, 0x73, 0x01, 0x01, 0x0a, 0x00,
|
||||
0x64, 0x6f, 0x66, 0x69, 0x72, 0x65, 0x74, 0x69, 0x63, 0x6b, 0x01, 0x01, 0x12, 0x00, 0x64, 0x6f,
|
||||
0x69, 0x6d, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x74, 0x65, 0x72, 0x65, 0x73, 0x70, 0x61, 0x77, 0x6e,
|
||||
0x00, 0x01, 0x0a, 0x00, 0x64, 0x6f, 0x69, 0x6e, 0x73, 0x6f, 0x6d, 0x6e, 0x69, 0x61, 0x01, 0x01,
|
||||
0x11, 0x00, 0x64, 0x6f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x63, 0x72, 0x61, 0x66, 0x74,
|
||||
0x69, 0x6e, 0x67, 0x00, 0x01, 0x09, 0x00, 0x64, 0x6f, 0x6d, 0x6f, 0x62, 0x6c, 0x6f, 0x6f, 0x74,
|
||||
0x01, 0x01, 0x0d, 0x00, 0x64, 0x6f, 0x6d, 0x6f, 0x62, 0x73, 0x70, 0x61, 0x77, 0x6e, 0x69, 0x6e,
|
||||
0x67, 0x01, 0x01, 0x0b, 0x00, 0x64, 0x6f, 0x74, 0x69, 0x6c, 0x65, 0x64, 0x72, 0x6f, 0x70, 0x73,
|
||||
0x01, 0x01, 0x0e, 0x00, 0x64, 0x6f, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x63, 0x79, 0x63,
|
||||
0x6c, 0x65, 0x01, 0x01, 0x0e, 0x00, 0x64, 0x72, 0x6f, 0x77, 0x6e, 0x69, 0x6e, 0x67, 0x64, 0x61,
|
||||
0x6d, 0x61, 0x67, 0x65, 0x01, 0x03, 0x0f, 0x00, 0x65, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x57, 0x6f,
|
||||
0x72, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x00, 0x00, 0x00, 0x00, 0x03, 0x08, 0x00, 0x65, 0x64,
|
||||
0x75, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x01, 0x18, 0x00, 0x65, 0x64, 0x75,
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e,
|
||||
0x61, 0x62, 0x6c, 0x65, 0x64, 0x00, 0x0a, 0x0b, 0x00, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x73, 0x01, 0x24, 0x00, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x64, 0x72, 0x69, 0x76,
|
||||
0x65, 0x6e, 0x5f, 0x76, 0x61, 0x6e, 0x69, 0x6c, 0x6c, 0x61, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
|
||||
0x73, 0x5f, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x01, 0x01, 0x15, 0x00, 0x65,
|
||||
0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x5f, 0x65, 0x76, 0x65, 0x72, 0x5f,
|
||||
0x75, 0x73, 0x65, 0x64, 0x00, 0x01, 0x1e, 0x00, 0x73, 0x61, 0x76, 0x65, 0x64, 0x5f, 0x77, 0x69,
|
||||
0x74, 0x68, 0x5f, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x65, 0x72,
|
||||
0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x00, 0x00, 0x01, 0x0a, 0x00, 0x66, 0x61, 0x6c, 0x6c, 0x64,
|
||||
0x61, 0x6d, 0x61, 0x67, 0x65, 0x01, 0x01, 0x0a, 0x00, 0x66, 0x69, 0x72, 0x65, 0x64, 0x61, 0x6d,
|
||||
0x61, 0x67, 0x65, 0x01, 0x01, 0x0c, 0x00, 0x66, 0x72, 0x65, 0x65, 0x7a, 0x65, 0x64, 0x61, 0x6d,
|
||||
0x61, 0x67, 0x65, 0x01, 0x03, 0x14, 0x00, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x10, 0x27, 0x00, 0x00, 0x01,
|
||||
0x17, 0x00, 0x68, 0x61, 0x73, 0x42, 0x65, 0x65, 0x6e, 0x4c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x49,
|
||||
0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x76, 0x65, 0x01, 0x01, 0x15, 0x00, 0x68, 0x61, 0x73,
|
||||
0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x50, 0x61,
|
||||
0x63, 0x6b, 0x00, 0x01, 0x15, 0x00, 0x68, 0x61, 0x73, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x00, 0x01, 0x0e, 0x00, 0x69,
|
||||
0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x00, 0x01, 0x11,
|
||||
0x00, 0x69, 0x73, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x49, 0x6e, 0x45, 0x64, 0x69, 0x74,
|
||||
0x6f, 0x72, 0x00, 0x01, 0x14, 0x00, 0x69, 0x73, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64,
|
||||
0x46, 0x72, 0x6f, 0x6d, 0x45, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x00, 0x01, 0x14, 0x00, 0x69, 0x73,
|
||||
0x46, 0x72, 0x6f, 0x6d, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61,
|
||||
0x74, 0x65, 0x00, 0x01, 0x13, 0x00, 0x69, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x57, 0x6f, 0x72, 0x6c,
|
||||
0x64, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x00, 0x01, 0x13, 0x00, 0x69, 0x73, 0x52,
|
||||
0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x53, 0x65, 0x65, 0x64, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
|
||||
0x00, 0x01, 0x10, 0x00, 0x69, 0x73, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x57,
|
||||
0x6f, 0x72, 0x6c, 0x64, 0x00, 0x01, 0x1b, 0x00, 0x69, 0x73, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x54,
|
||||
0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x6f, 0x63,
|
||||
0x6b, 0x65, 0x64, 0x00, 0x01, 0x0d, 0x00, 0x6b, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x76, 0x65, 0x6e,
|
||||
0x74, 0x6f, 0x72, 0x79, 0x01, 0x09, 0x15, 0x00, 0x6c, 0x61, 0x73, 0x74, 0x4f, 0x70, 0x65, 0x6e,
|
||||
0x65, 0x64, 0x57, 0x69, 0x74, 0x68, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x03, 0x05, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x0e, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69,
|
||||
0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0d, 0x00, 0x6c, 0x69,
|
||||
0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x00, 0x77, 0x01, 0x00, 0x03,
|
||||
0x11, 0x00, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x44, 0x65,
|
||||
0x70, 0x74, 0x68, 0x10, 0x00, 0x00, 0x00, 0x03, 0x11, 0x00, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65,
|
||||
0x64, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x57, 0x69, 0x64, 0x74, 0x68, 0x10, 0x00, 0x00, 0x00, 0x03,
|
||||
0x15, 0x00, 0x6d, 0x61, 0x78, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x63, 0x68, 0x61, 0x69,
|
||||
0x6e, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0xff, 0xff, 0x00, 0x00, 0x01, 0x0b, 0x00, 0x6d, 0x6f,
|
||||
0x62, 0x67, 0x72, 0x69, 0x65, 0x66, 0x69, 0x6e, 0x67, 0x01, 0x01, 0x13, 0x00, 0x6e, 0x61, 0x74,
|
||||
0x75, 0x72, 0x61, 0x6c, 0x72, 0x65, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x01, 0x01, 0x12, 0x00, 0x6e, 0x65, 0x74, 0x65, 0x61, 0x73, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79,
|
||||
0x70, 0x74, 0x46, 0x6c, 0x61, 0x67, 0x00, 0x09, 0x1f, 0x00, 0x6e, 0x65, 0x74, 0x65, 0x61, 0x73,
|
||||
0x65, 0x53, 0x74, 0x72, 0x6f, 0x6e, 0x67, 0x68, 0x6f, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63,
|
||||
0x74, 0x65, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10,
|
||||
0x00, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x65, 0x76, 0x65,
|
||||
0x6c, 0x01, 0x00, 0x00, 0x00, 0x03, 0x16, 0x00, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x50, 0x65,
|
||||
0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x01, 0x00,
|
||||
0x00, 0x00, 0x03, 0x19, 0x00, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x73, 0x6c, 0x65, 0x65,
|
||||
0x70, 0x69, 0x6e, 0x67, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x64, 0x00,
|
||||
0x00, 0x00, 0x08, 0x04, 0x00, 0x70, 0x72, 0x69, 0x64, 0x00, 0x00, 0x01, 0x19, 0x00, 0x70, 0x72,
|
||||
0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6c, 0x65, 0x73, 0x63, 0x61, 0x6e, 0x62, 0x72, 0x65, 0x61,
|
||||
0x6b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x01, 0x01, 0x03, 0x00, 0x70, 0x76, 0x70, 0x01, 0x05,
|
||||
0x09, 0x00, 0x72, 0x61, 0x69, 0x6e, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x03,
|
||||
0x08, 0x00, 0x72, 0x61, 0x69, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x80, 0xbb, 0x00, 0x00, 0x03, 0x0f,
|
||||
0x00, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x74, 0x69, 0x63, 0x6b, 0x73, 0x70, 0x65, 0x65, 0x64,
|
||||
0x01, 0x00, 0x00, 0x00, 0x01, 0x0d, 0x00, 0x72, 0x65, 0x63, 0x69, 0x70, 0x65, 0x73, 0x75, 0x6e,
|
||||
0x6c, 0x6f, 0x63, 0x6b, 0x01, 0x01, 0x1e, 0x00, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73,
|
||||
0x43, 0x6f, 0x70, 0x69, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x61,
|
||||
0x6c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x00, 0x01, 0x14, 0x00, 0x72, 0x65, 0x73, 0x70, 0x61, 0x77,
|
||||
0x6e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x65, 0x78, 0x70, 0x6c, 0x6f, 0x64, 0x65, 0x01, 0x01,
|
||||
0x13, 0x00, 0x73, 0x65, 0x6e, 0x64, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x66, 0x65, 0x65,
|
||||
0x64, 0x62, 0x61, 0x63, 0x6b, 0x01, 0x03, 0x14, 0x00, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43,
|
||||
0x68, 0x75, 0x6e, 0x6b, 0x54, 0x69, 0x63, 0x6b, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x04, 0x00, 0x00,
|
||||
0x00, 0x01, 0x10, 0x00, 0x73, 0x68, 0x6f, 0x77, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x65, 0x66,
|
||||
0x66, 0x65, 0x63, 0x74, 0x01, 0x01, 0x0f, 0x00, 0x73, 0x68, 0x6f, 0x77, 0x63, 0x6f, 0x6f, 0x72,
|
||||
0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x00, 0x01, 0x0e, 0x00, 0x73, 0x68, 0x6f, 0x77, 0x64,
|
||||
0x61, 0x79, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x00, 0x01, 0x11, 0x00, 0x73, 0x68, 0x6f,
|
||||
0x77, 0x64, 0x65, 0x61, 0x74, 0x68, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x01, 0x01,
|
||||
0x12, 0x00, 0x73, 0x68, 0x6f, 0x77, 0x72, 0x65, 0x63, 0x69, 0x70, 0x65, 0x6d, 0x65, 0x73, 0x73,
|
||||
0x61, 0x67, 0x65, 0x73, 0x01, 0x01, 0x08, 0x00, 0x73, 0x68, 0x6f, 0x77, 0x74, 0x61, 0x67, 0x73,
|
||||
0x01, 0x01, 0x09, 0x00, 0x73, 0x70, 0x61, 0x77, 0x6e, 0x4d, 0x6f, 0x62, 0x73, 0x00, 0x03, 0x0b,
|
||||
0x00, 0x73, 0x70, 0x61, 0x77, 0x6e, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x0a, 0x00, 0x00, 0x00,
|
||||
0x01, 0x13, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x57, 0x69, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x45,
|
||||
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x00, 0x01, 0x14, 0x00, 0x74, 0x65, 0x78, 0x74, 0x75, 0x72,
|
||||
0x65, 0x50, 0x61, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x00, 0x01,
|
||||
0x0b, 0x00, 0x74, 0x6e, 0x74, 0x65, 0x78, 0x70, 0x6c, 0x6f, 0x64, 0x65, 0x73, 0x01, 0x01, 0x15,
|
||||
0x00, 0x74, 0x6e, 0x74, 0x65, 0x78, 0x70, 0x6c, 0x6f, 0x73, 0x69, 0x6f, 0x6e, 0x64, 0x72, 0x6f,
|
||||
0x70, 0x64, 0x65, 0x63, 0x61, 0x79, 0x00, 0x01, 0x13, 0x00, 0x75, 0x73, 0x65, 0x4d, 0x73, 0x61,
|
||||
0x47, 0x61, 0x6d, 0x65, 0x72, 0x74, 0x61, 0x67, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x00, 0x04, 0x0f,
|
||||
0x00, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0xfe, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x0e, 0x00, 0x77, 0x6f, 0x72, 0x6c, 0x64,
|
||||
0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x00, 0x00,
|
||||
];
|
||||
178
src/debug/log.rs
Normal file
178
src/debug/log.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::System::Console::{
|
||||
CONSOLE_SCREEN_BUFFER_INFO, FOREGROUND_BLUE, FOREGROUND_GREEN, FOREGROUND_INTENSITY,
|
||||
FOREGROUND_RED, GetConsoleScreenBufferInfo, GetStdHandle, STD_OUTPUT_HANDLE,
|
||||
SetConsoleTextAttribute,
|
||||
};
|
||||
|
||||
static CONSOLE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ConsoleColor {
|
||||
Default,
|
||||
Green,
|
||||
Red,
|
||||
Yellow,
|
||||
Cyan,
|
||||
DarkGray,
|
||||
}
|
||||
|
||||
pub struct LineBuffer {
|
||||
buffer: String,
|
||||
filter_python: bool,
|
||||
}
|
||||
|
||||
impl LineBuffer {
|
||||
pub fn new(filter_python: bool) -> Self {
|
||||
Self {
|
||||
buffer: String::new(),
|
||||
filter_python,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append<F>(&mut self, bytes: &[u8], mut process_line: F)
|
||||
where
|
||||
F: FnMut(String),
|
||||
{
|
||||
self.buffer.push_str(&String::from_utf8_lossy(bytes));
|
||||
while let Some(pos) = self.buffer.find('\n') {
|
||||
let mut line = self.buffer[..pos].to_string();
|
||||
self.buffer.drain(..=pos);
|
||||
if line.ends_with('\r') {
|
||||
line.pop();
|
||||
}
|
||||
if self.filter_python && !line.contains("[Python] ") {
|
||||
continue;
|
||||
}
|
||||
process_line(line);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush<F>(&mut self, mut process_line: F)
|
||||
where
|
||||
F: FnMut(String),
|
||||
{
|
||||
if self.buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut line = std::mem::take(&mut self.buffer);
|
||||
if line.ends_with('\r') {
|
||||
line.pop();
|
||||
}
|
||||
if self.filter_python && !line.contains("[Python] ") {
|
||||
return;
|
||||
}
|
||||
process_line(line);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_stdout_line(line: &str) {
|
||||
if line.contains(" [INFO][Engine] ") {
|
||||
return;
|
||||
}
|
||||
let color = if line.contains("[INFO][Developer]") {
|
||||
ConsoleColor::DarkGray
|
||||
} else if contains_ignore_ascii_case(line, "SUC") {
|
||||
ConsoleColor::Green
|
||||
} else if contains_ignore_ascii_case(line, "ERROR") {
|
||||
ConsoleColor::Red
|
||||
} else if contains_ignore_ascii_case(line, "WARN") {
|
||||
ConsoleColor::Yellow
|
||||
} else if contains_ignore_ascii_case(line, "DEBUG") {
|
||||
ConsoleColor::Cyan
|
||||
} else {
|
||||
ConsoleColor::Default
|
||||
};
|
||||
print_colored(line, color);
|
||||
}
|
||||
|
||||
pub fn process_stderr_line(line: &str) {
|
||||
let line = rewrite_python_traceback_path(line);
|
||||
print_colored(&line, ConsoleColor::Red);
|
||||
}
|
||||
|
||||
pub fn rewrite_python_traceback_path(line: &str) -> String {
|
||||
let re = Regex::new(r#"File "([A-Za-z0-9_\.]+)", line (\d+)"#).unwrap();
|
||||
re.replace_all(line, |caps: &Captures<'_>| {
|
||||
format!(
|
||||
"File \"{}.py\", line {}",
|
||||
caps[1].replace('.', "/"),
|
||||
&caps[2]
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
pub fn print_colored(message: &str, color: ConsoleColor) {
|
||||
let _guard = CONSOLE_LOCK.lock().unwrap();
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if handle.is_null() {
|
||||
println!("{}", message);
|
||||
return;
|
||||
}
|
||||
let mut info: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed();
|
||||
let has_info = GetConsoleScreenBufferInfo(handle, &mut info) != 0;
|
||||
if color != ConsoleColor::Default {
|
||||
SetConsoleTextAttribute(handle, color_attr(color));
|
||||
}
|
||||
println!("{}", message);
|
||||
if color != ConsoleColor::Default && has_info {
|
||||
SetConsoleTextAttribute(handle, info.wAttributes);
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
println!("{}", message);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn color_attr(color: ConsoleColor) -> u16 {
|
||||
match color {
|
||||
ConsoleColor::Default => FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,
|
||||
ConsoleColor::Green => FOREGROUND_GREEN | FOREGROUND_INTENSITY,
|
||||
ConsoleColor::Red => FOREGROUND_RED | FOREGROUND_INTENSITY,
|
||||
ConsoleColor::Yellow => FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY,
|
||||
ConsoleColor::Cyan => FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY,
|
||||
ConsoleColor::DarkGray => FOREGROUND_INTENSITY,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
|
||||
haystack
|
||||
.as_bytes()
|
||||
.windows(needle.len())
|
||||
.any(|window| window.eq_ignore_ascii_case(needle.as_bytes()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{LineBuffer, rewrite_python_traceback_path};
|
||||
|
||||
#[test]
|
||||
fn process_buffer_append_handles_crlf_half_lines_and_flush() {
|
||||
let mut buffer = LineBuffer::new(true);
|
||||
let mut lines = Vec::new();
|
||||
buffer.append(b"noise\r\n[Python] first\r\n[Python] sec", |line| {
|
||||
lines.push(line)
|
||||
});
|
||||
assert_eq!(lines, vec!["[Python] first"]);
|
||||
buffer.append(b"ond\n", |line| lines.push(line));
|
||||
buffer.flush(|line| lines.push(line));
|
||||
assert_eq!(lines, vec!["[Python] first", "[Python] second"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_traceback_module_path_to_file_path() {
|
||||
assert_eq!(
|
||||
rewrite_python_traceback_path(r#"Traceback File "a.b", line 3"#),
|
||||
r#"Traceback File "a/b.py", line 3"#
|
||||
);
|
||||
}
|
||||
}
|
||||
43
src/debug/mod.rs
Normal file
43
src/debug/mod.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
pub mod addon;
|
||||
pub mod config;
|
||||
pub mod env;
|
||||
pub mod hotreload;
|
||||
pub mod ipc;
|
||||
pub mod level;
|
||||
pub mod log;
|
||||
pub mod nbt;
|
||||
pub mod process;
|
||||
pub mod win;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
pub fn run(project_dir: &Path) -> Result<()> {
|
||||
let mut config = config::load_or_create(project_dir)?;
|
||||
config::ensure_game_executable(project_dir, &mut config)?;
|
||||
|
||||
let mod_dirs = config.included_mod_dirs(project_dir)?;
|
||||
let mut linked_packs = Vec::new();
|
||||
|
||||
if !config::env_is_subprocess_mode() {
|
||||
env::clean_runtime_packs()?;
|
||||
|
||||
if config.include_debug_mod {
|
||||
let debug_pack = addon::register_debug_mod(&config, &mod_dirs)?;
|
||||
println!("[MCDK] 已注册调试MOD:{}", debug_pack.uuid);
|
||||
linked_packs.push(debug_pack);
|
||||
}
|
||||
|
||||
addon::link_user_mod_dirs(&mod_dirs, &mut linked_packs)?;
|
||||
level::prepare_world(&config, &linked_packs)?;
|
||||
}
|
||||
|
||||
let config_arg = if config.auto_join_game_effective() && !config::env_is_subprocess_mode() {
|
||||
Some(level::write_dev_config(&config)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
process::launch_game(&config, config_arg.as_deref(), &mod_dirs)
|
||||
}
|
||||
495
src/debug/nbt.rs
Normal file
495
src/debug/nbt.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use rand::RngCore;
|
||||
|
||||
use crate::error::{CliError, Result};
|
||||
|
||||
use super::config::DebugConfig;
|
||||
|
||||
#[path = "level_template.rs"]
|
||||
mod level_template;
|
||||
|
||||
const TAG_END: u8 = 0;
|
||||
const TAG_BYTE: u8 = 1;
|
||||
const TAG_SHORT: u8 = 2;
|
||||
const TAG_INT: u8 = 3;
|
||||
const TAG_LONG: u8 = 4;
|
||||
const TAG_FLOAT: u8 = 5;
|
||||
const TAG_DOUBLE: u8 = 6;
|
||||
const TAG_BYTE_ARRAY: u8 = 7;
|
||||
const TAG_STRING: u8 = 8;
|
||||
const TAG_LIST: u8 = 9;
|
||||
const TAG_COMPOUND: u8 = 10;
|
||||
const TAG_INT_ARRAY: u8 = 11;
|
||||
const TAG_LONG_ARRAY: u8 = 12;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Tag {
|
||||
Byte(i8),
|
||||
Short(i16),
|
||||
Int(i32),
|
||||
Long(i64),
|
||||
Float(f32),
|
||||
Double(f64),
|
||||
ByteArray(Vec<i8>),
|
||||
String(String),
|
||||
List { element_type: u8, items: Vec<Tag> },
|
||||
Compound(Vec<(String, Tag)>),
|
||||
IntArray(Vec<i32>),
|
||||
LongArray(Vec<i64>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LevelDat {
|
||||
version: u32,
|
||||
root_name: String,
|
||||
root: Tag,
|
||||
}
|
||||
|
||||
impl LevelDat {
|
||||
pub fn parse(bytes: &[u8]) -> Result<Self> {
|
||||
if bytes.len() < 8 {
|
||||
return Err(CliError::InvalidData(
|
||||
"level.dat is shorter than Bedrock header".to_string(),
|
||||
));
|
||||
}
|
||||
let version = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
|
||||
let declared_len = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
||||
let payload = &bytes[8..];
|
||||
if declared_len != payload.len() {
|
||||
return Err(CliError::InvalidData(format!(
|
||||
"level.dat payload length mismatch: header={declared_len}, actual={}",
|
||||
payload.len()
|
||||
)));
|
||||
}
|
||||
let mut reader = Reader {
|
||||
bytes: payload,
|
||||
pos: 0,
|
||||
};
|
||||
let tag_type = reader.u8()?;
|
||||
if tag_type != TAG_COMPOUND {
|
||||
return Err(CliError::InvalidData(
|
||||
"level.dat root tag is not a compound".to_string(),
|
||||
));
|
||||
}
|
||||
let root_name = reader.string()?;
|
||||
let root = reader.tag_payload(TAG_COMPOUND)?;
|
||||
if reader.pos != payload.len() {
|
||||
return Err(CliError::InvalidData(
|
||||
"trailing bytes after level.dat root compound".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
version,
|
||||
root_name,
|
||||
root,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
||||
let mut payload = Vec::new();
|
||||
payload.push(TAG_COMPOUND);
|
||||
write_string(&mut payload, &self.root_name)?;
|
||||
write_tag_payload(&mut payload, &self.root)?;
|
||||
|
||||
let mut bytes = Vec::with_capacity(8 + payload.len());
|
||||
bytes.extend_from_slice(&self.version.to_le_bytes());
|
||||
bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&payload);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn set_root(&mut self, key: &str, value: Tag) -> Result<()> {
|
||||
let Tag::Compound(items) = &mut self.root else {
|
||||
return Err(CliError::InvalidData(
|
||||
"level.dat root is not compound".to_string(),
|
||||
));
|
||||
};
|
||||
set_compound_item(items, key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn root_value(&self, key: &str) -> Option<&Tag> {
|
||||
let Tag::Compound(items) = &self.root else {
|
||||
return None;
|
||||
};
|
||||
items
|
||||
.iter()
|
||||
.find(|(name, _)| name == key)
|
||||
.map(|(_, tag)| tag)
|
||||
}
|
||||
|
||||
pub fn root_compound_mut(&mut self, key: &str) -> Result<&mut Vec<(String, Tag)>> {
|
||||
let Tag::Compound(items) = &mut self.root else {
|
||||
return Err(CliError::InvalidData(
|
||||
"level.dat root is not compound".to_string(),
|
||||
));
|
||||
};
|
||||
if let Some(index) = items.iter().position(|(name, _)| name == key) {
|
||||
if !matches!(items[index].1, Tag::Compound(_)) {
|
||||
items[index].1 = Tag::Compound(Vec::new());
|
||||
}
|
||||
let Tag::Compound(child) = &mut items[index].1 else {
|
||||
unreachable!()
|
||||
};
|
||||
return Ok(child);
|
||||
}
|
||||
items.push((key.to_string(), Tag::Compound(Vec::new())));
|
||||
let Tag::Compound(child) = &mut items.last_mut().unwrap().1 else {
|
||||
unreachable!()
|
||||
};
|
||||
Ok(child)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_level_dat(config: &DebugConfig) -> Result<Vec<u8>> {
|
||||
let mut level = LevelDat::parse(level_template::LEVEL_DAT_TEMPLATE)?;
|
||||
apply_config(&mut level, config, true)?;
|
||||
level.to_bytes()
|
||||
}
|
||||
|
||||
pub fn update_level_dat_world_data(
|
||||
bytes: &[u8],
|
||||
config: &DebugConfig,
|
||||
init: bool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut level = LevelDat::parse(bytes)?;
|
||||
apply_config(&mut level, config, init)?;
|
||||
level.to_bytes()
|
||||
}
|
||||
|
||||
pub fn update_level_dat_last_played(bytes: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut level = LevelDat::parse(bytes)?;
|
||||
level.set_root("LastPlayed", Tag::Long(now_seconds()))?;
|
||||
level.to_bytes()
|
||||
}
|
||||
|
||||
fn apply_config(level: &mut LevelDat, config: &DebugConfig, init: bool) -> Result<()> {
|
||||
level.set_root("LastPlayed", Tag::Long(now_seconds()))?;
|
||||
level.set_root("LevelName", Tag::String(config.world_name.clone()))?;
|
||||
if init && config.world_type != 2 {
|
||||
level.set_root(
|
||||
"RandomSeed",
|
||||
Tag::Long(config.world_seed.unwrap_or_else(random_seed)),
|
||||
)?;
|
||||
}
|
||||
level.set_root("GameType", Tag::Int(config.game_mode))?;
|
||||
if init {
|
||||
level.set_root("Generator", Tag::Int(config.world_type))?;
|
||||
}
|
||||
level.set_root("keepInventory", Tag::Byte(config.keep_inventory as i8))?;
|
||||
level.set_root("cheatsEnabled", Tag::Byte(config.enable_cheats as i8))?;
|
||||
level.set_root("doweathercycle", Tag::Byte(config.do_weather_cycle as i8))?;
|
||||
level.set_root("dodaylightcycle", Tag::Byte(config.do_daylight_cycle as i8))?;
|
||||
|
||||
if let Some(experiments) = &config.experiment_options {
|
||||
let child = level.root_compound_mut("experiments")?;
|
||||
set_compound_item(
|
||||
child,
|
||||
"data_driven_biomes",
|
||||
Tag::Byte(experiments.data_driven_biomes as i8),
|
||||
);
|
||||
set_compound_item(
|
||||
child,
|
||||
"data_driven_items",
|
||||
Tag::Byte(experiments.data_driven_items as i8),
|
||||
);
|
||||
set_compound_item(
|
||||
child,
|
||||
"experimental_molang_features",
|
||||
Tag::Byte(experiments.experimental_molang_features as i8),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn now_seconds() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn random_seed() -> i64 {
|
||||
rand::thread_rng().next_u64() as i64
|
||||
}
|
||||
|
||||
fn set_compound_item(items: &mut Vec<(String, Tag)>, key: &str, value: Tag) {
|
||||
if let Some((_, slot)) = items.iter_mut().find(|(name, _)| name == key) {
|
||||
*slot = value;
|
||||
} else {
|
||||
items.push((key.to_string(), value));
|
||||
}
|
||||
}
|
||||
|
||||
struct Reader<'a> {
|
||||
bytes: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl Reader<'_> {
|
||||
fn take(&mut self, len: usize) -> Result<&[u8]> {
|
||||
let end = self
|
||||
.pos
|
||||
.checked_add(len)
|
||||
.ok_or_else(|| CliError::InvalidData("NBT offset overflow".to_string()))?;
|
||||
if end > self.bytes.len() {
|
||||
return Err(CliError::InvalidData(
|
||||
"unexpected end of NBT data".to_string(),
|
||||
));
|
||||
}
|
||||
let out = &self.bytes[self.pos..end];
|
||||
self.pos = end;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn u8(&mut self) -> Result<u8> {
|
||||
Ok(self.take(1)?[0])
|
||||
}
|
||||
fn i8(&mut self) -> Result<i8> {
|
||||
Ok(self.u8()? as i8)
|
||||
}
|
||||
fn i16(&mut self) -> Result<i16> {
|
||||
Ok(i16::from_le_bytes(self.take(2)?.try_into().unwrap()))
|
||||
}
|
||||
fn i32(&mut self) -> Result<i32> {
|
||||
Ok(i32::from_le_bytes(self.take(4)?.try_into().unwrap()))
|
||||
}
|
||||
fn i64(&mut self) -> Result<i64> {
|
||||
Ok(i64::from_le_bytes(self.take(8)?.try_into().unwrap()))
|
||||
}
|
||||
fn f32(&mut self) -> Result<f32> {
|
||||
Ok(f32::from_le_bytes(self.take(4)?.try_into().unwrap()))
|
||||
}
|
||||
fn f64(&mut self) -> Result<f64> {
|
||||
Ok(f64::from_le_bytes(self.take(8)?.try_into().unwrap()))
|
||||
}
|
||||
|
||||
fn string(&mut self) -> Result<String> {
|
||||
let len = u16::from_le_bytes(self.take(2)?.try_into().unwrap()) as usize;
|
||||
let bytes = self.take(len)?;
|
||||
String::from_utf8(bytes.to_vec())
|
||||
.map_err(|err| CliError::InvalidData(format!("NBT string is not UTF-8: {err}")))
|
||||
}
|
||||
|
||||
fn tag_payload(&mut self, tag_type: u8) -> Result<Tag> {
|
||||
match tag_type {
|
||||
TAG_BYTE => Ok(Tag::Byte(self.i8()?)),
|
||||
TAG_SHORT => Ok(Tag::Short(self.i16()?)),
|
||||
TAG_INT => Ok(Tag::Int(self.i32()?)),
|
||||
TAG_LONG => Ok(Tag::Long(self.i64()?)),
|
||||
TAG_FLOAT => Ok(Tag::Float(self.f32()?)),
|
||||
TAG_DOUBLE => Ok(Tag::Double(self.f64()?)),
|
||||
TAG_BYTE_ARRAY => {
|
||||
let len = checked_len(self.i32()?)?;
|
||||
let mut values = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
values.push(self.i8()?);
|
||||
}
|
||||
Ok(Tag::ByteArray(values))
|
||||
}
|
||||
TAG_STRING => Ok(Tag::String(self.string()?)),
|
||||
TAG_LIST => {
|
||||
let element_type = self.u8()?;
|
||||
let len = checked_len(self.i32()?)?;
|
||||
let mut items = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
items.push(self.tag_payload(element_type)?);
|
||||
}
|
||||
Ok(Tag::List {
|
||||
element_type,
|
||||
items,
|
||||
})
|
||||
}
|
||||
TAG_COMPOUND => {
|
||||
let mut items = Vec::new();
|
||||
loop {
|
||||
let child_type = self.u8()?;
|
||||
if child_type == TAG_END {
|
||||
break;
|
||||
}
|
||||
let name = self.string()?;
|
||||
let value = self.tag_payload(child_type)?;
|
||||
items.push((name, value));
|
||||
}
|
||||
Ok(Tag::Compound(items))
|
||||
}
|
||||
TAG_INT_ARRAY => {
|
||||
let len = checked_len(self.i32()?)?;
|
||||
let mut values = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
values.push(self.i32()?);
|
||||
}
|
||||
Ok(Tag::IntArray(values))
|
||||
}
|
||||
TAG_LONG_ARRAY => {
|
||||
let len = checked_len(self.i32()?)?;
|
||||
let mut values = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
values.push(self.i64()?);
|
||||
}
|
||||
Ok(Tag::LongArray(values))
|
||||
}
|
||||
_ => Err(CliError::InvalidData(format!(
|
||||
"unsupported NBT tag type {tag_type}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_len(value: i32) -> Result<usize> {
|
||||
usize::try_from(value)
|
||||
.map_err(|_| CliError::InvalidData(format!("negative NBT collection length {value}")))
|
||||
}
|
||||
|
||||
fn write_named_tag(out: &mut Vec<u8>, name: &str, tag: &Tag) -> Result<()> {
|
||||
out.push(tag_id(tag));
|
||||
write_string(out, name)?;
|
||||
write_tag_payload(out, tag)
|
||||
}
|
||||
|
||||
fn write_string(out: &mut Vec<u8>, value: &str) -> Result<()> {
|
||||
let len = u16::try_from(value.len())
|
||||
.map_err(|_| CliError::InvalidData("NBT string is too long".to_string()))?;
|
||||
out.extend_from_slice(&len.to_le_bytes());
|
||||
out.extend_from_slice(value.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_tag_payload(out: &mut Vec<u8>, tag: &Tag) -> Result<()> {
|
||||
match tag {
|
||||
Tag::Byte(value) => out.push(*value as u8),
|
||||
Tag::Short(value) => out.extend_from_slice(&value.to_le_bytes()),
|
||||
Tag::Int(value) => out.extend_from_slice(&value.to_le_bytes()),
|
||||
Tag::Long(value) => out.extend_from_slice(&value.to_le_bytes()),
|
||||
Tag::Float(value) => out.extend_from_slice(&value.to_le_bytes()),
|
||||
Tag::Double(value) => out.extend_from_slice(&value.to_le_bytes()),
|
||||
Tag::ByteArray(values) => {
|
||||
write_len(out, values.len())?;
|
||||
out.extend(values.iter().map(|value| *value as u8));
|
||||
}
|
||||
Tag::String(value) => write_string(out, value)?,
|
||||
Tag::List {
|
||||
element_type,
|
||||
items,
|
||||
} => {
|
||||
out.push(*element_type);
|
||||
write_len(out, items.len())?;
|
||||
for item in items {
|
||||
if tag_id(item) != *element_type {
|
||||
return Err(CliError::InvalidData(
|
||||
"NBT list contains mixed element types".to_string(),
|
||||
));
|
||||
}
|
||||
write_tag_payload(out, item)?;
|
||||
}
|
||||
}
|
||||
Tag::Compound(items) => {
|
||||
let mut names = HashSet::with_capacity(items.len());
|
||||
for (name, value) in items {
|
||||
if !names.insert(name) {
|
||||
return Err(CliError::InvalidData(format!(
|
||||
"duplicate NBT compound key {name}"
|
||||
)));
|
||||
}
|
||||
write_named_tag(out, name, value)?;
|
||||
}
|
||||
out.push(TAG_END);
|
||||
}
|
||||
Tag::IntArray(values) => {
|
||||
write_len(out, values.len())?;
|
||||
for value in values {
|
||||
out.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
}
|
||||
Tag::LongArray(values) => {
|
||||
write_len(out, values.len())?;
|
||||
for value in values {
|
||||
out.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_len(out: &mut Vec<u8>, len: usize) -> Result<()> {
|
||||
let len = i32::try_from(len)
|
||||
.map_err(|_| CliError::InvalidData("NBT collection is too long".to_string()))?;
|
||||
out.extend_from_slice(&len.to_le_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tag_id(tag: &Tag) -> u8 {
|
||||
match tag {
|
||||
Tag::Byte(_) => TAG_BYTE,
|
||||
Tag::Short(_) => TAG_SHORT,
|
||||
Tag::Int(_) => TAG_INT,
|
||||
Tag::Long(_) => TAG_LONG,
|
||||
Tag::Float(_) => TAG_FLOAT,
|
||||
Tag::Double(_) => TAG_DOUBLE,
|
||||
Tag::ByteArray(_) => TAG_BYTE_ARRAY,
|
||||
Tag::String(_) => TAG_STRING,
|
||||
Tag::List { .. } => TAG_LIST,
|
||||
Tag::Compound(_) => TAG_COMPOUND,
|
||||
Tag::IntArray(_) => TAG_INT_ARRAY,
|
||||
Tag::LongArray(_) => TAG_LONG_ARRAY,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::debug::config::{DebugConfig, NeteaseConfig};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
fn config() -> DebugConfig {
|
||||
DebugConfig {
|
||||
included_mod_dirs: Vec::new(),
|
||||
world_seed: Some(123),
|
||||
reset_world: false,
|
||||
world_name: "UnitWorld".to_string(),
|
||||
world_folder_name: "UnitWorld".to_string(),
|
||||
auto_join_game: true,
|
||||
include_debug_mod: true,
|
||||
auto_hot_reload_mods: true,
|
||||
world_type: 1,
|
||||
game_mode: 1,
|
||||
enable_cheats: true,
|
||||
keep_inventory: true,
|
||||
do_weather_cycle: true,
|
||||
do_daylight_cycle: true,
|
||||
game_executable_path: String::new(),
|
||||
debug_options: Value::Object(Map::new()),
|
||||
netease_config: NeteaseConfig::default(),
|
||||
user_name: "developer".to_string(),
|
||||
skin_info: None,
|
||||
experiment_options: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_round_trips() {
|
||||
let level = LevelDat::parse(level_template::LEVEL_DAT_TEMPLATE).unwrap();
|
||||
let bytes = level.to_bytes().unwrap();
|
||||
let reparsed = LevelDat::parse(&bytes).unwrap();
|
||||
assert_eq!(level, reparsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updates_required_world_fields() {
|
||||
let bytes = create_level_dat(&config()).unwrap();
|
||||
let level = LevelDat::parse(&bytes).unwrap();
|
||||
assert_eq!(
|
||||
level.root_value("LevelName"),
|
||||
Some(&Tag::String("UnitWorld".to_string()))
|
||||
);
|
||||
assert_eq!(level.root_value("GameType"), Some(&Tag::Int(1)));
|
||||
assert_eq!(level.root_value("RandomSeed"), Some(&Tag::Long(123)));
|
||||
assert!(matches!(level.root_value("LastPlayed"), Some(Tag::Long(value)) if *value > 0));
|
||||
}
|
||||
}
|
||||
250
src/debug/process.rs
Normal file
250
src/debug/process.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use std::{io, path::Path, thread};
|
||||
|
||||
use crate::error::{CliError, Result};
|
||||
|
||||
use super::{
|
||||
config::{DebugConfig, ResolvedModDir},
|
||||
hotreload::HotReloadTask,
|
||||
ipc::DebugIpcServer,
|
||||
log::{self, LineBuffer},
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{
|
||||
CloseHandle, ERROR_BROKEN_PIPE, GetLastError, HANDLE, HANDLE_FLAG_INHERIT,
|
||||
SetHandleInformation,
|
||||
},
|
||||
Storage::FileSystem::ReadFile,
|
||||
System::{
|
||||
Console::{GetStdHandle, STD_INPUT_HANDLE},
|
||||
Pipes::CreatePipe,
|
||||
Threading::{
|
||||
CREATE_UNICODE_ENVIRONMENT, CreateProcessW, INFINITE, PROCESS_INFORMATION,
|
||||
STARTF_USESTDHANDLES, STARTUPINFOW, WaitForSingleObject,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
|
||||
|
||||
#[cfg(windows)]
|
||||
use super::win::WinHandle;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn launch_game(
|
||||
config: &DebugConfig,
|
||||
config_arg: Option<&Path>,
|
||||
mod_dirs: &[ResolvedModDir],
|
||||
) -> Result<()> {
|
||||
let enable_ipc = config.auto_hot_reload_mods;
|
||||
let ipc = if enable_ipc {
|
||||
let server = DebugIpcServer::start()?;
|
||||
println!("[MCDK] IPC调试服务器已启动,端口:{}", server.port());
|
||||
Some(server)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let command = build_command(config, config_arg);
|
||||
let mut command_w = wide_null(OsStr::new(&command));
|
||||
let env_block = ipc
|
||||
.as_ref()
|
||||
.map(|server| build_environment_block(server.port()));
|
||||
|
||||
let mut security = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: ptr::null_mut(),
|
||||
bInheritHandle: 1,
|
||||
};
|
||||
|
||||
let (out_read, out_write) = create_pipe_pair(&mut security)?;
|
||||
let (err_read, err_write) = create_pipe_pair(&mut security)?;
|
||||
|
||||
let mut startup: STARTUPINFOW = unsafe { std::mem::zeroed() };
|
||||
startup.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||
startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.hStdOutput = out_write.raw();
|
||||
startup.hStdError = err_write.raw();
|
||||
startup.hStdInput = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
|
||||
|
||||
let mut process_info: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
let env_ptr = env_block
|
||||
.as_ref()
|
||||
.map(|block| block.as_ptr().cast_mut().cast())
|
||||
.unwrap_or(ptr::null_mut());
|
||||
let creation_flags = if env_block.is_some() {
|
||||
CREATE_UNICODE_ENVIRONMENT
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let ok = unsafe {
|
||||
CreateProcessW(
|
||||
ptr::null(),
|
||||
command_w.as_mut_ptr(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
creation_flags,
|
||||
env_ptr,
|
||||
ptr::null(),
|
||||
&startup,
|
||||
&mut process_info,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
let err = io::Error::last_os_error();
|
||||
return Err(CliError::Io(io::Error::new(
|
||||
err.kind(),
|
||||
format!("CreateProcessW failed for command {command:?}: {err}"),
|
||||
)));
|
||||
}
|
||||
|
||||
drop(out_write);
|
||||
drop(err_write);
|
||||
|
||||
let pid = process_info.dwProcessId;
|
||||
let stdout_filter = config.include_debug_mod;
|
||||
let stderr_filter = config.include_debug_mod;
|
||||
let stdout_thread =
|
||||
thread::spawn(move || read_pipe(out_read, stdout_filter, log::process_stdout_line));
|
||||
let stderr_thread =
|
||||
thread::spawn(move || read_pipe(err_read, stderr_filter, log::process_stderr_line));
|
||||
|
||||
let mut hotreload = if config.auto_hot_reload_mods {
|
||||
ipc.clone()
|
||||
.and_then(|server| HotReloadTask::start(pid, mod_dirs, server))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
unsafe { WaitForSingleObject(process_info.hProcess, INFINITE) };
|
||||
|
||||
if let Some(task) = &mut hotreload {
|
||||
task.safe_exit();
|
||||
}
|
||||
if let Some(server) = &ipc {
|
||||
server.safe_exit();
|
||||
}
|
||||
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
|
||||
unsafe {
|
||||
CloseHandle(process_info.hProcess);
|
||||
CloseHandle(process_info.hThread);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn launch_game(
|
||||
_config: &DebugConfig,
|
||||
_config_arg: Option<&Path>,
|
||||
_mod_dirs: &[ResolvedModDir],
|
||||
) -> Result<()> {
|
||||
Err(CliError::InvalidInput(
|
||||
"debug launch is only supported on Windows".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn build_command(config: &DebugConfig, config_arg: Option<&Path>) -> String {
|
||||
let mut command = String::new();
|
||||
command.push('"');
|
||||
command.push_str(&config.game_executable_path);
|
||||
command.push('"');
|
||||
if !config.netease_config.chat_extension {
|
||||
command.push_str(" chatExtension=false");
|
||||
}
|
||||
if let Some(path) = config_arg {
|
||||
command.push_str(" config=\"");
|
||||
command.push_str(&path.to_string_lossy().replace('\\', "/"));
|
||||
command.push('"');
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn create_pipe_pair(security: &mut SECURITY_ATTRIBUTES) -> Result<(WinHandle, WinHandle)> {
|
||||
let mut read: HANDLE = ptr::null_mut();
|
||||
let mut write: HANDLE = ptr::null_mut();
|
||||
let ok = unsafe { CreatePipe(&mut read, &mut write, security, 0) };
|
||||
if ok == 0 {
|
||||
return Err(CliError::Io(io::Error::last_os_error()));
|
||||
}
|
||||
let read = WinHandle::new(read)?;
|
||||
let write = WinHandle::new(write)?;
|
||||
let ok = unsafe { SetHandleInformation(read.raw(), HANDLE_FLAG_INHERIT, 0) };
|
||||
if ok == 0 {
|
||||
return Err(CliError::Io(io::Error::last_os_error()));
|
||||
}
|
||||
Ok((read, write))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn read_pipe<F>(pipe: WinHandle, filter_python: bool, process_line: F)
|
||||
where
|
||||
F: Fn(&str) + Send + 'static,
|
||||
{
|
||||
const BUFSZ: usize = 4096;
|
||||
let mut line_buffer = LineBuffer::new(filter_python);
|
||||
let mut buffer = [0u8; BUFSZ];
|
||||
|
||||
loop {
|
||||
let mut bytes_read = 0u32;
|
||||
let ok = unsafe {
|
||||
ReadFile(
|
||||
pipe.raw(),
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len() as u32,
|
||||
&mut bytes_read,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
let err = unsafe { GetLastError() };
|
||||
if err == ERROR_BROKEN_PIPE {
|
||||
line_buffer.flush(|line| process_line(&line));
|
||||
}
|
||||
break;
|
||||
}
|
||||
if bytes_read == 0 {
|
||||
line_buffer.flush(|line| process_line(&line));
|
||||
break;
|
||||
}
|
||||
line_buffer.append(&buffer[..bytes_read as usize], |line| process_line(&line));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn build_environment_block(ipc_port: u16) -> Vec<u16> {
|
||||
let mut pairs: Vec<(Vec<u16>, Vec<u16>)> = std::env::vars_os()
|
||||
.map(|(key, value)| (key.encode_wide().collect(), value.encode_wide().collect()))
|
||||
.collect();
|
||||
pairs.push((
|
||||
OsStr::new("MCDEV_DEBUG_IPC_PORT").encode_wide().collect(),
|
||||
OsStr::new(&ipc_port.to_string()).encode_wide().collect(),
|
||||
));
|
||||
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let mut block = Vec::new();
|
||||
for (key, value) in pairs {
|
||||
block.extend(key);
|
||||
block.push('=' as u16);
|
||||
block.extend(value);
|
||||
block.push(0);
|
||||
}
|
||||
block.push(0);
|
||||
block
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn wide_null(value: &OsStr) -> Vec<u16> {
|
||||
value.encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
157
src/debug/win.rs
Normal file
157
src/debug/win.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::{ffi::OsStr, io, path::Path};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::{mem, os::windows::ffi::OsStrExt, ptr};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE},
|
||||
Storage::FileSystem::{
|
||||
CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, OPEN_EXISTING,
|
||||
},
|
||||
System::IO::DeviceIoControl,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
const IO_REPARSE_TAG_MOUNT_POINT: u32 = 0xA0000003;
|
||||
#[cfg(windows)]
|
||||
const FSCTL_SET_REPARSE_POINT: u32 = 0x0009_00A4;
|
||||
#[cfg(windows)]
|
||||
const GENERIC_WRITE_ACCESS: u32 = 0x4000_0000;
|
||||
|
||||
#[cfg(windows)]
|
||||
#[repr(C)]
|
||||
struct ReparseDataBufferHeader {
|
||||
reparse_tag: u32,
|
||||
reparse_data_length: u16,
|
||||
reserved: u16,
|
||||
substitute_name_offset: u16,
|
||||
substitute_name_length: u16,
|
||||
print_name_offset: u16,
|
||||
print_name_length: u16,
|
||||
}
|
||||
|
||||
pub struct WinHandle(#[cfg(windows)] pub HANDLE);
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe impl Send for WinHandle {}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl WinHandle {
|
||||
pub fn new(handle: HANDLE) -> io::Result<Self> {
|
||||
if handle == INVALID_HANDLE_VALUE || handle.is_null() {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(Self(handle))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> HANDLE {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl Drop for WinHandle {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE {
|
||||
unsafe { CloseHandle(self.0) };
|
||||
self.0 = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn wide_null(value: &OsStr) -> Vec<u16> {
|
||||
value.encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn create_junction(target: &Path, link: &Path) -> io::Result<()> {
|
||||
std::fs::create_dir_all(link.parent().unwrap_or_else(|| Path::new(".")))?;
|
||||
if link.exists() {
|
||||
std::fs::remove_dir_all(link)?;
|
||||
}
|
||||
std::fs::create_dir(link)?;
|
||||
|
||||
let link_w = wide_null(link.as_os_str());
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
link_w.as_ptr(),
|
||||
GENERIC_WRITE_ACCESS,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
let handle = WinHandle::new(handle)?;
|
||||
|
||||
let real = std::fs::canonicalize(target)?;
|
||||
let real_full_w: Vec<u16> = real.as_os_str().encode_wide().collect();
|
||||
// Mount point reparse data uses `\??\` for SubstituteName and the
|
||||
// plain DOS path for PrintName; `canonicalize` returns `\\?\...`.
|
||||
const VERBATIM_PREFIX: [u16; 4] = [b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16];
|
||||
let real_w = if real_full_w.starts_with(&VERBATIM_PREFIX) {
|
||||
&real_full_w[VERBATIM_PREFIX.len()..]
|
||||
} else {
|
||||
real_full_w.as_slice()
|
||||
};
|
||||
|
||||
const SUBSTITUTE_PREFIX: [u16; 4] = [b'\\' as u16, b'?' as u16, b'?' as u16, b'\\' as u16];
|
||||
let mut substitute_w = SUBSTITUTE_PREFIX.to_vec();
|
||||
substitute_w.extend_from_slice(real_w);
|
||||
|
||||
let subst_len = substitute_w.len() * mem::size_of::<u16>();
|
||||
let print_len = real_w.len() * mem::size_of::<u16>();
|
||||
let header_len = mem::size_of::<ReparseDataBufferHeader>();
|
||||
let total_len = header_len + subst_len + 2 + print_len + 2;
|
||||
let mut buffer = vec![0u8; total_len];
|
||||
|
||||
unsafe {
|
||||
let header = buffer.as_mut_ptr() as *mut ReparseDataBufferHeader;
|
||||
(*header).reparse_tag = IO_REPARSE_TAG_MOUNT_POINT;
|
||||
(*header).reparse_data_length = (total_len - 8) as u16;
|
||||
(*header).reserved = 0;
|
||||
(*header).substitute_name_offset = 0;
|
||||
(*header).substitute_name_length = subst_len as u16;
|
||||
(*header).print_name_offset = (subst_len + 2) as u16;
|
||||
(*header).print_name_length = print_len as u16;
|
||||
|
||||
let path_buffer = buffer.as_mut_ptr().add(header_len) as *mut u16;
|
||||
ptr::copy_nonoverlapping(substitute_w.as_ptr(), path_buffer, substitute_w.len());
|
||||
ptr::copy_nonoverlapping(
|
||||
real_w.as_ptr(),
|
||||
path_buffer.add(substitute_w.len() + 1),
|
||||
real_w.len(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut bytes_returned = 0u32;
|
||||
let ok = unsafe {
|
||||
DeviceIoControl(
|
||||
handle.raw(),
|
||||
FSCTL_SET_REPARSE_POINT,
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len() as u32,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
&mut bytes_returned,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn create_junction(_target: &Path, _link: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"debug launch is only supported on Windows",
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user