Skip to main content

mc_classic_js/
lib.rs

1mod random_level_worker;
2mod random;
3
4use fancy_regex::{Regex, SubCaptureMatches};
5
6use rusqlite::{Connection, Result};
7
8use serde::{Deserialize, Serialize};
9use serde_json;
10
11use snap;
12use snap::raw::{Decoder, Encoder};
13
14use core::time;
15use std::collections::HashMap;
16use std::fs::{self, create_dir, Metadata};
17use std::time::SystemTime;
18
19/**
20 * Data struct stores the savedGame and settings of the world
21 */
22#[derive(Serialize, Deserialize, Debug)]
23pub struct Data {
24    pub js_level: JSLevel,
25    pub settings: Settings
26}
27
28impl Data {
29    pub fn new (js_level: JSLevel, settings: Settings) -> Self {
30        Data {js_level, settings}
31    }
32}
33
34
35/**
36 * JSLevel struct stores the object format of a
37 * classic js level of type:
38 * {"worldSeed":0,"changedBlocks":{},"worldSize":128,"version":1}
39 * References the ChangedBlocks struct
40 * worldSeed: This is the seed of the world
41 * changedBlocks: This is an array of all changedBlocks in the world
42 * worldSize: This is the width/length of the world, must be 128, 256, or 512
43 * version: Yeah, I have no clue what this is, but it's seemingly always 1 so...
44 */
45#[derive(Serialize, Deserialize, Debug)]
46pub struct JSLevel {
47    pub worldSeed: i64,
48    pub changedBlocks: HashMap<String,ChangedBlocks>,
49    pub worldSize: i32,
50    pub version: u8
51}
52
53impl JSLevel {
54    pub fn new (worldSeed: i64, changedBlocks: HashMap<String,ChangedBlocks>, worldSize: i32, version: u8) -> Self {
55        JSLevel { worldSeed, changedBlocks, worldSize, version } 
56    }
57
58    pub fn default () -> Self {
59        JSLevel { worldSeed: 1, changedBlocks: HashMap::new(), worldSize: 256, version: 1 }
60    }
61}
62
63/**
64 * ChangedBlocks struct stores the json object of type:
65 * p0_0_0: {a: 0, bt: 0}
66 * This object is used inside the savedGame object to keep track
67 * of each changed block in the world:
68 * p0_0_0: position of block in world, where px_y_z
69 * a: 0 if block does match natural generation / 1 if block does not match natural generation
70 * bt: type of block
71 */
72#[derive(Serialize, Deserialize, Debug)]
73pub struct ChangedBlocks {pub a: u8, pub bt: u8}
74impl ChangedBlocks { pub fn new (a: u8, bt: u8) -> Self {ChangedBlocks { a, bt }}}
75
76/**
77 * Settings struct stores the json object containing all settings for javascript worlds
78 * These settings include typical control and sound settings, but they also contain the username
79 */
80#[derive(Serialize, Deserialize, Debug)]
81pub struct Settings {
82    pub music: bool,
83    pub sound: bool,
84    pub invert: bool,
85    pub fps: bool,
86    pub drawDistance: i32,
87    pub forward: String,
88    pub left: String,
89    pub backward: String,
90    pub right: String,
91    pub jump: String,
92    pub build: String,
93    pub chat: String,
94    pub fog: String,
95    pub saveLoc: String,
96    pub loadLoc: String,
97    pub username: String
98}
99
100impl Settings {
101    pub fn new(
102        music: bool,
103        sound: bool,
104        invert: bool,
105        fps: bool,
106        drawDistance: i32,
107        forward: String,
108        left: String,
109        backward: String,
110        right: String,
111        jump: String,
112        build: String,
113        chat: String,
114        fog: String,
115        saveLoc: String,
116        loadLoc: String,
117        username: String
118    ) -> Self {
119        Settings { music, sound, invert, fps, drawDistance, forward, left, backward, right, jump, build, chat, fog, saveLoc, loadLoc, username }
120    }
121
122    pub fn default () -> Self {
123        Settings {
124            music: false,
125            sound: true,
126            invert: false,
127            fps: false,
128            drawDistance: 0,
129            forward: String::from("W"),
130            left: String::from("A"),
131            backward: String::from("S"),
132            right: String::from("D"),
133            jump: String::from("<space>"),
134            build: String::from("B"),
135            chat: String::from("T"),
136            fog: String::from("F"),
137            saveLoc: String::from("<enter>"),
138            loadLoc: String::from("R"),
139            username: String::from("noname")
140        }
141    }
142}
143
144/**
145 * LocalStorage struct stores input from localStorage db files
146 * key: "savedGame"
147 * utf16_length: Length of uncompressed value
148 * conversion_type: 1
149 * compression_type: 1
150 * value: The actual savedGame, so the actual world
151 */
152pub struct LocalStorage {
153    key: String,
154    utf16_length: i32,
155    conversion_type: i32,
156    compression_type: i32,
157    last_access_time: i32,
158    value: Vec<u8>
159}
160
161/**
162 * Converts a json string in the savedGame format into
163 * a JSLevel struct
164 */
165pub fn deserialize_saved_game (json_string: String) -> JSLevel {
166    let level: JSLevel = serde_json::from_str(&json_string).unwrap();
167    return level;
168}
169
170/**
171 * Converts a json string in the settings format into
172 * a Settings struct
173 */
174pub fn deserialize_settings (json_string: String) -> Settings {
175    let settings: Settings = serde_json::from_str(&json_string).unwrap();
176    return settings;
177}
178
179/**
180 * Converts a savedGame json string and a settings json string
181 * into a Data struct
182 */
183pub fn deserialize_data (json_string1: String, json_string2: String) -> Data {
184    let level: JSLevel = serde_json::from_str(&json_string1).unwrap();
185    let settings: Settings = serde_json::from_str(&json_string2).unwrap();
186    return Data { js_level: level, settings: settings}
187}
188
189/**
190 * Following function accepts a level in the JS form, a tile_map, and optimization and
191 * writes it into the classic javascript object format
192 */
193pub fn serialize_saved_game (level: JSLevel, tile_map: Vec<u8>, opt: u8) -> String {
194
195    //Assigning x, y, and z of world
196    let x: i32 = level.worldSize;
197    let y: i32 = 64;
198    let z: i32 = level.worldSize;
199    let tile_map1 = get_tile_map(level.worldSize, level.worldSeed);
200
201    let mut output: String = String::from("{"); //Opening json object
202
203    output += &format!(r#""worldSeed":{},"#,level.worldSeed.to_string()); //Adding seed key value pair
204
205    //Adding changed blocks key value pair
206    output += r#""changedBlocks":"#; //Adding blocks key
207    output += "{"; //Opening block values object
208
209    //Variables for the tiles and a value
210    let mut t: u8;
211    let mut t1: u8;
212    let mut a: u8; //a = 0 if changed block matches generation, a = 1 if changed block does not match generation
213
214    //Iterating through all blocks
215    //Tilemaps are stored in X,Z,Y format, where [0] is X:0, Y:0, Z:0 & [1] is X:1, Y:0, Z:0 etc.
216    let mut flag: bool = false;
217    for i in 0..y {
218        for j in 0..z {
219            for k in 0..x {
220
221                /* Following code block will be more useful once a changed blocks hashmap is implemented */
222
223                //Setting tile for changed block and checking whether it matches tile generated by seed
224                let mut flag1 = false;
225                let key: String = String::from(format!(r#"p{}_{}_{}"#,k,i,j));
226                //Grabbing the block directly from level
227                let bt: u8 = level.changedBlocks.get(&key).unwrap_or(&ChangedBlocks::new(1,255)).bt;
228                //Grabbing block from passed in tile map
229                t = tile_map[((i*z*x) + (j*x) + k) as usize];
230                //Grabbing the block generated from world
231                t1 = tile_map1[((i*z*x) + (j*x) + k) as usize];
232                if bt != 255 { t = bt }
233                if t == t1 { a = 0 } else { a = 1 } //a = 0 if changed block matches generation, a = 1 if changed block does not match generation
234
235                //If opt == 2 the tile must differ from natural generation to write to array
236                //If opt == 1 either the tile differs from natural generation or it is already considered a changed block to write to array
237                //If opt == 0 tile is written to array
238                //Default value should be 1 or 2, opt 0 is storage intensive and causes unnecessary lag
239                if (opt == 2 && a == 1) || (opt == 1 && (bt != 255 || a == 1)) || opt == 0 { flag1 = true }
240                
241                if flag1 {
242                    //Creating key for changed block
243                    output += &key;
244
245                    //Creating value for changed block
246                    output += "{";
247                    output += &format!(r#""a":{},"bt":{}"#,a,t);
248                    output += "},";
249
250                    flag = true;
251                }
252
253            }
254        }
255    }
256
257    if flag {output.pop();} //Removing extra comma
258    output += "},"; //Closing Changed Blocks object
259
260    output += &format!{r#""worldSize":{},"#,level.worldSize}; //Adding world size key value pair
261    output += &format!{r#""version":{}"#,level.version}; //Adding version key value pair
262
263    output += "}"; //Closing json object
264    return output;
265
266}
267
268/**
269 * Following function accepts a settings object and returns 
270 * a serialized json string
271 */
272pub fn serialize_settings (settings: Settings) -> String {
273    let mut output: String = String::from("{"); //Opening json object
274    output += &format!{r#""music":{},"#,settings.music};
275    output += &format!{r#""sound":{},"#,settings.sound};
276    output += &format!{r#""invert":{},"#,settings.invert};
277    output += &format!{r#""fps":{},"#,settings.fps};
278    output += &format!{r#""drawDistance":{},"#,settings.drawDistance};
279    output += &format!{r#""forward":"{}","#,settings.forward};
280    output += &format!{r#""left":"{}","#,settings.left};
281    output += &format!{r#""backward":"{}","#,settings.backward};
282    output += &format!{r#""right":"{}","#,settings.right};
283    output += &format!{r#""jump":"{}","#,settings.jump};
284    output += &format!{r#""build":"{}","#,settings.build};
285    output += &format!{r#""chat":"{}","#,settings.chat};
286    output += &format!{r#""fog":"{}","#,settings.fog};
287    output += &format!{r#""saveLoc":"{}","#,settings.saveLoc};
288    output += &format!{r#""loadLoc":"{}","#,settings.loadLoc};
289    output += &format!{r#""username":"{}""#,settings.username};
290    output += "}"; //Closing json object
291    return output;
292}
293
294/**
295 * Follwing function accepts a Data struct and returns two serialized json
296 * strings
297 */
298pub fn serialize_data (data: Data) -> [String; 2] {
299    let tile_map = get_tile_map(data.js_level.worldSize, data.js_level.worldSeed);
300    let level_str: String = serialize_saved_game(data.js_level, tile_map, 1);
301    let settings_str: String = serialize_settings(data.settings);
302    return [level_str, settings_str]
303}
304
305/**
306 * Following function opens an sqlite database at the provided path,
307 * then retreives the specified object, and then decompresses it 
308 * before returning it
309 */
310pub fn read_from_db (file_path: String, object: &str) -> Result<String> {
311
312    let conn: Connection = Connection::open(file_path)?;
313
314    let mut stmt = conn.prepare(
315        "SELECT * FROM data where key=?1;"
316    )?;
317
318    //Iterating through the database
319    let entries = stmt.query_map([object], |row| Ok(
320        LocalStorage {
321            key: row.get(0)?,
322            utf16_length: row.get(1)?,
323            conversion_type: row.get(2)?,
324            compression_type: row.get(3)?,
325            last_access_time: row.get(4)?,
326            value: row.get(5)?,
327        }
328    ))?;
329
330    //Retreiving the compressed save game object and length
331    let mut compressed_object: Vec<u8> = Vec::new();
332    let mut decompressed_length: i32 = 0;
333    for entry in entries {
334        let local: LocalStorage = entry.unwrap();
335        if local.key == object {
336            compressed_object = local.value;
337            decompressed_length = local.utf16_length;
338            break;
339        }
340    }
341
342    //Creating an array with the correct length for storing the decompressed bytes
343    let mut decompressed: Vec<u8> = Vec::new();
344    for _ in 0..decompressed_length {
345        decompressed.push(0);
346    }
347
348    //Decompressing using snappy compression
349    Decoder::decompress(&mut Decoder::new(), &compressed_object, &mut decompressed).unwrap();
350
351    //Converting the character codes to characters
352    let mut characters: Vec<char> = Vec::new();
353    for ch in decompressed {
354        characters.push(ch as char)
355    }
356
357    //Returning the characters as a string
358    Ok(characters.iter().collect())
359
360}
361
362/**
363 * Following function opens an sqlite database at the provided path,
364 * then retreives the specified object, and then decompresses it 
365 * before returning it
366 */
367pub fn read_saved_game (file_path: String) -> Result<String> {
368    return read_from_db(file_path, "savedGame");
369}
370
371/**
372 * Following function opens an sqlite database at the provided path,
373 * then retreives the specified object, and then decompresses it 
374 * before returning it
375 */
376pub fn read_settings (file_path: String) -> Result<String> {
377    return read_from_db(file_path, "settings");
378}
379
380/**
381 * Following function accepts a path to a db file, and a 
382 * json string. The json string is parsed as the value and
383 * compressed using snappy compression, and is then passed
384 * to the db and saved. Note this only applies to Firefox,
385 * as firefox is the only browser that I know of that uses
386 * this structure. Chromium support in the future...
387 */
388pub fn write_data (file_path: String, json_strings: [String; 2], website: String) -> Result<()> {
389
390    let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_micros() as u64;
391
392    //Creating directories
393    let regex = Regex::new(r#"/|:|\*|\?|"|>|<|\||\\"#).unwrap();
394    let substitution = "+";
395    let dir_name = regex.replace_all(&website, substitution);
396
397    create_dir(file_path.clone() + "/" + &dir_name);
398    create_dir(file_path.clone() + "/" + &dir_name + "/ls");
399
400    //Building metadata file
401    let mut metadata: Vec<u8> = Vec::new();
402    metadata.extend_from_slice(&timestamp.to_be_bytes()); //Timestamp
403    metadata.push(0); //Persisted
404    metadata.extend_from_slice(&(0 as i32).to_be_bytes()); //Suffix
405    metadata.extend_from_slice(&(0 as i32).to_be_bytes()); //Group
406
407    //Origin
408    metadata.extend_from_slice(&(website.len() as u16).to_be_bytes());
409    metadata.extend_from_slice(website.as_bytes());
410    //let chars: Vec<char> = website.chars().collect();
411    //for ch in chars {metadata.push(ch as u8)}
412
413    metadata.push(0); //Is App
414
415    fs::write(file_path.clone() + "/" + &dir_name + "/.metadata-v2", metadata);
416
417    let keys: Vec<&str> = vec!["savedGame", "settings"];
418
419    let conn: Connection = Connection::open(file_path.clone() + "/" + &dir_name + "/ls/data.sqlite")?;
420
421    conn.pragma_update(None, "user_version", 80);
422    conn.pragma_update(None, "auto_vacuum", 2);
423    conn.pragma_update(None, "page_size", 1024);
424
425    conn.execute("VACUUM", []);
426
427    //Creates the localStorage data table inside the database if it does not exist
428    conn.execute(
429        "CREATE TABLE if not exists data ( 
430        key TEXT PRIMARY KEY, 
431        utf16_length INTEGER NOT NULL, 
432        conversion_type INTEGER NOT NULL, 
433        compression_type INTEGER NOT NULL, 
434        last_access_time INTEGER NOT NULL DEFAULT 0, 
435        value BLOB NOT NULL)", 
436        []
437    )?;
438
439    let mut len = 0;
440
441    //Inserting the savedGame into the database
442    let mut stmt = conn.prepare("INSERT OR REPLACE INTO data (key, utf16_length, conversion_type, compression_type, value) values (?1, ?2, ?3, ?4, ?5)" )?;
443
444    for i in 0..json_strings.len() {
445        //Converting the json_string into an array of chars
446        //let characters: Vec<char> = json_strings[i].chars().collect();
447        let utf16_length: i32  = json_strings[i].len() as i32;
448
449        len += utf16_length;
450
451        //Converting chars to u8
452        let mut decompressed: Vec<u8> = Vec::new();
453        decompressed.extend_from_slice(json_strings[i].as_bytes());
454
455        //Creating the output array
456        let max_comp_length = snap::raw::max_compress_len(decompressed.len());
457        let mut compressed: Vec<u8> = Vec::new();
458        for _ in 0..max_comp_length {
459            compressed.push(0);
460        }
461
462        //Compressing and cleaning the compressed value
463        Encoder::compress(&mut Encoder::new(), &decompressed, & mut compressed).unwrap();
464        let mut b: u8 = 0;
465        while b == 0 {
466            b = compressed.pop().unwrap();
467        }
468        compressed.push(b);
469
470        stmt.execute((keys[i], utf16_length, 1, 1, compressed))?;
471    }
472
473    len += 10;
474    let vacuum_size = fs::metadata(file_path.clone() + "/" + &dir_name + "/ls/data.sqlite").unwrap().len();
475
476    conn.execute(
477        "CREATE TABLE if not exists database ( 
478        origin TEXT NOT NULL, 
479        usage INTEGER NOT NULL DEFAULT 0, 
480        last_vacuum_time INTEGER NOT NULL DEFAULT 0, 
481        last_analyze_time INTEGER NOT NULL DEFAULT 0, 
482        last_vacuum_size INTEGER NOT NULL DEFAULT 0)",
483        [])?;
484
485    stmt = conn.prepare("INSERT OR REPLACE INTO database (origin,usage,last_vacuum_time,last_analyze_time,last_vacuum_size) values (?1, ?2, ?3, ?4, ?5)" )?;
486
487    stmt.execute((&website,len,timestamp,0,vacuum_size))?;
488
489    fs::write(file_path.clone() + "/" + &dir_name + "/ls/usage", "");
490
491    Ok(())
492
493}
494
495
496
497/**
498 * Following function accepts a path to a db file, and a 
499 * json string. The json string is parsed as the value and
500 * compressed using snappy compression, and is then passed
501 * to the db and saved. Note this only applies to Firefox,
502 * as firefox is the only browser that I know of that uses
503 * this structure. Chromium support in the future...
504 */
505pub fn write_saved_game (file_path: String, json_string: String, website: String) -> Result<()> {
506
507    let settings: String = serialize_settings(Settings::default());
508    write_data(file_path, [json_string,settings], website);
509
510    return Ok(());
511
512}
513
514/**
515 * Following function excepts a file location and a world save formatted as a 
516 * json string. It then creates a localStorage.setItem() command for the key
517 * savedGame, in order for it to be copy pasted into a browser console to 
518 * insert the world save
519 */
520pub fn write_saved_game_command (file: String, json_string: String) -> String {
521    let open: String = String::from(r#"localStorage.setItem("savedGame", `"#); //Opening command for localStorage
522    let close: String = String::from(r#"`)"#); //Closing command for localStorage
523
524    let output: String = String::from(format!{r"{open}{json_string}{close}"});
525
526    if file != "" {fs::write(file, output.clone()).expect("Error when writing to file")} //Attempting to write localStorage command to file
527
528    return output;
529
530}
531
532/**
533 * Following function excepts a file location and settings formatted as a 
534 * json string. It then creates a localStorage.setItem() command for the key
535 * settings, in order for it to be copy pasted into a browser console to 
536 * insert the world save
537 */
538pub fn write_settings_command (file: String, json_string: String) -> String {
539    let open: String = String::from(r#"localStorage.setItem("settings", `"#); //Opening command for localStorage
540    let close: String = String::from(r#"`)"#); //Closing command for localStorage
541
542    let output: String = String::from(format!{r"{open}{json_string}{close}"});
543
544    if file != "" {fs::write(file, output.clone()).expect("Error when writing to file")} //Attempting to write localStorage command to file
545
546    return output;
547
548}
549
550/**
551 * Following function excepts a file location and an array containing both a 
552 * world save and settings formatted as json string. It then creates a 
553 * localStorage.setItem() command for the key savedGame and settings, 
554 * in order for it to be copy pasted into a browser console to 
555 * insert the world save
556 */
557pub fn write_local_storage_command (file: String, json_strings: [String; 2]) -> String {
558    let open: String = String::from(r#"localStorage.setItem("savedGame", `"#); //Opening command for localStorage
559    let close: String = String::from(r#"`)"#); //Closing command for localStorage
560    let mut string: String = json_strings[0].clone();
561    
562    let mut output: String = String::from(format!{r"{open}{string}{close}"});
563    output += ";";
564    
565    string = json_strings[1].clone();
566    output += &format!{r"{open}{string}{close}"};
567
568    if file != "" {fs::write(file, output.clone()).expect("Error when writing to file")} //Attempting to write localStorage command to file
569
570    return output;
571
572}
573
574/**
575 * Following function takes a seed and creates a JSLevel from this seed
576 */
577pub fn generate_saved_game_from_seed (seed: i64, tile_map: Vec<u8>) -> JSLevel {
578
579    let world_size: i32 = ((tile_map.len()/64) as f64).sqrt() as i32;
580    let changed_blocks: HashMap<String, ChangedBlocks> = HashMap::new();
581    let level = JSLevel::new(seed, changed_blocks, world_size, 1);
582
583    return deserialize_saved_game(serialize_saved_game(level, tile_map, 2));
584
585}
586
587/**
588 * Following function accepts a world size and seed,
589 * and then passes them to the js world generation 
590 * functionality, and then returns the output as a Vec<>
591 */
592pub fn get_tile_map (world_size: i32, seed: i64) -> Vec<u8> {
593    let y: i32 = 64;
594    let level: HashMap<usize, u8> = random_level_worker::start_generation(world_size, seed); //Generating hashmap of all tiles in the world
595    let mut tile_map: Vec<u8> = Vec::new();
596
597    for i in 0..world_size * y * world_size {
598        tile_map.push(level.get(&(i as usize)).copied().unwrap_or(0)); //Copying hashmap to vec
599    }
600
601    return tile_map
602}
603
604/**
605 * Following function takes a seed and creates a JSLevel from this seed,
606 * and then compares it agains the given tilemap to create a json formatted
607 * JS world save
608 */
609#[deprecated(since="0.2.0", note="please use `generate_saved_game_from_seed` instead")]
610pub fn serialize_saved_game_from_seed (seed: i64, tile_map: Vec<u8>) -> String {
611
612    let world_size: i32 = ((tile_map.len()/64) as f64).sqrt() as i32;
613    let changed_blocks: HashMap<String, ChangedBlocks> = HashMap::new();
614    let level = JSLevel::new(seed, changed_blocks, world_size, 1);
615
616    return serialize_saved_game(level, tile_map, 2);
617}
618
619/*/**
620 * Following function accepts a path to a db file, and a 
621 * json string. The json string is parsed as the value and
622 * compressed using snappy compression, and is then passed
623 * to the db and saved. Note this only applies to Firefox,
624 * as firefox is the only browser that I know of that uses
625 * this structure. Chromium support in the future...
626 */ 
627pub fn write_settings (file_path: String, json_string: String, website: String) -> Result<()> {
628
629    let saved_game: String = serialize_saved_game(JSLevel::default(),);
630    write_data(file_path, [saved_game,json_string], website);
631
632    return Ok(());
633
634}*/