package edit2d import "core:strings" import "core:os" import "core:bytes" import "core:path/filepath" import "core:crypto/hash" import "core:time" import b2 "vendor:box2d" import "core:fmt" import "core:encoding/cbor" import draw "./draw" /* The engine can provide helper functions so that it'll be easier to write the level code in the game A Typical level code */ Engine_World :: struct { world_id : b2.WorldId, //This in engine code? static_indexes : map[Static_Index]int `cbor:"-"`, relations : map[^Static_Index][dynamic]Static_Index_Global `cbor:"-"`, relations_serializeable : map[ Static_Index][dynamic]Static_Index_Global, /* Seems okay to put the joint defs in engine rather than in game because we don't add more attributes in joints in the game Can be changed later without requireing refactor */ revolute_joint_defs : [dynamic]revolt_joint_def, distant_joint_defs : [dynamic]distance_joint_def, joints : [dynamic]b2.JointId `cbor:"-"`, cam : draw.Camera, name : string, } @(require_results) time_to_string_hms :: proc(t: time.Time, buf: []u8) -> (res: string) #no_bounds_check { assert(len(buf) >= time.MIN_HMS_LEN) h, m, s := time.clock(t) buf[7] = '0' + u8(s % 10); s /= 10 buf[6] = '0' + u8(s) buf[5] = '-' buf[4] = '0' + u8(m % 10); m /= 10 buf[3] = '0' + u8(m) buf[2] = '-' buf[1] = '0' + u8(h % 10); h /= 10 buf[0] = '0' + u8(h) return string(buf[:time.MIN_HMS_LEN]) } cbor_flags : cbor.Encoder_Flags : { .Self_Described_CBOR, .Deterministic_Int_Size, .Deterministic_Float_Size, } level_load_from_files :: proc( $G, $L, $E: typeid, game: ^G, state: ^Engine_State ) { files, err := os.read_all_directory_by_path("levels", context.allocator) for file in files { if !os.exists(file.fullpath) do continue if os.is_dir(file.fullpath) do continue level_name := strings.clone(filepath.stem(file.fullpath)) game.levels[level_name] = {} game.curr_level = strings.clone(level_name) level_init_from_path(L, E, &game.levels[level_name], file.fullpath, state) } } //Returns engine_world by reading from the file /* In the user code level_name = strings.clone(filepath.stem(path)) game.levels[level_name] = {} level_init_from_path(&game.levels[level_name], path) */ //We don't want the user to configure and use cbor everytime so we want to handle it // level_init_from_path :: proc( $T, $E: typeid, level_data : ^T, path : string, state: ^Engine_State, ) -> (ret : bool) { level := cast(^Engine_World)level_data if os.is_file(path) { level_name := strings.clone(filepath.stem(path)) data, err := os.read_entire_file_from_path(path, context.allocator) level.name = level_name //? level.relations = {} level.relations = make(map[^Static_Index][dynamic]Static_Index_Global) if len(data) > 0 { unmarshall_err := cbor.unmarshal(data, level_data) delete(data) if unmarshall_err == nil { level_reload(T, E, level_data) state.draw.cam = level.cam for key, val in level.relations_serializeable { index := level.static_indexes[key] entity := level_data.entities[index] level_data.relations[entity.index] = {} for v in val do append(&level.relations[entity.index], v) } ret = true }else { ret = false fmt.eprintf("Faield to unmarshall file %s \n", unmarshall_err) } }else { level_reload(T, E, level_data) state.draw.cam = level.cam for key, val in level.relations_serializeable { index := level.static_indexes[key] entity := level_data.entities[index] level_data.relations[entity.index] = {} for v in val do append(&level.relations[entity.index], v) } //Handle no data fmt.println("Empty level") ret = false } }else { fmt.eprintln("File not exist or is a directory") ret = false } return } /* We can't create the entities That should be done by the user */ level_reload :: proc($T: typeid, $E: typeid, level: ^T) { clear(&level.entities) clear(&level.static_indexes) if level.world_id != b2.nullWorldId do b2.DestroyWorld(level.world_id) //initilize world { world_def := b2.DefaultWorldDef() b2.SetLengthUnitsPerMeter(100) //Get the gravity from config world_def.gravity = {0, -9.8 * 100} level.world_id = b2.CreateWorld(world_def) } for &def, i in &level.entity_defs { //new_entity := engine_create_entity(&def, level.world_id, i32(i)) new_entity : E new_entity.type = def.type new_entity.engine = engine_create_entity(&def, level.world_id, i32(i)) //new_entity := entity_create_by_type(&def, level.world_id, i32(i)) append(&level.entities, new_entity) if new_entity.index != nil do level.static_indexes[new_entity.index^] = i //if new_entity.type == .PLAYER do level.player_index = i32(i) } { for &def in &level.distant_joint_defs { //Get entity_a and entity_b if def.entity_a <= 0 do continue if def.entity_b <= 0 do continue entity_a := &level.entities[level.static_indexes[def.entity_a]] def.bodyIdA = entity_a.body_id entity_b := &level.entities[level.static_indexes[def.entity_b]] def.bodyIdB = entity_b.body_id append(&level.joints, b2.CreateDistanceJoint(level.world_id, def.def)) } for &def in &level.revolute_joint_defs { //Get entity_a and entity_b if def.entity_a <= 0 do continue if def.entity_b <= 0 do continue entity_a := &level.entities[level.static_indexes[def.entity_a]] def.bodyIdA = entity_a.body_id entity_b := &level.entities[level.static_indexes[def.entity_b]] def.bodyIdB = entity_b.body_id append(&level.joints, b2.CreateRevoluteJoint(level.world_id, def.def)) } } //level.player.e = &level.entities[level.player_index] } /* Saves the level to the levels directory using level's name as filename It uses core library's hash and cbor to searialize the level into bytes It assumes that it uses engine_world struct as it's first member Checks hash of current level state and file's hash If both hash is same the exits */ level_save_to_file :: proc(level_data: ^$T, state: ^Engine_State) { level := cast(^Engine_World)level_data level_path := fmt.tprintf("levels/%s.cbor", level.name) level.cam = state.draw.cam fmt.println(level_path) //Relation serializeable { clear(&level.relations_serializeable) for key, val in &level.relations { if key != nil { level.relations_serializeable[key^] = {} for v in val do append(&level.relations_serializeable[key^], v) } } } binary, cbor_err := cbor.marshal_into_bytes(level_data^, cbor_flags) if cbor_err == nil { }else { fmt.eprintf("Failed to marshall level %s \n", cbor_err) return } //Get hash of current loaded game and current saved game binary_hash := hash.hash_bytes(.SHA3_512, binary) fmt.println(binary_hash) file_hash, hash_err := hash.hash_file_by_name(.SHA3_512, level_path) //If the hash is same that means no changes has been made if bytes.compare(binary_hash, file_hash) != 0 { if !os.exists("levels") { err := os.mkdir("levels") if err != nil { fmt.println("Failed to create levels directory") fmt.println(err) } } /* Create backeup before saving to file We create backup by copying the current level file to the backup directory Backup directory will contain the date and time of the backup */ if os.exists(level_path) { //Get time and date to create file and folder name for backup curr_time := time.now() buf : [time.MIN_YYYY_DATE_LEN]u8 time_buf : [time.MIN_HMS_LEN]u8 date_str := time.to_string_yyyy_mm_dd(curr_time, buf[:]) time_str := time_to_string_hms(curr_time, time_buf[:]) backup_dir_path := fmt.tprintf("levels/backups/%s",date_str) backup_file_path := fmt.tprintf("levels/backups/%s/%s-%s", date_str, level.name, time_str) bk_err := os.make_directory_all(backup_dir_path) if bk_err != nil { fmt.eprintf("Failed to create backup directory %s \n", bk_err) } else{ bk_err := os.copy_file(backup_file_path, level_path) if bk_err != nil { fmt.eprintf("Failed to copy file %s\n", bk_err) } } } //Save the current binary to file save_err := os.write_entire_file_from_bytes(level_path, binary) if save_err != nil { fmt.eprintf("Failed to save level %s\n", save_err) } }else{ fmt.println("No changes detected") } } /* The engine assumes that it has attribute entities */ game_to_engine_entities :: proc($Game: typeid, game_data: ^Game, state: ^Engine_State) { level := &game_data.levels[game_data.curr_level] interface : ^Interface_State = &game_data.interface clear(&interface.entities) clear(&interface.entity_defs) for &entity in &level.entities { append(&interface.entities, &entity.engine) } for &def in &level.entity_defs { append(&interface.entity_defs, &def.engine) } interface.state = state interface.world = &level.engine }