From 989ae6d5388b1dd900494963374cc0254762a422 Mon Sep 17 00:00:00 2001 From: Markus Kalkbrenner Date: Mon, 23 Mar 2026 13:04:49 +0100 Subject: [PATCH 1/5] don't write temp files anymore, only support cROMc and cROM on a real machine DMD --- AGENTS.md | 7 +- src/serum-decode.cpp | 674 ++++++++++++++++++++++++++++++++++++++++--- src/sparse-vector.h | 23 +- 3 files changed, 662 insertions(+), 42 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e528785..17aa714 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,10 +127,13 @@ Entry point: `Serum_Load(altcolorpath, romname, flags)`. 1. Reset all runtime state via `Serum_free()`. 2. On real-machine runtime (`is_real_machine()==true`): - - load only `*.cROMc` - do not scan or apply `*.pup.csv` - ignore `skip-cromc.txt` - - do not fall back to `*.cROM` / `*.cRZ` + - accept `*.cROMc` only for Serum v2 content + - if no acceptable `*.cROMc` is available, fall back only to plain `*.cROM` + - raw real-machine fallback must resolve to Serum v1 content; reject raw + Serum v2 loads + - skip `*.cRZ` entirely on real-machine runtime 3. On non-real-machine/runtime-update flows: - look for optional `*.pup.csv` - prefer loading `*.cROMc` unless `skip-cromc.txt` exists. diff --git a/src/serum-decode.cpp b/src/serum-decode.cpp index e4ba315..03f78e9 100644 --- a/src/serum-decode.cpp +++ b/src/serum-decode.cpp @@ -1339,6 +1339,76 @@ bool unzip_crz(const char* const filename, const char* const extractpath, return ok; } +struct FileCRomReader { + FILE* stream = nullptr; + + bool readExact(void* dst, size_t bytes) { + return fread(dst, 1, bytes, stream) == bytes; + } +}; + +struct MemoryCRomReader { + const uint8_t* data = nullptr; + size_t size = 0; + size_t offset = 0; + + bool readExact(void* dst, size_t bytes) { + if (!data || offset > size || bytes > (size - offset)) { + return false; + } + memcpy(dst, data + offset, bytes); + offset += bytes; + return true; + } +}; + +template +static bool ReadValue(Reader& reader, T& value) { + return reader.readExact(&value, sizeof(T)); +} + +template +static bool ReadBytes(Reader& reader, void* dst, size_t bytes) { + return reader.readExact(dst, bytes); +} + +static bool ExtractCRZEntryToMemory(const char* filename, + std::vector& outData) { + mz_zip_archive zip_archive = {0}; + if (!mz_zip_reader_init_file(&zip_archive, filename, 0)) { + return false; + } + + const mz_uint numFiles = mz_zip_reader_get_num_files(&zip_archive); + bool ok = false; + for (mz_uint i = 0; i < numFiles; ++i) { + mz_zip_archive_file_stat file_stat; + if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) { + continue; + } + if (mz_zip_reader_is_file_a_directory(&zip_archive, i) || + !mz_zip_reader_is_file_supported(&zip_archive, i) || + mz_zip_reader_is_file_encrypted(&zip_archive, i)) { + continue; + } + + size_t extractedSize = 0; + void* extracted = + mz_zip_reader_extract_to_heap(&zip_archive, i, &extractedSize, 0); + if (!extracted) { + break; + } + outData.assign(static_cast(extracted), + static_cast(extracted) + extractedSize); + mz_free(extracted); + ok = true; + break; + } + + mz_zip_reader_end(&zip_archive); + return ok; +} + void Full_Reset_ColorRotations(void) { memset(colorshifts, 0, MAX_COLOR_ROTATIONS * sizeof(uint32_t)); colorrotseruminit = GetMonotonicTimeMs(); @@ -1855,9 +1925,540 @@ Serum_Frame_Struc* Serum_LoadFilev2(FILE* pfile, const uint8_t flags, return &mySerum; } +template +static Serum_Frame_Struc* Serum_LoadFilev2Stream(Reader& reader, + const uint8_t flags, + uint32_t sizeheader) { + if (!ReadValue(reader, g_serumData.fwidth) || + !ReadValue(reader, g_serumData.fheight) || + !ReadValue(reader, g_serumData.fwidth_extra) || + !ReadValue(reader, g_serumData.fheight_extra)) { + enabled = false; + return NULL; + } + isoriginalrequested = false; + isextrarequested = false; + mySerum.width32 = 0; + mySerum.width64 = 0; + if (g_serumData.fheight == 32) { + if (flags & FLAG_REQUEST_32P_FRAMES) { + isoriginalrequested = true; + mySerum.width32 = g_serumData.fwidth; + } + if (flags & FLAG_REQUEST_64P_FRAMES) { + isextrarequested = true; + mySerum.width64 = g_serumData.fwidth_extra; + } + + } else { + if (flags & FLAG_REQUEST_64P_FRAMES) { + isoriginalrequested = true; + mySerum.width64 = g_serumData.fwidth; + } + if (flags & FLAG_REQUEST_32P_FRAMES) { + isextrarequested = true; + mySerum.width32 = g_serumData.fwidth_extra; + } + } + if (!ReadValue(reader, g_serumData.nframes) || + !ReadValue(reader, g_serumData.nocolors)) { + enabled = false; + return NULL; + } + mySerum.nocolors = g_serumData.nocolors; + if ((g_serumData.nframes == 0) || (g_serumData.nocolors == 0) || + !ValidateLoadedGeometry(true, "cROM/v2")) { + enabled = false; + return NULL; + } + if (!ReadValue(reader, g_serumData.ncompmasks) || + !ReadValue(reader, g_serumData.nsprites) || + !ReadValue(reader, g_serumData.nbackgrounds)) { + enabled = false; + return NULL; + } + if (sizeheader >= 20 * sizeof(uint32_t)) { + int is256x64; + if (!ReadValue(reader, is256x64)) { + enabled = false; + return NULL; + } + g_serumData.is256x64 = (is256x64 != 0); + } + + frameshape = (uint8_t*)malloc(g_serumData.fwidth * g_serumData.fheight); + + if (flags & FLAG_REQUEST_32P_FRAMES) { + mySerum.frame32 = + (uint16_t*)malloc(32 * mySerum.width32 * sizeof(uint16_t)); + mySerum.rotations32 = (uint16_t*)malloc( + MAX_COLOR_ROTATION_V2 * MAX_LENGTH_COLOR_ROTATION * sizeof(uint16_t)); + mySerum.rotationsinframe32 = + (uint16_t*)malloc(2 * 32 * mySerum.width32 * sizeof(uint16_t)); + if (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS) + mySerum.modifiedelements32 = (uint8_t*)malloc(32 * mySerum.width32); + if (!mySerum.frame32 || !mySerum.rotations32 || + !mySerum.rotationsinframe32 || + (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS && + !mySerum.modifiedelements32)) { + Serum_free(); + enabled = false; + return NULL; + } + } + if (flags & FLAG_REQUEST_64P_FRAMES) { + mySerum.frame64 = + (uint16_t*)malloc(64 * mySerum.width64 * sizeof(uint16_t)); + mySerum.rotations64 = (uint16_t*)malloc( + MAX_COLOR_ROTATION_V2 * MAX_LENGTH_COLOR_ROTATION * sizeof(uint16_t)); + mySerum.rotationsinframe64 = + (uint16_t*)malloc(2 * 64 * mySerum.width64 * sizeof(uint16_t)); + if (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS) + mySerum.modifiedelements64 = (uint8_t*)malloc(64 * mySerum.width64); + if (!mySerum.frame64 || !mySerum.rotations64 || + !mySerum.rotationsinframe64 || + (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS && + !mySerum.modifiedelements64)) { + Serum_free(); + enabled = false; + return NULL; + } + } + + g_serumData.hashcodes.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.shapecompmode.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.compmaskID.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.compmasks.readFromCRomReader( + g_serumData.is256x64 ? (256 * 64) + : (g_serumData.fwidth * g_serumData.fheight), + g_serumData.ncompmasks, reader); + g_serumData.isextraframe.readFromCRomReader(1, g_serumData.nframes, reader); + if (isextrarequested) { + for (uint32_t ti = 0; ti < g_serumData.nframes; ti++) { + if (g_serumData.isextraframe[ti][0] > 0) { + mySerum.flags |= FLAG_RETURNED_EXTRA_AVAILABLE; + break; + } + } + } else + g_serumData.isextraframe.clearIndex(); + g_serumData.cframes_v2.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, reader); + g_serumData.cframes_v2_extra.readFromCRomReader( + g_serumData.fwidth_extra * g_serumData.fheight_extra, g_serumData.nframes, + reader, &g_serumData.isextraframe); + g_serumData.dynamasks.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, reader); + g_serumData.dynamasks_extra.readFromCRomReader( + g_serumData.fwidth_extra * g_serumData.fheight_extra, g_serumData.nframes, + reader, &g_serumData.isextraframe); + g_serumData.dyna4cols_v2.readFromCRomReader( + MAX_DYNA_SETS_PER_FRAME_V2 * g_serumData.nocolors, g_serumData.nframes, + reader); + g_serumData.dyna4cols_v2_extra.readFromCRomReader( + MAX_DYNA_SETS_PER_FRAME_V2 * g_serumData.nocolors, g_serumData.nframes, + reader, &g_serumData.isextraframe); + g_serumData.isextrasprite.readFromCRomReader(1, g_serumData.nsprites, reader); + if (!isextrarequested) g_serumData.isextrasprite.clearIndex(); + g_serumData.framesprites.readFromCRomReader(MAX_SPRITES_PER_FRAME, + g_serumData.nframes, reader); + g_serumData.spriteoriginal.readFromCRomReader( + MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, reader); + g_serumData.spritecolored.readFromCRomReader( + MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, reader); + g_serumData.spritemask_extra.readFromCRomReader( + MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, reader, + &g_serumData.isextrasprite); + g_serumData.spritecolored_extra.readFromCRomReader( + MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, reader, + &g_serumData.isextrasprite); + g_serumData.activeframes.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.colorrotations_v2.readFromCRomReader( + MAX_LENGTH_COLOR_ROTATION * MAX_COLOR_ROTATION_V2, g_serumData.nframes, + reader); + g_serumData.colorrotations_v2_extra.readFromCRomReader( + MAX_LENGTH_COLOR_ROTATION * MAX_COLOR_ROTATION_V2, g_serumData.nframes, + reader, &g_serumData.isextraframe); + g_serumData.spritedetdwords.readFromCRomReader(MAX_SPRITE_DETECT_AREAS, + g_serumData.nsprites, reader); + g_serumData.spritedetdwordpos.readFromCRomReader( + MAX_SPRITE_DETECT_AREAS, g_serumData.nsprites, reader); + g_serumData.spritedetareas.readFromCRomReader(4 * MAX_SPRITE_DETECT_AREAS, + g_serumData.nsprites, reader); + g_serumData.triggerIDs.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.framespriteBB.readFromCRomReader(MAX_SPRITES_PER_FRAME * 4, + g_serumData.nframes, reader, + &g_serumData.framesprites); + g_serumData.isextrabackground.readFromCRomReader(1, g_serumData.nbackgrounds, + reader); + if (!isextrarequested) g_serumData.isextrabackground.clearIndex(); + g_serumData.backgroundframes_v2.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nbackgrounds, + reader); + g_serumData.backgroundframes_v2_extra.readFromCRomReader( + g_serumData.fwidth_extra * g_serumData.fheight_extra, + g_serumData.nbackgrounds, reader, &g_serumData.isextrabackground); + g_serumData.backgroundIDs.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.backgroundmask.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, reader, + &g_serumData.backgroundIDs); + g_serumData.backgroundmask_extra.readFromCRomReader( + g_serumData.fwidth_extra * g_serumData.fheight_extra, g_serumData.nframes, + reader, &g_serumData.backgroundIDs); + + if (sizeheader >= 15 * sizeof(uint32_t)) { + g_serumData.dynashadowsdir.readFromCRomReader(MAX_DYNA_SETS_PER_FRAME_V2, + g_serumData.nframes, reader); + g_serumData.dynashadowscol.readFromCRomReader(MAX_DYNA_SETS_PER_FRAME_V2, + g_serumData.nframes, reader); + g_serumData.dynashadowsdir_extra.readFromCRomReader( + MAX_DYNA_SETS_PER_FRAME_V2, g_serumData.nframes, reader, + &g_serumData.isextraframe); + g_serumData.dynashadowscol_extra.readFromCRomReader( + MAX_DYNA_SETS_PER_FRAME_V2, g_serumData.nframes, reader, + &g_serumData.isextraframe); + } else { + g_serumData.dynashadowsdir.reserve(MAX_DYNA_SETS_PER_FRAME_V2); + g_serumData.dynashadowscol.reserve(MAX_DYNA_SETS_PER_FRAME_V2); + g_serumData.dynashadowsdir_extra.reserve(MAX_DYNA_SETS_PER_FRAME_V2); + g_serumData.dynashadowscol_extra.reserve(MAX_DYNA_SETS_PER_FRAME_V2); + } + + if (sizeheader >= 18 * sizeof(uint32_t)) { + g_serumData.dynasprite4cols.readFromCRomReader( + MAX_DYNA_SETS_PER_SPRITE * g_serumData.nocolors, g_serumData.nsprites, + reader); + g_serumData.dynasprite4cols_extra.readFromCRomReader( + MAX_DYNA_SETS_PER_SPRITE * g_serumData.nocolors, g_serumData.nsprites, + reader, &g_serumData.isextraframe); + g_serumData.dynaspritemasks.readFromCRomReader( + MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, reader); + g_serumData.dynaspritemasks_extra.readFromCRomReader( + MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, reader, + &g_serumData.isextraframe); + } else { + g_serumData.dynasprite4cols.reserve(MAX_DYNA_SETS_PER_SPRITE * + g_serumData.nocolors); + g_serumData.dynasprite4cols_extra.reserve(MAX_DYNA_SETS_PER_SPRITE * + g_serumData.nocolors); + g_serumData.dynaspritemasks.reserve(MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT); + g_serumData.dynaspritemasks_extra.reserve(MAX_SPRITE_WIDTH * + MAX_SPRITE_HEIGHT); + } + + if (sizeheader >= 19 * sizeof(uint32_t)) { + g_serumData.sprshapemode.readFromCRomReader(1, g_serumData.nsprites, + reader); + for (uint32_t i = 0; i < g_serumData.nsprites; i++) { + if (g_serumData.sprshapemode[i][0] > 0) { + for (uint32_t j = 0; j < MAX_SPRITE_DETECT_AREAS; j++) { + uint32_t detdwords = g_serumData.spritedetdwords[i][j]; + if ((detdwords & 0xFF000000) > 0) + detdwords = (detdwords & 0x00FFFFFF) | 0x01000000; + if ((detdwords & 0x00FF0000) > 0) + detdwords = (detdwords & 0xFF00FFFF) | 0x00010000; + if ((detdwords & 0x0000FF00) > 0) + detdwords = (detdwords & 0xFFFF00FF) | 0x00000100; + if ((detdwords & 0x000000FF) > 0) + detdwords = (detdwords & 0xFFFFFF00) | 0x00000001; + g_serumData.spritedetdwords[i][j] = detdwords; + } + for (uint32_t j = 0; j < MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT; j++) { + if (g_serumData.spriteoriginal[i][j] > 0 && + g_serumData.spriteoriginal[i][j] != 255) + g_serumData.spriteoriginal[i][j] = 1; + } + } + } + } else { + g_serumData.sprshapemode.reserve(g_serumData.nsprites); + } + + g_serumData.BuildPackingSidecarsAndNormalize(); + + mySerum.ntriggers = 0; + uint32_t framespos = g_serumData.nframes / 2; + uint32_t framesspace = g_serumData.nframes - framespos; + uint32_t framescount = (framesspace + 9) / 10; + + if (framescount > 0) { + std::vector candidates; + candidates.reserve(framesspace); + for (uint32_t ti = framespos; ti < g_serumData.nframes; ++ti) { + if (g_serumData.triggerIDs[ti][0] == 0xffffffff) { + candidates.push_back(ti); + } + } + + if (!candidates.empty()) { + std::mt19937 rng(0xC0DE1234); + std::shuffle(candidates.begin(), candidates.end(), rng); + std::uniform_int_distribution triggerDist(65433u, 0xfffffffeu); + + uint32_t toAssign = std::min(framescount, candidates.size()); + for (uint32_t i = 0; i < toAssign; ++i) { + uint32_t triggerValue = triggerDist(rng); + g_serumData.triggerIDs.set(candidates[i], &triggerValue, 1); + } + + for (uint32_t offset = 0; (framespos + offset) < g_serumData.nframes; + ++offset) { + uint32_t idx = framespos + offset; + if (g_serumData.triggerIDs[idx][0] == 0xffffffff) { + uint32_t triggerValue = triggerDist(rng); + g_serumData.triggerIDs.set(idx, &triggerValue, 1); + break; + } + } + } + } + for (uint32_t ti = 0; ti < g_serumData.nframes; ti++) { + if (g_serumData.triggerIDs[ti][0] < PUP_TRIGGER_MAX_THRESHOLD) + mySerum.ntriggers++; + } + framechecked = (bool*)malloc(sizeof(bool) * g_serumData.nframes); + if (!framechecked) { + Serum_free(); + enabled = false; + return NULL; + } + if (flags & FLAG_REQUEST_32P_FRAMES) { + if (g_serumData.fheight == 32) + mySerum.width32 = g_serumData.fwidth; + else + mySerum.width32 = g_serumData.fwidth_extra; + } else + mySerum.width32 = 0; + if (flags & FLAG_REQUEST_64P_FRAMES) { + if (g_serumData.fheight == 32) + mySerum.width64 = g_serumData.fwidth_extra; + else + mySerum.width64 = g_serumData.fwidth; + } else + mySerum.width64 = 0; + + mySerum.SerumVersion = g_serumData.SerumVersion = SERUM_V2; + + Full_Reset_ColorRotations(); + cromloaded = true; + + enabled = true; + return &mySerum; +} + +template +static Serum_Frame_Struc* Serum_LoadFilev1Stream(Reader& reader, + const uint8_t flags) { + if (!ReadBytes(reader, g_serumData.rname, 64)) { + enabled = false; + return NULL; + } + uint32_t sizeheader; + if (!ReadValue(reader, sizeheader)) { + enabled = false; + return NULL; + } + if (sizeheader >= 14 * sizeof(uint32_t)) + return Serum_LoadFilev2Stream(reader, flags, sizeheader); + + mySerum.SerumVersion = g_serumData.SerumVersion = SERUM_V1; + uint32_t nframes32; + if (!ReadValue(reader, g_serumData.fwidth) || + !ReadValue(reader, g_serumData.fheight) || + !ReadValue(reader, nframes32) || + !ReadValue(reader, g_serumData.nocolors) || + !ReadValue(reader, g_serumData.nccolors)) { + enabled = false; + return NULL; + } + g_serumData.nframes = (uint16_t)nframes32; + mySerum.nocolors = g_serumData.nocolors; + if ((g_serumData.fwidth == 0) || (g_serumData.fheight == 0) || + (g_serumData.nframes == 0) || (g_serumData.nocolors == 0) || + (g_serumData.nccolors == 0)) { + enabled = false; + return NULL; + } + if (!ValidateLoadedGeometry(false, "cROM/v1")) { + enabled = false; + return NULL; + } + if (!ReadValue(reader, g_serumData.ncompmasks) || + !ReadValue(reader, g_serumData.nmovmasks) || + !ReadValue(reader, g_serumData.nsprites)) { + enabled = false; + return NULL; + } + if (sizeheader >= 13 * sizeof(uint32_t)) { + if (!ReadValue(reader, g_serumData.nbackgrounds)) { + enabled = false; + return NULL; + } + } else { + g_serumData.nbackgrounds = 0; + } + + uint8_t* spritedescriptionso = (uint8_t*)malloc( + g_serumData.nsprites * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); + uint8_t* spritedescriptionsc = (uint8_t*)malloc( + g_serumData.nsprites * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); + + mySerum.frame = (uint8_t*)malloc(g_serumData.fwidth * g_serumData.fheight); + mySerum.palette = (uint8_t*)malloc(3 * 64); + mySerum.rotations = (uint8_t*)malloc(MAX_COLOR_ROTATIONS * 3); + if (((g_serumData.nsprites > 0) && + (!spritedescriptionso || !spritedescriptionsc)) || + !mySerum.frame || !mySerum.palette || !mySerum.rotations) { + Serum_free(); + enabled = false; + return NULL; + } + + g_serumData.hashcodes.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.shapecompmode.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.compmaskID.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.movrctID.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.movrctID.clear(); + g_serumData.compmasks.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.ncompmasks, reader); + g_serumData.movrcts.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nmovmasks, reader); + g_serumData.movrcts.clear(); + g_serumData.cpal.readFromCRomReader(3 * g_serumData.nccolors, + g_serumData.nframes, reader); + g_serumData.cframes.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, reader); + g_serumData.dynamasks.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, reader); + g_serumData.dyna4cols.readFromCRomReader( + MAX_DYNA_4COLS_PER_FRAME * g_serumData.nocolors, g_serumData.nframes, + reader); + g_serumData.framesprites.readFromCRomReader(MAX_SPRITES_PER_FRAME, + g_serumData.nframes, reader); + + for (int ti = 0; + ti < (int)g_serumData.nsprites * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE; + ti++) { + if (!ReadValue(reader, spritedescriptionsc[ti]) || + !ReadValue(reader, spritedescriptionso[ti])) { + Free_element((void**)&spritedescriptionso); + Free_element((void**)&spritedescriptionsc); + Serum_free(); + enabled = false; + return NULL; + } + } + for (uint32_t i = 0; i < g_serumData.nsprites; i++) { + g_serumData.spritedescriptionsc.set( + i, &spritedescriptionsc[i * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE], + MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); + g_serumData.spritedescriptionso.set( + i, &spritedescriptionso[i * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE], + MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); + } + Free_element((void**)&spritedescriptionso); + Free_element((void**)&spritedescriptionsc); + + g_serumData.activeframes.readFromCRomReader(1, g_serumData.nframes, reader); + g_serumData.colorrotations.readFromCRomReader(3 * MAX_COLOR_ROTATIONS, + g_serumData.nframes, reader); + g_serumData.spritedetdwords.readFromCRomReader(MAX_SPRITE_DETECT_AREAS, + g_serumData.nsprites, reader); + g_serumData.spritedetdwordpos.readFromCRomReader( + MAX_SPRITE_DETECT_AREAS, g_serumData.nsprites, reader); + g_serumData.spritedetareas.readFromCRomReader(4 * MAX_SPRITE_DETECT_AREAS, + g_serumData.nsprites, reader); + mySerum.ntriggers = 0; + if (sizeheader >= 11 * sizeof(uint32_t)) { + g_serumData.triggerIDs.readFromCRomReader(1, g_serumData.nframes, reader); + } + uint32_t framespos = g_serumData.nframes / 2; + uint32_t framesspace = g_serumData.nframes - framespos; + uint32_t framescount = (framesspace + 9) / 10; + + if (framescount > 0) { + std::vector candidates; + candidates.reserve(framesspace); + for (uint32_t ti = framespos; ti < g_serumData.nframes; ++ti) { + if (g_serumData.triggerIDs[ti][0] == 0xffffffff) { + candidates.push_back(ti); + } + } + + if (!candidates.empty()) { + std::mt19937 rng(0xC0DE1234); + std::shuffle(candidates.begin(), candidates.end(), rng); + std::uniform_int_distribution triggerDist(65433u, 0xfffffffeu); + + uint32_t toAssign = std::min(framescount, candidates.size()); + for (uint32_t i = 0; i < toAssign; ++i) { + uint32_t triggerValue = triggerDist(rng); + g_serumData.triggerIDs.set(candidates[i], &triggerValue, 1); + } + + for (uint32_t offset = 0; (framespos + offset) < g_serumData.nframes; + ++offset) { + uint32_t idx = framespos + offset; + if (g_serumData.triggerIDs[idx][0] == 0xffffffff) { + uint32_t triggerValue = triggerDist(rng); + g_serumData.triggerIDs.set(idx, &triggerValue, 1); + break; + } + } + } + } + for (uint32_t ti = 0; ti < g_serumData.nframes; ti++) { + if (g_serumData.triggerIDs[ti][0] < PUP_TRIGGER_MAX_THRESHOLD) + mySerum.ntriggers++; + } + if (sizeheader >= 12 * sizeof(uint32_t)) + g_serumData.framespriteBB.readFromCRomReader(MAX_SPRITES_PER_FRAME * 4, + g_serumData.nframes, reader, + &g_serumData.framesprites); + else { + for (uint32_t tj = 0; tj < g_serumData.nframes; tj++) { + uint16_t tmp_framespriteBB[4 * MAX_SPRITES_PER_FRAME]; + for (uint32_t ti = 0; ti < MAX_SPRITES_PER_FRAME; ti++) { + tmp_framespriteBB[ti * 4] = 0; + tmp_framespriteBB[ti * 4 + 1] = 0; + tmp_framespriteBB[ti * 4 + 2] = g_serumData.fwidth - 1; + tmp_framespriteBB[ti * 4 + 3] = g_serumData.fheight - 1; + } + g_serumData.framespriteBB.set(tj, tmp_framespriteBB, + MAX_SPRITES_PER_FRAME * 4); + } + } + if (sizeheader >= 13 * sizeof(uint32_t)) { + g_serumData.backgroundframes.readFromCRomReader( + g_serumData.fwidth * g_serumData.fheight, g_serumData.nbackgrounds, + reader); + g_serumData.backgroundIDs.readFromCRomReader(1, g_serumData.nframes, + reader); + g_serumData.backgroundBB.readFromCRomReader(4, g_serumData.nframes, reader, + &g_serumData.backgroundIDs); + } + + g_serumData.BuildPackingSidecarsAndNormalize(); + + framechecked = (bool*)malloc(sizeof(bool) * g_serumData.nframes); + if (!framechecked) { + Serum_free(); + enabled = false; + return NULL; + } + if (g_serumData.fheight == 64) { + mySerum.width64 = g_serumData.fwidth; + mySerum.width32 = 0; + } else { + mySerum.width32 = g_serumData.fwidth; + mySerum.width64 = 0; + } + enabled = true; + return &mySerum; +} + Serum_Frame_Struc* Serum_LoadFilev1(const char* const filename, const uint8_t flags) { - char pathbuf[pathbuflen]; if (!crc32_ready) CRC32encode(); // check if we're using an uncompressed cROM file @@ -1866,26 +2467,21 @@ Serum_Frame_Struc* Serum_LoadFilev1(const char* const filename, if ((ext = strrchr(filename, '.')) != NULL) { if (strcasecmp(ext, ".cROM") == 0) { uncompressedCROM = true; - if (strcpy_s(pathbuf, pathbuflen, filename)) return NULL; } } - // extract file if it is compressed if (!uncompressedCROM) { - char cromname[pathbuflen]; - if (getenv("TMPDIR") != NULL) { - if (strcpy_s(pathbuf, pathbuflen, getenv("TMPDIR"))) return NULL; - size_t len = strlen(pathbuf); - if (len > 0 && pathbuf[len - 1] != '/') { - if (strcat_s(pathbuf, pathbuflen, "/")) return NULL; - } - } else if (strcpy_s(pathbuf, pathbuflen, filename)) + std::vector extractedCRom; + if (!ExtractCRZEntryToMemory(filename, extractedCRom)) { return NULL; - - if (!unzip_crz(filename, pathbuf, cromname, pathbuflen)) return NULL; - if (strcat_s(pathbuf, pathbuflen, cromname)) return NULL; + } + MemoryCRomReader reader{extractedCRom.data(), extractedCRom.size(), 0}; + return Serum_LoadFilev1Stream(reader, flags); } + char pathbuf[pathbuflen]; + if (strcpy_s(pathbuf, pathbuflen, filename)) return NULL; + // Open cRom FILE* pfile; pfile = fopen(pathbuf, "rb"); @@ -2201,35 +2797,37 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, } else { pFoundFile = find_case_insensitive_file(pathbuf, std::string(romname) + ".cROMc"); - if (!pFoundFile) { - Log("Real-machine mode only supports .cROMc; no concentrate file found " - "for %s", - romname); - enabled = false; - return NULL; - } - Log("Found %s", pFoundFile->c_str()); - NoteStartupRssSample("before-cromc-load"); - result = Serum_LoadConcentrate(pFoundFile->c_str(), flags); - loadedFromConcentrate = (result != NULL); - if (result) { - NoteStartupRssSample("after-cromc-load"); - LogLoadedColorizationSource(*pFoundFile, true); - } else { - Log("Failed to load %s", pFoundFile->c_str()); - enabled = false; - return NULL; + if (pFoundFile) { + Log("Found %s", pFoundFile->c_str()); + NoteStartupRssSample("before-cromc-load"); + result = Serum_LoadConcentrate(pFoundFile->c_str(), flags); + loadedFromConcentrate = (result != NULL); + if (result) { + if (g_serumData.SerumVersion != SERUM_V2) { + Log("Real-machine mode accepts .cROMc only for Serum v2; rejecting " + "%s", + pFoundFile->c_str()); + Serum_free(); + result = NULL; + loadedFromConcentrate = false; + } else { + NoteStartupRssSample("after-cromc-load"); + LogLoadedColorizationSource(*pFoundFile, true); + } + } else { + Log("Failed to load %s", pFoundFile->c_str()); + } } } - if (!result && !realMachine) { + if (!result) { #ifdef WRITE_CROMC // by default, we request both frame types flags |= FLAG_REQUEST_32P_FRAMES | FLAG_REQUEST_64P_FRAMES; #endif pFoundFile = find_case_insensitive_file(pathbuf, std::string(romname) + ".cROM"); - if (!pFoundFile) + if (!pFoundFile && !realMachine) pFoundFile = find_case_insensitive_file(pathbuf, std::string(romname) + ".cRZ"); if (!pFoundFile) { @@ -2240,6 +2838,14 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, NoteStartupRssSample("before-crom-load"); result = Serum_LoadFilev1(pFoundFile->c_str(), flags); if (result) { + if (realMachine && g_serumData.SerumVersion != SERUM_V1) { + Log("Real-machine mode accepts raw loads only from Serum v1 .cROM; " + "rejecting %s", + pFoundFile->c_str()); + Serum_free(); + enabled = false; + return NULL; + } NoteStartupRssSample("after-crom-load"); LogLoadedColorizationSource(*pFoundFile, false); if (csvFoundFile && g_serumData.SerumVersion == SERUM_V2) { diff --git a/src/sparse-vector.h b/src/sparse-vector.h index b96f3e3..88b26f1 100644 --- a/src/sparse-vector.h +++ b/src/sparse-vector.h @@ -776,15 +776,14 @@ class SparseVector { } } - template - void readFromCRomFile(size_t elementSize, uint32_t numElements, FILE *stream, - SparseVector *parent = nullptr) { + template + void readFromCRomReader(size_t elementSize, uint32_t numElements, + Reader &reader, SparseVector *parent = nullptr) { if (useIndex) { index.resize(numElements); for (uint32_t i = 0; i < numElements; ++i) { index[i].resize(elementSize); - if (fread(index[i].data(), sizeof(T), elementSize, stream) != - elementSize) { + if (!reader.readExact(index[i].data(), elementSize * sizeof(T))) { fprintf(stderr, "File read error\n"); exit(1); } @@ -793,7 +792,7 @@ class SparseVector { std::vector tmp(elementSize); for (uint32_t i = 0; i < numElements; ++i) { - if (fread(tmp.data(), elementSize * sizeof(T), 1, stream) != 1) { + if (!reader.readExact(tmp.data(), elementSize * sizeof(T))) { fprintf(stderr, "File read error\n"); exit(1); } @@ -803,6 +802,18 @@ class SparseVector { } } + template + void readFromCRomFile(size_t elementSize, uint32_t numElements, FILE *stream, + SparseVector *parent = nullptr) { + struct FileReader { + FILE *stream; + bool readExact(void *dst, size_t bytes) { + return fread(dst, 1, bytes, stream) == bytes; + } + } reader{stream}; + readFromCRomReader(elementSize, numElements, reader, parent); + } + void reserve(size_t elementSize) { if (noData.size() < elementSize) { noData.resize(elementSize, noData[0]); From 20039affbe36093ce8c25c48491cf1c68c5993c1 Mon Sep 17 00:00:00 2001 From: Markus Kalkbrenner Date: Tue, 24 Mar 2026 10:05:58 +0100 Subject: [PATCH 2/5] v1 cRZ loading fix --- src/serum-decode.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/serum-decode.cpp b/src/serum-decode.cpp index 03f78e9..31302b0 100644 --- a/src/serum-decode.cpp +++ b/src/serum-decode.cpp @@ -2453,6 +2453,8 @@ static Serum_Frame_Struc* Serum_LoadFilev1Stream(Reader& reader, mySerum.width32 = g_serumData.fwidth; mySerum.width64 = 0; } + Full_Reset_ColorRotations(); + cromloaded = true; enabled = true; return &mySerum; } From a7e613cfe3a7cd7b3c42d249734ba5c3588a9134 Mon Sep 17 00:00:00 2001 From: Markus Kalkbrenner Date: Tue, 24 Mar 2026 11:33:36 +0100 Subject: [PATCH 3/5] removed redundant code --- src/serum-decode.cpp | 575 ++----------------------------------------- 1 file changed, 21 insertions(+), 554 deletions(-) diff --git a/src/serum-decode.cpp b/src/serum-decode.cpp index 31302b0..1cbe768 100644 --- a/src/serum-decode.cpp +++ b/src/serum-decode.cpp @@ -1300,45 +1300,6 @@ uint32_t calc_crc32(uint8_t* source, uint8_t mask, uint32_t n, uint8_t Shape) { return crc32_fast(source, pixels); } -bool unzip_crz(const char* const filename, const char* const extractpath, - char* cromname, int cromsize) { - bool ok = true; - mz_zip_archive zip_archive = {0}; - - if (!mz_zip_reader_init_file(&zip_archive, filename, 0)) { - return false; - } - - int num_files = mz_zip_reader_get_num_files(&zip_archive); - - if (num_files == 0 || - !mz_zip_reader_get_filename(&zip_archive, 0, cromname, cromsize)) { - mz_zip_reader_end(&zip_archive); - return false; - } - - for (int i = 0; i < num_files; i++) { - mz_zip_archive_file_stat file_stat; - if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) continue; - - char dstPath[pathbuflen]; - if (strcpy_s(dstPath, pathbuflen, extractpath)) goto fail; - if (strcat_s(dstPath, pathbuflen, file_stat.m_filename)) goto fail; - - mz_zip_reader_extract_file_to_file(&zip_archive, file_stat.m_filename, - dstPath, 0); - } - - goto nofail; -fail: - ok = false; -nofail: - - mz_zip_reader_end(&zip_archive); - - return ok; -} - struct FileCRomReader { FILE* stream = nullptr; @@ -1372,6 +1333,15 @@ static bool ReadBytes(Reader& reader, void* dst, size_t bytes) { return reader.readExact(dst, bytes); } +template +static Serum_Frame_Struc* Serum_LoadFilev2Stream(Reader& reader, + const uint8_t flags, + uint32_t sizeheader); + +template +static Serum_Frame_Struc* Serum_LoadFilev1Stream(Reader& reader, + const uint8_t flags); + static bool ExtractCRZEntryToMemory(const char* filename, std::vector& outData) { mz_zip_archive zip_archive = {0}; @@ -1604,325 +1574,11 @@ Serum_Frame_Struc* Serum_LoadConcentrate(const char* filename, } Serum_Frame_Struc* Serum_LoadFilev2(FILE* pfile, const uint8_t flags, - bool uncompressedCROM, char* pathbuf, uint32_t sizeheader) { - fread(&g_serumData.fwidth, 4, 1, pfile); - fread(&g_serumData.fheight, 4, 1, pfile); - fread(&g_serumData.fwidth_extra, 4, 1, pfile); - fread(&g_serumData.fheight_extra, 4, 1, pfile); - isoriginalrequested = false; - isextrarequested = false; - mySerum.width32 = 0; - mySerum.width64 = 0; - if (g_serumData.fheight == 32) { - if (flags & FLAG_REQUEST_32P_FRAMES) { - isoriginalrequested = true; - mySerum.width32 = g_serumData.fwidth; - } - if (flags & FLAG_REQUEST_64P_FRAMES) { - isextrarequested = true; - mySerum.width64 = g_serumData.fwidth_extra; - } - - } else { - if (flags & FLAG_REQUEST_64P_FRAMES) { - isoriginalrequested = true; - mySerum.width64 = g_serumData.fwidth; - } - if (flags & FLAG_REQUEST_32P_FRAMES) { - isextrarequested = true; - mySerum.width32 = g_serumData.fwidth_extra; - } - } - fread(&g_serumData.nframes, 4, 1, pfile); - fread(&g_serumData.nocolors, 4, 1, pfile); - mySerum.nocolors = g_serumData.nocolors; - if ((g_serumData.nframes == 0) || (g_serumData.nocolors == 0) || - !ValidateLoadedGeometry(true, "cROM/v2")) { - // incorrect file format - fclose(pfile); - enabled = false; - return NULL; - } - fread(&g_serumData.ncompmasks, 4, 1, pfile); - fread(&g_serumData.nsprites, 4, 1, pfile); - fread(&g_serumData.nbackgrounds, 2, 1, - pfile); // g_serumData.nbackgrounds is a uint16_t - if (sizeheader >= 20 * sizeof(uint32_t)) { - int is256x64; - fread(&is256x64, sizeof(int), 1, pfile); - g_serumData.is256x64 = (is256x64 != 0); - } - - frameshape = (uint8_t*)malloc(g_serumData.fwidth * g_serumData.fheight); - - if (flags & FLAG_REQUEST_32P_FRAMES) { - mySerum.frame32 = - (uint16_t*)malloc(32 * mySerum.width32 * sizeof(uint16_t)); - mySerum.rotations32 = (uint16_t*)malloc( - MAX_COLOR_ROTATION_V2 * MAX_LENGTH_COLOR_ROTATION * sizeof(uint16_t)); - mySerum.rotationsinframe32 = - (uint16_t*)malloc(2 * 32 * mySerum.width32 * sizeof(uint16_t)); - if (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS) - mySerum.modifiedelements32 = (uint8_t*)malloc(32 * mySerum.width32); - if (!mySerum.frame32 || !mySerum.rotations32 || - !mySerum.rotationsinframe32 || - (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS && - !mySerum.modifiedelements32)) { - Serum_free(); - fclose(pfile); - enabled = false; - return NULL; - } - } - if (flags & FLAG_REQUEST_64P_FRAMES) { - mySerum.frame64 = - (uint16_t*)malloc(64 * mySerum.width64 * sizeof(uint16_t)); - mySerum.rotations64 = (uint16_t*)malloc( - MAX_COLOR_ROTATION_V2 * MAX_LENGTH_COLOR_ROTATION * sizeof(uint16_t)); - mySerum.rotationsinframe64 = - (uint16_t*)malloc(2 * 64 * mySerum.width64 * sizeof(uint16_t)); - if (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS) - mySerum.modifiedelements64 = (uint8_t*)malloc(64 * mySerum.width64); - if (!mySerum.frame64 || !mySerum.rotations64 || - !mySerum.rotationsinframe64 || - (flags & FLAG_REQUEST_FILL_MODIFIED_ELEMENTS && - !mySerum.modifiedelements64)) { - Serum_free(); - fclose(pfile); - enabled = false; - return NULL; - } - } - - g_serumData.hashcodes.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.shapecompmode.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.compmaskID.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.compmasks.readFromCRomFile( - g_serumData.is256x64 ? (256 * 64) - : (g_serumData.fwidth * g_serumData.fheight), - g_serumData.ncompmasks, pfile); - g_serumData.isextraframe.readFromCRomFile(1, g_serumData.nframes, pfile); - if (isextrarequested) { - for (uint32_t ti = 0; ti < g_serumData.nframes; ti++) { - if (g_serumData.isextraframe[ti][0] > 0) { - mySerum.flags |= FLAG_RETURNED_EXTRA_AVAILABLE; - break; - } - } - } else - g_serumData.isextraframe.clearIndex(); - g_serumData.cframes_v2.readFromCRomFile( - g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, pfile); - g_serumData.cframes_v2_extra.readFromCRomFile( - g_serumData.fwidth_extra * g_serumData.fheight_extra, g_serumData.nframes, - pfile, &g_serumData.isextraframe); - g_serumData.dynamasks.readFromCRomFile( - g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, pfile); - g_serumData.dynamasks_extra.readFromCRomFile( - g_serumData.fwidth_extra * g_serumData.fheight_extra, g_serumData.nframes, - pfile, &g_serumData.isextraframe); - g_serumData.dyna4cols_v2.readFromCRomFile( - MAX_DYNA_SETS_PER_FRAME_V2 * g_serumData.nocolors, g_serumData.nframes, - pfile); - g_serumData.dyna4cols_v2_extra.readFromCRomFile( - MAX_DYNA_SETS_PER_FRAME_V2 * g_serumData.nocolors, g_serumData.nframes, - pfile, &g_serumData.isextraframe); - g_serumData.isextrasprite.readFromCRomFile(1, g_serumData.nsprites, pfile); - if (!isextrarequested) g_serumData.isextrasprite.clearIndex(); - g_serumData.framesprites.readFromCRomFile(MAX_SPRITES_PER_FRAME, - g_serumData.nframes, pfile); - g_serumData.spriteoriginal.readFromCRomFile( - MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, pfile); - g_serumData.spritecolored.readFromCRomFile( - MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, pfile); - g_serumData.spritemask_extra.readFromCRomFile( - MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, pfile, - &g_serumData.isextrasprite); - g_serumData.spritecolored_extra.readFromCRomFile( - MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, pfile, - &g_serumData.isextrasprite); - g_serumData.activeframes.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.colorrotations_v2.readFromCRomFile( - MAX_LENGTH_COLOR_ROTATION * MAX_COLOR_ROTATION_V2, g_serumData.nframes, - pfile); - g_serumData.colorrotations_v2_extra.readFromCRomFile( - MAX_LENGTH_COLOR_ROTATION * MAX_COLOR_ROTATION_V2, g_serumData.nframes, - pfile, &g_serumData.isextraframe); - g_serumData.spritedetdwords.readFromCRomFile(MAX_SPRITE_DETECT_AREAS, - g_serumData.nsprites, pfile); - g_serumData.spritedetdwordpos.readFromCRomFile(MAX_SPRITE_DETECT_AREAS, - g_serumData.nsprites, pfile); - g_serumData.spritedetareas.readFromCRomFile(4 * MAX_SPRITE_DETECT_AREAS, - g_serumData.nsprites, pfile); - g_serumData.triggerIDs.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.framespriteBB.readFromCRomFile(MAX_SPRITES_PER_FRAME * 4, - g_serumData.nframes, pfile, - &g_serumData.framesprites); - g_serumData.isextrabackground.readFromCRomFile(1, g_serumData.nbackgrounds, - pfile); - if (!isextrarequested) g_serumData.isextrabackground.clearIndex(); - g_serumData.backgroundframes_v2.readFromCRomFile( - g_serumData.fwidth * g_serumData.fheight, g_serumData.nbackgrounds, - pfile); - g_serumData.backgroundframes_v2_extra.readFromCRomFile( - g_serumData.fwidth_extra * g_serumData.fheight_extra, - g_serumData.nbackgrounds, pfile, &g_serumData.isextrabackground); - g_serumData.backgroundIDs.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.backgroundmask.readFromCRomFile( - g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, pfile, - &g_serumData.backgroundIDs); - g_serumData.backgroundmask_extra.readFromCRomFile( - g_serumData.fwidth_extra * g_serumData.fheight_extra, g_serumData.nframes, - pfile, &g_serumData.backgroundIDs); - - if (sizeheader >= 15 * sizeof(uint32_t)) { - g_serumData.dynashadowsdir.readFromCRomFile(MAX_DYNA_SETS_PER_FRAME_V2, - g_serumData.nframes, pfile); - g_serumData.dynashadowscol.readFromCRomFile(MAX_DYNA_SETS_PER_FRAME_V2, - g_serumData.nframes, pfile); - g_serumData.dynashadowsdir_extra.readFromCRomFile( - MAX_DYNA_SETS_PER_FRAME_V2, g_serumData.nframes, pfile, - &g_serumData.isextraframe); - g_serumData.dynashadowscol_extra.readFromCRomFile( - MAX_DYNA_SETS_PER_FRAME_V2, g_serumData.nframes, pfile, - &g_serumData.isextraframe); - } else { - g_serumData.dynashadowsdir.reserve(MAX_DYNA_SETS_PER_FRAME_V2); - g_serumData.dynashadowscol.reserve(MAX_DYNA_SETS_PER_FRAME_V2); - g_serumData.dynashadowsdir_extra.reserve(MAX_DYNA_SETS_PER_FRAME_V2); - g_serumData.dynashadowscol_extra.reserve(MAX_DYNA_SETS_PER_FRAME_V2); - } - - if (sizeheader >= 18 * sizeof(uint32_t)) { - g_serumData.dynasprite4cols.readFromCRomFile( - MAX_DYNA_SETS_PER_SPRITE * g_serumData.nocolors, g_serumData.nsprites, - pfile); - g_serumData.dynasprite4cols_extra.readFromCRomFile( - MAX_DYNA_SETS_PER_SPRITE * g_serumData.nocolors, g_serumData.nsprites, - pfile, &g_serumData.isextraframe); - g_serumData.dynaspritemasks.readFromCRomFile( - MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, pfile); - g_serumData.dynaspritemasks_extra.readFromCRomFile( - MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT, g_serumData.nsprites, pfile, - &g_serumData.isextraframe); - } else { - g_serumData.dynasprite4cols.reserve(MAX_DYNA_SETS_PER_SPRITE * - g_serumData.nocolors); - g_serumData.dynasprite4cols_extra.reserve(MAX_DYNA_SETS_PER_SPRITE * - g_serumData.nocolors); - g_serumData.dynaspritemasks.reserve(MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT); - g_serumData.dynaspritemasks_extra.reserve(MAX_SPRITE_WIDTH * - MAX_SPRITE_HEIGHT); - } - - if (sizeheader >= 19 * sizeof(uint32_t)) { - g_serumData.sprshapemode.readFromCRomFile(1, g_serumData.nsprites, pfile); - for (uint32_t i = 0; i < g_serumData.nsprites; i++) { - if (g_serumData.sprshapemode[i][0] > 0) { - for (uint32_t j = 0; j < MAX_SPRITE_DETECT_AREAS; j++) { - uint32_t detdwords = g_serumData.spritedetdwords[i][j]; - if ((detdwords & 0xFF000000) > 0) - detdwords = (detdwords & 0x00FFFFFF) | 0x01000000; - if ((detdwords & 0x00FF0000) > 0) - detdwords = (detdwords & 0xFF00FFFF) | 0x00010000; - if ((detdwords & 0x0000FF00) > 0) - detdwords = (detdwords & 0xFFFF00FF) | 0x00000100; - if ((detdwords & 0x000000FF) > 0) - detdwords = (detdwords & 0xFFFFFF00) | 0x00000001; - g_serumData.spritedetdwords[i][j] = detdwords; - } - for (uint32_t j = 0; j < MAX_SPRITE_WIDTH * MAX_SPRITE_HEIGHT; j++) { - if (g_serumData.spriteoriginal[i][j] > 0 && - g_serumData.spriteoriginal[i][j] != 255) - g_serumData.spriteoriginal[i][j] = 1; - } - } - } - } else { - g_serumData.sprshapemode.reserve(g_serumData.nsprites); - } - - g_serumData.BuildPackingSidecarsAndNormalize(); - + FileCRomReader reader{pfile}; + Serum_Frame_Struc* result = Serum_LoadFilev2Stream(reader, flags, sizeheader); fclose(pfile); - - mySerum.ntriggers = 0; - uint32_t framespos = g_serumData.nframes / 2; - uint32_t framesspace = g_serumData.nframes - framespos; - uint32_t framescount = (framesspace + 9) / 10; - - if (framescount > 0) { - std::vector candidates; - candidates.reserve(framesspace); - for (uint32_t ti = framespos; ti < g_serumData.nframes; ++ti) { - if (g_serumData.triggerIDs[ti][0] == 0xffffffff) { - candidates.push_back(ti); - } - } - - if (!candidates.empty()) { - std::mt19937 rng(0xC0DE1234); - std::shuffle(candidates.begin(), candidates.end(), rng); - std::uniform_int_distribution triggerDist(65433u, 0xfffffffeu); - - uint32_t toAssign = std::min(framescount, candidates.size()); - for (uint32_t i = 0; i < toAssign; ++i) { - uint32_t triggerValue = triggerDist(rng); - g_serumData.triggerIDs.set(candidates[i], &triggerValue, 1); - } - - for (uint32_t offset = 0; (framespos + offset) < g_serumData.nframes; - ++offset) { - uint32_t idx = framespos + offset; - if (g_serumData.triggerIDs[idx][0] == 0xffffffff) { - uint32_t triggerValue = triggerDist(rng); - g_serumData.triggerIDs.set(idx, &triggerValue, 1); - break; - } - } - } - } - for (uint32_t ti = 0; ti < g_serumData.nframes; ti++) { - // Every trigger ID greater than PUP_TRIGGER_MAX_THRESHOLD is an internal - // trigger for rotation scenes and must not be communicated to the PUP - // Player. - if (g_serumData.triggerIDs[ti][0] < PUP_TRIGGER_MAX_THRESHOLD) - mySerum.ntriggers++; - } - framechecked = (bool*)malloc(sizeof(bool) * g_serumData.nframes); - if (!framechecked) { - Serum_free(); - enabled = false; - return NULL; - } - if (flags & FLAG_REQUEST_32P_FRAMES) { - if (g_serumData.fheight == 32) - mySerum.width32 = g_serumData.fwidth; - else - mySerum.width32 = g_serumData.fwidth_extra; - } else - mySerum.width32 = 0; - if (flags & FLAG_REQUEST_64P_FRAMES) { - if (g_serumData.fheight == 32) - mySerum.width64 = g_serumData.fwidth_extra; - else - mySerum.width64 = g_serumData.fwidth; - } else - mySerum.width64 = 0; - - mySerum.SerumVersion = g_serumData.SerumVersion = SERUM_V2; - - Full_Reset_ColorRotations(); - cromloaded = true; - - if (!uncompressedCROM) { - // remove temporary file that had been extracted from compressed CRZ file - remove(pathbuf); - } - - enabled = true; - return &mySerum; + return result; } template @@ -2481,219 +2137,30 @@ Serum_Frame_Struc* Serum_LoadFilev1(const char* const filename, return Serum_LoadFilev1Stream(reader, flags); } - char pathbuf[pathbuflen]; - if (strcpy_s(pathbuf, pathbuflen, filename)) return NULL; - - // Open cRom FILE* pfile; - pfile = fopen(pathbuf, "rb"); + pfile = fopen(filename, "rb"); if (!pfile) { enabled = false; return NULL; } - // read the header to know how much memory is needed - fread(g_serumData.rname, 1, 64, pfile); - uint32_t sizeheader; - fread(&sizeheader, 4, 1, pfile); - // if this is a new format file, we load with Serum_LoadNewFile() - if (sizeheader >= 14 * sizeof(uint32_t)) - return Serum_LoadFilev2(pfile, flags, uncompressedCROM, pathbuf, - sizeheader); - mySerum.SerumVersion = g_serumData.SerumVersion = SERUM_V1; - fread(&g_serumData.fwidth, 4, 1, pfile); - fread(&g_serumData.fheight, 4, 1, pfile); - // The serum file stored the number of frames as uint32_t, but in fact, the - // number of frames will never exceed the size of uint16_t (65535) - uint32_t nframes32; - fread(&nframes32, 4, 1, pfile); - g_serumData.nframes = (uint16_t)nframes32; - fread(&g_serumData.nocolors, 4, 1, pfile); - mySerum.nocolors = g_serumData.nocolors; - fread(&g_serumData.nccolors, 4, 1, pfile); - if ((g_serumData.fwidth == 0) || (g_serumData.fheight == 0) || - (g_serumData.nframes == 0) || (g_serumData.nocolors == 0) || - (g_serumData.nccolors == 0)) { - // incorrect file format - fclose(pfile); - enabled = false; - return NULL; - } - if (!ValidateLoadedGeometry(false, "cROM/v1")) { + FileCRomReader reader{pfile}; + if (!ReadBytes(reader, g_serumData.rname, 64)) { fclose(pfile); enabled = false; return NULL; } - fread(&g_serumData.ncompmasks, 4, 1, pfile); - fread(&g_serumData.nmovmasks, 4, 1, pfile); - fread(&g_serumData.nsprites, 4, 1, pfile); - if (sizeheader >= 13 * sizeof(uint32_t)) - fread(&g_serumData.nbackgrounds, 2, 1, pfile); - else - g_serumData.nbackgrounds = 0; - // allocate memory for the serum format - uint8_t* spritedescriptionso = (uint8_t*)malloc( - g_serumData.nsprites * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); - uint8_t* spritedescriptionsc = (uint8_t*)malloc( - g_serumData.nsprites * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); - - mySerum.frame = (uint8_t*)malloc(g_serumData.fwidth * g_serumData.fheight); - mySerum.palette = (uint8_t*)malloc(3 * 64); - mySerum.rotations = (uint8_t*)malloc(MAX_COLOR_ROTATIONS * 3); - if (((g_serumData.nsprites > 0) && - (!spritedescriptionso || !spritedescriptionsc)) || - !mySerum.frame || !mySerum.palette || !mySerum.rotations) { - Serum_free(); + uint32_t sizeheader; + if (!ReadValue(reader, sizeheader)) { fclose(pfile); enabled = false; return NULL; } - // read the cRom file - g_serumData.hashcodes.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.shapecompmode.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.compmaskID.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.movrctID.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.movrctID.clear(); // we don't need this anymore, but we need to - // read it to skip the data in the file - g_serumData.compmasks.readFromCRomFile( - g_serumData.fwidth * g_serumData.fheight, g_serumData.ncompmasks, pfile); - g_serumData.movrcts.readFromCRomFile(g_serumData.fwidth * g_serumData.fheight, - g_serumData.nmovmasks, pfile); - g_serumData.movrcts.clear(); // we don't need this anymore, but we need to - // read it to skip the data in the file - g_serumData.cpal.readFromCRomFile(3 * g_serumData.nccolors, - g_serumData.nframes, pfile); - g_serumData.cframes.readFromCRomFile(g_serumData.fwidth * g_serumData.fheight, - g_serumData.nframes, pfile); - g_serumData.dynamasks.readFromCRomFile( - g_serumData.fwidth * g_serumData.fheight, g_serumData.nframes, pfile); - g_serumData.dyna4cols.readFromCRomFile( - MAX_DYNA_4COLS_PER_FRAME * g_serumData.nocolors, g_serumData.nframes, - pfile); - g_serumData.framesprites.readFromCRomFile(MAX_SPRITES_PER_FRAME, - g_serumData.nframes, pfile); - - for (int ti = 0; - ti < (int)g_serumData.nsprites * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE; - ti++) { - fread(&spritedescriptionsc[ti], 1, 1, pfile); - fread(&spritedescriptionso[ti], 1, 1, pfile); - } - for (uint32_t i = 0; i < g_serumData.nsprites; i++) { - g_serumData.spritedescriptionsc.set( - i, &spritedescriptionsc[i * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE], - MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); - g_serumData.spritedescriptionso.set( - i, &spritedescriptionso[i * MAX_SPRITE_SIZE * MAX_SPRITE_SIZE], - MAX_SPRITE_SIZE * MAX_SPRITE_SIZE); - } - Free_element((void**)&spritedescriptionso); - Free_element((void**)&spritedescriptionsc); - - g_serumData.activeframes.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.colorrotations.readFromCRomFile(3 * MAX_COLOR_ROTATIONS, - g_serumData.nframes, pfile); - g_serumData.spritedetdwords.readFromCRomFile(MAX_SPRITE_DETECT_AREAS, - g_serumData.nsprites, pfile); - g_serumData.spritedetdwordpos.readFromCRomFile(MAX_SPRITE_DETECT_AREAS, - g_serumData.nsprites, pfile); - g_serumData.spritedetareas.readFromCRomFile(4 * MAX_SPRITE_DETECT_AREAS, - g_serumData.nsprites, pfile); - mySerum.ntriggers = 0; - if (sizeheader >= 11 * sizeof(uint32_t)) { - g_serumData.triggerIDs.readFromCRomFile(1, g_serumData.nframes, pfile); - } - uint32_t framespos = g_serumData.nframes / 2; - uint32_t framesspace = g_serumData.nframes - framespos; - uint32_t framescount = (framesspace + 9) / 10; - - if (framescount > 0) { - std::vector candidates; - candidates.reserve(framesspace); - for (uint32_t ti = framespos; ti < g_serumData.nframes; ++ti) { - if (g_serumData.triggerIDs[ti][0] == 0xffffffff) { - candidates.push_back(ti); - } - } - - if (!candidates.empty()) { - std::mt19937 rng(0xC0DE1234); - std::shuffle(candidates.begin(), candidates.end(), rng); - std::uniform_int_distribution triggerDist(65433u, 0xfffffffeu); - - uint32_t toAssign = std::min(framescount, candidates.size()); - for (uint32_t i = 0; i < toAssign; ++i) { - uint32_t triggerValue = triggerDist(rng); - g_serumData.triggerIDs.set(candidates[i], &triggerValue, 1); - } - - for (uint32_t offset = 0; (framespos + offset) < g_serumData.nframes; - ++offset) { - uint32_t idx = framespos + offset; - if (g_serumData.triggerIDs[idx][0] == 0xffffffff) { - uint32_t triggerValue = triggerDist(rng); - g_serumData.triggerIDs.set(idx, &triggerValue, 1); - break; - } - } - } - } - for (uint32_t ti = 0; ti < g_serumData.nframes; ti++) { - if (g_serumData.triggerIDs[ti][0] != 0xffffffff) mySerum.ntriggers++; - } - if (sizeheader >= 12 * sizeof(uint32_t)) - g_serumData.framespriteBB.readFromCRomFile(MAX_SPRITES_PER_FRAME * 4, - g_serumData.nframes, pfile, - &g_serumData.framesprites); - else { - for (uint32_t tj = 0; tj < g_serumData.nframes; tj++) { - uint16_t tmp_framespriteBB[4 * MAX_SPRITES_PER_FRAME]; - for (uint32_t ti = 0; ti < MAX_SPRITES_PER_FRAME; ti++) { - tmp_framespriteBB[ti * 4] = 0; - tmp_framespriteBB[ti * 4 + 1] = 0; - tmp_framespriteBB[ti * 4 + 2] = g_serumData.fwidth - 1; - tmp_framespriteBB[ti * 4 + 3] = g_serumData.fheight - 1; - } - g_serumData.framespriteBB.set(tj, tmp_framespriteBB, - MAX_SPRITES_PER_FRAME * 4); - } - } - if (sizeheader >= 13 * sizeof(uint32_t)) { - g_serumData.backgroundframes.readFromCRomFile( - g_serumData.fwidth * g_serumData.fheight, g_serumData.nbackgrounds, - pfile); - g_serumData.backgroundIDs.readFromCRomFile(1, g_serumData.nframes, pfile); - g_serumData.backgroundBB.readFromCRomFile(4, g_serumData.nframes, pfile, - &g_serumData.backgroundIDs); - } - - g_serumData.BuildPackingSidecarsAndNormalize(); + if (sizeheader >= 14 * sizeof(uint32_t)) + return Serum_LoadFilev2(pfile, flags, sizeheader); + Serum_Frame_Struc* result = Serum_LoadFilev1Stream(reader, flags); fclose(pfile); - - // allocate memory for previous detected frame - framechecked = (bool*)malloc(sizeof(bool) * g_serumData.nframes); - if (!framechecked) { - Serum_free(); - enabled = false; - return NULL; - } - if (g_serumData.fheight == 64) { - mySerum.width64 = g_serumData.fwidth; - mySerum.width32 = 0; - } else { - mySerum.width32 = g_serumData.fwidth; - mySerum.width64 = 0; - } - Full_Reset_ColorRotations(); - cromloaded = true; - - if (!uncompressedCROM) { - // remove temporary file that had been extracted from compressed CRZ file - remove(pathbuf); - } - - enabled = true; - return &mySerum; + return result; } SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, From 5ca68ab6147f2672cc41209fed0ade0f64b47085 Mon Sep 17 00:00:00 2001 From: Markus Kalkbrenner Date: Tue, 24 Mar 2026 12:16:31 +0100 Subject: [PATCH 4/5] on real-machine runtime accept only `*.cROMc` --- AGENTS.md | 14 ++++++++----- src/serum-decode.cpp | 50 +++++++------------------------------------- 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 17aa714..38c138a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,11 +129,8 @@ Entry point: `Serum_Load(altcolorpath, romname, flags)`. 2. On real-machine runtime (`is_real_machine()==true`): - do not scan or apply `*.pup.csv` - ignore `skip-cromc.txt` - - accept `*.cROMc` only for Serum v2 content - - if no acceptable `*.cROMc` is available, fall back only to plain `*.cROM` - - raw real-machine fallback must resolve to Serum v1 content; reject raw - Serum v2 loads - - skip `*.cRZ` entirely on real-machine runtime + - accept only `*.cROMc` + - do not fall back to `*.cROM` or `*.cRZ` 3. On non-real-machine/runtime-update flows: - look for optional `*.pup.csv` - prefer loading `*.cROMc` unless `skip-cromc.txt` exists. @@ -141,6 +138,9 @@ Entry point: `Serum_Load(altcolorpath, romname, flags)`. - Otherwise, try encrypted in-memory load (`vault::read` + `SerumData::LoadFromBuffer`). 4. If cROMc load fails or is absent on non-real-machine/runtime-update flows, load `*.cROM`/`*.cRZ`. + - Raw source loads are authoring/update inputs and may be re-saved as + `*.cROMc` when `WRITE_CROMC` is enabled and runtime generation is not + disabled through `Serum_SetGenerateCRomC(false)`. 5. If CSV exists and format is v2, parse scenes via `SceneGenerator::parseCSV`. 6. Set scene depth from color count when scenes are active. 7. Build or restore frame lookup acceleration: @@ -349,6 +349,10 @@ Flags (from `serum.h`): ## cROMc persistence Current concentrate version: **6**. +`cROMc` stores the full Serum model and supports both Serum v1 and Serum v2 +content. Real-machine policy may still restrict which source formats are +accepted at load time, but the persisted `cROMc` format itself is not v2-only. + Stored in v6: - Full Serum model payload. - Scene data (`SceneGenerator` scene vector). diff --git a/src/serum-decode.cpp b/src/serum-decode.cpp index 1cbe768..043c439 100644 --- a/src/serum-decode.cpp +++ b/src/serum-decode.cpp @@ -1573,14 +1573,6 @@ Serum_Frame_Struc* Serum_LoadConcentrate(const char* filename, return Serum_LoadConcentratePrepared(flags); } -Serum_Frame_Struc* Serum_LoadFilev2(FILE* pfile, const uint8_t flags, - uint32_t sizeheader) { - FileCRomReader reader{pfile}; - Serum_Frame_Struc* result = Serum_LoadFilev2Stream(reader, flags, sizeheader); - fclose(pfile); - return result; -} - template static Serum_Frame_Struc* Serum_LoadFilev2Stream(Reader& reader, const uint8_t flags, @@ -2143,21 +2135,7 @@ Serum_Frame_Struc* Serum_LoadFilev1(const char* const filename, enabled = false; return NULL; } - FileCRomReader reader{pfile}; - if (!ReadBytes(reader, g_serumData.rname, 64)) { - fclose(pfile); - enabled = false; - return NULL; - } - uint32_t sizeheader; - if (!ReadValue(reader, sizeheader)) { - fclose(pfile); - enabled = false; - return NULL; - } - if (sizeheader >= 14 * sizeof(uint32_t)) - return Serum_LoadFilev2(pfile, flags, sizeheader); Serum_Frame_Struc* result = Serum_LoadFilev1Stream(reader, flags); fclose(pfile); return result; @@ -2272,17 +2250,8 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, result = Serum_LoadConcentrate(pFoundFile->c_str(), flags); loadedFromConcentrate = (result != NULL); if (result) { - if (g_serumData.SerumVersion != SERUM_V2) { - Log("Real-machine mode accepts .cROMc only for Serum v2; rejecting " - "%s", - pFoundFile->c_str()); - Serum_free(); - result = NULL; - loadedFromConcentrate = false; - } else { - NoteStartupRssSample("after-cromc-load"); - LogLoadedColorizationSource(*pFoundFile, true); - } + NoteStartupRssSample("after-cromc-load"); + LogLoadedColorizationSource(*pFoundFile, true); } else { Log("Failed to load %s", pFoundFile->c_str()); } @@ -2290,13 +2259,18 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, } if (!result) { + if (realMachine) { + Log("Real-machine mode supports only .cROMc loads for %s", romname); + enabled = false; + return NULL; + } #ifdef WRITE_CROMC // by default, we request both frame types flags |= FLAG_REQUEST_32P_FRAMES | FLAG_REQUEST_64P_FRAMES; #endif pFoundFile = find_case_insensitive_file(pathbuf, std::string(romname) + ".cROM"); - if (!pFoundFile && !realMachine) + if (!pFoundFile) pFoundFile = find_case_insensitive_file(pathbuf, std::string(romname) + ".cRZ"); if (!pFoundFile) { @@ -2307,14 +2281,6 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, NoteStartupRssSample("before-crom-load"); result = Serum_LoadFilev1(pFoundFile->c_str(), flags); if (result) { - if (realMachine && g_serumData.SerumVersion != SERUM_V1) { - Log("Real-machine mode accepts raw loads only from Serum v1 .cROM; " - "rejecting %s", - pFoundFile->c_str()); - Serum_free(); - enabled = false; - return NULL; - } NoteStartupRssSample("after-crom-load"); LogLoadedColorizationSource(*pFoundFile, false); if (csvFoundFile && g_serumData.SerumVersion == SERUM_V2) { From 90837beed75f3b05936aeab673384d8e2e6c1550 Mon Sep 17 00:00:00 2001 From: Markus Kalkbrenner Date: Tue, 24 Mar 2026 12:48:07 +0100 Subject: [PATCH 5/5] fixed pup.csv handling --- AGENTS.md | 7 +++++++ src/serum-decode.cpp | 44 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 38c138a..fb8c129 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,7 +141,14 @@ Entry point: `Serum_Load(altcolorpath, romname, flags)`. - Raw source loads are authoring/update inputs and may be re-saved as `*.cROMc` when `WRITE_CROMC` is enabled and runtime generation is not disabled through `Serum_SetGenerateCRomC(false)`. + - When such a raw source load produces a new `*.cROMc`, libserum reloads + that generated `*.cROMc` in the same load cycle so first-run behavior + matches the next boot. 5. If CSV exists and format is v2, parse scenes via `SceneGenerator::parseCSV`. + - If CSV parsing updates scene data for a loaded `*.cROMc` and `WRITE_CROMC` + generation is enabled, libserum rewrites the `*.cROMc` and reloads it in + the same load cycle so the current process immediately uses the persisted + scene-aware data. 6. Set scene depth from color count when scenes are active. 7. Build or restore frame lookup acceleration: - If loaded from cROMc v6 and no CSV update in this run: use stored lookup diff --git a/src/serum-decode.cpp b/src/serum-decode.cpp index 043c439..c7b4ede 100644 --- a/src/serum-decode.cpp +++ b/src/serum-decode.cpp @@ -1404,6 +1404,17 @@ uint32_t min(uint32_t v1, uint32_t v2) { long serum_file_length; +static std::string BuildConcentratePathFromSourcePath(const char* filename) { + std::string concentratePath; + if (const char* dot = strrchr(filename, '.')) { + concentratePath = std::string(filename, dot); + } else { + concentratePath = filename; + } + concentratePath += ".cROMc"; + return concentratePath; +} + bool Serum_SaveConcentrate(const char* filename) { if (!cromloaded || is_real_machine()) return false; if (g_serumData.sceneGenerator && g_serumData.sceneGenerator->isActive()) { @@ -1411,14 +1422,8 @@ bool Serum_SaveConcentrate(const char* filename) { } BuildFrameLookupVectors(); - std::string concentratePath; - - // Remove extension and add .cROMc - if (const char* dot = strrchr(filename, '.')) { - concentratePath = std::string(filename, dot); - } - - concentratePath += ".cROMc"; + const std::string concentratePath = + BuildConcentratePathFromSourcePath(filename); return g_serumData.SaveToFile(concentratePath.c_str()); } @@ -2209,6 +2214,7 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, Serum_Frame_Struc* result = NULL; bool loadedFromConcentrate = false; bool sceneDataUpdatedFromCsv = false; + std::optional reloadConcentratePath; std::optional pFoundFile; if (!realMachine) { std::optional skipFoundFile = @@ -2233,7 +2239,9 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, NoteStartupRssSample("after-csv-update"); #ifdef WRITE_CROMC // Update the concentrate file with new PUP data - if (generateCRomC) Serum_SaveConcentrate(pFoundFile->c_str()); + if (generateCRomC && Serum_SaveConcentrate(pFoundFile->c_str())) { + reloadConcentratePath = *pFoundFile; + } #endif } } else { @@ -2291,12 +2299,28 @@ SERUM_API Serum_Frame_Struc* Serum_Load(const char* const altcolorpath, } } #ifdef WRITE_CROMC - if (generateCRomC) Serum_SaveConcentrate(pFoundFile->c_str()); + if (generateCRomC && Serum_SaveConcentrate(pFoundFile->c_str())) { + reloadConcentratePath = + BuildConcentratePathFromSourcePath(pFoundFile->c_str()); + } #endif } else { Log("Failed to load %s", pFoundFile->c_str()); } } + if (reloadConcentratePath) { + NoteStartupRssSample("before-cromc-reload"); + Serum_free(); + result = Serum_LoadConcentrate(reloadConcentratePath->c_str(), flags); + loadedFromConcentrate = (result != NULL); + sceneDataUpdatedFromCsv = false; + if (result) { + NoteStartupRssSample("after-cromc-reload"); + LogLoadedColorizationSource(*reloadConcentratePath, true); + } else { + Log("Failed to reload %s after update", reloadConcentratePath->c_str()); + } + } if (result && g_serumData.sceneGenerator->isActive()) g_serumData.sceneGenerator->setDepth(result->nocolors == 16 ? 4 : 2); if (result) {