CHGame 0.3.0
Libraries for the CHGame handheld
Loading...
Searching...
No Matches
Save.h
Go to the documentation of this file.
1/// @file Save.h
2/// @brief Saving: a record in flash that survives power cycles and re-uploads.
3///
4/// CHGame has no EEPROM. The bootloader erases only the flash pages a new
5/// sketch occupies, so the last two pages of the application region
6/// (0xF500, 0xF600) survive. A record is a header, the game's own data and a
7/// CRC; saves take turns between the pages, so a power cut in the middle of
8/// one loses only that save. Every game shares the two pages and tells its
9/// records apart by a magic number of its own (each game needs a different
10/// one). If the sketch grows into the pages, saving switches itself off
11/// rather than overwrite code: an image up to 50,432 B keeps both pages, up
12/// to 50,688 B one.
13#pragma once
14#include <stdint.h>
15
16/// @defgroup chgame_save Saving
17/// @ingroup lib_chgame
18/// @brief save::: a game's options and progress in flash, with a CRC, kept
19/// across power cycles and re-uploads.
20///
21/// @code
22/// struct SaveData { Options opt; Stats stats; }; // up to 244 bytes
23/// const uint32_t MAGIC = save::magic("CHXX");
24///
25/// SaveData d;
26/// if (save::load(MAGIC, 1, d)) { ... } // at start-up
27/// save::store(MAGIC, 1, d); // after gfx_wait()
28/// @endcode
29///
30/// Change the version when SaveData changes shape: old records are then
31/// ignored instead of misread. A flag byte travels in the header (most of
32/// the casino games mark "a game in progress" with it).
33///
34/// The page is built in CHGfx's chunk scratch, so store() must run between
35/// gfx_wait() and the next flush. read() points straight into flash: a game
36/// can take just the part it needs.
37///
38/// @note Load once at start-up, before the first store: the read remembers
39/// the newest record's sequence number, and the next save follows it.
40/// @{
41
42/// @brief Saved records: load, store, and the raw read/write underneath.
43namespace save {
44
45/// @brief The most data a record holds: a 256-byte page less the header and CRC.
46static const uint16_t MAX_DATA = 256 - 12; // a page less the header and CRC
47
48/// @brief Four characters as a magic number ("CHCR" -> 0x52434843).
49/// @param s Exactly four characters, e.g. `save::magic("CHXX")`. Each game
50/// needs its own: the pages are shared by everything ever uploaded.
51/// @return The magic number (computed at compile time).
52constexpr uint32_t magic(const char (&s)[5]) {
53 return (uint32_t)(uint8_t)s[0] | (uint32_t)(uint8_t)s[1] << 8 |
54 (uint32_t)(uint8_t)s[2] << 16 | (uint32_t)(uint8_t)s[3] << 24;
55}
56
57/// @brief Whether saving works in this image.
58/// @return false if the image is too big (it reaches into the save pages), or
59/// a write did not verify.
60bool available(); // false: the image is too big, or a write failed
61
62/// @brief Find the newest valid record.
63/// @param magic The game's magic number.
64/// @param version The record's version: records of another version are ignored.
65/// @param size The data's size in bytes: records of another size are ignored.
66/// @param flag Optional: gets the header's flag byte.
67/// @return A pointer to its data, in flash (read-only), or nullptr if there is
68/// no record with this magic, version and size.
69const void *read(uint32_t magic, uint8_t version, uint16_t size, uint8_t *flag = nullptr);
70/// @brief The page buffer to fill before write(): writing in place.
71/// @return MAX_DATA bytes, zeroed, in the chunk scratch (so only between
72/// gfx_wait() and the next flush).
73void *buffer();
74/// @brief Write what is in buffer() as a new record, to the older page.
75/// @param magic The game's magic number.
76/// @param version The record's version.
77/// @param size How many bytes of buffer() to keep (at most MAX_DATA).
78/// @param flag A byte for the header (read back by read()'s flag).
79/// @return true if the page was written and verified; false if saving is not
80/// available or the write failed (then available() turns false).
81bool write(uint32_t magic, uint8_t version, uint16_t size, uint8_t flag = 0);
82
83/// @brief Load a struct from the newest valid record.
84/// @param magic The game's magic number.
85/// @param version The record's version.
86/// @param data Gets a copy of the record; left as it was if there is none.
87/// @param flag Optional: gets the header's flag byte.
88/// @return true if a record was found.
89template <class T> bool load(uint32_t magic, uint8_t version, T &data, uint8_t *flag = nullptr) {
90 static_assert(sizeof(T) <= MAX_DATA, "save data must fit one flash page");
91 const T *p = (const T *)read(magic, version, sizeof(T), flag);
92 if (!p) return false;
93 data = *p;
94 return true;
95}
96/// @brief Save a struct as a new record. Call between gfx_wait() and the next flush.
97/// @param magic The game's magic number.
98/// @param version The record's version.
99/// @param data What to save (at most MAX_DATA bytes; checked at compile time).
100/// @param flag A byte for the header.
101/// @return true if it was written and verified.
102template <class T> bool store(uint32_t magic, uint8_t version, const T &data, uint8_t flag = 0) {
103 static_assert(sizeof(T) <= MAX_DATA, "save data must fit one flash page");
104 if (!available()) return false;
105 *(T *)buffer() = data;
106 return write(magic, version, sizeof(T), flag);
107}
108
109} // namespace save
110
111/// @}
Saved records: load, store, and the raw read/write underneath.
Definition Save.h:43
bool store(uint32_t magic, uint8_t version, const T &data, uint8_t flag=0)
Save a struct as a new record.
Definition Save.h:102
constexpr uint32_t magic(const char(&s)[5])
Four characters as a magic number ("CHCR" -> 0x52434843).
Definition Save.h:52
bool load(uint32_t magic, uint8_t version, T &data, uint8_t *flag=nullptr)
Load a struct from the newest valid record.
Definition Save.h:89
static const uint16_t MAX_DATA
The most data a record holds: a 256-byte page less the header and CRC.
Definition Save.h:46
void * buffer()
The page buffer to fill before write(): writing in place.
bool available()
Whether saving works in this image.
const void * read(uint32_t magic, uint8_t version, uint16_t size, uint8_t *flag=nullptr)
Find the newest valid record.
bool write(uint32_t magic, uint8_t version, uint16_t size, uint8_t flag=0)
Write what is in buffer() as a new record, to the older page.