Add a streamer mode that hides slurs, swearing and insults in text from other players, maps and mods

This commit is contained in:
MrBruh committed 2026-10-18 15:16:12 +13:00
1 parent efabb0d056
commit 90dd65cab4
29 files changed
+4901 -64

No files matched your search

+2 -1
View File
@@ -16,7 +16,7 @@ end.
| --- | --- |
| `source/` | The game: 466 C files of the decompilation, compiled as they are with few changes (each marked, below) |
| `port/linux/src/` | The platform layer the three ports share: Direct3D 8 on OpenGL (`d3d8_gl.c`), DirectSound and XInput on SDL3, files, threads, memory, sockets, settings (`port_config.c`), the mods folder (`port_mods.c`), internet play (`p2p*.c`) |
| `port/linux/game/` | The port's game-side code, compiled as the game's sources are: the distributed netcode (`network_*.c`), co-op, map sharing (`map_share*.c`), the menus (`menu_tags.c`, `menu_functions.c`), Custom Edition maps (`custom_edition_*.c`, `cache_file_formats.c`), the tag validator (`tag_validate.c`, `tag_schema_*.c`), mods (`mods.c`, `mod_*.c`), touch, FOV and more |
| `port/linux/game/` | The port's game-side code, compiled as the game's sources are: the distributed netcode (`network_*.c`), co-op, map sharing (`map_share*.c`), the menus (`menu_tags.c`, `menu_functions.c`), Custom Edition maps (`custom_edition_*.c`, `cache_file_formats.c`), the tag validator (`tag_validate.c`, `tag_schema_*.c`), mods (`mods.c`, `mod_*.c`), streamer mode's word filter (`text_filter*.c`), touch, FOV and more |
| `port/linux/include/` | The port's headers: the limits (`halo_port_limits.h`, with `HALO_PORT_NETWORK_VERSION`), capacities (`halo_port_capacity.h`), the prefix header, `halo_linux_source_fixups.h` (port functions the game's sources call) |
| `port/include/xdk/` | Stand-ins for the Xbox SDK's headers (declarations only) |
| `port/windows/` | The Windows build's own files (`win32_*.c`, headers) |
@@ -218,6 +218,7 @@ as documentation of file formats, never copied.
| [port/include/xdk/README.md](port/include/xdk/README.md) | How the SDK declarations were written and are checked |
| [docs/custom_edition_caches.md](docs/custom_edition_caches.md) | Loading and converting Custom Edition maps, and the evidence for each layout |
| [docs/mods.md](docs/mods.md) | Mods: folders of Halo PC tag files whose sounds, bitmaps, text and models play over a map's on one machine |
| [port/assets/text_filter/README.md](port/assets/text_filter/README.md) | Streamer mode's words: Surge AI's list, the review, the allowed words |
| [port/assets/menus/README.md](port/assets/menus/README.md) | The menu files' format and how to write them again |
| [port/assets/menus/NON_HANDDRAWN.md](port/assets/menus/NON_HANDDRAWN.md) | Menu pictures that are not redraws |
| [port/assets/menus/UNWIRED.md](port/assets/menus/UNWIRED.md) | Menu functions that do nothing yet |
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's settings screen, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/multiplayer_hud_settings_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="bitmaps/gradient">
<on event="b" back="true"/>
<on event="back" back="true"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/header_profile_network_settings"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/options_menu"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/header_profile_network_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_player_names" x="54" y="73"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_player_name_scale" x="54" y="103"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_scoreboard_team_layout" x="54" y="133"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_scoreboard_background" x="54" y="163"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/button_defaults" y="1"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_player_names" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_names_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_names_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_names_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_names_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ALL|ALLIES|ENEMIES|NONE" setting="display.player_names" values="all|allies|enemies|none" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_player_name_scale" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_name_scale_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_name_scale_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_name_scale_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/player_name_scale_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0.5x|0.75x|1x|1.25x|1.5x|2x" setting="display.player_name_scale" values="0.5|0.75|1|1.25|1.5|2" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_scoreboard_team_layout" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_team_layout_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_team_layout_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_team_layout_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_team_layout_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="TEAMS|BY SCORE" setting="display.scoreboard_team_layout" values="teams|score" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/op_scoreboard_background" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_background_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_background_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_background_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/scoreboard_background_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.scoreboard_background" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
<on event="start" run="port settings defaults"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="1" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings save" back="true"/>
<on event="start" run="port settings save" back="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/labels">
<string text="PLAYER NAMES:"/>
<string text="PLAYER NAME SIZE:"/>
<string text="SCOREBOARD LAYOUT:"/>
<string text="SCOREBOARD PANEL:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/help_strings">
<string text=""/>
<string text="In multiplayer, whose names are drawn above their\nheads."/>
<string text="How large the players' names are drawn."/>
<string text="A team game's scoreboard: a column for each team,\nor every player in order of score."/>
<string text="Draw a panel behind the scoreboard, for clearer\ntext."/>
</strings>
</menus>
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's settings screen, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/streamer_mode_settings_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="bitmaps/gradient">
<on event="b" back="true"/>
<on event="back" back="true"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/header_profile_network_settings"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/options_menu"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/header_profile_network_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/settings_select/player_setup/player_profile_edit/network_setup/header_profile_network_settings">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_enabled" x="54" y="73"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_slurs" x="54" y="103"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_swearing" x="54" y="133"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_insults" x="54" y="163"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/button_defaults" y="1"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_enabled" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/enabled_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/enabled_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/enabled_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="streamer.enabled" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_slurs" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/slurs_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/slurs_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/slurs_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/slurs_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|SEVERE|STRONG AND WORSE|MILD AND WORSE" setting="streamer.slurs" values="off|severe|strong|mild" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_swearing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/swearing_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/swearing_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/swearing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/swearing_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|SEVERE|STRONG AND WORSE|MILD AND WORSE" setting="streamer.swearing" values="off|severe|strong|mild" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/op_insults" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/insults_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/insults_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/insults_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/insults_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|SEVERE|STRONG AND WORSE|MILD AND WORSE" setting="streamer.insults" values="off|severe|strong|mild" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
<on event="start" run="port settings defaults"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="1" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings save" back="true"/>
<on event="start" run="port settings save" back="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/labels">
<string text="STREAMER MODE:"/>
<string text="CENSOR SLURS:"/>
<string text="CENSOR SWEARING:"/>
<string text="CENSOR INSULTS:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/help_strings">
<string text=""/>
<string text="Hide offensive words in names and text from\nother players, maps and mods, as stars."/>
<string text="Slurs about race, sexuality, gender and\ndisability."/>
<string text="Swearing and sexual words (STRONG AND WORSE\nleaves the mildest showing)."/>
<string text="Name-calling and insults that are not slurs."/>
</strings>
</menus>
@@ -17,15 +17,11 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_allow_upnp" x="54" y="103" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_join_from_clipboard" x="54" y="133" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_auto" x="54" y="163" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_player_names" x="54" y="193" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_player_names" x="54" y="103" platform="web"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_player_name_scale" x="54" y="223" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_player_name_scale" x="54" y="133" platform="web"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_scoreboard_team_layout" x="54" y="253" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_scoreboard_team_layout" x="54" y="163" platform="web"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_scoreboard_background" x="54" y="283" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_scoreboard_background" x="54" y="193" platform="web"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_map_sharing" x="54" y="313" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_map_sharing" x="54" y="193" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_multiplayer_hud" x="54" y="223" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_multiplayer_hud" x="54" y="103" platform="web"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_streamer_mode" x="54" y="253" platform="desktop android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_streamer_mode" x="54" y="133" platform="web"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -69,49 +65,29 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/auto_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="update.auto" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_player_names" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_names_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_names_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_names_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_names_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ALL|ALLIES|ENEMIES|NONE" setting="display.player_names" values="all|allies|enemies|none" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_player_name_scale" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_name_scale_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_name_scale_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_name_scale_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/player_name_scale_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0.5x|0.75x|1x|1.25x|1.5x|2x" setting="display.player_name_scale" values="0.5|0.75|1|1.25|1.5|2" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_scoreboard_team_layout" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_team_layout_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_team_layout_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_team_layout_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_team_layout_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="TEAMS|BY SCORE" setting="display.scoreboard_team_layout" values="teams|score" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_scoreboard_background" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_background_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_background_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_background_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/scoreboard_background_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.scoreboard_background" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_map_sharing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop android">
<on event="a" open="main_menu/settings_select/player_setup/player_profile_edit/network_setup/map_sharing/map_sharing_settings_screen"/>
<on event="start" open="main_menu/settings_select/player_setup/player_profile_edit/network_setup/map_sharing/map_sharing_settings_screen"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/map_sharing_label"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/map_sharing_label" type="text" controller="1" width="512" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/map_sharing_label" type="text" controller="1" width="512" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_multiplayer_hud" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" open="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/multiplayer_hud_settings_screen"/>
<on event="start" open="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud/multiplayer_hud_settings_screen"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud_label"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/multiplayer_hud_label" type="text" controller="1" width="512" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/op_streamer_mode" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" open="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/streamer_mode_settings_screen"/>
<on event="start" open="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode/streamer_mode_settings_screen"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode_label"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/streamer_mode_label" type="text" controller="1" width="512" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/network_setup/labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
@@ -128,11 +104,9 @@
<string text="UPNP PORT FORWARDING:"/>
<string text="JOIN FROM CLIPBOARD:"/>
<string text="CHECK FOR UPDATES:"/>
<string text="PLAYER NAMES:"/>
<string text="PLAYER NAME SIZE:"/>
<string text="SCOREBOARD LAYOUT:"/>
<string text="SCOREBOARD PANEL:"/>
<string text="MAP SHARING:"/>
<string text="MULTIPLAYER HUD:"/>
<string text="STREAMER MODE:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/network_setup/help_strings">
<string text=""/>
@@ -140,10 +114,8 @@
<string text="Let internet play ask the router to forward its\nport, for networks that stop connections."/>
<string text="Join the game of an invite link copied before\nswitching to the game."/>
<string text="Look for a new version when the game starts."/>
<string text="In multiplayer, whose names are drawn above their\nheads."/>
<string text="How large the players' names are drawn."/>
<string text="A team game's scoreboard: a column for each team,\nor every player in order of score."/>
<string text="Draw a panel behind the scoreboard, for clearer\ntext."/>
<string text="Downloading Custom Edition maps you do not have,\nand helping send them to other players."/>
<string text="Players' names above their heads, and the\nscoreboard."/>
<string text="Hide offensive words in names and text from\nother players, maps and mods, as stars."/>
</strings>
</menus>
+2
View File
@@ -48,6 +48,8 @@
"ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.name_edit.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.map_sharing.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.multiplayer_hud.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.streamer_mode.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.graphics.xml",
+67
View File
@@ -0,0 +1,67 @@
# Streamer mode's words
The words streamer mode hides (Settings > Network Setup > Streamer Mode; the
`streamer.*` settings in `port/linux/README.md`). The game builds them in
from `port/linux/game/text_filter_terms.inc`, which
`tools/text_filter_terms.py` writes from Surge AI's list and these files:
| File | Contents |
| --- | --- |
| `review.txt` | The review of every base word of Surge AI's list: kept (its group, severity and where it may be found) or dropped, and the spellings dropped. The words are ROT13'd. |
| `allow.txt` | Words never hidden, however a hidden word sits inside them (Scunthorpe), in plain text |
| `english.txt` | The words of the English word list (`port/third_party/english-words`) that the list hides, ROT13'd, which `tools/test_text_filter_terms.py` checks |
`python3 tools/text_filter_terms.py --list` shows the table decoded, and
`--check "some text"` shows what the game would hide in a text.
## Surge AI's list
The words, their categories and their severities come from Surge AI's
profanity list, "The Obscenity List": 1,598 English profanities and their
spellings, each with up to three base words, up to three categories and a
severity that five raters gave it (Mild, Strong or Severe).
Upstream: https://github.com/surge-ai/profanity (gone since; the Internet
Archive's copy of its page of 1 March 2025 shows an MIT `LICENSE`, whose text
it did not keep), copied at https://huggingface.co/datasets/mmathys/profanity
(MIT), revision `c2a17bf3ec64d9b6418ead1cb5f7ef26dbdffccb`: `profanity_en.csv`,
SHA-256 ca4ff7d713a8e6f6ec2616f489cd09f6fb96b8b3af404a207783be58a91f3ede.
The CSV is not in the repository: give `tools/text_filter_terms.py --surge`
its path to write the table again (and `HALO_SURGE_CSV` its path for the
test that checks it).
The groups are made from its categories (`tools/text_filter_terms.py`'s
`GROUPS`), each base word in one, then reviewed (`review.txt`):
| Group | Surge AI's categories |
| --- | --- |
| SLURS | racial / ethnic slurs, sexual orientation / gender, mental disability, physical disability, political |
| SWEARING | sexual anatomy / sexual acts, bodily fluids / excrement, religious offense |
| INSULTS | other / general insult, animal references, physical attributes |
The table is made from their list, so it carries their notice:
```
MIT License
Copyright (c) Surge AI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
+44
View File
@@ -0,0 +1,44 @@
# Streamer mode's allowed words (tools/text_filter_terms.py): words never
# hidden, nor any part of them, however a hidden word sits inside them
# (Scunthorpe). One a line, lowercase.
assess
assesses
bereshith
brushite
chashitsu
cushite
cushitic
elkoshite
girgashite
kaneshite
koreishite
mackintoshite
marshite
matsushita
metabrushite
mishit
mishits
mishitting
niggard
peshito
phage
phages
placuntitis
placuntoma
scunthorpe
shiitake
shiite
shiites
shiitic
shitake
shittah
shittahs
shitterton
shittim
shittims
shittimwood
snigger
unniggard
washita
wiggeries
wiggery
+252
View File
@@ -0,0 +1,252 @@
abapr
abaprf
arteb
artebf
artebrf
artebvq
artebvqf
avt
avtten
avttre
avttref
avttregbr
avttrejrrq
avttrel
avttreqbz
avttrerq
avttresvfu
avttresvfurf
avttretbbfr
avttreurnq
avttrevfu
avttrevfz
avttreyvat
betnfz
betnfzf
cevpx
cevpxf
chffl
chffvrf
crpxre
crpxref
crqbcuvyr
cvff
cvffrf
ergneq
ergneqf
ergneqrq
erqfxva
erqfxvaf
fcvp
fcvpf
fcvt
fcvx
fcvxf
fnzob
fnzobf
fphz
fphzf
fphzzl
fulybpx
fulybpxf
fung
fuvg
fuvgf
fuvggl
fuvggra
fuvggrq
fuvggvarff
fuvggvat
fuvggvre
fuvggvrfg
fuvggyr
fuvgn
fuvgrcbxr
fuvgure
fuvgurnq
fuvgurry
fvffl
fyhg
fyhgf
fyhggl
fynt
fyntf
gbffre
gbffref
gheq
gheqf
gjng
gjngf
gvg
gvgf
jbc
jbcf
jnax
jnaxre
jrgonpx
jrgonpxf
juber
juberf
jvttre
nany
navyvathf
nefr
nefrf
nefrubyr
nff
nffrf
nffubyr
nffubyrf
nob
nobf
obare
obaref
obbof
obyybk
obyybkrf
obyybkrq
obyybkvat
obyybpx
obyybpxf
ohgg
ohggf
ohttre
ohttref
ohyyfuvg
ohyyfuvgf
ohyyfuvggrq
ohyyfuvggvat
onfgneq
onfgneqf
onfgnneq
ornare
ornaref
ovgpu
ovgpul
ovgpurel
ovgpurevrf
ovgpurf
ovgpurq
ovgpuvat
oybjwbo
oybjwbof
oynpxl
pbba
pbbaf
pbbyvr
pbbyvrf
pbpx
pbpxf
pbpxurnq
phaavyvathf
phag
phagf
phz
pnjx
pnjxl
pnjxre
pnpn
pnpnf
pnpx
pnpxf
puvax
puvaxf
puvaxl
puvpxrafuvg
pyvg
qbhpur
qbhpurf
qbyg
qbygf
qlxr
qlxrf
qnexl
qnexvr
qnexvrf
qntb
qntbf
qnza
qnzaf
qvpx
qvpxf
qvqqyr
qvqqyrf
qvyqb
qvyqbf
shpx
shpxf
shpxjvg
shpxre
shpxrq
shpxvat
shttrq
sneg
snegf
snpx
snpxf
snt
sntbg
sntbgf
sntbgg
sntf
snttbg
snttbgel
snttbgf
snttbgl
snttbgrq
snttbgvat
snttl
snttvat
srpx
srpxf
sryyngbe
sryyngbel
sryyngvb
sryyngvbf
synzre
synzref
tbbx
tbbxf
tbjy
tbqqnz
tbqqnza
tbqqnzaf
tbqqnzarq
tbqqnzavg
tbqqnzf
tbqqnzzvg
tlc
tlcf
tlcfl
ubefrfuvg
ubr
ubrf
uror
uryy
uryyf
vawha
wnc
wnpxnff
wnpxnffrf
wnvyonvg
wrex
wrexf
wvtnobb
wvtnobbf
xabo
xabof
xaboore
xaboorq
xlxr
xvxr
xvxrf
xyna
xynaf
zbat
zbatbybvq
zbatbybvqf
zbgureshpxre
zhss
zhssf
znfgheongr
znfgheongrf
+296
View File
@@ -0,0 +1,296 @@
# Streamer mode's review of Surge AI's profanity list (tools/text_filter_terms.py):
# every base word of its profanity_en.csv kept (its group, severity and rule;
# - for Surge AI's severity and the rule worked out) or dropped. The words are
# ROT13'd; tools/text_filter_terms.py --list shows them.
#
# Dropped: everyday words the list has because they can be offensive in some
# uses (the filter has no context), and words for identities or anatomy that
# are neither slurs nor swearing. Moved: gendered insults filed under
# "sexual orientation / gender" go to SWEARING, homophobic phrases filed under
# the sexual categories go to SLURS, and the mild insults filed under "mental
# disability" or "sexual orientation / gender" go to INSULTS. Severities
# raised or lowered where one spelling's rating set the word's (damn and butt
# are mild, as Ofcom rates them; the Australian slur rated mild is strong).
drop 69
keep - - - norrq
keep - strong whole nob
drop noenunz
keep - - whole nany
drop nahf
drop ncr
keep - - - nefr
keep - - whole nff
keep - - - onol onggre
keep - - - onyyfnpx
drop onat
keep - - - onfgneq
keep - - - orna dhrra
keep - - whole ornare
keep - - - orng bss
keep - - - oryyraq
drop oraqre
keep swearing - - ovgpu
keep - - whole oynpxl
keep - - - oybj n ybnq
keep - - - oybjwbo
keep - - - oyhzcxva
keep - - - obvbynf
keep - - - obyybpxf
drop obaqntr
keep - - whole obare
drop obax
keep - - whole obbof
drop obbgl
drop oernfg
keep - - whole ohttre
drop ohz
drop ohat
keep - mild whole ohgg
drop ohggrezvyx
keep - - - pnzry wbpxrl
keep - - - pnzrygbr
keep slurs - - pnecrg zhapure
keep - - - puvan ivehf
keep - - - puvatpubat
keep - - whole puvax
keep - - - pubnq
keep - - - pubpun
keep - - - pubxr gur puvpxra
drop pubyb
keep - - - pyvg
keep - - whole pbpx
drop pbwbarf
keep - - whole pbbyvr
keep - - whole pbba
drop pbbg
drop pbbgvrf
keep - - - pbggba cvpxre
keep - - - pernzcvr
drop pebgpu
drop pebj
keep - - whole phz
keep - - - phaavyvathf
keep - - anywhere phag
keep - - whole qntb
keep - mild - qnza
keep - - whole qnexl
keep - - whole qvpx
keep - - whole qvqqyr
keep - - - qvyqb
drop qvcfgvpx
keep - - - qvegl fnapurm
keep - - - qbttl fglyr
keep insults - whole qbyg
drop qbat
keep - - - qbgurnq
drop qbhoyr cra
keep - - whole qbhpur
keep - - - qhzc n ybnq
keep - - whole qlxr
drop rwnphyngvba
keep - - - rerpgbcynfz
keep - - whole snt
keep - - anywhere snttbg
keep - - whole sneg
keep - - - sryyngvb
drop svatre
keep - - whole synzre
drop sberfxva
keep - - anywhere shpx
keep slurs - - shqtr cnpxre
keep - - - shaontf
drop tneontr ont
keep - - - tnfu-fgnoore
drop tnl
keep - - - tva wbpxrl
keep - - - tveyl ovgf
keep insults - - tveylobl
keep - - whole tbbx
keep - - whole tbjy
drop ternfre
keep - - start tebvq
drop tebcr
keep - - whole tlcfl
drop unt
keep - - - uneel cnyzf
keep - - whole uror
keep - - whole uryy
keep swearing - whole ubr
drop ubzbfrkhny
drop ubbgref
drop uhfgyre
keep - - whole vawha
keep - - - wnpx bss
keep - - - wnpxbss
keep - - - wnvyonvg
keep - - whole wnc
keep - - whole wrex
keep - - - wrex bss
drop wrj
keep - - - wvttnobb
drop wvz pebj
keep - - - wvmm
keep - - - xvqgbhpure
keep - - whole xvxr
keep - - - xvggl chapure
keep slurs - whole xxx
keep - - whole xyna
keep - strong whole xabo
keep - - - xh xyhkre
keep - - - ynqlobl
drop yrfovna
drop ybbfr
keep - - - zna pubjqre
keep - - - zna frrq
keep - - - znfgheongr
drop zrng
keep - - - zrng phegnvaf
keep - - - zvys
drop zbyrfg
keep - - whole zbat
keep - - whole zbatbybvq
drop zbatery
drop zbaxrl
keep - - - zbgureshpxre
keep - - whole zhss
keep - - - zhfuebbz gvc
keep - - whole arteb
keep - - anywhere avttn
keep - - anywhere avttre
keep - - - avttref
keep - - - avtttre
drop avccyr
keep - - whole abapr
drop ahg ohggre
keep - - - ahgfnpx
drop alzcu
keep - - - betnfz
keep - - - bira qbqtre
keep - - whole cnxv
keep - - - cnyz wbpxrl
keep - - - cnapnxr snpr
keep - - whole crpxre
keep insults - - crqbcuvyr
drop cravf
keep - - - crgre chssre
drop cvt
keep slurs - - cvyybj ovgre
keep - - - cvff
keep - - - cbyr yvpxre
keep - - - cbyr fzbxre
keep - - - cbyr fhpxre
keep - - - cbyrfzbxre
keep - - - cbepu zbaxrl
keep - - whole cevpx
keep - - - chyy gur chq
keep - - - chanav
drop chagn
keep - - whole chffl
drop dhrre
keep - - - enturnq
drop enzebq
keep - - - ernpunebhaq
drop erpghz
keep - - - erqfxva
keep - - whole ergneq
keep - - whole ergneqrq
keep - - - evz wbo
drop fnqbznfbpuvfz
keep - - - fnynq gbffre
keep - - whole fnzob
keep - - - fnhfntr wbpxrl
keep - - - fpuybat
keep insults - whole fphz
drop fphg
drop frk
keep - - - furznyr
drop fuvrfgre
keep - - anywhere fuvg
keep - - - fuybat
keep - - whole fulybpx
keep insults - whole fvffl
keep swearing - - fxnax
keep swearing - whole fynt
keep - - - fynag rlr
drop fyvg
keep swearing - - fyhg
drop fangpu
drop fcnax
drop fcrez
drop fcuvapgre
keep - - whole fcvp
drop fcbbx
drop fchax
keep - - - fghzc purjre
drop fhpx
drop fjvar
keep - - - gnpburnq
keep - - - gnqtre
keep - - - gnxvat gur cvff
keep - - - gnyyljnpxre
keep - - - gne-onol
drop gneg
drop grfgvpyrf
keep - - - guebng lbtheg
keep - - - guebngre
keep - - - gvpxyr gur cvpxyr
keep - - whole gvg
keep - - whole gvgf
keep - - whole gbffre
keep - - - gbffvat fnynq
keep - - - gbjryurnq
drop genzc
keep - - whole genaal
drop genafirfgvgr
drop genfu
keep - - - gebhfre fanxr
keep - - - gebhfrefanxr
keep - - whole gheq
keep - - - gjng
drop gjvax
keep - - - hcfxveg
drop intvan
drop ihyin
drop jnat
keep - - - jnax
drop jrravr
drop jrvare
keep - - - jrgonpx
keep - - - junpx bss
keep swearing - - juber
keep - - - jvyyl-junpxre
keep - - - jvaqbj yvpxre
keep - - whole jbc
drop jbhaq
keep - - - mvccreurnq
# spellings of kept words that are everyday words, or another's initials
drop onfgvanqb
drop crgre
drop ahttrgf
drop ahttrg
drop xavpxref
drop avpxre
drop avpxref
drop fnpx
drop ongre
drop ongrf
drop abof
drop abo
drop fbof
drop fbo
drop ubne
drop chfflpng
drop chffrf
drop chff
drop qvk
drop srq
drop sre
drop srt
drop sbexrq
drop furrg
drop oop
drop ojp
drop ow
drop fcvpx
+46
View File
@@ -403,6 +403,10 @@ the setting for one start of the game. It has priority over the file.
| `network.stun_servers` | Google and Cloudflare | `HALO_NET_STUN` | The public STUN servers (`host:port`, with commas between them) that give the internet address of a machine. |
| `discord.application_id` | the application of the project | `HALO_DISCORD_APPLICATION` | The Discord application for invites. Empty: no Discord. |
| `update.auto` | `true` | `HALO_UPDATE_AUTO` | `true`: at start-up, the game looks for a new version. Refer to "Updates". `false`: the game does not look. |
| `streamer.enabled` | `false` | `HALO_STREAMER_MODE` | `true`: streamer mode hides offensive words, as stars, in the names and text of other players, maps and mods, at the levels below. Refer to "Streamer mode". STREAMER MODE in Settings > Network Setup > Streamer Mode. |
| `streamer.slurs` | `"mild"` | `HALO_STREAMER_SLURS` | With `streamer.enabled`, the slurs (race, ethnicity, sexuality, gender, disability) hidden: `"off"`, `"severe"`, `"strong"` (strong and severe) or `"mild"` (all). CENSOR SLURS in Settings > Network Setup > Streamer Mode. |
| `streamer.swearing` | `"strong"` | `HALO_STREAMER_SWEARING` | With `streamer.enabled`, the swearing and sexual words hidden: `"off"`, `"severe"`, `"strong"` or `"mild"`. CENSOR SWEARING in Settings > Network Setup > Streamer Mode. |
| `streamer.insults` | `"off"` | `HALO_STREAMER_INSULTS` | With `streamer.enabled`, the insults that are not slurs hidden: `"off"`, `"severe"`, `"strong"` or `"mild"`. CENSOR INSULTS in Settings > Network Setup > Streamer Mode. |
| `crash_reports.upload` | `"ask"` | `HALO_CRASH_REPORTS` | Windows only. `"yes"`: the game sends a report of each crash to the developers. `"no"`: the game sends no reports. `"ask"`: the game asks at the next crash and writes the answer here. Refer to "Crash reports" in [port/windows/README.md](../windows/README.md#crash-reports). |
| `debug.update_answer` | `""` | `HALO_UPDATE_ANSWER` | The answer to the update question, for automatic tests: `yes`, `no` or `never`. Empty: the game asks. |
| `debug.exit_after` | `0.0` | `HALO_EXIT_AFTER` | The game stops after this number of seconds. `0`: never. |
@@ -991,6 +995,47 @@ checks every file before it plays it, and leaves out (with a line in
replaces. A mod changes nothing that the machines of a
network game agree on. Refer to [docs/mods.md](../../docs/mods.md).
## Streamer mode
Streamer mode (Settings > Network Setup > Streamer Mode, `streamer.enabled`)
hides offensive words as stars wherever the game draws text that other
people or files wrote: players' names over their heads, in the kill feed, on
the scoreboards and in the lobbies, the server browser's games, the host's
notices in the console, map and mod names, and the text of a map's or mod's
own string lists, as well as the message boxes about mods and maps. It
changes only what this machine shows: the names others see, and what the
machines send each other, stay as they are.
The words come from Surge AI's rated list of English profanities, reviewed
for the game (`port/assets/text_filter`), in three groups, each with a level
that hides that severity and worse:
| Group | Setting | Default | What it has |
| --- | --- | --- | --- |
| SLURS | `streamer.slurs` | `"mild"` (all) | Slurs about race, ethnicity, nationality, religion, sexuality, gender and disability |
| SWEARING | `streamer.swearing` | `"strong"` | Swearing and sexual words; the mildest (damn, hell) show at `"strong"` |
| INSULTS | `streamer.insults` | `"off"` | Name-calling that is not a slur |
A word is found however it is dressed: in capitals or with accents, in
look-alike letters (Cyrillic, Greek, full-width, small capitals), in leet
(`0` for o, `@` for a, `ph` for f), with its letters stretched ("fuuuck"),
with punctuation between them (`f.u.c.k`), spaced out (`f u c k`, `fu ck`),
and with characters the game does not draw (zero-width spaces, the menus'
`|` codes) between them. Each word may be found inside a name, at a word's
start, or only as a whole word, as fits it, and some words are never
hidden though a hidden one is inside them (Scunthorpe). `python3
tools/text_filter_terms.py --check "some text"` shows what would be hidden.
What it cannot catch: letters added into a word or its letters scrambled, new
spellings, a word split between two names or two lines, voice chat (mute a
player from the scoreboard, or set `audio.voice_volume` to 0), and pictures.
What you type is shown as you type it.
Your own words: `text_filter.txt` in the data root, with lines of `add
<word>` (hidden whenever streamer mode is on) and `allow <word>` (never
hidden), read when the game starts and when the settings change. The web
build's page draws its own list of mods, which is not filtered.
## Map checks
The game reads a map's tags straight into memory and uses them as its own
@@ -1168,6 +1213,7 @@ Other changes:
| `rasterizer/rasterizer.c` | The 128 KB of vertices of the debug display of a model's vertices are static, not on the stack: the compiler inlines the function in `render_model`, and the Windows build probed the stack a page at a time for every model drawn. |
| `render/render.c` | The 3D view of each window is antialiased before the HUD is drawn (`display.anti_aliasing`). |
| `interface/hud.c` | In multiplayer, players' names are drawn above their heads (`display.player_names`, `display.player_name_scale`). |
| `text/draw_string.c`, `interface/terminal.c`, `interface/virtual_keyboard.c` | Streamer mode's words are hidden in every string drawn or measured (`game/text_filter.c`), but in the console's line and the virtual keyboard's text being typed. |
| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the resolution the game draws at (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. |
| `interface/first_person_weapons.c` | The first-person hands are drawn with the unit's shield modifier shader, as its body is (`display.viewmodel_shield`). |
| `render/render_objects.c`, `objects/objects.c` | In first person, your own body is drawn from the waist down with the pose `first_person_legs.c` makes (`display.first_person_legs`); the waist it found and the mesh `object_mesh.c` read are forgotten on a new map. |
+7
View File
@@ -85,6 +85,7 @@ their handlers open opens.
#include "text/draw_string.h"
/* (internet play's server browser: the platform layer's) */
#include "../src/p2p.h"
#include "text_filter.h"
#include <stdlib.h>
#include <string.h>
@@ -2924,6 +2925,12 @@ static void text_field_show(struct widget_instance *value, char const *text, boo
shown[index++] = L'_';
shown[index] = 0;
text_set(value, shown);
/* (what is being typed shown as it is: streamer mode hides nothing of it,
text_filter.c) */
if (editing && value)
text_filter_exempt(value->parameters.text_box.text);
else if (!text_field_editing(NULL))
text_filter_exempt(NULL);
}
/* ---- the menu */
+392
View File
@@ -0,0 +1,392 @@
/*
TEXT_FILTER.C
Streamer mode (Settings > Network Setup > Streamer Mode): the words of
text_filter_match.c's list hidden as stars wherever text is drawn, at the
levels set (streamer.enabled, and streamer.slurs, streamer.swearing and
streamer.insults: "off", "severe", "strong" or "mild", each that severity
and worse).
draw_string.c's draw_string and draw_unicode_string take the string's
filtered copy before they draw or measure it (draw_string_compute_bounds
and the rest measure through them), so every string the game shows is
filtered: names over heads, the kill feed, the scoreboards, the lobbies, the
server browser, map and mod names, the console's lines, and a map's or mod's
own text. A copy is the same length or shorter (a censored character is one
'*'), so a fixed buffer is never overrun. Copies are kept, so that a string
drawn each frame is looked at once.
What the player types is drawn as it is: the menus' text field being edited
(text_filter_exempt), the console's input line and the virtual keyboard's
(text_filter_suspend). The player's own words: d:\text_filter.txt, lines of
"add <word>" (hidden whenever streamer mode is on) and "allow <word>" (never
hidden), read as the settings change.
Only what this machine draws changes: nothing sent between machines.
*/
#include "text_filter.h"
#include <stdio.h>
#include <string.h>
/* port/linux/src/port_config.c */
int config_boolean(const char *name);
const char *config_string(const char *name);
unsigned long config_changes(void);
#define USER_FILE "d:\\text_filter.txt"
#define CACHE_ENTRIES 128
#define CACHE_LENGTH 256
#define LONG_BUFFERS 4
struct text_filter_cache_entry
{
unsigned long hash;
short length;
unsigned char narrow;
unsigned char clean;
unsigned short original[CACHE_LENGTH];
union
{
unsigned short wide[CACHE_LENGTH];
char narrow[CACHE_LENGTH];
} filtered;
};
static struct
{
unsigned long changes;
int enabled;
unsigned char levels[NUMBER_OF_TEXT_FILTER_GROUPS];
long suspended;
void const *exempt;
struct text_filter_cache_entry cache[CACHE_ENTRIES];
union
{
unsigned short wide[TEXT_FILTER_MAXIMUM_LENGTH + 1];
char narrow[TEXT_FILTER_MAXIMUM_LENGTH + 1];
} long_buffers[LONG_BUFFERS];
long next_long_buffer;
unsigned short units[TEXT_FILTER_MAXIMUM_LENGTH];
unsigned char censored[TEXT_FILTER_MAXIMUM_LENGTH];
} text_filter_globals = { (unsigned long)-1 };
static unsigned char text_filter_level(
char const *name)
{
char const *value = config_string(name);
if (!value || !strcmp(value, "off"))
return _text_filter_off;
if (!strcmp(value, "severe"))
return _text_filter_severe;
if (!strcmp(value, "strong"))
return _text_filter_strong;
return _text_filter_mild;
}
/* the list again with the player's own words (USER_FILE) */
static void text_filter_user_words(
void)
{
FILE *file = fopen(USER_FILE, "r");
char line[128];
text_filter_words_reset();
if (!file)
return;
while (fgets(line, sizeof(line), file))
{
char *word = line;
line[strcspn(line, "\r\n")] = 0;
while (*word == ' ' || *word == '\t')
word++;
if (!strncmp(word, "add ", 4))
text_filter_word_add(word + 4, _text_filter_own, _text_filter_severe, _text_filter_anywhere);
else if (!strncmp(word, "allow ", 6))
text_filter_allow_add(word + 6);
}
fclose(file);
return;
}
/* whether anything is hidden: the settings, read again when they change */
static int text_filter_enabled(
void)
{
if (text_filter_globals.changes != config_changes())
{
long index;
text_filter_globals.changes = config_changes();
text_filter_globals.enabled = config_boolean("streamer.enabled") != 0;
text_filter_globals.levels[_text_filter_slurs] = text_filter_level("streamer.slurs");
text_filter_globals.levels[_text_filter_swearing] = text_filter_level("streamer.swearing");
text_filter_globals.levels[_text_filter_insults] = text_filter_level("streamer.insults");
text_filter_globals.levels[_text_filter_own] = _text_filter_mild;
for (index = 0; index < CACHE_ENTRIES; index++)
text_filter_globals.cache[index].length = -1;
if (text_filter_globals.enabled)
text_filter_user_words();
}
return text_filter_globals.enabled && !text_filter_globals.suspended;
}
/* whether the string is a copy of ours (a string measured is drawn again
through draw_string or draw_unicode_string) */
static int text_filter_ours(
void const *string)
{
char const *pointer = (char const *)string;
char const *start = (char const *)&text_filter_globals;
return pointer >= start && pointer < start + sizeof(text_filter_globals);
}
static unsigned long text_filter_hash(
unsigned short const *units,
long length)
{
unsigned long hash = 2166136261UL;
long index;
for (index = 0; index < length; index++)
hash = (hash ^ units[index]) * 16777619UL;
return hash;
}
/* the units with what is hidden as '*' (a wide string's "|n" and a narrow
string's style codes kept, and a wide "|"'s hidden character dropped with
it), into wide or narrow; whether anything was */
static int text_filter_apply(
unsigned short const *units,
long length,
int narrow,
unsigned short *wide,
char *narrow_text)
{
unsigned char *censored = text_filter_globals.censored;
long index;
long written = 0;
if (!text_filter_censor(units, length, narrow, text_filter_globals.levels, censored))
return 0;
for (index = 0; index < length; index++)
{
unsigned short unit = units[index];
if (censored[index])
{
if (unit == '|' && index + 1 < length)
{
unsigned short next = units[index + 1];
if (next == 'n' || (narrow && next && next < 0x80 && strchr("ibukprlctn", (char)next)))
{
/* a line break or a style code: kept */
if (narrow)
{
narrow_text[written++] = '|';
narrow_text[written++] = (char)next;
}
else
{
wide[written++] = '|';
wide[written++] = next;
}
index++;
continue;
}
if (!narrow)
{
/* a "|" that hides the character after it: both one '*' */
index++;
}
unit = '*';
}
else if (unit != '\r' && unit != '\n' && unit != '\t')
{
unit = '*';
}
}
if (narrow)
narrow_text[written++] = (char)unit;
else
wide[written++] = unit;
}
if (narrow)
narrow_text[written] = 0;
else
wide[written] = 0;
return 1;
}
/* the string's filtered copy, or the string itself (nothing hidden, or a
string longer than TEXT_FILTER_MAXIMUM_LENGTH, which is drawn as it is) */
static void const *text_filter(
void const *string,
int narrow)
{
unsigned short *units = text_filter_globals.units;
struct text_filter_cache_entry *entry;
unsigned long hash;
long length = 0;
if (!string || string == text_filter_globals.exempt || text_filter_ours(string) || !text_filter_enabled())
return string;
if (narrow)
{
unsigned char const *text = (unsigned char const *)string;
while (text[length] && length <= TEXT_FILTER_MAXIMUM_LENGTH)
{
if (length < TEXT_FILTER_MAXIMUM_LENGTH)
units[length] = text[length];
length++;
}
}
else
{
unsigned short const *text = (unsigned short const *)string;
while (text[length] && length <= TEXT_FILTER_MAXIMUM_LENGTH)
{
if (length < TEXT_FILTER_MAXIMUM_LENGTH)
units[length] = text[length];
length++;
}
}
if (!length || length > TEXT_FILTER_MAXIMUM_LENGTH)
return string;
if (length >= CACHE_LENGTH)
{
/* a long string: filtered each time it is drawn, into one of a few
buffers in turn */
long buffer = text_filter_globals.next_long_buffer;
text_filter_globals.next_long_buffer = (buffer + 1) % LONG_BUFFERS;
if (!text_filter_apply(units, length, narrow, text_filter_globals.long_buffers[buffer].wide,
text_filter_globals.long_buffers[buffer].narrow))
{
return string;
}
return narrow ?
(void const *)text_filter_globals.long_buffers[buffer].narrow :
(void const *)text_filter_globals.long_buffers[buffer].wide;
}
hash = text_filter_hash(units, length) ^ (unsigned long)narrow;
entry = &text_filter_globals.cache[hash % CACHE_ENTRIES];
if (entry->length != length || entry->hash != hash || entry->narrow != narrow ||
memcmp(entry->original, units, length * sizeof(*units)))
{
entry->hash = hash;
entry->length = (short)length;
entry->narrow = (unsigned char)narrow;
memcpy(entry->original, units, length * sizeof(*units));
entry->clean = !text_filter_apply(units, length, narrow, entry->filtered.wide, entry->filtered.narrow);
}
if (entry->clean)
return string;
return narrow ? (void const *)entry->filtered.narrow : (void const *)entry->filtered.wide;
}
/* draw_unicode_string's string (draw_string.c) */
wchar_t const *text_filter_unicode(
wchar_t const *string)
{
return (wchar_t const *)text_filter(string, 0);
}
/* draw_string's string (draw_string.c) */
char const *text_filter_string(
char const *string)
{
return (char const *)text_filter(string, 1);
}
/* what is drawn until text_filter_resume drawn as it is (what the player
types) */
void text_filter_suspend(
void)
{
text_filter_globals.suspended++;
return;
}
void text_filter_resume(
void)
{
if (text_filter_globals.suspended > 0)
text_filter_globals.suspended--;
return;
}
/* the text (a widget's buffer) drawn as it is while it is being edited;
NULL: none */
void text_filter_exempt(
void const *text)
{
text_filter_globals.exempt = text;
return;
}
/* text shown outside the game's drawing (the platform's message boxes),
UTF-8, filtered in place (a hidden character, of however many bytes, one
'*') */
void text_filter_utf8(
char *text)
{
unsigned short *units = text_filter_globals.units;
unsigned char *censored = text_filter_globals.censored;
long starts[TEXT_FILTER_MAXIMUM_LENGTH];
unsigned char const *byte = (unsigned char const *)text;
long length = 0;
long index;
long written = 0;
long read = 0;
long total;
if (!text || !text_filter_enabled())
return;
while (*byte && length < TEXT_FILTER_MAXIMUM_LENGTH)
{
unsigned long code = *byte;
long size = 1;
if (code >= 0xE0 && byte[1] && byte[2])
{
code = ((code & 0x0F) << 12) | ((byte[1] & 0x3F) << 6) | (byte[2] & 0x3F);
size = 3;
}
else if (code >= 0xC0 && byte[1])
{
code = ((code & 0x1F) << 6) | (byte[1] & 0x3F);
size = 2;
}
starts[length] = (long)(byte - (unsigned char const *)text);
units[length++] = (unsigned short)code;
byte += size;
}
if (*byte || !text_filter_censor(units, length, 0, text_filter_globals.levels, censored))
return;
total = (long)(byte - (unsigned char const *)text);
for (index = 0; index < length; index++)
{
long next = index + 1 < length ? starts[index + 1] : total;
read = starts[index];
if (censored[index])
{
text[written++] = '*';
}
else
{
while (read < next)
text[written++] = text[read++];
}
}
text[written] = 0;
return;
}
+65
View File
@@ -0,0 +1,65 @@
/*
TEXT_FILTER.H
Streamer mode (Settings > Network Setup > Streamer Mode, streamer.*): the
words hidden as stars in what is drawn. text_filter_match.c finds them, with
no game types, so that the harness builds it alone
(tools/harness/tests/test_text_filter.py); text_filter.c applies them to
what the game draws.
*/
#ifndef __TEXT_FILTER_H__
#define __TEXT_FILTER_H__
#include <stddef.h>
/* the groups a player sets a level for, and the player's own words
(text_filter.txt), hidden whenever streamer mode is on */
enum text_filter_group
{
_text_filter_slurs,
_text_filter_swearing,
_text_filter_insults,
_text_filter_own,
NUMBER_OF_TEXT_FILTER_GROUPS
};
/* a term's severity (Surge AI's: mild, strong, severe), and a group's
level: the least severity hidden, or none */
enum text_filter_severity
{
_text_filter_off,
_text_filter_mild,
_text_filter_strong,
_text_filter_severe
};
/* where a term may be found: inside any word, at a word's start, or as the
whole word (or it and "s" or "es") */
enum text_filter_rule
{
_text_filter_anywhere,
_text_filter_start,
_text_filter_whole,
NUMBER_OF_TEXT_FILTER_RULES
};
/* the most code units of a string looked at (a longer one is drawn as it is) */
#define TEXT_FILTER_MAXIMUM_LENGTH 1024
/* text_filter_match.c */
void text_filter_words_reset(void);
int text_filter_word_add(char const *term, int group, int severity, int rule);
int text_filter_allow_add(char const *word);
long text_filter_censor(unsigned short const *text, long length, int narrow, unsigned char const *levels,
unsigned char *censored);
/* text_filter.c */
wchar_t const *text_filter_unicode(wchar_t const *string);
char const *text_filter_string(char const *string);
void text_filter_suspend(void);
void text_filter_resume(void);
void text_filter_exempt(void const *text);
void text_filter_utf8(char *text);
#endif
File diff suppressed because it is too large. Load diff
+758
View File
@@ -0,0 +1,758 @@
/*
TEXT_FILTER_MATCH.C
Streamer mode's matcher: which code units of a string spell a hidden word,
however it is dressed (README.md, "Streamer mode"). A string is read as the
game draws it (draw_string.c's parsers: a wide string's "|n" breaks a line
and any other "|" hides the character after it; a narrow string's style
codes are not drawn), and each character it shows becomes an element:
- a letter, of a-z: the case folded, accents and look-alikes (Cyrillic,
Greek, full-width, small capitals: text_filter_fold.inc) made plain;
- a digit or symbol read as letters (leet: 0 o, 1 i or l, 3 e, @ a, $ s...),
or as any letter (* # % ?, the last of which is what the server browser's
listings show for a character they cannot), which may also be
punctuation;
- a space or punctuation, which a word may be spelled across;
- anything else, which no word crosses.
Two elements together may read as one letter (ph f, vv w, \/ v, |< k).
Then each term (text_filter_terms.inc, made by tools/text_filter_terms.py
from Surge AI's list; and the player's own) is looked for from each element:
a run of one letter reads as one ("fuuuck"); punctuation may come between
letters ("f.u.c.k"); spaces too where the pieces are a letter or two
("f u c k", "fu ck") or the match is whole words ("f uck"). A term's rule
says where it may be (inside a word, at a word's start, or as the whole
word), and the allowed words (Scunthorpe) are never hidden. Nothing here
knows the game: the harness builds it alone (tools/harness).
*/
#include "text_filter.h"
#include <string.h>
#define MAXIMUM_TERMS 2048
#define MAXIMUM_TERM_CHARACTERS 32768
#define MAXIMUM_TERM_LENGTH 40
#define MAXIMUM_ALLOWED 1024
#define MAXIMUM_ALLOWED_CHARACTERS 16384
#define MAXIMUM_GAP 4
#define ALL_LETTERS 0x03FFFFFFUL
#define LETTER(c) (1UL << ((c) - 'a'))
/* text_filter_terms.inc's terms: their letters with each byte xor'd by
TEXT_FILTER_TERM_KEY, so that the program holds no readable list */
struct text_filter_term_definition
{
unsigned char group;
unsigned char severity;
unsigned char rule;
char const *letters;
};
/* text_filter_fold.inc's characters read as letters */
struct text_filter_fold
{
unsigned short code;
unsigned long letters;
};
#include "text_filter_terms.inc"
#include "text_filter_fold.inc"
enum
{
_element_letter,
_element_digit,
_element_symbol,
_element_wildcard,
_element_space,
_element_punctuation,
_element_break
};
struct element
{
unsigned long letters;
unsigned long pair;
short first;
short last;
unsigned short code;
unsigned char kind;
};
struct term
{
short offset;
unsigned char length;
unsigned char group;
unsigned char severity;
unsigned char rule;
};
static struct
{
int ready;
long term_count;
long character_count;
struct term terms[MAXIMUM_TERMS];
char characters[MAXIMUM_TERM_CHARACTERS];
/* the terms by their first letter: first[letter] .. first[letter + 1] of
order */
short order[MAXIMUM_TERMS];
short first[27];
int sorted;
long allowed_count;
long allowed_character_count;
short allowed[MAXIMUM_ALLOWED];
char allowed_characters[MAXIMUM_ALLOWED_CHARACTERS];
} text_filter_words;
/* a term's or allowed word's letters, kept a-z only (others dropped);
their count */
static long text_filter_letters(
char *letters,
long size,
char const *text)
{
long length = 0;
for (; *text; text++)
{
char character = *text;
if (character >= 'A' && character <= 'Z')
character = (char)(character - 'A' + 'a');
if (character >= 'a' && character <= 'z' && length < size - 1)
letters[length++] = character;
}
letters[length] = 0;
return length;
}
static int text_filter_word_store(
char const *letters,
long length,
int group,
int severity,
int rule)
{
struct term *term;
if (length < 2 || length > MAXIMUM_TERM_LENGTH ||
text_filter_words.term_count >= MAXIMUM_TERMS ||
text_filter_words.character_count + length + 1 > MAXIMUM_TERM_CHARACTERS ||
group < 0 || group >= NUMBER_OF_TEXT_FILTER_GROUPS ||
severity < _text_filter_mild || severity > _text_filter_severe ||
rule < 0 || rule >= NUMBER_OF_TEXT_FILTER_RULES)
{
return 0;
}
term = &text_filter_words.terms[text_filter_words.term_count++];
term->offset = (short)text_filter_words.character_count;
term->length = (unsigned char)length;
term->group = (unsigned char)group;
term->severity = (unsigned char)severity;
term->rule = (unsigned char)rule;
memcpy(text_filter_words.characters + text_filter_words.character_count, letters, length + 1);
text_filter_words.character_count += length + 1;
text_filter_words.sorted = 0;
return 1;
}
/* the built-in terms and allowed words again, without the player's */
void text_filter_words_reset(
void)
{
long index;
text_filter_words.term_count = 0;
text_filter_words.character_count = 0;
text_filter_words.allowed_count = 0;
text_filter_words.allowed_character_count = 0;
for (index = 0; index < (long)(sizeof(text_filter_term_definitions) / sizeof(*text_filter_term_definitions)); index++)
{
struct text_filter_term_definition const *definition = &text_filter_term_definitions[index];
char letters[MAXIMUM_TERM_LENGTH + 1];
long length = 0;
char const *encoded;
for (encoded = definition->letters; *encoded && length < MAXIMUM_TERM_LENGTH; encoded++)
letters[length++] = (char)(*encoded ^ TEXT_FILTER_TERM_KEY);
letters[length] = 0;
text_filter_word_store(letters, length, definition->group, definition->severity, definition->rule);
}
for (index = 0; index < (long)(sizeof(text_filter_allowed_words) / sizeof(*text_filter_allowed_words)); index++)
text_filter_allow_add(text_filter_allowed_words[index]);
text_filter_words.ready = 1;
return;
}
/* a term of the player's, hidden at its group's level (a-z: others are
dropped); whether it was kept */
int text_filter_word_add(
char const *term,
int group,
int severity,
int rule)
{
char letters[MAXIMUM_TERM_LENGTH + 2];
long length;
if (!text_filter_words.ready)
text_filter_words_reset();
length = text_filter_letters(letters, sizeof(letters), term);
return text_filter_word_store(letters, length, group, severity, rule);
}
/* a word never hidden, nor any part of it (a-z); whether it was kept */
int text_filter_allow_add(
char const *word)
{
char letters[MAXIMUM_TERM_LENGTH + 2];
long length = text_filter_letters(letters, sizeof(letters), word);
if (length < 2 || length > MAXIMUM_TERM_LENGTH ||
text_filter_words.allowed_count >= MAXIMUM_ALLOWED ||
text_filter_words.allowed_character_count + length + 1 > MAXIMUM_ALLOWED_CHARACTERS)
{
return 0;
}
text_filter_words.allowed[text_filter_words.allowed_count++] = (short)text_filter_words.allowed_character_count;
memcpy(text_filter_words.allowed_characters + text_filter_words.allowed_character_count, letters, length + 1);
text_filter_words.allowed_character_count += length + 1;
return 1;
}
/* the terms by their first letter */
static void text_filter_words_sort(
void)
{
long letter;
long count = 0;
for (letter = 0; letter < 26; letter++)
{
long index;
text_filter_words.first[letter] = (short)count;
for (index = 0; index < text_filter_words.term_count; index++)
{
if (text_filter_words.characters[text_filter_words.terms[index].offset] == 'a' + letter)
text_filter_words.order[count++] = (short)index;
}
}
text_filter_words.first[26] = (short)count;
text_filter_words.sorted = 1;
return;
}
/* ---------- elements */
/* a character above ASCII read as letters (text_filter_fold.inc), or 0 */
static unsigned long text_filter_fold_letters(
unsigned short code)
{
long low = 0;
long high = (long)(sizeof(text_filter_folds) / sizeof(*text_filter_folds)) - 1;
while (low <= high)
{
long middle = (low + high) / 2;
if (text_filter_folds[middle].code == code)
return text_filter_folds[middle].letters;
if (text_filter_folds[middle].code < code)
low = middle + 1;
else
high = middle - 1;
}
return 0;
}
/* what a character is (an _element_ kind; NONE (-1) for one not drawn), and
the letters it may read as */
static int text_filter_classify(
unsigned short code,
unsigned long *letters)
{
*letters = 0;
if (code >= 0xFF01 && code <= 0xFF5E)
code = (unsigned short)(code - 0xFEE0);
if (code >= 'A' && code <= 'Z')
code = (unsigned short)(code - 'A' + 'a');
if (code >= 'a' && code <= 'z')
{
*letters = LETTER(code);
return _element_letter;
}
switch (code)
{
case '0': *letters = LETTER('o'); return _element_digit;
case '1': *letters = LETTER('i') | LETTER('l'); return _element_digit;
case '2': *letters = LETTER('z'); return _element_digit;
case '3': *letters = LETTER('e'); return _element_digit;
case '4': *letters = LETTER('a'); return _element_digit;
case '5': *letters = LETTER('s'); return _element_digit;
case '6': *letters = LETTER('g') | LETTER('b'); return _element_digit;
case '7': *letters = LETTER('t'); return _element_digit;
case '8': *letters = LETTER('b'); return _element_digit;
case '9': *letters = LETTER('g') | LETTER('q'); return _element_digit;
case '@': *letters = LETTER('a'); return _element_symbol;
case '$': *letters = LETTER('s'); return _element_symbol;
case '!': *letters = LETTER('i'); return _element_symbol;
case '+': *letters = LETTER('t'); return _element_symbol;
case '(': *letters = LETTER('c'); return _element_symbol;
case '<': *letters = LETTER('c'); return _element_symbol;
case '|': *letters = LETTER('i') | LETTER('l'); return _element_symbol;
case 0x00A1: *letters = LETTER('i'); return _element_symbol;
case 0x00A2: *letters = LETTER('c'); return _element_symbol;
case 0x00A3: *letters = LETTER('l') | LETTER('e'); return _element_symbol;
case 0x00A5: *letters = LETTER('y'); return _element_symbol;
case 0x00A7: *letters = LETTER('s'); return _element_symbol;
case 0x00A9: *letters = LETTER('c'); return _element_symbol;
case 0x00AE: *letters = LETTER('r'); return _element_symbol;
case 0x00D7: *letters = LETTER('x'); return _element_symbol;
case 0x20AC: *letters = LETTER('e'); return _element_symbol;
case '*': case '#': case '%': case '?':
*letters = ALL_LETTERS;
return _element_wildcard;
case ' ': case '\t': case '\r': case '\n': case 0x00A0: case 0x1680: case 0x202F: case 0x205F: case 0x3000:
return _element_space;
}
if (code >= 0x2000 && code <= 0x200A)
return _element_space;
/* invisible: the soft hyphen, joiners, marks over letters, direction
and variation selectors */
if (code == 0x00AD || code == 0x034F || code == 0x180E || code == 0xFEFF ||
(code >= 0x0300 && code <= 0x036F) || (code >= 0x1AB0 && code <= 0x1AFF) ||
(code >= 0x1DC0 && code <= 0x1DFF) || (code >= 0x20D0 && code <= 0x20FF) ||
(code >= 0x200B && code <= 0x200F) || (code >= 0x202A && code <= 0x202E) ||
(code >= 0x2060 && code <= 0x2069) || (code >= 0xFE00 && code <= 0xFE0F) ||
(code >= 0xFE20 && code <= 0xFE2F))
{
return -1;
}
if (code >= 0x80)
{
*letters = text_filter_fold_letters(code);
if (*letters)
return _element_letter;
}
if ((code > ' ' && code < 0x7F) || (code >= 0x00A1 && code <= 0x00BF) ||
(code >= 0x2010 && code <= 0x2027) || (code >= 0x2030 && code <= 0x205E))
{
return _element_punctuation;
}
return _element_break;
}
static int text_filter_style_code(
unsigned short code)
{
return code && code < 0x80 && strchr("ibukprlctn", (char)code) != NULL;
}
/* the string's elements as it is drawn; their count */
static long text_filter_elements(
unsigned short const *text,
long length,
int narrow,
struct element *elements,
long maximum)
{
long count = 0;
long index = 0;
long element_index;
while (index < length && count < maximum)
{
unsigned short code = text[index];
struct element *element = &elements[count];
long last = index;
int kind;
if (code == '|' && index + 1 < length)
{
unsigned short next = text[index + 1];
if (narrow ? text_filter_style_code(next) : 1)
{
last = index + 1;
if (next == 'n' || (narrow && next == 't'))
{
element->letters = 0;
element->kind = _element_space;
element->code = ' ';
element->first = (short)index;
element->last = (short)last;
count++;
index = last + 1;
continue;
}
if (narrow)
{
/* a style code: not drawn */
index = last + 1;
continue;
}
/* a wide string's "|": drawn, the character after it not */
}
}
kind = text_filter_classify(code, &element->letters);
if (kind < 0)
{
index++;
continue;
}
element->kind = (unsigned char)kind;
element->code = code >= 'A' && code <= 'Z' ? (unsigned short)(code - 'A' + 'a') : code;
element->first = (short)index;
element->last = (short)last;
count++;
index = last + 1;
}
/* pairs read as one letter */
for (element_index = 0; element_index < count; element_index++)
{
unsigned short first = elements[element_index].code;
unsigned short second = element_index + 1 < count ? elements[element_index + 1].code : 0;
unsigned long pair = 0;
if (first == 'p' && second == 'h') pair = LETTER('f');
else if (first == 'v' && second == 'v') pair = LETTER('w');
else if (first == '\\' && second == '/') pair = LETTER('v');
else if (first == '/' && second == '\\') pair = LETTER('a');
else if (first == '|' && second == '<') pair = LETTER('k');
else if (first == '>' && second == '<') pair = LETTER('x');
else if (first == '(' && second == ')') pair = LETTER('o');
else if (first == '[' && second == ']') pair = LETTER('o');
else if (first == '|' && second == ')') pair = LETTER('d');
elements[element_index].pair = pair;
}
return count;
}
/* ---------- matching */
struct attempt
{
struct element const *elements;
long count;
char const *letters;
long length;
int rule;
long start;
long end;
};
/* what a match has crossed so far */
struct path
{
long wildcards;
long real_letters;
long piece;
long longest_piece;
int spaced;
int crossed;
};
static int text_filter_try_letter(struct attempt *attempt, long term_index, long element_index, struct path path);
static int text_filter_word_character(
struct element const *element)
{
return element->kind == _element_letter;
}
/* whether an allowed word covers the match, inside the word it is in */
static int text_filter_allowed(
struct attempt const *attempt)
{
long word_start = attempt->start;
long word_end = attempt->end;
long index;
while (word_start > 0 &&
(attempt->elements[word_start - 1].kind == _element_letter || attempt->elements[word_start - 1].kind == _element_digit))
{
word_start--;
}
while (word_end < attempt->count &&
(attempt->elements[word_end].kind == _element_letter || attempt->elements[word_end].kind == _element_digit))
{
word_end++;
}
for (index = 0; index < text_filter_words.allowed_count; index++)
{
char const *allowed = text_filter_words.allowed_characters + text_filter_words.allowed[index];
long length = (long)strlen(allowed);
long at;
for (at = word_start; at + length <= word_end; at++)
{
long offset;
if (at > attempt->start || at + length < attempt->end)
continue;
for (offset = 0; offset < length; offset++)
{
if (!(attempt->elements[at + offset].letters & LETTER(allowed[offset])))
break;
}
if (offset == length)
return 1;
}
}
return 0;
}
/* the term read to the element before end: whether the match stands */
static int text_filter_finish(
struct attempt *attempt,
long end,
struct path path)
{
struct element const *elements = attempt->elements;
int start_boundary = attempt->start == 0 || !text_filter_word_character(&elements[attempt->start - 1]);
int end_boundary = end >= attempt->count || !text_filter_word_character(&elements[end]);
if (path.piece > path.longest_piece)
path.longest_piece = path.piece;
/* any letter (* ? #) once in a short word, twice in a longer one, and
never in a word of three letters */
if (path.wildcards && (attempt->length < 4 || path.wildcards > (attempt->length <= 5 ? 1 : 2)))
return 0;
/* a third of the letters letters, not digits or symbols (not "8008") */
if (!path.real_letters || path.real_letters * 3 < attempt->length)
return 0;
/* across spaces: pieces of a letter or two, or whole words */
if (path.spaced && path.longest_piece > 2 && !(start_boundary && end_boundary))
return 0;
switch (attempt->rule)
{
case _text_filter_start:
if (!start_boundary)
return 0;
break;
case _text_filter_whole:
if (!start_boundary)
return 0;
if (!end_boundary)
{
/* or with "s", or "es" after s, x, z, ch or sh */
long after = end;
char last = attempt->letters[attempt->length - 1];
char before_last = attempt->length > 1 ? attempt->letters[attempt->length - 2] : 0;
if ((last == 's' || last == 'x' || last == 'z' || (last == 'h' && (before_last == 'c' || before_last == 's'))) &&
after < attempt->count && (elements[after].letters & LETTER('e')) && elements[after].kind == _element_letter &&
after + 1 < attempt->count && (elements[after + 1].letters & LETTER('s')))
{
after++;
}
if (after < attempt->count && (elements[after].letters & LETTER('s')) &&
(elements[after].kind == _element_letter || elements[after].kind == _element_digit) &&
(after + 1 >= attempt->count || !text_filter_word_character(&elements[after + 1])))
{
end = after + 1;
}
else
{
return 0;
}
}
break;
}
if (!path.crossed)
{
attempt->end = end;
if (text_filter_allowed(attempt))
return 0;
}
attempt->end = end;
return 1;
}
/* the term's letter at term_index from element_index on, after what came
before (spaces and punctuation between letters skipped, at most
MAXIMUM_GAP) */
static int text_filter_next(
struct attempt *attempt,
long term_index,
long element_index,
long gap,
int spaced,
struct path path)
{
struct element const *element;
if (term_index == attempt->length)
return text_filter_finish(attempt, element_index, path);
if (element_index >= attempt->count)
return 0;
element = &attempt->elements[element_index];
if (gap > 0)
{
path.crossed = 1;
if (spaced)
{
path.spaced = 1;
if (path.piece > path.longest_piece)
path.longest_piece = path.piece;
path.piece = 0;
}
}
if ((element->letters || element->pair) && text_filter_try_letter(attempt, term_index, element_index, path))
return 1;
if (term_index > 0 && gap < MAXIMUM_GAP &&
(element->kind == _element_space || element->kind == _element_punctuation ||
element->kind == _element_symbol || element->kind == _element_wildcard))
{
return text_filter_next(attempt, term_index, element_index + 1, gap + 1,
spaced || element->kind == _element_space, path);
}
return 0;
}
/* after the term's letter at term_index, read to element end: the same
letter again ("fuuuck") is part of it, unless the term has it twice; for a
short whole word, only three or more of it ("woops" is not a short word's
"o" twice) */
static int text_filter_after(
struct attempt *attempt,
long term_index,
long end,
struct path path)
{
char letter = attempt->letters[term_index];
if (term_index + 1 >= attempt->length || attempt->letters[term_index + 1] != letter)
{
long repeated = end;
while (repeated < attempt->count &&
(attempt->elements[repeated].kind == _element_letter || attempt->elements[repeated].kind == _element_digit) &&
(attempt->elements[repeated].letters & LETTER(letter)))
{
repeated++;
}
if (repeated != end &&
(attempt->rule != _text_filter_whole || attempt->length > 4 || repeated - end >= 2))
{
struct path longer = path;
longer.piece += repeated - end;
if (text_filter_next(attempt, term_index + 1, repeated, 0, 0, longer))
return 1;
}
}
return text_filter_next(attempt, term_index + 1, end, 0, 0, path);
}
static int text_filter_try_letter(
struct attempt *attempt,
long term_index,
long element_index,
struct path path)
{
struct element const *element = &attempt->elements[element_index];
unsigned long letter = LETTER(attempt->letters[term_index]);
if (element->letters & letter)
{
struct path one = path;
one.piece++;
if (element->kind == _element_wildcard)
one.wildcards++;
else if (element->kind == _element_letter)
one.real_letters++;
if ((element->kind != _element_wildcard || term_index > 0) &&
text_filter_after(attempt, term_index, element_index + 1, one))
{
return 1;
}
}
if (element->pair & letter)
{
struct path two = path;
two.piece++;
if (element->kind == _element_letter)
two.real_letters++;
if (text_filter_after(attempt, term_index, element_index + 2, two))
return 1;
}
return 0;
}
/* which code units of the text (of a wide string, or narrow: char's widened)
spell a term of a group whose level its severity reaches (levels: one for
each group, _text_filter_off or the least severity hidden), marked in
censored (as long as the text); how many */
long text_filter_censor(
unsigned short const *text,
long length,
int narrow,
unsigned char const *levels,
unsigned char *censored)
{
static struct element elements[TEXT_FILTER_MAXIMUM_LENGTH];
long count;
long start;
long marked = 0;
memset(censored, 0, length);
if (!text_filter_words.ready)
text_filter_words_reset();
if (!text_filter_words.sorted)
text_filter_words_sort();
count = text_filter_elements(text, length, narrow, elements, TEXT_FILTER_MAXIMUM_LENGTH);
for (start = 0; start < count; start++)
{
unsigned long letters = elements[start].kind == _element_wildcard ? 0 : (elements[start].letters | elements[start].pair);
long letter;
for (letter = 0; letters && letter < 26; letter++)
{
long order;
if (!(letters & (1UL << letter)))
continue;
for (order = text_filter_words.first[letter]; order < text_filter_words.first[letter + 1]; order++)
{
struct term const *term = &text_filter_words.terms[text_filter_words.order[order]];
struct attempt attempt;
struct path path;
if (levels[term->group] == _text_filter_off || term->severity < levels[term->group])
continue;
attempt.elements = elements;
attempt.count = count;
attempt.letters = text_filter_words.characters + term->offset;
attempt.length = term->length;
attempt.rule = term->rule;
attempt.start = start;
attempt.end = start;
memset(&path, 0, sizeof(path));
if (text_filter_try_letter(&attempt, 0, start, path))
{
long unit;
for (unit = elements[start].first; unit <= elements[attempt.end - 1].last; unit++)
{
if (!censored[unit])
{
censored[unit] = 1;
marked++;
}
}
}
}
}
}
return marked;
}
+653
View File
@@ -0,0 +1,653 @@
/* port/linux/game/text_filter_terms.inc: streamer mode's words, written by
tools/text_filter_terms.py from Surge AI's profanity_en.csv
(sha256 ca4ff7d713a8e6f6ec2616f489cd09f6fb96b8b3af404a207783be58a91f3ede) and port/assets/text_filter/review.txt and allow.txt.
Each term's letters are xor'd with TEXT_FILTER_TERM_KEY. Do not edit. */
#define TEXT_FILTER_TERM_KEY 0x5A
static struct text_filter_term_definition const text_filter_term_definitions[] =
{
{ _text_filter_insults, 1, _text_filter_anywhere, "\x3D\x33\x28\x36\x33\x3F\x38\x35\x23" },
{ _text_filter_insults, 1, _text_filter_anywhere, "\x3D\x33\x28\x36\x23\x38\x35\x23" },
{ _text_filter_insults, 1, _text_filter_anywhere, "\x29\x39\x2F\x37\x37\x23" },
{ _text_filter_insults, 1, _text_filter_whole, "\x3E\x35\x36\x2E" },
{ _text_filter_insults, 1, _text_filter_whole, "\x30\x3F\x28\x31" },
{ _text_filter_insults, 1, _text_filter_whole, "\x34\x35\x34\x39\x3F" },
{ _text_filter_insults, 1, _text_filter_whole, "\x29\x39\x2F\x37" },
{ _text_filter_insults, 1, _text_filter_whole, "\x29\x33\x29\x29\x23" },
{ _text_filter_insults, 1, _text_filter_whole, "\x2E\x35\x29\x29\x3F\x28" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x38\x3B\x29\x29\x2E\x2F\x28\x3E" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x38\x3B\x29\x2E\x3B\x28\x3E\x33\x29\x32" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x38\x3B\x29\x2E\x3B\x28\x3E\x29" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x38\x3B\x29\x2E\x3F\x28\x3E" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x38\x3B\x29\x2E\x2F\x28\x3E" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x3E\x2F\x37\x38\x38\x3B\x29\x2E\x3B\x28\x3E" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x2A\x3F\x3E\x35\x38\x3F\x3B\x28" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x2A\x3F\x3E\x35\x2A\x32\x33\x36\x3F" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x2A\x3F\x3E\x35\x2A\x32\x36" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x2A\x3F\x3E\x35\x20" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x28\x3B\x2E\x38\x3B\x29\x2E\x3B\x28\x3E" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x28\x3B\x2E\x38\x3B\x20\x2E\x3B\x3E" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x2E\x2F\x28\x3E\x39\x2F\x2E\x2E\x3F\x28" },
{ _text_filter_insults, 2, _text_filter_anywhere, "\x2E\x2F\x28\x3E\x32\x3F\x3B\x3E" },
{ _text_filter_insults, 2, _text_filter_whole, "\x38\x3B\x29\x2E\x3B\x28\x3E" },
{ _text_filter_insults, 2, _text_filter_whole, "\x38\x3B\x29\x2E\x33\x3E" },
{ _text_filter_insults, 2, _text_filter_whole, "\x3D\x35\x2D\x36" },
{ _text_filter_insults, 2, _text_filter_whole, "\x2A\x3F\x3E\x35" },
{ _text_filter_insults, 2, _text_filter_whole, "\x2A\x3F\x3E\x35\x29" },
{ _text_filter_insults, 2, _text_filter_whole, "\x2E\x2F\x28\x3E" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x38\x3F\x3B\x34\x2B\x2F\x3F\x3F\x34" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x3C\x2F\x3E\x3D\x3F\x2A\x3B\x39\x31\x3F\x28" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x35\x2C\x3F\x34\x3E\x35\x3E\x3D\x3F\x28" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2A\x3B\x39\x31\x3F\x28\x3C\x2F\x3E\x3D\x3F\x32\x3F\x3B\x3E" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2A\x3B\x39\x31\x33\x34\x3D\x3C\x2F\x3E\x3D\x3F" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2A\x3B\x39\x31\x37\x23\x3C\x2F\x3E\x3D\x3F" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2A\x3B\x34\x39\x3B\x31\x3F\x3C\x3B\x39\x3F" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2A\x3F\x2E\x3F\x28\x2A\x2F\x3C\x3C\x3F\x28" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2A\x33\x36\x36\x35\x2D\x38\x33\x2E\x3F\x28" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x28\x3F\x3E\x29\x31\x33\x34" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2E\x3B\x39\x35\x32\x3F\x3B\x3E" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2D\x33\x34\x3E\x35\x2D\x36\x33\x39\x31\x3F\x28" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x2D\x35\x2A\x32\x3F\x3B\x3E" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x20\x33\x2A\x33\x34\x2E\x32\x3F\x2D\x33\x28\x3F" },
{ _text_filter_slurs, 2, _text_filter_anywhere, "\x20\x33\x2A\x2A\x3F\x28\x32\x3F\x3B\x3E" },
{ _text_filter_slurs, 2, _text_filter_start, "\x3B\x38\x35\x3F" },
{ _text_filter_slurs, 2, _text_filter_start, "\x3D\x28\x35\x33\x3E" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x3B\x38\x3F\x3F\x3E" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x3B\x38\x35" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x38\x36\x3B\x39\x31\x23" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x39\x35\x35\x36\x33\x3F" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x3E\x3B\x3D\x35" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x3D\x23\x2A" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x3D\x23\x2A\x29\x23" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x32\x3F\x38\x3F" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x32\x3F\x3F\x38" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x33\x34\x30\x2F\x34" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x31\x31\x31" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x31\x36\x3B\x34" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x37\x35\x34\x3D" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x37\x35\x34\x3D\x35\x36\x35\x33\x3E" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x29\x3B\x37\x38\x35" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x29\x32\x23\x36\x35\x39\x31" },
{ _text_filter_slurs, 2, _text_filter_whole, "\x2D\x35\x2A" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x38\x3F\x3B\x34\x3F\x28\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x3B\x37\x3F\x36\x30\x35\x39\x31\x3F\x23" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x3B\x28\x2A\x3F\x2E\x37\x2F\x34\x39\x32\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x3B\x28\x2A\x3F\x2E\x37\x2F\x34\x39\x32\x33\x34\x3D" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x32\x33\x34\x3B\x2C\x33\x28\x2F\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x32\x33\x34\x3F\x29\x3F\x2C\x33\x28\x2F\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x32\x33\x34\x3D\x39\x32\x35\x34\x3D" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x32\x33\x34\x31\x23" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x39\x35\x2E\x2E\x35\x34\x2A\x33\x39\x31\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3E\x3B\x28\x31\x33\x3F" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3E\x35\x2E\x32\x3F\x3B\x3E" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3E\x2F\x34\x3F\x39\x35\x35\x34" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3C\x3B\x3D\x38\x3B\x3D" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3C\x3B\x3D\x3F\x2E" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3C\x3B\x3D\x3D\x35\x2E" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3C\x3B\x3D\x3D\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3C\x3B\x3D\x2B\x2F\x3F\x3F\x34" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x3C\x3B\x3D\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x30\x33\x3D\x3B\x38\x35\x35" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x30\x33\x3D\x3D\x3B\x38\x35\x35" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x30\x33\x3D\x3D\x3B\x38\x2F" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x30\x33\x3D\x3D\x3B\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x30\x33\x3D\x3D\x3F\x28\x38\x35\x35" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x30\x33\x3D\x3D\x23\x38\x35\x35" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x31\x33\x31\x3F\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x31\x34\x3F\x3F\x3D\x28\x35\x2D\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x31\x2F\x31\x36\x2F\x22\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x31\x23\x31\x3F" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x36\x3B\x3E\x38\x35\x23\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x36\x3B\x3E\x38\x35\x23\x20" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x36\x3B\x3E\x33\x38\x35\x23" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x36\x3B\x3E\x23\x38\x35\x23" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x37\x3F\x39\x32\x3B\x3C\x3B\x3D" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x37\x3F\x3D\x3B\x3C\x3B\x3D" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x3F\x3D\x28\x35\x3F\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x39\x31\x3B\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x3B\x32" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x3D\x3B" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x3D\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x3D\x33\x28" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x3D\x2F\x32" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x36\x3F\x2E" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x34\x33\x3D\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x34\x35\x3D" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x3D\x2F\x32" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x33\x31\x31\x3B\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x34\x2F\x39\x31\x3B\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x2A\x32\x3B\x3D\x3D\x35\x2E\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x2A\x32\x3B\x3D\x29" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x2A\x35\x28\x39\x32\x37\x35\x34\x31\x3F\x23" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x28\x3B\x3D\x32\x3F\x3B\x3E" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x28\x3F\x2E\x3B\x28\x3E\x35" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x29\x32\x3F\x37\x3B\x36\x3F" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x29\x32\x37\x3B\x36\x3F" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x29\x36\x3B\x34\x2E\x3F\x23\x3F" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x29\x36\x3B\x34\x2E\x3C\x28\x3F\x3B\x31" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x29\x36\x3B\x34\x2E\x23" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x29\x2A\x33\x22" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x2E\x3B\x28\x38\x3B\x38\x23" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x2E\x35\x2D\x3F\x36\x32\x3F\x3B\x3E" },
{ _text_filter_slurs, 3, _text_filter_anywhere, "\x2D\x3F\x2E\x38\x3B\x39\x31" },
{ _text_filter_slurs, 3, _text_filter_start, "\x3C\x3B\x3D\x3D\x23" },
{ _text_filter_slurs, 3, _text_filter_start, "\x3C\x36\x3B\x37\x3F\x28\x29" },
{ _text_filter_slurs, 3, _text_filter_start, "\x32\x33\x3D\x3D\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_start, "\x32\x33\x3D\x3D\x3F\x28\x29" },
{ _text_filter_slurs, 3, _text_filter_start, "\x34\x33\x39\x39\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_start, "\x34\x33\x3D\x3F\x28\x29" },
{ _text_filter_slurs, 3, _text_filter_start, "\x34\x33\x3D\x29" },
{ _text_filter_slurs, 3, _text_filter_start, "\x34\x33\x31\x31\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_start, "\x2A\x32\x3B\x3D\x3D\x35\x2E" },
{ _text_filter_slurs, 3, _text_filter_start, "\x2A\x32\x3B\x3D\x35\x2E" },
{ _text_filter_slurs, 3, _text_filter_start, "\x29\x2A\x33\x39\x29" },
{ _text_filter_slurs, 3, _text_filter_start, "\x2D\x33\x3D\x3D\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x38\x3F\x3B\x34\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x39\x32\x33\x34\x31" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x39\x35\x35\x34" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x39\x35\x35\x34\x33\x3F" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x39\x35\x35\x34\x33\x3F\x29" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x39\x35\x35\x34\x29" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3E\x3B\x28\x31\x23" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3E\x23\x31\x3F" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3C\x3B\x3D" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3C\x3B\x3D\x3D\x33\x34\x3D" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3C\x3B\x3D\x3D\x35" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3C\x3B\x3D\x35\x2E" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3C\x36\x3B\x37\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x3D\x35\x35\x31" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x32\x33\x3D\x3D\x3B" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x30\x3B\x2A" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x30\x33\x3D\x3D\x29" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x31\x33\x31\x3F" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x3F\x3D\x3D\x3B" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x3F\x3D\x3D\x3B\x28" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x3F\x3D\x28\x35" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x3F\x3D\x28\x35\x33\x3E" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x33\x39\x31\x3B" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x33\x39\x31\x31\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x33\x3D" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x33\x3D\x3B" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x33\x3D\x3D\x3D\x3F\x28" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x33\x3D\x3D\x28\x3F\x29\x29" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x34\x33\x3D\x28\x3B" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x2A\x3B\x31\x33" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x28\x3F\x2E\x3B\x28\x3E" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x28\x3F\x2E\x3B\x28\x3E\x3F\x3E" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x29\x2A\x33\x39" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x29\x2A\x33\x3D" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x29\x2A\x33\x31" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x2E\x3B\x28\x3E" },
{ _text_filter_slurs, 3, _text_filter_whole, "\x2E\x28\x3B\x34\x34\x23" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x3B\x38\x23\x38\x3B\x2E\x2E\x3F\x28" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x3B\x36\x36\x29\x3B\x39\x31" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x3F\x3B\x2E\x35\x3C\x3C" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x3F\x36\x36\x3F\x34\x3E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x3F\x2D\x38\x29" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x2F\x3D\x3D\x3F\x28\x35\x3C\x3C" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x2F\x2E\x2E\x32\x35\x36\x3F" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x2F\x2E\x2E\x37\x2F\x34\x39\x32\x3F\x28\x29" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x38\x2F\x2E\x2E\x2A\x33\x28\x3B\x2E\x3F" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x39\x3B\x37\x3F\x36\x2E\x35\x3F" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x39\x32\x35\x31\x3F\x2E\x32\x3F\x39\x32\x33\x39\x31\x3F\x34" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3E\x35\x2F\x39\x32\x3F\x38\x3B\x3D" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3E\x35\x2F\x39\x32\x3F\x39\x3B\x34\x35\x3F" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3C\x2F\x34\x38\x3B\x3D\x29" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3D\x33\x28\x36\x23\x38\x33\x2E\x29" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3D\x35\x3E\x3B\x37\x34\x33\x2E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3D\x35\x3E\x3E\x3B\x37\x37\x33\x2E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3D\x35\x3E\x3E\x3B\x37\x34\x33\x2E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x3D\x35\x2E\x35\x32\x3F\x36\x36" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x30\x3B\x39\x31\x35\x3C\x3C" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x37\x3B\x34\x39\x32\x35\x2D\x3E\x3F\x28" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x37\x3B\x34\x29\x3F\x3F\x3E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x37\x2F\x29\x32\x28\x35\x35\x37\x2E\x33\x2A" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2A\x3B\x36\x37\x30\x35\x39\x31\x3F\x23" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2A\x33\x29\x29\x3C\x3B\x39\x3F" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2A\x33\x29\x29\x35\x3C\x3C" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2A\x2F\x36\x36\x2E\x32\x3F\x2A\x2F\x3E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x28\x3F\x3B\x39\x32\x3B\x28\x35\x2F\x34\x3E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x29\x39\x32\x36\x35\x34\x3D" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x29\x32\x36\x35\x34\x3D" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x29\x2E\x2F\x37\x2A\x39\x32\x3F\x2D\x3F\x28" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2E\x3B\x3E\x3D\x3F\x28" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2E\x3B\x31\x33\x34\x3D\x2E\x32\x3F\x2A\x33\x29\x29" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2E\x3B\x36\x36\x23\x2D\x3B\x39\x31\x3F\x28" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2E\x33\x39\x31\x36\x3F\x2E\x32\x3F\x2A\x33\x39\x31\x36\x3F" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2E\x28\x35\x2F\x29\x3F\x28\x29\x34\x3B\x31\x3F" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2F\x2A\x29\x31\x33\x28\x2E" },
{ _text_filter_swearing, 1, _text_filter_anywhere, "\x2D\x32\x3B\x39\x31\x35\x3C\x3C" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x38\x35\x34\x3F\x28" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x38\x35\x35\x38\x29" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x38\x2F\x3D\x3D\x3F\x28" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x38\x2F\x2E\x2E" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x3E\x3B\x37\x34" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x3E\x33\x36\x3E\x35" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x3E\x35\x35\x29\x32" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x3E\x35\x2F\x39\x32\x3F" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x3D\x35\x3E\x3E\x3B\x37" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x3D\x35\x3E\x3E\x3B\x37\x34" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x3D\x35\x3E\x3E\x3B\x37\x34\x3F\x3E" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x32\x3F\x36\x36" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x35\x28\x3D\x3B\x29\x37" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x2A\x33\x29\x29" },
{ _text_filter_swearing, 1, _text_filter_whole, "\x2E\x33\x2E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x34\x3B\x36\x32\x35\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x34\x3B\x36\x32\x35\x28\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x34\x3B\x36\x33\x34\x3D\x2F\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x34\x3B\x36\x2A\x33\x28\x3B\x2E\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x34\x3B\x36\x2A\x36\x2F\x3D" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x34\x33\x36\x33\x34\x3D\x2F\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x28\x29\x3F\x38\x3B\x34\x3E\x33\x2E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x28\x29\x3F\x3D\x35\x38\x38\x36\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x28\x29\x3F\x32\x35\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x28\x29\x3F\x29\x32\x3B\x3D\x3D\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3B\x28\x29\x3F\x2D\x33\x2A\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x3B\x36\x36\x38\x3B\x3D" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x3F\x3F\x35\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x3F\x23\x35\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x3F\x23\x35\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x3B\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x35\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x3F\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x3F\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x3F\x20" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x32\x35\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x33\x34\x3D" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x36\x33\x31\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x34\x3F\x29\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x2E\x33\x2E\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x2E\x39\x32\x23" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x33\x20\x20\x3B\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x36\x35\x2D\x3B\x36\x35\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x36\x35\x2D\x30\x35\x38" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x36\x2F\x37\x2A\x31\x33\x34" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x35\x36\x36\x35\x39\x31" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x35\x36\x36\x35\x22" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x38\x2E\x35\x2E\x32\x3F\x33\x34\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x39\x32\x35\x3B\x3E\x34\x3F\x39\x2E\x3B\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x39\x36\x33\x2E\x36\x33\x39\x31\x3F\x28\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x39\x35\x38\x38\x31\x34\x35\x38\x38\x36\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x39\x28\x3B\x20\x23\x29\x35\x38" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x39\x28\x3F\x3B\x37\x2A\x33\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x39\x2F\x34\x34\x33\x36\x33\x34\x3D\x2F\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3E\x33\x28\x2E\x23\x29\x3B\x34\x39\x32\x3F\x20" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3E\x35\x3D\x3D\x35\x29\x2E\x23\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3E\x35\x3D\x3D\x23\x29\x2E\x23\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3E\x35\x3D\x33\x3F\x29\x2E\x23\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3E\x35\x3D\x29\x2E\x23\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3E\x2F\x37\x2A\x3B\x36\x35\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3F\x28\x3F\x39\x2E\x35\x2A\x36\x3B\x29\x37" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3C\x3F\x39\x31\x3B\x28\x29\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3C\x3F\x36\x36\x3B\x2E\x33\x35\x3B\x3C\x33\x39\x33\x35\x34\x3B\x3E\x35" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3C\x3F\x36\x36\x3B\x2E\x35\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3D\x3B\x29\x32\x29\x2E\x3B\x38\x38\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x3D\x33\x34\x30\x35\x39\x31\x3F\x23" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x32\x3B\x28\x28\x23\x2A\x3B\x36\x37\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x32\x35\x3F\x38\x3B\x3D" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3B\x39\x31\x3B\x28\x29\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3B\x39\x31\x35\x3C\x3C\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3B\x39\x31\x35\x3C\x3C\x20" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3B\x3D\x35\x3C\x3C" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3B\x33\x36\x38\x3B\x33\x2E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3F\x28\x31\x33\x34\x3D\x35\x3C\x3C" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3F\x28\x31\x35\x3C\x3C" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x3F\x28\x31\x29\x35\x3C\x3C" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x30\x35\x3C\x3C\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x33\x3E\x3E\x23\x2E\x35\x2F\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x33\x3E\x2E\x35\x2F\x39\x32\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x33\x2E\x2E\x23\x2A\x2F\x34\x39\x32\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x36\x33\x2E\x35\x28\x33\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x34\x35\x38\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x34\x35\x38\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x34\x35\x38\x3C\x3B\x39\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x31\x34\x35\x38\x3D\x35\x38\x38\x36\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x37\x3F\x3B\x2E\x39\x2F\x28\x2E\x3B\x33\x34\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x34\x35\x38\x38\x23\x32\x3F\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x34\x35\x38\x32\x3F\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x34\x35\x38\x30\x35\x39\x31\x3F\x23" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x34\x35\x38\x30\x35\x39\x31\x33\x3F\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x34\x35\x38\x30\x35\x39\x31\x23" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x34\x35\x38\x30\x35\x31\x3F\x23" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x34\x2F\x2E\x29\x3B\x39\x31" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x3F\x39\x31\x3F\x28\x32\x3F\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x35\x36\x3F\x36\x33\x39\x31\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x35\x36\x3F\x29\x37\x35\x31\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x35\x36\x3F\x29\x2F\x39\x31\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x28\x33\x39\x31\x3C\x3B\x39\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x28\x33\x39\x31\x3D\x35\x38\x38\x36\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x28\x33\x39\x31\x32\x3F\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2A\x2F\x34\x3B\x34\x33" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x28\x33\x37\x30\x35\x38" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x3B\x36\x3B\x3E\x2E\x35\x29\x29\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x31\x3B\x34\x31" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x36\x2F\x2E\x32\x35\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x36\x2F\x2E\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x36\x2F\x2E\x2E\x23" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x35\x34\x34\x3B\x38\x33\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x35\x34\x35\x38\x33\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x35\x34\x35\x3C\x3B\x38\x33\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x35\x2A\x2A\x23\x38\x35\x36\x36\x2F\x39\x31\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x2E\x2F\x2A\x33\x3E\x32\x35\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x2F\x37\x38\x33\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x29\x2F\x37\x35\x3C\x3B\x38\x33\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x32\x28\x35\x3B\x2E\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x32\x28\x35\x3B\x2E\x23\x35\x3D\x2F\x28\x2E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x35\x29\x29\x33\x34\x3D\x29\x3B\x36\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x2D\x3B\x2E\x3C\x3B\x39\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x2D\x3B\x2E\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x2D\x3B\x2E\x2E\x33\x29\x32" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x2D\x3B\x2E\x2D\x3B\x3C\x3C\x36\x3F" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2E\x2D\x3B\x2E\x20\x33\x36\x36\x3B" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2D\x3B\x34\x31\x3F\x28\x29" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2D\x3B\x34\x31\x35\x3C\x3C" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2D\x32\x35\x3B\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2D\x33\x36\x36\x23\x2D\x32\x3B\x39\x31\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_anywhere, "\x2D\x34\x31\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_start, "\x38\x33\x39\x32\x3F\x29" },
{ _text_filter_swearing, 2, _text_filter_start, "\x38\x33\x29\x32\x3F\x29" },
{ _text_filter_swearing, 2, _text_filter_start, "\x39\x32\x35\x3B\x3E" },
{ _text_filter_swearing, 2, _text_filter_start, "\x39\x32\x35\x39\x32\x3B" },
{ _text_filter_swearing, 2, _text_filter_start, "\x29\x35\x3B\x38" },
{ _text_filter_swearing, 2, _text_filter_start, "\x2D\x3B\x34\x31\x3F\x3E" },
{ _text_filter_swearing, 2, _text_filter_start, "\x2D\x3B\x34\x31\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_start, "\x2D\x3B\x34\x31\x33\x3F\x29" },
{ _text_filter_swearing, 2, _text_filter_start, "\x2D\x3B\x34\x31\x33\x34\x3D" },
{ _text_filter_swearing, 2, _text_filter_start, "\x2D\x3B\x34\x31\x29" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x3B\x34\x3B\x36" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x3B\x28\x29\x3F" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x38\x33\x2E\x39\x32" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x38\x33\x2E\x39\x32\x33\x34" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x39\x36\x33\x2E" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x3C\x3F\x36\x36\x3B\x2E\x33\x35" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x32\x35\x3F" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x32\x35\x3F\x29" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x31\x34\x35\x38" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x37\x3B\x29\x2E\x2F\x28\x38\x3B\x2E\x3F" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x37\x33\x36\x3C" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2A\x3F\x39\x31\x3F\x28" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2A\x28\x33\x39\x31" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2A\x28\x33\x39\x31\x29" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x29\x36\x3B\x3D" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x29\x36\x2F\x2E" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2E\x2D\x3B\x2E" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2E\x2D\x3B\x2E\x2E" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2D\x3B\x34\x31" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2D\x32\x35\x28\x3F" },
{ _text_filter_swearing, 2, _text_filter_whole, "\x2D\x32\x35\x28\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x32\x35\x36\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x38\x3B\x34\x3E\x33\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x3F\x3B\x2E\x33\x34\x3D\x34\x35\x38\x30\x35\x31\x3F\x23\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x3C\x3B\x39\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x32\x3B\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x32\x35\x36\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x31\x33\x29\x29\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x36\x33\x39\x31\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x37\x2F\x34\x39\x32" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x2D\x32\x35\x36\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x29\x29\x2D\x33\x2A\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3B\x20\x20\x32\x35\x36\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x38\x35\x33\x35\x36\x3B\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x3B\x2D\x31\x37\x2F\x34\x39\x32\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x3B\x2D\x31\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x38\x35\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x3E\x28\x35\x2A\x36\x3F\x2E\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x3F\x3B\x2E\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x3C\x3B\x39\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x3D\x35\x38\x38\x36\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x32\x3F\x3B\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x32\x3F\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x30\x35\x39\x31\x3F\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x31\x34\x35\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x36\x33\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x37\x35\x34\x3D\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x37\x2F\x34\x39\x32" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x28\x33\x3E\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x29\x31\x33\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x29\x37\x35\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x29\x34\x35\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x29\x2F\x39\x39" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x29\x2F\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x29\x2F\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x31\x2E\x3F\x3B\x29\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x39\x29\x2F\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x31\x37\x2F\x34\x39\x32\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x35\x31\x29\x2F\x39\x31\x3B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x28\x3B\x20\x23\x37\x35\x3C\x35\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x2F\x31\x29\x2F\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x2F\x31\x29\x2F\x31\x31\x3B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x2F\x37\x3E\x2F\x37\x2A\x29\x2E\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x2F\x37\x3D\x35\x35\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x2F\x37\x29\x36\x2F\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x2F\x34\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x39\x23\x38\x3F\x28\x3C\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x3B\x3C\x2F\x2B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x38\x28\x3F\x3B\x2E\x32" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x3C\x3B\x39\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x3D\x35\x38\x38\x36\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x32\x3F\x3B\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x30\x35\x39\x31\x33\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x36\x3F\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x36\x33\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x37\x33\x36\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x37\x2F\x34\x39\x32\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x34\x33\x38\x38\x36\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x29\x31\x33\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x29\x36\x3B\x2A\x2A\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x29\x37\x35\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x29\x2F\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x2C\x3B\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x39\x31\x20" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x31\x32\x3F\x3B\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x2A\x29\x32\x3B\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x33\x2A\x29\x32\x33\x3E\x33\x35\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x36\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x35\x2F\x38\x36\x3F\x3E\x33\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x2F\x37\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x2F\x37\x38\x3B\x28\x28\x3B\x29\x29\x3F\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3E\x2F\x37\x38\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x3B\x39\x31\x2F\x32\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x3B\x2E\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x39\x2F\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x3B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x3F\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x33\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x23\x3F\x3B\x32" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x31\x23\x35\x2F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x2F\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x39\x2C\x31\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x3F\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x31\x38\x34\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x31\x38\x2F\x34\x34\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x31\x3F\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x31\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x31\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x35\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x35\x35\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x35\x35\x31\x2F\x32" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x35\x2E\x32\x3F\x28\x37\x2F\x39\x31\x3F\x28\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x39\x39\x3F\x28\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x39\x39\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x39\x39\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x39\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x39\x22" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x3D\x3D\x3F\x28\x20" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x3D\x3D\x33\x34\x3D\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x3D\x36\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x39\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x32\x3F\x3B\x3E\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x33\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x2F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x31\x2D\x33\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2F\x2E\x32\x3B\x37\x2F\x39\x31\x3B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x2C\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x3C\x22\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x32\x35\x28\x29\x3F\x29\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x3B\x39\x31\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x38\x3B\x3D\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x3F\x3B\x2E\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x3F\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x30\x35\x39\x31\x3F\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x36\x33\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x29\x3B\x39\x31\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x30\x33\x20\x20\x29\x2E\x3B\x33\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x31\x33\x3E\x3E\x33\x3E\x3E\x36\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x31\x33\x29\x29\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x31\x33\x29\x29\x37\x23\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x3B\x28\x31\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x3C\x35\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x3C\x2F\x39\x39\x31\x3F\x28\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x3C\x2F\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3B\x3C\x2F\x39\x39" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3F\x28\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3F\x28\x3F\x3C\x3C\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3F\x28\x3C\x3B\x39\x31\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3F\x28\x3C\x3F\x39\x31\x3B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3F\x28\x3C\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3F\x28\x3C\x2F\x39\x39\x3B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x35\x2E\x32\x3C\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x2F\x3E\x3E\x3F\x28\x3C\x2F\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x2F\x3C\x3E\x33\x2C\x3F" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x2F\x3C\x3E\x33\x2C\x33\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x2F\x3C\x3C\x3E\x33\x2C\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x37\x2F\x3C\x3C\x33\x34\x3E\x33\x2C\x33\x34" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x35\x39\x2E\x35\x2A\x2F\x29\x29\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x32\x2F\x39" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x32\x2F\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x32\x2F\x2B" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x32\x2C\x39\x31\x33\x34\x3D\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x34\x31\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x29\x29\x36\x3F\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x29\x29\x36\x33\x39\x31\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x29\x29\x23\x36\x33\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x29\x29\x23\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x29\x29\x23\x2D\x32\x33\x2A\x2A\x3F\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x20\x20\x33\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2A\x2F\x20\x20\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x29\x3B\x2F\x29\x3B\x3D\x3F\x30\x35\x39\x31\x3F\x23" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x29\x32\x3F\x33\x29\x2E\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x29\x32\x33\x2A\x3E\x33\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x29\x32\x33\x2E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x29\x2E\x2F\x2A\x33\x3E\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x29\x2F\x39\x31\x37\x23\x39\x35\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x29\x2F\x39\x31\x37\x23\x3E" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2E\x3B\x28\x3E\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_anywhere, "\x2D\x33\x29\x3F\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_start, "\x39\x3B\x2D\x31" },
{ _text_filter_swearing, 3, _text_filter_start, "\x39\x2F\x37\x38\x3B\x36\x36" },
{ _text_filter_swearing, 3, _text_filter_start, "\x39\x2F\x37\x20" },
{ _text_filter_swearing, 3, _text_filter_start, "\x39\x2F\x2F\x34\x2E" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3E\x3B\x28\x31\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3C\x3B\x2E\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3C\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3C\x2F\x3D\x3D\x3F\x3E" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3C\x2F\x3D\x3D\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3C\x2F\x31" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3C\x2F\x2B" },
{ _text_filter_swearing, 3, _text_filter_start, "\x3C\x2F\x22" },
{ _text_filter_swearing, 3, _text_filter_start, "\x37\x3C\x3F\x28" },
{ _text_filter_swearing, 3, _text_filter_start, "\x37\x3C\x3F\x28\x29" },
{ _text_filter_swearing, 3, _text_filter_start, "\x37\x3C\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_start, "\x37\x3C\x31" },
{ _text_filter_swearing, 3, _text_filter_start, "\x37\x3C\x29" },
{ _text_filter_swearing, 3, _text_filter_start, "\x37\x35\x3C\x35" },
{ _text_filter_swearing, 3, _text_filter_start, "\x37\x35\x3C\x35\x29" },
{ _text_filter_swearing, 3, _text_filter_start, "\x29\x2E\x3C\x2F" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3B\x29\x29\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3B\x20\x20" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x38\x2F\x36\x36\x29\x32\x33" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x3B\x3B\x3B\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x3B\x39\x3B" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x3B\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x35\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x35\x39\x31\x36\x3F\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x35\x39\x31\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x2F\x37" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x2F\x37\x37\x33\x34\x3D" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x39\x2F\x37\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3E\x33\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3E\x33\x39\x31\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3E\x33\x3E\x3E\x36\x3F" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3E\x33\x31\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x3B\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x3B\x28\x2E" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x3F\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x33\x39\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x35\x35\x31" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x2F\x39" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x2F\x39\x39" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x3C\x2F\x39\x39\x3B" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x30\x3B\x39\x31\x3B\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x30\x3B\x39\x31\x3B\x29\x29\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x30\x33\x20\x20" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x30\x33\x20\x20\x23" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x37\x2F\x3C\x3C" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x2A\x2F\x29\x29\x33\x3F" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x2A\x2F\x29\x29\x33\x3F\x29" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x2A\x2F\x29\x29\x23" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x2A\x2F\x2F\x29\x29\x23" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x29\x32\x3B\x2E" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x29\x32\x3F\x3F\x3F\x3F\x2E" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x29\x32\x3F\x2E" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x29\x32\x33\x3B\x2E" },
{ _text_filter_swearing, 3, _text_filter_whole, "\x2E\x33\x2E\x29" },
};
static char const *const text_filter_allowed_words[] =
{
"assess",
"assesses",
"bereshith",
"brushite",
"chashitsu",
"cushite",
"cushitic",
"elkoshite",
"girgashite",
"kaneshite",
"koreishite",
"mackintoshite",
"marshite",
"matsushita",
"metabrushite",
"mishit",
"mishits",
"mishitting",
"niggard",
"peshito",
"phage",
"phages",
"placuntitis",
"placuntoma",
"scunthorpe",
"shiitake",
"shiite",
"shiites",
"shiitic",
"shitake",
"shittah",
"shittahs",
"shitterton",
"shittim",
"shittims",
"shittimwood",
"snigger",
"unniggard",
"washita",
"wiggeries",
"wiggery",
};
+12
View File
@@ -472,6 +472,18 @@ static const struct config_setting config_settings[] =
{ "update.auto", _config_boolean, "true", "HALO_UPDATE_AUTO", _environment_value, _platform_all,
"Look for a new version when the game starts, and offer to update to it;\n"
"false never looks (the game's \"Do not ask again\" writes false here)." },
{ "streamer.enabled", _config_boolean, "false", "HALO_STREAMER_MODE", _environment_value, _platform_all,
"Hide offensive words in names and text from other players, maps and mods\n"
"as stars, at the levels below (Settings > Network Setup > Streamer Mode)." },
{ "streamer.slurs", _config_string, "\"mild\"", "HALO_STREAMER_SLURS", _environment_value, _platform_all,
"With streamer.enabled, the slurs (race, sexuality, gender, disability)\n"
"hidden: \"off\", \"severe\", \"strong\" (and severe) or \"mild\" (all)." },
{ "streamer.swearing", _config_string, "\"strong\"", "HALO_STREAMER_SWEARING", _environment_value, _platform_all,
"With streamer.enabled, the swearing and sexual words hidden: \"off\",\n"
"\"severe\", \"strong\" (and severe) or \"mild\" (all)." },
{ "streamer.insults", _config_string, "\"off\"", "HALO_STREAMER_INSULTS", _environment_value, _platform_all,
"With streamer.enabled, the insults that are not slurs hidden: \"off\",\n"
"\"severe\", \"strong\" (and severe) or \"mild\" (all)." },
{ "crash_reports.upload", _config_string, "\"ask\"", "HALO_CRASH_REPORTS", _environment_value, _platform_windows,
"Send a report of each crash (a minidump and halo.log) to the developers'\n"
"Sentry project (port/windows/src/win32_crash.c): \"yes\" sends them, \"no\"\n"
+31 -8
View File
@@ -17,6 +17,7 @@ and the debug keyboard that the game's console reads.
#include "port_mods.h"
#include "xiso.h"
#include "touch_input.h"
#include "../game/text_filter.h"
#include <SDL3/SDL.h>
#include <stdio.h>
@@ -1237,6 +1238,9 @@ void platform_show_message(const char *title, const char *message)
pthread_mutex_lock(&platform_message_lock);
snprintf(platform_message_title, sizeof(platform_message_title), "%s", title);
snprintf(platform_message_text, sizeof(platform_message_text), "%s", message);
/* (streamer mode's words hidden here too: a map's or a mod's name) */
text_filter_utf8(platform_message_title);
text_filter_utf8(platform_message_text);
platform_message_pending = TRUE;
pthread_mutex_unlock(&platform_message_lock);
}
@@ -1249,10 +1253,36 @@ void web_js_ask(const char *title, const char *text, const char *labels);
int platform_ask(const char *title, const char *text, const char *const *labels, int count, int default_index)
{
/* (streamer mode's words hidden here too: mods' names) */
static char shown_title[128];
static char shown_text[4096];
static char shown_labels[3][128];
const char *shown[3];
int index;
#ifdef HALO_WEB
char joined[512] = "";
int index;
#else
SDL_MessageBoxButtonData buttons[3];
SDL_MessageBoxData question;
int answer = -1;
#endif
if (count > 3)
count = 3;
snprintf(shown_title, sizeof(shown_title), "%s", title);
snprintf(shown_text, sizeof(shown_text), "%s", text);
text_filter_utf8(shown_title);
text_filter_utf8(shown_text);
for (index = 0; index < count; index++)
{
snprintf(shown_labels[index], sizeof(shown_labels[index]), "%s", labels[index]);
text_filter_utf8(shown_labels[index]);
shown[index] = shown_labels[index];
}
title = shown_title;
text = shown_text;
labels = shown;
#ifdef HALO_WEB
/* (the page's dialog: the game's thread waits for the player there, as
SDL's box would, which a page's worker cannot show) */
for (index = 0; index < count; index++)
@@ -1268,13 +1298,6 @@ int platform_ask(const char *title, const char *text, const char *const *labels,
(void)default_index;
return __atomic_load_n(&web_shared[_web_answer], __ATOMIC_SEQ_CST);
#else
SDL_MessageBoxButtonData buttons[3];
SDL_MessageBoxData question;
int answer = -1;
int index;
if (count > 3)
count = 3;
for (index = 0; index < count; index++)
{
buttons[index].flags = index == default_index ?
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>
+17
View File
@@ -0,0 +1,17 @@
# english-words
A list of 370,105 English words, by dwyl and its contributors, released into
the public domain (the Unlicense, see `LICENSE.md`).
Upstream: https://github.com/dwyl/english-words, commit
`20f5cc9b3f0ccc8ce45d814c532b7c2031bba31c`. `words_alpha.txt.gz` is its
`words_alpha.txt` (SHA-256
3ed0c94610d8bcf7c11bbb49c56aa49c7234d32b66824df91f554169e572da48), unchanged
but compressed (`gzip -9 -n`).
Nothing the game builds uses it. Streamer mode's tools do:
`tools/text_filter_terms.py` works out where each word it hides may be found
(inside a word, at a word's start, or only as a whole word) from which
English words contain it, and `tools/test_text_filter_terms.py` checks that
the words hidden in English are those expected
(`port/assets/text_filter/english.txt`).
Binary file not shown.
+5
View File
@@ -65,6 +65,7 @@ symbols in this file:
#include "tag_groups.h"
#include "text_group.h"
#include "font_group.h"
#include "text_filter.h" /* port: port/linux/game/text_filter.c */
/* ---------- constants */
@@ -247,7 +248,11 @@ void terminal_draw(
}
buffer[cursor_index] = '_';
}
/* port: the line being typed drawn as it is, not hidden by streamer
mode (port/linux/game/text_filter.c) */
text_filter_suspend();
rasterizer_draw_string(&terminal_gets_bounds, NULL, NULL, 0, buffer);
text_filter_resume();
}
if (terminal_render_enable)
+5
View File
@@ -96,6 +96,7 @@ symbols in this file:
#include "text/font_group.h"
#include "text/text_group.h"
#include "text/unicode.h"
#include "text_filter.h" /* port: port/linux/game/text_filter.c */
/* ---------- constants */
@@ -691,6 +692,9 @@ static void virtual_keyboard_render_internal(
rasterizer_draw_unicode_string(&bounds, &bounds, NULL, 0, caption);
}
/* port: the text being typed drawn as it is, not hidden by streamer mode
(port/linux/game/text_filter.c) */
text_filter_suspend();
draw_string_set_draw_mode(virtual_keyboard_globals.keyboard->font_tag.index, NONE, 2, 0, &text_color);
{
rectangle2d bounds;
@@ -716,6 +720,7 @@ static void virtual_keyboard_render_internal(
}
rasterizer_draw_unicode_string(&bounds, &bounds, NULL, 0, virtual_keyboard_globals.text_buffer);
}
text_filter_resume(); /* port */
if (!virtual_keyboard_globals.first_key_replaces_buffer &&
virtual_keyboard_globals.caret_bitmap_index != NONE &&
+9
View File
@@ -116,6 +116,7 @@ symbols in this file:
#include "text/font_group.h"
#include "text/international_strings.h"
#include "text/text_group.h"
#include "text_filter.h" /* port: port/linux/game/text_filter.c and text_filter_match.c */
#include <ctype.h>
@@ -1012,6 +1013,10 @@ void draw_string(
struct parse_string_state state;
point2d cursor;
/* port: streamer mode's words hidden, in what is drawn and what is measured
(port/linux/game/text_filter.c) */
string = text_filter_string(string);
match_assert("c:\\halo\\SOURCE\\text\\draw_string.c", 648, bounds);
match_assert("c:\\halo\\SOURCE\\text\\draw_string.c", 649, string);
@@ -1210,6 +1215,10 @@ void draw_unicode_string(
struct parse_string_state state;
point2d cursor;
/* port: streamer mode's words hidden, in what is drawn and what is measured
(port/linux/game/text_filter.c) */
string = text_filter_unicode(string);
match_assert("c:\\halo\\SOURCE\\text\\draw_string.c", 848, bounds);
match_assert("c:\\halo\\SOURCE\\text\\draw_string.c", 849, string);
+148
View File
@@ -0,0 +1,148 @@
"""Streamer mode's matcher (port/linux/game/text_filter_match.c), taken whole, with a list of made-up words so that the
cases spell nothing offensive: how a word is found however it is dressed (case, accents, look-alikes, leet, repeats,
spaces and punctuation, the game's "|" codes), the rules of where it may be, the allowed words and the levels."""
import subprocess
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import build, mutated, read # noqa: E402
# the made-up list: (group, severity, rule, word)
WORDS = [
("_text_filter_slurs", 3, "_text_filter_anywhere", "zorbul"),
("_text_filter_slurs", 2, "_text_filter_whole", "blik"),
("_text_filter_slurs", 1, "_text_filter_start", "quan"),
("_text_filter_swearing", 3, "_text_filter_anywhere", "frak"),
("_text_filter_swearing", 1, "_text_filter_whole", "darn"),
("_text_filter_swearing", 2, "_text_filter_anywhere", "bebo"),
("_text_filter_swearing", 2, "_text_filter_anywhere", "wug"),
("_text_filter_insults", 2, "_text_filter_whole", "gorp"),
("_text_filter_insults", 1, "_text_filter_anywhere", "trillo"),
("_text_filter_insults", 1, "_text_filter_whole", "zarch"),
]
ALLOWED = ["frakton"]
ALL = ("1", "1", "1")
# (text, levels, what is shown); narrow cases are the game's char strings
CASES = {
"plain": [("you frak", ALL, "you ****"), ("FRAK off", ALL, "**** off"), ("zorbul", ALL, "******")],
"inside-a-name": [("xXzorbulXx", ALL, "xX******Xx"), ("xxfrakxx", ALL, "xx****xx")],
"accents": [("fr\u00e4k", ALL, "****"), ("FR\u00c2K", ALL, "****"), ("z\u00f8rbul", ALL, "******")],
"look-alikes": [("fr\u0430k", ALL, "****"), ("\u0444rak", ALL, "\u0444rak"),
("\uff46\uff52\uff41\uff4b", ALL, "****"),
("\u1d00", ALL, "\u1d00"), ("fr\u1d00k", ALL, "****")],
"leet": [("fr4k", ALL, "****"), ("fr@k", ALL, "****"), ("phrak", ALL, "*****"), ("z0rbu1", ALL, "******"),
("vvug", ALL, "****"), ("|-|", ALL, "|-|")],
"repeats": [("frrraaak", ALL, "********"), ("ffrak", ALL, "*****"), ("zorrbuul", ALL, "********")],
"doubled-in-the-word": [("trilo", ALL, "trilo"), ("trillo", ALL, "******"), ("trilllllo", ALL, "*********")],
"punctuation": [("f.r.a.k", ALL, "*******"), ("f-r-a-k", ALL, "*******"), ("f_r_a_k!", ALL, "*******!")],
"spaces": [("f r a k", ALL, "*******"), ("fr ak", ALL, "*****"), ("f r a k", ALL, "**********"),
("zor bul", ALL, "*******"), ("mazor bulb", ALL, "mazor bulb"), ("f r\ra k", ALL, "*******")],
"too-far-apart": [("f rak", ALL, "f rak")],
"wildcards": [("fr*k", ALL, "****"), ("fr?k", ALL, "****"), ("f**k", ALL, "f**k"), ("*rak", ALL, "*rak"),
("zo?bu?", ALL, "******"), ("****", ALL, "****")],
"digits-alone": [("8380", ALL, "8380"), ("b3b0", ALL, "****")],
"hidden-characters": [("fr\u200bak", ALL, "*****"), ("fr\u00adak", ALL, "*****"), ("fra\u0301k", ALL, "*****")],
"breaks": [("fr\u4e2dak", ALL, "fr\u4e2dak")],
"wide-bar": [("fr|xak", ALL, "******"), ("|nfrak", ALL, "|n****")],
"whole": [("blik", ALL, "****"), ("bliks", ALL, "*****"), ("blikes", ALL, "blikes"), ("zarches", ALL, "*******"),
("bliker", ALL, "bliker"),
("ablik", ALL, "ablik"), ("a blik.", ALL, "a ****."), ("gorp!", ALL, "****!")],
"short-words": [("blik", ALL, "****"), ("bliik", ALL, "bliik"), ("bliiik", ALL, "******"),
("zarch", ALL, "*****")],
"start": [("quanx", ALL, "****x"), ("aquan", ALL, "aquan"), ("quan", ALL, "****")],
"allowed": [("frakton", ALL, "frakton"), ("FR4KTON", ALL, "FR4KTON"), ("frak frakton", ALL, "**** frakton")],
"levels": [("darn", ("1", "2", "1"), "darn"), ("darn", ("1", "1", "1"), "****"), ("frak", ("1", "0", "1"), "frak"),
("frak", ("0", "3", "0"), "****"), ("blik", ("3", "1", "1"), "blik"), ("gorp", ("1", "1", "0"), "gorp")],
}
NARROW_CASES = {
"narrow-style-codes": [("|bfrak", ALL, "|b****"), ("fr|bak", ALL, "******"), ("f|nr|na|nk", ALL, "**********")],
"narrow-bar": [("|<", ALL, "|<")],
}
# a fault in text_filter_match.c, and the case that must catch it
NEGATIVE_CONTROLS = {
"no-punctuation-between": (("if (term_index > 0 && gap < MAXIMUM_GAP &&", "if (0 &&"), "punctuation"),
"no-repeats": (("if (repeated != end &&", "if (0 &&"), "repeats"),
"short-words-stretched": (("attempt->length > 4 || repeated - end >= 2", "1"), "short-words"),
"no-look-alikes": (("*letters = text_filter_fold_letters(code);", "*letters = 0;"), "look-alikes"),
"nothing-allowed": (("if (text_filter_allowed(attempt))", "if (0)"), "allowed"),
"wildcards-unlimited": (("if (path.wildcards && (", "if (0 && ("), "wildcards"),
"digits-alone-enough": (("if (!path.real_letters || path.real_letters * 3 < attempt->length)", "if (0)"),
"digits-alone"),
"spaces-anywhere": (("if (path.spaced && path.longest_piece > 2 && !(start_boundary && end_boundary))", "if (0)"),
"spaces"),
"whole-anywhere": (("\tcase _text_filter_whole:\n\t\tif (!start_boundary)",
"\tcase _text_filter_whole:\n\t\tif (0)"),
"whole"),
"levels-ignored": (("term->severity < levels[term->group]", "0"), "levels"),
}
def encoded(word):
return "".join(f"\\x{ord(character) ^ 0x5A:02X}" for character in word)
def terms_include():
lines = ["#define TEXT_FILTER_TERM_KEY 0x5A", "static struct text_filter_term_definition const "
"text_filter_term_definitions[] = {"]
lines += [f'\t{{ {group}, {severity}, {rule}, "{encoded(word)}" }},' for group, severity, rule, word in WORDS]
lines += ["};", "static char const *const text_filter_allowed_words[] = {"]
lines += [f'\t"{word}",' for word in ALLOWED]
lines += ["};", ""]
return "\n".join(lines)
def generated(real=False, fault=None):
source = read("port/linux/game/text_filter_match.c")
if fault:
source = mutated(source, *fault)
terms = read("port/linux/game/text_filter_terms.inc") if real else terms_include()
return (("text_filter.h", read("port/linux/game/text_filter.h")),
("text_filter_terms.inc", terms),
("text_filter_fold.inc", read("port/linux/game/text_filter_fold.inc")),
("under_test.inc", source))
def censor(executable, texts, levels, narrow=False, extra=()):
command = [str(executable), "--censor", *(["--narrow"] if narrow else []), *extra, *levels]
data = ("\n".join(texts) + "\n").encode("latin-1" if narrow else "utf-8")
result = subprocess.run(command, input=data, capture_output=True, check=True, timeout=60)
return result.stdout.decode("latin-1" if narrow else "utf-8").split("\n")[:len(texts)]
def failures(executable, cases, narrow=False):
found = []
for text, levels, expected in cases:
shown = censor(executable, [text], levels, narrow)[0]
if shown != expected:
found.append(f"{text!r} at {levels}: {shown!r}, not {expected!r}")
return found
@pytest.mark.parametrize("case", sorted(CASES))
def test_case(case):
assert not failures(build("text_filter", generated()), CASES[case])
@pytest.mark.parametrize("case", sorted(NARROW_CASES))
def test_narrow_case(case):
assert not failures(build("text_filter", generated()), NARROW_CASES[case], narrow=True)
def test_players_words():
executable = build("text_filter", generated())
assert censor(executable, ["a plonk here", "zorbul"], ("0", "0", "0"), extra=("--add", "plonk")) == \
["a ***** here", "zorbul"]
assert censor(executable, ["xzorbulite"], ALL, extra=("--allow", "zorbulite")) == ["xzorbulite"]
@pytest.mark.parametrize("control", sorted(NEGATIVE_CONTROLS))
def test_negative_control(control):
fault, case = NEGATIVE_CONTROLS[control]
assert failures(build("text_filter", generated(fault=fault)), CASES[case]), \
f"{control}: the case {case} does not catch it"
+102
View File
@@ -0,0 +1,102 @@
/*
Streamer mode's matcher (port/linux/game/text_filter_match.c), taken whole: each line read (UTF-8; or, narrow, bytes as
the game's char strings) is written back with the code units it hides as '*'. The levels of SLURS, SWEARING and
INSULTS are the arguments (0 off, 1 mild and worse, 2 strong and worse, 3 severe); the player's own words are always
hidden. "--add word" and "--allow word" before them add a player's word and an allowed one.
*/
#include "harness.h"
#include "under_test.inc"
static long utf8_to_units(char const *text, unsigned short *units, long maximum)
{
long count = 0;
unsigned char const *byte = (unsigned char const *)text;
while (*byte && count < maximum)
{
unsigned long code = *byte++;
if (code >= 0xE0 && byte[0] && byte[1])
{
code = ((code & 0x0F) << 12) | ((byte[0] & 0x3F) << 6) | (byte[1] & 0x3F);
byte += 2;
}
else if (code >= 0xC0 && byte[0])
{
code = ((code & 0x1F) << 6) | (byte[0] & 0x3F);
byte += 1;
}
units[count++] = (unsigned short)code;
}
return count;
}
static void put_unit(unsigned short code)
{
if (code < 0x80)
{
putchar(code);
}
else if (code < 0x800)
{
putchar(0xC0 | (code >> 6));
putchar(0x80 | (code & 0x3F));
}
else
{
putchar(0xE0 | (code >> 12));
putchar(0x80 | ((code >> 6) & 0x3F));
putchar(0x80 | (code & 0x3F));
}
}
int main(int argc, char **argv)
{
static char line[8192];
static unsigned short units[TEXT_FILTER_MAXIMUM_LENGTH];
static unsigned char censored[TEXT_FILTER_MAXIMUM_LENGTH];
unsigned char levels[NUMBER_OF_TEXT_FILTER_GROUPS] = { 1, 1, 1, 1 };
int narrow = 0;
int argument = 1;
int level = 0;
if (argc < 2 || strcmp(argv[1], "--censor"))
{
fprintf(stderr, "usage: text_filter --censor [--narrow] [--add word] [--allow word] slurs swearing insults\n");
return 2;
}
text_filter_words_reset();
for (argument = 2; argument < argc; argument++)
{
if (!strcmp(argv[argument], "--narrow"))
narrow = 1;
else if (!strcmp(argv[argument], "--add") && argument + 1 < argc)
CHECK(text_filter_word_add(argv[++argument], _text_filter_own, _text_filter_severe, _text_filter_anywhere), "add");
else if (!strcmp(argv[argument], "--allow") && argument + 1 < argc)
CHECK(text_filter_allow_add(argv[++argument]), "allow");
else if (level < 3)
levels[level++] = (unsigned char)atoi(argv[argument]);
}
while (fgets(line, sizeof(line), stdin))
{
long length;
long index;
line[strcspn(line, "\n")] = 0;
if (narrow)
{
for (length = 0; line[length] && length < TEXT_FILTER_MAXIMUM_LENGTH; length++)
units[length] = (unsigned char)line[length];
}
else
{
length = utf8_to_units(line, units, TEXT_FILTER_MAXIMUM_LENGTH);
}
text_filter_censor(units, length, narrow, levels, censored);
for (index = 0; index < length; index++)
put_unit(censored[index] ? '*' : units[index]);
putchar('\n');
}
return 0;
}
+36 -1
View File
@@ -262,10 +262,20 @@ _audio["categories"] = [
# part in another's game, a screen of its own (a category, as Video Setup's)
SEED_UPLOADS = (64, 128, 256, 512, 1024, 2048)
_network = SCREENS["network_setup"]
# (and the multiplayer HUD's rows, and streamer mode, screens of their own:
# Network Setup has room for nine places)
_hud_settings = ("display.player_names", "display.player_name_scale", "display.scoreboard_team_layout",
"display.scoreboard_background")
_hud_rows = [row for row in _network["rows"] if row[1] in _hud_settings]
_network["rows"] = [row for row in _network["rows"] if row[1] not in _hud_settings]
_network["categories"] = [
("MAP SHARING:", "network_setup/map_sharing",
"Downloading Custom Edition maps you do not have,\nand helping send them to other players.",
"desktop android"),
("MULTIPLAYER HUD:", "network_setup/multiplayer_hud",
"Players' names above their heads, and the\nscoreboard."),
("STREAMER MODE:", "network_setup/streamer_mode",
"Hide offensive words in names and text from\nother players, maps and mods, as stars."),
]
SCREENS["network_setup/map_sharing"] = {
"screen": "map_sharing_settings_screen", "header": _network["header"], "spacing": 30,
@@ -281,13 +291,38 @@ SCREENS["network_setup/map_sharing"] = {
],
}
# Network Setup's multiplayer HUD: its rows, moved from Network Setup
SCREENS["network_setup/multiplayer_hud"] = {
"screen": "multiplayer_hud_settings_screen", "header": _network["header"], "spacing": 30,
"rows": _hud_rows,
}
# Network Setup's streamer mode (port/linux/game/text_filter.c): words hidden
# in what other players, maps and mods show, each group from a severity of
# Surge AI's on (port/assets/text_filter)
STREAMER_LEVELS = [("OFF", "off"), ("SEVERE", "severe"), ("STRONG AND WORSE", "strong"), ("MILD AND WORSE", "mild")]
SCREENS["network_setup/streamer_mode"] = {
"screen": "streamer_mode_settings_screen", "header": _network["header"], "spacing": 30,
"rows": [
("STREAMER MODE:", "streamer.enabled", ON_OFF,
"Hide offensive words in names and text from\nother players, maps and mods, as stars.", None),
("CENSOR SLURS:", "streamer.slurs", STREAMER_LEVELS,
"Slurs about race, sexuality, gender and\ndisability.", None),
("CENSOR SWEARING:", "streamer.swearing", STREAMER_LEVELS,
"Swearing and sexual words (STRONG AND WORSE\nleaves the mildest showing).", None),
("CENSOR INSULTS:", "streamer.insults", STREAMER_LEVELS,
"Name-calling and insults that are not slurs.", None),
],
}
# Controls Setup: the keyboard and mouse's actions, in groups (the order of
# port/linux/game/menu_functions.c's table of them)
CONTROL_GROUPS = ["MOVEMENT", "WEAPONS", "ACTIONS"]
CONTROL_ROWS = 7
# the settings whose rows have the wider spinner (a device's name)
WIDE_SETTINGS = {"audio.output_device", "audio.input_device"}
WIDE_SETTINGS = {"audio.output_device", "audio.input_device", "streamer.slurs", "streamer.swearing",
"streamer.insults"}
# the host's voice chat and vote kicks, on Teamplay Options
# (_teamplay_options_extras): each row's key, its label, its choices' words
+147
View File
@@ -0,0 +1,147 @@
"""Streamer mode's words (port/linux/game/text_filter_terms.inc, tools/text_filter_terms.py): the table agrees with the
review (port/assets/text_filter/review.txt) and the allowed words, hides in the English word list just the words
expected (english.txt), and hides nothing of the game's own text but its two lines of swearing: the menus' (asset-free,
at the widest levels) and the maps' string lists and HUD messages (assets/maps, else skipped; at the default levels).
With HALO_SURGE_CSV naming Surge AI's profanity_en.csv, the table is made again from it and must be the one committed.
The words are ROT13'd here and in the files, as in review.txt; a failure prints them so."""
import codecs
import gzip
import hashlib
import os
import re
import struct
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "tools"))
sys.path.insert(0, str(ROOT / "tools" / "harness" / "tests"))
import text_filter_terms # noqa: E402
from harness import build # noqa: E402
from test_text_filter import censor, generated # noqa: E402
ASSETS = ROOT / "port" / "assets" / "text_filter"
MAPS = ROOT / "assets" / "maps"
ALL = ("1", "1", "1")
# the settings' defaults (port_config.c): slurs mild and worse, swearing strong and worse, insults off
DEFAULTS = ("1", "2", "0")
def rot13(text):
return codecs.encode(text, "rot13")
@pytest.fixture(scope="module")
def matcher():
return build("text_filter", generated(real=True))
def hidden(matcher, texts, levels):
"""the texts something is hidden in"""
texts = [text.replace("\n", " ").replace("\r", " ") for text in texts]
return {text for text, shown in zip(texts, censor(matcher, texts, levels)) if shown != text}
def test_table_agrees_with_the_review():
review = text_filter_terms.read_review(ASSETS / "review.txt")
terms = text_filter_terms.read_terms()
keys = {key for _, _, _, key in terms}
allowed = re.findall(r'^\t"([a-z]+)",$', text_filter_terms.TERMS_INC.read_text(), re.M)
assert allowed == sorted(set(text_filter_terms.read_allowed(ASSETS / "allow.txt")))
dropped = {text_filter_terms.letters(word) for word in review["drop"]} - {None}
assert not keys & dropped, f"dropped words in the table: {sorted(rot13(key) for key in keys & dropped)}"
anywhere = [(group, severity, key) for group, severity, rule, key in terms if rule == "anywhere"]
for word, (group, severity, rule) in review["keep"].items():
key = text_filter_terms.letters(word)
assert key in keys or any(other in key for _, _, other in anywhere), \
f"a kept word is not in the table: {rot13(word)}"
for word in review["term"]:
assert text_filter_terms.letters(word) in keys, f"a review term is not in the table: {rot13(word)}"
recorded = re.search(r"sha256 ([0-9a-f]{64})", text_filter_terms.TERMS_INC.read_text()).group(1)
assert recorded in (ASSETS / "README.md").read_text(), "the README names another version of the list"
@pytest.mark.skipif(not os.environ.get("HALO_SURGE_CSV"), reason="HALO_SURGE_CSV (Surge AI's profanity_en.csv) unset")
def test_table_made_again():
csv_path = os.environ["HALO_SURGE_CSV"]
assert hashlib.sha256(Path(csv_path).read_bytes()).hexdigest() in text_filter_terms.TERMS_INC.read_text()
english = text_filter_terms.English(text_filter_terms.ENGLISH)
terms, unreviewed, _ = text_filter_terms.build(csv_path, text_filter_terms.read_review(ASSETS / "review.txt"),
english)
assert not unreviewed
assert terms == text_filter_terms.read_terms()
def test_english_words_hidden(matcher):
with gzip.open(text_filter_terms.ENGLISH, "rt") as file:
words = file.read().split()
found = hidden(matcher, words, ALL)
expected = {rot13(line) for line in (ASSETS / "english.txt").read_text().split()}
assert found == expected, (f"hidden, not expected: {sorted(rot13(word) for word in found - expected)}; "
f"expected, not hidden: {sorted(rot13(word) for word in expected - found)} "
"(ROT13'd: if right, port/assets/text_filter/english.txt)")
def menu_texts():
texts = []
for path in sorted((ROOT / "port" / "assets" / "menus").rglob("*.xml")):
for value in re.findall(r'\b(?:text|strings)="([^"]*)"', path.read_text(encoding="utf-8")):
texts += [part.replace("\\n", " ") for part in value.split("|")]
return [text for text in texts if text.strip()]
# the game's own text that has swearing in it (ROT13'd: a multiplayer profile name, and a tip of the loading
# screens the PC version's menus have too), hidden as other players' would be
PROFILE_NAME = rot13("Cevfba Ovgpu")
TIP = rot13("Lbh unir bar yvsr gb yvir. Ohg ng yrnfg lbh'er hfvat vg gb xvpx nff npebff gur tnynkl vafgrnq bs whfg "
"fvggvat va n znafvba pelvat jvgu lbhe evpu fbpvnyvgr ybiref.")
def test_menus_hide_nothing(matcher):
texts = menu_texts()
assert len(texts) > 500
found = hidden(matcher, texts, ALL)
assert found == {TIP}, f"the menus' own text hidden: {sorted(rot13(text) for text in found)}"
def map_texts(path):
from test_mods import TAG_BASE, MapTags
tags = MapTags(path)
texts = []
for group, _, definition in tags.instances.values():
if group == b"ustr":
references = [data for _, data in tags.block(definition, 0, 20)]
elif group == b"hmt ":
references = [tags.tags[definition - TAG_BASE:][:20]]
else:
continue
for reference in references:
size, _, _, address, _ = struct.unpack("<iiiII", reference)
if size > 0:
text = tags.tags[address - TAG_BASE:address - TAG_BASE + size].decode("utf-16-le", "replace")
texts += [part for part in text.split("\0") if part.strip()]
return texts
@pytest.mark.skipif(not MAPS.is_dir() or not any(MAPS.glob("*.map")), reason="no maps (assets/maps)")
def test_maps_hide_nothing_by_default(matcher):
texts = []
for path in sorted(MAPS.glob("*.map")):
try:
texts += map_texts(path)
except Exception: # (a map this reader cannot read: a Custom Edition one)
continue
assert len(texts) > 1000
found = {" ".join(text.split()) for text in hidden(matcher, sorted(set(texts)), DEFAULTS)}
assert found == {PROFILE_NAME, TIP}, f"the maps' own text hidden: {sorted(rot13(text) for text in found)}"
def test_check_command():
result = subprocess.run([sys.executable, str(ROOT / "tools" / "text_filter_terms.py"), "--check",
rot13("shpx guvf"), "Scunthorpe"], capture_output=True, text=True, check=True)
assert "-> '**** this'" in result.stdout and "-> 'Scunthorpe'" in result.stdout
+367
View File
@@ -0,0 +1,367 @@
#!/usr/bin/env python3
"""Streamer mode's words (port/linux/game/text_filter_match.c): writes
port/linux/game/text_filter_terms.inc from Surge AI's rated list of
profanities and this project's review of it, and lists or tries them.
python3 tools/text_filter_terms.py --surge .../profanity_en.csv
python3 tools/text_filter_terms.py --list [slurs|swearing|insults]
python3 tools/text_filter_terms.py --check "some text"
python3 tools/text_filter_terms.py --fold
The list (port/assets/text_filter/README.md): Surge AI's profanity_en.csv
(MIT), each row a spelling with up to three base words, three categories
and a severity (Mild, Strong, Severe). A base word's group is its own row's
first category's (failing that, the first category most of its spellings
give): SLURS, SWEARING or INSULTS (GROUPS). Its severity is the worst of the
rows that name it alone (failing that, of all its rows). Its spellings are
the rows that name it alone, kept to their letters; those with digits or
symbols are left to the matcher's leet, and those another term already
finds are left out.
port/assets/text_filter/review.txt is the review of every base word, the
words ROT13'd (nothing in the repository spells them out):
keep <group|-> <severity|-> <rule|-> <word> a base word kept: its group
(- Surge's first category's),
its severity (- Surge's),
its rule (- worked out)
drop <word> a base word or spelling left out
term <group> <severity> <rule|-> <word> a word of the project's, or
a spelling's own settings
A rule (where a term may be found: text_filter.h) worked out is "anywhere"
when no English word (tools/../port/third_party/english-words) has it but
its own forms (s, es, ed, er, ing, y), "start" when none starts with it but
those, and "whole" otherwise. port/assets/text_filter/allow.txt is the words
never hidden (Scunthorpe), in plain text.
A base word of the CSV that the review does not name stops the generator,
which then prints a line to review for each (--template prints them all).
The CSV is not in the repository: the .inc records its checksum, and
tools/test_text_filter_terms.py checks the .inc against the review without
it. --fold writes text_filter_fold.inc, the characters above ASCII that read
as letters, from Python's Unicode names (its version is recorded).
"""
import argparse
import codecs
import csv
import gzip
import hashlib
import re
import subprocess
import sys
import unicodedata
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ASSETS = ROOT / "port/assets/text_filter"
REVIEW = ASSETS / "review.txt"
ALLOW = ASSETS / "allow.txt"
TERMS_INC = ROOT / "port/linux/game/text_filter_terms.inc"
FOLD_INC = ROOT / "port/linux/game/text_filter_fold.inc"
ENGLISH = ROOT / "port/third_party/english-words/words_alpha.txt.gz"
KEY = 0x5A
GROUPS = {
"racial / ethnic slurs": "slurs", "sexual orientation / gender": "slurs", "mental disability": "slurs",
"physical disability": "slurs", "political": "slurs",
"sexual anatomy / sexual acts": "swearing", "bodily fluids / excrement": "swearing",
"religious offense": "swearing",
"other / general insult": "insults", "animal references": "insults", "physical attributes": "insults",
}
GROUP_NAMES = {"slurs": "_text_filter_slurs", "swearing": "_text_filter_swearing", "insults": "_text_filter_insults"}
SEVERITIES = {"Mild": 1, "Strong": 2, "Severe": 3}
SEVERITY_NAMES = {1: "mild", 2: "strong", 3: "severe"}
RULES = {"anywhere": "_text_filter_anywhere", "start": "_text_filter_start", "whole": "_text_filter_whole"}
FORMS = ("", "s", "es", "ed", "er", "ers", "ing", "y", "ies")
def rot13(text):
return codecs.encode(text, "rot13")
def letters(text):
"""a term's key: its letters, lowercase (spaces, hyphens, apostrophes
and stops dropped); None for one with digits or other symbols"""
text = text.lower()
if re.search(r"[^a-z \-'.]", text):
return None
return re.sub(r"[^a-z]", "", text)
def collapse(word):
"""a word with each run of one letter made one ("batter" "bater")"""
return re.sub(r"(.)\1+", r"\1", word)
def read_review(path):
review = {"keep": {}, "drop": set(), "term": {}}
for number, line in enumerate(path.read_text().splitlines(), 1):
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(None, 4 if line.startswith(("keep", "term")) else 1)
kind = parts[0]
if kind == "drop" and len(parts) == 2:
review["drop"].add(rot13(parts[1]))
elif kind in ("keep", "term") and len(parts) == 5:
_, group, severity, rule, word = parts
known_group = group in GROUP_NAMES or (kind == "keep" and group == "-")
known_severity = severity == "-" or severity in SEVERITY_NAMES.values()
if not known_group or not known_severity or (rule != "-" and rule not in RULES):
sys.exit(f"{path}:{number}: a group, severity or rule this does not know")
entry = (None if group == "-" else group, None if severity == "-" else
{name: value for value, name in SEVERITY_NAMES.items()}[severity], None if rule == "-" else rule)
review[kind][rot13(word)] = entry
else:
sys.exit(f"{path}:{number}: not a review line")
return review
def read_allowed(path):
return [line.strip().lower() for line in path.read_text().splitlines()
if line.strip() and not line.startswith("#")]
class English:
"""the English word list, for working out rules"""
def __init__(self, path):
with gzip.open(path, "rt") as file:
self.words = file.read().split()
self.text = "\n" + "\n".join(self.words) + "\n"
self.collapsed = "\n" + "\n".join(collapse(word) for word in self.words) + "\n"
@staticmethod
def _find(text, key, own):
found, at = [], 0
while True:
at = text.find(key, at)
if at < 0:
return found
start = text.rindex("\n", 0, at) + 1
end = text.index("\n", at)
word = text[start:end]
if word not in own:
found.append(word)
at = end
def containing(self, key):
"""the words that the matcher would find the key in (the key, it with
"ph" for "f" and "vv" for "w", or with any letter doubled), but its
own forms: (word, whether it starts with the key)"""
found = []
for variant in {key, key.replace("f", "ph"), key.replace("w", "vv")}:
own = {variant + form for form in FORMS}
found += [(word, word.startswith(variant)) for word in self._find(self.text, variant, own)]
short = collapse(variant)
found += [(word, word.startswith(short)) for word in self._find(self.collapsed, short,
{collapse(word) for word in own})]
return found
def rule(self, key):
others = self.containing(key)
if len(key) >= 4 and not others:
return "anywhere"
if not any(starts for _, starts in others):
return "start"
return "whole"
def build(csv_path, review, english):
"""the terms: (group, severity, rule, key), and the base words the review
has not named"""
rows = list(csv.DictReader(open(csv_path, encoding="utf-8")))
bases = defaultdict(list)
for row in rows:
forms = [row[f"canonical_form_{index}"] for index in (1, 2, 3) if row[f"canonical_form_{index}"]]
for form in forms:
bases[form].append((row, len(forms) == 1))
unreviewed, kept = [], {}
for base, base_rows in sorted(bases.items()):
if base in review["drop"]:
continue
if base not in review["keep"]:
unreviewed.append(base)
continue
own = [row for row, _ in base_rows if row["text"].lower() == base]
category = own[0]["category_1"] if own else \
Counter(row["category_1"] for row, _ in base_rows).most_common(1)[0][0]
alone = [row for row, single in base_rows if single] or [row for row, _ in base_rows]
group, severity, rule = review["keep"][base]
kept[base] = (group or GROUPS[category],
severity or max(SEVERITIES[row["severity_description"]] for row in alone),
rule, [row["text"] for row, single in base_rows if single])
terms = {}
dropped = {letters(word) for word in review["drop"]}
for base, (group, severity, rule, spellings) in kept.items():
key = letters(base)
if key:
terms[key] = (group, severity, rule or english.rule(key))
for base, (group, severity, rule, spellings) in kept.items():
for spelling in spellings:
key = letters(spelling)
if not key or key in terms or key in dropped:
continue
terms[key] = (group, severity, None)
for word, (group, severity, rule) in review["term"].items():
key = letters(word)
if not key:
sys.exit(f"a review term has digits or symbols: {rot13(word)}")
old = terms.get(key)
terms[key] = (group, severity or (old[1] if old else 3), rule or (old[2] if old else None))
for key, (group, severity, rule) in list(terms.items()):
if rule is None:
terms[key] = (group, severity, english.rule(key))
# (a spelling another term finds: left out)
found = {key for key, (group, severity, rule) in terms.items()
if any(other != key and other in key and terms[other][2] == "anywhere" and terms[other][0] == group and
terms[other][1] >= severity for other in terms)}
result = sorted((group, severity, rule, key) for key, (group, severity, rule) in terms.items() if key not in found)
return result, unreviewed, bases
def template_line(base, base_rows, english):
own = [row for row, _ in base_rows if row["text"].lower() == base]
category = own[0]["category_1"] if own else Counter(row["category_1"] for row, _ in base_rows).most_common(1)[0][0]
key = letters(base)
rule = english.rule(key) if key else "-"
return f"keep {GROUPS[category]} - {rule} {rot13(base)}"
def c_string(key):
return "".join(f"\\x{ord(character) ^ KEY:02X}" for character in key)
def write_terms(terms, allowed, csv_path):
digest = hashlib.sha256(Path(csv_path).read_bytes()).hexdigest() if csv_path else None
lines = ["/* port/linux/game/text_filter_terms.inc: streamer mode's words, written by",
"tools/text_filter_terms.py from Surge AI's profanity_en.csv",
f"(sha256 {digest}) and port/assets/text_filter/review.txt and allow.txt.",
"Each term's letters are xor'd with TEXT_FILTER_TERM_KEY. Do not edit. */", "",
f"#define TEXT_FILTER_TERM_KEY 0x{KEY:02X}", "",
"static struct text_filter_term_definition const text_filter_term_definitions[] =", "{"]
for group, severity, rule, key in terms:
lines.append(f'\t{{ {GROUP_NAMES[group]}, {severity}, {RULES[rule]}, "{c_string(key)}" }},')
lines += ["};", "", "static char const *const text_filter_allowed_words[] =", "{"]
lines += [f'\t"{word}",' for word in sorted(set(allowed))]
lines += ["};", ""]
TERMS_INC.write_text("\n".join(lines))
def read_terms():
"""the committed .inc's terms: (group, severity, rule, key)"""
text = TERMS_INC.read_text()
groups = {name: group for group, name in GROUP_NAMES.items()}
rules = {name: rule for rule, name in RULES.items()}
terms = []
for group, severity, rule, encoded in re.findall(r'\{ (\w+), (\d), (\w+), "([^"]*)" \}', text):
key = "".join(chr(int(byte, 16) ^ KEY) for byte in re.findall(r"\\x([0-9A-F]{2})", encoded))
terms.append((groups[group], int(severity), rules[rule], key))
return terms
# ---------- characters above ASCII read as letters
# Greek and Cyrillic letters that look like Latin ones
LOOK_ALIKES = {
"α": "a", "β": "b", "γ": "y", "ε": "e", "η": "n", "ι": "i", "κ": "k", "ν": "v", "ο": "o", "ρ": "p", "τ": "t",
"υ": "u", "χ": "x", "ω": "w", "Α": "a", "Β": "b", "Ε": "e", "Ζ": "z", "Η": "h", "Ι": "i", "Κ": "k", "Μ": "m",
"Ν": "n", "Ο": "o", "Ρ": "p", "Τ": "t", "Υ": "y", "Χ": "x",
"а": "a", "в": "b", "е": "e", "ё": "e", "ѕ": "s", "і": "i", "ї": "i", "ј": "j", "к": "k", "м": "m", "н": "h",
"о": "o", "п": "n", "р": "p", "с": "c", "т": "t", "у": "y", "х": "x", "ь": "b", "ԁ": "d", "ӏ": "l", "г": "r",
"А": "a", "В": "b", "Е": "e", "Ё": "e", "Ѕ": "s", "І": "i", "Ї": "i", "Ј": "j", "К": "k", "М": "m", "Н": "h",
"О": "o", "Р": "p", "С": "c", "Т": "t", "У": "y", "Х": "x", "Ь": "b", "Ԁ": "d", "Ӏ": "l",
"µ": "u", "ß": "sb", "ſ": "sf", "ı": "i", "ȷ": "j", "ł": "l", "Ł": "l", "ø": "o", "Ø": "o", "đ": "d", "Đ": "d",
"ħ": "h", "Ħ": "h", "ŧ": "t", "Ŧ": "t", "æ": "a", "Æ": "a", "œ": "o", "Œ": "o", "þ": "p", "Þ": "p",
}
NAME = re.compile(r"^(?:LATIN|CIRCLED LATIN|PARENTHESIZED LATIN|LATIN SUBSCRIPT|SUPERSCRIPT LATIN) "
r"(?:SMALL|CAPITAL) LETTER (?:SMALL CAPITAL )?([A-Z])(?: WITH .*| BAR| STROKE)?$")
OTHER_NAMES = [re.compile(r"^LATIN LETTER SMALL CAPITAL ([A-Z])$"),
re.compile(r"^MODIFIER LETTER (?:SMALL|CAPITAL) ([A-Z])$")]
def fold_letters(code):
character = chr(code)
found = LOOK_ALIKES.get(character)
if found is None:
base = unicodedata.normalize("NFD", character)[0]
found = LOOK_ALIKES.get(base) if base != character else None
if found is None:
name = unicodedata.name(character, "")
for pattern in [NAME, *OTHER_NAMES]:
match = pattern.match(name)
if match:
found = match.group(1).lower()
break
return found
def write_fold():
lines = ["/* port/linux/game/text_filter_fold.inc: the characters above ASCII that",
"streamer mode reads as letters (a-z bits), written by tools/text_filter_terms.py",
f"--fold from Unicode {unicodedata.unidata_version}'s names and its look-alikes. Do not edit. */", "",
"static struct text_filter_fold const text_filter_folds[] =", "{"]
for code in range(0x80, 0x10000):
if 0xD800 <= code <= 0xDFFF or 0xFF01 <= code <= 0xFF5E:
continue
found = fold_letters(code)
if found:
bits = " | ".join(f"LETTER('{letter}')" for letter in sorted(set(found)))
lines.append(f"\t{{ 0x{code:04X}, {bits} }},")
lines += ["};", ""]
FOLD_INC.write_text("\n".join(lines))
def check(texts):
"""the matcher's censoring of each text (all groups at mild), built as
the harness builds it"""
sys.path.insert(0, str(ROOT / "tools"))
sys.path.insert(0, str(ROOT / "tools" / "harness" / "tests"))
from harness import build # noqa: E402
from test_text_filter import generated # noqa: E402
executable = build("text_filter", generated(real=True))
result = subprocess.run([str(executable), "--censor", "1", "1", "1"], input="\n".join(texts) + "\n",
capture_output=True, text=True, check=True)
return result.stdout.splitlines()
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--surge", help="Surge AI's profanity_en.csv: write text_filter_terms.inc")
parser.add_argument("--template", action="store_true", help="with --surge: a review line for each base word")
parser.add_argument("--list", nargs="?", const="all", help="the terms, decoded (a group, or all)")
parser.add_argument("--check", nargs="+", help="texts to try: what is hidden")
parser.add_argument("--fold", action="store_true", help="write text_filter_fold.inc")
arguments = parser.parse_args()
if arguments.fold:
write_fold()
if arguments.surge:
english = English(ENGLISH)
review = read_review(REVIEW)
terms, unreviewed, bases = build(arguments.surge, review, english)
if arguments.template:
for base in sorted(bases):
print(template_line(base, bases[base], english))
return
if unreviewed:
print("base words the review does not name (review.txt lines to start from):", file=sys.stderr)
for base in unreviewed:
print(template_line(base, bases[base], english), file=sys.stderr)
sys.exit(1)
write_terms(terms, read_allowed(ALLOW), arguments.surge)
print(f"{len(terms)} terms: " + ", ".join(f"{count} {group}" for group, count in
sorted(Counter(term[0] for term in terms).items())))
if arguments.list:
for group, severity, rule, key in read_terms():
if arguments.list in ("all", group):
print(f"{group:9} {SEVERITY_NAMES[severity]:7} {rule:9} {key}")
if arguments.check:
for text, shown in zip(arguments.check, check(arguments.check)):
print(f"{text!r} -> {shown!r}")
if __name__ == "__main__":
main()