Files
netease-bedrock-wiki/mconline/30-网络服插件教程/4-插件制作/5-插件编写——制作篇(中).md
2025-08-25 18:36:29 +08:00

272 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 插件编写——制作篇(中)
## Service编写
根据之前对Service的了解Service中是只应该有developer_mods文件夹的而我们现在的soldierLotteryService文件夹因为是从Game/Lobby复制而来的所以需要手动将没有用的文件夹删除。只留下developer_mods文件夹删除下图中选择的文件夹。
![](./images/code-4.png)
然后我们根据官方插件规范将soldierLotteryService/developer_mods的Lottery文件夹改名为soldierLotteryDev。
这里使用IntelliJ IDEA (PyCharm同理)为例,介绍插件代码的编写。
进入IDEA后打开soldierLotteryService的项目找到mod.json按照官方规范进行修改修改后的mod.json供参考。需要注意的是support_server_type需要更改成service否则无法在Service服部署。
```json
{
"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
```python
# 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
```python
# 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是否开奖
![](./images/code-7.png)
因为这张表我们只需要通过id来设置是否已经开奖而id又是主键自带索引。所以无需创建索引直接设计好后将SQL复制到mod.sql中即可。
```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。
![](./images/code-5.png)
由于功能上需要查询指定抽奖id的玩家拥有的号码同时需要检索player和lottery两列数据所以我们需要给它们加上索引来提高后续的搜索速度。
索引添加如下图。索引涉及到的知识要求相对较高如果不理解可以在MySQL相关教程处学习这里我们简单理解成需要检索哪一列就给哪一列添加索引
![](./images/code-6.png)
随后在SQL预览处将SQL复制出来自行修改表名粘贴到mod.sql中。
```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```的一部分代码。
```python
# 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```。
```python
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部分](https://g79.gdl.netease.com/pluginguide04-06.zip)
随后我们就可以部署并测试啦!