[feat]: initial commit

This commit is contained in:
Kaime Welsh 2026-05-03 02:09:13 -07:00
commit 7b48f34e94
24 changed files with 1354 additions and 0 deletions

19
source/engine/main.odin Normal file
View file

@ -0,0 +1,19 @@
package engine
init :: proc() {
}
update :: proc() {
}
draw :: proc() {
}
unload :: proc() {
}

173
source/game.odin Normal file
View file

@ -0,0 +1,173 @@
/*
This file is the starting point of your game.
Some important procedures are:
- game_init_window: Opens the window
- game_init: Sets up the game state
- game_update: Run once per frame
- game_should_close: For stopping your game when close button is pressed
- game_shutdown: Shuts down game and frees memory
- game_shutdown_window: Closes window
The procs above are used regardless if you compile using the `build_release`
script or the `build_hot_reload` script. However, in the hot reload case, the
contents of this file is compiled as part of `build/hot_reload/game.dll` (or
.dylib/.so on mac/linux). In the hot reload cases some other procedures are
also used in order to facilitate the hot reload functionality:
- game_memory: Run just before a hot reload. That way game_hot_reload.exe has a
pointer to the game's memory that it can hand to the new game DLL.
- game_hot_reloaded: Run after a hot reload so that the `g` global
variable can be set to whatever pointer it was in the old DLL.
NOTE: When compiled as part of `build_release`, `build_debug` or `build_web`
then this whole package is just treated as a normal Odin package. No DLL is
created.
*/
// NOTE: `fmt.ctprintf` uses the temp allocator. The temp allocator is
// cleared at the end of the frame by the main application, meaning inside
// `main_hot_reload.odin`, `main_release.odin` or `main_web_entry.odin`.
package game
import "engine"
import rl "vendor:raylib"
GAME_WIDTH :: 640
GAME_HEIGHT :: 270
Game_Memory :: struct {
render_target: rl.RenderTexture,
render_scale: f32,
run: bool,
}
g: ^Game_Memory
update :: proc() {
if rl.IsKeyPressed(.ESCAPE) {
g.run = false
}
g.render_scale = min(
f32(rl.GetScreenWidth()) / f32(GAME_WIDTH),
f32(rl.GetScreenHeight()) / f32(GAME_HEIGHT),
)
rl.SetMouseOffset(
-i32((f32(rl.GetScreenWidth()) - (f32(GAME_WIDTH) * g.render_scale)) * 0.5),
-i32((f32(rl.GetScreenHeight()) - (f32(GAME_HEIGHT) * g.render_scale)) * 0.5),
)
rl.SetMouseScale(1 / g.render_scale, 1 / g.render_scale)
engine.update()
}
draw :: proc() {
rl.BeginTextureMode(g.render_target)
rl.ClearBackground(rl.BLACK)
engine.draw()
rl.EndTextureMode()
rl.BeginDrawing()
rl.ClearBackground(rl.BLACK)
rl.DrawTexturePro(
g.render_target.texture,
{0, 0, f32(g.render_target.texture.width), f32(-g.render_target.texture.height)},
{
(f32(rl.GetScreenWidth()) - (f32(GAME_WIDTH) * g.render_scale)) * 0.5,
(f32(rl.GetScreenHeight()) - (f32(GAME_HEIGHT) * g.render_scale)) * 0.5,
f32(GAME_WIDTH) * g.render_scale,
f32(GAME_HEIGHT) * g.render_scale,
},
{0, 0},
0,
rl.WHITE,
)
rl.EndDrawing()
}
@(export)
game_update :: proc() {
update()
draw()
free_all(context.temp_allocator)
}
@(export)
game_init_window :: proc() {
rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT})
rl.InitWindow(1280, 720, "Untitle Roguelike")
rl.SetWindowPosition(200, 200)
rl.SetTargetFPS(60)
rl.SetExitKey(nil)
}
@(export)
game_init :: proc() {
g = new(Game_Memory)
g^ = Game_Memory {
render_target = rl.LoadRenderTexture(GAME_WIDTH, GAME_HEIGHT),
run = true,
}
engine.init()
game_hot_reloaded(g)
}
@(export)
game_should_run :: proc() -> bool {
when ODIN_OS != .JS {
if rl.WindowShouldClose() {
return false
}
}
return g.run
}
@(export)
game_shutdown :: proc() {
free(g)
engine.unload()
}
@(export)
game_shutdown_window :: proc() {
rl.CloseWindow()
}
@(export)
game_memory :: proc() -> rawptr {
return g
}
@(export)
game_memory_size :: proc() -> int {
return size_of(Game_Memory)
}
@(export)
game_hot_reloaded :: proc(mem: rawptr) {
g = (^Game_Memory)(mem)
// Here you can also set your own global variables. A good idea is to make
// your global variables into pointers that point to something inside `g`.
}
@(export)
game_force_reload :: proc() -> bool {
return rl.IsKeyPressed(.F5)
}
@(export)
game_force_restart :: proc() -> bool {
return rl.IsKeyPressed(.F6)
}
// In a web build, this is called when browser changes size. Remove the
// `rl.SetWindowSize` call if you don't want a resizable game.
game_parent_window_size_changed :: proc(w, h: int) {
rl.SetWindowSize(i32(w), i32(h))
}

