Compare commits

..

2 Commits

Author SHA1 Message Date
900cd48509 implemented command
refactor command to a new crate
2026-02-08 17:34:52 +08:00
bfcd414a14 Add working command validator 2026-02-08 14:37:32 +08:00
18 changed files with 19360 additions and 38 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
/target **/target
.idea .idea

10
Cargo.lock generated
View File

@@ -374,6 +374,7 @@ dependencies = [
"clap_mangen", "clap_mangen",
"color-eyre", "color-eyre",
"dirs", "dirs",
"magma-command",
"pest", "pest",
"pest_derive", "pest_derive",
"semver", "semver",
@@ -382,6 +383,15 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "magma-command"
version = "0.1.0"
dependencies = [
"color-eyre",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.7.6"

View File

@@ -3,14 +3,24 @@ name = "magma"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[workspace]
members = [ "magma-command" ]
[dependencies] [dependencies]
clap = { version = "4.5.54", features = ["cargo", "derive"] } clap = { version = "4.5.54", features = ["cargo", "derive"] }
clap_mangen = "0.2.31" clap_mangen = "0.2.31"
pest = { version = "2.8.5", features = ["pretty-print"] } pest = { version = "2.8.5", features = ["pretty-print"] }
pest_derive = { version = "2.8.5", features = ["grammar-extras"] } pest_derive = { version = "2.8.5", features = ["grammar-extras"] }
color-eyre = "0.6.5"
tokio = { version = "1.49.0", features = ["full"] } tokio = { version = "1.49.0", features = ["full"] }
serde = { version = "1.0.228", features = ["derive"] }
toml = "0.9.11" toml = "0.9.11"
semver = { version = "1.0.27", features = ["serde"] } semver = { version = "1.0.27", features = ["serde"] }
dirs = "6.0.0" dirs = "6.0.0"
magma-command = { path = "./magma-command" }
color-eyre.workspace = true
serde.workspace = true
[workspace.dependencies]
color-eyre = "0.6.5"
serde = { version = "1.0.228", features = ["derive"] }

18734
assets/commands.json Normal file

File diff suppressed because it is too large Load Diff

7
magma-command/Cargo.lock generated Normal file
View File

@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "magma-command"
version = "0.1.0"

10
magma-command/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "magma-command"
version = "0.1.0"
edition = "2024"
[dependencies]
serde_json = "1.0.149"
serde.workspace = true
color-eyre.workspace = true

View File

@@ -0,0 +1,48 @@
#[derive(Debug, Clone)]
pub enum CommandError {
EmptyCommand,
IncompleteCommand {
path: String,
suggestions: Vec<String>,
},
TooManyArguments {
path: String,
},
InvalidArgument {
path: String,
argument: String,
expected: Vec<String>,
},
InvalidRedirect {
from: String,
to: Vec<String>,
},
}
impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::EmptyCommand => write!(f, "Empty command"),
Self::IncompleteCommand { path, suggestions } => {
write!(f, "Incomplete command: {}\nExpected one of: {}", path, suggestions.join(", "))
}
Self::TooManyArguments { path } => {
write!(f, "Too many arguments for command: {}", path)
}
Self::InvalidArgument { path, argument, expected } => {
write!(
f,
"Invalid argument '{}' for command: {}\nExpected one of: {}",
argument,
path,
expected.join(", ")
)
},
Self::InvalidRedirect { from, to } => {
write!(f, "Invalid redirect from '{}' to '{}'", from, to.join(", "))
}
}
}
}
impl std::error::Error for CommandError {}

242
magma-command/src/lib.rs Normal file
View File

