diff --git a/src/ngcw/lfg.c b/src/ngcw/lfg.c new file mode 100644 index 0000000..c8665eb --- /dev/null +++ b/src/ngcw/lfg.c @@ -0,0 +1,194 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see + * . + * + * Lagged Fibonacci Generator for Nintendo GameCube/Wii junk fill. + * Based on Dolphin emulator's LaggedFibonacciGenerator (CC0 licensed). + */ + +#include "lfg.h" + +#include + +static inline uint32_t swap32(uint32_t x) +{ return ((x >> 24) & 0xFF) | ((x >> 8) & 0xFF00) | ((x << 8) & 0xFF0000) | ((x << 24) & 0xFF000000); } + +static void lfg_forward(struct ngc_lfg_ctx *ctx) +{ + for(size_t i = 0; i < NGC_LFG_J; i++) ctx->buffer[i] ^= ctx->buffer[i + NGC_LFG_K - NGC_LFG_J]; + + for(size_t i = NGC_LFG_J; i < NGC_LFG_K; i++) ctx->buffer[i] ^= ctx->buffer[i - NGC_LFG_J]; +} + +static void lfg_backward(struct ngc_lfg_ctx *ctx, size_t start_word, size_t end_word) +{ + size_t loop_end = NGC_LFG_J > start_word ? NGC_LFG_J : start_word; + size_t upper = end_word < NGC_LFG_K ? end_word : NGC_LFG_K; + + for(size_t i = upper; i > loop_end; --i) ctx->buffer[i - 1] ^= ctx->buffer[i - 1 - NGC_LFG_J]; + + size_t upper2 = end_word < NGC_LFG_J ? end_word : NGC_LFG_J; + + for(size_t i = upper2; i > start_word; --i) ctx->buffer[i - 1] ^= ctx->buffer[i - 1 + NGC_LFG_K - NGC_LFG_J]; +} + +static bool lfg_initialize(struct ngc_lfg_ctx *ctx, bool check_existing) +{ + for(size_t i = NGC_LFG_SEED_SIZE; i < NGC_LFG_K; i++) + { + uint32_t calculated = (ctx->buffer[i - 17] << 23) ^ (ctx->buffer[i - 16] >> 9) ^ ctx->buffer[i - 1]; + + if(check_existing) + { + uint32_t actual = (ctx->buffer[i] & 0xFF00FFFF) | (ctx->buffer[i] << 2 & 0x00FC0000); + + if((calculated & 0xFFFCFFFF) != actual) return false; + } + + ctx->buffer[i] = calculated; + } + + /* Apply the shift-by-18-instead-of-16 quirk + byteswap */ + for(size_t i = 0; i < NGC_LFG_K; i++) + ctx->buffer[i] = swap32((ctx->buffer[i] & 0xFF00FFFF) | ((ctx->buffer[i] >> 2) & 0x00FF0000)); + + for(int i = 0; i < 4; i++) lfg_forward(ctx); + + return true; +} + +void ngc_lfg_set_seed(struct ngc_lfg_ctx *ctx, const uint32_t seed[NGC_LFG_SEED_SIZE]) +{ + ctx->position_bytes = 0; + + for(size_t i = 0; i < NGC_LFG_SEED_SIZE; i++) ctx->buffer[i] = swap32(seed[i]); + + lfg_initialize(ctx, false); +} + +void ngc_lfg_get_bytes(struct ngc_lfg_ctx *ctx, uint8_t *out, size_t count) +{ + while(count > 0) + { + size_t avail = NGC_LFG_K * sizeof(uint32_t) - ctx->position_bytes; + size_t chunk = count < avail ? count : avail; + + memcpy(out, (uint8_t *)ctx->buffer + ctx->position_bytes, chunk); + + ctx->position_bytes += chunk; + count -= chunk; + out += chunk; + + if(ctx->position_bytes == NGC_LFG_K * sizeof(uint32_t)) + { + lfg_forward(ctx); + ctx->position_bytes = 0; + } + } +} + +static bool lfg_reinitialize(struct ngc_lfg_ctx *ctx, uint32_t seed_out[NGC_LFG_SEED_SIZE]) +{ + for(int i = 0; i < 4; i++) lfg_backward(ctx, 0, NGC_LFG_K); + + for(size_t i = 0; i < NGC_LFG_K; i++) ctx->buffer[i] = swap32(ctx->buffer[i]); + + /* Reconstruct bits lost by the shift-by-18-instead-of-16 quirk */ + for(size_t i = 0; i < NGC_LFG_SEED_SIZE; i++) + { + ctx->buffer[i] = (ctx->buffer[i] & 0xFF00FFFF) | (ctx->buffer[i] << 2 & 0x00FC0000) | + ((ctx->buffer[i + 16] ^ ctx->buffer[i + 15]) << 9 & 0x00030000); + } + + for(size_t i = 0; i < NGC_LFG_SEED_SIZE; i++) seed_out[i] = swap32(ctx->buffer[i]); + + return lfg_initialize(ctx, true); +} + +size_t ngc_lfg_get_seed(const uint8_t *data, size_t size, size_t data_offset, uint32_t seed_out[NGC_LFG_SEED_SIZE]) +{ + /* Alignment: data - data_offset must be 4-byte aligned */ + if(((uintptr_t)data - data_offset) % sizeof(uint32_t) != 0) return 0; + + /* Work on whole u32 words */ + size_t bytes_to_skip = ((data_offset + 3) & ~(size_t)3) - data_offset; + + if(bytes_to_skip > size) return 0; + + const uint32_t *u32_data = (const uint32_t *)(data + bytes_to_skip); + size_t u32_size = (size - bytes_to_skip) / sizeof(uint32_t); + size_t u32_data_offset = (data_offset + bytes_to_skip) / sizeof(uint32_t); + + if(u32_size < NGC_LFG_K) return 0; + + /* Quick sanity check: the top bits have a specific pattern from the shift quirk */ + for(size_t i = 0; i < NGC_LFG_K; i++) + { + uint32_t x = swap32(u32_data[i]); + + if((x & 0x00C00000) != (x >> 2 & 0x00C00000)) return 0; + } + + struct ngc_lfg_ctx lfg; + size_t data_offset_mod_k = u32_data_offset % NGC_LFG_K; + size_t data_offset_div_k = u32_data_offset / NGC_LFG_K; + + /* Place the data into the buffer at the correct position. + * Copy raw native-endian u32 values — NO byte-swapping here. + * The swap happens later inside lfg_reinitialize/lfg_initialize. */ + size_t first_part = NGC_LFG_K - data_offset_mod_k; + + if(first_part > NGC_LFG_K) first_part = NGC_LFG_K; + + for(size_t i = 0; i < first_part && i < NGC_LFG_K; i++) lfg.buffer[data_offset_mod_k + i] = u32_data[i]; + + for(size_t i = 0; i < data_offset_mod_k; i++) lfg.buffer[i] = u32_data[first_part + i]; + + lfg_backward(&lfg, 0, data_offset_mod_k); + + for(size_t i = 0; i < data_offset_div_k; i++) lfg_backward(&lfg, 0, NGC_LFG_K); + + if(!lfg_reinitialize(&lfg, seed_out)) return 0; + + lfg.position_bytes = data_offset % (NGC_LFG_K * sizeof(uint32_t)); + + /* Advance the LFG forward to match the data_offset position. */ + for(size_t i = 0; i < data_offset_div_k; i++) lfg_forward(&lfg); + + /* Count how many bytes from data match the LFG output */ + size_t result = 0; + const uint8_t *p = data; + const uint8_t *end = data + size; + + while(p < end) + { + uint8_t expected = ((uint8_t *)lfg.buffer)[lfg.position_bytes]; + + if(*p != expected) break; + + result++; + p++; + lfg.position_bytes++; + + if(lfg.position_bytes == NGC_LFG_K * sizeof(uint32_t)) + { + lfg_forward(&lfg); + lfg.position_bytes = 0; + } + } + + return result; +} diff --git a/src/ngcw/lfg.h b/src/ngcw/lfg.h new file mode 100644 index 0000000..b01163d --- /dev/null +++ b/src/ngcw/lfg.h @@ -0,0 +1,86 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see + * . + * + * Lagged Fibonacci Generator for Nintendo GameCube/Wii junk fill. + * Based on Dolphin emulator's LaggedFibonacciGenerator (CC0 licensed). + */ + +#ifndef LIBAARUFORMAT_NGCW_LFG_H +#define LIBAARUFORMAT_NGCW_LFG_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define NGC_LFG_K 521 /**< LFG buffer size (number of uint32 words in state). */ +#define NGC_LFG_J 32 /**< LFG second tap. */ +#define NGC_LFG_SEED_SIZE 17 /**< Number of uint32 words needed to seed the LFG. */ + + /** + * @brief LFG context holding the 521-word circular buffer and byte position. + */ + struct ngc_lfg_ctx + { + uint32_t buffer[NGC_LFG_K]; + size_t position_bytes; + }; + + /** + * @brief Initialize the LFG from a 17-word big-endian seed. + * + * @param ctx LFG context to initialize. + * @param seed Array of 17 uint32 seed words (big-endian). + */ + void ngc_lfg_set_seed(struct ngc_lfg_ctx *ctx, const uint32_t seed[NGC_LFG_SEED_SIZE]); + + /** + * @brief Generate count bytes of junk data into out. + * + * @param ctx LFG context (must be initialized via ngc_lfg_set_seed). + * @param out Output buffer to fill. + * @param count Number of bytes to generate. + */ + void ngc_lfg_get_bytes(struct ngc_lfg_ctx *ctx, uint8_t *out, size_t count); + + /** + * @brief Try to extract the LFG seed from a chunk of data. + * + * Attempts to reverse-engineer the 17-word seed from a buffer of suspected + * PRNG output. Requires at least NGC_LFG_K * 4 = 2084 bytes of contiguous + * PRNG output aligned to uint32 boundaries. + * + * @param data Pointer to the data to test. + * @param size Number of bytes available. + * @param data_offset Byte offset of data[0] within its LFG block + * (0x8000-aligned for Wii, arbitrary for GC). + * @param seed_out If successful, receives the 17-word seed (big-endian). + * + * @return The number of consecutive bytes from data[0] that match the LFG. + * Returns 0 if the data does not look like junk. + */ + size_t ngc_lfg_get_seed(const uint8_t *data, size_t size, size_t data_offset, uint32_t seed_out[NGC_LFG_SEED_SIZE]); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBAARUFORMAT_NGCW_LFG_H */