diff --git a/README.md b/README.md index 94f3a2a..c37f84f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Cog Function | ccrole | **Release** |
Create custom commands that also assign rolesMay have some bugs, please create an issue if you find any
| | chatter | **Beta** |
Chat-bot trained to talk like your guildMissing some key features, but currently functional. See [Chatter](https://github.com/bobloy/Fox-V3/tree/master/chatter) for install instructions
| | coglint | **Alpha** |
Error check code in python syntax posted to discordWorks, but probably needs more turning to work for cogs
| +| conquest | **Alpha** |
Manage maps for war games and RPGsLots of additional features are planned, currently function with simple map
| | dad | **Beta** |
Tell dad jokesWorks great!
| | exclusiverole | **Alpha** |
Prevent certain roles from getting any other rolesFully functional, but pretty simple
| | fight | **Incomplete** |
Organize bracket tournaments within discordStill in-progress, a massive project
| @@ -20,7 +21,7 @@ Cog Function | infochannel | **Beta** |
Create a channel to display server infoJust released, please report bugs
| | lovecalculator | **Alpha** |
Calculate the love between two users[Snap-Ons] Just updated to V3
| | lseen | **Alpha** |
Track when a member was last onlineAlpha release, please report bugs
| -| nudity | **Incomplete** |
Checks for NSFW images posted in non-NSFW channelsLibrary this is based on has a bug, waiting for author to merge my PR
| +| nudity | **Alpha** |
Checks for NSFW images posted in non-NSFW channelsSwitched libraries, now functional
| | planttycoon | **Alpha** |
Grow your own plants![Snap-Ons] Updated to V3, likely to contain bugs
| | qrinvite | **Alpha** |
Create a QR code invite for the serverAlpha release, please report any bugs
| | reactrestrict | **Alpha** |
Removes reactions by role per channelA bit clunky, but functional
| diff --git a/announcedaily/info.json b/announcedaily/info.json index 99c6b74..cb092b6 100644 --- a/announcedaily/info.json +++ b/announcedaily/info.json @@ -8,7 +8,7 @@ 0 ], "description": "Send daily announcements to all servers at a specified times", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing AnnounceDaily! Get started with `[p]load announcedaily` and `[p]help AnnounceDaily`", "short": "Send daily announcements", "end_user_data_statement": "This cog does not store any End User Data", diff --git a/audiotrivia/audiotrivia.py b/audiotrivia/audiotrivia.py index 586f5b7..9465d9a 100644 --- a/audiotrivia/audiotrivia.py +++ b/audiotrivia/audiotrivia.py @@ -12,6 +12,7 @@ from redbot.core import commands, Config, checks from redbot.core.bot import Red from redbot.core.data_manager import cog_data_path from redbot.core.utils.chat_formatting import box + # from redbot.cogs.audio.utils import userlimit @@ -65,7 +66,9 @@ class AudioTrivia(Trivia): """Set whether or not short audio will be repeated""" settings = self.audioconf.guild(ctx.guild) await settings.repeat.set(true_or_false) - await ctx.send("Done. Repeating short audio is now set to {}.".format(true_or_false)) + await ctx.send( + "Done. Repeating short audio is now set to {}.".format(true_or_false) + ) @commands.group(invoke_without_command=True) @commands.guild_only() @@ -89,19 +92,25 @@ class AudioTrivia(Trivia): categories = [c.lower() for c in categories] session = self._get_trivia_session(ctx.channel) if session is not None: - await ctx.send("There is already an ongoing trivia session in this channel.") + await ctx.send( + "There is already an ongoing trivia session in this channel." + ) return status = await self.audio.config.status() notify = await self.audio.config.guild(ctx.guild).notify() if status: await ctx.send( - "It is recommended to disable audio status with `{}audioset status`".format(ctx.prefix) + "It is recommended to disable audio status with `{}audioset status`".format( + ctx.prefix + ) ) if notify: await ctx.send( - "It is recommended to disable audio notify with `{}audioset notify`".format(ctx.prefix) + "It is recommended to disable audio notify with `{}audioset notify`".format( + ctx.prefix + ) ) if not self.audio._player_check(ctx): @@ -109,7 +118,9 @@ class AudioTrivia(Trivia): if not ctx.author.voice.channel.permissions_for( ctx.me ).connect or self.audio.is_vc_full(ctx.author.voice.channel): - return await ctx.send("I don't have permission to connect to your channel.") + return await ctx.send( + "I don't have permission to connect to your channel." + ) await lavalink.connect(ctx.author.voice.channel) lavaplayer = lavalink.get_player(ctx.guild.id) lavaplayer.store("connect", datetime.datetime.utcnow()) @@ -166,7 +177,10 @@ class AudioTrivia(Trivia): # Delay in audiosettings overwrites delay in settings combined_settings = {**settings, **audiosettings} session = AudioSession.start( - ctx=ctx, question_list=trivia_dict, settings=combined_settings, player=lavaplayer + ctx=ctx, + question_list=trivia_dict, + settings=combined_settings, + player=lavaplayer, ) self.trivia_sessions.append(session) LOG.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id) @@ -200,7 +214,9 @@ class AudioTrivia(Trivia): try: path = next(p for p in self._audio_lists() if p.stem == category) except StopIteration: - raise FileNotFoundError("Could not find the `{}` category.".format(category)) + raise FileNotFoundError( + "Could not find the `{}` category.".format(category) + ) with path.open(encoding="utf-8") as file: try: diff --git a/ccrole/ccrole.py b/ccrole/ccrole.py index bea979f..5dbbf1f 100644 --- a/ccrole/ccrole.py +++ b/ccrole/ccrole.py @@ -33,7 +33,7 @@ class CCRole(commands.Cog): Highly customizable custom commands with role management.""" if not ctx.invoked_subcommand: - await ctx.send_help() + pass @ccrole.command(name="add") @checks.mod_or_permissions(administrator=True) diff --git a/ccrole/info.json b/ccrole/info.json index dc6a10c..d107ecd 100644 --- a/ccrole/info.json +++ b/ccrole/info.json @@ -7,10 +7,10 @@ 0, 0 ], - "description": "[Incomplete] Creates custom commands to adjust roles and send custom messages", + "description": "Creates custom commands to adjust roles and send custom messages", "hidden": false, "install_msg": "Thank you for installing Custom Commands w/ Roles. Get started with `[p]load ccrole` and `[p]help CCRole`", - "short": "[Incomplete] Creates commands that adjust roles", + "short": "Creates commands that adjust roles", "end_user_data_statement": "This cog does not store any End User Data", "tags": [ "fox", diff --git a/chatter/README.md b/chatter/README.md index 933162a..e8c03d6 100644 --- a/chatter/README.md +++ b/chatter/README.md @@ -162,12 +162,53 @@ This command trains Chatter on the specified channel based on the configured settings. This can take a long time to process. +### Train Ubuntu + +``` +[p]chatter trainubuntu +``` +*WARNING:* This will trigger a large download and use a lot of processing power + +This command trains Chatter on the publicly available Ubuntu Dialogue Corpus. (It'll talk like a geek) + + ## Switching Algorithms ``` [p]chatter algorithm X ``` +or +``` +[p]chatter algo X 0.95 +``` Chatter can be configured to use one of three different Similarity algorithms. Changing this can help if the response speed is too slow, but can reduce the accuracy of results. + +The second argument is the maximum similarity threshold, +lowering that will make the bot stop searching sooner. + +Default maximum similarity threshold is 0.90 + + +## Switching Pretrained Models + +``` +[p]chatter model X +``` + +Chatter can be configured to use one of three pretrained statistical models for English. + +I have not noticed any advantage to changing this, +but supposedly it would help by splitting the search term into more useful parts. + +See [here](https://spacy.io/models) for more info on spaCy models. + +Before you're able to use the *large* model (option 3), you must install it through pip. + +*Warning:* This is ~800MB download. + +``` +[p]pipinstall https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-2.3.1/en_core_web_lg-2.3.1.tar.gz#egg=en_core_web_lg +``` diff --git a/chatter/chat.py b/chatter/chat.py index 9aed9b4..424b683 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -1,4 +1,5 @@ import asyncio +import logging import os import pathlib from datetime import datetime, timedelta @@ -8,14 +9,17 @@ import discord from chatterbot import ChatBot from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity from chatterbot.response_selection import get_random_response -from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer +from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer from redbot.core import Config, commands from redbot.core.commands import Cog from redbot.core.data_manager import cog_data_path +from redbot.core.utils.predicates import MessagePredicate from redbot.core.utils import AsyncIter +log = logging.getLogger("red.fox_v3.chat") -class ENG_LG: # TODO: Add option to use this large model + +class ENG_LG: ISO_639_1 = "en_core_web_lg" ISO_639 = "eng" ENGLISH_NAME = "English" @@ -27,6 +31,12 @@ class ENG_MD: ENGLISH_NAME = "English" +class ENG_SM: + ISO_639_1 = "en_core_web_sm" + ISO_639 = "eng" + ENGLISH_NAME = "English" + + class Chatter(Cog): """ This cog trains a chatbot that will talk like members of your Guild @@ -41,7 +51,13 @@ class Chatter(Cog): path: pathlib.Path = cog_data_path(self) self.data_path = path / "database.sqlite3" - self.chatbot = self._create_chatbot(self.data_path, SpacySimilarity, 0.45, ENG_MD) + # TODO: Move training_model and similarity_algo to config + # TODO: Add an option to see current settings + + self.tagger_language = ENG_MD + self.similarity_algo = SpacySimilarity + self.similarity_threshold = 0.90 + self.chatbot = self._create_chatbot() # self.chatbot.set_trainer(ListTrainer) # self.trainer = ListTrainer(self.chatbot) @@ -55,18 +71,18 @@ class Chatter(Cog): """Nothing to delete""" return - def _create_chatbot( - self, data_path, similarity_algorithm, similarity_threshold, tagger_language - ): + def _create_chatbot(self): + return ChatBot( "ChatterBot", storage_adapter="chatterbot.storage.SQLStorageAdapter", - database_uri="sqlite:///" + str(data_path), - statement_comparison_function=similarity_algorithm, + database_uri="sqlite:///" + str(self.data_path), + statement_comparison_function=self.similarity_algo, response_selection_method=get_random_response, logic_adapters=["chatterbot.logic.BestMatch"], - # maximum_similarity_threshold=similarity_threshold, - tagger_language=tagger_language, + maximum_similarity_threshold=self.similarity_threshold, + tagger_language=self.tagger_language, + logger=log, ) async def _get_conversation(self, ctx, in_channel: discord.TextChannel = None): @@ -136,6 +152,11 @@ class Chatter(Cog): return out + def _train_ubuntu(self): + trainer = UbuntuCorpusTrainer(self.chatbot) + trainer.train() + return True + def _train_english(self): trainer = ChatterBotCorpusTrainer(self.chatbot) # try: @@ -192,12 +213,14 @@ class Chatter(Cog): "Failed to clear training database. Please wait a bit and try again" ) - self._create_chatbot(self.data_path, SpacySimilarity, 0.45, ENG_MD) + self._create_chatbot() await ctx.tick() - @chatter.command(name="algorithm") - async def chatter_algorithm(self, ctx: commands.Context, algo_number: int): + @chatter.command(name="algorithm", aliases=["algo"]) + async def chatter_algorithm( + self, ctx: commands.Context, algo_number: int, threshold: float = None + ): """ Switch the active logic algorithm to one of the three. Default after reload is Spacy @@ -206,17 +229,61 @@ class Chatter(Cog): 2: Levenshtein """ - algos = [(SpacySimilarity, 0.45), (JaccardSimilarity, 0.75), (LevenshteinDistance, 0.75)] + algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance] if algo_number < 0 or algo_number > 2: await ctx.send_help() return - self.chatbot = self._create_chatbot( - self.data_path, algos[algo_number][0], algos[algo_number][1], ENG_MD - ) + if threshold is not None: + if threshold >= 1 or threshold <= 0: + await ctx.maybe_send_embed( + "Threshold must be a number between 0 and 1 (exclusive)" + ) + return + else: + self.similarity_algo = threshold - await ctx.tick() + self.similarity_algo = algos[algo_number] + async with ctx.typing(): + self.chatbot = self._create_chatbot() + + await ctx.tick() + + @chatter.command(name="model") + async def chatter_model(self, ctx: commands.Context, model_number: int): + """ + Switch the active model to one of the three. Default after reload is Medium + + 0: Small + 1: Medium + 2: Large (Requires additional setup) + """ + + models = [ENG_SM, ENG_MD, ENG_LG] + + if model_number < 0 or model_number > 2: + await ctx.send_help() + return + + if model_number == 2: + await ctx.maybe_send_embed( + "Additional requirements needed. See guide before continuing.\n" "Continue?" + ) + pred = MessagePredicate.yes_or_no(ctx) + try: + await self.bot.wait_for("message", check=pred, timeout=30) + except TimeoutError: + await ctx.send("Response timed out, please try again later.") + return + if not pred.result: + return + + self.tagger_language = models[model_number] + async with ctx.typing(): + self.chatbot = self._create_chatbot() + + await ctx.maybe_send_embed(f"Model has been switched to {self.tagger_language.ISO_639_1}") @chatter.command(name="minutes") async def minutes(self, ctx: commands.Context, minutes: int): @@ -268,6 +335,27 @@ class Chatter(Cog): else: await ctx.send("Error occurred :(") + @chatter.command(name="trainubuntu") + async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False): + """ + WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data. + """ + + if not confirmation: + await ctx.maybe_send_embed( + "Warning: This command downloads ~500MB then eats your CPU for training\n" + "If you're sure you want to continue, run `[p]chatter trainubuntu True`" + ) + return + + async with ctx.typing(): + future = await self.loop.run_in_executor(None, self._train_ubuntu) + + if future: + await ctx.send("Training successful!") + else: + await ctx.send("Error occurred :(") + @chatter.command(name="trainenglish") async def chatter_train_english(self, ctx: commands.Context): """ diff --git a/conquest/Map Ideas/AxisAllies_MAP_006L.jpg b/conquest/Map Ideas/AxisAllies_MAP_006L.jpg new file mode 100644 index 0000000..9bb2755 Binary files /dev/null and b/conquest/Map Ideas/AxisAllies_MAP_006L.jpg differ diff --git a/conquest/Map Ideas/j0e88vlp28pz.png b/conquest/Map Ideas/j0e88vlp28pz.png new file mode 100644 index 0000000..3c97e53 Binary files /dev/null and b/conquest/Map Ideas/j0e88vlp28pz.png differ diff --git a/conquest/__init__.py b/conquest/__init__.py new file mode 100644 index 0000000..bb8992e --- /dev/null +++ b/conquest/__init__.py @@ -0,0 +1,15 @@ +from redbot.core import data_manager + +from .conquest import Conquest +from .mapmaker import MapMaker + + +async def setup(bot): + cog = Conquest(bot) + data_manager.bundled_data_path(cog) + await cog.load_data() + + bot.add_cog(cog) + + cog2 = MapMaker(bot) + bot.add_cog(cog2) diff --git a/conquest/conquest.py b/conquest/conquest.py new file mode 100644 index 0000000..fb8b280 --- /dev/null +++ b/conquest/conquest.py @@ -0,0 +1,410 @@ +import asyncio +import json +import os +import pathlib +from abc import ABC +from shutil import copyfile +from typing import Optional + +import discord +from PIL import Image, ImageChops, ImageColor, ImageOps +from discord.ext.commands import Greedy +from redbot.core import Config, commands +from redbot.core.bot import Red +from redbot.core.data_manager import bundled_data_path, cog_data_path + + +class Conquest(commands.Cog): + """ + Cog for + """ + + default_zoom_json = {"enabled": False, "x": -1, "y": -1, "zoom": 1.0} + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + self.config = Config.get_conf( + self, identifier=67111110113117101115116, force_registration=True + ) + + default_guild = {} + default_global = {"current_map": None} + self.config.register_guild(**default_guild) + self.config.register_global(**default_global) + + self.data_path: pathlib.Path = cog_data_path(self) + self.asset_path: Optional[pathlib.Path] = None + + self.current_map = None + self.map_data = None + self.ext = None + self.ext_format = None + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete""" + return + + async def load_data(self): + """ + Initial loading of data from bundled_data_path and config + """ + self.asset_path = bundled_data_path(self) / "assets" + self.current_map = await self.config.current_map() + + if self.current_map: + await self.current_map_load() + + async def current_map_load(self): + map_data_path = self.asset_path / self.current_map / "data.json" + with map_data_path.open() as mapdata: + self.map_data: dict = json.load(mapdata) + self.ext = self.map_data["extension"] + self.ext_format = "JPEG" if self.ext.upper() == "JPG" else self.ext.upper() + + @commands.group() + async def conquest(self, ctx: commands.Context): + """ + Base command for conquest cog. Start with `[p]conquest set map` to select a map. + """ + if ctx.invoked_subcommand is None: + if self.current_map is not None: + await self._conquest_current(ctx) + + @conquest.command(name="list") + async def _conquest_list(self, ctx: commands.Context): + """ + List currently available maps + """ + maps_json = self.asset_path / "maps.json" + + with maps_json.open() as maps: + maps_json = json.load(maps) + map_list = "\n".join(map_name for map_name in maps_json["maps"]) + await ctx.maybe_send_embed(f"Current maps:\n{map_list}") + + @conquest.group(name="set") + async def conquest_set(self, ctx: commands.Context): + """Base command for admin actions like selecting a map""" + if ctx.invoked_subcommand is None: + pass + + @conquest_set.command(name="resetzoom") + async def _conquest_set_resetzoom(self, ctx: commands.Context): + """Resets the zoom level of the current map""" + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + zoom_json_path = self.data_path / self.current_map / "settings.json" + if not zoom_json_path.exists(): + await ctx.maybe_send_embed( + f"No zoom data found for {self.current_map}, reset not needed" + ) + return + + with zoom_json_path.open("w+") as zoom_json: + json.dump({"enabled": False}, zoom_json) + + await ctx.tick() + + @conquest_set.command(name="zoom") + async def _conquest_set_zoom(self, ctx: commands.Context, x: int, y: int, zoom: float): + """ + Set the zoom level and position of the current map + + x: positive integer + y: positive integer + zoom: float greater than or equal to 1 + """ + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + if x < 0 or y < 0 or zoom < 1: + await ctx.send_help() + return + + zoom_json_path = self.data_path / self.current_map / "settings.json" + + zoom_data = self.default_zoom_json.copy() + zoom_data["enabled"] = True + zoom_data["x"] = x + zoom_data["y"] = y + zoom_data["zoom"] = zoom + + with zoom_json_path.open("w+") as zoom_json: + json.dump(zoom_data, zoom_json) + + await ctx.tick() + + @conquest_set.command(name="zoomtest") + async def _conquest_set_zoomtest(self, ctx: commands.Context, x: int, y: int, zoom: float): + """ + Test the zoom level and position of the current map + + x: positive integer + y: positive integer + zoom: float greater than or equal to 1 + """ + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + if x < 0 or y < 0 or zoom < 1: + await ctx.send_help() + return + + zoomed_path = await self._create_zoomed_map( + self.data_path / self.current_map / f"current.{self.ext}", x, y, zoom + ) + + await ctx.send(file=discord.File(fp=zoomed_path, filename=f"current_zoomed.{self.ext}",)) + + async def _create_zoomed_map(self, map_path, x, y, zoom, **kwargs): + current_map = Image.open(map_path) + + w, h = current_map.size + zoom2 = zoom * 2 + zoomed_map = current_map.crop((x - w / zoom2, y - h / zoom2, x + w / zoom2, y + h / zoom2)) + # zoomed_map = zoomed_map.resize((w, h), Image.LANCZOS) + zoomed_map.save(self.data_path / self.current_map / f"zoomed.{self.ext}", self.ext_format) + return self.data_path / self.current_map / f"zoomed.{self.ext}" + + @conquest_set.command(name="save") + async def _conquest_set_save(self, ctx: commands.Context, *, save_name): + """Save the current map to be loaded later""" + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + current_map_folder = self.data_path / self.current_map + current_map = current_map_folder / f"current.{self.ext}" + + if not current_map_folder.exists() or not current_map.exists(): + await ctx.maybe_send_embed("Current map doesn't exist! Try setting a new one") + return + + copyfile(current_map, current_map_folder / f"{save_name}.{self.ext}") + await ctx.tick() + + @conquest_set.command(name="load") + async def _conquest_set_load(self, ctx: commands.Context, *, save_name): + """Load a saved map to be the current map""" + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + current_map_folder = self.data_path / self.current_map + current_map = current_map_folder / f"current.{self.ext}" + saved_map = current_map_folder / f"{save_name}.{self.ext}" + + if not current_map_folder.exists() or not saved_map.exists(): + await ctx.maybe_send_embed(f"Saved map not found in the {self.current_map} folder") + return + + copyfile(saved_map, current_map) + await ctx.tick() + + @conquest_set.command(name="map") + async def _conquest_set_map(self, ctx: commands.Context, mapname: str, reset: bool = False): + """ + Select a map from current available maps + + To add more maps, see the guide (WIP) + """ + map_dir = self.asset_path / mapname + if not map_dir.exists() or not map_dir.is_dir(): + await ctx.maybe_send_embed( + f"Map `{mapname}` was not found in the {self.asset_path} directory" + ) + return + + self.current_map = mapname + await self.config.current_map.set(self.current_map) # Save to config too + + await self.current_map_load() + + # map_data_path = self.asset_path / mapname / "data.json" + # with map_data_path.open() as mapdata: + # self.map_data = json.load(mapdata) + # + # self.ext = self.map_data["extension"] + + current_map_folder = self.data_path / self.current_map + current_map = current_map_folder / f"current.{self.ext}" + + if not reset and current_map.exists(): + await ctx.maybe_send_embed( + "This map is already in progress, resuming from last game\n" + "Use `[p]conquest set map [mapname] True` to start a new game" + ) + else: + if not current_map_folder.exists(): + os.makedirs(current_map_folder) + copyfile(self.asset_path / mapname / f"blank.{self.ext}", current_map) + + await ctx.tick() + + @conquest.command(name="current") + async def _conquest_current(self, ctx: commands.Context): + """ + Send the current map. + """ + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + current_img = self.data_path / self.current_map / f"current.{self.ext}" + + await self._send_maybe_zoomed_map(ctx, current_img, f"current_map.{self.ext}") + + async def _send_maybe_zoomed_map(self, ctx, map_path, filename): + zoom_data = {"enabled": False} + + zoom_json_path = self.data_path / self.current_map / "settings.json" + + if zoom_json_path.exists(): + with zoom_json_path.open() as zoom_json: + zoom_data = json.load(zoom_json) + + if zoom_data["enabled"]: + map_path = await self._create_zoomed_map(map_path, **zoom_data) + + await ctx.send(file=discord.File(fp=map_path, filename=filename)) + + @conquest.command("blank") + async def _conquest_blank(self, ctx: commands.Context): + """ + Print the blank version of the current map, for reference. + """ + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + current_blank_img = self.asset_path / self.current_map / f"blank.{self.ext}" + + await self._send_maybe_zoomed_map(ctx, current_blank_img, f"blank_map.{self.ext}") + + @conquest.command("numbered") + async def _conquest_numbered(self, ctx: commands.Context): + """ + Print the numbered version of the current map, for reference. + """ + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + numbers_path = self.asset_path / self.current_map / f"numbers.{self.ext}" + if not numbers_path.exists(): + await ctx.send( + file=discord.File( + fp=self.asset_path / self.current_map / f"numbered.{self.ext}", + filename=f"numbered.{self.ext}", + ) + ) + return + + current_map = Image.open(self.data_path / self.current_map / f"current.{self.ext}") + numbers = Image.open(numbers_path).convert("L") + + inverted_map = ImageOps.invert(current_map) + + loop = asyncio.get_running_loop() + current_numbered_img = await loop.run_in_executor( + None, Image.composite, current_map, inverted_map, numbers + ) + + current_numbered_img.save( + self.data_path / self.current_map / f"current_numbered.{self.ext}", self.ext_format + ) + + await self._send_maybe_zoomed_map( + ctx, + self.data_path / self.current_map / f"current_numbered.{self.ext}", + f"current_numbered.{self.ext}", + ) + + @conquest.command(name="multitake") + async def _conquest_multitake( + self, ctx: commands.Context, start_region: int, end_region: int, color: str + ): + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + try: + color = ImageColor.getrgb(color) + except ValueError: + await ctx.maybe_send_embed(f"Invalid color {color}") + return + + if end_region > self.map_data["region_max"] or start_region < 1: + await ctx.maybe_send_embed( + f"Max region number is {self.map_data['region_max']}, minimum is 1" + ) + return + regions = [r for r in range(start_region, end_region + 1)] + + await self._process_take_regions(color, ctx, regions) + + async def _process_take_regions(self, color, ctx, regions): + current_img_path = self.data_path / self.current_map / f"current.{self.ext}" + im = Image.open(current_img_path) + async with ctx.typing(): + out: Image.Image = await self._composite_regions(im, regions, color) + out.save(current_img_path, self.ext_format) + await self._send_maybe_zoomed_map(ctx, current_img_path, f"map.{self.ext}") + + @conquest.command(name="take") + async def _conquest_take(self, ctx: commands.Context, regions: Greedy[int], *, color: str): + """ + Claim a territory or list of territories for a specified color + + :param regions: List of integer regions + :param color: Color to claim regions + """ + if not regions: + await ctx.send_help() + return + + if self.current_map is None: + await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") + return + + try: + color = ImageColor.getrgb(color) + except ValueError: + await ctx.maybe_send_embed(f"Invalid color {color}") + return + + for region in regions: + if region > self.map_data["region_max"] or region < 1: + await ctx.maybe_send_embed( + f"Max region number is {self.map_data['region_max']}, minimum is 1" + ) + return + + await self._process_take_regions(color, ctx, regions) + + async def _composite_regions(self, im, regions, color) -> Image.Image: + im2 = Image.new("RGB", im.size, color) + + loop = asyncio.get_running_loop() + + combined_mask = None + for region in regions: + mask = Image.open( + self.asset_path / self.current_map / "masks" / f"{region}.{self.ext}" + ).convert("L") + if combined_mask is None: + combined_mask = mask + else: + # combined_mask = ImageChops.logical_or(combined_mask, mask) + combined_mask = await loop.run_in_executor( + None, ImageChops.multiply, combined_mask, mask + ) + + out = await loop.run_in_executor(None, Image.composite, im, im2, combined_mask) + + return out diff --git a/conquest/data/assets/HoI/blank.png b/conquest/data/assets/HoI/blank.png new file mode 100644 index 0000000..7cf4e82 Binary files /dev/null and b/conquest/data/assets/HoI/blank.png differ diff --git a/conquest/data/assets/HoI/data.json b/conquest/data/assets/HoI/data.json new file mode 100644 index 0000000..9baa7fa --- /dev/null +++ b/conquest/data/assets/HoI/data.json @@ -0,0 +1,3 @@ +{ + "region_max": 70 +} \ No newline at end of file diff --git a/conquest/data/assets/HoI/numbered.jpg b/conquest/data/assets/HoI/numbered.jpg new file mode 100644 index 0000000..228e9bb Binary files /dev/null and b/conquest/data/assets/HoI/numbered.jpg differ diff --git a/conquest/data/assets/ck2/blank.png b/conquest/data/assets/ck2/blank.png new file mode 100644 index 0000000..058abe7 Binary files /dev/null and b/conquest/data/assets/ck2/blank.png differ diff --git a/conquest/data/assets/ck2/data.json b/conquest/data/assets/ck2/data.json new file mode 100644 index 0000000..9baa7fa --- /dev/null +++ b/conquest/data/assets/ck2/data.json @@ -0,0 +1,3 @@ +{ + "region_max": 70 +} \ No newline at end of file diff --git a/conquest/data/assets/ck2/numbered.png b/conquest/data/assets/ck2/numbered.png new file mode 100644 index 0000000..91a4c76 Binary files /dev/null and b/conquest/data/assets/ck2/numbered.png differ diff --git a/conquest/data/assets/maps.json b/conquest/data/assets/maps.json new file mode 100644 index 0000000..a7d1c03 --- /dev/null +++ b/conquest/data/assets/maps.json @@ -0,0 +1,7 @@ +{ + "maps": [ + "simple_blank_map", + "test", + "test2" + ] +} \ No newline at end of file diff --git a/conquest/data/assets/simple/blank.jpg b/conquest/data/assets/simple/blank.jpg new file mode 100644 index 0000000..f8f4f07 Binary files /dev/null and b/conquest/data/assets/simple/blank.jpg differ diff --git a/conquest/data/assets/simple/data.json b/conquest/data/assets/simple/data.json new file mode 100644 index 0000000..56a4c69 --- /dev/null +++ b/conquest/data/assets/simple/data.json @@ -0,0 +1,4 @@ +{ + "region_max": 70, + "extension": "jpg" +} \ No newline at end of file diff --git a/conquest/data/assets/simple/masks/1.jpg b/conquest/data/assets/simple/masks/1.jpg new file mode 100644 index 0000000..084384b Binary files /dev/null and b/conquest/data/assets/simple/masks/1.jpg differ diff --git a/conquest/data/assets/simple/masks/10.jpg b/conquest/data/assets/simple/masks/10.jpg new file mode 100644 index 0000000..078f526 Binary files /dev/null and b/conquest/data/assets/simple/masks/10.jpg differ diff --git a/conquest/data/assets/simple/masks/11.jpg b/conquest/data/assets/simple/masks/11.jpg new file mode 100644 index 0000000..7d8092e Binary files /dev/null and b/conquest/data/assets/simple/masks/11.jpg differ diff --git a/conquest/data/assets/simple/masks/12.jpg b/conquest/data/assets/simple/masks/12.jpg new file mode 100644 index 0000000..89ef52a Binary files /dev/null and b/conquest/data/assets/simple/masks/12.jpg differ diff --git a/conquest/data/assets/simple/masks/13.jpg b/conquest/data/assets/simple/masks/13.jpg new file mode 100644 index 0000000..d5bcf44 Binary files /dev/null and b/conquest/data/assets/simple/masks/13.jpg differ diff --git a/conquest/data/assets/simple/masks/14.jpg b/conquest/data/assets/simple/masks/14.jpg new file mode 100644 index 0000000..86a7c01 Binary files /dev/null and b/conquest/data/assets/simple/masks/14.jpg differ diff --git a/conquest/data/assets/simple/masks/15.jpg b/conquest/data/assets/simple/masks/15.jpg new file mode 100644 index 0000000..b18a720 Binary files /dev/null and b/conquest/data/assets/simple/masks/15.jpg differ diff --git a/conquest/data/assets/simple/masks/16.jpg b/conquest/data/assets/simple/masks/16.jpg new file mode 100644 index 0000000..1de9ab3 Binary files /dev/null and b/conquest/data/assets/simple/masks/16.jpg differ diff --git a/conquest/data/assets/simple/masks/17.jpg b/conquest/data/assets/simple/masks/17.jpg new file mode 100644 index 0000000..6f428bd Binary files /dev/null and b/conquest/data/assets/simple/masks/17.jpg differ diff --git a/conquest/data/assets/simple/masks/18.jpg b/conquest/data/assets/simple/masks/18.jpg new file mode 100644 index 0000000..c3f13cc Binary files /dev/null and b/conquest/data/assets/simple/masks/18.jpg differ diff --git a/conquest/data/assets/simple/masks/19.jpg b/conquest/data/assets/simple/masks/19.jpg new file mode 100644 index 0000000..bf9ec38 Binary files /dev/null and b/conquest/data/assets/simple/masks/19.jpg differ diff --git a/conquest/data/assets/simple/masks/2.jpg b/conquest/data/assets/simple/masks/2.jpg new file mode 100644 index 0000000..6edd658 Binary files /dev/null and b/conquest/data/assets/simple/masks/2.jpg differ diff --git a/conquest/data/assets/simple/masks/20.jpg b/conquest/data/assets/simple/masks/20.jpg new file mode 100644 index 0000000..39fac15 Binary files /dev/null and b/conquest/data/assets/simple/masks/20.jpg differ diff --git a/conquest/data/assets/simple/masks/21.jpg b/conquest/data/assets/simple/masks/21.jpg new file mode 100644 index 0000000..a6f2d88 Binary files /dev/null and b/conquest/data/assets/simple/masks/21.jpg differ diff --git a/conquest/data/assets/simple/masks/22.jpg b/conquest/data/assets/simple/masks/22.jpg new file mode 100644 index 0000000..a4c8057 Binary files /dev/null and b/conquest/data/assets/simple/masks/22.jpg differ diff --git a/conquest/data/assets/simple/masks/23.jpg b/conquest/data/assets/simple/masks/23.jpg new file mode 100644 index 0000000..3229d3e Binary files /dev/null and b/conquest/data/assets/simple/masks/23.jpg differ diff --git a/conquest/data/assets/simple/masks/24.jpg b/conquest/data/assets/simple/masks/24.jpg new file mode 100644 index 0000000..413bf4a Binary files /dev/null and b/conquest/data/assets/simple/masks/24.jpg differ diff --git a/conquest/data/assets/simple/masks/25.jpg b/conquest/data/assets/simple/masks/25.jpg new file mode 100644 index 0000000..c040946 Binary files /dev/null and b/conquest/data/assets/simple/masks/25.jpg differ diff --git a/conquest/data/assets/simple/masks/26.jpg b/conquest/data/assets/simple/masks/26.jpg new file mode 100644 index 0000000..232b004 Binary files /dev/null and b/conquest/data/assets/simple/masks/26.jpg differ diff --git a/conquest/data/assets/simple/masks/27.jpg b/conquest/data/assets/simple/masks/27.jpg new file mode 100644 index 0000000..c8229a3 Binary files /dev/null and b/conquest/data/assets/simple/masks/27.jpg differ diff --git a/conquest/data/assets/simple/masks/28.jpg b/conquest/data/assets/simple/masks/28.jpg new file mode 100644 index 0000000..d5484a1 Binary files /dev/null and b/conquest/data/assets/simple/masks/28.jpg differ diff --git a/conquest/data/assets/simple/masks/29.jpg b/conquest/data/assets/simple/masks/29.jpg new file mode 100644 index 0000000..0f138f6 Binary files /dev/null and b/conquest/data/assets/simple/masks/29.jpg differ diff --git a/conquest/data/assets/simple/masks/3.jpg b/conquest/data/assets/simple/masks/3.jpg new file mode 100644 index 0000000..112d1b0 Binary files /dev/null and b/conquest/data/assets/simple/masks/3.jpg differ diff --git a/conquest/data/assets/simple/masks/30.jpg b/conquest/data/assets/simple/masks/30.jpg new file mode 100644 index 0000000..b04de09 Binary files /dev/null and b/conquest/data/assets/simple/masks/30.jpg differ diff --git a/conquest/data/assets/simple/masks/31.jpg b/conquest/data/assets/simple/masks/31.jpg new file mode 100644 index 0000000..90812e7 Binary files /dev/null and b/conquest/data/assets/simple/masks/31.jpg differ diff --git a/conquest/data/assets/simple/masks/32.jpg b/conquest/data/assets/simple/masks/32.jpg new file mode 100644 index 0000000..fd12574 Binary files /dev/null and b/conquest/data/assets/simple/masks/32.jpg differ diff --git a/conquest/data/assets/simple/masks/33.jpg b/conquest/data/assets/simple/masks/33.jpg new file mode 100644 index 0000000..87704c5 Binary files /dev/null and b/conquest/data/assets/simple/masks/33.jpg differ diff --git a/conquest/data/assets/simple/masks/34.jpg b/conquest/data/assets/simple/masks/34.jpg new file mode 100644 index 0000000..1c610df Binary files /dev/null and b/conquest/data/assets/simple/masks/34.jpg differ diff --git a/conquest/data/assets/simple/masks/35.jpg b/conquest/data/assets/simple/masks/35.jpg new file mode 100644 index 0000000..e86c726 Binary files /dev/null and b/conquest/data/assets/simple/masks/35.jpg differ diff --git a/conquest/data/assets/simple/masks/36.jpg b/conquest/data/assets/simple/masks/36.jpg new file mode 100644 index 0000000..7f4ad3d Binary files /dev/null and b/conquest/data/assets/simple/masks/36.jpg differ diff --git a/conquest/data/assets/simple/masks/37.jpg b/conquest/data/assets/simple/masks/37.jpg new file mode 100644 index 0000000..c8b6b87 Binary files /dev/null and b/conquest/data/assets/simple/masks/37.jpg differ diff --git a/conquest/data/assets/simple/masks/38.jpg b/conquest/data/assets/simple/masks/38.jpg new file mode 100644 index 0000000..6fcd5a6 Binary files /dev/null and b/conquest/data/assets/simple/masks/38.jpg differ diff --git a/conquest/data/assets/simple/masks/39.jpg b/conquest/data/assets/simple/masks/39.jpg new file mode 100644 index 0000000..455f0e5 Binary files /dev/null and b/conquest/data/assets/simple/masks/39.jpg differ diff --git a/conquest/data/assets/simple/masks/4.jpg b/conquest/data/assets/simple/masks/4.jpg new file mode 100644 index 0000000..b77cc24 Binary files /dev/null and b/conquest/data/assets/simple/masks/4.jpg differ diff --git a/conquest/data/assets/simple/masks/40.jpg b/conquest/data/assets/simple/masks/40.jpg new file mode 100644 index 0000000..5692661 Binary files /dev/null and b/conquest/data/assets/simple/masks/40.jpg differ diff --git a/conquest/data/assets/simple/masks/41.jpg b/conquest/data/assets/simple/masks/41.jpg new file mode 100644 index 0000000..f0e3972 Binary files /dev/null and b/conquest/data/assets/simple/masks/41.jpg differ diff --git a/conquest/data/assets/simple/masks/42.jpg b/conquest/data/assets/simple/masks/42.jpg new file mode 100644 index 0000000..b41a652 Binary files /dev/null and b/conquest/data/assets/simple/masks/42.jpg differ diff --git a/conquest/data/assets/simple/masks/43.jpg b/conquest/data/assets/simple/masks/43.jpg new file mode 100644 index 0000000..463c17f Binary files /dev/null and b/conquest/data/assets/simple/masks/43.jpg differ diff --git a/conquest/data/assets/simple/masks/44.jpg b/conquest/data/assets/simple/masks/44.jpg new file mode 100644 index 0000000..b2da82b Binary files /dev/null and b/conquest/data/assets/simple/masks/44.jpg differ diff --git a/conquest/data/assets/simple/masks/45.jpg b/conquest/data/assets/simple/masks/45.jpg new file mode 100644 index 0000000..fdeca1c Binary files /dev/null and b/conquest/data/assets/simple/masks/45.jpg differ diff --git a/conquest/data/assets/simple/masks/46.jpg b/conquest/data/assets/simple/masks/46.jpg new file mode 100644 index 0000000..6da7ec6 Binary files /dev/null and b/conquest/data/assets/simple/masks/46.jpg differ diff --git a/conquest/data/assets/simple/masks/47.jpg b/conquest/data/assets/simple/masks/47.jpg new file mode 100644 index 0000000..4e7a2a4 Binary files /dev/null and b/conquest/data/assets/simple/masks/47.jpg differ diff --git a/conquest/data/assets/simple/masks/48.jpg b/conquest/data/assets/simple/masks/48.jpg new file mode 100644 index 0000000..87223f7 Binary files /dev/null and b/conquest/data/assets/simple/masks/48.jpg differ diff --git a/conquest/data/assets/simple/masks/49.jpg b/conquest/data/assets/simple/masks/49.jpg new file mode 100644 index 0000000..d749cae Binary files /dev/null and b/conquest/data/assets/simple/masks/49.jpg differ diff --git a/conquest/data/assets/simple/masks/5.jpg b/conquest/data/assets/simple/masks/5.jpg new file mode 100644 index 0000000..c392916 Binary files /dev/null and b/conquest/data/assets/simple/masks/5.jpg differ diff --git a/conquest/data/assets/simple/masks/50.jpg b/conquest/data/assets/simple/masks/50.jpg new file mode 100644 index 0000000..514a67f Binary files /dev/null and b/conquest/data/assets/simple/masks/50.jpg differ diff --git a/conquest/data/assets/simple/masks/51.jpg b/conquest/data/assets/simple/masks/51.jpg new file mode 100644 index 0000000..c928321 Binary files /dev/null and b/conquest/data/assets/simple/masks/51.jpg differ diff --git a/conquest/data/assets/simple/masks/52.jpg b/conquest/data/assets/simple/masks/52.jpg new file mode 100644 index 0000000..9321238 Binary files /dev/null and b/conquest/data/assets/simple/masks/52.jpg differ diff --git a/conquest/data/assets/simple/masks/53.jpg b/conquest/data/assets/simple/masks/53.jpg new file mode 100644 index 0000000..4b36e75 Binary files /dev/null and b/conquest/data/assets/simple/masks/53.jpg differ diff --git a/conquest/data/assets/simple/masks/54.jpg b/conquest/data/assets/simple/masks/54.jpg new file mode 100644 index 0000000..b592522 Binary files /dev/null and b/conquest/data/assets/simple/masks/54.jpg differ diff --git a/conquest/data/assets/simple/masks/55.jpg b/conquest/data/assets/simple/masks/55.jpg new file mode 100644 index 0000000..5bed3a8 Binary files /dev/null and b/conquest/data/assets/simple/masks/55.jpg differ diff --git a/conquest/data/assets/simple/masks/56.jpg b/conquest/data/assets/simple/masks/56.jpg new file mode 100644 index 0000000..627473c Binary files /dev/null and b/conquest/data/assets/simple/masks/56.jpg differ diff --git a/conquest/data/assets/simple/masks/57.jpg b/conquest/data/assets/simple/masks/57.jpg new file mode 100644 index 0000000..6e0e1e4 Binary files /dev/null and b/conquest/data/assets/simple/masks/57.jpg differ diff --git a/conquest/data/assets/simple/masks/58.jpg b/conquest/data/assets/simple/masks/58.jpg new file mode 100644 index 0000000..47e98fe Binary files /dev/null and b/conquest/data/assets/simple/masks/58.jpg differ diff --git a/conquest/data/assets/simple/masks/59.jpg b/conquest/data/assets/simple/masks/59.jpg new file mode 100644 index 0000000..f23e469 Binary files /dev/null and b/conquest/data/assets/simple/masks/59.jpg differ diff --git a/conquest/data/assets/simple/masks/6.jpg b/conquest/data/assets/simple/masks/6.jpg new file mode 100644 index 0000000..95eb45f Binary files /dev/null and b/conquest/data/assets/simple/masks/6.jpg differ diff --git a/conquest/data/assets/simple/masks/60.jpg b/conquest/data/assets/simple/masks/60.jpg new file mode 100644 index 0000000..c1b149e Binary files /dev/null and b/conquest/data/assets/simple/masks/60.jpg differ diff --git a/conquest/data/assets/simple/masks/61.jpg b/conquest/data/assets/simple/masks/61.jpg new file mode 100644 index 0000000..e343d5d Binary files /dev/null and b/conquest/data/assets/simple/masks/61.jpg differ diff --git a/conquest/data/assets/simple/masks/62.jpg b/conquest/data/assets/simple/masks/62.jpg new file mode 100644 index 0000000..413a5ad Binary files /dev/null and b/conquest/data/assets/simple/masks/62.jpg differ diff --git a/conquest/data/assets/simple/masks/63.jpg b/conquest/data/assets/simple/masks/63.jpg new file mode 100644 index 0000000..f03435f Binary files /dev/null and b/conquest/data/assets/simple/masks/63.jpg differ diff --git a/conquest/data/assets/simple/masks/64.jpg b/conquest/data/assets/simple/masks/64.jpg new file mode 100644 index 0000000..59e80fe Binary files /dev/null and b/conquest/data/assets/simple/masks/64.jpg differ diff --git a/conquest/data/assets/simple/masks/65.jpg b/conquest/data/assets/simple/masks/65.jpg new file mode 100644 index 0000000..fd2cb0d Binary files /dev/null and b/conquest/data/assets/simple/masks/65.jpg differ diff --git a/conquest/data/assets/simple/masks/66.jpg b/conquest/data/assets/simple/masks/66.jpg new file mode 100644 index 0000000..2ac9dda Binary files /dev/null and b/conquest/data/assets/simple/masks/66.jpg differ diff --git a/conquest/data/assets/simple/masks/67.jpg b/conquest/data/assets/simple/masks/67.jpg new file mode 100644 index 0000000..e2ebdc9 Binary files /dev/null and b/conquest/data/assets/simple/masks/67.jpg differ diff --git a/conquest/data/assets/simple/masks/68.jpg b/conquest/data/assets/simple/masks/68.jpg new file mode 100644 index 0000000..535e506 Binary files /dev/null and b/conquest/data/assets/simple/masks/68.jpg differ diff --git a/conquest/data/assets/simple/masks/69.jpg b/conquest/data/assets/simple/masks/69.jpg new file mode 100644 index 0000000..6b1d010 Binary files /dev/null and b/conquest/data/assets/simple/masks/69.jpg differ diff --git a/conquest/data/assets/simple/masks/7.jpg b/conquest/data/assets/simple/masks/7.jpg new file mode 100644 index 0000000..a0202b0 Binary files /dev/null and b/conquest/data/assets/simple/masks/7.jpg differ diff --git a/conquest/data/assets/simple/masks/70.jpg b/conquest/data/assets/simple/masks/70.jpg new file mode 100644 index 0000000..a1716af Binary files /dev/null and b/conquest/data/assets/simple/masks/70.jpg differ diff --git a/conquest/data/assets/simple/masks/8.jpg b/conquest/data/assets/simple/masks/8.jpg new file mode 100644 index 0000000..647c649 Binary files /dev/null and b/conquest/data/assets/simple/masks/8.jpg differ diff --git a/conquest/data/assets/simple/masks/9.jpg b/conquest/data/assets/simple/masks/9.jpg new file mode 100644 index 0000000..3bafeda Binary files /dev/null and b/conquest/data/assets/simple/masks/9.jpg differ diff --git a/conquest/data/assets/simple/numbered.jpg b/conquest/data/assets/simple/numbered.jpg new file mode 100644 index 0000000..228e9bb Binary files /dev/null and b/conquest/data/assets/simple/numbered.jpg differ diff --git a/conquest/data/assets/simple/numbers.jpg b/conquest/data/assets/simple/numbers.jpg new file mode 100644 index 0000000..de87fd7 Binary files /dev/null and b/conquest/data/assets/simple/numbers.jpg differ diff --git a/conquest/data/assets/simple/numbers.png b/conquest/data/assets/simple/numbers.png new file mode 100644 index 0000000..2f72371 Binary files /dev/null and b/conquest/data/assets/simple/numbers.png differ diff --git a/conquest/info.json b/conquest/info.json new file mode 100644 index 0000000..2687209 --- /dev/null +++ b/conquest/info.json @@ -0,0 +1,28 @@ +{ + "author": [ + "Bobloy" + ], + "bot_version": [ + 3, + 4, + 0 + ], + "description": "Handle war games by filling in specified territories with colors", + "hidden": false, + "install_msg": "Thank you for installing Conquest. Get started with `[p]load conquest`, then `[p]help Conquest`", + "short": "War Game Map", + "requirements": [ + "Pillow" + ], + "tags": [ + "bobloy", + "games", + "game", + "war", + "map", + "axisandallies", + "heartsofiron", + "conquest", + "rpg" + ] +} diff --git a/conquest/mapmaker.py b/conquest/mapmaker.py new file mode 100644 index 0000000..0cde96a --- /dev/null +++ b/conquest/mapmaker.py @@ -0,0 +1,50 @@ +import discord +from redbot.core import Config, commands +from redbot.core.bot import Red + + +class MapMaker(commands.Cog): + """ + Create Maps to be used with Conquest + """ + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + + self.config = Config.get_conf( + self, identifier=77971127797107101114, force_registration=True + ) + + default_guild = {} + default_global = {} + self.config.register_guild(**default_guild) + self.config.register_global(**default_global) + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete""" + return + + @commands.group() + async def mapmaker(self, ctx: commands.context): + """ + Base command for managing current maps or creating new ones + """ + if ctx.invoked_subcommand is None: + pass + + @mapmaker.command(name="upload") + async def _mapmaker_upload(self, ctx: commands.Context, map_path=""): + """Load a map image to be modified. Upload one with this command or provide a path""" + message: discord.Message = ctx.message + if not message.attachments and not map_path: + await ctx.maybe_send_embed( + "Either upload an image with this command or provide a path to the image" + ) + return + await ctx.maybe_send_embed("WIP") + + @mapmaker.command(name="load") + async def _mapmaker_load(self, ctx: commands.Context, map_name=""): + """Load an existing map to be modified.""" + await ctx.maybe_send_embed("WIP") diff --git a/conquest/regioner.py b/conquest/regioner.py new file mode 100644 index 0000000..dc77373 --- /dev/null +++ b/conquest/regioner.py @@ -0,0 +1,132 @@ +import os +import pathlib +from PIL import Image, ImageColor, ImageFont, ImageOps, ImageDraw +from PIL.ImageDraw import _color_diff + + +def get_center(points): + """ + Taken from https://stackoverflow.com/questions/4355894/how-to-get-center-of-set-of-points-using-python + """ + x = [p[0] for p in points] + y = [p[1] for p in points] + return sum(x) / len(points), sum(y) / len(points) + + +def floodfill(image, xy, value, border=None, thresh=0) -> set: + """ + Taken and modified from PIL.ImageDraw.floodfill + + (experimental) Fills a bounded region with a given color. + + :param image: Target image. + :param xy: Seed position (a 2-item coordinate tuple). See + :ref:`coordinate-system`. + :param value: Fill color. + :param border: Optional border value. If given, the region consists of + pixels with a color different from the border color. If not given, + the region consists of pixels having the same color as the seed + pixel. + :param thresh: Optional threshold value which specifies a maximum + tolerable difference of a pixel value from the 'background' in + order for it to be replaced. Useful for filling regions of + non-homogeneous, but similar, colors. + """ + # based on an implementation by Eric S. Raymond + # amended by yo1995 @20180806 + pixel = image.load() + x, y = xy + try: + background = pixel[x, y] + if _color_diff(value, background) <= thresh: + return set() # seed point already has fill color + pixel[x, y] = value + except (ValueError, IndexError): + return set() # seed point outside image + edge = {(x, y)} + # use a set to keep record of current and previous edge pixels + # to reduce memory consumption + filled_pixels = set() + full_edge = set() + while edge: + filled_pixels.update(edge) + new_edge = set() + for (x, y) in edge: # 4 adjacent method + for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + # If already processed, or if a coordinate is negative, skip + if (s, t) in full_edge or s < 0 or t < 0: + continue + try: + p = pixel[s, t] + except (ValueError, IndexError): + pass + else: + full_edge.add((s, t)) + if border is None: + fill = _color_diff(p, background) <= thresh + else: + fill = p != value and p != border + if fill: + pixel[s, t] = value + new_edge.add((s, t)) + full_edge = edge # discard pixels processed + edge = new_edge + return filled_pixels + + +class Regioner: + def __init__( + self, filepath: pathlib.Path, filename: str, wall_color="black", region_color="white" + ): + self.filepath = filepath + self.filename = filename + self.wall_color = ImageColor.getcolor(wall_color, "L") + self.region_color = ImageColor.getcolor(region_color, "L") + + def execute(self): + base_img_path = self.filepath / self.filename + if not base_img_path.exists(): + return None + + masks_path = self.filepath / "masks" + + if not masks_path.exists(): + os.makedirs(masks_path) + + black = ImageColor.getcolor("black", "L") + white = ImageColor.getcolor("white", "L") + + base_img: Image.Image = Image.open(base_img_path).convert("L") + already_processed = set() + + mask_count = 0 + mask_centers = {} + + for y1 in range(base_img.height): + for x1 in range(base_img.width): + if (x1, y1) in already_processed: + continue + if base_img.getpixel((x1, y1)) == self.region_color: + filled = floodfill(base_img, (x1, y1), black, self.wall_color) + if filled: # Pixels were updated, make them into a mask + mask = Image.new("L", base_img.size, 255) + for x2, y2 in filled: + mask.putpixel((x2, y2), 0) + + mask_count += 1 + mask = mask.convert("L") + mask.save(masks_path / f"{mask_count}.png", "PNG") + + mask_centers[mask_count] = get_center(filled) + + already_processed.update(filled) + + number_img = Image.new("L", base_img.size, 255) + fnt = ImageFont.load_default() + d = ImageDraw.Draw(number_img) + for mask_num, center in mask_centers.items(): + d.text(center, str(mask_num), font=fnt, fill=0) + + number_img.save(self.filepath / f"numbers.png", "PNG") + + return mask_centers diff --git a/dad/dad.py b/dad/dad.py index 4da2ece..de4361e 100644 --- a/dad/dad.py +++ b/dad/dad.py @@ -46,7 +46,7 @@ class Dad(Cog): async with aiohttp.ClientSession(headers=headers) as session: joke = await fetch_url(session, "https://icanhazdadjoke.com/") - await ctx.maybe_send_embed(joke['joke']) + await ctx.maybe_send_embed(joke["joke"]) # print(joke) # @@ -97,21 +97,21 @@ class Dad(Cog): if self.cooldown[guild.id] > datetime.now(): return - lower = message.clean_content.lower() - lower_split = lower.split() - if len(lower_split) == 0: + cleaned_content = message.clean_content + content_split = cleaned_content.split() + if len(content_split) == 0: return - if lower_split[0] == "i'm" and len(lower_split) >= 2: + if content_split[0].lower() == "i'm" and len(content_split) >= 2: if await guild_config.nickname(): try: - await message.author.edit(nick=lower[4:]) + await message.author.edit(nick=cleaned_content[4:]) except discord.Forbidden: - out = lower[4:] + out = cleaned_content[4:] else: out = message.author.mention else: - out = lower[4:] + out = cleaned_content[4:] try: await message.channel.send(f"Hi {out}, I'm {guild.me.display_name}!") except discord.HTTPException: diff --git a/dad/info.json b/dad/info.json index 65c9d3e..a67c63e 100644 --- a/dad/info.json +++ b/dad/info.json @@ -8,7 +8,7 @@ 0 ], "description": "Tell dad jokes and give out bad nicknames", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing Dad. Get started with `[p]load dad`, then `[p]help Dad`", "short": "Dad joke bot", "end_user_data_statement": "This cog does not store any End User Data", diff --git a/info.json b/info.json index 4d8e5e1..23cc49a 100644 --- a/info.json +++ b/info.json @@ -1,7 +1,7 @@ { - "AUTHOR": "Bobloy", - "INSTALL_MSG": "Thank you for installing Fox-V3 by Bobloy", - "NAME": "Fox-V3", - "SHORT": "Cogs by Bobloy", - "DESCRIPTION": "Cogs for RED Discord Bot by Bobloy" -} \ No newline at end of file + "author": ["Bobloy"], + "install_msg": "Thank you for installing Fox-V3 by Bobloy", + "name": "Fox-V3", + "short": "Cogs by Bobloy", + "description": "Cogs for RED Discord Bot by Bobloy" +} diff --git a/nudity/__init__.py b/nudity/__init__.py new file mode 100644 index 0000000..09d9dbf --- /dev/null +++ b/nudity/__init__.py @@ -0,0 +1,6 @@ +from .nudity import Nudity + + +def setup(bot): + n = Nudity(bot) + bot.add_cog(n) diff --git a/nudity/info..json b/nudity/info..json new file mode 100644 index 0000000..34c4804 --- /dev/null +++ b/nudity/info..json @@ -0,0 +1,26 @@ +{ + "author": [ + "Bobloy" + ], + "min_bot_version": [ + 3, + 3, + 11 + ], + "description": "Monitor images for NSFW content and moves them to a nsfw channel if possible", + "hidden": false, + "install_msg": "Thank you for installing Nudity. Get started with `[p]load nudity`, then `[p]help Nudity`", + "requirements": [ + "nudenet", + "tensorflow>=1.14,<2.0", + "keras>=2.4" + ], + "short": "NSFW image tracker and mover", + "tags": [ + "bobloy", + "utils", + "tools", + "nude", + "nsfw" + ] +} diff --git a/nudity/nudity.py b/nudity/nudity.py new file mode 100644 index 0000000..6eb4221 --- /dev/null +++ b/nudity/nudity.py @@ -0,0 +1,147 @@ +import pathlib + +import discord +from nudenet import NudeClassifier +from redbot.core import Config, commands +from redbot.core.bot import Red +from redbot.core.data_manager import cog_data_path + + +class Nudity(commands.Cog): + """ + V3 Cog Template + """ + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True) + + default_guild = {"enabled": False, "channel_id": None} + + self.config.register_guild(**default_guild) + + # self.detector = NudeDetector() + self.classifier = NudeClassifier() + + self.data_path: pathlib.Path = cog_data_path(self) + + self.current_processes = 0 + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete""" + return + + @commands.command(aliases=["togglenudity"], name="nudity") + async def nudity(self, ctx: commands.Context): + """Toggle nude-checking on or off""" + is_on = await self.config.guild(ctx.guild).enabled() + await self.config.guild(ctx.guild).enabled.set(not is_on) + await ctx.send("Nude checking is now set to {}".format(not is_on)) + + @commands.command() + async def nsfwchannel(self, ctx: commands.Context, channel: discord.TextChannel = None): + if channel is None: + await self.config.guild(ctx.guild).channel_id.set(None) + await ctx.send("NSFW Channel cleared") + else: + if not channel.is_nsfw(): + await ctx.send("This channel isn't NSFW!") + return + else: + await self.config.guild(ctx.guild).channel_id.set(channel.id) + await ctx.send("NSFW channel has been set to {}".format(channel.mention)) + + async def get_nsfw_channel(self, guild: discord.Guild): + channel_id = await self.config.guild(guild).channel_id() + + if channel_id is None: + return None + else: + return guild.get_channel(channel_id=channel_id) + + async def nsfw(self, message: discord.Message, images: dict): + content = message.content + guild: discord.Guild = message.guild + if not content: + content = "*`None`*" + try: + await message.delete() + except discord.Forbidden: + await message.channel.send("NSFW Image detected!") + return + + embed = discord.Embed(title="NSFW Image Detected") + embed.add_field(name="Original Message", value=content) + embed.set_author(name=message.author.name, icon_url=message.author.avatar_url) + await message.channel.send(embed=embed) + + nsfw_channel = await self.get_nsfw_channel(guild) + + if nsfw_channel is None: + return + else: + for image, r in images.items(): + if r["unsafe"] > 0.7: + await nsfw_channel.send( + "NSFW Image from {}".format(message.channel.mention), + file=discord.File(image,), + ) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + is_private = isinstance(message.channel, discord.abc.PrivateChannel) + + if not message.attachments or is_private or message.author.bot: + # print("did not qualify") + return + + try: + is_on = await self.config.guild(message.guild).enabled() + except AttributeError: + return + + if not is_on: + print("Not on") + return + + channel: discord.TextChannel = message.channel + + if channel.is_nsfw(): + print("nsfw channel is okay") + return + + check_list = [] + for attachment in message.attachments: + # async with aiohttp.ClientSession() as session: + # img = await fetch_img(session, attachment.url) + + ext = attachment.filename + + temp_name = self.data_path / f"nudecheck{self.current_processes}_{ext}" + + self.current_processes += 1 + + print("Pre attachment save") + await attachment.save(temp_name) + check_list.append(temp_name) + + print("Pre nude check") + # nude_results = self.detector.detect(temp_name) + nude_results = self.classifier.classify([str(n) for n in check_list]) + # print(nude_results) + + if True in [r["unsafe"] > 0.7 for r in nude_results.values()]: + # print("Is nude") + await message.add_reaction("❌") + await self.nsfw(message, nude_results) + else: + # print("Is not nude") + await message.add_reaction("✅") + + +# async def fetch_img(session, url): +# with aiohttp.Timeout(10): +# async with session.get(url) as response: +# assert response.status == 200 +# return await response.read() diff --git a/reactrestrict/info.json b/reactrestrict/info.json index e64dbf9..36ae9e4 100644 --- a/reactrestrict/info.json +++ b/reactrestrict/info.json @@ -8,9 +8,9 @@ 0 ], "description": "Cog to prevent reactions on specific messages from certain users", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing ReactRestrict.", - "short": "[Incomplete] Prevent reactions", + "short": "Prevent reactions to messages", "end_user_data_statement": "This cog does not store any End User Data", "tags": [ "react", diff --git a/stealemoji/info.json b/stealemoji/info.json index c926c3b..76a6681 100644 --- a/stealemoji/info.json +++ b/stealemoji/info.json @@ -8,7 +8,7 @@ 0 ], "description": "Steals custom emojis the bot sees and moves them to an emoji server", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing StealEmoji", "requirements": [], "short": "Steals custom emojis", diff --git a/tts/info.json b/tts/info.json index 4ad16c9..5a64d92 100644 --- a/tts/info.json +++ b/tts/info.json @@ -8,7 +8,7 @@ 0 ], "description": "Send Text2Speech messages as an uploaded mp3", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing TTS. Get started with `[p]load tts`, then `[p]help TTS`", "requirements": [ "gTTS"