/* * pop_config.c — ВСЯ модель пользовательских настроек и POP.CFG (банк 9). * * Здесь вместе живут state, дефолты, бинарный codec и DSS I/O: это один * модуль приложения, не библиотека. Данные остаются в общем W2 (для * --bank-data нет ключа), код уезжает в банк 9. Игра читает только * pop_settings либо свои runtime-флаги, которые синхронизирует * pop_settings_apply(). * * POP.CFG v1: * 0..3 "PCFG", 4 version, 5..6 payload size LE, payload, XOR, ~XOR. * Неверный файл не меняет уже установленные дефолты. Расширенный payload * v1 допустим: известны первые девять байтов, неизвестный хвост пропускается. */ #include #include #ifndef POP_CONFIG_HOST_TEST #include #include #include #include #include #endif #include "pop_settings.h" #include "pop_cfg.h" #include "pop_pace.h" #include "pop_cheat.h" #include "pop_sfx.h" #include "_pop_config.h" #define CFG_BASE 0x100 pop_settings_t pop_settings; static uint8_t cfg_checksum(const uint8_t *data, uint16_t size) { uint8_t sum = 0; while (size--) sum ^= *data++; return sum; } void pop_settings_defaults(void) __banked { pop_settings.sound_enabled = 1; pop_settings.music_enabled = 1; pop_settings.speed_mode = POP_SPEED_NORMAL; pop_settings.gameplay_profile = POP_PROFILE_VANILLA; pop_settings.cheats_enabled = 1; /* пока roomtest — dev-сборка */ pop_settings.show_build_info = 0; pop_settings.enhancement_flags = 0; pop_settings.time_limit_enabled = 1; pop_settings.show_debug_bar = 0; /* по умолчанию чистый экран */ } /* Применить модель к настоящим владельцам runtime-состояния. Sound нельзя * применять до pop_sfx_init(): тот поднимает набор и только затем знает, * можно ли честно включить CBL. */ void pop_settings_apply(uint8_t sound_ready) __banked { pop_speed_mode = pop_settings.speed_mode; pop_cheats = pop_settings.cheats_enabled; if (sound_ready && pop_snd_want != pop_settings.sound_enabled) { pop_sfx_toggle(); /* Если звуковой набор не поднялся, toggle возвращает want в 0. * Файл должен отражать фактически применимое состояние. */ pop_settings.sound_enabled = pop_snd_want; } /* Музыке набор эффектов не нужен: она читается с диска отдельно, * поэтому применяется всегда, а не по sound_ready. */ pop_mus_want = pop_settings.music_enabled; } void pop_cfg_encode(uint8_t *buf) { uint8_t sum; memcpy(buf, "PCFG", 4); buf[4] = POP_CFG_VERSION; buf[5] = POP_CFG_PAYLOAD_SIZE; buf[6] = 0; buf[7] = pop_settings.sound_enabled; buf[8] = pop_settings.speed_mode; buf[9] = pop_settings.gameplay_profile; buf[10] = pop_settings.cheats_enabled; buf[11] = pop_settings.show_build_info; buf[12] = (uint8_t)pop_settings.enhancement_flags; buf[13] = (uint8_t)(pop_settings.enhancement_flags >> 8); buf[14] = pop_settings.time_limit_enabled; buf[15] = pop_settings.show_debug_bar; buf[16] = pop_settings.music_enabled; sum = cfg_checksum(buf + POP_CFG_HEADER_SIZE, POP_CFG_PAYLOAD_SIZE); buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = sum; buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] = (uint8_t)~sum; } uint8_t pop_cfg_decode(const uint8_t *buf, uint16_t size) { uint16_t payload_size, flags; uint8_t sum; if (size < POP_CFG_MIN_SIZE || memcmp(buf, "PCFG", 4) != 0 || buf[4] != POP_CFG_VERSION) return 0; payload_size = (uint16_t)(buf[5] | ((uint16_t)buf[6] << 8)); if (payload_size < POP_CFG_LEGACY_PAYLOAD_SIZE || payload_size > POP_CFG_MAX_PAYLOAD || size != (uint16_t)(POP_CFG_HEADER_SIZE + payload_size + POP_CFG_TAIL_SIZE)) return 0; sum = cfg_checksum(buf + POP_CFG_HEADER_SIZE, payload_size); if (buf[POP_CFG_HEADER_SIZE + payload_size] != sum || buf[POP_CFG_HEADER_SIZE + payload_size + 1] != (uint8_t)~sum) return 0; /* Сначала проверить ВСЕ известные поля, только затем менять state. */ if (buf[7] > 1 || buf[8] >= POP_SPEED_MODES || buf[9] > POP_PROFILE_ENHANCED || buf[10] > 1 || buf[11] > 1) return 0; if (payload_size >= POP_CFG_TIMELIMIT_PAYLOAD_SIZE && buf[14] > 1) return 0; if (payload_size >= POP_CFG_DEBUGBAR_PAYLOAD_SIZE && buf[15] > 1) return 0; if (payload_size >= POP_CFG_PAYLOAD_SIZE && buf[16] > 1) return 0; flags = (uint16_t)(buf[12] | ((uint16_t)buf[13] << 8)); pop_settings.sound_enabled = buf[7]; pop_settings.speed_mode = buf[8]; /* ENHANCED пока не имеет аудита: безопасно понижаем до VANILLA. */ pop_settings.gameplay_profile = POP_PROFILE_VANILLA; pop_settings.cheats_enabled = buf[10]; pop_settings.show_build_info = buf[11]; pop_settings.enhancement_flags = flags; /* Старый POP.CFG создавался до настройки таймера (и до тумблера * отладочной полосы): отсутствующее поле означает исторический дефолт — * ограничение 60 минут, включённую полосу и музыку, а не случайное * значение в памяти. */ pop_settings.time_limit_enabled = (payload_size >= POP_CFG_TIMELIMIT_PAYLOAD_SIZE) ? buf[14] : 1; pop_settings.show_debug_bar = (payload_size >= POP_CFG_DEBUGBAR_PAYLOAD_SIZE) ? buf[15] : 1; pop_settings.music_enabled = (payload_size >= POP_CFG_PAYLOAD_SIZE) ? buf[16] : 1; return 1; } #ifndef POP_CONFIG_HOST_TEST /* В отличие от bank_load_file здесь нужен точный размер файла: только так * отличаем валидный будущий payload от короткого/дописанного мусора. Буфер * на стеке лежит в W2, то есть разрешён ESTEX; файл мал, EMM ему не нужен. */ static uint8_t cfg_read_file(uint8_t *buf, uint16_t *size) { int fd, got; long n; fd = open("POP.CFG", O_RDONLY); if (fd < 0) return 0; n = lseek(fd, 0, SEEK_END); if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE || lseek(fd, 0, SEEK_SET) < 0) { close(fd); return 0; } got = read(fd, buf, (uint16_t)n); if (close(fd) != 0 || got != (int)n) return 0; *size = (uint16_t)n; return 1; } int8_t pop_cfg_load(void) __banked { uint8_t buf[POP_CFG_MAX_SIZE]; uint16_t size; if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0; if (buf[9] != POP_PROFILE_VANILLA) printf("POP.CFG: ENHANCED profile -> VANILLA\n"); return 1; } int8_t pop_cfg_save(void) __banked { uint8_t blk, page; uint8_t buf[POP_CFG_SIZE]; int rc; blk = mem_alloc_pages(1); if (!blk) return -1; page = mem_get_page(blk, 0); gfx_w0_page_prepare(page); pop_cfg_encode(buf); gfx_w0_map(page); memcpy((uint8_t *)CFG_BASE, buf, sizeof(buf)); gfx_w0_unmap(); /* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего * окна, когда POP.CFG вообще отсутствует после ошибки unlink. */ rc = bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf)); mem_free_block(blk); return (rc == sizeof(buf)) ? 1 : 0; } #endif