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:
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!");
}
}