1use convert_case::{Case, Casing};
24use regex::Regex;
25use std::{
26 collections::HashMap,
27 fmt::Display,
28 fs::{create_dir_all, read_dir, remove_dir_all, File},
29 io::{Read, Write},
30 path::Path,
31 process::Command,
32};
33use toml_edit::{table, value, DocumentMut};
34use uuid::Uuid;
35
36pub static CURRENT_ENGINE_VERSION: &str = include_str!("../engine.version");
37pub static CURRENT_EDITOR_VERSION: &str = include_str!("../editor.version");
38pub static CURRENT_SCRIPTS_VERSION: &str = include_str!("../scripts.version");
39
40fn write_file<P: AsRef<Path>, S: AsRef<str>>(path: P, content: S) -> Result<(), String> {
41 let mut file = File::create(path.as_ref()).map_err(|e| e.to_string())?;
42 file.write_all(content.as_ref().as_bytes()).map_err(|x| {
43 format!(
44 "Error happened while writing to file: {}.\nError:\n{}",
45 path.as_ref().to_string_lossy(),
46 x
47 )
48 })
49}
50
51fn write_file_binary<P: AsRef<Path>>(path: P, content: &[u8]) -> Result<(), String> {
52 let mut file = File::create(path.as_ref()).map_err(|e| e.to_string())?;
53 file.write_all(content).map_err(|x| {
54 format!(
55 "Error happened while writing to file: {}.\nError:\n{}",
56 path.as_ref().to_string_lossy(),
57 x
58 )
59 })
60}
61
62#[derive(Debug)]
63pub enum NameError {
64 Empty,
65 CargoReserved(String),
66 StartsWithNumber,
67 InvalidCharacter(char),
68}
69
70impl std::error::Error for NameError {}
71
72impl Display for NameError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 Self::CargoReserved(name) => write!(
76 f,
77 "The project name cannot be `{name}` due to cargo's reserved keywords"
78 ),
79 Self::StartsWithNumber => write!(f, "The project name cannot start with a number"),
80 Self::InvalidCharacter(ch) => write!(
81 f,
82 "The project name cannot contain {ch} \
83 characters! It can start from most letters or '_' symbol and the rest of the name \
84 must be letters, '-', '_', numbers."
85 ),
86 NameError::Empty => {
87 write!(f, "The project name cannot be empty!")
88 }
89 }
90 }
91}
92
93pub fn check_name(name: &str) -> Result<&str, NameError> {
94 const RESERVED_NAMES: [&str; 53] = [
95 "abstract", "alignof", "as", "become", "box", "break", "const", "continue", "crate", "do",
96 "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in", "let", "loop",
97 "macro", "match", "mod", "move", "mut", "offsetof", "override", "priv", "proc", "pub",
98 "pure", "ref", "return", "self", "sizeof", "static", "struct", "super", "test", "trait",
99 "true", "type", "typeof", "try", "unsafe", "unsized", "use", "virtual", "where", "while",
100 "yield",
101 ];
102
103 if name.is_empty() {
104 return Err(NameError::Empty);
105 }
106
107 if RESERVED_NAMES.contains(&name) {
108 return Err(NameError::CargoReserved(name.to_string()));
109 }
110
111 let mut chars = name.chars();
112 if let Some(ch) = chars.next() {
113 if ch.is_ascii_digit() {
114 return Err(NameError::StartsWithNumber);
115 }
116 if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') {
117 return Err(NameError::InvalidCharacter(ch));
118 }
119 }
120
121 for ch in chars {
122 if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-') {
123 return Err(NameError::InvalidCharacter(ch));
124 }
125 }
126
127 Ok(name)
128}
129
130pub fn convert_name(name: &str) -> String {
131 name.replace("-", "_").to_lowercase()
132}
133
134fn init_game(base_path: &Path, name: &str) -> Result<(), String> {
135 Command::new("cargo")
136 .args(["init", "--lib", "--vcs", "none"])
137 .arg(base_path.join("game"))
138 .output()
139 .map_err(|e| e.to_string())?;
140
141 write_file(
143 base_path.join("game/Cargo.toml"),
144 format!(
145 r#"
146[package]
147name = "{name}"
148version = "0.1.0"
149edition = "2021"
150
151[dependencies]
152fyrox = {{workspace = true}}
153
154[features]
155default = ["fyrox/default"]
156dylib-engine = ["fyrox/dylib"]
157"#,
158 ),
159 )?;
160
161 write_file(
163 base_path.join("game/src/lib.rs"),
164 r#"//! Game project.
165#[allow(unused_imports)]
166use fyrox::graph::prelude::*;
167use fyrox::{
168 core::pool::Handle, core::visitor::prelude::*, core::reflect::prelude::*,
169 event::Event,
170 gui::{message::UiMessage, UserInterface},
171 plugin::{Plugin, PluginContext, PluginRegistrationContext, error::GameResult},
172};
173
174// Re-export the engine.
175pub use fyrox;
176
177#[derive(Default, Visit, Reflect, Debug)]
178#[reflect(non_cloneable)]
179pub struct Game { }
180
181impl Plugin for Game {
182 fn register(&self, _context: PluginRegistrationContext) -> GameResult {
183 // Register your scripts here.
184 Ok(())
185 }
186
187 fn init(&mut self, scene_path: Option<&str>, mut context: PluginContext) -> GameResult {
188 context.load_scene_or_ui::<Self>(scene_path.unwrap_or("data/scene.rgs"));
189 Ok(())
190 }
191
192 fn on_deinit(&mut self, _context: PluginContext) -> GameResult {
193 // Do a cleanup here.
194 Ok(())
195 }
196
197 fn update(&mut self, _context: &mut PluginContext) -> GameResult {
198 // Add your global update code here.
199 Ok(())
200 }
201
202 fn on_os_event(
203 &mut self,
204 _event: &Event<()>,
205 _context: PluginContext,
206 ) -> GameResult {
207 // Do something on OS event here.
208 Ok(())
209 }
210
211 fn on_ui_message(
212 &mut self,
213 _context: &mut PluginContext,
214 _message: &UiMessage,
215 _ui_handle: Handle<UserInterface>
216 ) -> GameResult {
217 // Handle UI events here.
218 Ok(())
219 }
220}
221"#,
222 )
223}
224
225fn init_executor(base_path: &Path, name: &str) -> Result<(), String> {
226 Command::new("cargo")
227 .args(["init", "--bin", "--vcs", "none"])
228 .arg(base_path.join("executor"))
229 .output()
230 .map_err(|e| e.to_string())?;
231
232 write_file(
234 base_path.join("executor/Cargo.toml"),
235 format!(
236 r#"
237[package]
238name = "executor"
239version = "0.1.0"
240edition = "2021"
241
242[dependencies]
243fyrox = {{ workspace = true }}
244{name} = {{ path = "../game", optional = true }}
245
246[features]
247default = ["{name}"]
248dylib = ["fyrox/dylib"]
249"#,
250 ),
251 )?;
252
253 write_file(
255 base_path.join("executor/src/main.rs"),
256 format!(
257 r#"//! Executor with your game connected to it as a plugin.
258use fyrox::engine::executor::Executor;
259use fyrox::event_loop::EventLoop;
260use fyrox::core::log::Log;
261
262fn main() {{
263 Log::set_file_name("{name}.log");
264
265 let mut executor = Executor::new(Some(EventLoop::new().unwrap()));
266
267 // Dynamic linking with hot reloading.
268 #[cfg(feature = "dylib")]
269 {{
270 #[cfg(target_os = "windows")]
271 let file_name = "game_dylib.dll";
272 #[cfg(target_os = "linux")]
273 let file_name = "libgame_dylib.so";
274 #[cfg(target_os = "macos")]
275 let file_name = "libgame_dylib.dylib";
276 executor.add_dynamic_plugin(file_name, true, true).unwrap();
277 }}
278
279 // Static linking.
280 #[cfg(not(feature = "dylib"))]
281 {{
282 use {name}::Game;
283 executor.add_plugin(Game::default());
284 }}
285
286 executor.run()
287}}"#,
288 ),
289 )
290}
291
292fn init_export_cli(base_path: &Path, name: &str) -> Result<(), String> {
293 Command::new("cargo")
294 .args(["init", "--bin", "--vcs", "none"])
295 .arg(base_path.join("export-cli"))
296 .output()
297 .map_err(|e| e.to_string())?;
298
299 write_file(
301 base_path.join("export-cli/Cargo.toml"),
302 format!(
303 r#"
304[package]
305name = "export-cli"
306version = "0.1.0"
307edition = "2021"
308
309[dependencies]
310fyrox = {{ workspace = true }}
311fyrox-build-tools = {{ workspace = true }}
312{name} = {{ path = "../game" }}
313"#,
314 ),
315 )?;
316
317 write_file(
319 base_path.join("export-cli/src/main.rs"),
320 format!(
321 r#"//! Exporter command line interface (CLI) with your game connected to it as a plugin.
322//! This tool can be used to automate project export in CI/CD.
323//! Typical usage: `cargo run --package export-cli -- --target-platform pc`
324//! or `cargo run --package export-cli -- --help` for the docs.
325
326use {name}::Game;
327use fyrox::core::log::Log;
328use fyrox::engine::executor::Executor;
329use fyrox::event_loop::EventLoop;
330use fyrox_build_tools::export::cli_export;
331
332fn main() {{
333 Log::set_file_name("{name}Export.log");
334 let mut executor = Executor::new(EventLoop::new().ok());
335 executor.add_plugin(Game::default());
336 cli_export(executor.resource_manager.clone())
337}}"#,
338 ),
339 )
340}
341
342fn init_wasm_executor(base_path: &Path, name: &str) -> Result<(), String> {
343 Command::new("cargo")
344 .args(["init", "--lib", "--vcs", "none"])
345 .arg(base_path.join("executor-wasm"))
346 .output()
347 .map_err(|e| e.to_string())?;
348
349 write_file(
351 base_path.join("executor-wasm/Cargo.toml"),
352 format!(
353 r#"
354[package]
355name = "executor-wasm"
356version = "0.1.0"
357edition = "2021"
358
359[lib]
360crate-type = ["cdylib", "rlib"]
361
362[dependencies]
363fyrox = {{workspace = true}}
364{name} = {{ path = "../game" }}"#,
365 ),
366 )?;
367
368 write_file(
370 base_path.join("executor-wasm/src/lib.rs"),
371 format!(
372 r#"//! Executor with your game connected to it as a plugin.
373#![cfg(target_arch = "wasm32")]
374
375use fyrox::engine::executor::Executor;
376use fyrox::event_loop::EventLoop;
377use fyrox::core::wasm_bindgen::{{self, prelude::*}};
378
379use {name}::Game;
380
381#[wasm_bindgen]
382pub fn main() {{
383 let mut executor = Executor::new(Some(EventLoop::new().unwrap()));
384 executor.add_plugin(Game::default());
385 executor.run()
386}}"#,
387 ),
388 )?;
389
390 write_file_binary(
399 base_path.join("executor-wasm/index.html"),
400 include_bytes!("wasm/index.html"),
401 )?;
402 write_file_binary(
403 base_path.join("executor-wasm/styles.css"),
404 include_bytes!("wasm/styles.css"),
405 )?;
406 write_file_binary(
407 base_path.join("executor-wasm/main.js"),
408 include_bytes!("wasm/main.js"),
409 )?;
410 write_file_binary(
411 base_path.join("executor-wasm/README.md"),
412 include_bytes!("wasm/README.md"),
413 )
414}
415
416fn init_editor(base_path: &Path, name: &str) -> Result<(), String> {
417 Command::new("cargo")
418 .args(["init", "--bin", "--vcs", "none"])
419 .arg(base_path.join("editor"))
420 .output()
421 .map_err(|e| e.to_string())?;
422
423 write_file(
425 base_path.join("editor/Cargo.toml"),
426 format!(
427 r#"
428[package]
429name = "editor"
430version = "0.1.0"
431edition = "2021"
432
433[dependencies]
434fyrox = {{ workspace = true }}
435fyroxed_base = {{ workspace = true }}
436{name} = {{ path = "../game", optional = true }}
437
438[features]
439default = ["{name}", "fyroxed_base/default"]
440dylib = ["fyroxed_base/dylib_engine"]
441"#,
442 ),
443 )?;
444
445 write_file(
446 base_path.join("editor/src/main.rs"),
447 format!(
448 r#"//! Editor with your game connected to it as a plugin.
449use fyroxed_base::{{fyrox::event_loop::EventLoop, Editor, StartupData, fyrox::core::log::Log}};
450
451fn main() {{
452 Log::set_file_name("{name}.log");
453
454 let event_loop = EventLoop::new().unwrap();
455 let mut editor = Editor::new(
456 Some(StartupData {{
457 working_directory: Default::default(),
458 scenes: vec!["data/scene.rgs".into()],
459 named_objects: false
460 }}),
461 );
462
463 // Dynamic linking with hot reloading.
464 #[cfg(feature = "dylib")]
465 {{
466 #[cfg(target_os = "windows")]
467 let file_name = "game_dylib.dll";
468 #[cfg(target_os = "linux")]
469 let file_name = "libgame_dylib.so";
470 #[cfg(target_os = "macos")]
471 let file_name = "libgame_dylib.dylib";
472 editor.add_dynamic_plugin(file_name, true, true).unwrap();
473 }}
474
475 // Static linking.
476 #[cfg(not(feature = "dylib"))]
477 {{
478 use {name}::Game;
479 editor.add_game_plugin(Game::default());
480 }}
481
482 editor.run(event_loop)
483}}
484"#,
485 ),
486 )
487}
488
489fn init_game_dylib(base_path: &Path, name: &str) -> Result<(), String> {
490 Command::new("cargo")
491 .args(["init", "--lib", "--vcs", "none"])
492 .arg(base_path.join("game-dylib"))
493 .output()
494 .map_err(|e| e.to_string())?;
495
496 write_file(
498 base_path.join("game-dylib/Cargo.toml"),
499 format!(
500 r#"
501[package]
502name = "game_dylib"
503version = "0.1.0"
504edition = "2021"
505
506[lib]
507crate-type = ["cdylib"]
508
509[dependencies]
510{name} = {{ path = "../game", default-features = false }}
511
512[features]
513default = ["{name}/default"]
514dylib-engine = ["{name}/dylib-engine"]
515"#,
516 ),
517 )?;
518
519 write_file(
521 base_path.join("game-dylib/src/lib.rs"),
522 format!(
523 r#"//! Wrapper for hot-reloadable plugin.
524use {name}::{{fyrox::plugin::Plugin, Game}};
525
526#[no_mangle]
527pub fn fyrox_plugin() -> Box<dyn Plugin> {{
528 Box::new(Game::default())
529}}
530"#,
531 ),
532 )
533}
534
535fn init_android_executor(base_path: &Path, name: &str) -> Result<(), String> {
536 Command::new("cargo")
537 .args(["init", "--lib", "--vcs", "none"])
538 .arg(base_path.join("executor-android"))
539 .output()
540 .map_err(|e| e.to_string())?;
541
542 write_file(
544 base_path.join("executor-android/Cargo.toml"),
545 format!(
546 r#"
547[package]
548name = "executor-android"
549version = "0.1.0"
550edition = "2021"
551
552[package.metadata.android]
553# This folder is used as a temporary storage for assets. Project exporter will clone everything
554# from data folder to this folder and cargo-apk will create the apk with these assets.
555assets = "assets"
556strip = "strip"
557
558[package.metadata.android.sdk]
559min_sdk_version = 26
560target_sdk_version = 30
561max_sdk_version = 29
562
563[package.metadata.android.signing.release]
564path = "release.keystore"
565keystore_password = "fyrox-template"
566
567[lib]
568crate-type = ["cdylib"]
569
570[dependencies]
571fyrox = {{ workspace = true }}
572{name} = {{ path = "../game" }}"#,
573 ),
574 )?;
575
576 write_file(
578 base_path.join("executor-android/src/lib.rs"),
579 format!(
580 r#"//! Android executor with your game connected to it as a plugin.
581#![cfg(target_os = "android")]
582use fyrox::{{
583 core::io, engine::executor::Executor, event_loop::EventLoopBuilder,
584 platform::android::EventLoopBuilderExtAndroid,
585}};
586use {name}::Game;
587
588#[no_mangle]
589fn android_main(app: fyrox::platform::android::activity::AndroidApp) {{
590 io::ANDROID_APP
591 .set(app.clone())
592 .expect("ANDROID_APP cannot be set twice.");
593 #[allow(deprecated)]
594 let event_loop = EventLoopBuilder::new().with_android_app(app).build().unwrap();
595 let mut executor = Executor::from_params(Some(event_loop), Default::default());
596 executor.add_plugin(Game::default());
597 executor.run()
598}}"#,
599 ),
600 )?;
601
602 write_file_binary(
603 base_path.join("executor-android/README.md"),
604 include_bytes!("android/README.md"),
605 )?;
606 write_file_binary(
607 base_path.join("executor-android/release.keystore"),
608 include_bytes!("android/release.keystore"),
609 )?;
610 create_dir_all(base_path.join("executor-android/assets")).map_err(|e| e.to_string())
611}
612
613fn init_workspace(base_path: &Path, vcs: &str) -> Result<(), String> {
614 Command::new("cargo")
615 .args(["init", "--vcs", vcs])
616 .arg(base_path)
617 .output()
618 .map_err(|e| e.to_string())?;
619
620 let src_path = base_path.join("src");
621 if src_path.exists() {
622 remove_dir_all(src_path).map_err(|e| e.to_string())?;
623 }
624
625 write_file(
627 base_path.join("Cargo.toml"),
628 format!(
629 r#"
630[workspace]
631members = ["editor", "executor", "executor-wasm", "executor-android", "export-cli", "game", "game-dylib"]
632resolver = "2"
633
634[workspace.dependencies.fyrox]
635version = "{CURRENT_ENGINE_VERSION}"
636default-features = false
637[workspace.dependencies.fyroxed_base]
638version = "{CURRENT_EDITOR_VERSION}"
639default-features = false
640[workspace.dependencies.fyrox-build-tools]
641version = "{CURRENT_EDITOR_VERSION}"
642
643# Separate build profiles for hot reloading. These profiles ensures that build artifacts for
644# hot reloading will be placed into their own folders and does not interfere with standard (static)
645# linking.
646[profile.dev-hot-reload]
647inherits = "dev"
648[profile.release-hot-reload]
649inherits = "release"
650
651# Optimize the engine in debug builds, but leave project's code non-optimized.
652# By using this technique, you can still debug you code, but engine will be fully
653# optimized and debug builds won't be terribly slow. With this option, you can
654# compile your game in debug mode, which is much faster (at least x3), than release.
655[profile.dev.package."*"]
656opt-level = 3
657"#,
658 ),
659 )?;
660
661 if vcs == "git" {
662 write_file(
664 base_path.join(".gitignore"),
665 r#"
666/target
667*.log
668"#,
669 )?;
670 }
671
672 write_file_binary(
674 base_path.join("flake.nix"),
675 include_bytes!("nixos/flake.nix"),
676 )?;
677
678 Ok(())
679}
680
681fn init_data(base_path: &Path, style: &str) -> Result<(), String> {
682 let data_path = base_path.join("data");
683 create_dir_all(&data_path).map_err(|e| e.to_string())?;
684
685 let scene_path = data_path.join("scene.rgs");
686 match style {
687 "2d" => write_file_binary(scene_path, include_bytes!("2d.rgs")),
688 "3d" => write_file_binary(scene_path, include_bytes!("3d.rgs")),
689 _ => Err(format!("Unknown style: {style}. Use either `2d` or `3d`")),
690 }
691}
692
693pub fn init_script(root_path: &Path, raw_name: &str) -> Result<(), String> {
694 let mut base_path = root_path.join("game/src/");
695 if !base_path.exists() {
696 eprintln!("game/src directory does not exists! Fallback to root directory...");
697 base_path = root_path.to_path_buf();
698 }
699
700 let script_file_stem = raw_name.to_case(Case::Snake);
701 let script_name = raw_name.to_case(Case::UpperCamel);
702 let file_name = base_path.join(script_file_stem.clone() + ".rs");
703
704 if file_name.exists() {
705 return Err(format!("Script {script_name} already exists!"));
706 }
707
708 let script_uuid = Uuid::new_v4().to_string();
709
710 write_file(
711 file_name,
712 format!(
713 r#"
714#[allow(unused_imports)]
715use fyrox::graph::prelude::*;
716use fyrox::{{
717 core::{{visitor::prelude::*, reflect::prelude::*, type_traits::prelude::*}},
718 event::Event, script::{{ScriptContext, ScriptDeinitContext, ScriptTrait}},
719 plugin::error::GameResult
720}};
721
722#[derive(Visit, Reflect, Default, Debug, Clone, TypeUuidProvider, ComponentProvider)]
723#[type_uuid(id = "{script_uuid}")]
724#[visit(optional)]
725pub struct {script_name} {{
726 // Add fields here.
727}}
728
729impl ScriptTrait for {script_name} {{
730 fn on_init(&mut self, context: &mut ScriptContext) -> GameResult {{
731 // Put initialization logic here.
732 Ok(())
733 }}
734
735 fn on_start(&mut self, context: &mut ScriptContext) -> GameResult {{
736 // There should be a logic that depends on other scripts in scene.
737 // It is called right after **all** scripts were initialized.
738 Ok(())
739 }}
740
741 fn on_deinit(&mut self, context: &mut ScriptDeinitContext) -> GameResult {{
742 // Put de-initialization logic here.
743 Ok(())
744 }}
745
746 fn on_os_event(&mut self, event: &Event<()>, context: &mut ScriptContext) -> GameResult {{
747 // Respond to OS events here.
748 Ok(())
749 }}
750
751 fn on_update(&mut self, context: &mut ScriptContext) -> GameResult {{
752 // Put object logic here.
753 Ok(())
754 }}
755}}
756 "#
757 ),
758 )
759}
760
761pub fn init_project(
762 root_path: &Path,
763 name: &str,
764 style: &str,
765 vcs: &str,
766 overwrite: bool,
767) -> Result<(), String> {
768 let name = check_name(name);
769 let name = match name {
770 Ok(s) => s,
771 Err(name_error) => {
772 println!("{name_error}");
773 return Err(name_error.to_string());
774 }
775 };
776
777 let base_path = root_path.join(name);
778 let base_path = &base_path;
779
780 if !overwrite
782 && base_path.exists()
783 && read_dir(base_path)
784 .expect("Failed to check if path is not empty")
785 .next()
786 .is_some()
787 {
788 return Err(format!(
789 "Non-empty folder named {} already exists, provide --overwrite to create the project anyway",
790 base_path.display()
791 ));
792 }
793
794 let name = convert_name(name);
795 init_workspace(base_path, vcs)?;
796 init_data(base_path, style)?;
797 init_game(base_path, &name)?;
798 init_game_dylib(base_path, &name)?;
799 init_editor(base_path, &name)?;
800 init_executor(base_path, &name)?;
801 init_wasm_executor(base_path, &name)?;
802 init_android_executor(base_path, &name)?;
803 init_export_cli(base_path, &name)
804}
805
806pub fn upgrade_project(root_path: &Path, version: &str, local: bool) -> Result<(), String> {
807 let semver_regex = Regex::new(include_str!("regex")).map_err(|e| e.to_string())?;
808
809 if version != "latest" && version != "nightly" && !semver_regex.is_match(version) {
810 return Err(format!(
811 "Invalid version: {version}. Please specify one of the following:\n\
812 \tnightly - uses latest nightly version of the engine from GitHub directly.\
813 \tlatest - uses latest stable version of the engine.\n\
814 \tmajor.minor.patch - uses specific stable version from crates.io (0.30.0 for example).",
815 ));
816 }
817
818 let editor_versions = [
821 (
822 CURRENT_ENGINE_VERSION.to_string(),
823 (
824 CURRENT_EDITOR_VERSION.to_string(),
825 Some(CURRENT_SCRIPTS_VERSION.to_string()),
826 ),
827 ),
828 (
829 "0.34.0".to_string(),
830 ("0.21.0".to_string(), Some("0.3.0".to_string())),
831 ),
832 (
833 "0.33.0".to_string(),
834 ("0.20.0".to_string(), Some("0.2.0".to_string())),
835 ),
836 (
837 "0.32.0".to_string(),
838 ("0.19.0".to_string(), Some("0.1.0".to_string())),
839 ),
840 ("0.31.0".to_string(), ("0.18.0".to_string(), None)),
841 ("0.30.0".to_string(), ("0.17.0".to_string(), None)),
842 ("0.29.0".to_string(), ("0.16.0".to_string(), None)),
843 ("0.28.0".to_string(), ("0.15.0".to_string(), None)),
844 ("0.27.1".to_string(), ("0.14.1".to_string(), None)),
845 ("0.27.0".to_string(), ("0.14.0".to_string(), None)),
846 ("0.26.0".to_string(), ("0.13.0".to_string(), None)),
847 ]
848 .into_iter()
849 .collect::<HashMap<_, _>>();
850
851 let workspace_manifest_path = root_path.join("Cargo.toml");
853 match File::open(&workspace_manifest_path) {
854 Ok(mut file) => {
855 let mut toml = String::new();
856 if file.read_to_string(&mut toml).is_ok() {
857 drop(file);
858
859 if let Ok(mut document) = toml.parse::<DocumentMut>() {
860 if let Some(workspace) =
861 document.get_mut("workspace").and_then(|i| i.as_table_mut())
862 {
863 if let Some(dependencies) = workspace
864 .get_mut("dependencies")
865 .and_then(|i| i.as_table_mut())
866 {
867 if version == "latest" {
868 if local {
869 let mut engine_table = table();
870 engine_table["path"] = value("../Fyrox/fyrox");
871 dependencies["fyrox"] = engine_table;
872
873 let mut editor_table = table();
874 editor_table["path"] = value("../Fyrox/editor");
875 dependencies["fyroxed_base"] = editor_table;
876
877 if dependencies.contains_key("fyrox_scripts") {
878 let mut scripts_table = table();
879 scripts_table["path"] = value("../Fyrox/fyrox-scripts");
880 dependencies["fyrox_scripts"] = scripts_table;
881 }
882
883 if dependencies.contains_key("fyrox-build-tools") {
884 let mut scripts_table = table();
885 scripts_table["path"] = value("../Fyrox/fyrox-build-tools");
886 dependencies["fyrox-build-tools"] = scripts_table;
887 }
888 } else {
889 dependencies["fyrox"] = value(CURRENT_ENGINE_VERSION);
890 dependencies["fyroxed_base"] = value(CURRENT_EDITOR_VERSION);
891 if dependencies.contains_key("fyrox_scripts") {
892 dependencies["fyrox_scripts"] =
893 value(CURRENT_SCRIPTS_VERSION);
894 }
895 if dependencies.contains_key("fyrox-build-tools") {
896 dependencies["fyrox-build-tools"] =
897 value(CURRENT_ENGINE_VERSION);
898 }
899 }
900 } else if version == "nightly" {
901 let mut table = table();
902 table["git"] = value("https://github.com/FyroxEngine/Fyrox");
903
904 dependencies["fyrox"] = table.clone();
905 dependencies["fyroxed_base"] = table.clone();
906 dependencies["fyrox-build-tools"] = table.clone();
907 } else {
908 dependencies["fyrox"] = value(version);
909 if let Some((editor_version, scripts_version)) =
910 editor_versions.get(version)
911 {
912 dependencies["fyroxed_base"] = value(editor_version);
913 if let Some(scripts_version) = scripts_version {
914 if dependencies.contains_key("fyrox_scripts") {
915 dependencies["fyrox_scripts"] = value(scripts_version);
916 }
917 }
918 if dependencies.contains_key("fyrox-build-tools") {
919 dependencies["fyrox-build-tools"] = value(version);
920 }
921 } else {
922 println!("WARNING: matching editor/scripts version not found!");
923 }
924 }
925 }
926 }
927
928 let mut file =
929 File::create(&workspace_manifest_path).map_err(|e| e.to_string())?;
930 file.write_all(document.to_string().as_bytes())
931 .map_err(|e| e.to_string())?;
932 }
933 }
934 }
935 Err(err) => {
936 return Err(err.to_string());
937 }
938 }
939
940 Command::new("cargo")
941 .arg("update")
942 .arg("--manifest-path")
943 .arg(workspace_manifest_path)
944 .output()
945 .map_err(|e| e.to_string())?;
946
947 Ok(())
948}