415 lines
8.9 KiB
Odin
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
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|