Files
Edit2D/level.odin
T

415 lines
8.9 KiB
Odin

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"
/*
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 : 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
}