diff --git a/__init__.py b/__init__.py index 7d8c9a7..977f4ff 100644 --- a/__init__.py +++ b/__init__.py @@ -57,6 +57,13 @@ __plugin_meta__ = PluginMetadata( help="我的农场返回图片的清晰度, [low, medium, hight, original]", default_value="low", ), + RegisterConfig( + key="动态背景", + value=True, + help="是否开启根据季节和节日变化的动态背景", + type=bool, + default_value=True, + ), RegisterConfig( key="签到图片样式", value="text", diff --git a/core/database/database.py b/core/database/database.py index e1161a3..488bbed 100644 --- a/core/database/database.py +++ b/core/database/database.py @@ -35,10 +35,10 @@ class CSqlManager(ABC): if cls.m_pDB is None: cls.m_pDB = await aiosqlite.connect(config.g_sDBFilePath) cls.m_pDB.row_factory = aiosqlite.Row - logger.info("真寻农场数据库连接初始化成功") + logger.debug("真寻农场数据库连接初始化成功") return True else: - logger.info("真寻农场数据库连接已存在,跳过初始化") + logger.debug("真寻农场数据库连接已存在,跳过初始化") return True except Exception as e: logger.error(f"真寻农场数据库连接初始化失败: {e}") @@ -46,10 +46,33 @@ class CSqlManager(ABC): @classmethod async def cleanup(cls): - if cls.m_pDB: - await cls.m_pDB.close() + try: + if not cls.isConnectionValid(): + logger.warning("真寻农场数据库连接已关闭或无效,跳过清理") + return + + if hasattr(cls, "m_pDB") and cls.m_pDB: + await cls.m_pDB.close() + cls.m_pDB = None + cls._initialized = False + logger.debug("真寻农场数据库连接已安全关闭") + else: + logger.debug("真寻农场数据库连接已不存在,无需关闭") + + except Exception as e: + logger.error(f"真寻农场数据库清理过程中出错: {e}") cls.m_pDB = None - logger.info("真寻农场数据库连接已关闭") + cls._initialized = False + + @classmethod + def isConnectionValid(cls) -> bool: + if not hasattr(cls, "m_pDB") or cls.m_pDB is None: + return False + + try: + return hasattr(cls.m_pDB, "_conn") and cls.m_pDB._conn is not None + except: + return False @classmethod def getDB(cls) -> aiosqlite.Connection: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/resource/background/frame.png b/resource/background/frame.png deleted file mode 100644 index a37e8bb..0000000 Binary files a/resource/background/frame.png and /dev/null differ diff --git a/utils/__init__.py b/utils/__init__.py index 14a0990..68a2bea 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,3 +1,6 @@ +from . import utils + + def getConfig(): from . import config @@ -34,4 +37,5 @@ __all__ = [ "getJsonManager", "getRequestManager", "getToolManager", + "utils", ] diff --git a/utils/darw/drawFarm.py b/utils/darw/drawFarm.py index eb7f556..ff65407 100644 --- a/utils/darw/drawFarm.py +++ b/utils/darw/drawFarm.py @@ -4,8 +4,9 @@ from zhenxun.utils._build_image import BuildImage from zhenxun.utils.image_utils import ImageTemplate from zhenxun.utils.platform import PlatformUtils +from ...core import CPlayer from ...core.dbService import g_pDBService -from .. import config, getJsonManager, getToolManager +from .. import config, getJsonManager, getToolManager, utils class CDarwFarm: @@ -19,7 +20,9 @@ class CDarwFarm: Returns: bytes: 返回绘制结果 """ - img, soilSize, soilPos, grass = await cls.createBaseCanvas() + dynamic = Config.get_config("zhenxun_plugin_farm", "动态背景") + + img, soilSize, soilPos, grass = await cls.createBaseCanvas(dynamic) player = await getToolManager().getPlayerByUid(uid) if not player: @@ -48,20 +51,44 @@ class CDarwFarm: isFirstExpansion = False await cls.drawUserInfo(img, player, uid) + await cls.applyDefinitionResize(img) return img.pic2bytes() @classmethod - async def createBaseCanvas(cls) -> tuple[BuildImage, tuple, dict, BuildImage]: + async def createBaseCanvas( + cls, dynamic: bool + ) -> tuple[BuildImage, tuple, dict, BuildImage]: """创建基础画布并返回 (img, soilSize, soilPos, grass) Returns: tuple: (主画布, 土地尺寸, 土地坐标映射, 草地图) """ - img = BuildImage( - background=config.g_sResourcePath / "background/background.jpg" - ) + + if dynamic: + season = getToolManager().getSeason() + + backgroundPath = config.g_sResourcePath / "background/decorateIcon" + + # 节日优先级更高 + if season == utils.Season.SPRING: + bgFile = backgroundPath / "decorate_spring_1.jpg" + elif season == utils.Season.SUMMER: + bgFile = backgroundPath / "decorate_summer_1.jpg" + elif season == utils.Season.AUTUMN: + bgFile = backgroundPath / "decorate_autumn_1.jpg" + elif season == utils.Season.WINTER: + bgFile = backgroundPath / "decorate_winter_1.jpg" + else: + bgFile = backgroundPath / "decorate_default_1.jpg" + + img = BuildImage(background=bgFile) + else: + img = BuildImage( + background=config.g_sResourcePath + / "background/decorateIcon/decorate_default_1jpg" + ) soil = await getJsonManager().getSoil() soilSize = soil["size"] @@ -76,17 +103,17 @@ class CDarwFarm: async def drawUnlockedTile( cls, img: BuildImage, uid: str, soilIndex: int, x: int, y: int, soilSize ) -> bool: - """绘制已解锁土地格(包含土地贴图与作物),返回该格是否可收获。 + """绘制已解锁土地格(包含土地贴图与作物),返回该格是否可收获 Args: - img (BuildImage): 主画布。 - uid (str): 用户UID。 - soilIndex (int): 土地索引(从1开始)。 - x (int), y (int): 贴图位置。 - soilSize: 土地贴图尺寸([w,h] 或 (w,h))。 + img (BuildImage): 主画布 + uid (str): 用户UID + soilIndex (int): 土地索引(从1开始) + x (int), y (int): 贴图位置 + soilSize: 土地贴图尺寸([w,h] 或 (w,h)) Returns: - bool: 是否有可收获作物(isRipe)。 + bool: 是否有可收获作物(isRipe) """ soilUrl = "soil/普通土地.png" soilInfo = await g_pDBService.userSoil.getUserSoil(uid, soilIndex) @@ -128,10 +155,10 @@ class CDarwFarm: grass: BuildImage, isFirstExpansion: bool, ) -> bool: - """绘制未解锁格子并在首次出现时绘制扩建图标。 + """绘制未解锁格子并在首次出现时绘制扩建图标 Returns: - bool: 是否在此格绘制了扩建图标(仅首次返回 True)。 + bool: 是否在此格绘制了扩建图标(仅首次返回 True) """ await img.paste(grass, (x, y)) @@ -153,7 +180,7 @@ class CDarwFarm: @classmethod async def pasteRipeOnce(cls, img: BuildImage, x: int, y: int, soilSize): - """首次发现成熟作物时贴上成熟提示图(ripe)。""" + """首次发现成熟作物时贴上成熟提示图(ripe)""" ripe = BuildImage(background=config.g_sResourcePath / "background/ripe.png") await img.paste( ripe, @@ -161,22 +188,32 @@ class CDarwFarm: ) @classmethod - async def drawUserInfo(cls, img: BuildImage, player, uid: str): - """绘制左上角用户信息(头像、框、昵称、经验、等级、货币等)。 + async def drawUserInfo(cls, img: BuildImage, player: CPlayer, uid: str): + """绘制左上角用户信息(头像、框、昵称、经验、等级、货币等) TODO 需要详细的绘制头像框 Args: - img (BuildImage): 主画布。 - player: 用户对象(包含 user 字段)。 - uid (str): 用户UID。 + img (BuildImage): 主画布 + player(CPlayer): 用户 + uid (str): 用户Uid """ + # 背景板 + background = BuildImage( + background=config.g_sResourcePath / "background/userInfo.png" + ) + await img.paste(background, (150, 30)) + + # 头像 image = await PlatformUtils.get_user_avatar(uid, "qq") if image: avatar = BuildImage(background=image) await avatar.resize(0, 140, 150) await img.paste(avatar, (125, 85)) - frame = BuildImage(background=config.g_sResourcePath / "background/frame.png") + # 头像框 + frame = BuildImage( + background=config.g_sResourcePath / "background/frame/frame.png" + ) await img.paste(frame, (75, 44)) nameImg = await BuildImage.build_text_image( @@ -214,9 +251,37 @@ class CDarwFarm: ) await img.paste(bondsImg, (570, 255)) + @classmethod + async def drawUserDecorateIcon(cls, img: BuildImage, player: CPlayer) -> bool: + """绘制用户装饰图标 + + Args: + img (BuildImage): 主画布 + player (CPlayer): 用户 + + Returns: + bool: 是否成功绘制 + """ + + decorateIconId = player.user.get("decorateIcon", 0) + if decorateIconId <= 0: + return False + + iconPath = ( + config.g_sResourcePath / f"decorateIcon/DecorateIcon{decorateIconId}.png" + ) + if not iconPath.exists(): + return False + + icon = BuildImage(background=iconPath) + await icon.resize(0, 100, 100) + await img.paste(icon, (x, y)) + + return True + @classmethod async def applyDefinitionResize(cls, img: BuildImage): - """根据配置调整绘制清晰度(original/medium/hight/其他)。""" + """根据配置调整绘制清晰度(original/medium/hight/其他)""" definition = Config.get_config("zhenxun_plugin_farm", "绘制农场清晰度") if definition == "medium": await img.resize(0.6) diff --git a/utils/tool.py b/utils/tool.py index 5976b1a..852525c 100644 --- a/utils/tool.py +++ b/utils/tool.py @@ -5,11 +5,13 @@ from zoneinfo import ZoneInfo from jinja2 import Template from playwright.async_api import async_playwright +from zhdate import ZhDate from zhenxun.services.log import logger from zhenxun.utils.message import MessageUtils from ..core import CPlayer, g_pUserPool +from . import utils class CToolManager: @@ -110,6 +112,47 @@ class CToolManager: tz = ZoneInfo("Asia/Shanghai") return datetime.now(tz) + @classmethod + def getSeason(cls) -> utils.Season: + month = cls.dateTime().month + + if month in [3, 4, 5]: + return utils.Season.SPRING + elif month in [6, 7, 8]: + return utils.Season.SUMMER + elif month in [9, 10, 11]: + return utils.Season.AUTUMN + elif month in [12, 1, 2]: + return utils.Season.WINTER + else: + return utils.Season.NO + + @classmethod + def getFestival(cls) -> utils.Festival: + now = cls.dateTime() + + lunarDate = ZhDate.from_datetime(now) + lunarMonth = lunarDate.lunar_month + lunarDay = lunarDate.lunar_day + + festivalChecks = [ + (1, lambda: lunarMonth == 8 and lunarDay == 15), # 中秋节 + (2, lambda: (now.month, now.day) == (10, 1)), # 国庆节 + (3, lambda: lunarMonth == 1 and lunarDay == 15), # 元宵节 + (4, lambda: lunarMonth == 5 and lunarDay == 5), # 端午节 + (5, lambda: lunarMonth == 1 and lunarDay == 1), # 春节 + (6, lambda: (now.month, now.day) == (10, 31)), # 万圣节 + (7, lambda: (now.month, now.day) == (12, 25)), # 圣诞节 + (8, lambda: (now.month, now.day) == (1, 1)), # 新年 + (9, lambda: (now.month, now.day) == (2, 14)), # 情人节 + ] + + for code, checkFunc in festivalChecks: + if checkFunc(): + return utils.Festival(code) + + return utils.Festival.NO + @classmethod def renderHtmlToFile( cls, path: Path | str, context: dict, output: Path | str diff --git a/utils/utils.py b/utils/utils.py new file mode 100644 index 0000000..4390413 --- /dev/null +++ b/utils/utils.py @@ -0,0 +1,22 @@ +from enum import Enum + + +class Festival(Enum): + NO = 0 # 无节日 + MIDAUTUMN = 1 # 中秋节 + NATIONALDAY = 2 # 国庆节 + LANTERNFESTIVAL = 3 # 元宵节 + DRAGONBOAT = 4 # 端午节 + SPRINGFESTIVAL = 5 # 春节 + HALLOWEEN = 6 # 万圣节 + CHRISTMAS = 7 # 圣诞节 + NEWYEAR = 8 # 新年 + VALENTINE = 9 # 情人节 + + +class Season(Enum): + NO = 0 # 无季节 + SPRING = 1 # 春季 + SUMMER = 2 # 夏季 + AUTUMN = 3 # 秋季 + WINTER = 4 # 冬季