Merge branch 'master' into chatter_develop

pull/118/head
bobloy 5 years ago
commit e5947953aa

@ -1,6 +1,7 @@
from redbot.core import data_manager from redbot.core import data_manager
from .conquest import Conquest from .conquest import Conquest
from .mapmaker import MapMaker
async def setup(bot): async def setup(bot):
@ -9,3 +10,6 @@ async def setup(bot):
await cog.load_data() await cog.load_data()
bot.add_cog(cog) bot.add_cog(cog)
cog2 = MapMaker(bot)
bot.add_cog(cog2)

@ -1,11 +1,13 @@
import asyncio
import json import json
import os import os
import pathlib import pathlib
from abc import ABC
from shutil import copyfile from shutil import copyfile
from typing import Optional from typing import Optional
import discord import discord
from PIL import Image, ImageColor, ImageOps from PIL import Image, ImageChops, ImageColor, ImageOps
from discord.ext.commands import Greedy from discord.ext.commands import Greedy
from redbot.core import Config, commands from redbot.core import Config, commands
from redbot.core.bot import Red from redbot.core.bot import Red
@ -14,9 +16,7 @@ from redbot.core.data_manager import bundled_data_path, cog_data_path
class Conquest(commands.Cog): class Conquest(commands.Cog):
""" """
Cog Description Cog for
Less important information about the cog
""" """
default_zoom_json = {"enabled": False, "x": -1, "y": -1, "zoom": 1.0} default_zoom_json = {"enabled": False, "x": -1, "y": -1, "zoom": 1.0}
@ -38,18 +38,29 @@ class Conquest(commands.Cog):
self.current_map = None self.current_map = None
self.map_data = None self.map_data = None
self.ext = None
self.ext_format = None
async def red_delete_data_for_user(self, **kwargs): async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete""" """Nothing to delete"""
return return
async def load_data(self): async def load_data(self):
"""
Initial loading of data from bundled_data_path and config
"""
self.asset_path = bundled_data_path(self) / "assets" self.asset_path = bundled_data_path(self) / "assets"
self.current_map = await self.config.current_map() 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" map_data_path = self.asset_path / self.current_map / "data.json"
with map_data_path.open() as mapdata: with map_data_path.open() as mapdata:
self.map_data = json.load(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() @commands.group()
async def conquest(self, ctx: commands.Context): async def conquest(self, ctx: commands.Context):
@ -57,7 +68,8 @@ class Conquest(commands.Cog):
Base command for conquest cog. Start with `[p]conquest set map` to select a map. Base command for conquest cog. Start with `[p]conquest set map` to select a map.
""" """
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
pass # TODO: Print current map probably if self.current_map is not None:
await self._conquest_current(ctx)
@conquest.command(name="list") @conquest.command(name="list")
async def _conquest_list(self, ctx: commands.Context): async def _conquest_list(self, ctx: commands.Context):
@ -144,10 +156,10 @@ class Conquest(commands.Cog):
return return
zoomed_path = await self._create_zoomed_map( zoomed_path = await self._create_zoomed_map(
self.data_path / self.current_map / "current.jpg", x, y, zoom self.data_path / self.current_map / f"current.{self.ext}", x, y, zoom
) )
await ctx.send(file=discord.File(fp=zoomed_path, filename="current_zoomed.jpg",)) 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): async def _create_zoomed_map(self, map_path, x, y, zoom, **kwargs):
current_map = Image.open(map_path) current_map = Image.open(map_path)
@ -156,8 +168,8 @@ class Conquest(commands.Cog):
zoom2 = zoom * 2 zoom2 = zoom * 2
zoomed_map = current_map.crop((x - w / zoom2, y - h / zoom2, x + w / zoom2, y + h / zoom2)) 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 = zoomed_map.resize((w, h), Image.LANCZOS)
zoomed_map.save(self.data_path / self.current_map / "zoomed.jpg", "jpeg") zoomed_map.save(self.data_path / self.current_map / f"zoomed.{self.ext}", self.ext_format)
return self.data_path / self.current_map / "zoomed.jpg" return self.data_path / self.current_map / f"zoomed.{self.ext}"
@conquest_set.command(name="save") @conquest_set.command(name="save")
async def _conquest_set_save(self, ctx: commands.Context, *, save_name): async def _conquest_set_save(self, ctx: commands.Context, *, save_name):
@ -167,13 +179,13 @@ class Conquest(commands.Cog):
return return
current_map_folder = self.data_path / self.current_map current_map_folder = self.data_path / self.current_map
current_map = current_map_folder / "current.jpg" current_map = current_map_folder / f"current.{self.ext}"
if not current_map_folder.exists() or not current_map.exists(): 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") await ctx.maybe_send_embed("Current map doesn't exist! Try setting a new one")
return return
copyfile(current_map, current_map_folder / f"{save_name}.jpg") copyfile(current_map, current_map_folder / f"{save_name}.{self.ext}")
await ctx.tick() await ctx.tick()
@conquest_set.command(name="load") @conquest_set.command(name="load")
@ -184,8 +196,8 @@ class Conquest(commands.Cog):
return return
current_map_folder = self.data_path / self.current_map current_map_folder = self.data_path / self.current_map
current_map = current_map_folder / "current.jpg" current_map = current_map_folder / f"current.{self.ext}"
saved_map = current_map_folder / f"{save_name}.jpg" saved_map = current_map_folder / f"{save_name}.{self.ext}"
if not current_map_folder.exists() or not saved_map.exists(): 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") await ctx.maybe_send_embed(f"Saved map not found in the {self.current_map} folder")
@ -211,12 +223,16 @@ class Conquest(commands.Cog):
self.current_map = mapname self.current_map = mapname
await self.config.current_map.set(self.current_map) # Save to config too await self.config.current_map.set(self.current_map) # Save to config too
map_data_path = self.asset_path / mapname / "data.json" await self.current_map_load()
with map_data_path.open() as mapdata:
self.map_data = json.load(mapdata) # 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_folder = self.data_path / self.current_map
current_map = current_map_folder / "current.jpg" current_map = current_map_folder / f"current.{self.ext}"
if not reset and current_map.exists(): if not reset and current_map.exists():
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
@ -226,7 +242,7 @@ class Conquest(commands.Cog):
else: else:
if not current_map_folder.exists(): if not current_map_folder.exists():
os.makedirs(current_map_folder) os.makedirs(current_map_folder)
copyfile(self.asset_path / mapname / "blank.jpg", current_map) copyfile(self.asset_path / mapname / f"blank.{self.ext}", current_map)
await ctx.tick() await ctx.tick()
@ -239,9 +255,9 @@ class Conquest(commands.Cog):
await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`")
return return
current_jpg = self.data_path / self.current_map / "current.jpg" current_img = self.data_path / self.current_map / f"current.{self.ext}"
await self._send_maybe_zoomed_map(ctx, current_jpg, "current_map.jpg") 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): async def _send_maybe_zoomed_map(self, ctx, map_path, filename):
zoom_data = {"enabled": False} zoom_data = {"enabled": False}
@ -266,10 +282,9 @@ class Conquest(commands.Cog):
await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`")
return return
current_blank_jpg = self.asset_path / self.current_map / "blank.jpg" current_blank_img = self.asset_path / self.current_map / f"blank.{self.ext}"
await self._send_maybe_zoomed_map(ctx, current_blank_jpg, "blank_map.jpg") await self._send_maybe_zoomed_map(ctx, current_blank_img, f"blank_map.{self.ext}")
# await ctx.send(file=discord.File(fp=current_blank_jpg, filename="blank_map.jpg"))
@conquest.command("numbered") @conquest.command("numbered")
async def _conquest_numbered(self, ctx: commands.Context): async def _conquest_numbered(self, ctx: commands.Context):
@ -280,35 +295,66 @@ class Conquest(commands.Cog):
await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`") await ctx.maybe_send_embed("No map is currently set. See `[p]conquest set map`")
return return
numbers_path = self.asset_path / self.current_map / "numbers.jpg" numbers_path = self.asset_path / self.current_map / f"numbers.{self.ext}"
if not numbers_path.exists(): if not numbers_path.exists():
await ctx.send( await ctx.send(
file=discord.File( file=discord.File(
fp=self.asset_path / self.current_map / "numbered.jpg", fp=self.asset_path / self.current_map / f"numbered.{self.ext}",
filename="numbered.jpg", filename=f"numbered.{self.ext}",
) )
) )
return return
current_map = Image.open(self.data_path / self.current_map / "current.jpg") current_map = Image.open(self.data_path / self.current_map / f"current.{self.ext}")
numbers = Image.open(numbers_path).convert("L") numbers = Image.open(numbers_path).convert("L")
inverted_map = ImageOps.invert(current_map) inverted_map = ImageOps.invert(current_map)
current_numbered_jpg: Image.Image = Image.composite(current_map, inverted_map, numbers) loop = asyncio.get_running_loop()
current_numbered_jpg.save( current_numbered_img = await loop.run_in_executor(
self.data_path / self.current_map / "current_numbered.jpg", "jpeg" 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( await self._send_maybe_zoomed_map(
ctx, self.data_path / self.current_map / "current_numbered.jpg", "current_numbered.jpg" ctx,
self.data_path / self.current_map / f"current_numbered.{self.ext}",
f"current_numbered.{self.ext}",
) )
# await ctx.send(
# file=discord.File( @conquest.command(name="multitake")
# fp=self.data_path / self.current_map / "current_numbered.jpg", async def _conquest_multitake(
# filename="current_numbered.jpg", 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") @conquest.command(name="take")
async def _conquest_take(self, ctx: commands.Context, regions: Greedy[int], *, color: str): async def _conquest_take(self, ctx: commands.Context, regions: Greedy[int], *, color: str):
@ -337,27 +383,28 @@ class Conquest(commands.Cog):
await ctx.maybe_send_embed( await ctx.maybe_send_embed(
f"Max region number is {self.map_data['region_max']}, minimum is 1" f"Max region number is {self.map_data['region_max']}, minimum is 1"
) )
return
current_jpg_path = self.data_path / self.current_map / "current.jpg" await self._process_take_regions(color, ctx, regions)
im = Image.open(current_jpg_path)
out: Image.Image = await self._composite_regions(im, regions, color)
out.save(current_jpg_path, "jpeg")
await self._send_maybe_zoomed_map(ctx, current_jpg_path, "map.jpg")
# await ctx.send(file=discord.File(fp=current_jpg_path, filename="map.jpg"))
async def _composite_regions(self, im, regions, color) -> Image.Image: async def _composite_regions(self, im, regions, color) -> Image.Image:
im2 = Image.new("RGB", im.size, color) im2 = Image.new("RGB", im.size, color)
out = None loop = asyncio.get_running_loop()
combined_mask = None
for region in regions: for region in regions:
mask = Image.open( mask = Image.open(
self.asset_path / self.current_map / "masks" / f"{region}.jpg" self.asset_path / self.current_map / "masks" / f"{region}.{self.ext}"
).convert("L") ).convert("L")
if out is None: if combined_mask is None:
out = Image.composite(im, im2, mask) combined_mask = mask
else: else:
out = Image.composite(out, im2, mask) # 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 return out

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

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

@ -9,10 +9,10 @@
], ],
"description": "Handle war games by filling in specified territories with colors", "description": "Handle war games by filling in specified territories with colors",
"hidden": false, "hidden": false,
"install_msg": "Thank you for installing Dad. Get started with `[p]load conquest`, then `[p]help Conquest`", "install_msg": "Thank you for installing Conquest. Get started with `[p]load conquest`, then `[p]help Conquest`",
"short": "War Game Map", "short": "War Game Map",
"requirements": [ "requirements": [
"pillow" "Pillow"
], ],
"tags": [ "tags": [
"bobloy", "bobloy",

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

@ -1,7 +1,7 @@
{ {
"AUTHOR": "Bobloy", "author": ["Bobloy"],
"INSTALL_MSG": "Thank you for installing Fox-V3 by Bobloy", "install_msg": "Thank you for installing Fox-V3 by Bobloy",
"NAME": "Fox-V3", "name": "Fox-V3",
"SHORT": "Cogs by Bobloy", "short": "Cogs by Bobloy",
"DESCRIPTION": "Cogs for RED Discord Bot by Bobloy" "description": "Cogs for RED Discord Bot by Bobloy"
} }

Loading…
Cancel
Save