View file

@ -0,0 +1,229 @@
/*
Development game exe. Loads build/hot_reload/game.dll and reloads it whenever it
changes.
*/
package main
import "core:dynlib"
import "core:fmt"
import "core:c/libc"
import "core:os"
import "core:log"
import "core:mem"
import "core:path/filepath"
import "core:time"
when ODIN_OS == .Windows {
DLL_EXT :: ".dll"
} else when ODIN_OS == .Darwin {
DLL_EXT :: ".dylib"
} else {
DLL_EXT :: ".so"
}
GAME_DLL_DIR :: "build/hot_reload/"
GAME_DLL_PATH :: GAME_DLL_DIR + "game" + DLL_EXT
// We copy the DLL because using it directly would lock it, which would prevent
// the compiler from writing to it.
copy_dll :: proc(to: string) -> bool {
copy_err := os.copy_file(to, GAME_DLL_PATH)
if copy_err != nil {
fmt.printfln("Failed to copy " + GAME_DLL_PATH + " to {0}: %v", to, copy_err)
return false
}
return true
}
Game_API :: struct {
lib: dynlib.Library,
init_window: proc(),
init: proc(),
update: proc(),
should_run: proc() -> bool,
shutdown: proc(),
shutdown_window: proc(),
memory: proc() -> rawptr,
memory_size: proc() -> int,
hot_reloaded: proc(mem: rawptr),
force_reload: proc() -> bool,
force_restart: proc() -> bool,
modification_time: time.Time,
api_version: int,
}
load_game_api :: proc(api_version: int) -> (api: Game_API, ok: bool) {
mod_time, mod_time_error := os.last_write_time_by_name(GAME_DLL_PATH)
if mod_time_error != os.ERROR_NONE {
fmt.printfln(
"Failed getting last write time of " + GAME_DLL_PATH + ", error code: {1}",
mod_time_error,
)
return
}
game_dll_name := fmt.tprintf(GAME_DLL_DIR + "game_{0}" + DLL_EXT, api_version)
copy_dll(game_dll_name) or_return
// This proc matches the names of the fields in Game_API to symbols in the
// game DLL. It actually looks for symbols starting with `game_`, which is
// why the argument `"game_"` is there.
_, ok = dynlib.initialize_symbols(&api, game_dll_name, "game_", "lib")
if !ok {
fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error())
}
api.api_version = api_version
api.modification_time = mod_time
ok = true
return
}
unload_game_api :: proc(api: ^Game_API) {
if api.lib != nil {
if !dynlib.unload_library(api.lib) {
fmt.printfln("Failed unloading lib: {0}", dynlib.last_error())
}
}
if os.remove(fmt.tprintf(GAME_DLL_DIR + "game_{0}" + DLL_EXT, api.api_version)) != nil {
fmt.printfln("Failed to remove {0}game_{1}" + DLL_EXT + " copy", GAME_DLL_DIR, api.api_version)
}
}
main :: proc() {
// Set working dir to dir of executable.
exe_path := os.args[0]
exe_dir := filepath.dir(string(exe_path), context.temp_allocator)
os.set_working_directory(exe_dir)
context.logger = log.create_console_logger()
default_allocator := context.allocator
tracking_allocator: mem.Tracking_Allocator
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
context.allocator = mem.tracking_allocator(&tracking_allocator)
reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool {
err := false
for _, value in a.allocation_map {
log.errorf("%v: Leaked %v bytes\n", value.location, value.size)
err = true
}
mem.tracking_allocator_clear(a)
return err
}
game_api_version := 0
game_api, game_api_ok := load_game_api(game_api_version)
if !game_api_ok {
fmt.println("Failed to load Game API")
return
}
game_api_version += 1
game_api.init_window()
game_api.init()
old_game_apis := make([dynamic]Game_API, default_allocator)
for game_api.should_run() {
game_api.update()
force_reload := game_api.force_reload()
force_restart := game_api.force_restart()
reload := force_reload || force_restart
game_dll_mod, game_dll_mod_err := os.last_write_time_by_name(GAME_DLL_PATH)
if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod {
reload = true
}
if reload {
new_game_api, new_game_api_ok := load_game_api(game_api_version)
if new_game_api_ok {
force_restart = force_restart || game_api.memory_size() != new_game_api.memory_size()
if !force_restart {
// This does the normal hot reload
// Note that we don't unload the old game APIs because that
// would unload the DLL. The DLL can contain stored info
// such as string literals. The old DLLs are only unloaded
// on a full reset or on shutdown.
append(&old_game_apis, game_api)
game_memory := game_api.memory()
game_api = new_game_api
game_api.hot_reloaded(game_memory)
} else {
// This does a full reset. That's basically like opening and
// closing the game, without having to restart the executable.
//
// You end up in here if the game requests a full reset OR
// if the size of the game memory has changed. That would
// probably lead to a crash anyways.
game_api.shutdown()
reset_tracking_allocator(&tracking_allocator)
for &g in old_game_apis {
unload_game_api(&g)
}
clear(&old_game_apis)
unload_game_api(&game_api)
game_api = new_game_api
game_api.init()
}
game_api_version += 1
}
}
if len(tracking_allocator.bad_free_array) > 0 {
for b in tracking_allocator.bad_free_array {
log.errorf("Bad free at: %v", b.location)
}
// This prevents the game from closing without you seeing the bad
// frees. This is mostly needed because I use Sublime Text and my game's
// console isn't hooked up into Sublime's console properly.
libc.getchar()
panic("Bad free detected")
}
}
free_all(context.temp_allocator)
game_api.shutdown()
if reset_tracking_allocator(&tracking_allocator) {
// This prevents the game from closing without you seeing the memory
// leaks. This is mostly needed because I use Sublime Text and my game's
// console isn't hooked up into Sublime's console properly.
libc.getchar()
}
for &g in old_game_apis {
unload_game_api(&g)
}
delete(old_game_apis)
game_api.shutdown_window()
unload_game_api(&game_api)
mem.tracking_allocator_destroy(&tracking_allocator)
}
// Make game use good GPU on laptops.
@(export)
NvOptimusEnablement: u32 = 1
@(export)
AmdPowerXpressRequestHighPerformance: i32 = 1

