简单的SimpleImpl演示

在之前初步完成了VoteRestart这个mod后,我遇到了一个关于国际化的问题。

来看之前发送消息的实现:

com.seraphjack.voterestart.VoteRestart.java部分:

1
2
3
4
5
6
7
8
String info = player.getGameProfile().getName() + StatCollector.translateToLocal("voterestart.info.display0")
+ votes + StatCollector.translateToLocal("voterestart.info.display1")
+ server.getCurrentPlayerCount() + StatCollector.translateToLocal("voterestart.info.display2");
String info2 = StatCollector.translateToLocal("voterestart.info.display3")
+ (int) Math.ceil((double) server.getCurrentPlayerCount() * ConfigLoader.votes) + StatCollector.translateToLocal("voterestart.info.display4");
logger.info(info);
server.getConfigurationManager().sendChatMsg(new ChatComponentTranslation(info));
server.getConfigurationManager().sendChatMsg(new ChatComponentTranslation(info2));

用这种方法有两个很明显的弊端:

1.服务器的I18n(由于是1.7.10版本所以叫做StatCollector)默认语言为en_US,无法切换语言,只能手动替换lang文件。

2.不同的客户端只能接收到同一种消息。

要解决这个问题,实际上很简单,只需要让服务器把投票的数据发给客户端,让客户端来打印信息就解决了。

对于发送数据包,Forge提供了一个简单易懂的封装,叫做SimpleImpl

在com.seraphjack.voterestart下新建包network,用于存放所有用于网络交互的类

MessageAlreadyVoted.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.seraphjack.voterestart.network;

import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
import cpw.mods.fml.common.network.simpleimpl.MessageContext;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.StatCollector;

public class MessageAlreadyVoted implements IMessage {
@Override
public void fromBytes(ByteBuf buf) {

}

@Override
public void toBytes(ByteBuf buf) {

}

public static class MessageHandler implements IMessageHandler<MessageAlreadyVoted, IMessage> {
@Override
public IMessage onMessage(MessageAlreadyVoted message, MessageContext ctx) {
Minecraft.getMinecraft().thePlayer.addChatComponentMessage(new ChatComponentTranslation(StatCollector.translateToLocal("voterestart.alreadyVoted")));
return null;
}
}
}

先来简单看一下这个MessageAlreadyVoted,实现了Forge的IMessage接口,这个接口有两个需要我们实现的方法:

fromBytes和toBytes

用于从ByteBuf中读取我们需要的数据,由于这里并不需要传输任何数据,所以留空。

之后来看这个内部类:MessageHandler,意思是这个数据包对应的处理机制,实现了了Forge的IMessageHandler接口

这个接口有两个泛型参数,第一个泛型参数代表接收的数据包的类型,这里显而易见是MessageAlreadyVoted,第二个泛型参数是用于返回的数据包的类型,由于这里并不需要返回任何数据,所以随便填上IMessage就好。

有一个名为onMessage的方法需要我们实现,显而易见是用来处理数据包的,这个方法的返回值就是返回的数据包。

这个方法的实现十分简单,只需要调用Minecraft.getMinecraft().thePlayer就可以得到一个EntityClientPlayerMP实例,之后给这个实例打印信息就避免了本地化的问题。

再来看第二个数据包

MessageCancelVote.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.seraphjack.voterestart.network;

import cpw.mods.fml.common.network.ByteBufUtils;
import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
import cpw.mods.fml.common.network.simpleimpl.MessageContext;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.StatCollector;

public class MessageCancelVote implements IMessage {
public int votes;
public String player;

@Override
public void fromBytes(ByteBuf buf) {
votes = buf.readInt();
player = ByteBufUtils.readUTF8String(buf);
}

@Override
public void toBytes(ByteBuf buf) {
buf.writeInt(votes);
ByteBufUtils.writeUTF8String(buf, player);
}

public static class MessageHandler implements IMessageHandler<MessageCancelVote, IMessage> {

@Override
public IMessage onMessage(MessageCancelVote message, MessageContext ctx) {
Minecraft.getMinecraft().thePlayer.addChatComponentMessage(new ChatComponentTranslation(StatCollector.translateToLocalFormatted("voterestart.devoteInfo", message.player, message.votes)));
return null;
}
}
}