@@ -0,0 +1,242 @@
pub mod types;
pub mod error;
use types::{CommandNode, CommandTree, ParserType, StringKind};
use error::CommandError;
pub struct MinecraftCommandValidator {
root: CommandNode,
}
impl MinecraftCommandValidator {
pub fn new() -> color_eyre::Result<Self> {
let commands_json = include_str!("../../assets/commands.json");
let tree: CommandTree = serde_json::from_str(commands_json)?;
Ok(Self { root: tree.root })
}
pub fn validate(&self, command: &str) -> Result<ValidationResult, CommandError> {
let tokens = self.tokenize(command);
if tokens.is_empty() {
return Err(CommandError::EmptyCommand);
}
let result = self.validate_tokens(&tokens, &self.root, 0, Vec::new())?;
Ok(result)
}
fn tokenize(&self, command: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut escape_next = false;
for ch in command.chars() {
if escape_next {
current.push(ch);
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'"' => {
in_quotes = !in_quotes;
current.push(ch);
}
' ' | '\t' if !in_quotes => {
if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
fn validate_tokens(
&self,
tokens: &[String],
node: &CommandNode,
index: usize,
mut path: Vec<String>,
) -> Result<ValidationResult, CommandError> {
if index >= tokens.len() {
return if node.is_executable() {
Ok(ValidationResult {
valid: true,
path,
suggestions: self.get_suggestions_from_node(node),
})
} else {
Err(CommandError::IncompleteCommand {
path: path.join(" "),
suggestions: self.get_suggestions_from_node(node),
})
};
}
let current_token = &tokens[index];
let children = node.children();
if children.is_empty() && node.redirects().is_empty() {
return if node.is_executable() {
Err(CommandError::TooManyArguments {
path: path.join(" "),
})
} else {
Err(CommandError::IncompleteCommand {
path: path.join(" "),
suggestions: self.get_suggestions_from_node(node),
})
}
}
if !node.redirects().is_empty() {
let nodes = self.get_redirects(&node.redirects());
for node in nodes {
if let Ok(result) = self.validate_tokens(tokens, node, index, path.clone()) {
return Ok(result);
}
}
return Err(CommandError::InvalidRedirect {
from: path.join(" "),
to: node.redirects().to_vec(),
})
}
for child in children {
if let CommandNode::Literal { name, .. } = child && name == current_token {
path.push(current_token.clone());
return self.validate_tokens(tokens, child, index + 1, path);
}
}
self.parse_args(node, tokens, index, path)
}
/// an empty string value will redirect to all children
fn get_redirects(&self, name: &[String]) -> Vec<&CommandNode> {
self.root.children().iter()
.filter(
|child|
name.contains(child.name()) || name.contains(&String::from(""))
)
.collect()
}
fn parse_args(
&self,
node: &CommandNode,
tokens: &[String],
index: usize,
mut path: Vec<String>
) -> Result<ValidationResult, CommandError> {
let token = &tokens[index];
for child in node.children() {
let CommandNode::Argument { name, parser, .. } = child else {
continue;
};
let parser_type = ParserType::from_parser_info(parser);
if self.is_greedy_parser(&parser_type)
&& self.validate_argument(&tokens[index..].join(" "), &parser_type).is_ok()
{
path.push(format!("<{}>", name));
return self.validate_tokens(tokens, child, tokens.len(), path);
} else if self.validate_argument(token, &parser_type).is_ok() {
path.push(format!("<{}>", name));
return self.validate_tokens(tokens, child, index + 1, path);
}
}
Err(CommandError::InvalidArgument {
path: path.join(" "),
argument: token.to_string(),
expected: self.get_suggestions_from_node(node),
})
}
fn is_greedy_parser(&self, parser_type: &ParserType) -> bool {
matches!(
parser_type,
ParserType::String { kind: StringKind::GreedyPhrase }
| ParserType::Message
| ParserType::Component
)
}
fn validate_argument(&self, value: &str, parser_type: &ParserType) -> Result<(), String> {
match parser_type {
ParserType::Bool => {
if value == "true" || value == "false" {
Ok(())
} else {
Err("Expected 'true' or 'false'".to_string())
}
}
ParserType::Integer { min, max } => {
let num: i32 = value.parse().map_err(|_| "Invalid integer")?;
if let Some(min) = min && (num as f64) < *min {
return Err(format!("Value must be >= {}", min));
}
if let Some(max) = max && (num as f64) > *max {
return Err(format!("Value must be <= {}", max));
}
Ok(())
}
ParserType::Float { min, max } | ParserType::Double { min, max } => {
let num: f64 = value.parse().map_err(|_| "Invalid number")?;
if let Some(min) = min && num < *min {
return Err(format!("Value must be >= {}", min));
}
if let Some(max) = max && num > *max {
return Err(format!("Value must be <= {}", max));
}
Ok(())
}
ParserType::Entity { .. } => {
//TODO: have to check amount
if value.starts_with('@') {
Ok(())
} else {
Err("Invalid entity selector".to_string())
}
}
ParserType::String { .. } => Ok(()),
ParserType::Message | ParserType::Component => Ok(()),
_ => Ok(()),
}
}
fn get_suggestions_from_node(&self, node: &CommandNode) -> Vec<String> {
let mut suggestions = Vec::new();
for child in node.children() {
match child {
CommandNode::Literal { name, .. } => suggestions.push(name.clone()),
CommandNode::Argument { name, .. } => suggestions.push(format!("<{}>", name)),
_ => {}
}
}
suggestions
}
pub fn get_all_commands(&self) -> Vec<String> {
self.get_suggestions_from_node(&self.root)
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub path: Vec<String>,
pub suggestions: Vec<String>,
}

View File

@@ -0,0 +1,75 @@
#[derive(Debug, Clone)]
pub enum ParserType {
Bool,
Double { min: Option<f64>, max: Option<f64> },
Float { min: Option<f64>, max: Option<f64> },
Integer { min: Option<f64>, max: Option<f64> },
Long { min: Option<f64>, max: Option<f64> },
String { kind: StringKind },
Entity { amount: EntityAmount, entity_type: EntityType },
ScoreHolder { amount: ScoreHolderAmount },
GameProfile,
BlockPos,
ColumnPos,
Vec3,
Vec2,
BlockState,
BlockPredicate,
ItemStack,
ItemPredicate,
Color,
Component,
Message,
Nbt,
NbtTag,
NbtPath,
Objective,
ObjectiveCriteria,
Operation,
Particle,
Angle,
Rotation,
ScoreboardSlot,
Swizzle,
Team,
ItemSlot,
ResourceLocation { registry: Option<String> },
Function,
EntityAnchor,
IntRange,
FloatRange,
Dimension,
Gamemode,
Time,
ResourceOrTag { registry: Option<String> },
Resource { registry: Option<String> },
TemplateMirror,
TemplateRotation,
Uuid,
Unknown(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StringKind {
SingleWord,
QuotablePhrase,
GreedyPhrase,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntityAmount {
Single,
Multiple,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntityType {
Players,
Entities,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScoreHolderAmount {
Single,
Multiple,
}

View File

@@ -0,0 +1,86 @@
mod enums;
mod parser;
pub use enums::*;
pub use parser::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CommandTree {
pub root: CommandNode,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CommandNode {
Root {
name: String,
#[serde(default)]
executable: bool,
#[serde(default)]
redirects: Vec<String>,
#[serde(default)]
children: Vec<CommandNode>,
},
Literal {
name: String,
#[serde(default)]
executable: bool,
#[serde(default)]
redirects: Vec<String>,
#[serde(default)]
children: Vec<CommandNode>,
},
Argument {
name: String,
#[serde(default)]
executable: bool,
#[serde(default)]
redirects: Vec<String>,
#[serde(default)]
children: Vec<CommandNode>,
parser: ParserInfo,
},
}
impl CommandNode {
pub fn name(&self) -> &String {
match self {
CommandNode::Root { name, .. } => name,
CommandNode::Literal { name, .. } => name,
CommandNode::Argument { name, .. } => name,
}
}
pub fn is_executable(&self) -> bool {
match self {
CommandNode::Root { executable, .. } => *executable,
CommandNode::Literal { executable, .. } => *executable,
CommandNode::Argument { executable, .. } => *executable,
}
}
pub fn children(&self) -> &[CommandNode] {
match self {
CommandNode::Root { children, .. } => children,
CommandNode::Literal { children, .. } => children,
CommandNode::Argument { children, .. } => children,
}
}
pub fn redirects(&self) -> &[String] {
match self {
CommandNode::Root { redirects, .. } => redirects,
CommandNode::Literal { redirects, .. } => redirects,
CommandNode::Argument { redirects, .. } => redirects,
}
}
pub fn parser(&self) -> Option<&ParserInfo> {
match self {
CommandNode::Argument { parser, .. } => Some(parser),
_ => None,
}
}
}

View File

@@ -0,0 +1,67 @@
use serde::{Deserialize, Serialize};
use crate::types::{EntityAmount, EntityType, ParserType, ScoreHolderAmount, StringKind};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ParserInfo {
pub parser: String,
pub modifier: Option<serde_json::Value>,
}
impl ParserType {
pub fn from_parser_info(info: &ParserInfo) -> Self {
match info.parser.as_str() {
"brigadier:bool" => ParserType::Bool,
"brigadier:double" => ParserType::Double { min: None, max: None },
"brigadier:float" => ParserType::Float { min: None, max: None },
"brigadier:integer" => ParserType::Integer { min: None, max: None },
"brigadier:long" => ParserType::Long { min: None, max: None },
"brigadier:string" => ParserType::String { kind: StringKind::SingleWord },
"minecraft:entity" => ParserType::Entity {
amount: EntityAmount::Multiple,
entity_type: EntityType::Entities,
},
"minecraft:score_holder" => ParserType::ScoreHolder {
amount: ScoreHolderAmount::Multiple,
},
"minecraft:game_profile" => ParserType::GameProfile,
"minecraft:block_pos" => ParserType::BlockPos,
"minecraft:column_pos" => ParserType::ColumnPos,
"minecraft:vec3" => ParserType::Vec3,
"minecraft:vec2" => ParserType::Vec2,
"minecraft:block_state" => ParserType::BlockState,
"minecraft:block_predicate" => ParserType::BlockPredicate,
"minecraft:item_stack" => ParserType::ItemStack,
"minecraft:item_predicate" => ParserType::ItemPredicate,
"minecraft:color" => ParserType::Color,
"minecraft:component" => ParserType::Component,
"minecraft:message" => ParserType::Message,
"minecraft:nbt_compound_tag" => ParserType::Nbt,
"minecraft:nbt_tag" => ParserType::NbtTag,
"minecraft:nbt_path" => ParserType::NbtPath,
"minecraft:objective" => ParserType::Objective,
"minecraft:objective_criteria" => ParserType::ObjectiveCriteria,
"minecraft:operation" => ParserType::Operation,
"minecraft:particle" => ParserType::Particle,
"minecraft:angle" => ParserType::Angle,
"minecraft:rotation" => ParserType::Rotation,
"minecraft:scoreboard_slot" => ParserType::ScoreboardSlot,
"minecraft:swizzle" => ParserType::Swizzle,
"minecraft:team" => ParserType::Team,
"minecraft:item_slot" => ParserType::ItemSlot,
"minecraft:resource_location" => ParserType::ResourceLocation { registry: None },
"minecraft:function" => ParserType::Function,
"minecraft:entity_anchor" => ParserType::EntityAnchor,
"minecraft:int_range" => ParserType::IntRange,
"minecraft:float_range" => ParserType::FloatRange,
"minecraft:dimension" => ParserType::Dimension,
"minecraft:gamemode" => ParserType::Gamemode,
"minecraft:time" => ParserType::Time,
"minecraft:resource_or_tag" => ParserType::ResourceOrTag { registry: None },
"minecraft:resource" => ParserType::Resource { registry: None },
"minecraft:template_mirror" => ParserType::TemplateMirror,
"minecraft:template_rotation" => ParserType::TemplateRotation,
"minecraft:uuid" => ParserType::Uuid,
_ => ParserType::Unknown(info.parser.clone()),
}
}
}

View File

@@ -60,7 +60,7 @@ impl Cli {
if !out_dir.exists() { if !out_dir.exists() {
fs::create_dir_all(&out_dir).await?; fs::create_dir_all(&out_dir).await?;
} }
let mut compiler = ProjectCompiler::new(config, out_dir); let compiler = ProjectCompiler::new(config, out_dir)?;
compiler.run(&path).await?; compiler.run(&path).await?;
} else { } else {
return Err(color_eyre::eyre::eyre!("Path must be a directory for compilation")); return Err(color_eyre::eyre::eyre!("Path must be a directory for compilation"));

View File

@@ -8,15 +8,16 @@ pub(crate) struct ProjectCompiler {
} }
impl ProjectCompiler { impl ProjectCompiler {
pub fn new(config: MagmaProjectConfig, out_dir: PathBuf) -> Self { pub fn new(config: MagmaProjectConfig, out_dir: PathBuf) -> color_eyre::Result<Self> {
Self { let instance = Self {
compiler: MagmaCompiler::new(config), compiler: MagmaCompiler::new(config)?,
out_dir out_dir
} };
Ok(instance)
} }
pub async fn run(&mut self, path: &PathBuf) -> color_eyre::Result<()> { pub async fn run(self, path: &PathBuf) -> color_eyre::Result<()> {
self.compiler.compile(path).await?; let _ = self.compiler.compile(path).await?;
Ok(()) Ok(())
} }
} }

View File

@@ -1,30 +1,34 @@
use std::path::PathBuf; use std::path::PathBuf;
use pest::iterators::{Pair, Pairs}; use pest::iterators::{Pair, Pairs};
use pest::Parser; use pest::Parser;
use magma_command::MinecraftCommandValidator;
use crate::parser::{MagmaParser, Rule}; use crate::parser::{MagmaParser, Rule};
use crate::types::{MagmaProjectConfig, McFunctionFile}; use crate::types::{MagmaProjectConfig, McFunctionFile};
pub(crate) struct MagmaCompiler { pub(crate) struct MagmaCompiler {
out_functions: Vec<McFunctionFile>, out_functions: Vec<McFunctionFile>,
command_validator: MinecraftCommandValidator,
config: MagmaProjectConfig config: MagmaProjectConfig
} }
impl MagmaCompiler { impl MagmaCompiler {
pub(crate) fn new(config: MagmaProjectConfig) -> Self { pub(crate) fn new(config: MagmaProjectConfig) -> color_eyre::Result<Self> {
Self { let instance = Self {
out_functions: Vec::new(), out_functions: Vec::new(),
command_validator: MinecraftCommandValidator::new()?,
config config
} };
Ok(instance)
} }
pub async fn compile(&mut self, path: &PathBuf) -> color_eyre::Result<()> { pub async fn compile(mut self, path: &PathBuf) -> color_eyre::Result<Vec<McFunctionFile>> {
let main_file_path = path.join(&self.config.entrypoint); let main_file_path = path.join(&self.config.entrypoint);
let main_file_content = tokio::fs::read_to_string(&main_file_path).await?; let main_file_content = tokio::fs::read_to_string(&main_file_path).await?;
let parse_result = MagmaParser::parse(Rule::program, &main_file_content)?; let parse_result = MagmaParser::parse(Rule::program, &main_file_content)?;
for pair in parse_result { for pair in parse_result {
self.parse(pair)?; self.parse(pair)?;
} }
Ok(()) Ok(self.out_functions)
} }
fn parse(&mut self, pair: Pair<Rule>) -> color_eyre::Result<()> { fn parse(&mut self, pair: Pair<Rule>) -> color_eyre::Result<()> {
@@ -32,15 +36,17 @@ impl MagmaCompiler {
Rule::functionExpression => { Rule::functionExpression => {
let mut pairs = pair.into_inner(); let mut pairs = pair.into_inner();
let identifier = pairs.next().unwrap(); let identifier = pairs.next().unwrap();
let args = if let Some(rule) = pairs.peek() let args = if let Some(rule) = pairs.peek()
&& rule.as_rule() == Rule::functionArgs && rule.as_rule() == Rule::functionArgs
{ { pairs.next() }
Some(pairs.next().unwrap()) else { None };
} else { None };
let statements = if let Some(rule) = pairs.peek() let statements = if let Some(rule) = pairs.peek()
&& rule.as_rule() == Rule::block && rule.as_rule() == Rule::block
{ unbox_rule(pairs.next().unwrap()) } { expand_next_rule(pairs) }
else { None }; else { None };
self.parse_function(None, identifier, args, statements)?; self.parse_function(None, identifier, args, statements)?;
} }
_ => {} _ => {}
@@ -60,35 +66,59 @@ impl MagmaCompiler {
return Ok(file); return Ok(file);
}; };
for statement in statements { for statement in statements {
let Some(statement) = self.parse_statement(statement)? else { let statements = self.parse_statement(statement)?;
continue; file.add_lines(statements);
};
file.add_line(statement);
} }
println!("{}", file.lines().join("\n")); println!("{}", file.lines().join("\n"));
println!();
Ok(file) Ok(file)
} }
fn parse_statement(&self, statement: Pair<Rule>) -> color_eyre::Result<Option<String>> { fn parse_statement(&self, statement: Pair<Rule>) -> color_eyre::Result<Vec<String>> {
let statement = statement.into_inner().next().unwrap();
let mut lines = Vec::new();
match statement.as_rule() { match statement.as_rule() {
Rule::commandLine => { Rule::commandLine => {
let command = statement.into_inner().next().unwrap(); let command = statement.into_inner().next().unwrap();
Ok(Some(self.parse_command(command)?)) lines.push(self.parse_command(command)?);
},
Rule::commandBlock => {
let commands = statement.into_inner();
for command in commands {
lines.push(self.parse_command(command)?);
}
} }
_ => { _ => {
Ok(None) println!("{:?}", statement);
} }
} }
Ok(lines)
} }
fn parse_command(&self, command: Pair<Rule>) -> color_eyre::Result<String> { fn parse_command(&self, command: Pair<Rule>) -> color_eyre::Result<String> {
Ok(command.as_str().to_string()) let mut primitives = Vec::<String>::new();
for primitive in command.into_inner() {
match primitive.as_rule() {
Rule::string => {
let string = unbox_string(primitive);
primitives.push(string);
}
_ => { primitives.push(primitive.as_str().to_string())}
}
}
let unboxed_command = primitives.join(" ");
self.command_validator.validate(&unboxed_command)?;
Ok(unboxed_command)
} }
} }
fn unbox_rule(rule: Pair<Rule>) -> Option<Pairs<Rule>> { fn expand_next_rule(mut rule: Pairs<Rule>) -> Option<Pairs<Rule>> {
let mut block = rule.into_inner(); if let Some(inner) = rule.next() {
if let Some(inner) = block.next() {
Some(inner.into_inner()) Some(inner.into_inner())
} else { None } } else { None }
} }
fn unbox_string(string: Pair<Rule>) -> String {
let str = string.as_str();
str[1..str.len()-1].to_string()
}

View File

@@ -46,7 +46,8 @@ commandStatement = _{ commandLine | commandBlock }
commandLine = { "command" ~ command } commandLine = { "command" ~ command }
commandBlock = { "command" ~ "{" ~ command* ~ "}" } commandBlock = { "command" ~ "{" ~ command* ~ "}" }
command = { mcArg+ ~ ";" } command = { commandName ~ mcArg+ ~ ";" }
commandName = @{ ASCII_ALPHA+ }
mcArg = _{ nbtBlock | string | mcPrimitive } mcArg = _{ nbtBlock | string | mcPrimitive }
nbtBlock = { "{" ~ (nbtBlock | string | !("}" | "{") ~ ANY)* ~ "}" } nbtBlock = { "{" ~ (nbtBlock | string | !("}" | "{") ~ ANY)* ~ "}" }
mcPrimitive = @{ (!(";" | "{" | "}" | "\"" | WHITESPACE) ~ ANY)+ } mcPrimitive = @{ (!(";" | "{" | "}" | "\"" | WHITESPACE) ~ ANY)+ }

View File

@@ -2,8 +2,4 @@ use pest_derive::Parser;
#[derive(Parser)] #[derive(Parser)]
#[grammar = "grammar.pest"] #[grammar = "grammar.pest"]
pub struct MagmaParser; pub struct MagmaParser;
impl MagmaParser {
}

View File

@@ -12,7 +12,7 @@ pub struct MagmaProjectConfig {
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Version { pub struct Version {
pub pack: [u8; 2], pub pack: [u8; 2],
pub redoxide: semver::Version, pub magma: semver::Version,
} }
impl Default for MagmaProjectConfig { impl Default for MagmaProjectConfig {
@@ -29,7 +29,7 @@ impl Default for Version {
fn default() -> Self { fn default() -> Self {
Self { Self {
pack: [94, 1], pack: [94, 1],
redoxide: semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap() magma: semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
} }
} }
} }

View File

@@ -1,3 +1,4 @@
#[derive(Debug, Clone)]
pub(crate) struct McFunctionFile { pub(crate) struct McFunctionFile {
content: Vec<String>, content: Vec<String>,
namespace: Option<String>, namespace: Option<String>,
@@ -17,6 +18,10 @@ impl McFunctionFile {
self.content.push(line); self.content.push(line);
} }
pub fn add_lines(&mut self, lines: Vec<String>) {
self.content.extend(lines);
}
pub fn lines(&self) -> &Vec<String> { pub fn lines(&self) -> &Vec<String> {
&self.content &self.content
} }