View file

@ -0,0 +1,72 @@
/*
For making a release exe that does not use hot reload.
*/
package main_release
import "core:log"
import "core:os"
import "core:path/filepath"
import "core:mem"
import game ".."
_ :: mem
USE_TRACKING_ALLOCATOR :: #config(USE_TRACKING_ALLOCATOR, false)
main :: proc() {
// Set working dir to dir of executable.
exe_path := os.args[0]
exe_dir := filepath.dir(string(exe_path), context.temp_allocator)
os.set_working_directory(exe_dir)
mode := os.Permissions { .Read_User, .Write_User, .Read_Group, .Read_Other }
logh, logh_err := os.open("log.txt", {.Create, .Trunc, .Read, .Write}, mode)
if logh_err == os.ERROR_NONE {
os.stdout = logh
os.stderr = logh
}
logger_alloc := context.allocator
logger := logh_err == os.ERROR_NONE ? log.create_file_logger(logh, allocator = logger_alloc) : log.create_console_logger(allocator = logger_alloc)
context.logger = logger
when USE_TRACKING_ALLOCATOR {
default_allocator := context.allocator
tracking_allocator: mem.Tracking_Allocator
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
context.allocator = mem.tracking_allocator(&tracking_allocator)
}
game.game_init_window()
game.game_init()
for game.game_should_run() {
game.game_update()
}
free_all(context.temp_allocator)
game.game_shutdown()
game.game_shutdown_window()
when USE_TRACKING_ALLOCATOR {
for _, value in tracking_allocator.allocation_map {
log.errorf("%v: Leaked %v bytes\n", value.location, value.size)
}
mem.tracking_allocator_destroy(&tracking_allocator)
}
if logh_err == os.ERROR_NONE {
log.destroy_file_logger(logger, logger_alloc)
}
}
// make game use good GPU on laptops etc
@(export)
NvOptimusEnablement: u32 = 1
@(export)
AmdPowerXpressRequestHighPerformance: i32 = 1

