Devlog #1

· 8min · tyfee
cover

First devlog for the Novelty Engine project. I'd like to briefly talk on this postabout:

  1. What i aim for with this project;
  2. How i am approaching it;
  3. What parts have i been struggling with
  4. How i want to proceed with the development.
  5. What are my long-term goals.

What am i trying to do?

Novelty is a personal project of mine, which i am building and seeing how far i can get it for the sake of having fun and learning some new coding perspectives.

That's exactly why i picked Rust and Svelte to be the main tools in the engine, because they are two very exciting concepts that i've been meaning to try for the longest time, a thing that can also be said about the Tauri and Macroquad projects.

My main goal is to allow people to write visual novels quickly, without the NEED to learn new stacks or libraries or even coding at all, and focus only on the visual and the novel part of the game.

first rendition of the novelty studio

What have i done this far, and how have i done it?

Up until this part, the novelty engine is nothing more than something that exists in my head. I've built a skeleton for what the project is supposed to be but i haven't gotten any further than that yet.

So what can it do at this point?

  1. Run on Windows systems.

In the future, i want to Port the studio to linux based operating systems, specially Ubuntu and Debian and also port it to MacOS. Because i chose a flexible language like Rust, i don't imagine i will suffer more than the necessary to make it cross-platform. As of right now it only works on windows, since it is the OS running on the machine i use to develop, cause i simply prefer to work with Rust in windows.

  1. Create new projects.

Novelty can create new projects, thanks to Rust and Tauri. When a new project is created on the Svelte frontend, tauri calls the rust method, which initializes a folder with the game name, the cargo.toml and main.rs files, a project-specific file called 'project.nvl', and the folders that will receive the data as the user builds their game.

The method takes the 'path' and 'name' String parameters, and uses tokio::fs to create all the necessary files and directories:

#[tauri::command]
async fn create_project(path: String, name: String) -> Result<(), String> {
    let project_path = Path::new(&path).join(&name);

    tokio_fs::create_dir_all(&project_path)
        .await
        .map_err(|e| format!("Failed to create directory: {}", e))?;
    println!("Created project directory at: {:?}", project_path);
    let file_path = project_path.join("project.nvl");
    let file_content: String = format!("{{ name: \"{}\" }}", name);
    let cargo_toml = project_path.join("cargo.toml");
    let cargo_content = format!(
        "[package] \nname = \"{}\" \nversion = \"0.1.0\" \nedition = \"2021\" \n\n[dependencies] \nmacroquad = \"0.4\"",
        name
    ); let main_rs: PathBuf = project_path.join("src").join("main.rs");
    let default_code = format!(
        "use macroquad::prelude::*; \n\n#[macroquad::main(\"{}\")] \nasync fn main() {{ \nloop {{ \nclear_background(LIGHTGRAY); \nnext_frame().await \n}} \n}}",
        name 
    );

The javascript call is made asynchronously like the following:

 async function create_project(project_name: any){
    const path = "C:\\Novelty\\Projects";

  try {
    await invoke("create_project", { path, name: project_name });
    console.log("Project created successfully");
    navigate(project_name)
    creating_new_project = false;
  } 
  catch (error) {
    console.error("Error creating project:", error);
    }
}


  1. Open existing projects.

The project opening method is not fully built nor is it the most sophisticated yet. Up until this point all it does is receive the project name and then apply all saves to it's path.

In the future, opening a project will not only take the project name and it's path, but also all the file contents of 'project.nvl'.

As it can't do any of this right now, the studio doesn't have a save system for the assets, meaning that they reset everytime you open them. Setting this whole thing up is a top priority right now.

  1. Add and delete simple assets such as: scenes, dialog nodes and text.

The svelte studio holds all it's data in reactive arrays, i find it easier to work with persistent state in Svelte than React (That i am very used to) for some reason, so i'm glad i'm working with svelte in this.

Variables such as:

let text: Array<any> = [];

let audio = [{
    title: "love",
    file: "love.wav"
}]
let characters = [
    {name: "me", id: "0000"}, 
    {name: "Dude", id: "0001", poses: [neutral00]}, 
    {name: "Guy", id: "0001", poses: [neutral00]}
]

let script_code = [{
    scene: "city",
   type: "dialogue",
   character: "0000",
   text: "Hello.",
   options: [''],
   x: 0,
   y: 20,
  },
{
    scene: "city",
type: "dialogue",
 character: "0000",
 text: "What would you like to eat?",
 options: [''],
 x: 3,
 y: 20,
},
]


let scene_nodes = [script_code]

store all the game data, which then gets easily turned into delight code, when calling:

transpileToScript(project_name, scenes, characters, assets, scene_nodes, main_menu_info, text, audio);

The returned source code gets then transformed again, this time into Rust code, calling:

transpileToRust(delight_code);

So all it takes to add or delete any assets or game specific data, is altering the reactive array variables.

  1. Compile the game into a windows executable and run.

Thanks again to Tauri, calling the 'cargo run' command to compile and run the game is an extremely easy task.

This simple method 'call', takes the 'path', 'commands' and 'args' parameters. Then of course, calls the 'command', in this case cargo, with the 'args' run, in the 'path'.

#[tauri::command]
async fn call(path: PathBuf, command: String, args: Vec<String>) -> Result<String, String> {
 
    let result = Command::new(&command)
        .args(&args)
        .current_dir(&path)
        .output()
        .map_err(|e| format!("Failed to run command: {}", e))?;

    if !result.status.success() {
        return Err(format!(
            "Command failed with exit code {}: {}",
            result.status.code().unwrap_or(-1),
            String::from_utf8_lossy(&result.stderr)
        ));
    }

    Ok(String::from_utf8_lossy(&result.stdout).to_string())
}

The method is called from the svelte frontend using the build_and_run.js file:

import { invoke } from "@tauri-apps/api/tauri";
import updateMain from "./update_main";
import transpileToRust from "./rustTranspiler";

export default async function buildAndRun(name, rust_code){

    updateMain(name ,rust_code);
    const projectPath = `C:\\Novelty\\Projects\\${name}`;
    const command = 'cargo';
    const args = ['run'];
    
    const result = await invoke('call', {
      path: projectPath,
      command: command,
      args: args,
    });
    console.log(rust_code)
}

Which executes when the user requests the 'build and run' tool:

Calling build and run svelte

What am i currently working on?

In the moment, my main priorities to get it to a basic and functional state that are currently in development are:

  • Improving the deLIGHT syntax and also symplify the transpiling processes;

I am not happy with how the deLIGHT syntax looks like as of right now in terms of readability, i stated to myself at some point that i want it to look more or less like a movie script, which it is far from right now. I feel like it will be a constant work, as i don't think the results will ever be as equally functional and readable. The transpiling methods i've written also work with some crazy deep-nesting stuff, which will absolutely lead to errors when a user is writing their own code, so that's a long way as well.

  • Develop the character system;

For the character system, all i know is that each character will be an array object that takes, name, id and poses attributes. Meaning that each character will be able to hold more than one asset to represent it's speech. Something like:

{
id: 0000,
name: Dude,
poses: ['neutral.png', 'angry.png', 'sad.png']
}

I'm currently working on the transpiling process to turn these objects into a DrawTextureEx macroquad method, but i haven't reached a standard object structure yet. It is very probably the next thing i'll get fully done though.

  • File importing;

For some reason, i'm having some trouble using Rust to copy files from one directory to the project assets folder, meaning that it is impossible to add new files to a project right now. That, together with the character creation methods are top priority right now, cause after all, you can't build a visual novel without the visuals.

At this point, i'm using some default assets from Freepik to figure out how everything is going to work.

  • Audio;

Novelty will use macroquad to play audio in the forms of sfx and bgm, i have sketched how it is going to work when transpiled to rust, but i haven't really decided how much freedom i want to give to the user without it being too much effort on my side.

My idea as of right now is allow one BGM to each scene, and also allow the user to use an audio as dialog nodes to serve as SFX, with a limit per scene.

Final Considerations

As this is the first ever post on this blog, i'd like to keep it quick and simple. I'd like to update this weekly, just for myself, to track my development process and see what seems to be the hardest and easiest things to implement, but also to whoever feels insterested in the project if i eventually reach a solid version and unprivate the github repo. Bye.

PAY ME EPITATH RECORDS!!!!!!

Tyfee.