A game engine for top-down 2D RPG games.
rpg game-engine raylib c99

Implement LZMA (XZ) compression

+2
.gitignore
··· 1 1 /build/ 2 + 3 + /data.bak/
+3
include/keraforge.h
··· 3 3 4 4 5 5 #include <keraforge/_header.h> 6 + 7 + #include <keraforge/log.h> 8 + 6 9 #include <keraforge/actor.h> 7 10 #include <keraforge/error.h> 8 11 #include <keraforge/fs.h>
-32
include/keraforge/_header.h
··· 16 16 #endif 17 17 18 18 19 - #ifdef KF_SANITY_CHECKS 20 - # include <stdio.h> /* fprintf, stderr */ 21 - # include <stdlib.h> /* exit */ 22 - # define KF_SANITY_CHECK(EXPR, ...) \ 23 - do \ 24 - { \ 25 - if (!(EXPR)) \ 26 - { \ 27 - fprintf(stderr, "\x1b[1;31msanity check failed: \x1b[0;34m%s:%d\x1b[0m in \x1b[34m%s: \x1b[0m", __FILE__, __LINE__, __FUNCTION_NAME__); \ 28 - fprintf(stderr, __VA_ARGS__); \ 29 - fprintf(stderr, "\n"); \ 30 - kf_printbacktrace(stderr); \ 31 - exit(1); \ 32 - } \ 33 - } \ 34 - while (0) 35 - # define KF_UNREACHABLE(...) \ 36 - do \ 37 - { \ 38 - fprintf(stderr, "\x1b[1;31munreachable: \x1b[0;34m%s:%d\x1b[0m in \x1b[34m%s: \x1b[0m", __FILE__, __LINE__, __FUNCTION_NAME__); \ 39 - fprintf(stderr, __VA_ARGS__); \ 40 - fprintf(stderr, "\n"); \ 41 - kf_printbacktrace(stderr); \ 42 - exit(1); \ 43 - } \ 44 - while (0) 45 - #else 46 - # define KF_SANITY_CHECK(EXPR, ...) do { ; } while (0) 47 - # define KF_UNREACHABLE(...) do { ; } while (0) 48 - #endif 49 - 50 - 51 19 typedef int8_t i8; 52 20 typedef int16_t i16; 53 21 typedef int32_t i32;
+5
include/keraforge/fs.h
··· 13 13 /* Write binary file contents. */ 14 14 int kf_writebin(char *filename, u8 *data, size_t len); 15 15 16 + /* Compress a file using LZMA (XZ). */ 17 + int kf_compress(char *infile, char *outfile); 18 + /* Decompress a file using LZMA (XZ). */ 19 + int kf_decompress(char *infile, char *outfile); 20 + 16 21 17 22 #endif
+65
include/keraforge/log.h
··· 1 + #ifndef __kf_log__ 2 + #define __kf_log__ 3 + 4 + #include <stdio.h> /* fprintf, stderr */ 5 + #include <stdlib.h> /* exit */ 6 + #include <stdarg.h> 7 + 8 + void kf_vlog(char *level, char *fmt, va_list va); 9 + void kf_log(char *level, char *fmt, ...); 10 + void kf_logdbg(char *fmt, ...); 11 + void kf_loginfo(char *fmt, ...); 12 + void kf_logerr(char *fmt, ...); 13 + 14 + /* Errors */ 15 + 16 + /* Throw a formatted error message without printing a traceback or exiting. */ 17 + #define KF_THROWSOFTER(MSG, ...) \ 18 + kf_logerr("\x1b[0;34m%s:%d\x1b[0m in \x1b[34m%s: \x1b[0m" MSG, __FILE__, __LINE__, __FUNCTION_NAME__, __VA_ARGS__) 19 + 20 + /* Throw a formatted error message and print a traceback if available. */ 21 + #define KF_THROWSOFT(MSG, ...) \ 22 + do \ 23 + { \ 24 + kf_logerr("\x1b[0;34m%s:%d\x1b[0m in \x1b[34m%s: \x1b[0m" MSG, __FILE__, __LINE__, __FUNCTION_NAME__, __VA_ARGS__); \ 25 + kf_printbacktrace(stderr); \ 26 + } \ 27 + while (0) 28 + 29 + /* Throw a formatted error message, print a traceback if available, and aborts. */ 30 + #define KF_THROW(MSG, ...) \ 31 + do \ 32 + { \ 33 + kf_logerr("\x1b[0;34m%s:%d\x1b[0m in \x1b[34m%s: \x1b[0m" MSG, __FILE__, __LINE__, __FUNCTION_NAME__ __VA_OPT__(,) __VA_ARGS__); \ 34 + kf_printbacktrace(stderr); \ 35 + abort(); \ 36 + } \ 37 + while (0) 38 + 39 + /* Sanity Checking */ 40 + 41 + #ifdef KF_SANITY_CHECKS 42 + /* Indicate that the given expression should never resolve to false and throw an error if it manages to. */ 43 + # define KF_SANITY_CHECK(EXPR, MSG, ...) \ 44 + do \ 45 + { \ 46 + if (!(EXPR)) \ 47 + { \ 48 + KF_THROW("sanity check failed: \x1b[0;34m%s:%d\x1b[0m in \x1b[34m%s:\x1b[0m " MSG, __FILE__, __LINE__, __FUNCTION_NAME__ __VA_OPT__(,) __VA_ARGS__); \ 49 + } \ 50 + } \ 51 + while (0) 52 + 53 + /* Indicate that this branch of code should never be reached. Throws an error if it ever manages to. */ 54 + # define KF_UNREACHABLE(MSG, ...) \ 55 + do \ 56 + { \ 57 + KF_THROW("unreachable: \x1b[0;34m%s:%d\x1b[0m in \x1b[34m%s:\x1b[0m " MSG, __FILE__, __LINE__, __FUNCTION_NAME__ __VA_OPT__(,) __VA_ARGS__); \ 58 + } \ 59 + while (0) 60 + #else 61 + # define KF_SANITY_CHECK(EXPR, MSG, ...) do { ; } while (0) 62 + # define KF_UNREACHABLE(MSG, ...) do { ; } while (0) 63 + #endif 64 + 65 + #endif
+5
include/keraforge/world.h
··· 104 104 /* Draw the part of the world visible to the given camera. */ 105 105 void kf_world_draw(struct kf_world *world, Camera2D camera); 106 106 107 + /* Save a world to map.bin(.xz). */ 108 + int kf_world_save(struct kf_world *world, bool compress); 109 + /* Load a world from a map.bin(.xz). */ 110 + int kf_world_load(struct kf_world **world, bool compressed); 111 + 107 112 #endif
+1 -1
scripts/_config.sh
··· 11 11 export KF_DEBUG_LFLAGS="-g -rdynamic" 12 12 13 13 export CFLAGS="-Wall -Wextra -Werror -std=c99 -Iinclude/ -c -DKF_GNU $KF_DEBUG_CFLAGS" 14 - export LFLAGS="-lraylib -lm -lGL -lpthread -ldl -lrt -lX11 $KF_DEBUG_LFLAGS" 14 + export LFLAGS="-lraylib -lm -lGL -lpthread -ldl -lrt -lX11 -llzma $KF_DEBUG_LFLAGS"
+27
scripts/build-tools.sh
··· 1 + #!/usr/bin/env sh 2 + set -e 3 + 4 + . scripts/_config.sh 5 + 6 + mkdir -p build/tools/ 7 + 8 + echo ": compiling keraforge" 9 + for f in `find src/ -name '*.c'` 10 + do 11 + ff=$(echo "$f" | tr '/' '_') 12 + gcc $CFLAGS $f -o build/${ff%.c}.o 13 + done 14 + 15 + echo ": compiling tools" 16 + for f in `find tools/ -name '*.c'` 17 + do 18 + ff=$(echo "$f" | tr '/' '_') 19 + o=build/tools/${ff%.c}.o 20 + echo ": tool: $f->build/tools/${ff%.c}" 21 + gcc $CFLAGS $f -o $o 22 + gcc \ 23 + -o build/tools/${ff%.c} \ 24 + `find build -maxdepth 1 -name '*.o' ! -name '*src_main.o'` \ 25 + $o \ 26 + $LFLAGS 27 + done
+127
src/fs.c
··· 1 1 #include <keraforge.h> 2 2 #include <stdio.h> 3 3 #include <stdlib.h> 4 + #include <string.h> 5 + #include <errno.h> 6 + #include <lzma.h> 4 7 5 8 6 9 int kf_exists(char *filename) ··· 49 52 50 53 return 1; 51 54 } 55 + 56 + static 57 + int _kf_compress(lzma_stream *s, FILE *ifp, FILE *ofp) 58 + { 59 + lzma_action a = LZMA_RUN; 60 + u8 inbuf[BUFSIZ], outbuf[BUFSIZ]; 61 + 62 + s->next_in = NULL; 63 + s->avail_in = 0; 64 + s->next_out = outbuf; 65 + s->avail_out = sizeof(outbuf); 66 + 67 + for (;;) 68 + { 69 + if (s->avail_in == 0 && !feof(ifp)) 70 + { 71 + s->next_in = inbuf; 72 + s->avail_in = fread(inbuf, 1, sizeof(inbuf), ifp); 73 + 74 + if (ferror(ifp)) 75 + { 76 + KF_THROW("read error: %s", strerror(errno)); 77 + return 0; /* unreachable */ 78 + } 79 + 80 + if (feof(ifp)) 81 + a = LZMA_FINISH; 82 + } 83 + 84 + lzma_ret r = lzma_code(s, a); 85 + 86 + if (s->avail_out == 0 || r == LZMA_STREAM_END) 87 + { 88 + size_t nwrote = sizeof(outbuf) - s->avail_out; 89 + 90 + if (fwrite(outbuf, 1, nwrote, ofp) != nwrote) 91 + { 92 + KF_THROW("write error: %s", strerror(errno)); 93 + return 0; /* unreachable */ 94 + } 95 + 96 + s->next_out = outbuf; 97 + s->avail_out = sizeof(outbuf); 98 + } 99 + 100 + if (r != LZMA_OK) 101 + { 102 + if (r == LZMA_STREAM_END) 103 + return 1; 104 + 105 + KF_THROW("lzma compression error: code=%d", r); 106 + return 0; /* unreachable */ 107 + } 108 + } 109 + 110 + KF_UNREACHABLE("broke out of for(;;) loop"); 111 + return 0; /* unreachable */ 112 + } 113 + 114 + int kf_compress(char *infile, char *outfile) 115 + { 116 + FILE *ifp = fopen(infile, "r"); 117 + if (!ifp) 118 + { 119 + KF_THROW("failed to open input file: %s", infile); 120 + return 0; /* unreachable */ 121 + } 122 + FILE *ofp = fopen(outfile, "w"); 123 + if (!ofp) 124 + { 125 + fclose(ifp); 126 + KF_THROW("failed to open output file: %s", outfile); 127 + return 0; /* unreachable */ 128 + } 129 + 130 + lzma_stream s = LZMA_STREAM_INIT; 131 + lzma_ret r = lzma_easy_encoder(&s, LZMA_PRESET_DEFAULT, LZMA_CHECK_CRC64); 132 + if (r != LZMA_OK) 133 + { 134 + KF_THROW("failed to initialize lzma encoder: code=%d\n", r); 135 + return 0; /* unreachable */ 136 + } 137 + 138 + int res = _kf_compress(&s, ifp, ofp); 139 + 140 + lzma_end(&s); 141 + fclose(ifp); 142 + fclose(ofp); 143 + 144 + return res; 145 + } 146 + 147 + int kf_decompress(char *infile, char *outfile) 148 + { 149 + FILE *ifp = fopen(infile, "r"); 150 + if (!ifp) 151 + { 152 + KF_THROW("failed to open input file: %s", infile); 153 + return 0; /* unreachable */ 154 + } 155 + FILE *ofp = fopen(outfile, "w"); 156 + if (!ofp) 157 + { 158 + fclose(ifp); 159 + KF_THROW("failed to open output file: %s", outfile); 160 + return 0; /* unreachable */ 161 + } 162 + 163 + lzma_stream s = LZMA_STREAM_INIT; 164 + lzma_ret r = lzma_stream_decoder(&s, UINT64_MAX, LZMA_CONCATENATED); 165 + if (r != LZMA_OK) 166 + { 167 + KF_THROW("failed to initialize lzma decoder: code=%d\n", r); 168 + return 0; /* unreachable */ 169 + } 170 + 171 + int res = _kf_compress(&s, ifp, ofp); 172 + 173 + lzma_end(&s); 174 + fclose(ifp); 175 + fclose(ofp); 176 + 177 + return res; 178 + }
+41
src/log.c
··· 1 + #include <keraforge.h> 2 + 3 + void kf_vlog(char *level, char *fmt, va_list va) 4 + { 5 + fprintf(stderr, "\x1b[0;1m-> %s\x1b[0;1m:\x1b[0m ", level); 6 + vfprintf(stderr, fmt, va); 7 + fprintf(stderr, "\n"); 8 + } 9 + 10 + void kf_log(char *level, char *fmt, ...) 11 + { 12 + va_list va; 13 + va_start(va, fmt); 14 + kf_vlog(level, fmt, va); 15 + va_end(va); 16 + } 17 + 18 + void kf_logdbg(char *fmt, ...) 19 + { 20 + va_list va; 21 + va_start(va, fmt); 22 + kf_vlog("\x1b[1;33mdbg", fmt, va); 23 + va_end(va); 24 + } 25 + 26 + void kf_loginfo(char *fmt, ...) 27 + { 28 + va_list va; 29 + va_start(va, fmt); 30 + kf_vlog("\x1b[1;34minfo", fmt, va); 31 + va_end(va); 32 + } 33 + 34 + void kf_logerr(char *fmt, ...) 35 + { 36 + va_list va; 37 + va_start(va, fmt); 38 + kf_vlog("\x1b[1;31merr", fmt, va); 39 + va_end(va); 40 + } 41 +
+15 -33
src/main.c
··· 1 - #include "keraforge/input.h" 2 - #include "keraforge/sprites.h" 3 - #include "keraforge/world.h" 4 1 #include <keraforge.h> 5 2 #include <raylib.h> 6 3 #include <raymath.h> ··· 22 19 } modal; 23 20 static char *modals[] = { "play", "edit" }; 24 21 static bool dirty = false; 22 + static bool preserve_mapdotbin = false; 25 23 26 24 static kf_inputbind_t 27 25 inputbind_move_up, ··· 96 94 { 97 95 case 0: self->pointing = kf_east; break; 98 96 case 1: self->pointing = kf_north; break; 99 - case -2: 97 + case -2: /* fallthrough */ 100 98 case 2: self->pointing = kf_west; break; 101 99 case -1: self->pointing = kf_south; break; 102 100 } ··· 186 184 (void)argc; 187 185 (void)argv; 188 186 189 - // SetTraceLogLevel(LOG_WARNING); 187 + SetTraceLogLevel(LOG_WARNING); 190 188 InitWindow(800, 600, "Keraforge"); 191 189 SetTargetFPS(target_fps); 192 190 SetExitKey(KEY_NULL); ··· 225 223 .sprite = {8, 0}, 226 224 .collide = true, 227 225 ); 228 - printf("loaded %d tiles\n", kf_tiles.count); 226 + kf_logdbg("loaded %d tiles", kf_tiles.count); 229 227 230 228 struct kf_uiconfig *uiconfig = kf_ui_getconfig(); 231 229 uiconfig->select = inputbind_select; ··· 237 235 MakeDirectory("data"); 238 236 239 237 struct kf_world *world = NULL; 240 - if (!kf_exists("data/map.bin")) 241 - { 242 - printf("-> creating world\n"); 243 - world = kf_world_new(4096, 4096, 2); 244 - printf("-> saving world\n"); 245 - size_t len = kf_world_getsize(world); 246 - printf("-> writing of %lu bytes\n", len); 247 - if (!kf_writebin("data/map.bin", (u8 *)world, len)) 248 - { 249 - fprintf(stderr, "error creating map: failed to save map.bin\n"); 250 - free(world); 251 - exit(1); 252 - } 253 - } 254 - else 255 - { 256 - printf("-> loading world\n"); 257 - size_t len = 0; 258 - world = (struct kf_world *)kf_readbin("data/map.bin", &len); 259 - printf("-> world is %lu bytes\n", len); 260 - } 238 + kf_world_load(&world, true); 261 239 if (!world) 262 - { 263 - fprintf(stderr, "error: failed to load world\n"); 264 - exit(1); 265 - } 240 + KF_THROW("failed to load world: %p", world); 266 241 267 242 struct kf_actor *player = kf_actor_new(kf_actor_loadspritesheet("data/res/img/char/whom.png"), 10, 10, true); 268 243 player->sizeoffset.y = 6; ··· 368 343 369 344 if (world) 370 345 { 371 - if (dirty && !kf_writebin("data/map.bin", (u8 *)world, kf_world_getsize(world))) 372 - fprintf(stderr, "error: failed to save map.bin\n"); 346 + if (dirty) 347 + { 348 + if (!kf_writebin("data/tmp/map.bin", (u8 *)world, kf_world_getsize(world))) 349 + KF_THROW("failed to save map.bin"); 350 + if (!kf_compress("data/tmp/map.bin", "data/map.bin.xz")) 351 + KF_THROW("failed to compress map.bin into map.bin.xz"); 352 + if (!preserve_mapdotbin) 353 + remove("data/tmp/map.bin"); 354 + } 373 355 free(world); 374 356 } 375 357 CloseWindow();
+69
src/world.c
··· 1 + #include "keraforge/fs.h" 1 2 #include <keraforge.h> 2 3 #include <raylib.h> 3 4 #include <stdio.h> ··· 213 214 tile += down; /* shift tile pointer down */ 214 215 } 215 216 } 217 + 218 + #define _KF_MAPFILE_TMP "data/tmp/map.bin" 219 + #define _KF_MAPFILE_XZ "data/map.bin.xz" 220 + #define _KF_MAPFILE "data/map.bin" 221 + 222 + int kf_world_save(struct kf_world *world, bool compress) 223 + { 224 + char *outfile = compress ? _KF_MAPFILE_TMP : _KF_MAPFILE; 225 + if (!kf_writebin(outfile, (u8 *)world, kf_world_getsize(world))) 226 + { 227 + KF_THROW("failed to write to %s", outfile); 228 + return 0; /* unreachable */ 229 + } 230 + 231 + if (compress) 232 + { 233 + if (!kf_compress(_KF_MAPFILE_TMP, _KF_MAPFILE_XZ)) 234 + { 235 + KF_THROW("failed to compress %s", _KF_MAPFILE_XZ); 236 + return 0; /* unreachable */ 237 + } 238 + /* we don't need this anymore, might as well toss it to save file storage. */ 239 + remove(_KF_MAPFILE_TMP); 240 + } 241 + 242 + return 1; 243 + } 244 + 245 + int kf_world_load(struct kf_world **pworld, bool compressed) 246 + { 247 + if (compressed) /* decompress before loading */ 248 + { 249 + if (!kf_exists(_KF_MAPFILE_XZ)) 250 + { 251 + KF_THROW("no such file: %s", _KF_MAPFILE_XZ); 252 + return 0; /* unreachable */ 253 + } 254 + 255 + kf_logdbg("decompressing %s to %s", _KF_MAPFILE_XZ, _KF_MAPFILE_TMP); 256 + if (!kf_decompress(_KF_MAPFILE_XZ, _KF_MAPFILE_TMP)) 257 + { 258 + KF_THROW("failed to decompress %s", _KF_MAPFILE_XZ); 259 + return 0; /* unreachable */ 260 + } 261 + } 262 + 263 + char *infile = compressed ? _KF_MAPFILE_TMP : _KF_MAPFILE; 264 + kf_logdbg("loading world: %s", infile); 265 + 266 + size_t len = 0; 267 + *pworld = (struct kf_world *)kf_readbin(infile, &len); 268 + kf_logdbg("loaded world (%p): r=%d, wh=%dx%d, len=%lu", pworld, (*pworld)->revision, (*pworld)->width, (*pworld)->height, len); 269 + 270 + if (compressed) 271 + { 272 + /* we don't need this anymore, might as well toss it to save file storage. */ 273 + remove(_KF_MAPFILE_TMP); 274 + } 275 + 276 + if (!pworld) 277 + { 278 + KF_THROW("failed to load world"); 279 + return 0; /* unreachable */ 280 + } 281 + 282 + return 1; 283 + } 284 +
+3 -1
todo
··· 11 11 . World 12 12 / Tiles 13 13 . Actors 14 - . Compression (perhaps https://github.com/google/brotli/) 14 + x Compression 15 + . Compress without saving the world binary as an intermediate step. 16 + All I need to do for this is adapt the compression functions to have an in-memory (`u8 *`) compression equivalent instead of just `FILE *` compression. 15 17 . Dialogue 16 18 . Quests 17 19 . Combat
+100
tools/newgame.c
··· 1 + #include <keraforge.h> 2 + #include <raylib.h> 3 + #include <stdio.h> 4 + #include <stdlib.h> 5 + #include <string.h> 6 + 7 + 8 + static const char *HELP = 9 + "usage: newgame [options...]\n\n" 10 + "options:\n" 11 + "\t-w --width <int> specify width for the world (default: 1024)\n" 12 + "\t-h --height <int> specify height for the world (default: 1024)\n" 13 + "\t-s --size <int> specify width and height for the world\n" 14 + "\t-p --path <str> specify path to save the game in (default: path)\n" 15 + "\t-f --force create the new game even if the directory exists, this will delete data\n" 16 + "\t --no-force opposite of -f (default)\n" 17 + "\t-c --compress compress the world after creating it (recommended)\n" 18 + "\t --no-compress don't compress the world after creating it (default)\n" 19 + "\t --help display this message\n" 20 + ; 21 + 22 + 23 + int main(int argc, char *argv[]) 24 + { 25 + char *path = "data"; 26 + int width = 1024, height = 1024; 27 + bool compress = false; 28 + bool force = false; 29 + 30 + for (int i = 1 ; i < argc ; i++) 31 + { 32 + char *arg = argv[i]; 33 + 34 + # define _checkshort(SHORT) (strncmp(arg, "-" SHORT, strlen("-" SHORT)) == 0) 35 + # define _checklong(LONG) (strncmp(arg, "--" LONG, strlen("--" LONG)) == 0) 36 + # define _check(SHORT, LONG) (_checkshort(SHORT) || _checklong(LONG)) 37 + 38 + if (_check("w", "width")) 39 + width = atoi(argv[++i]); 40 + else if (_check("h", "height")) 41 + height = atoi(argv[++i]); 42 + else if (_check("s", "size")) 43 + width = height = atoi(argv[++i]); 44 + else if (_check("p", "path")) 45 + path = argv[++i]; 46 + else if (_check("c", "compress")) 47 + compress = true; 48 + else if (_checklong("no-compress")) 49 + compress = false; 50 + else if (_check("f", "force")) 51 + force = true; 52 + else if (_checklong("no-force")) 53 + force = false; 54 + else if (_checklong("help")) 55 + { 56 + kf_loginfo("%s", HELP); 57 + exit(0); 58 + } 59 + else 60 + { 61 + kf_logerr("invalid argument: %s", arg); 62 + exit(1); 63 + } 64 + 65 + # undef _checkshort 66 + # undef _checklong 67 + # undef _check 68 + } 69 + 70 + if (!force && DirectoryExists(path)) 71 + KF_THROW("path exists: %s", path); 72 + 73 + struct kf_world *world = NULL; 74 + 75 + kf_loginfo("creating world"); 76 + world = kf_world_new(width, height, 2); 77 + 78 + /* path for our map.bin */ 79 + char worldpath[4096] = {0}; 80 + strcpy(worldpath, path); 81 + strcpy(&worldpath[0] + strlen(path), compress ? "/tmp/map.bin" : "/map.bin"); 82 + MakeDirectory(GetDirectoryPath(worldpath)); 83 + 84 + size_t len = kf_world_getsize(world); 85 + kf_loginfo("saving world (%lu bytes uncompressed)", len); 86 + if (!kf_writebin(worldpath, (u8 *)world, len)) 87 + KF_THROW("failed to save %s", worldpath); 88 + 89 + if (compress) 90 + { 91 + char worldxzpath[4096] = {0}; 92 + strcpy(worldxzpath, path); 93 + strcpy(&worldxzpath[0] + strlen(path), "/map.bin.xz"); 94 + if (!kf_compress(worldpath, worldxzpath)) 95 + KF_THROW("failed to compress %s", worldpath); 96 + remove(worldpath); /* no longer needed */ 97 + } 98 + 99 + free(world); 100 + }