gif gif

The interesting corner

gif gif

Creating a Minecraft datapack to list coordinates, and storing them in a MySQL database using Rust

I am running a Minecraft server for me and my friends, and we use coordinates (pressing F3) to store coordinates of different interesting locations. There are some datapacks in the server, and one that we use the locations for a lot is the Dungeons Now Loading... datapack. I decided to make my own datapack to store the coordinates, and show them to players when they want to.

I first had the idea to display the coordinates using a scoreboard that was assigned to a team, and players could enable the scoreboard by using a command that added or removed them to and from that team. However, the DNL datapack prevented this, as it made players automatically leave any teams that were not used by the datapack. Kind of shitty and lazy programming, but oh well ¯\_(ツ)_/¯. I then decided to just make a function that shows a text message with all the names of the locations and enables the player to click them to get teleported to the location.

The datapack

To make a datapack called ic-coords, the folder structure has to be very specific. The structure is as follows:

world folder\
\
 datapacks\
 \ 
  ic-coords\
  \ 
   |data\
   \
    ic-coords\
   |\
   | functions\
   | |load.mcfunction
   | |list_coords.mcfunction
   \
    minecraft\ 
    \
     tags\
     \
      functiions\
      |load.json
                    

The datapack itself is quite simple. It only has a load and a list-coords function. The load function only displays a message that players can execute the function to show the locations. The list-coords function, obviously, shows the coordinates. The load function:

load.mcfunction

tellraw @a {"text":"Coordinations datapack loaded! (= ФェФ=)","color":"blue","bold":"true"}
tellraw @a {"text":"enter '/function ic-coords:list_coords' to show coordinates","color":"dark_aqua"}
                    

To make sure the load message gets shown when the datapack loads, you have to enable it. This is done in a file called load.json.

load.json

{
	"values": [
	"ic-coords:load"
	]
}
                    

The actual file to list the coordinates will be generated by a Rust program. I chose Rust because I wanted to learn it, and normally I would do things like this in Python. I made a new year's resolution to write all the programs that I would normally write in Python in Rust insted. The actual locations will be stored in a MySQL database.

The datbase

The locations will be stored in a table called minecraft_coordinates in a MySQL database. The table will look like this:

Field Type Null Key Default Extra
id int No PRI NULL auto_increment
name varchar(255) No NULL
x int No NULL
y int No NULL
z int No NULL
description varchar(255) No NULL
color enum Yes white
nether tinyint(1) Yes 0

The color enum holds all possible minecraft color values. The CREATE TABLE statement is as follows:

CREATE TABLE `minecraft_coordinates` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `x` int NOT NULL,
  `y` int NOT NULL,
  `z` int NOT NULL,
  `description` varchar(255) NOT NULL,
  `color` enum('dark_red','red','black','blue','aqua','dark_aqua','green','gold','dark_purple','light_purple','yellow','dark_green','gray','dark_gray','white','dark_blue') DEFAULT 'white',
  `nether` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`)
)

The Rust file can be found here. It can add a new coordinate to the database and update the mcfunction file. You can use the -a flag to add a coordinate and adding the arguments name x y z description color nether. With the -u flag you can only update the mcfunction file. When the datapack loads and you execute the function the result is as follows:

datapack screenshot

You can see that the different locations are visible and that you get a description when you hover over the location. When you click on it, you get teleported to that location.

The Rust code is also visible below:

use std::env;
use std::process;
use std::io::Write;

use mysql::*;
use mysql::prelude::*;

const MC_COLORS: [&'static str; 16] = ["black","dark_blue","dark_green","dark_aqua","dark_red","dark_purple","gold","gray","dark_gray","blue","green","aqua","red","light_purple","yellow","white"];

const URL: &'static str = "mysql://username:password@localhost:3306/db_name"; 

trait McPrintable {
    fn mc_print(&self) -> String;
}

/// A Minecraft coordinate with a name, x, y, z, and description.
#[derive(Debug)]
struct Coordinate {
    /// The name of the coordinate.
    name: String,
    /// The x coordinate.
    x: i32,
    /// The y coordinate.
    y: i32,
    /// The z coordinate.
    z: i32,
    /// The description of the coordinate. This gets displayed when hovering over it.
    description: String,
    /// Color of the coordinate that gets displayed
    color: String,
    /// wether the location is in the nether or not
    nether: bool
}
// example entry: {"text":"The Tower","color":"gold","bold":"true","clickEvent":{"action":"run_command","value":"/tp @s 2707 65 432"},"hoverEvent":{"action":"show_text","value":[{"text":"Teleport to the Tower base "},{"text":"[","color":"blue"},{"text":"2707 65 432","color":"gray"},{"text":"]","color":"blue"}]}}

impl McPrintable for Coordinate {
    fn mc_print(&self) -> String{
        let subcolor = if self.nether {"red"} else {"blue"};
        //TODO add all values
        format!("tellraw @s {{\"text\":\"{}\",\"color\":\"{}\",\"bold\":\"true\",\"clickEvent\":{{\"action\":\"run_command\",\"value\":\"/tp @s {} {} {}\"}},\"hoverEvent\":{{\"action\":\"show_text\",\"value\":[{{\"text\":\"{} \"}},{{\"text\":\"[\",\"color\":\"{}\"}},{{\"text\":\"{} {} {}\",\"color\":\"gray\"}},{{\"text\":\"]\",\"color\":\"{}\"}}]}}}}\n",self.name,self.color,self.x,self.y,self.z,self.description,subcolor,self.x,self.y,self.z,subcolor)
    }
}

/// Parses the input arguments into a coordinate.
/// # Arguments
/// * `args` - The arguments to parse.
/// # Returns
/// * `Ok(Coordinate)` - The coordinate parsed from the arguments.
/// * `Err(&'static str)` - Error message if something went wrong.
fn parse_input_arguments(mut args: Vec<String>) -> core::result::Result<Coordinate,&'static str> {
    println!("args: {:?}", args);
    if args.len() < 5 || args.len() > 7 {
        Err("5 or 6 input parameters must be given!")
    } else {
        let mut coordinate = Coordinate {
            name: args.remove(0),
            x: 0,
            y: 0,
            z: 0,
            description: args.remove(3),
            color: "white".to_owned(),
            nether: false
        };
        for i in 0..3 {
            match args.remove(0).parse::<i32>() {
                Ok(n) => {
                    match i {
                        0 => coordinate.x = n,
                        1 => coordinate.y = n,
                        2 => coordinate.z = n,
                        _ => println!{"The fuck?"}
                    }
                },
                Err(_) => return Err("Error parsing input coordinates, must be integers!")
            }
        }
        if args.len() > 0 {
            let c: &str = &args.remove(0)[..]; // take full slice of the string, because it is a String object and we want a &'static str.
            if !MC_COLORS.contains(&c) {
                return Err("Wrong color value!");
            } else {
                coordinate.color = c.to_owned();
            }
            if args.len() > 0 {
                match args.remove(0).parse::<i32>() {
                    Ok(a) => {
                        match a {
                            1 => coordinate.nether = true,
                            _ => coordinate.nether = false
                        }
                    },
                    Err(_) => return Err("Error parsing input coordinates, wrong nether value!")
                }
            }
        }
        Ok(coordinate)
    }
}