View file

@ -0,0 +1,126 @@
/*
This allocator uses the malloc, calloc, free and realloc procs that emscripten
exposes in order to allocate memory. Just like Odin's default heap allocator
this uses proper alignment, so that maps and simd works.
*/
package main_web
import "core:mem"
import "core:c"
import "base:intrinsics"
// This will create bindings to emscripten's implementation of libc
// memory allocation features.
@(default_calling_convention = "c")
foreign {
calloc :: proc(num, size: c.size_t) -> rawptr ---
free :: proc(ptr: rawptr) ---
malloc :: proc(size: c.size_t) -> rawptr ---
realloc :: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
}
emscripten_allocator :: proc "contextless" () -> mem.Allocator {
return mem.Allocator{emscripten_allocator_proc, nil}
}
emscripten_allocator_proc :: proc(
allocator_data: rawptr,
mode: mem.Allocator_Mode,
size, alignment: int,
old_memory: rawptr,
old_size: int,
location := #caller_location
) -> (data: []byte, err: mem.Allocator_Error) {
// These aligned alloc procs are almost indentical those in
// `_heap_allocator_proc` in `core:os`. Without the proper alignment you
// cannot use maps and simd features.
aligned_alloc :: proc(size, alignment: int, zero_memory: bool, old_ptr: rawptr = nil) -> ([]byte, mem.Allocator_Error) {
a := max(alignment, align_of(rawptr))
space := size + a - 1
allocated_mem: rawptr
if old_ptr != nil {
original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^
allocated_mem = realloc(original_old_ptr, c.size_t(space+size_of(rawptr)))
} else if zero_memory {
// calloc automatically zeros memory, but it takes a number + size
// instead of just size.
allocated_mem = calloc(c.size_t(space+size_of(rawptr)), 1)
} else {
allocated_mem = malloc(c.size_t(space+size_of(rawptr)))
}
aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr)))
ptr := uintptr(aligned_mem)
aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a)
diff := int(aligned_ptr - ptr)
if (size + diff) > space || allocated_mem == nil {
return nil, .Out_Of_Memory
}
aligned_mem = rawptr(aligned_ptr)
mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem
return mem.byte_slice(aligned_mem, size), nil
}
aligned_free :: proc(p: rawptr) {
if p != nil {
free(mem.ptr_offset((^rawptr)(p), -1)^)
}
}
aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int) -> ([]byte, mem.Allocator_Error) {
if p == nil {
return nil, nil
}
return aligned_alloc(new_size, new_alignment, true, p)
}
switch mode {
case .Alloc:
return aligned_alloc(size, alignment, true)
case .Alloc_Non_Zeroed:
return aligned_alloc(size, alignment, false)
case .Free:
aligned_free(old_memory)
return nil, nil
case .Resize:
if old_memory == nil {
return aligned_alloc(size, alignment, true)
}
bytes := aligned_resize(old_memory, old_size, size, alignment) or_return
// realloc doesn't zero the new bytes, so we do it manually.
if size > old_size {
new_region := raw_data(bytes[old_size:])
intrinsics.mem_zero(new_region, size - old_size)
}
return bytes, nil
case .Resize_Non_Zeroed:
if old_memory == nil {
return aligned_alloc(size, alignment, false)
}
return aligned_resize(old_memory, old_size, size, alignment)
case .Query_Features:
set := (^mem.Allocator_Mode_Set)(old_memory)
if set != nil {
set^ = {.Alloc, .Free, .Resize, .Query_Features}
}
return nil, nil
case .Free_All, .Query_Info:
return nil, .Mode_Not_Implemented
}
return nil, .Mode_Not_Implemented
}

