diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 2151d26b3..bc2fc30bc 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -54,6 +54,8 @@ add_library(core mdec.h memory_card.cpp memory_card.h + memory_card_image.cpp + memory_card_image.h namco_guncon.cpp namco_guncon.h negcon.cpp diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 706bb946a..e0121765a 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -76,6 +76,7 @@ + @@ -125,6 +126,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index ae512e65e..84d774ca5 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -48,6 +48,7 @@ + @@ -99,5 +100,6 @@ + \ No newline at end of file diff --git a/src/core/memory_card.cpp b/src/core/memory_card.cpp index cdcb9505f..60666c63e 100644 --- a/src/core/memory_card.cpp +++ b/src/core/memory_card.cpp @@ -107,7 +107,7 @@ bool MemoryCard::Transfer(const u8 data_in, u8* data_out) case State::ReadData: { - const u8 bits = m_data[ZeroExtend32(m_address) * SECTOR_SIZE + m_sector_offset]; + const u8 bits = m_data[ZeroExtend32(m_address) * MemoryCardImage::FRAME_SIZE + m_sector_offset]; if (m_sector_offset == 0) { Log_DevPrintf("Reading memory card sector %u", ZeroExtend32(m_address)); @@ -122,7 +122,7 @@ bool MemoryCard::Transfer(const u8 data_in, u8* data_out) ack = true; m_sector_offset++; - if (m_sector_offset == SECTOR_SIZE) + if (m_sector_offset == MemoryCardImage::FRAME_SIZE) { m_state = State::ReadChecksum; m_sector_offset = 0; @@ -153,7 +153,7 @@ bool MemoryCard::Transfer(const u8 data_in, u8* data_out) m_checksum ^= data_in; } - const u32 offset = ZeroExtend32(m_address) * SECTOR_SIZE + m_sector_offset; + const u32 offset = ZeroExtend32(m_address) * MemoryCardImage::FRAME_SIZE + m_sector_offset; m_changed |= (m_data[offset] != data_in); m_data[offset] = data_in; @@ -161,7 +161,7 @@ bool MemoryCard::Transfer(const u8 data_in, u8* data_out) ack = true; m_sector_offset++; - if (m_sector_offset == SECTOR_SIZE) + if (m_sector_offset == MemoryCardImage::FRAME_SIZE) { m_state = State::WriteChecksum; m_sector_offset = 0; @@ -262,96 +262,15 @@ std::unique_ptr MemoryCard::Open(std::string_view filename) return mc; } -u8 MemoryCard::ChecksumFrame(const u8* fptr) -{ - u8 value = 0; - for (u32 i = 0; i < SECTOR_SIZE - 1; i++) - value ^= fptr[i]; - - return value; -} - void MemoryCard::Format() { - // fill everything with FF - m_data.fill(u8(0xFF)); - - // header - { - u8* fptr = GetSectorPtr(0); - std::fill_n(fptr, SECTOR_SIZE, u8(0)); - fptr[0] = 'M'; - fptr[1] = 'C'; - fptr[0x7F] = ChecksumFrame(fptr); - } - - // directory - for (u32 frame = 1; frame < 16; frame++) - { - u8* fptr = GetSectorPtr(frame); - std::fill_n(fptr, SECTOR_SIZE, u8(0)); - fptr[0] = 0xA0; // free - fptr[8] = 0xFF; // pointer to next file - fptr[9] = 0xFF; // pointer to next file - fptr[0x7F] = ChecksumFrame(fptr); // checksum - } - - // broken sector list - for (u32 frame = 16; frame < 36; frame++) - { - u8* fptr = GetSectorPtr(frame); - std::fill_n(fptr, SECTOR_SIZE, u8(0)); - fptr[0] = 0xFF; - fptr[1] = 0xFF; - fptr[2] = 0xFF; - fptr[3] = 0xFF; - fptr[8] = 0xFF; // pointer to next file - fptr[9] = 0xFF; // pointer to next file - fptr[0x7F] = ChecksumFrame(fptr); // checksum - } - - // broken sector replacement data - for (u32 frame = 36; frame < 56; frame++) - { - u8* fptr = GetSectorPtr(frame); - std::fill_n(fptr, SECTOR_SIZE, u8(0x00)); - } - - // unused frames - for (u32 frame = 56; frame < 63; frame++) - { - u8* fptr = GetSectorPtr(frame); - std::fill_n(fptr, SECTOR_SIZE, u8(0x00)); - } - - // write test frame - std::memcpy(GetSectorPtr(63), GetSectorPtr(0), SECTOR_SIZE); - + MemoryCardImage::Format(&m_data); m_changed = true; } -u8* MemoryCard::GetSectorPtr(u32 sector) -{ - Assert(sector < NUM_SECTORS); - return &m_data[sector * SECTOR_SIZE]; -} - bool MemoryCard::LoadFromFile() { - std::unique_ptr stream = - FileSystem::OpenFile(m_filename.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); - if (!stream) - return false; - - const size_t num_read = stream->Read(m_data.data(), SECTOR_SIZE * NUM_SECTORS); - if (num_read != (SECTOR_SIZE * NUM_SECTORS)) - { - Log_ErrorPrintf("Only read %zu of %u sectors from '%s'", num_read / SECTOR_SIZE, NUM_SECTORS, m_filename.c_str()); - return false; - } - - Log_InfoPrintf("Loaded memory card from %s", m_filename.c_str()); - return true; + return MemoryCardImage::LoadFromFile(&m_data, m_filename.c_str()); } bool MemoryCard::SaveIfChanged(bool display_osd_message) @@ -366,27 +285,19 @@ bool MemoryCard::SaveIfChanged(bool display_osd_message) if (m_filename.empty()) return false; - std::unique_ptr stream = - FileSystem::OpenFile(m_filename.c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_WRITE | - BYTESTREAM_OPEN_ATOMIC_UPDATE | BYTESTREAM_OPEN_STREAMED); - if (!stream) + if (!MemoryCardImage::SaveToFile(m_data, m_filename.c_str())) { - Log_ErrorPrintf("Failed to open '%s' for writing.", m_filename.c_str()); + if (display_osd_message) + { + g_host_interface->AddOSDMessage( + StringUtil::StdStringFromFormat("Failed to save memory card to '%s'", m_filename.c_str()), 20.0f); + } + return false; } - if (!stream->Write2(m_data.data(), SECTOR_SIZE * NUM_SECTORS) || !stream->Commit()) - { - Log_ErrorPrintf("Failed to write sectors to '%s'", m_filename.c_str()); - stream->Discard(); - return false; - } - - Log_InfoPrintf("Saved memory card to '%s'", m_filename.c_str()); if (display_osd_message) - { g_host_interface->AddOSDMessage(StringUtil::StdStringFromFormat("Saved memory card to '%s'", m_filename.c_str())); - } return true; } diff --git a/src/core/memory_card.h b/src/core/memory_card.h index 61fd8f34d..c95131bf0 100644 --- a/src/core/memory_card.h +++ b/src/core/memory_card.h @@ -1,6 +1,7 @@ #pragma once #include "common/bitfield.h" #include "controller.h" +#include "memory_card_image.h" #include #include #include @@ -11,13 +12,6 @@ class TimingEvent; class MemoryCard final { public: - enum : u32 - { - DATA_SIZE = 128 * 1024, // 1mbit - SECTOR_SIZE = 128, - NUM_SECTORS = DATA_SIZE / SECTOR_SIZE - }; - MemoryCard(); ~MemoryCard(); @@ -76,10 +70,6 @@ private: WriteEnd, }; - static u8 ChecksumFrame(const u8* fptr); - - u8* GetSectorPtr(u32 sector); - bool LoadFromFile(); bool SaveIfChanged(bool display_osd_message); void QueueFileSave(); @@ -94,7 +84,7 @@ private: u8 m_last_byte = 0; bool m_changed = false; - std::array m_data{}; + MemoryCardImage::DataArray m_data{}; std::string m_filename; }; diff --git a/src/core/memory_card_image.cpp b/src/core/memory_card_image.cpp new file mode 100644 index 000000000..e329bc077 --- /dev/null +++ b/src/core/memory_card_image.cpp @@ -0,0 +1,460 @@ +#include "memory_card_image.h" +#include "common/byte_stream.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/shiftjis.h" +#include "common/state_wrapper.h" +#include "common/string_util.h" +#include "host_interface.h" +#include "system.h" +#include +#include +#include +Log_SetChannel(MemoryCard); + +namespace MemoryCardImage { + +#pragma pack(push, 1) + +struct DirectoryFrame +{ + u32 block_allocation_state; + u32 file_size; + u16 next_block_number; + char filename[21]; + u8 zero_pad_1; + u8 pad_2[95]; + u8 checksum; +}; + +static_assert(sizeof(DirectoryFrame) == FRAME_SIZE); + +struct TitleFrame +{ + char id[2]; + u8 icon_flag; + u8 unk_block_number; + u8 title[64]; + u8 pad_1[12]; + u8 pad_2[16]; + u16 icon_palette[16]; +}; + +static_assert(sizeof(TitleFrame) == FRAME_SIZE); + +#pragma pack(pop) + +static u8 GetChecksum(const u8* frame) +{ + u8 checksum = frame[0]; + for (u32 i = 1; i < FRAME_SIZE - 1; i++) + checksum ^= frame[i]; + return checksum; +} + +static void UpdateChecksum(DirectoryFrame* df) +{ + df->checksum = GetChecksum(reinterpret_cast(df)); +} + +template +T* GetFramePtr(DataArray* data, u32 block, u32 frame) +{ + return reinterpret_cast(data->data() + (block * BLOCK_SIZE) + (frame * FRAME_SIZE)); +} + +template +const T* GetFramePtr(const DataArray& data, u32 block, u32 frame) +{ + return reinterpret_cast(&data[(block * BLOCK_SIZE) + (frame * FRAME_SIZE)]); +} + +static constexpr u32 RGBA5551ToRGBA8888(u16 color) +{ + u8 r = Truncate8(color & 31); + u8 g = Truncate8((color >> 5) & 31); + u8 b = Truncate8((color >> 10) & 31); + u8 a = Truncate8((color >> 15) & 1); + + // 00012345 -> 1234545 + b = (b << 3) | (b & 0b111); + g = (g << 3) | (g & 0b111); + r = (r << 3) | (r & 0b111); + // a = a ? 255 : 0; + a = (color == 0) ? 0 : 255; + + return ZeroExtend32(r) | (ZeroExtend32(g) << 8) | (ZeroExtend32(b) << 16) | (ZeroExtend32(a) << 24); +} + +bool LoadFromFile(DataArray* data, const char* filename) +{ + FILESYSTEM_STAT_DATA sd; + if (!FileSystem::StatFile(filename, &sd) || sd.Size != DATA_SIZE) + return false; + + std::unique_ptr stream = FileSystem::OpenFile(filename, BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); + if (!stream || stream->GetSize() != DATA_SIZE) + return false; + + const size_t num_read = stream->Read(data->data(), DATA_SIZE); + if (num_read != DATA_SIZE) + { + Log_ErrorPrintf("Only read %zu of %u sectors from '%s'", num_read / FRAME_SIZE, NUM_FRAMES, filename); + return false; + } + + Log_InfoPrintf("Loaded memory card from %s", filename); + return true; +} + +bool SaveToFile(const DataArray& data, const char* filename) +{ + std::unique_ptr stream = + FileSystem::OpenFile(filename, BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_WRITE | + BYTESTREAM_OPEN_ATOMIC_UPDATE | BYTESTREAM_OPEN_STREAMED); + if (!stream) + { + Log_ErrorPrintf("Failed to open '%s' for writing.", filename); + return false; + } + + if (!stream->Write2(data.data(), DATA_SIZE) || !stream->Commit()) + { + Log_ErrorPrintf("Failed to write sectors to '%s'", filename); + stream->Discard(); + return false; + } + + Log_InfoPrintf("Saved memory card to '%s'", filename); + return true; +} + +void Format(DataArray* data) +{ + // fill everything with FF + data->fill(u8(0xFF)); + + // header + { + u8* fptr = GetFramePtr(data, 0, 0); + std::fill_n(fptr, FRAME_SIZE, u8(0)); + fptr[0] = 'M'; + fptr[1] = 'C'; + fptr[0x7F] = GetChecksum(fptr); + } + + // directory + for (u32 frame = 1; frame < 16; frame++) + { + u8* fptr = GetFramePtr(data, 0, frame); + std::fill_n(fptr, FRAME_SIZE, u8(0)); + fptr[0] = 0xA0; // free + fptr[8] = 0xFF; // pointer to next file + fptr[9] = 0xFF; // pointer to next file + fptr[0x7F] = GetChecksum(fptr); // checksum + } + + // broken sector list + for (u32 frame = 16; frame < 36; frame++) + { + u8* fptr = GetFramePtr(data, 0, frame); + std::fill_n(fptr, FRAME_SIZE, u8(0)); + fptr[0] = 0xFF; + fptr[1] = 0xFF; + fptr[2] = 0xFF; + fptr[3] = 0xFF; + fptr[8] = 0xFF; // pointer to next file + fptr[9] = 0xFF; // pointer to next file + fptr[0x7F] = GetChecksum(fptr); // checksum + } + + // broken sector replacement data + for (u32 frame = 36; frame < 56; frame++) + { + u8* fptr = GetFramePtr(data, 0, frame); + std::fill_n(fptr, FRAME_SIZE, u8(0x00)); + } + + // unused frames + for (u32 frame = 56; frame < 63; frame++) + { + u8* fptr = GetFramePtr(data, 0, frame); + std::fill_n(fptr, FRAME_SIZE, u8(0x00)); + } + + // write test frame + std::memcpy(GetFramePtr(data, 0, 63), GetFramePtr(data, 0, 0), FRAME_SIZE); +} + +static std::optional GetNextFreeBlock(const DataArray& data) +{ + for (u32 dir_frame = 1; dir_frame < FRAMES_PER_BLOCK; dir_frame++) + { + const DirectoryFrame* df = GetFramePtr(data, 0, dir_frame); + if ((df->block_allocation_state & 0xF0) == 0xA0) + return dir_frame; + } + + return std::nullopt; +} + +u32 GetFreeBlockCount(const DataArray& data) +{ + u32 count = 0; + for (u32 dir_frame = 1; dir_frame < FRAMES_PER_BLOCK; dir_frame++) + { + const DirectoryFrame* df = GetFramePtr(data, 0, dir_frame); + if ((df->block_allocation_state & 0xF0) == 0xA0) + count++; + } + + return count; +} + +std::vector EnumerateFiles(const DataArray& data) +{ + std::vector files; + + for (u32 dir_frame = 1; dir_frame < FRAMES_PER_BLOCK; dir_frame++) + { + const DirectoryFrame* df = GetFramePtr(data, 0, dir_frame); + if (df->block_allocation_state != 0x51) + continue; + + u32 filename_length = 0; + while (filename_length < sizeof(df->filename) && df->filename[filename_length] != '\0') + filename_length++; + + FileInfo fi; + fi.filename.append(df->filename, filename_length); + fi.first_block = dir_frame; + fi.size = df->file_size; + fi.num_blocks = 1; + + const DirectoryFrame* next_df = df; + while (next_df->next_block_number < (NUM_BLOCKS - 1) && fi.num_blocks < FRAMES_PER_BLOCK) + { + fi.num_blocks++; + next_df = GetFramePtr(data, 0, next_df->next_block_number + 1); + } + + if (fi.num_blocks == FRAMES_PER_BLOCK) + { + // invalid + Log_WarningPrintf("Invalid block chain in block %u", dir_frame); + continue; + } + + const TitleFrame* tf = GetFramePtr(data, dir_frame, 0); + u32 num_icon_frames = 0; + if (tf->icon_flag == 0x11) + num_icon_frames = 1; + else if (tf->icon_flag == 0x12) + num_icon_frames = 2; + else if (tf->icon_flag == 0x13) + num_icon_frames = 3; + else + { + Log_WarningPrintf("Unknown icon flag 0x%02X", tf->icon_flag); + continue; + } + + char title_sjis[sizeof(tf->title) + 2]; + std::memcpy(title_sjis, tf->title, sizeof(tf->title)); + title_sjis[sizeof(tf->title)] = 0; + title_sjis[sizeof(tf->title) + 1] = 0; + char* title_utf8 = sjis2utf8(title_sjis); + fi.title = title_utf8; + std::free(title_utf8); + + fi.icon_frames.resize(num_icon_frames); + for (u32 icon_frame = 0; icon_frame < num_icon_frames; icon_frame++) + { + const u8* indices_ptr = GetFramePtr(data, dir_frame, 1 + icon_frame); + u32* pixels_ptr = fi.icon_frames[icon_frame].pixels; + for (u32 i = 0; i < ICON_WIDTH * ICON_HEIGHT; i += 2) + { + *(pixels_ptr++) = RGBA5551ToRGBA8888(tf->icon_palette[*indices_ptr & 0xF]); + *(pixels_ptr++) = RGBA5551ToRGBA8888(tf->icon_palette[*indices_ptr >> 4]); + indices_ptr++; + } + } + + files.push_back(std::move(fi)); + } + + return files; +} + +bool ReadFile(const DataArray& data, const FileInfo& fi, std::vector* buffer) +{ + buffer->resize(fi.num_blocks * BLOCK_SIZE); + + u32 block_number = fi.first_block; + for (u32 i = 0; i < fi.num_blocks; i++) + { + Assert(block_number < FRAMES_PER_BLOCK); + std::memcpy(buffer->data() + (i * BLOCK_SIZE), GetFramePtr(data, block_number, 0), BLOCK_SIZE); + + const DirectoryFrame* df = GetFramePtr(data, 0, block_number); + block_number = df->next_block_number; + } + + return true; +} + +bool WriteFile(DataArray* data, const std::string_view& filename, const std::vector& buffer) +{ + if (buffer.empty()) + { + Log_ErrorPrintf("Failed to write file to memory card: buffer is empty"); + return false; + } + + const u32 num_blocks = (static_cast(buffer.size()) + (BLOCK_SIZE - 1)) / BLOCK_SIZE; + if (GetFreeBlockCount(*data) < num_blocks) + { + Log_ErrorPrintf("Failed to write file to memory card: insufficient free blocks"); + return false; + } + + DirectoryFrame* last_df = nullptr; + for (u32 i = 0; i < num_blocks; i++) + { + std::optional block_number = GetNextFreeBlock(*data); + Assert(block_number.has_value()); + + DirectoryFrame* df = GetFramePtr(data, 0, block_number.value()); + std::memset(df, 0, sizeof(DirectoryFrame)); + + if (last_df) + { + // not first sector + last_df->next_block_number = Truncate16(block_number.value() - 1); + UpdateChecksum(last_df); + + // 53 for last otherwise 52 + df->block_allocation_state = (i == (num_blocks - 1)) ? 0x53 : 0x52; + } + else + { + // first sector + df->block_allocation_state = 0x51; + df->file_size = static_cast(buffer.size()); + StringUtil::Strlcpy(df->filename, filename, sizeof(df->filename)); + } + + df->next_block_number = 0xFFFF; + UpdateChecksum(df); + last_df = df; + + u8* data_block = GetFramePtr(data, block_number.value(), 0); + const u32 src_offset = i * BLOCK_SIZE; + const u32 size_to_copy = std::min(BLOCK_SIZE, static_cast(buffer.size()) - src_offset); + const u32 size_to_zero = BLOCK_SIZE - size_to_copy; + std::memcpy(data_block, buffer.data() + src_offset, size_to_copy); + if (size_to_zero) + std::memset(data_block + size_to_copy, 0, size_to_zero); + } + + Log_InfoPrintf("Wrote %zu byte (%u block) file to memory card", buffer.size(), num_blocks); + return true; +} + +bool DeleteFile(DataArray* data, const FileInfo& fi) +{ + Log_InfoPrintf("Deleting '%s' from memory card (%u blocks)", fi.filename.c_str(), fi.num_blocks); + + u32 block_number = fi.first_block; + for (u32 i = 0; i < fi.num_blocks && (block_number > 0 && block_number < NUM_BLOCKS); i++) + { + DirectoryFrame* df = GetFramePtr(data, 0, block_number); + const u32 next_block_number = ZeroExtend32(df->next_block_number) + 1; + std::memset(df, 0, sizeof(DirectoryFrame)); + if (i == 0) + df->block_allocation_state = 0xA1; + else if (i == (fi.num_blocks - 1)) + df->block_allocation_state = 0xA3; + else + df->block_allocation_state = 0xA2; + + df->next_block_number = 0xFFFF; + UpdateChecksum(df); + } + + return true; +} + +static bool ImportCardMCD(DataArray* data, const char* filename) +{ + std::optional> file_data = FileSystem::ReadBinaryFile(filename); + if (!file_data.has_value()) + return false; + + if (file_data->size() != DATA_SIZE) + { + Log_ErrorPrintf("Failed to import memory card from '%s': file is incorrect size.", filename); + return false; + } + + std::memcpy(data->data(), file_data->data(), DATA_SIZE); + return true; +} + +static bool ImportCardGME(DataArray* data, const char* filename) +{ +#pragma pack(push, 1) + struct GMEHeader + { + char id[12]; + u8 unk1[4]; + u8 unk2[5]; + u8 sector0[16]; + u8 sector1[16]; + u8 unk3[11]; + char descriptions[256][15]; + }; + static_assert(sizeof(GMEHeader) == 0xF40); +#pragma pack(pop) + + std::optional> file_data = FileSystem::ReadBinaryFile(filename); + if (!file_data.has_value()) + return false; + + if (file_data->size() < (sizeof(GMEHeader) + DATA_SIZE)) + { + Log_ErrorPrintf("Failed to import GME memory card from '%s': file is incorrect size.", filename); + return false; + } + + // we don't actually care about the header, just skip over it + std::memcpy(data->data(), file_data->data() + sizeof(GMEHeader), DATA_SIZE); + return true; +} + +bool ImportCard(DataArray* data, const char* filename) +{ + const char* extension = std::strrchr(filename, '.'); + if (!extension) + { + Log_ErrorPrintf("Failed to import memory card from '%s': missing extension?", filename); + return false; + } + + if (StringUtil::Strcasecmp(extension, ".mcd") == 0 || StringUtil::Strcasecmp(extension, ".mcr") == 0 || + StringUtil::Strcasecmp(extension, ".mc") == 0 || StringUtil::Strcasecmp(extension, ".srm") == 0) + { + return ImportCardMCD(data, filename); + } + else if (StringUtil::Strcasecmp(extension, ".gme") == 0) + { + return ImportCardGME(data, filename); + } + else + { + Log_ErrorPrintf("Failed to import memory card from '%s': unknown extension?", filename); + return false; + } +} + +} // namespace MemoryCardImage \ No newline at end of file diff --git a/src/core/memory_card_image.h b/src/core/memory_card_image.h new file mode 100644 index 000000000..a45151106 --- /dev/null +++ b/src/core/memory_card_image.h @@ -0,0 +1,53 @@ +#pragma once +#include "common/bitfield.h" +#include "controller.h" +#include +#include +#include +#include +#include + +namespace MemoryCardImage +{ +enum : u32 +{ + DATA_SIZE = 128 * 1024, // 1mbit + BLOCK_SIZE = 8192, + FRAME_SIZE = 128, + FRAMES_PER_BLOCK = BLOCK_SIZE / FRAME_SIZE, + NUM_BLOCKS = DATA_SIZE / BLOCK_SIZE, + NUM_FRAMES = DATA_SIZE / FRAME_SIZE, + ICON_WIDTH =16, + ICON_HEIGHT = 16 +}; + +using DataArray = std::array; + +bool LoadFromFile(DataArray* data, const char* filename); +bool SaveToFile(const DataArray& data, const char* filename); + +void Format(DataArray* data); + +struct IconFrame +{ + u32 pixels[ICON_WIDTH * ICON_HEIGHT]; +}; + +struct FileInfo +{ + std::string filename; + std::string title; + u32 size; + u32 first_block; + u32 num_blocks; + + std::vector icon_frames; +}; + +u32 GetFreeBlockCount(const DataArray& data); +std::vector EnumerateFiles(const DataArray& data); +bool ReadFile(const DataArray& data, const FileInfo& fi, std::vector* buffer); +bool WriteFile(DataArray* data, const std::string_view& filename, const std::vector& buffer); +bool DeleteFile(DataArray* data, const FileInfo& fi); +bool ImportCard(DataArray* data, const char* filename); +}