定义配置表
假设我们有一张成就的配置表,格式大概如下:
| id | group | task_id | conditon | reward | point |
|---|---|---|---|---|---|
| int | int | int | int | vector3_array_int | int |
| allkey | all | all | all | all | all |
| 10001 | 101 | 510001 | 1,1,50;2,10021,5 | 5 |
第一行标识每个字段的含义,在程序中直接对应着变量名,第二行标识这个字段的解析格式,第三行标识这个字段的作用域(客户端需要、服务端需要、或者两边都需要),不同的项目的配置表格式不尽相同,可根据具体情况修改解析逻辑即可。
与之对应的,在程序中配置表的结构以及解析代码,我们希望它最后是这样的:
#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct AchievementConfig {
pub id: i32,
pub group: i32,
pub task_id: i32,
pub condition: i32,
pub reward: Vec<(i32, i32, i32)>,
pub point: i32,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct AchievementConfigs {
pub achievement: HashMap<i32, AchievementConfig>,
}
impl AchievementConfigs {
pub fn get_by_id(&self, id: &i32) -> anyhow::Result<&AchievementConfig> {
self.achievement.get(id).ok_or(anyhow::anyhow!(
"{} not found, id: {}",
id,
std::any::type_name::<AchievementConfig>()
))
}
}
impl Deref for AchievementConfigs {
type Target = HashMap<i32, AchievementConfig>;
fn deref(&self) -> &Self::Target {
&self.achievement
}
}
impl DerefMut for AchievementConfigs {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.achievement
}
}
impl Config for AchievementConfig {
fn id(&self) -> Id {
self.id.clone().into()
}
fn parse(row: Row) -> anyhow::Result<Self>
where
Self: Sized,
{
let id = row.get("id")?;
let group = row.get("group")?;
let task_id = row.get("task_id")?;
let condition = row.get("condition")?;
let reward = row.get("reward")?;
let point = row.get("point")?;
Ok(Self {
id,
group,
task_id,
condition,
reward,
point,
})
}
}
impl Configs for AchievementConfigs {
fn ids(&self) -> Vec<Id> {
self.achievement
.keys()
.map(|id| (*id).clone().into())
.collect()
}
fn name() -> &'static str
where
Self: Sized,
{
"achievement"
}
fn load(&mut self, excel_config: &ExcelConfig) -> anyhow::Result<()> {
let name_index = excel_config.name_index();
for (row_number, row) in excel_config.data.iter().enumerate() {
let config = AchievementConfig::parse(Row {
excel_name: Self::name(),
index: &name_index,
row: &row,
row_number,
})?;
self.achievement.insert(config.id.clone(), config);
}
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
配置表在Rust中的定义
首先定义一个Config trait,用来把配置表数据解析为Rust中的数据结构。
pub trait Config: Debug + Send + Sync + Clone {
fn id(&self) -> Id;
fn parse(row: Row) -> anyhow::Result<Self> where Self: Sized;
}
这里其实就相当于配置表中的一行数据,其中parse方法会把读取到的配置表数据解析为具体的数据结构。
Row的定义如下
- excel_name 这个是配置表的名字
- index 这个是表头的数据的名字对应所在列的索引
- row 这个配置表的一行数据
- row_number 这一行数据在配置表中具体哪一行
#[derive(Debug, Clone)]
pub struct Row<'a> {
pub excel_name: &'a str,
pub index: &'a HashMap<String, usize>,
pub row: &'a Vec<String>,
pub row_number: usize,
}
const ROW_HEADER_SIZE: usize = 5;
impl<'a> Row<'a> {
pub fn get<T>(&self, name: &str) -> anyhow::Result<T>
where
T: Parser,
{
let index = self.index.get(name).ok_or(anyhow::anyhow!("{} not found", name))?;
let value = self.row[*index].as_str();
T::parse_from(value).context(format!("`{}` failed to parse field `{}` with value `{}` at row `{}`", self.excel_name, name, value, self.row_number + ROW_HEADER_SIZE + 1))
}
}
在解析每一行数据的时候,直接传入数据的名字就可以拿到这个数据了,而不用关心这条数据在这一行的哪一列,Parser就是根据不同的数据类型,解析逻辑的具体实现,这个放到后面再说。row_number是用来帮助我们在数据解析出错的时候,定位到配置表中哪里的数据有问题的。
Id的定义如下 省略Display impl Into<Id> for u8,u16等的实现
#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
pub enum Id {
U8(u8),
U16(u16),
U32(u32),
U64(u64),
U128(u128),
I8(i8),
I32(i32),
I64(i64),
I128(i128),
Usize(usize),
Isize(isize),
String(String),
}
然后定义一个Configs的trait,表示这个配置表的所有数据。
pub trait Configs: Debug + Send + Sync + DynClone + Any {
fn ids(&self) -> Vec<Id>;
fn name() -> &'static str where Self: Sized;
fn load(&mut self, excel_config: &ExcelConfig) -> anyhow::Result<()>;
#[allow(unused_variables)]
fn complete(&mut self, mgr: &GameConfigMgr) -> anyhow::Result<()> {
Ok(())
}
#[allow(unused_variables)]
fn validate(&self, mgr: &GameConfigMgr) -> anyhow::Result<()> {
Ok(())
}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn into_any(self: Box<Self>) -> Box<dyn Any>;
}
dyn_clone::clone_trait_object!(Configs);
- ids 返回配置表的主键,存到数据库中,如果这些主键不在游戏中存库,可以返回空,目的是为了防止策划意外的删除这些数据,导致线上事故
- name 配置表名字
- load 从
ExcelConfig数据中解析配置表数据 - complete 当所有的配置表数据都加载完成之后,提供一个接口重新组织当前配置表的数据结构的机会,例如遍历当前配置表中所有的数据,按id分组,然后存到另外的结构中去
- validate 配置表校验方法,因为配置表的数据会相互引用id,需要校验配置表引用的数据的合法性
GameConfigMgr 定义如下
#[derive(Debug, Clone)]
pub struct GameConfigMgr {
pub configs: HashMap<&'static str, Box<dyn Configs>>,
pub complete_ordering: Vec<&'static str>,
}
- complete_ordering 如果某些配置表的
complete函数会依赖其它配置表执行complete之后的数据,可以在这里指定执行complete时的顺序
配置表解析
配置表数据类型映射到Rust数据类型
定义一个枚举结构,其中包含所有的配置表数据类型,可以直接通过配置表的字符串解析为对应的数据类型枚举,方便我们后面数据的解析。
#[derive(
Debug,
Clone,
Serialize,
Deserialize,
Encode,
Decode,
strum::EnumString,
strum::Display,
strum::EnumIter
)]
#[strum(serialize_all = "snake_case")]
pub enum CellType {
#[strum(serialize = "uint")]
UInt,
Int,
Long,
String,
Bool,
Vector3ArrayInt,
#[strum(serialize = "vector[float,float,float]_array_float")]
Vector3ArrayFloat,
Vector3Int,
#[strum(serialize = "vector2_int", serialize = "vector[int,int]")]
Vector2Int,
#[strum(serialize = "vector3_uint")]
Vector3UInt,
#[strum(serialize = "vector2_uint")]
Vector2UInt,
#[strum(serialize = "Vector2_array_int", serialize = "vector2_array_int")]
Vector2ArrayInt,
ArrayInt,
#[strum(serialize = "array_uint")]
ArrayUInt,
DictionaryStringFloat,
DictionaryStringInt,
Lang,
Float,
Double,
#[strum(serialize = "vector[float,float]")]
Vector2Float,
#[strum(serialize = "vector[float,float,float]")]
Vector3Float,
#[strum(serialize = "vector[string,string]")]
Vector2String,
}
在解析完配置表生成对应的rust代码时,我们使用quote这个库进行生成,所以只需要根据枚举的类型,转换成rust基本类型的TokenStream就行了。
impl CellType {
pub fn token(&self) -> TokenStream {
match self {
CellType::UInt => quote!(u32),
CellType::Int => quote!(i32),
CellType::Long => quote!(i64),
CellType::String => quote!(String),
CellType::Bool => quote!(bool),
CellType::Vector3ArrayInt => quote!(Vec<(i32,i32,i32)>),
CellType::Vector3Int => quote!((i32,i32,i32)),
CellType::Vector2Int => quote!((i32,i32)),
CellType::Vector3UInt => quote!((u32,u32,u32)),
CellType::Vector2UInt => quote!((u32,u32)),
CellType::Vector2ArrayInt => quote!(Vec<(i32,i32)>),
CellType::ArrayInt => quote!(Vec<i32>),
CellType::ArrayUInt => quote!(Vec<u32>),
CellType::DictionaryStringFloat => quote!(ahash::HashMap<String,f32>),
CellType::DictionaryStringInt => quote!(ahash::HashMap<String,i32>),
CellType::Lang => quote!(String),
CellType::Float => quote!(f32),
CellType::Double => quote!(f64),
CellType::Vector2Float => quote!((f32,f32)),
CellType::Vector3Float => quote!((f32,f32,f32)),
CellType::Vector2String => quote!((String,String)),
CellType::Vector3ArrayFloat => quote!(Vec<(f32,f32,f32)>),
}
}
}
定义配置表作用域枚举
#[derive(
Debug,
Clone,
Serialize,
Deserialize,
Encode,
Decode,
strum::EnumString,
strum::Display,
strum::EnumIter
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
pub enum KeyType {
#[strum(serialize = "allkey")]
AllKey,
All,
Client,
#[strum(serialize = "clientkey")]
ClientKey,
#[strum(serialize = "server", serialize = "sever")]
Server,
#[strum(serialize = "serverkey")]
ServerKey,
}
服务端进行配置表代码生成的时候,只需要关系AllKey All Server ServerKey这些字段就行了,按需设计,比如可以只设计Id Client All 就行了,Id用来标识这一条数据的主键,为啥没有Server?因为服务端要读的数据一般客户端也要读,没必要单独再开一个。
定义Parser解析配置表数据
pub trait Parser: Sized {
fn parse_from(value: &str) -> anyhow::Result<Self>;
}
#[macro_export]
macro_rules! default {
($value: ident) => {
if $value.is_empty() || $value == "0" {
return Ok(Default::default());
}
};
}
trait很简单,每个单元格的数据都读取成字符串类型,然后再根据Parser的具体类型解析为具体类型的数据。另外定义了一个default!宏,允许单元格数据为空或者0值的情况,这种情况处理为Parser实现结构的默认值。
解析基本类型
基本类型包括所有的数字类型i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64以及布尔类型bool,由于类型较多,且解析规则一样,因此我们定义一个宏来进行批量解析。
macro_rules! number_parser {
($ty: ty) => {
impl crate::excel::parser::Parser for $ty {
fn parse_from(value: &str) -> anyhow::Result<Self> {
default!(value);
value.parse::<$ty>().map_err(|e| anyhow::anyhow!(e))
}
}
};
($($ty: ty)*) => {
$(number_parser!($ty);)*
};
}
number_parser!(i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64);
布尔类型单独解析
impl Parser for bool {
fn parse_from(value: &str) -> anyhow::Result<Self> {
default!(value);
match value {
"true" | "1" => Ok(true),
"false" | "0" | "" => Ok(false),
_ => Err(anyhow::anyhow!("Invalid boolean value: {}", value)),
}
}
}
解析元组类型(tuple2,tuple3)
impl<T1, T2> Parser for (T1, T2) where
T1: Parser + Default,
T2: Parser + Default,
{
fn parse_from(value: &str) -> anyhow::Result<Self> {
default!(value);
let values = value.split(",").collect::<Vec<_>>();
ensure!(values.len() == 2, "Expected 2 elements, found {}", values.len());
let t1 = T1::parse_from(values[0])?;
let t2 = T2::parse_from(values[1])?;
Ok((t1, t2))
}
}
impl<T1, T2, T3> Parser for (T1, T2, T3) where
T1: Parser + Default,
T2: Parser + Default,
T3: Parser + Default,
{
fn parse_from(value: &str) -> anyhow::Result<Self> {
default!(value);
let values = value.split(",").collect::<Vec<_>>();
ensure!(values.len() == 3, "Expected 3 elements, found {}", values.len());
let t1 = T1::parse_from(values[0])?;
let t2 = T2::parse_from(values[1])?;
let t3 = T3::parse_from(values[2])?;
Ok((t1, t2, t3))
}
}
解析数组类型
同样的,数组类型会有很多种,我们依然要写宏来处理这种问题,防止写出重复代码。针对不同的数组类型,可能有不同的分隔符,分隔符不能写死。
macro_rules! impl_parser_for_vec {
($ty:ty, $pat:expr) => {
impl crate::excel::parser::Parser for Vec<$ty> where $ty: crate::excel::parser::Parser {
fn parse_from(value: &str) -> anyhow::Result<Self> {
crate::default!(value);
let mut vec = Vec::new();
for item in value.split($pat) {
vec.push(<$ty as crate::excel::parser::Parser>::parse_from(item)?);
}
Ok(vec)
}
}
};
($($ty:ty)*, $pat:expr) => {
$(impl_parser_for_vec!($ty, $pat);)*
};
}
impl_parser_for_vec!(i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 bool, ',');
impl_parser_for_vec!(Asset, ';');
impl_parser_for_vec!(AssetPackage, '|');
impl<T1, T2> Parser for Vec<(T1, T2)> where
T1: Parser,
T2: Parser,
{
fn parse_from(value: &str) -> anyhow::Result<Self> {
default!(value);
let mut vec = Vec::new();
for item in value.split(';') {
let values = item.split(',').collect::<Vec<_>>();
ensure!(values.len() == 2, "Expected 2 elements, found {}", values.len());
let t1 = T1::parse_from(values[0])?;
let t2 = T2::parse_from(values[1])?;
vec.push((t1, t2));
}
Ok(vec)
}
}
impl<T1, T2, T3> Parser for Vec<(T1, T2, T3)> where
T1: Parser,
T2: Parser,
T3: Parser,
{
fn parse_from(value: &str) -> anyhow::Result<Self> {
default!(value);
let mut vec = Vec::new();
for item in value.split(';') {
let values = item.split(',').collect::<Vec<_>>();
ensure!(values.len() == 3, "Expected 3 elements, found {}", values.len());
let t1 = T1::parse_from(values[0])?;
let t2 = T2::parse_from(values[1])?;
let t3 = T3::parse_from(values[2])?;
vec.push((t1, t2, t3));
}
Ok(vec)
}
}
可以看到这种解析方式比较灵活,当我们需要直接把配置表中的数据解析成游戏中对应的数据结构时,只需要对对应的结构实现Parser就好了,例如代码里面的Asset、AssetPackage,又或者某个数字其实是代表的枚举,那我们直接解析成枚举就好了。
读取数据
为了同时满足把配置表结构生成到Rust配置表代码以及读取配置表数据的功能,先定义一个中间结构ExcelConfig,这个结构其实就代表了一张表的数据,可以根据name、cell_name、cell_type、cell_key生成对应的rust配置表结构,也可以读取data做配置表解析工作。第一层Vec表示每一行的数据,第二层的Vec表示这一行数据的每一列数据。
这个结构中有一个name_index的方法,其主要的作用是可以直接根据数据的名字找到数据所在的索引,我们在读数据的时候,肯定是用名字最为直观。
#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct ExcelConfig {
pub name: String,
pub cell_name: Vec<String>,
pub cell_type: Vec<CellType>,
pub cell_key: Vec<KeyType>,
pub data: Vec<Vec<String>>,
}
impl ExcelConfig {
pub fn name_index(&self) -> HashMap<String, usize> {
self.cell_name.iter().enumerate().map(|(i, name)| (name.clone(), i)).collect()
}
}
关键字处理
由于配置表的字段可能和Rust的语言关键字冲突,所以当配置表的字段是Rust中的关键字时,需要在字段名前面加上r#,下面列出了Rust中的关键字,在生成Rust配置表结构的时候,需要遍历进行判断。
const KEYWORDS: [&str; 51] = ["as", "break", "const", "continue", "crate", "else", "enum", "extern",
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
"ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
"use", "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final",
"macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try"];
读取excel
读取excel数据用到库是calamine,按照顺序来就好了,比较简单,如果只是生成配置表对应的Rust数据结构,可以不读data,只读表头就可以了,加快速度。
pub fn read_excel_config<P: AsRef<Path>>(path: P) -> anyhow::Result<ExcelConfig> {
let path = path.as_ref();
let display_path = path.display();
info!("read: {}", display_path);
let mut workbook: Xlsx<_> = open_workbook(path).context(format!("open excel: {} failed", display_path))?;
let (sheet_name, data) = &workbook.worksheets()[0];
let mut cell_name = vec![];
let mut cell_type = vec![];
let mut cell_key = vec![];
let mut excel_data = vec![];
for (i, row) in data.rows().enumerate() {
let mut row_data = vec![];
for data_type in row.iter() {
match i {
0 => {
match data_type {
Data::String(data) => {
//convert non snake case cell name to snake case
let data = prefix_if_starts_with_digit(data.trim()).to_case(Case::Snake);
cell_name.push(data);
}
other => {
return Err(anyhow!("excel {} `string` expected, got: `{}` at row {}", sheet_name, other, i + 1));
}
}
}
1 => {
match data_type {
Data::String(data) => {
cell_type.push(CellType::from_str(data.trim()).context(format!("convert string {} to enum CellType error", data))?);
}
other => {
return Err(anyhow!("excel {} `string` expected, got: `{}` at row {}", sheet_name, other, i + 1));
}
}
}
2 => {
match data_type {
Data::String(data) => {
let key_type = KeyType::from_str(data.trim()).unwrap_or(KeyType::Client);
cell_key.push(key_type);
}
other => {
return Err(anyhow!("excel {} `string` expected, got: `{}` at row {}", sheet_name, other, i + 1));
}
}
}
3 | 4 => {}
_ => {
row_data.push(data_type.to_string().trim().to_string());
}
}
}
if i >= 5 {
excel_data.push(row_data);
}
}
let config = ExcelConfig {
name: prefix_if_starts_with_digit(sheet_name).to_case(Case::Snake),
cell_name,
cell_type,
cell_key,
data: excel_data,
};
Ok(config)
}
配置表生成 序列化 反序列化
通过以上步骤,我们可以得到名为ExcelConfig结构的配置表数据,这个不是游戏业务中使用的配置表数据的最终形式,还需要解析此结构,得到GameConfigMgr,此时GameConfigMgr中已经是解析好的数据了,但是此时的配置表是以HashMap的形式存在的,在获取到配置表后,还需要向下转型为具体的类型才可以使用,比较麻烦,所以我们再引入一个配置表结构,叫做GameConfigs,把GameConfigMgr中的配置表数据直接拆成字段放到GameConfigs里面,这样引用配置表就方便了。
#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct GameConfigs {
pub achievement_configs: Box<AchievementConfigs>,
pub achievement_frame_configs: Box<AchievementFrameConfigs>,
pub achievement_point_configs: Box<AchievementPointConfigs>,
...//省略若干字段
}
需要注意的是,由于配置表非常的多,这个结构的字段会非常的多,所以数据需要进行堆分配(用Box包裹)避免爆栈。生成以上数据结构之后,我们使用bincode将此结构序列化成二进制文件,供游戏启动时加载或者热更新时替换。
至此,将策划配置的excel配置表数据解析成可供程序使用的数据结构就完成了。
将配置表生成对应的Rust结构体的逻辑以及如何将ExcelConfig经过一系列步骤生成GameConfigs,另外开一篇文章进行讲解,代码比较多,这部分涉及到大量的样板代码生成,会使用到syn以及quote