View file

@ -0,0 +1,91 @@
/*
This logger is largely a copy of the console logger in `core:log`, but it uses
emscripten's `puts` proc to write into he console of the web browser.
This is more or less identical to the logger in Aronicu's repository:
https://github.com/Aronicu/Raylib-WASM/tree/main
*/
package main_web
import "core:c"
import "core:fmt"
import "core:log"
import "core:strings"
Emscripten_Logger_Opts :: log.Options{.Level, .Short_File_Path, .Line}
create_emscripten_logger :: proc (lowest := log.Level.Debug, opt := Emscripten_Logger_Opts) -> log.Logger {
return log.Logger{data = nil, procedure = logger_proc, lowest_level = lowest, options = opt}
}
// This create's a binding to `puts` which will be linked in as part of the
// emscripten runtime.
@(default_calling_convention = "c")
foreign {
puts :: proc(buffer: cstring) -> c.int ---
}
@(private="file")
logger_proc :: proc(
logger_data: rawptr,
level: log.Level,
text: string,
options: log.Options,
location := #caller_location
) {
b := strings.builder_make(context.temp_allocator)
strings.write_string(&b, Level_Headers[level])
do_location_header(options, &b, location)
fmt.sbprint(&b, text)
if bc, bc_err := strings.to_cstring(&b); bc_err == nil {
puts(bc)
}
}
@(private="file")
Level_Headers := [?]string {
0 ..< 10 = "[DEBUG] --- ",
10 ..< 20 = "[INFO ] --- ",
20 ..< 30 = "[WARN ] --- ",
30 ..< 40 = "[ERROR] --- ",
40 ..< 50 = "[FATAL] --- ",
}
@(private="file")
do_location_header :: proc(opts: log.Options, buf: ^strings.Builder, location := #caller_location) {
if log.Location_Header_Opts & opts == nil {
return
}
fmt.sbprint(buf, "[")
file := location.file_path
if .Short_File_Path in opts {
last := 0
for r, i in location.file_path {
if r == '/' {
last = i + 1
}
}
file = location.file_path[last:]
}
if log.Location_File_Opts & opts != nil {
fmt.sbprint(buf, file)
}
if .Line in opts {
if log.Location_File_Opts & opts != nil {
fmt.sbprint(buf, ":")
}
fmt.sbprint(buf, location.line)
}
if .Procedure in opts {
if (log.Location_File_Opts | {.Line}) & opts != nil {
fmt.sbprint(buf, ":")
}
fmt.sbprintf(buf, "%s()", location.procedure)
}
fmt.sbprint(buf, "] ")
}

