From 3b7d4f6eb6aea5d2b98d00246a618a71793aace1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Cedro?= <61706594+SpontanCombust@users.noreply.github.com> Date: Mon, 17 Jul 2023 10:52:36 +0200 Subject: [PATCH] v0.3.1 (#7) --- .github/workflows/check-build.yml | 20 +- .github/workflows/regenerate-samples.yml | 55 ++++++ doc/mod-menu.ws | 11 +- .../scripts/local/difficulty_mod_base.ws | 2 +- settings-parser/Cargo.toml | 2 +- settings-parser/src/main.rs | 26 +-- settings-parser/src/settings_group.rs | 73 ++++++- settings-parser/src/settings_master.rs | 54 +++++- settings-parser/src/settings_var.rs | 47 ++++- settings-parser/src/utils.rs | 30 +++ settings-parser/src/xml_parsing.rs | 178 ------------------ 11 files changed, 292 insertions(+), 206 deletions(-) create mode 100644 .github/workflows/regenerate-samples.yml create mode 100644 settings-parser/src/utils.rs delete mode 100644 settings-parser/src/xml_parsing.rs diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 4a34ea8..b0fea28 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -11,8 +11,18 @@ env: jobs: build: + strategy: + matrix: + target: + - x86_64-pc-windows-msvc + - x86_64-unknown-linux-gnu + include: + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest - runs-on: windows-latest + runs-on: ${{matrix.os}} steps: - name: Checkout @@ -21,11 +31,11 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable - target: x86_64-pc-windows-msvc + target: ${{matrix.target}} override: true - name: Build with Cargo - run: cargo build --release --target=x86_64-pc-windows-msvc - working-directory: ./settings-parser + run: cargo build --release --target=${{matrix.target}} + working-directory: settings-parser - name: Run tests run: cargo test --verbose - working-directory: ./settings-parser \ No newline at end of file + working-directory: settings-parser \ No newline at end of file diff --git a/.github/workflows/regenerate-samples.yml b/.github/workflows/regenerate-samples.yml new file mode 100644 index 0000000..d2e491e --- /dev/null +++ b/.github/workflows/regenerate-samples.yml @@ -0,0 +1,55 @@ +name: regenerate-samples + +on: + push: + branches: '*' + pull_request: + branches: '*' + +env: + CARGO_TERM_COLOR: always + +jobs: + regenerate-samples: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Use Rust stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: x86_64-unknown-linux-gnu + override: true + - name: Build with Cargo + run: cargo build --release --target=x86_64-unknown-linux-gnu + working-directory: settings-parser + - name: Parse doc example + run: > + cargo run --release -- + -f="../doc/mod-menu.xml" + -o="../doc/mod-menu.ws" + --omit-prefix="MOD" + -m="MyModSettings" + -v="1.23" + working-directory: settings-parser + - name: Parse DifficultyMod sample + run: > + cargo run --release -- + -f="../samples/DifficultyMod/bin/config/r4game/user_config_matrix/pc/modSettingsFrameworkSampleDifficultyMod.xml" + -o="../samples/DifficultyMod/modSettingsFrameworkSampleDifficultyMod/content/scripts/local/difficulty_mod_base.ws" + -m="ModDifficultySettingsBase" + --omit-prefix="DM" + --default-preset-keyword="DEFAULT" + -v="1.1" + working-directory: settings-parser + - name: Check for changes in the project + id: get_changes + run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT + - name: Committing changes if there are any + if: steps.get_changes.outputs.changed != 0 + uses: EndBug/add-and-commit@v7 + with: + message: "Update samples" + \ No newline at end of file diff --git a/doc/mod-menu.ws b/doc/mod-menu.ws index 020a457..c169f78 100644 --- a/doc/mod-menu.ws +++ b/doc/mod-menu.ws @@ -1,4 +1,4 @@ -// Code generated using Mod Settings Framework & Utilites v0.2.0 by SpontanCombust +// Code generated using Mod Settings Framework v0.3.1 by SpontanCombust & Aeltoth class MyModSettings extends ISettingsMaster { @@ -57,13 +57,16 @@ class MyModSettings extends ISettingsMaster public function ResetSettingsToDefault() : void { - tab1.ResetSettingsToDefault(); - tab2subtab1.ResetSettingsToDefault(); - tab2subtab2.ResetSettingsToDefault(); + tab1.ResetToDefault(); + tab2subtab1.ResetToDefault(); + tab2subtab2.ResetToDefault(); } public function ShouldResetSettingsToDefaultOnInit() : bool { + var config : CInGameConfigWrapper; + config = theGame.GetInGameConfigWrapper(); + return config.GetVarValue('MODtab1','MODoption') == ""; } } diff --git a/samples/DifficultyMod/modSettingsFrameworkSampleDifficultyMod/content/scripts/local/difficulty_mod_base.ws b/samples/DifficultyMod/modSettingsFrameworkSampleDifficultyMod/content/scripts/local/difficulty_mod_base.ws index b63e8be..260054f 100644 --- a/samples/DifficultyMod/modSettingsFrameworkSampleDifficultyMod/content/scripts/local/difficulty_mod_base.ws +++ b/samples/DifficultyMod/modSettingsFrameworkSampleDifficultyMod/content/scripts/local/difficulty_mod_base.ws @@ -1,4 +1,4 @@ -// Code generated using Mod Settings Framework & Utilites v0.2.0 by SpontanCombust +// Code generated using Mod Settings Framework v0.3.1 by SpontanCombust & Aeltoth class ModDifficultySettingsBase extends ISettingsMaster { diff --git a/settings-parser/Cargo.toml b/settings-parser/Cargo.toml index c76be67..8644bcb 100644 --- a/settings-parser/Cargo.toml +++ b/settings-parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tw3-mod-settings-framework-parser" -version = "0.3.0" +version = "0.3.1" edition = "2021" [[bin]] diff --git a/settings-parser/src/main.rs b/settings-parser/src/main.rs index 5c28f3b..e324f36 100644 --- a/settings-parser/src/main.rs +++ b/settings-parser/src/main.rs @@ -2,18 +2,20 @@ mod var_type; mod settings_var; mod settings_group; mod settings_master; -mod xml_parsing; mod to_witcher_script; mod cli; +mod utils; use std::{fs::OpenOptions, io::{Read, Write}, path::{Path, PathBuf}}; use clap::Parser; use cli::CLI; -use to_witcher_script::ToWitcherScript; +use settings_master::SettingsMaster; +use crate::to_witcher_script::ToWitcherScript; -fn main() { + +fn main() -> Result<(), String>{ let cli = CLI::parse(); let input_file_path = Path::new(&cli.xml_file_path); @@ -26,8 +28,7 @@ fn main() { let mut xml_file = match xml_file { Ok(f) => f, Err(e) => { - println!("Error opening menu xml file: {}", e); - return; + return Err(format!("Error opening menu xml file: {}", e)); } }; @@ -45,8 +46,7 @@ fn main() { let mut ws_file = match ws_file { Ok(f) => f, Err(e) => { - println!("Error creating witcher script output file: {}", e); - return; + return Err(format!("Error creating witcher script output file: {}", e)); } }; @@ -54,11 +54,10 @@ fn main() { let mut xml_text = String::new(); if let Err(e) = xml_file.read_to_string(&mut xml_text) { - println!("Error reading menu xml file: {}", e); - return; + return Err(format!("Error reading menu xml file: {}", e)); }; - match xml_parsing::parse_settings_xml(xml_text, &cli) { + match SettingsMaster::from_xml(xml_text, &cli) { Ok(master) => { let mut code = String::new(); @@ -70,14 +69,15 @@ fn main() { } if let Err(e) = ws_file.write_all(code.as_bytes()) { - println!("Error writing witcher script output file: {}", e); - return; + return Err(format!("Error writing witcher script output file: {}", e)); } println!("Successfully parsed {} into {}", cli.xml_file_path, ws_path.to_str().unwrap_or("")); } Err(e) => { - println!("Error parsing menu xml file: {}", e); + return Err(format!("Error parsing menu xml file: {}", e)); } } + + return Ok(()) } diff --git a/settings-parser/src/settings_group.rs b/settings-parser/src/settings_group.rs index 147c09d..18c922b 100644 --- a/settings-parser/src/settings_group.rs +++ b/settings-parser/src/settings_group.rs @@ -1,4 +1,6 @@ -use crate::{settings_var::SettingsVar, to_witcher_script::ToWitcherScript}; +use roxmltree::Node; + +use crate::{settings_var::SettingsVar, to_witcher_script::ToWitcherScript, cli::CLI, utils::{validate_name, node_pos, id_to_script_name}}; #[derive(Default)] pub struct SettingsGroup { @@ -9,6 +11,75 @@ pub struct SettingsGroup { pub vars: Vec } +impl SettingsGroup { + pub fn from_xml(group_node: &Node, cli: &CLI) -> Result, String> { + if let Some(group_id) = group_node.attribute("id") { + + if let Err(err) = validate_name(group_id) { + return Err(format!("Invalid Group id {} at {}: {}", group_id, node_pos(group_node), err)); + } + + let mut default_preset_index: Option = None; + if let Some(presets_array_node) = group_node.children().find(|n| n.has_tag_name("PresetsArray")) { + default_preset_index = SettingsGroup::parse_presets_array_node(&presets_array_node, cli); + } + + if let Some(visible_vars_node) = group_node.children().find(|n| n.has_tag_name("VisibleVars")) { + let var_nodes: Vec = visible_vars_node.children().filter(|n| n.has_tag_name("Var")).collect(); + + if var_nodes.is_empty() { + println!("Group {} at {} has no vars and will be ignored.", group_id, node_pos(group_node)); + return Ok(None); + } + + let mut sg = SettingsGroup::default(); + sg.master_name = cli.settings_master_name.to_owned(); + sg.id = group_id.to_owned(); + sg.name = id_to_script_name(group_id, &cli.omit_prefix); + sg.default_preset_index = default_preset_index; + + for var_node in &var_nodes { + match SettingsVar::from_xml(&var_node, group_id, cli) { + Ok(var_opt) => { + if let Some(var) = var_opt { + sg.vars.push(var); + } + } + Err(err) => { + return Err(err); + } + } + } + + return Ok(Some(sg)); + } + else { + println!("Group {} at {} has no vars and will be ignored.", group_id, node_pos(group_node)); + return Ok(None); + } + } + else { + println!("No id attribute found for Group tag at {}", node_pos(group_node)); + return Ok(None); + } + } + + fn parse_presets_array_node(presets_array_node: &Node, cli: &CLI) -> Option { + for preset_node in presets_array_node.children() { + if preset_node.has_tag_name("Preset") && preset_node.has_attribute("id") && preset_node.has_attribute("displayName") { + if preset_node.attribute("displayName").unwrap().contains(&cli.default_preset_keyword.to_lowercase()) { + return preset_node.attribute("id").unwrap().parse::().ok(); + } + } + } + + return None; + } +} + + + + const SETTINGS_GROUP_PARENT_CLASS: &str = "ISettingsGroup"; const SETTINGS_GROUP_ID_VAR_NAME: &str = "id"; const SETTINGS_GROUP_DEFAULT_PRESET_VAR_NAME: &str = "defaultPresetIndex"; diff --git a/settings-parser/src/settings_master.rs b/settings-parser/src/settings_master.rs index 83a2991..b6d2f2d 100644 --- a/settings-parser/src/settings_master.rs +++ b/settings-parser/src/settings_master.rs @@ -1,4 +1,6 @@ -use crate::{settings_group::SettingsGroup, to_witcher_script::ToWitcherScript, var_type::VarType}; +use roxmltree::{Document, Node}; + +use crate::{settings_group::SettingsGroup, to_witcher_script::ToWitcherScript, var_type::VarType, cli::CLI, utils::validate_name}; #[derive(Default)] pub struct SettingsMaster { @@ -7,6 +9,54 @@ pub struct SettingsMaster { pub groups: Vec } +impl SettingsMaster { + pub fn from_xml(xml_text: String, cli: &CLI) -> Result { + if let Err(err) = validate_name(&cli.settings_master_name) { + return Err(format!("Invalid settings master name: {}", err)); + } + + let doc = match Document::parse(&xml_text) { + Ok(doc) => doc, + Err(err) => { + return Err(err.to_string()) + } + }; + + let mut master = SettingsMaster::default(); + master.name = cli.settings_master_name.clone(); + master.mod_version = cli.mod_version.clone(); + + if let Some(root_node) = doc.descendants().find(|n| n.has_tag_name("UserConfig")) { + let group_nodes: Vec = root_node.children().filter(|n| n.has_tag_name("Group")).collect(); + + if group_nodes.is_empty() { + return Err("No Groups found inside UserConfig".to_string()); + } + + for group_node in &group_nodes { + match SettingsGroup::from_xml(group_node, cli) { + Ok(group_opt) => { + if let Some(group) = group_opt { + master.groups.push(group); + } + } + Err(err) => { + return Err(err); + } + } + } + } + else { + return Err("No UserConfig root node found".to_string()); + } + + return Ok(master); + } +} + + + + const MASTER_BASE_CLASS_NAME: &str = "ISettingsMaster"; const MASTER_MOD_VERSION_VAR_NAME: &str = "modVersion"; const MASTER_INIT_FUNC_NAME: &str = "Init"; @@ -26,7 +76,7 @@ impl ToWitcherScript for SettingsMaster { fn ws_code_body(&self) -> String { let mut code = String::new(); - code += &format!("// Code generated using Mod Settings Framework & Utilites v{} by SpontanCombust\n\n", option_env!("CARGO_PKG_VERSION").unwrap()); + code += &format!("// Code generated using Mod Settings Framework v{} by SpontanCombust & Aeltoth\n\n", option_env!("CARGO_PKG_VERSION").unwrap()); code += &format!("class {} extends {}\n", self.name, MASTER_BASE_CLASS_NAME); code += "{\n"; diff --git a/settings-parser/src/settings_var.rs b/settings-parser/src/settings_var.rs index 552c749..3b368c0 100644 --- a/settings-parser/src/settings_var.rs +++ b/settings-parser/src/settings_var.rs @@ -1,4 +1,6 @@ -use crate::{var_type::VarType, to_witcher_script::ToWitcherScript}; +use roxmltree::Node; + +use crate::{var_type::VarType, to_witcher_script::ToWitcherScript, cli::CLI, utils::{node_pos, validate_name, id_to_script_name}}; pub struct SettingsVar { pub id: String, @@ -6,6 +8,49 @@ pub struct SettingsVar { pub var_type: VarType } +impl SettingsVar { + pub fn from_xml(var_node: &Node, group_id: &str, cli: &CLI) -> Result, String> { + let var_id = match var_node.attribute("id") { + Some(id) => id, + None => { + println!("Var node without id found in Group {} at {}", group_id, node_pos(var_node)); + return Ok(None); + } + }; + + if let Err(err) = validate_name(var_id) { + return Err(format!("Invalid Var id {} at {}: {}", var_id, node_pos(var_node), err)); + } + + let var_display_type = match var_node.attribute("displayType") { + Some(dt) => dt, + None => { + println!("Var node without displayType found in Group {} at {}", group_id, node_pos(var_node)); + return Ok(None); + } + }; + + let var_type = match VarType::from_display_type(var_display_type) { + Ok(vto) => match vto { + Some(vt) => vt, + None => return Ok(None), + }, + Err(err) => { + println!("Error parsing Var node's display_type in Group {} at {}: {}", group_id, node_pos(var_node), err); + return Ok(None); + } + }; + + + return Ok(Some(SettingsVar { + id: var_id.to_owned(), + name: id_to_script_name(var_id, &cli.omit_prefix), + var_type: var_type + })); + } +} + + impl ToWitcherScript for SettingsVar { fn ws_type_name(&self) -> String { diff --git a/settings-parser/src/utils.rs b/settings-parser/src/utils.rs new file mode 100644 index 0000000..4f15783 --- /dev/null +++ b/settings-parser/src/utils.rs @@ -0,0 +1,30 @@ +use roxmltree::Node; + +pub(crate) fn validate_name(name: &str) -> Result<(), String> { + if name.is_empty() { + return Err("name cannot be empty".to_string()); + } + if name.chars().nth(0).unwrap().is_numeric() { + return Err("name cannot start with a number".to_string()); + } + if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') { + return Err("name can only have alphanumeric characters and underscores and have no spaces".to_string()); + } + + return Ok(()); +} + +pub(crate) fn node_pos(node: &Node) -> String { + let pos = node.document().text_pos_at(node.range().start); + format!("line {}, column {}", pos.row, pos.col) +} + +pub(crate) fn id_to_script_name(id: &str, omit_prefix: &Option) -> String { + if let Some(prefix) = omit_prefix { + if id.starts_with(prefix) { + return id[prefix.len()..].to_string(); + } + } + + return id.to_string(); +} \ No newline at end of file diff --git a/settings-parser/src/xml_parsing.rs b/settings-parser/src/xml_parsing.rs deleted file mode 100644 index 332e502..0000000 --- a/settings-parser/src/xml_parsing.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::{settings_master::SettingsMaster, settings_group::SettingsGroup, settings_var::SettingsVar, var_type::VarType, CLI}; -use roxmltree::{self, Document, Node}; - -pub fn parse_settings_xml(xml_text: String, cli: &CLI) -> Result { - if let Err(err) = validate_name(&cli.settings_master_name) { - return Err(format!("Invalid settings master name: {}", err)); - } - - let doc = match Document::parse(&xml_text) { - Ok(doc) => doc, - Err(err) => { - return Err(err.to_string()) - } - }; - - let mut master = SettingsMaster::default(); - master.name = cli.settings_master_name.clone(); - master.mod_version = cli.mod_version.clone(); - - if let Some(root_node) = doc.descendants().find(|n| n.has_tag_name("UserConfig")) { - let group_nodes: Vec = root_node.children().filter(|n| n.has_tag_name("Group")).collect(); - - if group_nodes.is_empty() { - return Err("No Groups found inside UserConfig".to_string()); - } - - for group_node in &group_nodes { - match parse_group_node(group_node, cli) { - Ok(group_opt) => { - if let Some(group) = group_opt { - master.groups.push(group); - } - } - Err(err) => { - return Err(err); - } - } - } - } - else { - return Err("No UserConfig root node found".to_string()); - } - - return Ok(master); -} - -fn validate_name(name: &str) -> Result<(), String> { - if name.is_empty() { - return Err("name cannot be empty".to_string()); - } - if name.chars().nth(0).unwrap().is_numeric() { - return Err("name cannot start with a number".to_string()); - } - if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') { - return Err("name can only have alphanumeric characters and underscores and have no spaces".to_string()); - } - - return Ok(()); -} - -fn parse_group_node(group_node: &Node, cli: &CLI) -> Result, String> { - if let Some(group_id) = group_node.attribute("id") { - - if let Err(err) = validate_name(group_id) { - return Err(format!("Invalid Group id {} at {}: {}", group_id, node_pos(group_node), err)); - } - - let mut default_preset_index: Option = None; - if let Some(presets_array_node) = group_node.children().find(|n| n.has_tag_name("PresetsArray")) { - default_preset_index = parse_presets_array_node(&presets_array_node, cli); - } - - if let Some(visible_vars_node) = group_node.children().find(|n| n.has_tag_name("VisibleVars")) { - let var_nodes: Vec = visible_vars_node.children().filter(|n| n.has_tag_name("Var")).collect(); - - if var_nodes.is_empty() { - println!("Group {} at {} has no vars and will be ignored.", group_id, node_pos(group_node)); - return Ok(None); - } - - let mut sg = SettingsGroup::default(); - sg.master_name = cli.settings_master_name.to_owned(); - sg.id = group_id.to_owned(); - sg.name = id_to_script_name(group_id, &cli.omit_prefix); - sg.default_preset_index = default_preset_index; - - for var_node in &var_nodes { - match parse_var_node(&var_node, group_id, cli) { - Ok(var_opt) => { - if let Some(var) = var_opt { - sg.vars.push(var); - } - } - Err(err) => { - return Err(err); - } - } - } - - return Ok(Some(sg)); - } - else { - println!("Group {} at {} has no vars and will be ignored.", group_id, node_pos(group_node)); - return Ok(None); - } - } - else { - println!("No id attribute found for Group tag at {}", node_pos(group_node)); - return Ok(None); - } -} - -fn parse_presets_array_node(presets_array_node: &Node, cli: &CLI) -> Option { - for preset_node in presets_array_node.children() { - if preset_node.has_tag_name("Preset") && preset_node.has_attribute("id") && preset_node.has_attribute("displayName") { - if preset_node.attribute("displayName").unwrap().contains(&cli.default_preset_keyword.to_lowercase()) { - return preset_node.attribute("id").unwrap().parse::().ok(); - } - } - } - - return None; -} - -fn parse_var_node(var_node: &Node, group_id: &str, cli: &CLI) -> Result, String> { - let var_id = match var_node.attribute("id") { - Some(id) => id, - None => { - println!("Var node without id found in Group {} at {}", group_id, node_pos(var_node)); - return Ok(None); - } - }; - - if let Err(err) = validate_name(var_id) { - return Err(format!("Invalid Var id {} at {}: {}", var_id, node_pos(var_node), err)); - } - - let var_display_type = match var_node.attribute("displayType") { - Some(dt) => dt, - None => { - println!("Var node without displayType found in Group {} at {}", group_id, node_pos(var_node)); - return Ok(None); - } - }; - - let var_type = match VarType::from_display_type(var_display_type) { - Ok(vto) => match vto { - Some(vt) => vt, - None => return Ok(None), - }, - Err(err) => { - println!("Error parsing Var node's display_type in Group {} at {}: {}", group_id, node_pos(var_node), err); - return Ok(None); - } - }; - - - return Ok(Some(SettingsVar { - id: var_id.to_owned(), - name: id_to_script_name(var_id, &cli.omit_prefix), - var_type: var_type - })); -} - -fn node_pos(node: &Node) -> String { - let pos = node.document().text_pos_at(node.range().start); - format!("line {}, column {}", pos.row, pos.col) -} - -fn id_to_script_name(id: &str, omit_prefix: &Option) -> String { - if let Some(prefix) = omit_prefix { - if id.starts_with(prefix) { - return id[prefix.len()..].to_string(); - } - } - - return id.to_string(); -} \ No newline at end of file