diff --git a/README.md b/README.md index 749fd17..0acfac2 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ for translation or other purposes. The available modes are: Suppress text hook on specified scenario pages. _list_ is a comma-separated list of page numbers. +#### `-censor` _file_ +Enables **Streamer Mode**, which mosaics specific images to make them safe for +streaming. The _file_ is a text file containing a list of image numbers +to censor, one per line. Lines starting with '#' are treated as comments. + #### `-trace` Enables trace mode, which prints debug information to the console. diff --git a/src/config.cpp b/src/config.cpp index 5a2440a..6be2a5a 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -140,6 +140,8 @@ Config::Config(int argc, char *argv[]) font_file = argv[++i]; else if (strcmp(argv[i], "-playlist") == 0) playlist = argv[++i]; + else if (strcmp(argv[i], "-censor") == 0) + censor_list = argv[++i]; else if (strcmp(argv[i], "-fm") == 0) use_fm = true; else if (strcmp(argv[i], "-mididevice") == 0) @@ -208,6 +210,8 @@ void Config::load_ini() font_file = normalize_path(val); else if (!strcasecmp(key, "playlist")) playlist = normalize_path(val); + else if (!strcasecmp(key, "censor")) + censor_list = normalize_path(val); else if (!strcasecmp(key, "fm")) use_fm = to_bool(val, lineno); else if (!strcasecmp(key, "mididevice")) diff --git a/src/config.h b/src/config.h index b1f830b..0947742 100644 --- a/src/config.h +++ b/src/config.h @@ -39,6 +39,7 @@ struct Config { std::string encoding; std::string save_dir; std::string playlist; + std::string censor_list; std::string title; int midi_device = -1; bool print_version = false; diff --git a/src/sys/ags.cpp b/src/sys/ags.cpp index 58c123d..d8ca35b 100644 --- a/src/sys/ags.cpp +++ b/src/sys/ags.cpp @@ -5,6 +5,7 @@ */ #include "ags.h" +#include #include #include "nact.h" #include "game_id.h" @@ -64,6 +65,9 @@ AGS::AGS(const Config& config, const GameId& game_id) : game_id(game_id) SDL_SetSurfacePalette(hBmpScreen[1], screen_palette); SDL_SetSurfacePalette(hBmpScreen[2], screen_palette); + if (!config.censor_list.empty()) + load_censor_list(config.censor_list.c_str()); + // フォント TTF_Init(); if (!config.font_file.empty()) { @@ -483,6 +487,43 @@ int AGS::calculate_menu_max(int window) { return 11; } +void AGS::load_censor_list(const char* fname) +{ + censor_list.clear(); + if (!fname || !*fname) + return; + + FILE *fp = fopen(fname, "r"); + if (!fp) { + ERROR("failed to open %s", fname); + return; + } + + char line[256]; + int line_no = 0; + while (fgets(line, sizeof(line), fp)) { + line_no++; + char* p = line; + char* comment = strchr(p, '#'); + if (comment) + *comment = '\0'; + while (*p && isspace((unsigned char)*p)) + p++; + if (*p == '\0') + continue; + + char *endptr; + censor_list.insert(strtol(p, &endptr, 10)); + + while (*endptr && isspace((unsigned char)*endptr)) + endptr++; + if (*endptr != '\0') + WARNING("%s:%d: syntax error", fname, line_no); + } + + fclose(fp); +} + #ifdef __EMSCRIPTEN__ extern "C" { @@ -490,5 +531,9 @@ bool EMSCRIPTEN_KEEPALIVE save_screenshot(const char* path) { return g_nact->ags->save_screenshot(path); } +void EMSCRIPTEN_KEEPALIVE load_censor_list(const char *path) { + return g_nact->ags->load_censor_list(path); +} + } #endif diff --git a/src/sys/ags.h b/src/sys/ags.h index e98cecc..8c93b47 100644 --- a/src/sys/ags.h +++ b/src/sys/ags.h @@ -103,6 +103,8 @@ public: void load_cg(int page, int transparent); void set_cg_file(const char *file_name); + void load_censor_list(const char* fname); + void set_palette(int index, uint8_t r, uint8_t g, uint8_t b); std::vector get_screen_palette() const; uint8 get_pixel(int dest, int x, int y) const { return vram[dest][y][x]; } @@ -203,6 +205,7 @@ public: bool get_palette; bool extract_palette; bool extract_cg; + std::unordered_set censor_list; std::unordered_set ignore_palette; int palette_bank; diff --git a/src/sys/ags_draw.cpp b/src/sys/ags_draw.cpp index 2d025c3..d3cc560 100644 --- a/src/sys/ags_draw.cpp +++ b/src/sys/ags_draw.cpp @@ -14,6 +14,23 @@ #include "game_id.h" #include "debugger/debugger.h" +namespace { + +const int MOSAIC_SIZE = 16; + +void mosaic(SDL_Surface* sf) { + SDL_Surface *tmp = SDL_CreateRGBSurfaceWithFormat( + 0, (sf->w + MOSAIC_SIZE - 1) / MOSAIC_SIZE, (sf->h + MOSAIC_SIZE - 1) / MOSAIC_SIZE, 8, SDL_PIXELFORMAT_INDEX8); + if (sf->format->palette) + SDL_SetSurfacePalette(tmp, sf->format->palette); + // NOTE: SDL_BlitScaled() does not support 8-bit surfaces. + SDL_SoftStretch(sf, NULL, tmp, NULL); + SDL_SoftStretch(tmp, NULL, sf, NULL); + SDL_FreeSurface(tmp); +} + +} // namespace + void AGS::load_cg(int page, int transparent) { if (bmp_prefix) { @@ -78,6 +95,9 @@ void AGS::load_cg(int page, int transparent) } if (extract_cg && cg.surface()) { SDL_SetSurfacePalette(cg.surface(), screen_palette); + if (censor_list.count(page)) { + mosaic(cg.surface()); + } SDL_Rect dstrect = { cg.x, cg.y, cg.surface()->w, cg.surface()->h }; SDL_BlitSurface(cg.surface(), NULL, hBmpScreen[dest_screen], &dstrect); if (dest_screen == 0) { diff --git a/system3.ini.example b/system3.ini.example index 2317b94..531f753 100644 --- a/system3.ini.example +++ b/system3.ini.example @@ -46,6 +46,12 @@ texthook = none ; The example below is for Rance 4.1, to filter out system messages. ;texthook_suppress = 2,7 +; Censor list for Streamer Mode. +; This mosaics images listed in the specified file, making them safe for +; streaming or public viewing. The file should contain one image number +; per line, with '#' treated as comments. +;censor = censor_list.txt + ; Enables trace mode, which prints debug information to the console. ;trace = true