loginPlayerList = new ArrayList<>(); // 已登录列表
58 |
59 | @Override
60 | public void onEnable() {
61 | plugin = this;
62 |
63 | this.getServer().getPluginManager().registerEvents(new AuthEventHandler(), this); // 注册Event
64 | }
65 |
66 | @Override
67 | public void onDisable() {
68 |
69 | }
70 |
71 | /**
72 | * 传入一个 Player
73 | * 返回 Boolean 判断该玩家是否在已登录列表内
74 | */
75 | public boolean isPlayerLogin(Player player) {
76 | return loginPlayerList.contains(player);
77 | /*
78 | * 通过 contains 方法
79 | * 如果玩家已登录这里返回 true
80 | * 反之返回 false
81 | * */
82 | }
83 |
84 | /**
85 | * 传入一个Player
86 | * 记录该玩家已经登陆
87 | */
88 | public void playerLogin(Player player) {
89 | this.loginPlayerList.add(player); // 将该玩家加入已登录玩家的List中
90 | }
91 |
92 | /**
93 | * 传入一个Player
94 | * 记录该玩家已经登出
95 | */
96 | public void playerLogout(Player player) {
97 | this.loginPlayerList.remove(player); // 将该玩家从已登录玩家的List中移除
98 | }
99 |
100 | /**
101 | * 通过这个方法获得Plugin实例
102 | */
103 | public static LoginPlugin getInstance() {
104 | return plugin;
105 | }
106 | }
107 |
108 | ```
109 |
110 | handlers/AuthEventHandler.java
111 | ```java
112 | package site.dotdream.handlers;
113 |
114 | import cn.nukkit.event.Listener;
115 |
116 | public class AuthEventHandler implements Listener {
117 | private LoginPlugin plugin = LoginPlugin.getInstance();
118 | }
119 | ```
120 |
121 | 添加Package
122 |
123 | 
124 |
125 | 命名
126 |
127 | 
128 |
129 | 创建类
130 |
131 | 
132 |
133 | 可以看到我们定义了一个列表`loginPlayerList`来记录已经登录的玩家,同时创建了`isPlayerLogin`,`playerLogin`,`playerLogout`方法,来获取是否登录,以及操作该列表
134 |
135 | 然后我们又注册了一个Event。为了代码简洁方便维护,我建立了一个Package并将它独立放到一个叫AuthEventHandler的类里。同时又在onEnable中将它注册,使其生效。:wink:
136 |
137 | 到这里插件的初步配置就完成了,接下来我们大部分操作都在这个AuthEventHandler.java中...
138 |
139 | ### 三、从玩家加入开始...
140 |
141 |
142 | 我们在制作插件的时候,有必要认真的考虑一下插件的逻辑。
143 |
144 | 以本教程将要编写的登录插件为例,想要实现一个 `登录前 --> 输入密码 --> 登录后` ,那么势必要判断:
145 | - 某玩家登录过了吗?
146 | - 没有登陆?
147 | - 那么我就 ~~禁止~~取消 你:走动,放置与破坏,伤害与被伤害,饥饿值变化,与其他玩家聊天。
148 |
149 | 为什么这里说是**取消**而不是**禁止**?
150 |
151 | 因为当玩家进行这些活动,Nukkit询问我们事先注册的Event
152 |
153 | `Nukkit:你!插件LoginPlugin,对这个动作有什么意见吗?`
154 |
155 | 没有插件的情况下,这些动作都是会被同意的,通过一个重要的方法`event.setCancelled()`
156 |
157 | 告诉Nukkit
158 |
159 | `LoginPlugin:我不同意!你要取消掉本次活动`
160 |
161 | ```
162 | 伪代码如下
163 |
164 | 如果 isPlayerLogin(玩家) 等于 否:
165 | Event设置为取消
166 | 否则:
167 | 不设置取消,即是允许
168 | ```
169 |
170 | 这些都是为了实现我们的目标而需要考虑在内的
171 |
172 | **好的,脑海中有了这些概念,我们来实际编写吧!**
173 |
174 | 向AuthEventHandler.java中加入如下代码
175 | ```java
176 | @EventHandler
177 | public void onPlayerJoin(PlayerJoinEvent event) {
178 | Player player = event.getPlayer(); // 重要方法!可以通过getPlayer来获得该动作发起的玩家
179 |
180 | /* PlayerJoinEvent 独有方法,用来设置玩家欢迎词
181 | 这里玩家刚刚加入还没有登录
182 | 我们将其设为空字符串,暂时先不欢迎
183 | */
184 | event.setJoinMessage("");
185 |
186 | plugin.getServer().getScheduler().scheduleDelayedTask(plugin, () -> {
187 | player.sendTitle("欢迎来到OOO服务器", "请先登录", 20, Integer.MAX_VALUE, 20);
188 | }, 2 * 20);
189 | }
190 | ```
191 | 这里涉及了几个方法,我们来分开说:
192 |
193 | 1. :red_circle:**event.getPlayer()** 很常用的方法,也很重要,通过它,我们可以获得这个动作(Event)的发起者。
194 | 2. :red_circle:**event.setJoinMessage()** 这是一个PlayerJoinEvent独有的方法。通过它,我们可以设置这名玩家加入服务器后,发送一条向全服广播某玩家已加入游戏的消息
195 | 3. **player.sendTitle()** 这个方法可以向玩家发送两行几乎占满全屏的字,它接受5个参数:(第一行字,第二行字,渐入的时间,显示的时间,渐出的时间),需要注意的是,Nukkit使用红石刻计算时间,也就是20刻=1秒钟。将显示的时间设为Integer变量的最大值,让其一直显示,直到取消。**同时这里还涉及一个Nukkit的BUG,如果直接在玩家加入时发送Message或者Title,玩家是不显示的,所以使用`DelayedTask`做了2秒延迟**
196 |
197 | 这样我们就对新加入的玩家发送了一个要求登录的提示。
198 |
199 | 那取消未登录玩家的放置破坏啥的在哪呢?
200 |
201 | 别急!我们这就来~
202 |
203 |
204 | ### 四、我不许你动 你就不能动
205 |
206 | 继续向AuthEventHandler.java中加入如下代码
207 |
208 | ```java
209 | @EventHandler
210 | public void onPlayerMove(PlayerMoveEvent event) {
211 | Player player = event.getPlayer();
212 |
213 | if (!plugin.isPlayerLogin(player)) {
214 | // 当返回false,则玩家处在未登录状态
215 | Location from = event.getFrom();
216 | Location to = event.getTo();
217 | if (from.getX() != to.getX() || from.getZ() != to.getZ()) {
218 | // 当移动前地点 from 与 移动后地点 to 的 X,Z 坐标有任何一项不相等
219 | event.setCancelled();
220 | }
221 | }
222 | }
223 | ```
224 |
225 | 同样的方法不再解释,我们主要来看 :red_circle:**event.getFrom()** 与 :red_circle:**event.getTo()**
226 | 这两个方法为PlayerMoveEvent特有的方法,并且都能够得到一个类型为Location的变量,区别是from为玩家移动前的位置,to为玩家移动后的位置
227 |
228 | Location类型变量包含4个值得注意的方法
229 | ```java
230 | getX() // 玩家X坐标
231 | getY() // 玩家Y坐标(高度)
232 | getZ() // 玩家Z坐标
233 | getDirectionVector() // 玩家视角
234 | ```
235 |
236 | 所以,当我们判断玩家的X,Z其中任何一个值不相等,就可以:red_circle:`event.setCancelled()`来告诉Nukkit,我不批准!这样玩家此次的移动就会被取消。
237 |
238 | 同理,大部分的Event都可以通过`setCancelled()`来取消。但是例如`PlayerQuitEvent`便不可以,因为你没有办法阻止玩家离开服务器。
239 |
240 | 在这里需要大家思考,为什么不
241 | ```java
242 | if(!to.equals(from)){
243 | event.setCancelled();
244 | }
245 | // 或者
246 | if (from.getX() != to.getX() || from.getY() != to.getY() || from.getZ() != to.getZ()){
247 | // ...
248 | }
249 | ```
250 | 这样会有什么缺点?
251 |
252 |
253 | ### 五、禁止其余动作
254 |
255 | 以下的代码禁止了:
256 | 1. 放置与破坏
257 | 2. 玩家模型碰撞
258 | 3. 玩家饥饿值变化
259 |
260 | 它们相比之前并没有什么特殊,无非是检查是否登录,判断是否取消,我直接放代码在这里
261 | ```java
262 | /**
263 | * 放置方块Event
264 | */
265 | @EventHandler
266 | public void onBlockPlace(BlockPlaceEvent event) {
267 | if (!plugin.isPlayerLogin(event.getPlayer())) {
268 | event.setCancelled();
269 | }
270 | }
271 |
272 | /**
273 | * 破坏方块Event
274 | */
275 | @EventHandler
276 | public void onBlockBreak(BlockBreakEvent event) {
277 | if (!plugin.isPlayerLogin(event.getPlayer())) {
278 | event.setCancelled();
279 | }
280 | }
281 |
282 | /**
283 | * 玩家模型碰撞Event
284 | */
285 | @EventHandler
286 | public void onPlayerInteractEvent(PlayerInteractEvent event) {
287 | if (!plugin.isPlayerLogin(event.getPlayer())) {
288 | event.setCancelled();
289 | }
290 | }
291 |
292 | /**
293 | * 玩家饥饿值变化Event
294 | */
295 | @EventHandler
296 | public void onPlayerFoodLevelChangeEvent(PlayerFoodLevelChangeEvent event) {
297 | if (!plugin.isPlayerLogin(event.getPlayer())) {
298 | event.setCancelled();
299 | }
300 | }
301 | ```
302 |
303 | 除以上几个Event外,存在2个需要大家重点看的Event
304 | 1. 玩家伤害及受伤害
305 | 2. 与其他玩家聊天
306 |
307 | 先上代码吧...
308 | ```java
309 | /**
310 | * 实体伤害Event
311 | */
312 | @EventHandler
313 | public void onEntityDamage(EntityDamageEvent event) {
314 | Player player = null; // 先定义一个Player变量
315 | if (event.getEntity() instanceof Player) {
316 | // 如果受到伤害的实体是一名玩家
317 | player = (Player) event.getEntity(); // 强制转换实体为Player类型,并赋值。
318 | } else if (event instanceof EntityDamageByEntityEvent) {
319 | // 如果本次Event属于被一个实体被另一个实体攻击的Event
320 | Entity damager = ((EntityDamageByEntityEvent) event).getDamager(); // 获得本次Event的发起者
321 | if (damager instanceof Player)
322 | // 如果发起伤害的实体是一名玩家
323 | player = (Player) damager;
324 | } else if (event instanceof EntityDamageByChildEntityEvent) {
325 | // 如果本次Event属于被一个实体被另一个实体的子实体攻击的Event
326 | Entity damager = ((EntityDamageByChildEntityEvent) event).getDamager(); // 获得本次Event的发起者
327 | if (damager instanceof Player)
328 | // 如果发起伤害的实体是一名玩家
329 | player = (Player) damager;
330 | }
331 |
332 | if (player != null && !plugin.isPlayerLogin(player)) {
333 | event.setCancelled();
334 | }
335 | }
336 | ```
337 | 这个EntityDamageEvent比较特殊,它没有 `getPlayer()` 方法,因为它是一个涵盖的很广,不仅仅是玩家的行为会调用这个Event。
338 |
339 | 例如:
340 | 1. 玩家对玩家造成伤害 (发起受到伤害的是玩家)
341 | 2. 玩家对生物造成伤害 (发起伤害的是玩家)
342 | 3. 生物对玩家造成伤害 (受到伤害的是玩家)
343 | 4. 玩家的箭对玩家造成伤害 (子实体)
344 |
345 | 这些都会调用这个方法。我们可以根据判断:发起伤害的实体:red_circle:`getDamager()`和受到伤害的实体:red_circle:`getEntity()`,只要其中有一个是未登录的玩家,就取消本次Event。
346 |
347 | 然后是:
348 | ```java
349 | /**
350 | * 玩家聊天Event
351 | */
352 | @EventHandler
353 | public void onPlayerChat(PlayerChatEvent event) {
354 | Player player = event.getPlayer();
355 | if (!plugin.isPlayerLogin(event.getPlayer())) {
356 | String message = event.getMessage(); // 获取玩家发送的内容
357 | if (message.equals("A_Bad_Password")) {
358 | /*
359 | * 这里判断是否密码正确
360 | * !但仅是为了举例,通常需要将message用md5之类的非对称加密算法加密
361 | * !然后再与之前就保存在数据库里的结果进行比对
362 | */
363 | plugin.playerLogin(player); // 将玩家加入已登录的列表(List)中
364 | player.sendTitle("", "", 0, 0, 0); // 还记得我们给玩家发了一个占满全屏的两行提示吗?在这里我们把它取消。
365 | // 另一个BUG player.clearTitle() 在这里不起作用
366 |
367 | player.sendMessage(player.getName() + ",欢迎~"); // 给这个玩家发送欢迎消息
368 | // 或者
369 | plugin.getServer().broadcastMessage("玩家:" + player.getName() + ",已加入游戏"); // 给全服的玩家发送该玩家已加入的欢迎消息
370 | } else {
371 | player.sendMessage("抱歉,密码错误!");
372 | }
373 | event.setCancelled(); // 在 if 外取消,确保无论密码正确与否,都不会发送给其他玩家看到
374 | }
375 | }
376 | ```
377 |
378 | 我们在这里编写了玩家聊天与输入密码的逻辑
379 | 先判断玩家是否已登录,然后通过:red_circle:`event.getMessage()`获得消息,实际上也就是密码,来做对比。如果错误进行提示,如果正确则将其加入已登录玩家的列表中。
380 |
381 | 这时我们回头来看之前写的全部Event,如果该玩家登陆成功后,由于已被加入`loginPlayerList`中,此时`isPlayerLogin()`便会返回true,条件不成立,玩家的各种活动便不会被取消。
382 |
383 | > ——发散一下思维,也可以在这里修改玩家发送的内容。加称号等等都是如此实现的。
384 |
385 |
386 | ### 六、当玩家离开
387 |
388 | 当玩家登录,我们把他保存在`loginPlayerList`中。最后,当玩家离开,我们还需要将他移除,不然,如果一名玩家登录后下线,某人冒充顶替的话再上线的话,由于已登录列表中有他,就不需要登录了。
389 |
390 | ```java
391 | @EventHandler
392 | public void onPlayerQuit(PlayerQuitEvent event) {
393 | if (plugin.isPlayerLogin(event.getPlayer())) {
394 | // 如果玩家在线,将其移除,反之不用管
395 | plugin.playerLogout(event.getPlayer());
396 | }
397 | }
398 | ```
399 |
400 | ### 七、效果
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 | ### 八、最后
411 |
412 | 一个简单的登录插件就编写完了。希望大家能够了解一些Event下面的方法,最重要的是去实践。无论是Eclipse还是IDEA,都自带了非常友好的提示功能。还有反编译。可以直接通过方法名,类名来了解Nukkit。
413 |
414 | 当然,这个插件还有一部分功能是残缺的,比如注册。有一些Event没有禁止掉,比如扔物品和捡起物品。这些都等待屏幕前的你来开发。
415 |
416 | [上一节](2-1_主要的事件介绍.md) [下一节](2-3_计时器的介绍.md)
417 |
--------------------------------------------------------------------------------
/第二章/2-3_计时器的介绍.md:
--------------------------------------------------------------------------------
1 | [上一节](2-2_事件相关方法.md) [下一节](2-4_Server类和PluginManager类.md)
2 | # 第二章 第三节 计时器的介绍
3 | 参与编写者:`zzz1999`
4 | #### 建议学习时间:3分钟
5 | ##### 学习要点:了解计时器的功能以及如何使用
6 |
7 | 一.概念
8 | 1.计时器的作用是 __立即,延时,循环或延时循环__ 某些任务,通过计时器我们可以实现例如 *在线奖励* , *倒计时* 等插件功能, 是一些复杂插件中经常用到的功能之一.
9 |
10 | 2.执行计时器代码块的单元叫做任务,我们可以编写任务代码来使用计时器的功能.
11 |
12 | 3.任务执行分为两种类型,一种是同步,一种是异步.同步通过主线程所调度管理执行,异步通过主线程调度管理,异步线程池执行.所以,太多的任务会导致服务器卡顿.
13 |
14 | 4.计时器的功能以`minecraft刻`(tick)作为单位,一刻为现实时间的0.05秒,现实时间的1秒为20刻,服务器每秒刻数由TPS决定,最高为20.
15 |
16 |
17 | 二.代码
18 |
19 | 我们先来看一下nukkit的[Task](https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/scheduler/Task.java)类.
20 |
21 | ```java
22 | package cn.nukkit.scheduler;
23 |
24 | import cn.nukkit.Server;
25 |
26 | /**
27 | * 表达一个任务的类。
A class that describes a task.
28 | *
29 | * 一个任务可以被Nukkit服务器立即,延时,循环或延时循环执行。参见:{@link ServerScheduler}
30 | * A task can be executed by Nukkit server with a/an express, delay, repeat or delay&repeat.
31 | * See:{@link ServerScheduler}
32 | *
33 | * 对于插件开发者,为确保自己任务能够在安全的情况下执行(比如:在插件被禁用时不执行),
34 | * 建议让任务继承{@link PluginTask}类而不是这个类。
35 | * For plugin developers: To make sure your task will only be executed in the case of safety
36 | * (such as: prevent this task from running if its owner plugin is disabled),
37 | * it's suggested to use {@link PluginTask} instead of extend this class.
38 | *
39 | * @author MagicDroidX(code) @ Nukkit Project
40 | * @author 粉鞋大妈(javadoc) @ Nukkit Project
41 | * @since Nukkit 1.0 | Nukkit API 1.0.0
42 | */
43 | public abstract class Task implements Runnable {
44 | private TaskHandler taskHandler = null;
45 |
46 | public final TaskHandler getHandler() {
47 | return this.taskHandler;
48 | }
49 |
50 | public final int getTaskId() {
51 | return this.taskHandler != null ? this.taskHandler.getTaskId() : -1;
52 | }
53 |
54 | public final void setHandler(TaskHandler taskHandler) {
55 | if (this.taskHandler == null || taskHandler == null) {
56 | this.taskHandler = taskHandler;
57 | }
58 | }
59 |
60 | /**
61 | * 这个任务被执行时,会调用的过程。
62 | * What will be called when the task is executed.
63 | *
64 | * @param currentTick 服务器从开始运行到现在所经过的tick数,20ticks = 1秒,1tick = 0.05秒。
65 | * The elapsed tick count from the server is started. 20ticks = 1second, 1tick = 0.05second.
66 | * @since Nukkit 1.0 | Nukkit API 1.0.0
67 | */
68 | public abstract void onRun(int currentTick);
69 |
70 | @Override
71 | public final void run() {
72 | this.onRun(taskHandler.getLastRunTick());
73 | }
74 |
75 | public void onCancel() {
76 |
77 | }
78 |
79 | public void cancel() {
80 | try {
81 | this.getHandler().cancel();
82 | } catch (RuntimeException ex) {
83 | Server.getInstance().getLogger().critical("Exception while invoking onCancel", ex);
84 | }
85 | }
86 |
87 | }
88 | ```
89 |
90 | 我们可以捕捉到几个关键信息,首先我们可以看到`onRun(int)`方法,它是一个抽象方法并且nk开发者很贴心的写上了注释示意我们这是Task中会被运行到代码块.
91 |
92 | 还有`onCancel()`和`cancel()`方法,onCancel()内部是一个空代码块,当Task被取消的时候(被任务调度器取消或者手动调用cancel()取消都会触发),这段代码块会被调用
93 |
94 | 比较常见的用途比如小游戏时间到的时候,一些判定胜负,给予奖励,关闭房间的一些代码可以用Task来实现或者调用.
95 |
96 | 我们还可以使用`cancel()`方法来强制取消一个Task,例如你想要写一个登录插件,每个玩家进入服务器时创建一个Task循环发送提示登录的信息,如果玩家输入了正确的密码并且登录成功才会取消掉这个Task.这时候就可以使用cancel方法了(仅举例,实际生产环境中,每个玩家进入创一个计时器是很费资源的,不会使用这种方法)
97 |
98 | Handler是服务器调度Task所用到的一些东西,可以不懂,不妨碍计时器的使用.
99 |
100 | 开发者可以使用`PluginBase#getServer()`方法获取到Server对象,然后使用`Server#getSchedule()`获取到计时器的实例类.
101 | ```java
102 | public class Main extends PluginBase{
103 | @Override
104 | public void onEnable(){
105 | ServerScheduler scheduler = this.getServer().getScheduler();
106 | }
107 | }
108 | ```
109 | 获取到后,可以创建 __立即,延时,循环或延时循环__ 类型的计时器
110 | 立即(ServerSchedule#scheduleTask)
111 | 延时(ServerSchedule#scheduleDelayedTask)
112 | 循环(ServerSchedule#scheduleRepeatingTask)
113 | 延时循环(ServerSchedule#scheduleDelayedRepeatingTask)
114 |
115 | 计时器是需要一个被触发的时间间隔的,以tick为单位,如果是循环计时器,则是每次循环的时间,如果是延时,则是延迟多少tick才被触发,如果是延迟循环,则会要求填写延迟间隔与循环间隔.
116 |
117 | 举例将会在下一小节讲到
118 |
119 | 3.使用
120 |
121 | 下面会使用到`PluginTask`类,你需要对`PluginTask`类的方法做一个初步的了解,以及知道它继承了Task类.
122 | ```java
123 | package cn.nukkit.scheduler;
124 |
125 |
126 | import cn.nukkit.plugin.Plugin;
127 |
128 | /**
129 | * 插件创建的任务。
Task that created by a plugin.
130 | *
131 | * 对于插件作者,通过继承这个类创建的任务,可以在插件被禁用时不被执行。
132 | * For plugin developers: Tasks that extend this class, won't be executed when the plugin is disabled.
133 | *
134 | * 另外,继承这个类的任务可以通过{@link #getOwner()}来获得这个任务所属的插件。
135 | * Otherwise, tasks that extend this class can use {@link #getOwner()} to get its owner.
136 | *
137 | * 下面是一个插件创建任务的例子:
An example for plugin create a task:
138 | *
139 | * public class ExampleTask extends PluginTask<ExamplePlugin>{
140 | * public ExampleTask(ExamplePlugin plugin){
141 | * super(plugin);
142 | * }
143 | *
144 | * {@code @Override}
145 | * public void onRun(int currentTick){
146 | * getOwner().getLogger().info("Task is executed in tick "+currentTick);
147 | * }
148 | * }
149 | *
150 | *
151 | * 如果要让Nukkit能够延时或循环执行这个任务,请使用{@link ServerScheduler}。
152 | * If you want Nukkit to execute this task with delay or repeat, use {@link ServerScheduler}.
153 | *
154 | * @param 这个任务所属的插件。
The plugin that owns this task.
155 | * @author MagicDroidX(code) @ Nukkit Project
156 | * @author 粉鞋大妈(javadoc) @ Nukkit Project
157 | * @since Nukkit 1.0 | Nukkit API 1.0.0
158 | */
159 | public abstract class PluginTask extends Task {
160 |
161 | protected final T owner;
162 |
163 | /**
164 | * 构造一个插件拥有的任务的方法。
Constructs a plugin-owned task.
165 | *
166 | * @param owner 这个任务的所有者插件。
The plugin object that owns this task.
167 | * @since Nukkit 1.0 | Nukkit API 1.0.0
168 | */
169 | public PluginTask(T owner) {
170 | this.owner = owner;
171 | }
172 |
173 | /**
174 | * 返回这个任务的所有者插件。
175 | * Returns the owner of this task.
176 | *
177 | * @return 这个任务的所有者插件。
The plugin that owns this task.
178 | * @since Nukkit 1.0 | Nukkit API 1.0.0
179 | */
180 | public final T getOwner() {
181 | return this.owner;
182 | }
183 |
184 | }
185 | ```
186 |
187 | 我们可以通过创建一个继承PluginTask的类来创建一个PluginTask(为什么不直接继承Task类?,因为这个类添加了`getOwner()`方法,可以通过构造方法传入一份主类或任意你需要类实例的引用,方便编写代码)
188 |
189 | ```java
190 |
191 | package net.noyark.NukkitLearn;
192 |
193 | import cn.nukkit.scheduler.PluginTask;
194 |
195 | public class NukkitLearnExampleTask extends PluginTask {
196 | public NukkitLearnExampleTask(Main owner) {
197 | super(owner);
198 | }
199 |
200 | @Override
201 | public void onRun(int i) {
202 |
203 | }
204 | }
205 |
206 |
207 | ```
208 |
209 | 上面代码是一个简单Task类的样例,我们现在带入一个插件情境进一步说明计时器的用法.
210 | > 小明是一个服主,他非常喜欢玩家来他的服务器玩并且希望玩家每玩45分钟能够停下来休息一下保护视力.于是他想要开发一个插件,当玩家单次进入服务器并且呆了45分钟后,能够发送一条信息提示(Player#sendMessage(String)玩家休息一会)
211 |
212 | 于是我们来头脑风暴一下,我们知道计时器有4种类型,不知道你还能不能想得起来
213 |
214 |
215 |
216 | ##### 立即(ServerSchedule#scheduleTask)
217 | ##### 延时(ServerSchedule#scheduleDelayedTask)
218 | ##### 循环(ServerSchedule#scheduleRepeatingTask)
219 | ##### 延时循环(ServerSchedule#scheduleDelayedRepeatingTask)
220 |
221 | > 小明火速写好了一个Task类
222 |
223 | ```java
224 | package net.noyark.NukkitLearn;
225 |
226 | import cn.nukkit.Player;
227 | import cn.nukkit.scheduler.PluginTask;
228 |
229 | public class PresentationRestTask extends PluginTask {
230 |
231 | private Player player;
232 |
233 | public PresentationRestTask(Main owner, Player player) {
234 | super(owner);
235 | this.player = player;
236 | }
237 |
238 | @Override
239 | public void onRun(int i) {
240 | player.sendMessage("你已经玩了45分钟了,快下线休息一下眼睛吧!");
241 | }
242 | }
243 |
244 | ```
245 |
246 | > 以及一个主类
247 |
248 | ```java
249 |
250 | package net.noyark.NukkitLearn;
251 |
252 | import cn.nukkit.event.EventHandler;
253 | import cn.nukkit.event.EventPriority;
254 | import cn.nukkit.event.Listener;
255 | import cn.nukkit.event.player.PlayerJoinEvent;
256 | import cn.nukkit.plugin.PluginBase;
257 |
258 | public class Main extends PluginBase implements Listener {
259 |
260 | @EventHandler(priority = EventPriority.LOW,ignoreCancelled = true)
261 | public void onJoin(PlayerJoinEvent event){
262 |
263 | }
264 |
265 | }
266 |
267 | ```
268 |
269 | ### 思考过程
270 | * 使用立刻类型的计时器
271 | - 使用立刻类型的计时器,在主类onJoin方法中填写入代码
272 | > this.getServer().getScheduler().scheduleTask(new PresentationRestTask(this,event.getPlayer()));
273 | > 小明找了个玩家小红进入服务器,小红刚进入服务器,左上角就被发了消息:"你已经玩了45分钟了,快下线休息一下眼睛吧!".
274 | * 使用延迟类型的计时器
275 | - 使用延时类型的计时器,在主类onJoin方法中填写入代码
276 | > this.getServer().getScheduler().scheduleDelayedTask(new PresentationRestTask(this,event.getPlayer()),54000);
277 | > 小明找了个玩家小红进入服务器,小红刚进入服务器45分钟后,左上角才被发了消息:"你已经玩了45分钟了,快下线休息一下眼睛吧!".
278 | * 使用循环类型的计时器
279 | - 似乎这很不符合我们的常理,并且增加了很多不必要的开销
280 | - 修改Task,加入时间作为判断依据
281 | >
282 | ```java
283 | package net.noyark.NukkitLearn;
284 |
285 | import cn.nukkit.Player;
286 | import cn.nukkit.scheduler.PluginTask;
287 |
288 | public class PresentationRestTask extends PluginTask {
289 |
290 | private Player player;
291 | private long stp;
292 |
293 | public PresentationRestTask(Main owner, Player player,long stp) {
294 | super(owner);
295 | this.player = player;
296 | this.stp = stp;
297 | }
298 |
299 | @Override
300 | public void onRun(int i) {
301 | if (System.currentTimeMillis() >= stp) {
302 | player.sendMessage("你已经玩了45分钟了,快下线休息一下眼睛吧!");
303 | this.cancel();
304 | }
305 | }
306 | }
307 |
308 | ```
309 | - 使用循环类型的计时器,在主类onJoin方法中填写入代码
310 | > this.getServer().getScheduler().scheduleRepeatingTask(new PresentationRestTask(this,event.getPlayer(),System.currentTimeMillis() + (45 * 60 * 1000)),60*20);
311 | > 可以达成效果,但是实在是`太麻烦`
312 | - 使用延时循环类型的计时器,在主类onJoin方法中填写入代码
313 | > this.getServer().getScheduler().scheduleDelayedRepeatingTask(new PresentationRestTask(this,event.getPlayer()),45 * 60 * 20,0);
314 | > 同时计时器也需要修改,否则就会45分钟一到就会无尽发送消息
315 | >
316 | ```java
317 | package net.noyark.NukkitLearn;
318 |
319 | import cn.nukkit.Player;
320 | import cn.nukkit.scheduler.PluginTask;
321 |
322 | public class PresentationRestTask extends PluginTask {
323 |
324 | private Player player;
325 |
326 | public PresentationRestTask(Main owner, Player player) {
327 | super(owner);
328 | this.player = player;
329 | }
330 |
331 | @Override
332 | public void onRun(int i) {
333 | player.sendMessage("你已经玩了45分钟了,快下线休息一下眼睛吧!");
334 | this.cancel();
335 | }
336 | }
337 |
338 | ```
339 | > 最终从各种角度来看,小明更应该使用`第二种`方法
340 |
341 |
342 |
343 | [上一节](2-2_事件相关方法.md) [下一节](2-4_Server类和PluginManager类.md)
344 |
--------------------------------------------------------------------------------
/第二章/2-4_Server类和PluginManager类.md:
--------------------------------------------------------------------------------
1 | [上一节](2-3_计时器的介绍.md) [下一节](2-5_各种实体类的方法介绍.md)
2 | # 第二章 第四节 Server类和PluginManager类
3 | 参与编写者: MagicLu550
4 | #### 建议学习时间: 20分钟
5 | ##### 学习要点: 了解Server类和PluginManager类
6 |
7 | 一. Server
8 |
9 | 1.概述
10 |
11 | Server类是插件几乎所有接口的入口,几乎一切的接口都是基于这个类获得的,而在nukkit中,Server类
12 | 是作为一个对象单独存在,Server的实例化意味着nukkit服务器的启动,并且不允许外部调用其构造方法,但可以根据getInstance方法或者插件主类提供的
13 | getServer方法可以获得,这里我们提到了两个获得Server的方法
14 | ```
15 | Server.getInstance();
16 | this.getServer();//this.getClass() == mainClass
17 | ```
18 | Server对象是在Nukkit类里完成初始化的,所以我们不需要担心Server对象是否存在的问题.加载插件
19 | 前,Server对象已经存在.一切启动的初始化操作都在Server的构造方法中完成.对于Server的复杂原理,
20 | 这里不过多赘述,可以参见第四部分的内容
21 |
22 | Nukkit.java Line. 108-115
23 | ```
24 |
25 | try {
26 | if (TITLE) {
27 | System.out.print((char) 0x1b + "]0;Nukkit is starting up..." + (char) 0x07);
28 | }
29 | new Server(PATH, DATA_PATH, PLUGIN_PATH, language);
30 | } catch (Throwable t) {
31 | log.throwing(t);
32 | }
33 | ```
34 | 2. Server类的常用方法
35 | * addOp(String) 可以添加op管理员的名称,name是玩家名称
36 | * addWhitelist(String) 可以添加白名单
37 | * batchPackets(Player[], DataPacket[]) 批量发送数据包,后面[数据包发送篇](2-7_如何发送数据包.md)详细讲解
38 | * broadMessage(String) 发送服务器广播信息,所有玩家可见
39 | * addRecipe(Recipe) 添加配方,这个配方指包括合成、炉子、炼药台等使用的配方。
40 | * broadcastPacket(Player[], DataPacket) 向所有玩家广播数据包
41 | * forceShutdown() 强制关闭服务端
42 | * doAutoSave() 自动保存
43 | * generateLevel(String) 产生一个level(世界),String为名字,返回创建是否成功
44 | * getAllowFlight() 获得这个服务器是否是允许飞行的
45 | * getApiVersion() 获取插件api的版本
46 | * getCommandAliases() 将返回以(一个指令名对应着多个别名)为一对的Map集合
47 | * getCommandMap() 获取指令Map,通过它可以注册一些命令,[前面已经说到过](../第一章/1-4_如何编写命令.md)
48 | * getDataPath() 获取服务端的数据目录
49 | * getDefaultGamemode() 获取服务端的默认模式(如创造模式等)
50 | * getDefaultLevel() 获取默认的世界对象,如World
51 | * getDifficulty() 获得游戏难度,如和平模式等
52 | * getIp() 获得服务端的ip地址
53 | * getIpBans() 获得封禁的信息(ban)
54 | * getLanguage() 获得服务端默认语言(如zh等)
55 | * getLevelByName(String) 通过世界名称获得世界对象
56 | * getMaxPlayers() 获得最大人数
57 | * getMotd() 获得服务端的motd
58 | * getName() 获得服务端的名称
59 | * getNameBans() 获得迸封禁的玩家表
60 | * getOfflinePlayer(String) 通过玩家名称得到不在线玩家
61 | * getOnlinePlayers() 获得在线玩家,UUID都是唯一的标识符
62 | * getPlayerExact(String) 通过名称获得一个确切的玩家
63 | * getPluginManager() 获得插件管理器
64 | * getOps() 获得管理员清单
65 | * getPort() 获得端口
66 | * getPluginPath() 获得插件文件夹的位置
67 | * getScheduler() 获得任务表,可以注册有关多线程之类的东西
68 | * getSubMotd() 获得附属的motd
69 | * hasWhitelist() 是否有白名单
70 | * isOp(String) 判断一个玩家是否是op,String为玩家名称
71 | * reload() 重启服务端
72 | * reloadWhitelist() 重新加载白名单
73 | * removeOnlinePlayer(Player) 删除在线玩家
74 | * removeOp(String) 删除指定管理员
75 | * removeWhitelist(String) 删除一个玩家的白名单
76 | * shutdown() 关闭服务端
77 | * unloadLevel(Level) 卸载一个世界
78 |
79 | 二. PluginManager
80 | 1. 概述
81 | PluginManager是插件管理器,很多的插件加载和数据储存都在这里进行,如监听器
82 | 的注册等
83 | 使用这个类也可以实现动态加载插件等一系列的操作,PluginManager的加载基于JavaPluginLoader
84 |
85 | 2. 常用的方法
86 | * addPermission(Permission) 添加Permission对象
87 | * callEvent(Event) 触发一个事件
88 | * clearPlugins() 清空插件
89 | * disablePlugin(Plugin) 停止一个插件,这个插件会提前调用onDisable
90 | 并卸载
91 | * getPlugin(String) 得到其他插件的插件对象
92 | * loadPlugin(File) 加载一个插件,路径默认为服务端根目录
93 | * loadPlugins(File) 加载一个文件夹的插件
94 | * registerEvents(Listener, Plugin) 注册监听器
95 | * removePermission(String) 删除一个Permission
96 |
97 | [上一节](2-3_计时器的介绍.md) [下一节](2-5_各种实体类的方法介绍.md)
98 |
--------------------------------------------------------------------------------
/第二章/2-5_各种实体类的方法介绍.md:
--------------------------------------------------------------------------------
1 | [上一节](2-4_Server类和PluginManager类.md) [下一节](2-6_各种工具类的介绍.md)
2 | # 第二章 第五节 各种实体类的方法介绍
3 |
4 | 参与编写者: ruo_shui
5 |
6 | 建议学习时间: 20分钟
7 |
8 | ##### 学习要点:
9 |
10 | 了解Entity类
11 |
12 | 1. 概述
13 |
14 | **Entity**类是所有生物的基类,任何生物实体,包括掉落物都继承自**Entity**类。掉落物为一个实体继承 自**EntityItem**类。 若要实现**自定义NPC**则需要继承**EntityHuman**类。
15 |
16 | 2. 自定义生物的实现
17 |
18 | 首先创建一个类继承 Entity / EntityHuman
19 |
20 | ```java
21 | public CustomEntity extends Entity
22 | ```
23 |
24 | 重写方法
25 |
26 | ```java
27 | /**
28 | * 注意在PowerNukkit中 此构造方法必须保留
29 | */
30 | public CustomEntity(FullChunk chunk, CompoundTag nbt) {
31 | super(chunk, nbt);
32 | }
33 |
34 | /**
35 | * 这里填写生物的 ID 值 建议参考WIKI百科内生物的命名空间ID
36 | */
37 | @Override
38 | public int getNetworkId() {
39 | return 0;
40 | }
41 | ```
42 |
43 | [WIKI 百科](https://minecraft.fandom.com/zh/wiki/%E7%94%9F%E7%89%A9#.E7.94.9F.E7.89.A9.E5.88.97.E8.A1.A8)
44 |
45 | 如果我们要实现其他功能则需要设置或重写方法
46 |
47 | **需要注意一点 基于Entity 类创建的实体需要重写 getHeight() 方法与 getWidth() 方法设置生物的碰撞箱不然会出现一些生物名称向下的情况**
48 |
49 | 这里举几个常用设置
50 |
51 | ```java
52 | /**
53 | 这里是构造函数
54 | */
55 | public CustomEntity(FullChunk chunk, CompoundTag nbt) {
56 | super(chunk, nbt);
57 | this.setHealth(20);//设置生物当前血量为20点
58 | this.setMaxHealth(20);//设置生物最大血量为20点
59 | this.setImmobile(true);//当设置为true时 生物不会进行移动
60 | this.setNameTagVisible();//设置显示名称 虽然和下面表达效果相同但是不加这个会不显示
61 | this.setNameTagAlwaysVisible();//设置生物头部名称显示
62 | this.setNameTag("生物头部显示的内容"); //设置头部显示的内容
63 |
64 | }
65 |
66 |
67 | /**碰撞箱体积*/
68 | @Override
69 | public float getHeight() {
70 | return 0.8f;
71 | }
72 |
73 | /**碰撞箱体积*/
74 | @Override
75 | public float getWidth() {
76 | return 1.2f;
77 | }
78 |
79 | /**
80 | 生物AI的实现方法
81 | 需要编写算法实现
82 | 此方法相当于一个定时器,在每个tick循环执行
83 | 编写算法让生物行动
84 | */
85 | @Override
86 | public boolean onUpdate(int currentTick) {
87 | return super.onUpdate(currentTick);
88 | }
89 | ```
90 |
91 | 生成实体
92 |
93 | ```java
94 | //生成实体时我们需要获取 Position 对象 就是实体生成的位置
95 | //获取之后实例化 自定义实体
96 | //传入对象
97 | CustomEntity custom = new CustomEntity(position.getChunk(),Entity.getDefaultNBT(position));
98 | custom.spawnToAll();//生成实体展示给全部玩家
99 |
100 | // 如果只想给单个玩家发送实体 则调用 spawnTo(玩家对象);
101 | custom.spawnTo(Server.getInstance().getPlayer("Steve")/*玩家对象*/);
102 | ```
103 |
104 | 关闭实体
105 |
106 | ```java
107 | //几种常用的方法
108 |
109 | //despawn方法并不会关闭实体,只是不显示给玩家
110 | custom.despawnFromAll(); //和spawnToAll()相反,取消展示实体给全部玩家
111 | custom.despawnFrom(Server.getInstance().getPlayer("Steve")/*玩家对象*/); //和spawnTo(玩家对象)相反,取消展示实体给指定玩家
112 |
113 | //关闭实体 关闭后不能再调用spawnTo等方法
114 | custom.close();
115 |
116 | //杀死实体 和close()方法相比,使用此方法会正常掉落物品(如果有的话)
117 | //如果custom是继承EntityLiving的 还会触发EntityDeathEvent
118 | custom.kill();
119 |
120 | ```
121 |
122 | [上一节](2-4_Server类和PluginManager类.md) [下一节](2-6_各种工具类的介绍.md)
123 |
--------------------------------------------------------------------------------
/第二章/2-6_各种工具类的介绍.md:
--------------------------------------------------------------------------------
1 | [上一节](2-5_各种实体类的方法介绍.md) [下一节](2-7_如何发送数据包.md)
2 |
3 | [上一节](2-5_各种实体类的方法介绍.md) [下一节](2-7_如何发送数据包.md)
--------------------------------------------------------------------------------
/第二章/2-7_如何发送数据包.md:
--------------------------------------------------------------------------------
1 | [上一节](2-6_各种工具类的介绍.md) [下一节]()
2 | # 第二章 第七节 如何发送数据包
3 | 参与编写者: MagicLu550
4 | #### 建议学习时间: 40分钟
5 | ##### 学习要点: 了解数据包和主要的发送形式
6 |
7 | 一. 概述
8 |
9 | Nukkit实现客户端与服务端交互,是通过发送和接收数据包实现的.数据包在nukkit
10 | 的工作过程是占有很重的分量,包括玩家的移动等,都是由一个个数据包接连不断的实现这一
11 | 功能.实现收发数据包的机制是RakNet,通过UDP实现的这些功能.RakNet实现的基础是
12 | Netty框架,如下文可以看到
13 |
14 |
15 | cn/nukkit/raknet/server/UDPServerSocket.java
16 | ```
17 | package cn.nukkit.raknet.server;
18 |
19 | import cn.nukkit.utils.ThreadedLogger;
20 | import io.netty.bootstrap.Bootstrap;
21 | import io.netty.buffer.PooledByteBufAllocator;
22 | import io.netty.buffer.Unpooled;
23 | import io.netty.channel.Channel;
24 | import io.netty.channel.ChannelHandlerContext;
25 | import io.netty.channel.ChannelInboundHandlerAdapter;
26 | import io.netty.channel.ChannelOption;
27 | import io.netty.channel.epoll.Epoll;
28 | import io.netty.channel.epoll.EpollDatagramChannel;
29 | import io.netty.channel.epoll.EpollEventLoopGroup;
30 | import io.netty.channel.nio.NioEventLoopGroup;
31 | import io.netty.channel.socket.DatagramPacket;
32 | import io.netty.channel.socket.nio.NioDatagramChannel;
33 |
34 | import java.io.IOException;
35 | import java.net.InetSocketAddress;
36 | import java.util.concurrent.ConcurrentLinkedQueue;
37 |
38 | /**
39 | * author: MagicDroidX
40 | * Nukkit Project
41 | */
42 | public class UDPServerSocket extends ChannelInboundHandlerAdapter {
43 |
44 | ```
45 |
46 | RakNetServer.java
47 | ```
48 | @Override
49 | public void run() {
50 | this.setName("RakNet Thread #" + Thread.currentThread().getId());
51 | Runtime.getRuntime().addShutdownHook(new ShutdownHandler());
52 | UDPServerSocket socket = new UDPServerSocket(this.getLogger(), port, this.interfaz);
53 | try {
54 | new SessionManager(this, socket);
55 | } catch (Exception e) {
56 | Server.getInstance().getLogger().logException(e);
57 | }
58 | }
59 | ```
60 |
61 | 二. Netty框架
62 |
63 | [Netty框架](https://github.com/netty/netty)是使用最广泛的java-nio框架之一,由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,
64 | 用以快速开发高性能、高可靠1性的网络服务器和客户端程序。Netty相当于简化和流线化了网络应用的编程开发过程.
65 |
66 | 如果你想要更深入研究这个框架可以参见[Netty框架的github](https://github.com/netty/netty)
67 |
68 | 三. Nukkit的发包机制
69 |
70 | 1. Nukkit 数据包的结构
71 |
72 | Nukkit的数据包类的继承结构是
73 | ```
74 | BinaryStream
75 | |------- DataPacket
76 | |-------- 我们主要操作的数据包
77 | ```
78 |
79 | pid() 一般为数据包的NETWORK_ID,在Player,Server,RakNetInterface,DataPacket类中被调用过
80 |
81 | DataPacket的主要方法是decode()和encode(),数据包的传输过程中,通过这两个方法实现解码和
82 | 编码,使得数据包在服务端与客户端间相互识别.
83 |
84 | decode() 是解码方法,一般是客户端发来的数据包,解码到对象的具体属性,之后在服务端中使用这些数据,
85 |
86 | 即 客户端 -> 服务端
87 |
88 | 如代码中这样
89 |
90 | CameraPacket.java
91 | ```
92 | package cn.nukkit.network.protocol;
93 |
94 | import lombok.ToString;
95 |
96 | @ToString
97 | public class CameraPacket extends DataPacket {
98 |
99 | public long cameraUniqueId;
100 | public long playerUniqueId;
101 |
102 | @Override
103 | public byte pid() {
104 | return ProtocolInfo.CAMERA_PACKET;
105 | }
106 |
107 | @Override
108 | public void decode() {
109 | this.cameraUniqueId = this.getVarLong();
110 | this.playerUniqueId = this.getVarLong();
111 | }
112 |
113 | ```
114 |
115 | encode() 是编码方法,会在发包时被调用,将在服务端设置的数据值写入发出到客户端
116 |
117 | 即 服务端 -> 客户端
118 |
119 | 如代码这样
120 |
121 | CameraPacket.java
122 | ```
123 | @Override
124 | public void encode() {
125 | this.reset();
126 | this.putEntityUniqueId(this.cameraUniqueId);
127 | this.putEntityUniqueId(this.playerUniqueId);
128 | }
129 | ```
130 |
131 | 从这里,我们就可以引入接下来的发包环节,事实上,它很简单
132 |
133 | 2. 事件
134 |
135 | 发送数据包和接收数据包的时候会触发几种事件,我们可以通过这几种事件进行
136 | 抓包
137 | 我们比较常用的是这几种
138 | - BatchPacketsEvent: 批处理数据包事件
139 | - DataPacketReceiveEvent: 数据包接收事件
140 | - DataPacketSendEvent: 数据包发送事件
141 | 这里我们主要介绍Receive和Send
142 |
143 | DataPacketSendEvent主要触发在服务端向客户端发送数据包的时候
144 |
145 | Player.java
146 | ```
147 | public int dataPacket(DataPacket packet, boolean needACK) {
148 | if (!this.connected) {
149 | return -1;
150 | }
151 |
152 | try (Timing timing = Timings.getSendDataPacketTiming(packet)) {
153 | //There!!!!!
154 | DataPacketSendEvent ev = new DataPacketSendEvent(this, packet);
155 | this.server.getPluginManager().callEvent(ev);
156 | if (ev.isCancelled()) {
157 | return -1;
158 | }
159 | ```
160 |
161 | DataPacketReceiveEvent主要触发在客户端向服务端发送数据包并且服务端接收到的时候.
162 |
163 | Player.java
164 |
165 | ```
166 | public void handleDataPacket(DataPacket packet) {
167 | if (!connected) {
168 | return;
169 | }
170 |
171 | try (Timing timing = Timings.getReceiveDataPacketTiming(packet)) {
172 | //There!!!!!!!!!
173 | DataPacketReceiveEvent ev = new DataPacketReceiveEvent(this, packet);
174 | this.server.getPluginManager().callEvent(ev);
175 | if (ev.isCancelled()) {
176 | return;
177 | }
178 |
179 | ```
180 | 3. 发包
181 |
182 | Nukkit提供了友好的数据包机制,我们可以通过需求定义,发送数据包
183 |
184 | Nukkit提供了发送数据包的方法,并允许开发者直接发送数据包和监听数据包的收发
185 | 一般的,发送数据包的方式都是使用玩家对象的dataPacket实现
186 | `player.dataPacket(DataPacket)`,这是一个最常用的方式。
187 |
188 | 当然,先前的Server类也提到了批量发包的方法(Server类)
189 |
190 | * batchPackets(Player[], DataPacket[]) 批量发送数据包
191 | * broadcastPacket(Player[], DataPacket) 向所有玩家广播数据包
192 |
193 | 这三个方法就是发包所常使用的方法了。
194 |
195 | 这里我们用dataPacket方法做案例
196 |
197 | 这里用MovePlayerPacket做一个样例
198 | ```java
199 | package net.noyark.www;
200 |
201 | import cn.nukkit.event.EventHandler;
202 | import cn.nukkit.event.Listener;
203 | import cn.nukkit.event.player.PlayerJoinEvent;
204 | import cn.nukkit.network.protocol.MovePlayerPacket;
205 |
206 | public class TestListener implements Listener {
207 |
208 | @EventHandler
209 | public void onPlayer(PlayerJoinEvent e){
210 | //1. 定义数据包对象
211 | MovePlayerPacket packet = new MovePlayerPacket();
212 | //2. 设置数据包数值,这里我随便写了几个值
213 | packet.x = 0;
214 | packet.y = 100;
215 | packet.z = 1000;
216 | //3.发出
217 | e.getPlayer().dataPacket(packet);
218 | }
219 | }
220 | ```
221 |
222 | 四. 常用数据包的解释
223 |
224 | [上一节](2-6_各种工具类的介绍.md) [下一节]()
225 |
--------------------------------------------------------------------------------