# 插件编写——制作篇(中)

# Service编写

根据之前对Service的了解,Service中是只应该有developer_mods文件夹的,而我们现在的soldierLotteryService文件夹,因为是从Game/Lobby复制而来的,所以需要手动将没有用的文件夹删除。只留下developer_mods文件夹,删除下图中选择的文件夹。

然后我们根据官方插件规范,将soldierLotteryService/developer_mods的Lottery文件夹,改名为soldierLotteryDev。

这里使用IntelliJ IDEA (PyCharm同理)为例,介绍插件代码的编写。

进入IDEA后,打开soldierLotteryService的项目,找到mod.json,按照官方规范进行修改,修改后的mod.json供参考。需要注意的是,support_server_type需要更改成service,否则无法在Service服部署。

{
  "netgame_mod_name": "soldierLotteryDev",
  "netgame_mod_version": "1.0.0",
  "min_app_version": "1.23.0.release20210722",
  "author": "Soldier",
  "module_names": "soldierLottery",
  "support_server_type": [
    "service"
  ],
  "group": "soldierLottery"
}

随后我们就可以开始编写代码了,Service服的入口modMain和一般Mod的结构非常相似,创建一个consts.py用来存放一些常量后,就可以参照下方代码编写了。

modMain.py

# coding=utf-8
from mod.common.mod import Mod
import mod.server.extraServiceApi as serviceApi

from soldierLotteryScripts.consts import ModName, ModVersion, ServiceSystemName, ServiceSystemClsPath


@Mod.Binding(name=ModName, version=ModVersion)
class SoldierLotteryService(object):
    @Mod.InitService()
    def InitService(self):
        serviceApi.RegisterSystem(ModName, ServiceSystemName, ServiceSystemClsPath)
        print "SoldierLotteryService启动"

    @Mod.DestroyService()
    def DestroyService(self):
        print "SoldierLotteryService卸载"

lotteryServiveSystem.py

# coding=utf-8
from mod.server.system.serviceSystem import ServiceSystem

from soldierLotteryScripts.mysqlManager import MysqlManager


class LotteryServiceSystem(ServiceSystem):

    def __init__(self, namespace, systemName):
        ServiceSystem.__init__(self, namespace, systemName)

随后我们可以开始编写MySQL数据库相关的代码。

在编写数据库的代码之前,我们首先需要设计好数据表结构。分析需求,可以得出该插件需要两张数据表,分别用来存储抽奖信息和玩家信息。

# 抽奖信息表

主要需要记录 抽奖id,是否开奖

因为这张表我们只需要通过id来设置是否已经开奖,而id又是主键,自带索引。所以无需创建索引,直接设计好后将SQL复制到mod.sql中即可。

CREATE TABLE `soldierLotteryState`  (
    `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID既抽奖ID',
    `finish` int(1) UNSIGNED NOT NULL COMMENT '是否已经开奖',
    PRIMARY KEY (`id`)
);

# 玩家信息表

主要需要记录 玩家uid,玩家持有的抽奖id,对应抽奖的号码

则可以使用Navicat设计出数据表如下图所示。其中需要注意的是,玩家uid的类型必须为无符号 int,在sql命令中表达为 UNSIGNED INT,不能是int。

由于功能上需要查询指定抽奖id的玩家拥有的号码,同时需要检索player和lottery两列数据,所以我们需要给它们加上索引来提高后续的搜索速度。

索引添加如下图。(索引涉及到的知识要求相对较高,如果不理解,可以在MySQL相关教程处学习,这里我们简单理解成需要检索哪一列就给哪一列添加索引)

随后在SQL预览处将SQL复制出来,自行修改表名,粘贴到mod.sql中。

CREATE TABLE `soldierLotteryPlayer`  (
    `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID',
    `player` int UNSIGNED NOT NULL COMMENT '玩家UID',
    `lottery` int(10) NOT NULL COMMENT '抽奖ID',
    `number` int(10) NOT NULL COMMENT '玩家持有的抽奖号码',
    PRIMARY KEY (`id`),
    INDEX `player_lottery`(`player`, `lottery`) USING BTREE COMMENT '玩家uid和抽奖id的索引'
);

# 数据接口

设计完数据表,就可以根据需求,编写可能会用到的数据相关接口。

这里我们对照接口一项一项编写,下面展示mysqlManager.py的一部分代码。

# coding=utf-8
import random
from collections import OrderedDict

from apolloCommon import mysqlPool

from soldierLotteryScripts import consts


