Merge branch 'master' into 3.4_compatibility

# Conflicts:
#	ccrole/info.json
#	chatter/chat.py
#	reactrestrict/info.json
pull/112/head
bobloy 5 years ago
commit 79b755556e

@ -9,6 +9,7 @@ Cog Function
| ccrole | **Release** | <details><summary>Create custom commands that also assign roles</summary>May have some bugs, please create an issue if you find any</details> |
| chatter | **Beta** | <details><summary>Chat-bot trained to talk like your guild</summary>Missing some key features, but currently functional. See [Chatter](https://github.com/bobloy/Fox-V3/tree/master/chatter) for install instructions</details> |
| coglint | **Alpha** | <details><summary>Error check code in python syntax posted to discord</summary>Works, but probably needs more turning to work for cogs</details> |
| conquest | **Alpha** | <details><summary>Manage maps for war games and RPGs</summary>Lots of additional features are planned, currently function with simple map</details> |
| dad | **Beta** | <details><summary>Tell dad jokes</summary>Works great!</details> |
| exclusiverole | **Alpha** | <details><summary>Prevent certain roles from getting any other roles</summary>Fully functional, but pretty simple</details> |
| fight | **Incomplete** | <details><summary>Organize bracket tournaments within discord</summary>Still in-progress, a massive project</details> |
@ -20,7 +21,7 @@ Cog Function
| infochannel | **Beta** | <details><summary>Create a channel to display server info</summary>Just released, please report bugs</details> |
| lovecalculator | **Alpha** | <details><summary>Calculate the love between two users</summary>[Snap-Ons] Just updated to V3</details> |
| lseen | **Alpha** | <details><summary>Track when a member was last online</summary>Alpha release, please report bugs</details> |
| nudity | **Incomplete** | <details><summary>Checks for NSFW images posted in non-NSFW channels</summary>Library this is based on has a bug, waiting for author to merge my PR</details> |
| nudity | **Alpha** | <details><summary>Checks for NSFW images posted in non-NSFW channels</summary>Switched libraries, now functional</details> |
| planttycoon | **Alpha** | <details><summary>Grow your own plants!</summary>[Snap-Ons] Updated to V3, likely to contain bugs</details> |
| qrinvite | **Alpha** | <details><summary>Create a QR code invite for the server</summary>Alpha release, please report any bugs</details> |
| reactrestrict | **Alpha** | <details><summary>Removes reactions by role per channel</summary>A bit clunky, but functional</details> |

@ -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",

@ -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:

@ -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)

@ -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",

@ -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
```

@ -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):
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

@ -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)

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

@ -0,0 +1,3 @@
{
"region_max": 70
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

@ -0,0 +1,3 @@
{
"region_max": 70
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

@ -0,0 +1,7 @@
{
"maps": [
"simple_blank_map",
"test",
"test2"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

@ -0,0 +1,4 @@
{
"region_max": 70,
"extension": "jpg"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -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"
]
}

@ -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")

@ -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

@ -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:

@ -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",

@ -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"
}
"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"
}

@ -0,0 +1,6 @@
from .nudity import Nudity
def setup(bot):
n = Nudity(bot)
bot.add_cog(n)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save