fn add_and_update(args: Vec<String>, mut conn: PooledConn) -> core::result::Result<i32,&'static str> {
    let mut coord = Coordinate {
        name: String::from(""),
        x: 0,
        y: 0,
        z: 0,
        description: String::from(""),
        color: "white".to_owned(),
        nether: false
    };

    match parse_input_arguments(args) {
        Ok(coordinate) => coord = coordinate,
        Err(e) => return Err(e)
    }
    println!("successfully parsed coordinate: {:?}",coord);
    conn.exec_drop(r"INSERT INTO minecraft_coordinates (name,x,y,z,description,color,nether) values (:name, :x, :y ,:z, :desc, :color, :nether)",
        params! {
            "name" => coord.name,
            "x" => coord.x,
            "y" => coord.y,
            "z" => coord.z,
            "desc" => coord.description,
            "color" => coord.color,
            "nether" => coord.nether
        },
    ).unwrap();

    println!("Successfully added coordinate!");
    update_mcfunction(conn)

}

fn update_mcfunction(mut conn:PooledConn) -> core::result::Result<i32,&'static str> {
    let mut all_coords = Vec::new();
    match conn.exec_map(
        "select name, x, y, z, description, color, nether from minecraft_coordinates where id > :zero", 
        params!{"zero" => 0},
        |(name, x, y, z, desc, color, nether)| Coordinate {
            name: name,
            x: x,
            y: y,
            z: z,
            description: desc,
            color: color,
            nether: nether
        }) {
        Ok(n) => all_coords = n,
        Err(e) => return Err("SQL error")
    }
    
    println!("all coords: {:?}",all_coords);

    let mut file = std::fs::OpenOptions::new().write(true).truncate(true).create(true).open("/srv/minecraft-server/minecraft-1.19-server/world/datapacks/ic-coords/data/ic-coords/functions/list_coords.mcfunction").expect("Could not open file!");
    // tellraw @s [{"text":"-----------","color":"light_purple"},{"text":"Coordinates","color":"dark_aqua","italic":"true"},{"text":"-----------","color":"light_purple"}]

    file.write_all(b"tellraw @s [{\"text\":\"-----------\",\"color\":\"light_purple\"},{\"text\":\"Coordinates\",\"color\":\"dark_aqua\",\"italic\":\"true\"},{\"text\":\"-----------\",\"color\":\"light_purple\"}]\n").expect("Error writing head of file!");

    for cor in all_coords {
        println!("Writing coordinate...");
        println!("{}",cor.mc_print());

        file.write_all(cor.mc_print().as_bytes()).expect("Could not write to file!");

    }

    Ok(69)
}




//TODO add entries in minecraft start script to run program that retrieves entries and writes to mcfunction file


fn main() {

    let mut args: Vec<String> = env::args().collect();
    args.remove(0); // remove the first argument (the program name)
    println!("connecting to db...");
    let pool = Pool::new(URL).unwrap();
    let mut conn = pool.get_conn().unwrap();

    let operation = &args.remove(0)[..]; // get operation argument
    if operation.starts_with("-") {
        match operation {
            "-a" => {
                match add_and_update(args,conn) {
                    Ok(_) => println!("success!"),
                    Err(e) => println!("Error! {}",e)
                }
            },
            "-u" => {
                match update_mcfunction(conn) {
                    Ok(_) => println!("success!"),
                    Err(e) => println!("Error! {}",e)
                }

            },
            _ => {
                println!("No valid value");
                process::exit(-1);
            }
        }
    } else {
        println!("Error! wrong operation!");
    }

}