diff --git a/README.md b/README.md index e6fc248..9fffab1 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ and examine or modify game variables. There are two ways to use the debugger: launch the debugger with a console interface. Type `help` to see a list of available commands. +### Streamer Mode + +xsystem35 introduces a "Streamer Mode" to make playing games with NSFW content +safer for streaming or public viewing. When enabled with the `-censor ` +option, images specified in the provided file will be automatically mosaiced. + +The `misc/censor/` directory contains example censor list files for some games. + ## Installation Prebuilt packages for Windows and Android can be downloaded from the diff --git a/doc/xsystem35.6.adoc b/doc/xsystem35.6.adoc index 3613a68..faa9233 100644 --- a/doc/xsystem35.6.adoc +++ b/doc/xsystem35.6.adoc @@ -49,6 +49,11 @@ finds System 3.x game files (*.ALD) from the current directory. Suppress text hook on specified scenario pages. _list_ is a comma-separated list of page numbers. Used to suppress system messages, etc. +*-censor* _file_:: + Enable 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. + *-renderer* _name_:: Use the SDL rendering driver named _name_. diff --git a/misc/censor/README.md b/misc/censor/README.md new file mode 100644 index 0000000..4f61e66 --- /dev/null +++ b/misc/censor/README.md @@ -0,0 +1,21 @@ +# Censor Lists for Streamer Mode + +This directory contains sample censor lists for various games, intended for use +with xsystem35's Streamer Mode. + +## Usage + +To use a censor list, specify its path with the `-censor` command-line option +or in your `.xsys35rc` file. For example: + +```bash +xsystem35 -censor misc/censor/kichikuou.txt +``` + +The list files contain image numbers (integers), one per line. Lines starting +with `#` are treated as comments. + +## Contributing + +We welcome contributions of censor lists for other games! If you have created +a censor list that you'd like to share, please open a Pull Request. diff --git a/misc/censor/kichikuou.txt b/misc/censor/kichikuou.txt new file mode 100644 index 0000000..cac5ae1 --- /dev/null +++ b/misc/censor/kichikuou.txt @@ -0,0 +1,164 @@ +# Censor list for 鬼畜王ランス (Kichikuou Rance) +27 +70 +93 +101 +102 +103 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +171 +172 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +193 +195 +196 +197 +198 +199 +200 +201 +203 +204 +205 +206 +207 +208 +209 +210 +214 +215 +216 +217 +218 +219 +221 +222 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +239 +299 +300 +361 +362 +363 +375 +376 +377 +393 +422 +461 +466 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +517 +844 +845 +846 +847 +848 +851 +852 +853 +854 +855 diff --git a/src/cache.c b/src/cache.c index 75d5d19..788e401 100644 --- a/src/cache.c +++ b/src/cache.c @@ -130,3 +130,19 @@ void *cache_foreach(cacher *id, int key) { } return NULL; } + +void cache_clear(cacher *id) { + cacheinfo *ic = id->top->next; + cacheinfo *next; + + while (ic != NULL) { + next = ic->next; + if (ic->data) { + totalsize -= ic->size; + id->free_(ic->data); + } + free(ic); + ic = next; + } + id->top->next = NULL; +} diff --git a/src/cache.h b/src/cache.h index 53aad16..bac7315 100644 --- a/src/cache.h +++ b/src/cache.h @@ -46,5 +46,6 @@ typedef struct _cacher cacher; extern cacher *cache_new(void *delcallback); extern void cache_insert(cacher *id, int key, void *data, int size, int *in_use); extern void *cache_foreach(cacher *id, int key); +extern void cache_clear(cacher *id); #endif /* !__CASHE__ */ diff --git a/src/cg.c b/src/cg.c index 66bd012..9458a5e 100644 --- a/src/cg.c +++ b/src/cg.c @@ -21,9 +21,14 @@ */ /* $Id: cg.c,v 1.12 2001/09/16 15:59:11 chikama Exp $ */ +#include #include #include +#include #include +#ifdef __EMSCRIPTEN__ +#include +#endif #include "portab.h" #include "system.h" #include "nact.h" @@ -41,6 +46,9 @@ #include "filecheck.h" #include "cache.h" +static int *censor_list; // sorted list of cg numbers +static int censor_list_size; + /* VSPのパレット展開バンク */ int cg_vspPB; /* cg,palette 展開フラグ (funciotn flag) */ @@ -50,6 +58,8 @@ int *cg_loadCountVar; /* Brightness (PD command) */ int cg_brightness; +#define MOSAIC_SIZE 16 + #define GCMD_EXTRACTCG(c) ((c) & 0x01) #define GCMD_SET_PALETTE(c) ((c) & 0x02) #define GCMD_LOAD_PALETTE(c) ((c) & 0x04) @@ -69,6 +79,46 @@ static void clear_display_loc(); static void display_cg(cgdata *cg, int x, int y, int sprite_color, bool alpha_blend); static cgdata *loader(int no); +static int compare_integers(const void *a, const void *b) { + int int_a = *(const int *)a; + int int_b = *(const int *)b; + return (int_a > int_b) - (int_a < int_b); +} + +static bool is_censored_cg(int no) { + no += 1; // CG numbers are 1-based. + if (!censor_list) + return false; + return bsearch(&no, censor_list, censor_list_size, sizeof(int), compare_integers) != NULL; +} + +static void cg_mosaic(cgdata *cg) { + Uint32 format; + switch (cg->depth) { + case 8: + format = SDL_PIXELFORMAT_INDEX8; + break; + case 16: + format = SDL_PIXELFORMAT_RGB565; + break; + case 24: + format = SDL_PIXELFORMAT_RGB24; + break; + default: + SYSERROR("Unsupported cg depth %d", cg->depth); + } + SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormatFrom( + cg->pic, cg->width, cg->height, cg->depth, cg->width * (cg->depth / 8), format); + SDL_Surface *tmp = SDL_CreateRGBSurfaceWithFormat( + 0, (cg->width + MOSAIC_SIZE - 1) / MOSAIC_SIZE, (cg->height + MOSAIC_SIZE - 1) / MOSAIC_SIZE, cg->depth, format); + 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); + SDL_FreeSurface(sf); +} /* * Identify cg format @@ -230,8 +280,12 @@ static cgdata *loader(int no) { default: break; } - /* insert to cache */ + if (cg) { + if (is_censored_cg(no)) { + cg_mosaic(cg); + } + /* insert to cache */ int size = cg->width * cg->height * (cg->depth / 8); cache_insert(cacheid, no, cg, size, NULL); } @@ -536,6 +590,10 @@ SDL_Surface *cg_load_as_sdlsurface(int no) { ald_freedata(dfile); if (!cg) return NULL; + if (is_censored_cg(no)) { + cg_mosaic(cg); + } + SDL_Surface *pic = type == ALCG_PMS16 ? SDL_CreateRGBSurfaceWithFormatFrom(cg->pic, cg->width, cg->height, 16, cg->width * 2, SDL_PIXELFORMAT_RGB565) : SDL_CreateRGBSurfaceWithFormatFrom(cg->pic, cg->width, cg->height, 24, cg->width * 3, SDL_PIXELFORMAT_RGB24); @@ -562,3 +620,60 @@ SDL_Surface *cg_load_as_sdlsurface(int no) { cgdata_free(cg); return sf; } + +EMSCRIPTEN_KEEPALIVE +void load_censor_list(const char *path) { + if (cacheid) + cache_clear(cacheid); + if (censor_list) { + free(censor_list); + censor_list = NULL; + censor_list_size = 0; + } + if (!path || !*path) + return; + + FILE *fp = fopen(path, "r"); + if (!fp) { + SYSERROR("failed to open censor list file %s", path); + return; + } + + char line[256]; + int count = 0, cap = 256; + censor_list = malloc(cap * sizeof(int)); + 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; + + if (count >= cap) { + cap *= 2; + censor_list = realloc(censor_list, cap * sizeof(int)); + } + char *endptr; + censor_list[count++] = (int)strtol(p, &endptr, 10); + + while (*endptr && isspace((unsigned char)*endptr)) + endptr++; + if (*endptr != '\0') + WARNING("invalid line %d in %s", line_no, path); + } + censor_list_size = count; + + fclose(fp); + + if (censor_list_size > 0) { + qsort(censor_list, censor_list_size, sizeof(int), compare_integers); + } else { + free(censor_list); + censor_list = NULL; + } +} diff --git a/src/cg.h b/src/cg.h index 221574d..da54abd 100644 --- a/src/cg.h +++ b/src/cg.h @@ -84,6 +84,7 @@ extern void cg_get_info(int no, SDL_Rect *info); extern void cg_clear_display_loc(); extern void cgdata_free(cgdata *cg); extern struct SDL_Surface *cg_load_as_sdlsurface(int no); +extern void load_censor_list(const char *path); extern int cg_vspPB; extern int cg_fflg; diff --git a/src/xsystem35.c b/src/xsystem35.c index bc2911b..d6a6cb9 100644 --- a/src/xsystem35.c +++ b/src/xsystem35.c @@ -56,6 +56,7 @@ #include "randMT.h" #include "ags.h" #include "font.h" +#include "cg.h" #include "gfx.h" #include "event.h" #include "menu.h" @@ -114,6 +115,7 @@ static void sys35_usage(bool verbose) { puts(" -playlist file : load CD playlist from 'file'"); puts(" -texthook mode : text hook mode. 'none' (default), 'print' or 'copy'"); puts(" -texthook_suppress list : suppress text hook on specified pages"); + puts(" -censor file : mosaic images listed in 'file'"); puts(" -M? : select output midi methos"); #ifdef ENABLE_MIDI_SDLMIXER @@ -254,6 +256,10 @@ static void sys35_ParseOption(int *argc, char **argv) { if (argv[i + 1] != NULL) { texthook_set_suppression_list(argv[i + 1]); } + } else if (0 == strcmp(argv[i], "-censor")) { + if (argv[i + 1] != NULL) { + load_censor_list(argv[i + 1]); + } } else if (0 == strncmp(argv[i], "-M", 2)) { int subdev = 0; if (argv[i][3] != '\0') { @@ -399,6 +405,12 @@ static void check_profile() { if (param) { texthook_set_suppression_list(param); } + + /* Censor list for streamer mode */ + param = get_profile("censor"); + if (param) { + load_censor_list(param); + } } #ifdef HAVE_SIGACTION diff --git a/xsys35rc.sample b/xsys35rc.sample index 13a6c7a..9ce8e0e 100644 --- a/xsys35rc.sample +++ b/xsys35rc.sample @@ -105,6 +105,13 @@ #texthook_suppress: 7,11,15,16,17,18,23,24,32,33,197 +# 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 the ZB command (change the font weight). It is disabled by default # because it makes the text too bold with our default fonts. # This option is experimental and may be removed in the future. See