first commit

This commit is contained in:
boybook
2025-03-17 13:24:39 +08:00
commit 9a0334ee84
6410 changed files with 221907 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
---
front:
hard: 入门
time: 60分钟
---
# 支持基岩版客户端的Java版网络游戏概述
## 目的
- 本文档旨在阐述通过Apollo工具搭建并部署Java版网络游戏的流程
- 玩家能够通过基岩版手机客户端进入Java版网络游戏中进行游玩
- 开发者可以使用基岩版已有的模组SDK和附加包Json制作客户端的玩法逻辑和表现需要的技术与中国版基岩版组件开发相似。部分功能可能无法生效
## 前置知识
### 名词介绍
- Spigot、Spigot服
> Spigot是一个开源的、高效的Java版MC服务器并且提供了丰富的api可以编写插件。他是由第三方提供的并非官方提供是由官方的minecraft server反编译后打进去一些patch后而来
- BC、BungeeCord、BC服
> BungeeCord后面简称BC是一个代理提供了切服、数据包转发、插件编写、BC指令等等
- Geyser、Geyser服、协议服
> Geyser是第三方开源的MC协议转换工具它能把MC-Bedrock版本协议转换为MC-java版本协议也就是说通过Geyser我们能实现基岩版客户端进入Java版服务器游玩的效果
- Master服、Proxy服
> 见[名词解释](../课程2Apollo基础知识/第2节Apollo框架.md)
## 框架示例和讲解
![框架图](./res/frame.png)
如框架图所示为了尽可能地兼容不同服主Spigot服框架我们把整体的Java版网络游戏分为两部分
- 负载/协议转换部分(上半部分)
- Java服部分(下半部分)
接着阐述每个部分的具体作用
- 负载/协议转换部分
- 这一部分包括了Master服、Proxy服、Geyser服
- Master和Proxy服作用和基岩版网络游戏一致
- 由于基岩版和Java版的游戏数据协议包格式并不相同因此Geyser服则扮演着协议转换者的角色它负责把基岩版的协议转换为Java版的协议
- Java服部分
- 这一部分为Java游戏服它的作用相当于基岩版网络游戏中Lobby服和Game服的结合体是存储玩家实际游玩数据以及世界数据的服务器
- 它主要由两部分组成BC服和Spigot服
- 架构图中的DB则指额外数据库可选、其他则是指用于进一步管理BC、Spigot或者其他更多功能的第三方应用程序可选
为了可扩展地连接起上下两部分框架中我们会把Java服视为黑盒并为BC服提供名为**BungeeMaster**的插件这个插件安装在BC服上负责建立起BC服和Master服的通信连接。
## 搭建流程
### 准备阶段
在进行Java版网络游戏搭建和部署流程前首先需要注册成为我的世界开发者并完成入驻申请申请开发阶段服务器。具体步骤请详细阅读如下文档
1. [开发准备阶段概述](../课程1成为Apollo服主及相关准备/第2节开发准备阶段概述.md)
2. [入驻申请](../课程1成为Apollo服主及相关准备/第3节入驻申请.md)
3. [申请开发阶段服务器](../课程1成为Apollo服主及相关准备/第4节申请开发阶段服务器.md)
4. [使用数据库前端连接数据库](../课程1成为Apollo服主及相关准备/第5节拓展使用数据库前端连接数据库.md)
5. [连接开发机](../课程1成为Apollo服主及相关准备/第5节连接开发机.md)
### 特别提示
1. 拿到机器后,要先访问添加白名单,之后才可以正常登入
2. spigot机器的java环境对应的操作命令分别是**java8**和**java18**其中运行Spigot请使用java8(具体原因详见[常见问题合集](./90-常见问题合集.md))
### JAVA服部署流程
- 在准备好开发机后,连接上开发机
- 通过服主各自的Java服框架部署各自的Java服(必须存在BC服)
- 经过初步的调研确定目前使用的Spigot版本为1.12.2因此为了兼容基岩版1.18客户端,需要在**Spigot服**使用**ViaVersion**插件
- 下载[**ViaVersion插件**](https://www.spigotmc.org/resources/viaversion.19254/),并放置于 **Spigot服目录/plugins/** 目录下。
- 下载[**BungeeMaster插件**](./99-下载内容.md#bungeemaster插件),并放置于 **BC服目录/plugins/** 目录下。
-**BC服目录/plugins/** 下,新建文件夹 **BungeeMaster**,并在 **BungeeMaster** 文件夹下新建文件 **config.yml**此为BungeeMaster插件配置
![目录结构1](./res/bc01.png)
![目录结构2](./res/bc02.png)
![目录结构3](./res/bc03.png)
- 然后填入如下内容,其中
- server_id 取值区间在[12000, 14000)内,并且保持单次部署唯一即可
- master_port端口范围要求[29000,31000)
- 填写完毕后请记下相关参数后面需要在studio中填写
![config.yml](./res/bc04.png)
- 打开**BC服目录/config.yml**文件(此为bc服配置)查看bc服监听的端口并记下端口参数后面需要在studio中填写
- 注意,此处的**query_port**和**host中冒号后面的端口值**,请保持一致
- 该处的端口,均为**BC服**用于监听来自**Geyser**服相关连接的端口
![config.yml](./res/bc05.png)
- 配置完成后可使用Java版客户端连接BC测试Java服部署是否成功
### 负载/协议转换部分部署流程
- 打开MCStudio选择基岩版服务器并选择右上角新建选项打开后选择空白Spigot服
![Studio部署流程](./res/studio1.png)
- 选择更多后,点击服务器配置,开始配置
![Studio部署配置](./res/studio2.png)
- 其中,控制服、代理服、协议服的配置不再赘述,和基岩版网络服务器相似
![框架上半部分部署](./res/studio3.png)
- BC服的配置则根据Java服部署中BC服的数量和配置来决定。如根据上个步骤截图数据则最终配置为
![框架上半部分部署](./res/studio4.png)
- 配置完成后,点击部署
- 查看日志若部署成功则可以通过工具箱打开ModPC开发包进入游戏开始测试
![框架上半部分部署](./res/studio5.png)
## 常见部署问题合集
[常见部署问题合集](./90-常见问题合集.md)
## 常见问题
Q如何申请使用Spigot开服
A在开发者平台提交入驻申请选择作品管理-上架与资源管理-网络游戏-基岩版开服工具游戏-入驻申请使用框架勾选spigot认真填写入驻申请。
Q服务端可以使用Spigot插件么
A理论上可以使用来自Spigot插件市场的插件目前已有一批广泛使用的插件经过了测试。
Q我什么时候可以上架使用Spigot的基岩版服务器
A目前暂时无法上架此类游戏您可以先进行服务器的开发我们会在近期支持上架操作。
Q我需要使用哪个版本的Spigot是否支持Paper-Spigot
A使用的是1.12版本的Spigot暂时不支持Paper未来会考虑支持。
Q是否支持手机和电脑同服
A目前仅支持手机端如果您需要双端同服可以选择使用Apollo进行开服。
Q使用的客户端是什么支持forge么
A使用的客户端是中国版的《我的世界》基岩版目前是1.18很遗憾基岩版客户端不支持forge推荐使用官方推出的模组SDK替代。
Q我是否可以将Spigot PC服移植到PE
A可以我们欢迎您这么做。但是对于客户端逻辑来说您需要重写逻辑。

View File

@@ -0,0 +1,67 @@
---
front:
hard: 入门
time: 60分钟
---
# Spigot服务器开发规范
## Spigot服部署规范
- **<span style="font-size: 25px; color: red;">仅支持spigot官方BuildTools编译出的1.12.2版本不可以使用魔改过的spigot</span>**
- [官方BuildTools wiki](https://www.spigotmc.org/wiki/buildtools/)
## Spigot服 server.properties参数规范
- view-distance
```
建议值: 4-6
```
决定服务端发送的区块信息数据,对服务器性能与流量消耗影响巨大,建议调小。
- online-mode
```
必须值: false
```
决定Spigot服是否进行正版验证。在Apollo中登录认证由proxy处理spigot不需要再次验证。
- allow-flight
```
必须值: true
```
决定Spigot是否进行飞行检测。由于目前基岩版登录Java服务器需要经过Geyser服翻译协议在这过程中玩家移动相关包体不能完美翻译若这个字段设为false存在玩家移动被服务器当作飞行封禁的可能性。
- network-compression-threshold
```
必须值:-1
```
决定spigot与bc通信协议的压缩等级。因为spigot与bc在同一个内网流量不需要成本因此不压缩网络协议节省性能
## BungeeCord服 config.yml参数规范
- connection_throttle_limit
```
建议值: 100
```
BC服允许来自同一个ip同时登录的玩家数。因为apollo中玩家登录都来自协议服所以需要调大这个值。
- online-mode
```
必须值: false
```
决定BC服是否进行正版验证在Apollo中登录认证由proxy处理bc不需要再次验证。
- network_compression_threshold
```
必须值:-1
```
决定bc与协议服通信协议的压缩等级。因为bc与协议服在同一个内网流量不需要成本因此不压缩网络协议节省性能

View File

@@ -0,0 +1,120 @@
---
front:
hard: 入门
time: 60分钟
---
# Spigot服务器Mod简介
## 名词释义
- **SpigotMaster插件**
为实现Spigot服务端和客户端进行自定义事件通信实现的Spigot服务器收发信息的官方插件
- **客户端Mod**
客户端通过**ModSdk**接口实现的,仅包含客户端逻辑的自定义模组
- **Spigot插件**
服主基于**SpigotMaster插件**、Spigot原生插件接口实现的Spigot插件
[Spigot原生插件开发文档](https://www.spigotmc.org/wiki/spigot-plugin-development/)
- **游戏服Mod**
指服主实现的玩法mod它一般包括**客户端Mod**和**Spigot插件**
## 游戏服Mod的组成结构
目前Spigot的Mod主要由两部分组成
- Spigot插件服主自行实现逻辑并修改Spigot配置加载对应的插件用以实现服务端相关逻辑
- 客户端Mod通过目前提供的ModSdk客户端接口编写客户端相关逻辑用以实现客户端相关逻辑主要包含
- behavior_packs用来控制客户端行为逻辑
- resource_packs存放客户端资源
## 客户端Mod的目录结构样例
我们以demoMod为示例介绍游戏服Mod目录结构
demoMod
behavior_packs
behavior_pack_geyser_demo_mod
geyserDemoMod
geyserDemoModScript
modClient
modCommon
modMain.py
__init__.py
entities
pack_manifest.json
resource_packs
resource_pack_geyser_demo_mod
pack_manifest.json
| 文件/文件夹 | 解释 |
| ------------------------- | ------------------------------------------------------------ |
| demoMod | 游戏服Mod根目录 |
| behavior_packs | 存放客户端行为包,可以包含多个行为包 |
| behavior_pack_geyser_demo_mod | 行为包 |
| geyserDemoMod | python脚本的根目录该目录的脚本会被加到python运行环境下可以从该路径开始import脚本文件例如from geyserDemoMod import modMain |
| modClient | pytho客户端行为逻辑包含GeyserDemoModClientSystem|
| modCommon | 通用数据包含Modname、自定义事件等定义 |
| resource_packs | 存放客户端资源,可以包含多个资源包 |
| resource_pack_geyser_demo_mod | 资源包 |
| pack_manifest.json | mod资源版本信息 |
更多模组SDK内容详见:
<a href="../../20-玩法开发/13-模组SDK编程/2-Python脚本开发/0-脚本开发入门.html#modmain-py是什么" target="_blank">mod开发简介</a>
### Spigot插件目录结构样例
![结构](./res/spigotPlugin/plugin1.png)
listen_plugin
main
java\com\netease
command
ServerCommand.java
ToClientCommand.java
App.java
ClientListener.java
GlobalVar.java
ServerOriginalListen.java
resouces
plugin.yml
| 文件/文件夹 | 解释 |
| ------------------------- | ------------------------------------------------------------ |
| java\com\netease | 插件逻辑代码目录 |
| command | 指令处理类目录 |
| App.java | 插件入口 |
| ClientListener.java | 客户端事件监听处理函数 |
| GlobalVar | 通用数据储存类 |
| ServerOriginalListen | Spigot原生事件监听处理函数 |
| resouces | 插件资源版本信息 |
| plugin.yml | mod资源版本信息 |
## 客户端Mod上传加载流程
- 如图所示
- mod目录结构
![上传1](./res/spigotPlugin/plugin16.png)
- studio配置
![上传](./res/spigotPlugin/plugin10.png)
- Geyser读取客户端Mod后会有如下输出
![加载成功1](./res/spigotPlugin/plugin13.png)
## Spigot插件加载流程
- 如图所示把mvn clean install编译出的xxx.jar放入Spigot的Plugin文件夹
![上传](./res/spigotPlugin/plugin11.png)
- Spigot加载插件后会有输出具体命名由plugin.yml决定
![加载成功2](./res/spigotPlugin/plugin12.png)
## DemoMod样例详细刨析
- 游戏服Mod相关的开发和样例请参见如下示例
[游戏服Mod详解](./21-Spigot服与客户端python通信.md)

View File

@@ -0,0 +1,191 @@
# Spigot服与客户端python通信
## 使用方法
1. spigot服需要安装SpigotMaster插件插件api文档见[SpigotMasterAPI文档](./81-SpigotMasterAPI文档.html)
2. 客户端到spigot
- 在spigot使用spigotMaster.listenForEvent监听事件。
- 在客户端使用<a href="../../../mcdocs/1-ModAPI/接口/通用/事件.html#notifytoserver" rel="noopenner">NotifyToServer</a>发送事件
3. spigot到客户端
- 在客户端使用<a href="../../../mcdocs/1-ModAPI/接口/通用/事件.html#listenforevent" rel="noopenner">ListenForEvent</a>监听事件
- 在spigot使用spigotMaster.notifyToClient或其他多播接口发送事件
示例:
- spigot侧
```java
public void onEnable() {
SpigotMaster spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
if (spigotMaster != null){
// 监听事件,然后原封不动发回去
spigotMaster.listenForEvent("MyMod", "MySystemClient", "clientEvent", new PyRpcHandler() {
@Override
public void onEvent(Player player, Map<String, Object> map) {
spigotMaster.notifyToClient(player, "MyMod", "MySystemServer", "serverEvent", map);
}
});
}
}
```
- python侧
```python
# modMain.py
@Mod.InitClient()
def InitClient(self):
clientApi.RegisterSystem("MyMod", "MySystemClient", client_system_class_path)
# clientSystem
class MySystemClient(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
# 注册事件,在回调函数中打印参数
self.ListenForEvent("MyMod", "MySystemServer", "serverEvent", self, self.onEvent)
# 给spigot发一个事件
self.NotifyToServer("clientEvent", {'a': 1})
def onEvent(self, data):
# 可以在客户端日志中看到onEvent {"a": 1}
print 'onEvent', data
```
## 事件支持的参数类型及映射关系
### Java发送给Python
| Java类型 | Python类型 |
| ------------------------ | ---------- |
| null | None |
| boolean | bool |
| int | int |
| long | long |
| BigInteger(2^63到2^64-1) | long |
| float | float |
| double | float |
| String | str |
| List\<Object\> | list |
| Map<String, Object> | dict |
### Python发送给Java
| Python类型 | Java类型 |
| ---------------------------------------- | ---------- |
| None | null |
| bool | Boolean |
| int/long-2^31到2^31-1 | Integer |
| int/long-2^63到-2^31-12^31到2^63-1 | Long |
| int/long2^63到2^64-1 | BigInteger |
| float | Double |
| str | String |
| list | List\<Object\> |
| dictkey必须为str | Map<String, Object> |
## 关于entityId的注意事项
- 客户端侧的**非玩家实体**的entityId与spigot侧org.bukkit.entity.Entity.getEntityId()获取的实体id相同
- 请注意spigot获取的实体id类型为**int**而客户端modsdk接口需要的实体id类型为**str**
- 但客户端侧会存在一些负数的实体id会geyser做协议转换时生成的虚拟实体在spigot侧没有对应的实体
- 在**每个客户端**视角来看,**本地玩家的entityId永远为-2**其他玩家的entityId与spigot侧getEntityId相同也就是说
- 客户端使用GetLocalPlayerId永远返回-2。如果将他发给spigot那spigot是不能直接根据这个id获取到玩家的需要做一些特殊处理
```java
spigotMaster.listenForEvent("MyMod", "MySystemClient", "clientEvent", new PyRpcHandler() {
@Override
public void onEvent(Player player, Map<String, Object> map) {
Player eventPlayer;
String entityId = (String) map.get("entityId");
if (entityId.equals("-2")) {
eventPlayer = player;
}
else {
// 将entityId转成int然后去获取对应player
}
// 处理eventPlayer的逻辑
}
});
```
- 如果在spigot侧使用getEntityId的返回值发给该玩家那玩家客户端无法根据这个id获取到本地玩家需要做一些特殊处理
```java
int entityId;
if (sendPlayer == player) {
entityId = -2;
}
else {
entityId = player.getEntityId()
}
map.put("entityId", entityId)
spigotMaster.notifyToClient(sendPlayer, "MyMod", "MySystemServer", "serverEvent", map);
```
## DEMO详解
[示例Demo](./99-下载内容.html#示例demo)中的PyRpcDemo包含了客户端mod及spigot插件。
进入游戏后会在右侧显示3个按钮
* 点击“打开窗口”会弹出一个UI再点击“获取随机数”会从spigot获取一个0-9的随机数并显示在ui上。点击x关闭
* 点击“绑定特效”会通知当前world内所有玩家给发起玩家替换模型并挂接一个特效
* 点击”广播消息“会在spigot内所有玩家的聊天栏显示一条消息
### 客户端部分
1. 在客户端初始化时注册UiInitFinished事件并在UiInitFinished事件中创建三个按钮的ui注册后续将要使用的弹出窗口ui
注册两个自定义事件:
- bindEffect给entityId参数对应实体更换模型以及创建特效
- showMsg在本地显示聊天栏消息
![image-20221011175949410](./res/spigotPlugin/plugin17.png)
![image-20221011181717291](./res/spigotPlugin/plugin21.png)
2. 在为三个按钮注册回调函数
”打开窗口“按钮抬起时弹出随机数的ui
”绑定特效“和”广播消息“按钮抬起时给spigot发送自定义消息
![image-20221011180653132](./res/spigotPlugin/plugin18.png)
3. 在随机数的ui创建时监听获取随机数的回调事件将参数中的值显示到label控件上
注册按钮回调”获取随机数“按钮抬起时向spigot发送一个自定义消息关闭按钮抬起时弹出界面
界面销毁时反监听获取随机数的回调事件
![image-20221011180914374](./res/spigotPlugin/plugin19.png)
### spigot部分
- 初始化时注册自定义事件,分别为:
- requestRandom给玩家返回随机数获取回调
- requestBindEffect给本人返回-2的entityId给world内其他人返回spigot的entityId
- requestMsg给spigot内所有人返回消息
![image-20221011181216699](./res/spigotPlugin/plugin20.png)

View File

@@ -0,0 +1,186 @@
---
front:
hard: 入门
time: 60分钟
---
# Spigot自定义物品
## 使用方法
1. spigot服需要安装SpigotMaster插件插件api文档见[SpigotMasterAPI文档](./81-SpigotMasterAPI文档.html)
2. 客户端Mod编写json、python逻辑详见[自定义物品](../../20-玩法开发/15-自定义游戏内容/1-自定义物品/1-自定义基础物品.html)
3. spigot构造物品、发放给玩家并编写物品具体使用逻辑
## 注意事项
- 目前版本中的自定义物品在Java服务端实际上是木剑的换皮物品基岩客户端通过物品不同的命名空间显示不同的贴图icon
- 目前的局限性:
- 由于目前Mod由Geyser进行分发、预加载因此通过相同Geyser连接的玩家加载的Mod都相同加载到的自定义物品也相同
- 由于配置中的**Components**有一部分为双端逻辑,因此此类**Components**的逻辑需要由Spigot插件实现直接在Json中配置无法生效或效果异常。目前已知<font color="red" size=4>不可用</font>**Components**如下:
- 基岩版自定义物品中用于物品防火的组件
```
设置物品是否防火
"netease:fire_resistant"{ "value" : true}
```
- 基岩版自定义物品中用于物品是否可做燃料的组件
```
设置物品是否可作为燃料
"netease:fuel" { "value" : true}
```
- 基岩版自定义物品中用于物品的使用间隔
```
设置物品使用间隔
"netease:cooldown" : { "duration" : 5}
```
- 基岩版自定义物品中用于物品武器属性的组件
- 当**type**值为sword时能达到客户端玩家模型手持物品的效果
- 其他**type**类型以及字段暂时不可用
```
设置物品作为物品时的各类属性
"netease:weapon" : { "type" : "pickaxe", "level" : 3}
```
- 由于目前物品在Spigot服中实际上为木剑换皮下述组件为固定值
```
**"minecraft:max_damage" : 59**
```
- 当使用这个组件时,客户端可显示出自定义物品耐久条
- 当值为**非59**时,会导致客户端物品耐久异常
- 当值为 **59**时,客户端耐久条会随服务端设置的物品耐久变化而变化
- 当不使用这个组件时,客户端不显示出自定义物品耐久条
## Demo详解
Demo实现了三个不同的自定义物品
- 爆炸头盔
当鼠标右键方块时方块所在位置发生半径为1.5格的爆炸
- Amiibo之剑
当鼠标右键方块时,方块所在位置出现落雷效果,并随机生成物品,必定掉落**落雷剑**
- 落雷剑
当鼠标右键方块时,方块所在位置生成落雷,效果和打雷效果一致,对附近生物造成伤害
### 开发流程
#### 客户端Mod编写
##### 目的
为了让Geyser能加载新增的自定义物品我们需要编写客户端Mod
##### 流程
- 在behavior文件夹中新建**netease_item_beh**目录,如下图所示
![示例1](./res/spigotCustomItem/customItem1.png)
- 在**netease_item_beh**目录下新增三个物品Json样例如下
![示例2](./res/spigotCustomItem/customItem2.png)
- 在**resource**文件夹中新建**netease_item_res**、**texts**、**textures**目录,如下图所示
这三个目录的作用分别如下:
- **netease_item_res**用于存放自定义物品客户端贴图表现Json
- **texts**用于存放自定义内容的中文命名
- **textures**用于存放自定义内容的贴图文件
![示例3](./res/spigotCustomItem/customItem3.png)
- 在**netease_item_res**目录下新增三个物品Json样例如下
样例中,我们通过**minecraft:icon**这个**Component**为物品设置贴图
![示例4](./res/spigotCustomItem/customItem4.png)
- 在**texts**目录下新增**zh_CN.lang**文件,设置自定义物品的名称,样例如下:
![示例5](./res/spigotCustomItem/customItem5.png)
- 在**textures**目录下新增**item_texture.json**文件,设置物品贴图对应的贴图路径,样例如下:
![示例6](./res/spigotCustomItem/customItem6.png)
![示例7](./res/spigotCustomItem/customItem7.png)
- 至此客户端Mod中自定义物品完成
#### Spigot插件编写
##### 目的
为了让自定义物品能有不同的效果、功能我们还需要编写Spigot插件实现不同物品的不同效果逻辑
##### 流程
- 插件编写
1. 在plugin.yml中添加依赖
```yml
depend:
- SpigotMaster
```
2. 如App.java所示实例ServerOriginalListener监听了Spigot原生事件
通过下述接口实现
```
getServer().getPluginManager().registerEvents(new ServerOriginalListener(), this);
```
3. 实例**ServerOriginalListener**监听了Spigot原生的事件**ServerOriginalListener**监听方法如图:
示例中,**ServerOriginalListener**一共监听了两个事件,如下:
- 玩家加入事件(PlayerJoinEvent), 效果为玩家加入游戏时,给玩家自动发放两个物品
- 玩家交互事件(PlayerInteractEvent),效果为玩家和方块右键交互时,根据不同自定义物品,触发不同的效果
![示例8](./res/spigotCustomItem/customItem8.png)
4. 创建物品
- 通过Spigot提供的Api创建物品ItemStack
```
@Param material 物品material(自定义物品的material通过下一步获取)
@Param amount 物品数量
ItemStack customItem1 = new ItemStack(material, amount);
```
- 在创建ItemStack时通过下述接口获取自定义物品中的Material
```
@Param itemIdentifier 自定义物品Identifier,需要和客户端Mod中定义一致
@Return Material 自定义物品在Spigot中对应的Material
SpigotMaster.getCustomItemMaterial(itemIdentifier)
```
- 同时通过下述接口为物品设置基岩版中对应的Identifier
**PS: 这一步为必须步骤,缺少这一步时,会导致客户端无法正常生成自定义物品**
```
@Param itemStack 通过Spigot接口生成的ItemStack
@Param itemIdentifier 自定义物品Identifier,需要和客户端Mod中定义一致
SpigotMaster.setCustomItemIdentifier(itemStack, itenIdentifier)
```
- 根据需要再进一步修改ItemStack的其他属性如样例中的lore
5. 使用物品
- 通过事件参数获取ItemStack
```
ItemStack item = event.getItem();
```
- 通过接口获取物品的Material:
```
@Param itemIdentifier 自定义物品Identifier,需要和客户端Mod中定义一致
@Return Material 自定义物品在Spigot中对应的Material
SpigotMaster.getCustomItemMaterial(itemIdentifier)
```
- 当事件物品的Material和自定义物品的Material一致时判定为使用自定义物品具体物品效果、逻辑还需进一步区分
- 通过接口获取物品的Identifier:
```
@Param itemStack 通过Spigot接口生成的ItemStack
@Return String itemIdentifier非自定义物品返回null
SpigotMaster.getCustomItemIdentifier(itemStack)
```
根据获取到的Identifier实现不同的逻辑,如图所示
![示例9](./res/spigotCustomItem/customItem9.png)
6.运行mvn clean install会在插件target下生成插件.jar,把生成的jar放置于Spigot服的plugin文件夹下
7.最终实现的效果如下:
- 玩家进入游戏即可获取两个自定义物品
![示例10](./res/spigotCustomItem/customItem10.png)
- 使用自定义物品1时发生爆炸
- 使用自定义物品2时有打雷效果并生成物品其中物品必掉落自定义物品3
- 使用自定义物品3时生成闪电

View File

@@ -0,0 +1,205 @@
---
front:
hard: 入门
time: 60分钟
---
# Spigot自定义物品Demo详解
## 前情提要
[Spigot服务器Mod简介](./20-Spigot服务器Mod简介.md)
## 简要原理
- 客户端Mod实现流程可以参照以下文档
[自定义物品](../../20-玩法开发/15-自定义游戏内容/1-自定义物品/1-自定义基础物品.md)
- 目前版本中的自定义物品在Java服务端实际上是木剑的换皮物品基岩客户端通过物品不同的命名空间显示不同的贴图icon
### Demo效果
Demo实现了三个不同的自定义物品
- 爆炸头盔
当鼠标右键方块时方块所在位置发生半径为1.5格的爆炸
- Amiibo之剑
当鼠标右键方块时,方块所在位置出现落雷效果,并随机生成物品,必定掉落**落雷剑**
- 落雷剑
当鼠标右键方块时,方块所在位置生成落雷,效果和打雷效果一致,对附近生物造成伤害
### 开发流程
#### 客户端Mod编写
##### 目的
为了让Geyser能加载新增的自定义物品我们需要编写客户端Mod
##### 流程
- 在behavior文件夹中新建**netease_item_beh**目录,如下图所示
![示例1](./res/spigotCustomItem/customItem1.png)
- 在**netease_item_beh**目录下新增三个物品Json样例如下
![示例2](./res/spigotCustomItem/customItem2.png)
- 在**resource**文件夹中新建**netease_item_res**、**texts**、**textures**目录,如下图所示
![示例3](./res/spigotCustomItem/customItem3.png)
这三个目录的作用分别如下:
- **netease_item_res**用于存放自定义物品客户端贴图表现Json
- **texts**用于存放自定义内容的中文命名
- **textures**用于存放自定义内容的贴图文件
- 在**netease_item_res**目录下新增三个物品Json样例如下
样例中,我们通过**minecraft:icon**这个**Component**为物品设置贴图
![示例4](./res/spigotCustomItem/customItem4.png)
- 在**texts**目录下新增**zh_CN.lang**文件,设置自定义物品的名称,样例如下:
![示例5](./res/spigotCustomItem/customItem5.png)
- 在**textures**目录下新增**item_texture.json**文件,设置物品贴图对应的贴图路径,样例如下:
![示例6](./res/spigotCustomItem/customItem6.png)
![示例7](./res/spigotCustomItem/customItem7.png)
- 至此客户端Mod中自定义物品完成
#### Spigot插件编写
##### 目的
为了让自定义物品能有不同的效果、功能我们还需要编写Spigot插件实现不同物品的不同效果逻辑
##### 流程
- 安装插件到本地maven
下载 **SpigotMaster** 的jar包然后执行以下指令**path-to-jar**替换为jar路径
```shell
mvn install:install-file -Dfile=path-to-jar
-DgroupId=com.neteasemc
-DartifactId=SpigotMaster
-Dversion=1.0-SNAPSHOT
-Dpackaging=jar
```
- 插件编写
1. 在pom.xml中添加依赖
```xml
<dependency>
<groupId>com.neteasemc</groupId>
<artifactId>SpigotMaster</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
```
2. 在plugin.yml中添加依赖
```yml
depend:
- SpigotMaster
```
3. 如App.java所示实例ServerOriginalListener监听了Spigot原生事件
通过下述接口实现
```
getServer().getPluginManager().registerEvents(new ServerOriginalListener(), this);
```
4. 实例**ServerOriginalListener**监听了Spigot原生的事件**ServerOriginalListener**监听方法如图:
示例中,**ServerOriginalListener**一共监听了两个事件,如下:
- 玩家加入事件(PlayerJoinEvent), 效果为玩家加入游戏时,给玩家自动发放两个物品
- 玩家交互事件(PlayerInteractEvent),效果为玩家和方块右键交互时,根据不同自定义物品,触发不同的效果
![示例8](./res/spigotCustomItem/customItem8.png)
5. 创建物品
- 通过Spigot提供的Api创建物品ItemStack
```
@Param material 物品material(自定义物品的material通过下一步获取)
@Param amount 物品数量
ItemStack customItem1 = new ItemStack(material, amount);
```
- 在创建ItemStack时通过下述接口获取自定义物品中的Material
```
@Param itemIdentifier 自定义物品Identifier,需要和客户端Mod中定义一致
@Return Material 自定义物品在Spigot中对应的Material
SpigotMaster.getCustomItemMaterial(itemIdentifier)
```
- 同时通过下述接口为物品设置基岩版中对应的Identifier
**PS: 这一步为必须步骤,缺少这一步时,会导致客户端无法正常生成自定义物品**
```
@Param itemStack 通过Spigot接口生成的ItemStack
@Param itemIdentifier 自定义物品Identifier,需要和客户端Mod中定义一致
SpigotMaster.setCustomItemIdentifier(itemStack, itenIdentifier)
```
- 根据需要再进一步修改ItemStack的其他属性如样例中的lore
6. 使用物品
- 通过事件参数获取ItemStack
```
ItemStack item = event.getItem();
```
- 通过接口获取物品的Material:
```
@Param itemIdentifier 自定义物品Identifier,需要和客户端Mod中定义一致
@Return Material 自定义物品在Spigot中对应的Material
SpigotMaster.getCustomItemMaterial(itemIdentifier)
```
- 当事件物品的Material和自定义物品的Material一致时判定为使用自定义物品具体物品效果、逻辑还需进一步区分
- 通过接口获取物品的Identifier:
```
@Param itemStack 通过Spigot接口生成的ItemStack
@Return String itemIdentifier非自定义物品返回null
SpigotMaster.getCustomItemIdentifier(itemStack)
```
根据获取到的Identifier实现不同的逻辑,如图所示
![示例9](./res/spigotCustomItem/customItem9.png)
7.运行mvn clean install会在插件target下生成插件.jar,把生成的jar放置于Spigot服的plugin文件夹下
8.最终实现的效果如下:
- 玩家进入游戏即可获取两个自定义物品
![示例10](./res/spigotCustomItem/customItem10.png)
- 使用自定义物品1时发生爆炸
- 使用自定义物品2时有打雷效果并生成物品其中物品必掉落自定义物品3
- 使用自定义物品3时生成闪电
### Q&A
- **SpigotMaster插件**简要API文档
[SpigotMaster插件API文档](./80-SpigotMaster简要API文档.md)
### **需要注意的是**
- 目前的局限性:
- 由于目前Mod由Geyser进行分发、预加载因此通过相同Geyser连接的玩家加载的Mod都相同加载到的自定义物品也相同
- 由于配置中的**Components**有一部分为双端逻辑,因此此类**Components**的逻辑需要由Spigot插件实现直接在Json中配置无法生效或效果异常。目前已知不可用**Component**如下:
- 基岩版自定义物品中用于物品防火的组件
```
设置物品是否防火
"netease:fire_resistant"{ "value" : true}
```
- 基岩版自定义物品中用于物品是否可做燃料的组件
```
设置物品是否可作为燃料
"netease:fuel" { "value" : true}
```
- 基岩版自定义物品中用于物品的使用间隔
```
设置物品使用间隔
"netease:cooldown" : { "duration" : 5}
```
- 基岩版自定义物品中用于物品武器属性的组件
```
设置物品作为物品时的各类属性
"netease:weapon" : { "type" : "pickaxe", "level" : 3}
```
- 由于目前物品在Spigot服中实际上为木剑换皮下述组件为固定值
```
**"minecraft:max_damage" : 59**
```
- 当使用这个组件时,客户端可显示出自定义物品耐久条
- 当值为**非59**时,会导致客户端物品耐久异常
- 当值为 **59**时,客户端耐久条会随服务端设置的物品耐久变化而变化
- 当不使用这个组件时,客户端不显示出自定义物品耐久条

View File

@@ -0,0 +1,100 @@
# SpigotMaster简要API文档
- 注册与发送客户端事件
spigot侧
- map中的值支持的数据类型
| Java类型 | Python类型 |
| ----------------------- | ----------------- |
| null | None |
| boolean | bool |
| int | int |
| long | long |
| BigInteger(2^63~2^64-1) | long(2^63~2^64-1) |
| float | float |
| double | float |
| String | str |
| List\<Object\> | list |
| Map<String, Object> | dict |
- 客户端侧的实体id与spigot侧org.bukkit.entity.Entity.getEntityId()获取的实体id相同
- 请注意spigot获取的实体id类型为**int**而客户端modsdk接口需要的实体id类型为**str**
- 但客户端侧会存在一些负数的实体id会geyser做协议转换时生成的虚拟实体在spigot侧没有对应的实体
示例:
```java
public void onEnable() {
SpigotMaster spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
if (spigotMaster != null){
spigotMaster.listenForEvent("MyMod", "MySystemClient", "clientEvent", new PyRpcHandler() {
@Override
public void onEvent(Player player, Map<String, Object> map) {
spigotMaster.notifyToClient(player, "MyMod", "MySystemServer", "serverEvent", map);
}
});
}
}
```
客户端侧:
```python
# modMain.py
@Mod.InitClient()
def InitClient(self):
clientApi.RegisterSystem("MyMod", "MySystemClient", client_system_class_path)
# clientSystem
class MySystemClient(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ListenForEvent("MyMod", "MySystemServer", "serverEvent", self, self.onEvent)
self.NotifyToServer("clientEvent", {'a': 1})
def onEvent(self, data):
print 'onEvent', data
```
## API
class SpigotMaster
- listenForEvent
监听客户端事件
- notifyToClient
发事件到指定player
- notifyToMultiClients
发事件到多个player
- notifyToClientsNearby
发事件到位置附近的player
- broadcastToAllClient
广播事件到server中所有player
- getCustomItemIdentifier(itemStack)
获取自定义物品identifier
@Param ItemStack itemStack spigot生成的itemStack
@Return String customItemIdentifier 当物品为自定义物品时返回自定义物品的identifier其他情况返回null
- setCustomItemIdentifier(itemStack, customItemIdentifier)
设置自定义物品identifier
@Param itemStack spigot生成的itemStack
@Param String customItemIdentifier 自定义物品的identifier
- getCustomItemMaterial(customItemIdentifier)
获取自定义物品identifier
@Param String customItemIdentifier 自定义物品的identifier
@Return Mateiral 目前版本V1.1.0版本中固定返回Material.WOOD_SWORD(亦即木剑换皮)

View File

@@ -0,0 +1,312 @@
# SpigotMasterAPI文档
com.neteasemc.spigotmaster
## 类 SpigotMaster
- java.lang.Object
- org.bukkit.plugin.PluginBase
- org.bukkit.plugin.java.JavaPlugin
- com.neteasemc.spigotmaster.SpigotMaster
- 所有已实现的接口:
org.bukkit.command.CommandExecutor, org.bukkit.command.TabCompleter, org.bukkit.command.TabExecutor, org.bukkit.plugin.Plugin
------
```
public class SpigotMaster
extends org.bukkit.plugin.java.JavaPlugin
```
- ### 构造器概要
| 构造器和说明 |
| :--------------- |
| `SpigotMaster()` |
- ### 方法概要
| 限定符和类型 | 方法和说明 |
| :------------------------------- | :----------------------------------------------------------- |
| `void` | `broadcastToAllClient(org.bukkit.entity.Player except, java.lang.String namespace, java.lang.String system, java.lang.String event, java.util.Map<java.lang.String,java.lang.Object> data)`<br>给服务器内的所有玩家发送服务端事件。 |
| `void` | `broadcastToAllClient(org.bukkit.entity.Player except, org.bukkit.World world, java.lang.String namespace, java.lang.String system, java.lang.String event, java.util.Map<java.lang.String,java.lang.Object> data)`<br/>给某个world内的所有玩家发送服务端事件。 |
| `java.lang.String` | `getCustomItemIdentifier(org.bukkit.inventory.ItemStack spigotItemStack)`<br/>获取自定义物品名的identifier |
| `org.bukkit.Material` | `getCustomItemMaterial(java.lang.String customItemIdentifier)`<br/>获取自定义物品名的java材质 在目前的实现中固定是Material.WOOD_SWORD,亦即木剑换皮 |
| `void` | `listenForEvent(java.lang.String namespace, java.lang.String system, java.lang.String event, com.neteasemc.spigotmaster.PyRpcHandler handler)`<br/>注册客户端事件 |
| `void` | `notifyToClient(org.bukkit.entity.Player player, java.lang.String namespace, java.lang.String system, java.lang.String event, java.util.Map<java.lang.String,java.lang.Object> data)`<br/>给指定玩家发送服务端事件 |
| `void` | `notifyToClientsNearby(org.bukkit.entity.Player except, org.bukkit.Location loc, double dist, java.lang.String namespace, java.lang.String system, java.lang.String event, java.util.Map<java.lang.String,java.lang.Object> data)`<br/>给某个位置附近一定半径内的所有玩家发送服务端事件。 |
| `void` | `notifyToMultiClients(java.util.List<org.bukkit.entity.Player> players, java.lang.String namespace, java.lang.String system, java.lang.String event, java.util.Map<java.lang.String,java.lang.Object> data)`<br/>给多个玩家发送服务端事件。 |
| `void` | `onDisable()` |
| `void` | `onEnable()` |
| `org.bukkit.inventory.ItemStack` | `setCustomItemIdentifier(org.bukkit.inventory.ItemStack spigotItemStack, java.lang.String customIdentifier)`<br/>设置自定义物品的identifier |
- ### 从类继承的方法 org.bukkit.plugin.java.JavaPlugin
```
getCommand, getConfig, getDataFolder, getDefaultWorldGenerator, getDescription, getLogger, getPlugin, getPluginLoader, getProvidingPlugin, getResource, getServer, isEnabled, isNaggable, onCommand, onLoad, onTabComplete, reloadConfig, saveConfig, saveDefaultConfig, saveResource, setNaggable, toString
```
- ### 从类继承的方法 org.bukkit.plugin.PluginBase
```
equals, getName, hashCode
```
- ### 从类继承的方法 java.lang.Object
```
getClass, notify, notifyAll, wait, wait, wait
```
- ### 构造器详细资料
- #### SpigotMaster
```
public SpigotMaster()
```
- ### 方法详细资料
- #### onEnable
```
public void onEnable()
```
- 指定者:
`onEnable` 在接口中 `org.bukkit.plugin.Plugin`
- 覆盖:
`onEnable` 在类中 `org.bukkit.plugin.java.JavaPlugin`
- #### onDisable
```
public void onDisable()
```
- 指定者:
`onDisable` 在接口中 `org.bukkit.plugin.Plugin`
- 覆盖:
`onDisable` 在类中 `org.bukkit.plugin.java.JavaPlugin`
- #### getCustomItemMaterial
```
public org.bukkit.Material getCustomItemMaterial(java.lang.String customItemIdentifier)
```
获取自定义物品名的java材质 在目前的实现中固定是Material.WOOD_SWORD,亦即木剑换皮
- 参数:
`customItemIdentifier` - 自定义物品identifier
- #### getCustomItemIdentifier
```
public java.lang.String getCustomItemIdentifier(org.bukkit.inventory.ItemStack spigotItemStack)
```
获取自定义物品名的identifier
- 参数:
`spigotItemStack` - itemStack
- #### setCustomItemIdentifier
```
public org.bukkit.inventory.ItemStack setCustomItemIdentifier(org.bukkit.inventory.ItemStack spigotItemStack,
java.lang.String customIdentifier)
```
设置自定义物品的identifier
- 参数:
`spigotItemStack` - itemStack
`customIdentifier` - 自定义物品identifier
- #### listenForEvent
```
public void listenForEvent(java.lang.String namespace,
java.lang.String system,
java.lang.String event,
com.neteasemc.spigotmaster.PyRpcHandler handler)
```
注册客户端事件
- 参数:
`namespace` - 来源客户端系统的namespace
`system` - 来源客户端系统的systemName
`event` - 事件名
`handler` - 回调函数
- #### notifyToClient
```
public void notifyToClient(org.bukkit.entity.Player player,
java.lang.String namespace,
java.lang.String system,
java.lang.String event,
java.util.Map<java.lang.String,java.lang.Object> data)
```
给指定玩家发送服务端事件
- 参数:
`player` - 接收事件的玩家
`namespace` - 在客户端系统使用ListenForEvent监听的namespace
`system` - 在客户端系统使用ListenForEvent监听的systemName
`event` - 事件名
`data` - 事件参数。注意,要使用-2指代本地玩家的entityId。
- #### notifyToMultiClients
```
public void notifyToMultiClients(java.util.List<org.bukkit.entity.Player> players,
java.lang.String namespace,
java.lang.String system,
java.lang.String event,
java.util.Map<java.lang.String,java.lang.Object> data)
```
给多个玩家发送服务端事件。 因为-2的entityId对于不同玩家来说都指代本机玩家而非某个固定的实体所以不要在多播中发送这种信息。
- 参数:
`players` - 接收事件的玩家列表
`namespace` - 在客户端系统使用ListenForEvent监听的namespace
`system` - 在客户端系统使用ListenForEvent监听的systemName
`event` - 事件名
`data` - 事件参数
- #### notifyToClientsNearby
```
public void notifyToClientsNearby(org.bukkit.entity.Player except,
org.bukkit.Location loc,
double dist,
java.lang.String namespace,
java.lang.String system,
java.lang.String event,
java.util.Map<java.lang.String,java.lang.Object> data)
```
给某个位置附近一定半径内的所有玩家发送服务端事件。 因为-2的entityId对于不同玩家来说都指代本机玩家而非某个固定的实体所以不要在多播中发送这种信息。
- 参数:
`except` - 发送事件时排除掉这个玩家可以为null表示不排除
`loc` - 圆心位置
`dist` - 半径
`namespace` - 在客户端系统使用ListenForEvent监听的namespace
`system` - 在客户端系统使用ListenForEvent监听的systemName
`event` - 事件名
`data` - 事件参数
- #### broadcastToAllClient
```
public void broadcastToAllClient(org.bukkit.entity.Player except,
org.bukkit.World world,
java.lang.String namespace,
java.lang.String system,
java.lang.String event,
java.util.Map<java.lang.String,java.lang.Object> data)
```
给某个world内的所有玩家发送服务端事件。 因为-2的entityId对于不同玩家来说都指代本机玩家而非某个固定的实体所以不要在多播中发送这种信息。
- 参数:
`except` - 发送事件时排除掉这个玩家可以为null表示不排除
`world` - 所在world
`namespace` - 在客户端系统使用ListenForEvent监听的namespace
`system` - 在客户端系统使用ListenForEvent监听的systemName
`event` - 事件名
`data` - 事件参数
- #### broadcastToAllClient
```
public void broadcastToAllClient(org.bukkit.entity.Player except,
java.lang.String namespace,
java.lang.String system,
java.lang.String event,
java.util.Map<java.lang.String,java.lang.Object> data)
```
给服务器内的所有玩家发送服务端事件。 因为-2的entityId对于不同玩家来说都指代本机玩家而非某个固定的实体所以不要在多播中发送这种信息。
- 参数:
`except` - 发送事件时排除掉这个玩家可以为null表示不排除
`namespace` - 在客户端系统使用ListenForEvent监听的namespace
`system` - 在客户端系统使用ListenForEvent监听的systemName
`event` - 事件名
`data` - 事件参数

View File

@@ -0,0 +1,44 @@
---
front:
hard: 入门
time: 60分钟
---
# 常见问题合集
- 目前Mod经过Geyser加载并分发因此当前版本中通过同一个Geyser连接的玩家加载的Mod都相同如果需要实现不同bc或者spigot加载不同的Mod效果可通过配置不同的geyser连接不同的bc实现
- 由于目前Geyser和Spigot使用的java版本不同因此开发机上存在两个版本的java对应的命令分别是 **java18** 以及 **java8**因此在启动Spigot时请使用命令**java8**
![问题1-8](./res/quest8.png)
- 当部署时,开启了如下标识
![问题1-1](./res/quest2.png)
> 则在bc服和master未成功连接时登录会有如下提示
![问题1-2](./res/quest1.png)
> 当对应标识未开启时,提示如下:
![问题1-3](./res/quest3.png)
> 当出现上述提示时请查看BC服日志查看BungeeMaster插件是否正常加载以及master服是否正常连接上正常连接会有如下日志打印:
![问题1-4](./res/quest4.png)
- 使用ssh登录服务器后查看对应网络id下的geyser目录可以查看geyser的日志
![问题1-5](./res/quest5.png)
> 当geyser与proxy及master正常连接时会输出如下日志
![问题1-6](./res/quest6.png)
> 当玩家正常登录上proxy及geyser时会输出如下日志
1、当玩家登录时没有图示输出时请检查master、proxy和geyser的配置及日志检查是否有报错打印
2、当玩家登录时有图示输出但是玩家无法正常进服请检查geyser、bc、spigot相关配置及日志检查是否有报错打印
![问题1-7](./res/quest7.png)

View File

@@ -0,0 +1,36 @@
---
front:
hard: 入门
time: 分钟
---
# 下载内容
## BungeeMaster插件
[下载链接](https://g79.gdl.netease.com/BungeeMaster-1.0.2-SNAPSHOT.jar)
bc服插件必须用于bc服组网
版本1.0.2-SNAPSHOT
更新日期2022.10.18
## SpigotMaster插件
[下载地址](https://g79.gdl.netease.com/SpigotMaster-1.2-SNAPSHOT.jar)
spigot服插件可选用于客户端python通信自定义物品
版本1.2-SNAPSHOT
更新日期2022.10.18
## 示例Demo
[下载地址](https://g79.gdl.netease.com/Apollo2.0Demo01.zip)
更新日期2022.10.18

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB