чување 443cce90c457fd326e3c1042616795c3ba42e030
родитељ 96d2bd44ee81077181261f4115dbe336511a7a87
Аутор: Страхиња Радић <sr@strahinja.org>
Датум: Tue, 4 Aug 2026 16:54:55 +0000
Add hall of fame saving and loading
Diffstat:
измењених датотека: 7, додавања: 228(+), брисања: 12(-)
diff --git a/Makefile b/Makefile
@@ -6,7 +6,7 @@ include config.mk
.SUFFIXES: .c .o .h.in .h
.h.in.h:
- sh -c 'DATADIR=$(DATADIR) lib/replacepat $< $@'
+ sh -c 'DATADIR=$(DATADIR) USERDIR=$(USERDIR) lib/replacepat $< $@'
all: makedate makeversion $(PROG)
if [ -f .rebuild ]; then rm .rebuild; make -e all; else true; fi
@@ -25,7 +25,7 @@ makedate:
makeversion:
sh lib/makeversion -l
-hiscore.o: hiscore.c hiscore.h
+hiscore.o: hiscore.c hiscore.h local.h
linije.o: linije.c hiscore.h local.h version.h
diff --git a/TODO b/TODO
@@ -1,9 +1,9 @@
TODO
====
-[~] High score?
+[x] High score?
[x] Updating high score
- [ ] Saving high score to a file
- [ ] Loading high score from a file
+ [x] Saving high score to a file
+ [x] Loading high score from a file
< > Menu?
diff --git a/hiscore.c b/hiscore.c
@@ -3,11 +3,16 @@
* See the file LICENSE for exact copyright and license details. */
#include "hiscore.h"
+#include "local.h"
#include <SDL3/SDL.h>
#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/stat.h>
int
hs_add(struct HiScoreEntry** table, const char* name, const int name_len,
@@ -114,10 +119,11 @@ hs_init(struct HiScoreEntry** table)
goto hs_init_malloc_error;
for (p = *table; p < *table + HI_SCORE_MAX; p++)
{
- p->name = malloc(len);
+ /* Zero out for privacy and security */
+ p->name = calloc(1, HI_SCORE_NAME_MAX);
if (!p->name)
goto hs_init_malloc_error;
- memccpy(p->name, HI_SCORE_DEFAULT_NAME, 0, len);
+ memccpy(p->name, HI_SCORE_DEFAULT_NAME, 0, HI_SCORE_NAME_MAX);
p->name_len = len - 1;
p->score = 0;
}
@@ -128,3 +134,188 @@ hs_init_malloc_error:
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "malloc failed");
return 1;
}
+
+/* USERDIR not existing is ok, create it in that case */
+char*
+hs_get_hofdir(void)
+{
+ struct stat st;
+ static char* result = NULL;
+ char* home = NULL;
+ int result_size = PATH_MAX + 1;
+
+ if (!result)
+ result = malloc(result_size);
+ else
+ goto hs_get_hofdir_skip_generating;
+ if (!result)
+ goto hs_get_hofdir_error;
+
+ home = getenv("HOME");
+ if (!home || !*home)
+ goto hs_get_hofdir_no_home;
+
+ snprintf(result, result_size, "%s/%s", home, USERDIR);
+
+ if (stat(result, &st) == 0)
+ goto hs_get_hofdir_skip_generating;
+
+ if (errno == ENOENT || errno == ENOTDIR)
+ {
+ if (mkdir(result,
+ S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)
+ < 0)
+ goto hs_get_hofdir_error;
+ }
+ else
+ goto hs_get_hofdir_error;
+
+hs_get_hofdir_skip_generating:
+ return result;
+
+hs_get_hofdir_no_home:
+ SDL_LogError(SDL_LOG_CATEGORY_ERROR,
+ "hs_get_hofdir: HOME environment variable is unset or empty");
+ errno = ENOENT;
+ return NULL;
+
+hs_get_hofdir_error:
+ SDL_LogError(SDL_LOG_CATEGORY_ERROR, "hs_get_hofdir: %s",
+ strerror(errno));
+ return NULL;
+}
+
+/*
+ * File structure:
+ * HI_SCORE_MAX entries, each consisting of
+ *
+ * ,-----------------------------------------------------------.
+ * | name (HI_SCORE_NAME_MAX unsigned chars) |
+ * |-----------------------------------------------------------|
+ * | score 2-byte unsigned int BE |
+ * `-----------------------------------------------------------'
+ */
+int
+hs_load(struct HiScoreEntry** table, const char* hofdir)
+{
+ FILE* infile = NULL;
+ char* filename = NULL;
+ char name[HI_SCORE_NAME_MAX + 1];
+ int filename_size = PATH_MAX + 1;
+ int name_len, i;
+ unsigned int score;
+ uint8_t byte;
+
+ if (!hofdir)
+ goto hs_load_error;
+
+ filename = malloc(filename_size);
+ if (!filename)
+ goto hs_load_error;
+
+ snprintf(filename, filename_size, "%s/%s", hofdir, HOFFILE);
+
+ if (!(infile = fopen(filename, "rb")))
+ goto hs_load_error;
+
+ i = 0;
+ while (!feof(infile) && i < HI_SCORE_MAX)
+ {
+ /* Zero out for privacy and security */
+ memset(name, 0, HI_SCORE_NAME_MAX);
+ if (fread(name, HI_SCORE_NAME_MAX, 1, infile) < 1)
+ {
+ if (feof(infile))
+ continue;
+ else
+ {
+ fclose(infile);
+ goto hs_load_error;
+ }
+ }
+ name_len = strlen(name);
+ if (fread(&byte, 1, 1, infile) < 1)
+ {
+ fclose(infile);
+ goto hs_load_error;
+ }
+ score = byte << 8;
+ if (fread(&byte, 1, 1, infile) < 1)
+ {
+ fclose(infile);
+ goto hs_load_error;
+ }
+ score |= byte;
+ hs_add(table, name, name_len, score);
+ i++;
+ }
+
+ if (fclose(infile) == EOF)
+ goto hs_load_error;
+
+ free(filename);
+ return 0;
+
+hs_load_error:
+ SDL_LogError(SDL_LOG_CATEGORY_ERROR, "hs_load: %s", strerror(errno));
+ return 1;
+}
+
+int
+hs_write(struct HiScoreEntry* table, const char* hofdir)
+{
+ FILE* outfile = NULL;
+ struct HiScoreEntry* p = NULL;
+ char* filename = NULL;
+ int filename_size = PATH_MAX + 1;
+ int i;
+ uint8_t byte;
+
+ if (!hofdir)
+ goto hs_write_error;
+
+ filename = malloc(filename_size);
+ if (!filename)
+ goto hs_write_error;
+
+ snprintf(filename, filename_size, "%s/%s", hofdir, HOFFILE);
+
+ if (!(outfile = fopen(filename, "wb")))
+ goto hs_write_error;
+
+ for (p = table; p < table + HI_SCORE_MAX; p++)
+ {
+ for (i = 0; i < HI_SCORE_NAME_MAX; i++)
+ {
+ errno = 0;
+ byte = p->name[i];
+ if (fwrite(&byte, 1, 1, outfile) < 1)
+ {
+ fclose(outfile);
+ goto hs_write_error;
+ }
+ }
+ errno = 0;
+ byte = HIBYTE(p->score);
+ if (fwrite(&byte, 1, 1, outfile) < 1)
+ {
+ fclose(outfile);
+ goto hs_write_error;
+ }
+ byte = LOBYTE(p->score);
+ if (fwrite(&byte, 1, 1, outfile) < 1)
+ {
+ fclose(outfile);
+ goto hs_write_error;
+ }
+ }
+ if (fclose(outfile) == EOF)
+ goto hs_write_error;
+
+ free(filename);
+ return 0;
+
+hs_write_error:
+ SDL_LogError(SDL_LOG_CATEGORY_ERROR, "hs_write: %s", strerror(errno));
+ return 1;
+}
diff --git a/hiscore.h b/hiscore.h
@@ -6,6 +6,13 @@
#define HI_SCORE_MAX 10
#define HI_SCORE_NAME_MAX 40
+#ifndef PATH_MAX
+#define PATH_MAX _POSIX_PATH_MAX
+#endif
+
+#define HIBYTE(i) ((uint8_t)(((i) & 0xFF00) >> 8))
+#define LOBYTE(i) ((uint8_t)(((i) & 0xFF)))
+
struct HiScoreEntry {
char* name;
int name_len;
@@ -18,4 +25,7 @@ int hs_assign(const struct HiScoreEntry* source, struct HiScoreEntry* dest);
int hs_assign_ss(const struct HiScoreEntry* source, struct HiScoreEntry* dest);
int hs_assign_ps(const char* name, const int name_len, const unsigned int score,
struct HiScoreEntry* dest);
+char* hs_get_hofdir(void);
int hs_init(struct HiScoreEntry** table);
+int hs_load(struct HiScoreEntry** table, const char* hofdir);
+int hs_write(struct HiScoreEntry* table, const char* hofdir);
diff --git a/lib/replacepat b/lib/replacepat
@@ -6,5 +6,5 @@ DATE=$(cat date 2>/dev/null)
VERSION=$(cat version 2>/dev/null)
sed -e 's|%VERSION%|'"$VERSION"'|g' -e 's|%DATE%|'"$DATE"'|g' \
- -e 's|%DATADIR%|'"$DATADIR"'|g' "$infile" \
- > "$outfile"
+ -e 's|%DATADIR%|'"$DATADIR"'|g' -e 's|%USERDIR%|'"$USERDIR"'|g' \
+ "$infile" > "$outfile"
diff --git a/linije.c b/linije.c
@@ -1265,6 +1265,13 @@ input_name_callback(struct State* state)
state->player_name[i]);
hs_add(&state->hall_of_fame, state->player_name,
state->player_name_length, state->score);
+
+ if (hs_write(state->hall_of_fame, hs_get_hofdir()))
+ {
+ state->running = 1;
+ return;
+ }
+
prepare_dialog(state, GAME_OVER_DIALOG_WIDTH, GAME_OVER_DIALOG_HEIGHT,
game_over_text, 0, 1);
}
@@ -2218,6 +2225,12 @@ main(int argc, char** argv)
init_state(&state);
hs_init(&hof);
+ if (hs_load(&hof, hs_get_hofdir()))
+ {
+ SDL_LogError(SDL_LOG_CATEGORY_ERROR, "hs_load: %s",
+ strerror(errno));
+ exit(1);
+ }
state.hall_of_fame = hof;
diff --git a/local.h.in b/local.h.in
@@ -2,6 +2,8 @@
* any later version. Copyright (C) 2025-2026 Страхиња Радић.
* See the file LICENSE for exact copyright and license details. */
-#define DATADIR "%DATADIR%"
-#define TER12_PNG "ter12.png"
-#define SPRITES_PNG "sprites.png"
+#define DATADIR "%DATADIR%"
+#define USERDIR "%USERDIR%"
+#define TER12_PNG "ter12.png"
+#define SPRITES_PNG "sprites.png"
+#define HOFFILE "linije.hof"