diff --git a/README.md b/README.md index 2062a45..e83d363 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,26 @@ ![预览图](https://user-images.githubusercontent.com/39523898/208238006-900bd5fe-f9f7-42a9-b726-da829162fbed.png) +![MSRV 1.75.0](https://img.shields.io/badge/MSRV-1.75.0-orange) + 使用 Rust 编程语言编写,内存占用相当之小,性能相当之优秀,针对二进制大小做了力所能及的压缩优化。 原生跨平台,支持 Windows,Linux,MacOS 三大主流操作系统。 +- 官网:[https://steve-xmh.github.io/scl](https://steve-xmh.github.io/scl) +- 开发文档:[https://steve-xmh.github.io/scl/scl-docs](https://steve-xmh.github.io/scl/scl-docs) - 设计图:[https://www.figma.com/file/i2Sl8uD5nKS4dIki0yK29n/Sharp-Craft-Launcher-%E8%AE%BE%E8%AE%A1%E5%9B%BE](https://www.figma.com/file/i2Sl8uD5nKS4dIki0yK29n/Sharp-Craft-Launcher-%E8%AE%BE%E8%AE%A1%E5%9B%BE) -- 介绍/发布贴:[https://www.mcbbs.net/thread-1223867-1-1.html](https://www.mcbbs.net/thread-1223867-1-1.html) +- 介绍/发布贴(MineBBS):[https://www.minebbs.com/resources/sharp-craft-launcher-_-_.7177/](https://www.minebbs.com/resources/sharp-craft-launcher-_-_.7177/) +- 介绍/发布贴(MCBBS):[https://www.mcbbs.net/thread-1223867-1-1.html](https://www.mcbbs.net/thread-1223867-1-1.html) - 官网源代码分支:[https://github.com/Steve-xmh/scl/tree/site](https://github.com/Steve-xmh/scl/tree/site) ## 源代码架构 -- `scl-core`: 启动器核心库,包含了游戏启动,游戏下载,正版登录,模组下载等游戏操作功能 -- `scl-webview`: 启动器 WebView 网页浏览器库,提供了用于微软正版登录的浏览器窗口 -- `scl-macro`: 启动器过程宏库,包含了部分用于代码生成的过程宏代码,目前包含图标代码生成的简易过程宏 -- `scl-gui-animation`: 启动器图形页面动画函数库,包含了一些方便用来制作非线性动画的函数和工具类 -- `scl-gui-widgets`: 启动器图形页面组件库,基于 [Druid](https://github.com/linebender/druid) 框架,提供了大量基于 WinUI3 设计规范制作的图形页面组件 +- `scl-core`: [![](https://img.shields.io/badge/docs-passing-green)](https://steve-xmh.github.io/scl/scl-doc/scl_core/index.html) 启动器核心库,包含了游戏启动,游戏下载,正版登录,模组下载等游戏操作功能 +- `scl-webview`: [![](https://img.shields.io/badge/docs-passing-green)](https://steve-xmh.github.io/scl/scl-doc/scl_webview/index.html) 启动器 WebView 网页浏览器库,提供了用于微软正版登录的浏览器窗口 +- `scl-macro`: [![](https://img.shields.io/badge/docs-passing-green)](https://steve-xmh.github.io/scl/scl-doc/scl_macro/index.html) 启动器过程宏库,包含了部分用于代码生成的过程宏代码,目前包含图标代码生成的简易过程宏 +- `scl-gui-animation`: [![](https://img.shields.io/badge/docs-passing-green)](https://steve-xmh.github.io/scl/scl-doc/scl_gui_animation/index.html) 启动器图形页面动画函数库,包含了一些方便用来制作非线性动画的函数和工具类 +- `scl-gui-widgets`: [![](https://img.shields.io/badge/docs-passing-green)](https://steve-xmh.github.io/scl/scl-doc/scl_gui_widgets/index.html) 启动器图形页面组件库,基于 [Druid](https://github.com/linebender/druid) 框架,提供了大量基于 WinUI3 设计规范制作的图形页面组件 ## 关于开源协议和代码协作协议 diff --git a/scl-core/Cargo.toml b/scl-core/Cargo.toml index 939b03b..8030d65 100644 --- a/scl-core/Cargo.toml +++ b/scl-core/Cargo.toml @@ -8,6 +8,7 @@ license = "LGPL-3.0-only" readme = "README.md" authors = ["Steve-xmh "] edition = "2021" +rust-version = "1.75" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -15,7 +16,7 @@ edition = "2021" anyhow = "^1.0" base64 = "^0.21" futures = "^0.3.21" -surf = { version = "^2.3", default-features = false, features = [ "curl-client", "encoding" ] } +surf = { version = "^2.3", default-features = false, features = [ "h1-client", "encoding" ] } image = { version = "^0.24", default-features = false, features = ["jpeg", "png", "gif", "bmp", "rgb"] } webp = "^0.2" nom = "^7.1" @@ -25,23 +26,23 @@ serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" sha1_smol = { version = "^1.0", features = ["std"] } shell-words = "^1.0" -smol = "^1.2" -toml = "^0.7" +smol = "^2" +toml = "^0.8" url = "^2.2" urlencoding = "^2.1" -uuid = { version = "1.0.0", features = ["v4"] } +concat-string = "^1.0" +md5 = "^0.7" zip = "^0.6.2" dirs = "^5.0" -async-trait = "^0.1" shellwords = "1.1.0" fs_extra = "1.3.0" -tracing = "0.1.40" +tracing = "^0.1" [target.'cfg(target_os = "windows")'.dependencies] -winreg = "^0.50" +winreg = "^0.52" [target.'cfg(target_os = "windows")'.dependencies.windows] -version = "0.48" +version = "0.52" features = [ "Win32_System_Diagnostics_Debug", "Win32_Foundation", diff --git a/scl-core/src/auth/mod.rs b/scl-core/src/auth/mod.rs index 12c0027..18c336a 100644 --- a/scl-core/src/auth/mod.rs +++ b/scl-core/src/auth/mod.rs @@ -19,6 +19,31 @@ pub mod authlib; pub mod microsoft; pub mod structs; +/// 根据玩家名称生成一个固定的离线 UUID +/// +/// 返回值可以通过传入 `format!("{:x}", uuid)` 来转换为十六进制字符串形式 +/// +/// 代码参考: +/// ```rust +/// # use scl_core::auth::generate_offline_uuid; +/// # fn main() { +/// assert_eq!(format!("{:x}", generate_offline_uuid("Steve")), "5627dd98e6be3c21b8a8e92344183641"); +/// assert_eq!(format!("{:x}", generate_offline_uuid("Alex")), "36532b5ec4423dbba24cc7e55d0f979a"); +/// # } +/// ``` +/// 生成方式参考: +pub fn generate_offline_uuid(player_name: &str) -> md5::Digest { + let mut ctx = md5::Context::new(); + ctx.consume("OfflinePlayer:"); + ctx.consume(player_name); + let mut result = ctx.compute().0; + + result[6] = (result[6] & 0x0f) | 0x30; + result[8] = (result[8] & 0x3f) | 0x80; + + md5::Digest(result) +} + /// 提取一个皮肤位图的正面头部部分,用于 GUI 展示头像 /// /// 传入的皮肤大小必须是 32x64 或 64x64 diff --git a/scl-core/src/download/authlib.rs b/scl-core/src/download/authlib.rs index cf45026..e3c8608 100644 --- a/scl-core/src/download/authlib.rs +++ b/scl-core/src/download/authlib.rs @@ -1,7 +1,5 @@ //! 获取 authlib-injector 第三方登录代理 jar -use async_trait::async_trait; - use super::{DownloadSource, Downloader}; use crate::prelude::*; @@ -14,7 +12,6 @@ struct LatestData { /// Authlib 第三方正版登录模块的下载特质 /// /// 你可以通过引入本特质和 [`crate::download::Downloader`] 来下载并安装 Authlib Injector -#[async_trait] pub trait AuthlibDownloadExt: Sync { /// 下载最新版本的 Authlib Injector 并存放到指定路径,如果路径的文件夹不存在则会先创建它,如果文件已存在则会被覆盖 async fn download_authlib_injector(&self, dest_path: &str) -> DynResult; @@ -22,7 +19,6 @@ pub trait AuthlibDownloadExt: Sync { async fn install_authlib_injector(&self) -> DynResult; } -#[async_trait] impl AuthlibDownloadExt for Downloader { async fn download_authlib_injector(&self, dest_path: &str) -> DynResult { // https://authlib-injector.yushi.moe/ diff --git a/scl-core/src/download/fabric.rs b/scl-core/src/download/fabric.rs index d1b4e63..5ad557a 100644 --- a/scl-core/src/download/fabric.rs +++ b/scl-core/src/download/fabric.rs @@ -1,5 +1,4 @@ //! Fabric 下载源数据结构 -use async_trait::async_trait; use serde::Deserialize; use super::{DownloadSource, Downloader}; @@ -39,7 +38,6 @@ pub struct LoaderStruct { /// Fabric 模组加载器的安装特质 /// /// 可以通过引入本特质和使用 [`crate::download::Downloader`] 来安装模组加载器 -#[async_trait] pub trait FabricDownloadExt: Sync { /// 根据原版版本号获取该版本下可用的 Fabric 模组加载器 async fn get_avaliable_loaders(&self, vanilla_version: &str) -> DynResult>; @@ -57,7 +55,6 @@ pub trait FabricDownloadExt: Sync { async fn download_fabric_post(&self, version_name: &str) -> DynResult; } -#[async_trait] impl FabricDownloadExt for Downloader { async fn get_avaliable_loaders(&self, vanilla_version: &str) -> DynResult> { let mut result = crate::http::retry_get(match self.source { diff --git a/scl-core/src/download/forge.rs b/scl-core/src/download/forge.rs index fc06d96..af2c742 100644 --- a/scl-core/src/download/forge.rs +++ b/scl-core/src/download/forge.rs @@ -7,7 +7,6 @@ use std::{ }; use anyhow::Context; -use async_trait::async_trait; use inner_future::io::{AsyncBufReadExt, AsyncWriteExt}; use serde_json::Value; @@ -32,7 +31,6 @@ const CLASS_PATH_SPAREATOR: &str = ":"; /// Forge 模组加载器的安装特质 /// /// 可以通过引入本特质和使用 [`crate::download::Downloader`] 来安装模组加载器 -#[async_trait] pub trait ForgeDownloadExt: Sync { /// 根据纯净版本号获取当前可用的所有 Forge 版本 async fn get_avaliable_installers(&self, vanilla_version: &str) @@ -64,7 +62,6 @@ pub trait ForgeDownloadExt: Sync { ) -> DynResult; } -#[async_trait] impl ForgeDownloadExt for Downloader { async fn get_avaliable_installers( &self, diff --git a/scl-core/src/download/mod.rs b/scl-core/src/download/mod.rs index 39bc5b8..341c605 100644 --- a/scl-core/src/download/mod.rs +++ b/scl-core/src/download/mod.rs @@ -14,7 +14,6 @@ pub mod vanilla; use std::{fmt::Display, path::Path, str::FromStr}; use anyhow::Context; -use async_trait::async_trait; pub use authlib::AuthlibDownloadExt; pub use fabric::FabricDownloadExt; pub use forge::ForgeDownloadExt; @@ -246,8 +245,7 @@ impl Default for Downloader { } /// 一个游戏安装特质,如果你并不需要单独安装其它部件,则可以单独引入这个特质来安装游戏 -#[async_trait] -pub trait GameDownload<'a>: +pub trait GameDownload: FabricDownloadExt + ForgeDownloadExt + VanillaDownloadExt + QuiltMCDownloadExt { /// 根据参数安装一个游戏,允许安装模组加载器 @@ -262,8 +260,7 @@ pub trait GameDownload<'a>: ) -> DynResult; } -#[async_trait] -impl GameDownload<'_> for Downloader { +impl GameDownload for Downloader { async fn download_game( &self, version_name: &str, diff --git a/scl-core/src/download/optifine.rs b/scl-core/src/download/optifine.rs index 9a68217..238471b 100644 --- a/scl-core/src/download/optifine.rs +++ b/scl-core/src/download/optifine.rs @@ -2,7 +2,6 @@ //! //! 因 Optifine 并不提供一个稳定的下载方式,故此处会使用镜像源的额外 API 来获取版本下载信息 -use async_trait::async_trait; use inner_future::io::AsyncWriteExt; use super::{structs::OptifineVersionMeta, Downloader}; @@ -18,7 +17,6 @@ const CLASS_PATH_SPAREATOR: &str = ":"; const CLASS_PATH_SPAREATOR: &str = ":"; /// 一个用于下载 Optifine 模组下载安装的扩展特质,可以使用 [`crate::download::Downloader`] 来安装 -#[async_trait] pub trait OptifineDownloadExt: Sync { /// 根据纯净版本号获取当前可用的所有 Optifine 版本 async fn get_avaliable_installers( @@ -46,7 +44,6 @@ pub trait OptifineDownloadExt: Sync { ) -> DynResult; } -#[async_trait] impl OptifineDownloadExt for Downloader { async fn get_avaliable_installers( &self, diff --git a/scl-core/src/download/quiltmc.rs b/scl-core/src/download/quiltmc.rs index a392a22..6eeba7c 100644 --- a/scl-core/src/download/quiltmc.rs +++ b/scl-core/src/download/quiltmc.rs @@ -1,6 +1,5 @@ //! QuiltMC 下载源数据结构 use anyhow::Context; -use async_trait::async_trait; use serde::Deserialize; use super::Downloader; @@ -36,7 +35,6 @@ pub struct LoaderStruct { /// QuiltMC 模组加载器的安装特质 /// /// 可以通过引入本特质和使用 [`crate::download::Downloader`] 来安装模组加载器 -#[async_trait] pub trait QuiltMCDownloadExt: Sync { /// 根据原版版本号获取该版本下可用的 QuiltMC 模组加载器 async fn get_avaliable_loaders(&self, vanilla_version: &str) -> DynResult>; @@ -54,7 +52,6 @@ pub trait QuiltMCDownloadExt: Sync { async fn download_quiltmc_post(&self, version_name: &str) -> DynResult; } -#[async_trait] impl QuiltMCDownloadExt for Downloader { async fn get_avaliable_loaders(&self, vanilla_version: &str) -> DynResult> { let mut result = crate::http::retry_get(format!( diff --git a/scl-core/src/download/vanilla.rs b/scl-core/src/download/vanilla.rs index 95841ed..9451982 100644 --- a/scl-core/src/download/vanilla.rs +++ b/scl-core/src/download/vanilla.rs @@ -3,7 +3,6 @@ use std::{borrow::Cow, collections::HashMap, path::Path}; use anyhow::Context; -use async_trait::async_trait; use inner_future::{fs::create_dir_all, io::AsyncWriteExt}; use tracing::*; @@ -19,7 +18,6 @@ use crate::{ }; /// 一个用于下载安装原版的扩展特质,可以使用 [`crate::download::Downloader`] 来安装 -#[async_trait] pub trait VanillaDownloadExt: Sync { /// 获取现在所有可下载版本 async fn get_avaliable_vanilla_versions(&self) -> DynResult; @@ -66,7 +64,6 @@ pub trait VanillaDownloadExt: Sync { async fn install_vanilla(&self, version_name: &str, version_info: &VersionInfo) -> DynResult; } -#[async_trait] impl VanillaDownloadExt for Downloader { async fn get_avaliable_vanilla_versions(&self) -> DynResult { let res = crate::http::retry_get_json(match self.source { diff --git a/scl-core/src/http.rs b/scl-core/src/http.rs index eef9cdf..cff0785 100644 --- a/scl-core/src/http.rs +++ b/scl-core/src/http.rs @@ -58,13 +58,16 @@ fn logger( } static GLOBAL_CLIENT: Lazy> = Lazy::new(|| { + let scl_version = std::option_env!("SCL_VERSION_TYPE").unwrap_or("0.0.0"); let client = Config::new() .add_header( "User-Agent", - "github.com/Steve-xmh/SharpCraftLauncher (stevexmh@qq.com)", + format!("SharpCraftLauncher/{scl_version} (github.com/Steve-xmh/SharpCraftLauncher) (stevexmh@qq.com)"), ) .unwrap() - .set_timeout(Some(Duration::from_secs(30))); + .set_timeout(Some(Duration::from_secs(30))) + .set_http_keep_alive(false) // async-h1 似乎不兼容使用 Keep Alive,会导致解析响应出错 + .set_max_connections_per_host(1024); let client = if let Ok(mut proxy) = std::env::var("HTTP_PROXY") { let proxy = if proxy.ends_with('/') { proxy diff --git a/scl-core/src/lib.rs b/scl-core/src/lib.rs index e6a5ab9..ed0e33d 100644 --- a/scl-core/src/lib.rs +++ b/scl-core/src/lib.rs @@ -30,6 +30,7 @@ */ #![forbid(missing_docs)] +#![allow(async_fn_in_trait)] pub mod auth; pub mod client; diff --git a/scl-core/src/prelude.rs b/scl-core/src/prelude.rs index a1b5ac4..fbf4a4b 100644 --- a/scl-core/src/prelude.rs +++ b/scl-core/src/prelude.rs @@ -2,5 +2,4 @@ pub(crate) use smol as inner_future; pub(crate) type DynResult = anyhow::Result; pub(crate) use serde::*; -pub use crate::download::GameDownload; pub(crate) use crate::progress::*; diff --git a/scl-gui-widgets/src/widgets/download_module_item.rs b/scl-gui-widgets/src/widgets/download_module_item.rs index c9b6f30..1aa76a7 100644 --- a/scl-gui-widgets/src/widgets/download_module_item.rs +++ b/scl-gui-widgets/src/widgets/download_module_item.rs @@ -1,8 +1,8 @@ use druid::{ kurbo::{BezPath, Shape}, piet::{PaintBrush, TextStorage}, - widget::{Click, ControllerHost, LabelText}, - Affine, Data, Env, Event, LifeCycle, RenderContext, Widget, WidgetExt, WidgetPod, + widget::{Click, ControllerHost, Image, LabelText}, + Affine, Data, Env, Event, LifeCycle, Point, RenderContext, Widget, WidgetExt, WidgetPod, }; use super::label; @@ -15,10 +15,15 @@ use crate::theme::{ icons::IconKeyPair, }; +enum Icon { + BezPath(BezPath), + Image(Box>), +} + /// 一个左侧有图标和说明信息,右侧有副文本信息的可点击项组件 pub struct DownloadModuleItem { icon_key: IconKeyPair, - icon_path: BezPath, + icon: Icon, text: WidgetPod>>, desc: WidgetPod>>, } @@ -32,7 +37,32 @@ impl DownloadModuleItem { ) -> Self { Self { icon_key, - icon_path: BezPath::new(), + icon: Icon::BezPath(BezPath::new()), + text: WidgetPod::new(Box::new( + label::new(text) + .with_text_size(14.) + .with_text_color(base::MEDIUM) + .with_font(BODY) + .align_vertical(druid::UnitPoint::LEFT), + )), + desc: WidgetPod::new(Box::new( + label::new(desc) + .with_text_color(base::MEDIUM) + .with_font(CAPTION_ALT) + .align_vertical(druid::UnitPoint::RIGHT), + )), + } + } + + /// 根据所给的图像组件创建组件 + pub fn new_image( + img: Image, + text: impl Into>, + desc: impl Into>, + ) -> Self { + Self { + icon_key: crate::theme::icons::EMPTY, + icon: Icon::Image(Box::new(WidgetPod::new(img))), text: WidgetPod::new(Box::new( label::new(text) .with_text_size(14.) @@ -57,7 +87,7 @@ impl DownloadModuleItem { ) -> Self { Self { icon_key, - icon_path: BezPath::new(), + icon: Icon::BezPath(BezPath::new()), text: WidgetPod::new(Box::new( label::dynamic(text) .with_text_size(14.) @@ -74,6 +104,31 @@ impl DownloadModuleItem { } } + /// 根据所给的图像组件创建组件,但可以是动态文字 + pub fn dynamic_image( + img: Image, + text: impl Fn(&D, &Env) -> String + 'static, + desc: impl Fn(&D, &Env) -> String + 'static, + ) -> Self { + Self { + icon_key: crate::theme::icons::EMPTY, + icon: Icon::Image(Box::new(WidgetPod::new(img))), + text: WidgetPod::new(Box::new( + label::new(text) + .with_text_size(14.) + .with_text_color(base::MEDIUM) + .with_font(BODY) + .align_vertical(druid::UnitPoint::LEFT), + )), + desc: WidgetPod::new(Box::new( + label::new(desc) + .with_text_color(base::MEDIUM) + .with_font(CAPTION_ALT) + .align_vertical(druid::UnitPoint::RIGHT), + )), + } + } + /// Provide a closure to be called when this button is clicked. pub fn on_click( self, @@ -83,7 +138,9 @@ impl DownloadModuleItem { } fn reload_icon(&mut self, env: &druid::Env) { - self.icon_path = BezPath::from_svg(env.get(&self.icon_key.0).as_str()).unwrap_or_default(); + if let Icon::BezPath(p) = &mut self.icon { + *p = BezPath::from_svg(env.get(&self.icon_key.0).as_str()).unwrap_or_default(); + } } } @@ -104,6 +161,9 @@ impl Widget for DownloadModuleItem { } self.text.event(ctx, event, data, env); self.desc.event(ctx, event, data, env); + if let Icon::Image(img) = &mut self.icon { + img.event(ctx, event, data, env); + } } fn lifecycle( @@ -120,6 +180,9 @@ impl Widget for DownloadModuleItem { } self.text.lifecycle(ctx, event, data, env); self.desc.lifecycle(ctx, event, data, env); + if let Icon::Image(img) = &mut self.icon { + img.lifecycle(ctx, event, data, env); + } } fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &D, data: &D, env: &druid::Env) { @@ -133,6 +196,9 @@ impl Widget for DownloadModuleItem { self.text.update(ctx, data, env); self.desc.update(ctx, data, env); } + if let Icon::Image(img) = &mut self.icon { + img.update(ctx, data, env); + } } fn layout( @@ -150,6 +216,14 @@ impl Widget for DownloadModuleItem { let desc_size = self.desc.layout(ctx, &desc_bc, data, env); self.text.set_origin(ctx, (40., 0.).into()); self.desc.set_origin(ctx, (40., 0.).into()); + if let Icon::Image(img) = &mut self.icon { + let img_size = img.layout(ctx, &bc, data, env); + let top_left = Point::new( + (40.0 - img_size.width) / 2.0, + (40.0 - img_size.height) / 2.0, + ); + img.set_origin(ctx, top_left); + } bc.constrain((text_size.width.max(desc_size.width) + 40., 40.)) } @@ -174,11 +248,16 @@ impl Widget for DownloadModuleItem { ) } let icon_size = druid::Size::new(size.height, size.height); - ctx.with_save(|ctx| { - ctx.transform(Affine::translate( - ((icon_size - self.icon_path.bounding_box().size()) / 2.).to_vec2(), - )); - ctx.fill_even_odd(&self.icon_path, &icon_brush) + ctx.with_save(|ctx| match &mut self.icon { + Icon::BezPath(p) => { + ctx.transform(Affine::translate( + ((icon_size - p.bounding_box().size()) / 2.).to_vec2(), + )); + ctx.fill_even_odd(p.to_owned(), &icon_brush); + } + Icon::Image(img) => { + img.paint(ctx, data, env); + } }); self.text.paint(ctx, data, env); self.desc.paint(ctx, data, env); diff --git a/scl-macro/Cargo.toml b/scl-macro/Cargo.toml index 8905042..5e0d810 100644 --- a/scl-macro/Cargo.toml +++ b/scl-macro/Cargo.toml @@ -15,9 +15,9 @@ authors = ["Steve-xmh "] proc-macro = true [dependencies] -quote = "^1.0" -syn = "^2.0" +quote = "^1" +syn = "^2" [dev-dependencies] druid = { git = "https://github.com/linebender/druid.git", features = ["im", "serde", "raw-win-handle"] } -serde = { version = "1.0", features = ["derive"] } +serde = { version = "^1", features = ["derive"] } diff --git a/scl-macro/src/lib.rs b/scl-macro/src/lib.rs index 704f17f..967fe24 100644 --- a/scl-macro/src/lib.rs +++ b/scl-macro/src/lib.rs @@ -9,7 +9,7 @@ use icons::*; /// 使用简易的语法定义图标们 /// 中间的颜色值为可选,分别为亮色主题色和暗色主题色 -/// ``` +/// ```rust /// use druid::{ArcStr, Color, Data, Key}; /// use serde::{Deserialize, Serialize}; /// /// 一个存放明色,暗色,填充路径字符串的类型