Skip to main content

fyrox_template_core/
lib.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Fyrox Project Template Generator.
22
23use 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 Cargo.toml
142    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 lib.rs
162    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 Cargo.toml
233    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 main.rs
254    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 Cargo.toml
300    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 main.rs
318    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 Cargo.toml
350    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 lib.rs
369    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 "entry" point stuff. This includes:
391    //
392    // - Index page with a "Start" button. The button is needed to solve sound issues in some browsers.
393    //   Some browsers (mostly Chrome) prevent sound from playing until user click on something on the
394    //   game page.
395    // - Entry JavaScript code - basically a web launcher for your game.
396    // - Styles - to make "Start" button to look decent.
397    // - A readme file with build instructions.
398    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 Cargo.toml
424    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 Cargo.toml
497    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 lib.rs
520    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 Cargo.toml
543    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 main.rs
577    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 Cargo.toml
626    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 .gitignore
663        write_file(
664            base_path.join(".gitignore"),
665            r#"
666/target
667*.log
668"#,
669        )?;
670    }
671
672    // Write flake.nix for nixOS
673    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    // Check the path is empty / doesn't already exist (To prevent overriding)
781    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    // Engine -> (Editor, Scripts) version mapping.
819    // TODO: This will be obsolete in 1.0 and should be removed.
820    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    // Open workspace manifest.
852    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}