class MysqlManager(object):
    """
    MysqlManager主要用来管理数据库数据
    提供函数快捷获取玩家数据
    """

    def __init__(self, serviceSystem):
        self.mServiceSystem = serviceSystem
        print "初始化MySQL数据库连接"
        mysqlPool.InitDB(10)
        self.mPlayerCache = {}  # 玩家数据缓存
        self.mStateCache = OrderedDict()  # 抽奖状态缓存
        self.mLotteryNumbers = {}  # 抽奖号码缓存
        self.CacheLotteryStates()
        self.CacheAllNumbers()

    def Destroy(self):
        mysqlPool.Finish()
        print "关闭MySQL连接"

    def GetPlayerCache(self, uid):
        """
        获取玩家缓存,没有缓存则从数据库获取,并返回None
        :param uid:
        :return:
        """
        cache = self.mPlayerCache.get(uid)
        if cache is not None:
            return cache

        def callback(uid, result):
            data = {}
            if result:
                for line in result:
                    tmp = data.get(line[0], [])
                    tmp.append(line[1])
                    data[line[0]] = tmp
            print "缓存玩家{}的数据为{}".format(uid, data)
            self.mPlayerCache[uid] = data

        sql = "SELECT `lottery`,`number` FROM `{}` WHERE `player` = %s;".format(consts.LotteryPlayerTable)
        mysqlPool.AsyncQueryWithOrderKey("cache_player_{}".format(uid), sql, (uid,),
                                         lambda result: callback(uid, result))
        return None

    def RemovePlayerCache(self, uid):
        """
        移除玩家缓存,退出时触发
        :param uid: 玩家UID
        :return:None
        """
        cache = self.mPlayerCache.get(uid)
        if cache is None:
            # 因为正常获取过缓存数据的玩家,都存储过cache为{},不可能是None。所以此处是None的话不处理
            return
        del self.mPlayerCache[uid]

需要从这串代码中注意的是,所有数据都采用了缓存的方式来处理。即玩家加入游戏,从MySQL获取数据并缓存;玩家退出游戏,如果玩家的数据发生了变化,则存到数据库并删除缓存,如果没变化则直接删除缓存。

并且,在执行SQL命令时,需要注意尽量让每个操作的OrderKey唯一,这样就可以使SQL命令并发执行,提高效率。

# 发送邮件

在需求中,有一条是使用官方邮箱插件发送奖励。所以我们首先需要下载官方的邮箱插件,并查看他的简介。

简介可以在官方插件页面下载后,在插件的右键菜单中找到。

在简介中可以找到发送邮件的接口

(7)向一组指定uid的玩家发送私人邮件(功能服用) 函数:OutSendMailToMany(touids, title, content, itemList=[], expire=None, srcName="") 参数: touids:list(int), 玩家唯一ID的列表 title:str, 邮件标题 content:str, 邮件正文 itemList:list(dict), 附件物品列表,格式与通用的【物品信息字典】相同(参照ModSDk中往背包中塞物品的字典),额外支持【durability】关键字定义耐久度。另外,如果同经济插件一起使用,还支持货币,格式为: itemType为"currency"表示货币类型,itemName对应经济插件mod.json中dough_id配置,icon对应济插件mod.json中dough_icon配置,count表示货币数量。 expire:int, 邮件有效期,单位秒 srcName:str, 邮件发送者名字 示例: import server.extraServiceApi as serviceApi itemDict = { 'itemName': 'minecraft:bow', 'count': 1, 'enchantData': [(19, 1),], 'auxValue': 0, 'customTips':'§c new item §r', 'extraId': 'abc' } currencyDict = { 'itemName' : 'RMB', 'itemType' : 'currency', 'icon' : 'textures/ui/netease_trade/icon03@3x.png', 'count' : 3 } mailSystem = serviceApi.GetSystem("neteaseAnnounce", "neteaseAnnounceService") mailSystem.OutSendMailToMany([123,234], "欢迎新人", "欢迎首次登录,开发组送上弓一把", [itemDict, currencyDict], 86400, "开发组")

我们可以参考介绍编写发送奖励邮件的代码。

# 通信

我们在Service服上编写的接口如果需要被Game/Lobby调用,就需要用到服务器之间的通信。下方的例子就实现了接口请求。对应代码文件lotteryServiceSystem.py

class LotteryServiceSystem(ServiceSystem):

    def __init__(self, namespace, systemName):
        ServiceSystem.__init__(self, namespace, systemName)
        self.mMysqlManager = MysqlManager(self)
        self.RegisterRpcMethodForMod(consts.GetPlayerRandomNumber, self.OnGetPlayerRandomNumber)

    def OnGetPlayerRandomNumber(self, serverId, callbackId, args):
        player = args["player"]
        lottery = self.mMysqlManager.GetUnfinishedLottery()
        if lottery == -1:
            eventData = {
                "code": 1,
                "msg": -1
            }
        else:
            eventData = {
                "code": 0,
                "msg": self.mMysqlManager.GetPlayerRandomNumber(player, lottery)
            }

        self.ResponseToServer(serverId, callbackId, eventData)

# 代码下载

教程中仅展示了部分代码,全部代码可以在这里下载。

抽奖插件——service部分 (opens new window)

随后我们就可以部署并测试啦!