与上一个数据包不同的是,由于玩家取消投票需要一些信息,所以这个数据包多了几个信息:当前的票数和一个字符串,也就是玩家名称。实现很简单,简单提一下,很显然Java提供的ByteBuf有很多缺陷,比如我们可能想传递一个NBTTag,亦或者是ItemStack甚至String,都没有现成的办法。但是不要着急:Forge给我们提供了ByteBufUtils,可以简单的利用ByteBufUtils实现上述功能。

以此类推,接下来是另外几个数据包

MessageRestartingInfo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.seraphjack.voterestart.network;

import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
import cpw.mods.fml.common.network.simpleimpl.MessageContext;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.StatCollector;

public class MessageRestartInfo implements IMessage {
public int votes;
public int currentPlayers;
public double votesToRestart;

@Override
public void fromBytes(ByteBuf buf) {
votesToRestart = buf.readDouble();
votes = buf.readInt();
currentPlayers = buf.readInt();
}

@Override
public void toBytes(ByteBuf buf) {
buf.writeDouble(votesToRestart);
buf.writeInt(votes);
buf.writeInt(currentPlayers);
}

public static class MessageHandler implements IMessageHandler<MessageRestartInfo, IMessage> {
@Override
public IMessage onMessage(MessageRestartInfo message, MessageContext ctx) {
Minecraft.getMinecraft().thePlayer.addChatComponentMessage(new ChatComponentTranslation(StatCollector.translateToLocalFormatted("voterestart.info.voteInfo", ((double) message.votes / (double) message.currentPlayers * 100), message.votesToRestart * 100)));
return null;
}
}
}

MessageRestarting.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.seraphjack.voterestart.network;

import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
import cpw.mods.fml.common.network.simpleimpl.MessageContext;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.StatCollector;

public class MessageRestarting implements IMessage {
public int seconds;

@Override
public void fromBytes(ByteBuf buf) {
seconds = buf.readInt();
}

@Override
public void toBytes(ByteBuf buf) {
buf.writeInt(seconds);
}

public static class MessageHandler implements IMessageHandler<MessageRestarting, IMessage> {

@Override
public IMessage onMessage(MessageRestarting message, MessageContext ctx) {
Minecraft.getMinecraft().thePlayer.addChatComponentMessage(new ChatComponentTranslation(StatCollector.translateToLocalFormatted("voterestart.stopInfo", message.seconds)));
return null;
}
}
}

接下来这些数据包都实现完了,要怎么注册呢?

在network下新建NetworkLoader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.seraphjack.voterestart.network;

import com.seraphjack.voterestart.VoteRestart;
import cpw.mods.fml.common.network.NetworkRegistry;
import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
import cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper;
import cpw.mods.fml.relauncher.Side;

public class NetworkLoader {
public static SimpleNetworkWrapper instance = NetworkRegistry.INSTANCE.newSimpleChannel(VoteRestart.MODID);

private static int messageId = 0;

public NetworkLoader() {
registerMessage(MessageAlreadyVoted.MessageHandler.class, MessageAlreadyVoted.class, Side.CLIENT);
registerMessage(MessageCancelVote.MessageHandler.class, MessageCancelVote.class, Side.CLIENT);
registerMessage(MessageRestartInfo.MessageHandler.class, MessageRestartInfo.class, Side.CLIENT);
registerMessage(MessageRestarting.MessageHandler.class, MessageRestarting.class, Side.CLIENT);
registerMessage(MessageVoteInfo.MessageHandler.class, MessageVoteInfo.class, Side.CLIENT);

}

private static <REQ extends IMessage, REPLY extends IMessage> void registerMessage(Class<? extends IMessageHandler<REQ, REPLY>> messageHandler, Class<REQ> requestMessageType, Side side) {
instance.registerMessage(messageHandler, requestMessageType, messageId++, side);
}
}

第一件事就是调用

1
NetworkRegistry.INSTANCE.newSimpleChannel();

并传入MODID,生成一个新的频道。之后调用这个频道的实例下的registerMessage方法注册信息,该方法一共有4个参数:第一个class代表用于处理该消息的class,第二个class代表要注册的消息的class,第三个int是这条消息的id,第四个参数代表接收该数据包的是服务端还是客户端。

只需要简单的用一个messageId并在每次注册之后将其加一,就可以简单的注册了。

显而易见,这里所有的数据包都是让客户端接收的。

之后在preInit阶段实例化NetworkLoader,所有数据包就注册完成了。

接下来forge已经托管了所有的数据包,并绑定了对应的Handler,那要怎么发送数据包呢?

还记得NetworkLoader的instance吗?他提供了一些方法,这里我们用得到的是这两个:

sendToAll(IMessage) 用于服务端将数据包发送给所有玩家

sendTo(IMessage,EntityPlayerMP) 用于服务端将数据包发送给指定玩家

之后只要简单修改一下之前投票的几个方法就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package com.seraphjack.voterestart;

import com.seraphjack.voterestart.event.EventLoader;
import com.seraphjack.voterestart.items.ItemLoader;
import com.seraphjack.voterestart.network.*;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import cpw.mods.fml.common.event.FMLServerStartingEvent;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.StatCollector;
import net.minecraftforge.common.util.FakePlayer;
import org.apache.logging.log4j.Logger;

import java.util.LinkedList;
import java.util.List;

@Mod(modid = VoteRestart.MODID, version = VoteRestart.VERSION, name = VoteRestart.NAME)
public class VoteRestart {
public static final String MODID = "voterestart";
public static final String VERSION = "@version@";
public static final String NAME = "VoteRestart";
private static Logger logger;
private static int votes;
private static List<String> voteList;
public static MinecraftServer server;
private static boolean isRestarting = false;

@Mod.EventHandler
public void preInit(FMLPreInitializationEvent e) {
logger = e.getModLog();
votes = 0;
logger.info("Vote Restart loaded");
new ConfigLoader(e);
new ItemLoader();
new NetworkLoader();
}

@Mod.EventHandler
public void serverStarting(FMLServerStartingEvent e) {
voteList = new LinkedList<String>();
server = e.getServer();
}

@Mod.EventHandler
public void init(FMLInitializationEvent e) {
new CraftingLoader();
new EventLoader();
}

public static void vote(EntityPlayer player) {
if (player instanceof FakePlayer) {
player.clearItemInUse();
return;
}

for (String aVoteList : voteList) {
if (player.getGameProfile().getName().equals(aVoteList)) {
NetworkLoader.instance.sendTo(new MessageAlreadyVoted(), (EntityPlayerMP) player);
return;
}
}
MessageVoteInfo msg = new MessageVoteInfo();
msg.player = player.getGameProfile().getName();
NetworkLoader.instance.sendToAll(msg);

voteList.add(player.getGameProfile().getName());
votes++;
update();
}

private static void update() {
MessageRestartInfo msg = new MessageRestartInfo();
msg.votesToRestart = ConfigLoader.votes;
msg.votes = votes;
msg.currentPlayers = server.getCurrentPlayerCount();
NetworkLoader.instance.sendToAll(msg);
if ((double) votes / (double) server.getCurrentPlayerCount() >= ConfigLoader.votes) {
restartServer();
}
}

private static void restartServer() {
isRestarting = true;
new Thread(new Runnable() {
@Override
public void run() {
int i = 10;
for (; i >= 0; i--) {
MessageRestarting msg = new MessageRestarting();
msg.seconds = i;
NetworkLoader.instance.sendToAll(msg);
logger.info(StatCollector.translateToLocalFormatted("voterestart.stopInfo", i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info("Voted player list:");
for (String aVoteList : voteList)
logger.info(aVoteList);
MinecraftServer.getServer().initiateShutdown();
}
}).start();
}

public static void deVote(EntityPlayer player) {
if (isRestarting)
return;
for (int i = 0; i < voteList.size(); i++) {
if (voteList.get(i).equals(player.getGameProfile().getName())) {
votes--;
voteList.remove(i);
MessageCancelVote msg = new MessageCancelVote();
msg.player = player.getGameProfile().getName();
msg.votes = votes;
NetworkLoader.instance.sendToAll(msg);
update();
}
}
}
}

最后一个小细节:因为是将数据包发送给客户端,在客户端打印消息,所以服务器的log中并不会显示相应的信息,所以我在重启服务器的时候简单的打印了投票玩家的列表:

1
for (String aVoteList : voteList)	logger.info(aVoteList);

大功告成!