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 roles
May have some bugs, please create an issue if you find any |
| chatter | **Beta** | Chat-bot trained to talk like your guild
Missing 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 discord
Works, but probably needs more turning to work for cogs |
+| conquest | **Alpha** | Manage maps for war games and RPGs
Lots of additional features are planned, currently function with simple map |
| dad | **Beta** | Tell dad jokes
Works great! |
| exclusiverole | **Alpha** | Prevent certain roles from getting any other roles
Fully functional, but pretty simple |
| fight | **Incomplete** | Organize bracket tournaments within discord
Still in-progress, a massive project |
@@ -20,7 +21,7 @@ Cog Function
| infochannel | **Beta** | Create a channel to display server info
Just 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 online
Alpha release, please report bugs |
-| nudity | **Incomplete** | Checks for NSFW images posted in non-NSFW channels
Library this is based on has a bug, waiting for author to merge my PR |
+| nudity | **Alpha** | Checks for NSFW images posted in non-NSFW channels
Switched 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 server
Alpha release, please report any bugs |
| reactrestrict | **Alpha** | Removes reactions by role per channel
A 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"