View file

@ -0,0 +1,114 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Odin + Raylib on the web</title>
<meta name="title" content="Odin + Raylib on the web">
<meta name="description" content="Make games using Odin + Raylib that work in the browser">
<meta name="viewport" content="width=device-width">
<style>
body {
margin: 0px;
overflow: hidden;
background-color: black;
}
canvas.game_canvas {
border: 0px none;
background-color: black;
padding-left: 0;
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
}
</style>
</head>
<body>
<canvas class="game_canvas" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1" onmousedown="event.target.focus()" onkeydown="event.preventDefault()"></canvas>
<script type="text/javascript" src="odin.js"></script>
<script>
var odinMemoryInterface = new odin.WasmMemoryInterface();
odinMemoryInterface.setIntSize(4);
var odinImports = odin.setupDefaultImports(odinMemoryInterface);
// The Module is used as configuration for emscripten.
var Module = {
// This is called by emscripten when it starts up.
instantiateWasm: (imports, successCallback) => {
const newImports = {
...odinImports,
...imports
}
return WebAssembly.instantiateStreaming(fetch("index.wasm"), newImports).then(function(output) {
var e = output.instance.exports;
odinMemoryInterface.setExports(e);
odinMemoryInterface.setMemory(e.memory);
return successCallback(output.instance);
});
},
// This happens a bit after `instantiateWasm`, when everything is
// done setting up. At that point we can run code.
onRuntimeInitialized: () => {
var e = wasmExports;
// Calls any procedure marked with @init
e._start();
// See source/main_web/main_web.odin for main_start,
// main_update and main_end.
e.main_start();
function send_resize() {
var canvas = document.getElementById('canvas');
e.web_window_size_changed(canvas.width, canvas.height);
}
window.addEventListener('resize', function(event) {
send_resize();
}, true);
// This can probably be done better: Ideally we'd feed the
// initial size to `main_start`. But there seems to be a
// race condition. `canvas` doesn't have it's correct size yet.
send_resize();
// Runs the "main loop".
function do_main_update() {
if (!e.main_update()) {
e.main_end();
// Calls procedures marked with @fini
e._end();
return;
}
window.requestAnimationFrame(do_main_update);
}
window.requestAnimationFrame(do_main_update);
},
print: (function() {
var element = document.getElementById("output");
if (element) element.value = ''; // clear browser cache
return function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
if (element) {
element.value += text + "\n";
element.scrollTop = element.scrollHeight; // focus on bottom
}
};
})(),
canvas: (function() {
return document.getElementById("canvas");
})()
};
</script>
<!-- Emscripten injects its javascript here -->
{{{ SCRIPT }}}
</body>
</html>

View file

@ -0,0 +1,54 @@
/*
These procs are the ones that will be called from `main_wasm.c`.
*/
package main_web
import "base:runtime"
import "core:c"
import "core:mem"
import game ".."
@(private="file")
web_context: runtime.Context
@export
main_start :: proc "c" () {
context = runtime.default_context()
// The WASM allocator doesn't seem to work properly in combination with
// emscripten. There is some kind of conflict with how the manage memory.
// So this sets up an allocator that uses emscripten's malloc.
context.allocator = emscripten_allocator()
runtime.init_global_temporary_allocator(1*mem.Megabyte)
// Since we now use js_wasm32 we should be able to remove this and use
// context.logger = log.create_console_logger(). However, that one produces
// extra newlines on web. So it's a bug in that core lib.
context.logger = create_emscripten_logger()
web_context = context
game.game_init_window()
game.game_init()
}
@export
main_update :: proc "c" () -> bool {
context = web_context
game.game_update()
return game.game_should_run()
}
@export
main_end :: proc "c" () {
context = web_context
game.game_shutdown()
game.game_shutdown_window()
}
@export
web_window_size_changed :: proc "c" (w: c.int, h: c.int) {
context = web_context
game.game_parent_window_size_changed(int(w), int(h))
}

12
source/utils.odin Normal file
View file

@ -0,0 +1,12 @@
// Wraps os.read_entire_file and os.write_entire_file, but they also work with emscripten.
package game
@(require_results)
read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) {
return _read_entire_file(name, allocator, loc)
}
write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) {
return _write_entire_file(name, data, truncate)
}

16
source/utils_default.odin Normal file
View file

@ -0,0 +1,16 @@
#+build !wasm32
#+build !wasm64p32
package game
import "core:os"
_read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) {
err: os.Error
data, err = os.read_entire_file(name, allocator, loc)
return data, err == nil
}
_write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (err: bool) {
return os.write_entire_file(name, data, truncate = truncate) == nil
}

109
source/utils_web.odin Normal file
View file

@ -0,0 +1,109 @@
// Implementations of `read_entire_file` and `write_entire_file` using the libc
// stuff emscripten exposes. You can read the files that get bundled by
// `--preload-file assets` in `build_web` script.
#+build wasm32, wasm64p32
package game
import "base:runtime"
import "core:log"
import "core:c"
import "core:strings"
// These will be linked in by emscripten.
@(default_calling_convention = "c")
foreign {
fopen :: proc(filename, mode: cstring) -> ^FILE ---
fseek :: proc(stream: ^FILE, offset: c.long, whence: Whence) -> c.int ---
ftell :: proc(stream: ^FILE) -> c.long ---
fclose :: proc(stream: ^FILE) -> c.int ---
fread :: proc(ptr: rawptr, size: c.size_t, nmemb: c.size_t, stream: ^FILE) -> c.size_t ---
fwrite :: proc(ptr: rawptr, size: c.size_t, nmemb: c.size_t, stream: ^FILE) -> c.size_t ---
}
@(private="file")
FILE :: struct {}
Whence :: enum c.int {
SET,
CUR,
END,
}
// Similar to raylib's LoadFileData
_read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) {
if name == "" {
log.error("No file name provided")
return
}
file := fopen(strings.clone_to_cstring(name, context.temp_allocator), "rb")
if file == nil {
log.errorf("Failed to open file %v", name)
return
}
defer fclose(file)
fseek(file, 0, .END)
size := ftell(file)
fseek(file, 0, .SET)
if size <= 0 {
log.errorf("Failed to read file %v", name)
return
}
data_err: runtime.Allocator_Error
data, data_err = make([]byte, size, allocator, loc)
if data_err != nil {
log.errorf("Error allocating memory: %v", data_err)
return
}
read_size := fread(raw_data(data), 1, c.size_t(size), file)
if read_size != c.size_t(size) {
log.warnf("File %v partially loaded (%i bytes out of %i)", name, read_size, size)
}
log.debugf("Successfully loaded %v", name)
return data, true
}
// Similar to raylib's SaveFileData.
//
// Note: This can save during the current session, but I don't think you can
// save any data between sessions. So when you close the tab your saved files
// are gone. Perhaps you could communicate back to emscripten and save a cookie.
// Or communicate with a server and tell it to save data.
_write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) {
if name == "" {
log.error("No file name provided")
return
}
file := fopen(strings.clone_to_cstring(name, context.temp_allocator), truncate ? "wb" : "ab")
defer fclose(file)
if file == nil {
log.errorf("Failed to open '%v' for writing", name)
return
}
bytes_written := fwrite(raw_data(data), 1, len(data), file)
if bytes_written == 0 {
log.errorf("Failed to write file %v", name)
return
} else if bytes_written != len(data) {
log.errorf("File partially written, wrote %v out of %v bytes", bytes_written, len(data))
return
}
log.debugf("File written successfully: %v", name)
return true
}